fix(runtime): 上游错误体统一走 trace.UpstreamError,正文不进 error

M41 的「LLM / embedding / 视觉 上游错误体」一族(M38 #5 #6 #7 #8 #9 #14):
非 2xx 时把响应体截断后拼进 error,而 error 会被 chat run / loop step /
retrieve_fallback 打日志,等于把她的提问、记忆正文、要念的台词写进日志。

- model:openai/anthropic 两处 upstreamError 收口到 upstream.go 的
  providerUpstreamError;anthropic 流内 error 事件的 evt.Error.Message
  (原本无上限)整段丢弃,只保留令牌形状的 type
- embedding:readErrorBody 删除,改走 helper(input 就是她的记忆正文)
- vision:错误路径不再沿用取图的 50 MiB 上限,改为 4 KiB 有界读且不拼原文
- voice:classifyDashscopeStatus 只解 code/request_id,正文只留在内存里做
  分类判据;错误体读取同样改为 4 KiB 有界
- 测试:五个上游各一条「日志/error 里搜不到哨兵正文」用例,且能变异打红
This commit is contained in:
AskaEth committed 2026-10-26 13:44:32 +08:00
1 parent 46a6da13e0
commit 66dda084cd
16 files changed
+570 -183

No files matched your search

+3 -5
View File
@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
@@ -24,8 +23,6 @@ const (
sseBufInit = 64 * 1024
// maxSSELineBytes 限制单条 SSE 事件行的长度。
maxSSELineBytes = 1024 * 1024
// errBodyCap 限制为取出消息而读取的错误响应体大小。
errBodyCap = 512
// defaultHTTPTimeout 在 Options.Timeout 为 0 时约束单次提供商调用。
defaultHTTPTimeout = 180 * time.Second
)
@@ -234,9 +231,10 @@ func (a *OpenAIChatAdapter) doRequest(httpReq *http.Request, stream bool) (*http
return resp, nil
}
// upstreamError 把一次非 2xx 响应交给包内唯一的上游错误收口(见 upstream.go):
// 只留 status / code / type 与体积摘要,响应体正文与 error.message 不出镜。
func (a *OpenAIChatAdapter) upstreamError(resp *http.Response) error {
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, errBodyCap))
return fmt.Errorf("model: %w: status %d: %s", ErrUpstream, resp.StatusCode, strings.TrimSpace(string(snippet)))
return providerUpstreamError(a.name, a.baseURL, resp)
}
// record 上报一次成功调用的用量:先记一条结构化用量日志(含缓存命中率,