Skip to content

feat(provider): 直连 HTTP 与 WebSocket 音频路径返回 *sdk.APIError - #65

Merged
HoneyBBQ merged 2 commits into
feat/errors-apierrorfrom
feat/errors-direct-http
Sep 30, 2026
Merged

HoneyBBQ merged 2 commits into
feat/errors-apierrorfrom
feat/errors-direct-http

Conversation

@HoneyBBQ

@HoneyBBQ HoneyBBQ commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

问题

语音、转录和 multipart 图片请求直接调用 http.Client,不经过 FetchJSON。遇到非 2xx 响应时,它们返回 fmt.Errorf,错误文本里带着响应体。调用方拿不到 *sdk.APIError,也拿不到 Kind,而响应体可能把终端用户的输入回显进日志。在 200 响应体或 WebSocket 帧里报告的失败同样是纯字符串。

火山引擎 SAMI 的 invoke URL 在 query 里携带 token 和 app key。请求在传输层失败时,*url.Error 会把这个 URL 打印进错误文本。

改动

  • 直接调用 http.Client 的路径遇到非 2xx 响应时返回 utils.NewHTTPError。每个响应按 provider 文档中的错误格式解码,并映射到 ErrorKind。
  • Deepgram 和 ElevenLabs 的格式各有语音和转录两个包在用,解码器放进 internal/errorformat。OpenRouter 的语音和转录改用共享的 OpenRouter 解码器。
  • 200 响应体中的失败(MiniMax 的 base_resp、SAMI 的 status_code、SAMI GetToken)和 DashScope 的 task-failed WebSocket 帧,返回 StatusCode 为 0 的 *sdk.APIError。WebSocket 握手被拒且带 HTTP 响应时,走 NewHTTPError。
  • 新增 SpeechStreamResult.Err(),返回导致流结束的错误。Bytes 也返回这个错误。
  • ElevenLabs 有的接口只返回旧字段 {"detail":{"status":"...","message":"..."}},此时 Code 取 status。RequestID 取 detail.request_id,没有时取 x-trace-id 响应头,两者是同一个值。
  • MiniMax 的错误体常常不带 trace_id。RequestID 先取 trace_id,没有时取 Trace-Id 响应头,两者的值相同。
  • SAMI 传输层失败时,错误里的 invoke URL 不再带 query。
  • 网络、context 和解码错误用 %w 包装,不是 APIError。

验证

  • go build ./...、go vet ./...、go test ./... -short -count=1 -race 和 golangci-lint run ./... 均通过,go mod tidy 无 diff。
  • 每个解码器都有表格测试,用例取自 provider 文档中的错误体,并注明来源 URL。每个包都有 httptest 用例,断言 errors.As、StatusCode、Kind,以及 Error() 不含 key 和 body。
  • 有一个测试在 SAMI invoke 之前关闭服务器,检查错误里没有 token 和 app key。去掉修复后这个测试会失败。
  • 2026-09-29 对阿里云 DashScope CosyVoice(WebSocket)、MiniMax、小米 MiMo、ElevenLabs、Deepgram 和 OpenRouter 发了真实请求。前五家测到的合成、流式合成和转录均成功。
  • 每家都测了 key 无效,Kind 均为 authentication。其中包括 DashScope WebSocket 握手的 401,以及 MiniMax 在 200 响应体里返回的 1004。
  • 不存在的模型或音色均为 unknown。Deepgram 转录是例外,它对不存在的模型返回 403 INSUFFICIENT_PERMISSIONS,Kind 为 permission_denied。
  • ElevenLabs 旧格式的错误体和 MiniMax 只在响应头带 trace ID 的错误体取自这次请求,原文写入测试。
  • 火山引擎 SAMI 没有对线上端点运行。OpenRouter 的语音合成与转录只验证了错误路径。

⚠️ No human QA

@HoneyBBQ
HoneyBBQ added this pull request to stack #67 September 28, 2026 13:17
@HoneyBBQ HoneyBBQ changed the title feat(provider): return *sdk.APIError from direct HTTP and WebSocket audio paths feat(provider): 直连 HTTP 与 WebSocket 音频路径返回 *sdk.APIError Sep 28, 2026
@HoneyBBQ
HoneyBBQ force-pushed the feat/errors-direct-http branch from 1080603 to 66a185b Compare September 28, 2026 19:55
@HoneyBBQ
HoneyBBQ force-pushed the feat/errors-direct-http branch 3 times, most recently from 26ce7d2 to 2022758 Compare September 29, 2026 11:45
@HoneyBBQ
HoneyBBQ force-pushed the feat/errors-direct-http branch 4 times, most recently from 8c82ba6 to a1fa752 Compare September 29, 2026 21:58
@HoneyBBQ
HoneyBBQ force-pushed the feat/errors-direct-http branch from a1fa752 to b424aba Compare September 29, 2026 22:11
…udio paths

Speech, transcription and image multipart requests that use http.Client
directly now return utils.NewHTTPError on a non-2xx response instead of a
formatted string that embedded the response body. Each response is decoded
by the decoder for its provider's documented error format and mapped to an
ErrorKind. OpenAI and MiMo use errorformat.DecodeOpenAI, Google
transcription errorformat.DecodeGoogle, OpenRouter errorformat.DecodeOpenRouter.
The Deepgram and ElevenLabs formats, each received by a speech and a
transcription package, are added to internal/errorformat.

Failures that providers report inside a 200 body (MiniMax base_resp, SAMI
status_code, SAMI GetToken) and WebSocket failure frames (DashScope
task-failed) become an *sdk.APIError with StatusCode 0. WebSocket handshake
rejections with an HTTP response go through NewHTTPError.

SpeechStreamResult gains Err(), which reports the error that ended a
stream; Bytes returns it.

The Volcengine SAMI invoke URL carries the token and app key in its query.
A transport failure no longer prints that URL.

Network, context and decode errors are still wrapped with %w and are not
APIErrors.
…rs they send on errors

ElevenLabs error responses carry x-trace-id, equal to detail.request_id; the request-id header appears only on successful responses. Some ElevenLabs endpoints still send only the legacy detail.status field, which now fills Code. MiniMax error bodies often omit trace_id while the Trace-Id header carries the same value.
@HoneyBBQ
HoneyBBQ force-pushed the feat/errors-direct-http branch from b424aba to d267e0c Compare September 30, 2026 06:53
@HoneyBBQ
HoneyBBQ marked this pull request as ready for review September 30, 2026 06:54
@HoneyBBQ
HoneyBBQ merged commit d0ae284 into main Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant