Skip to content

feat(api-keys): per-key wire protocol + fix parallel tool_use blocks#611

Merged
icebear0828 merged 6 commits into
devfrom
feat/third-party-wire-protocol
May 30, 2026
Merged

feat(api-keys): per-key wire protocol + fix parallel tool_use blocks#611
icebear0828 merged 6 commits into
devfrom
feat/third-party-wire-protocol

Conversation

@icebear0828
Copy link
Copy Markdown
Owner

Summary

源自与 cc-switch 的对照调研。两块独立改动 + 一块测试基建:(1) 修一个第三方后端多工具并行场景下会丢工具入参的真 bug(#610);(2) 给第三方 API key 增加上游协议选择(Chat Completions 默认 / 原生 Responses 可选);(3) 把此前不在任何自动化里跑的 web 前端测试接进 CI 门禁。

Changes

fix(translation) — 并行 tool call 块索引碰撞(#610)

  • 第三方 OpenAI 兼容后端一次返回 ≥2 个 tool call 时,codex-to-anthropic 流式状态机让多个 tool_use 块抢同一个 content block index,deltas 混入同一块、第二个工具入参丢失(Claude Code 多工具并行必现)。
  • 改为维护 callId → 块索引 映射,functionCallStart 分配并自增 index,delta/done 按 callId 路由(src/translation/codex-to-anthropic.tstests/unit/translation/codex-to-anthropic-parallel-tools.test.ts)。

feat(api-keys) — 上游协议(wire)选择

  • ApiKeyEntry.wire: "chat" | "responses",默认 + 旧数据 + anthropic/gemini 迁移为 chat
  • 新增近透传 ResponsesUpstream(POST /responses + 原生 SSE 透传);adapter-factory 按 wire 分流;不做自动 fallback
  • Dashboard 对 OpenAI-family 显示协议选择器 + 提示多数第三方只支持 chat(src/proxy/responses-upstream.tssrc/proxy/adapter-factory.tssrc/auth/api-key-pool.tssrc/routes/api-keys.tsshared/hooks/use-api-keys.tsweb/src/components/ApiKeyManager.tsx)。

ci(quality) — web 测试接入门禁

  • web/ 独立 package(vitest jsdom)的组件测试此前不在 npm test 内。新增 test:web 脚本 + frontend-tests job(.github/workflows/ci-quality.ymlpackage.json)。

Test Plan

  • npm test(后端全量 2396 passed / 1 skipped)
  • npx tsc --noEmit(干净)
  • npm run test:web(web 7 文件 16 passed)
  • cd web && npm run build(通过)
  • npm run test:real — 未跑(本次不直连真实上游;ResponsesUpstream 已用 mock fetch 单测覆盖请求/解析路径)

Notes

  • ResponsesUpstream 走真实 OpenAI/网关 /responses 的 E2E 尚未做(没有可用的 Responses-capable 第三方 key)。建议有 key 后补一轮真实连测再大规模启用 wire: "responses"
  • 本地 pre-push hook(.claude/ gitignored,不在本 PR)已同步加了 web 测试步骤;仓库内的门禁由本 PR 的 CI job 提供。

Linked Issues

Closes #610

… index

openai-upstream emits every output_item.added during the stream but defers
all function_call_arguments.done to the end, so for >=2 tool calls the event
order is start(A) start(B) done(A) done(B). codex-to-anthropic only tracked a
single contentIndex and did not advance it on functionCallStart, so concurrent
tool_use blocks collided on one content_block index — their input_json_deltas
mixed and the second tool call's arguments were lost.

Track a callId -> block index map: assign and advance contentIndex on
functionCallStart, route deltas/done by callId, and defensively open a block if
a done arrives without a start. Multiple tool_use blocks can now be open at
once. The non-streaming collect path was unaffected.

Closes #610
Third-party OpenAI-family keys (openai/openrouter/custom) always called
/chat/completions. That is correct for DeepSeek/Kimi/GLM and most gateways
(they only speak Chat Completions), but OpenAI and Responses-capable gateways
can serve the native Responses API with higher fidelity and one fewer lossy
conversion.

Add ApiKeyEntry.wire ("chat" | "responses", default chat, legacy + anthropic/
gemini migrate to chat) and a near-passthrough ResponsesUpstream adapter that
POSTs the (internally Responses-shaped) request to /responses and streams the
native SSE back. adapter-factory routes OpenAI-family entries by wire;
anthropic/gemini ignore it. No auto-fallback by design — a first failed probe
doubles latency, provider error semantics differ, and a stream cannot be
switched mid-flight. Dashboard exposes an Upstream protocol selector for
OpenAI-family with a warning that most third parties are chat-only.
web/ is an independent package (not a root workspace) with its own vitest under
jsdom, so its component tests were never picked up by the root `npm test`
config and ran in no automation. Add a `test:web` script and a dedicated
frontend-tests job that installs web deps and runs the web vitest, bringing the
existing web test files into the PR gate.
Address Codex review of #611.

- ResponsesUpstream now builds the upstream /responses body from an explicit
  allowlist of standard Responses generation fields instead of spreading the
  internal request. This stops store/prompt_cache_key/include and any future
  internal field from leaking; in particular `include:
  ["reasoning.encrypted_content"]` (OpenAI-org-bound, rejected by/meaningless to
  third parties) is dropped. store=false is kept so third parties cannot retain
  proxied data. previous_response_id stays dropped and is documented as a known
  limitation (chatgpt.com-scoped id, no affinity on the direct path; matches the
  Chat Completions wire).
- codex-to-anthropic done-without-start fallback now sets hasToolCalls/hasContent
  so the final message_delta reports stop_reason "tool_use" and the empty-
  response guard does not misfire.
Fresh Codex re-review of #611 confirmed the three prior findings resolved and
raised that the allowlist omits temperature/top_p/max_output_tokens/etc. Those
fields are not part of codex-proxy's Codex-shaped CodexResponsesRequest, so they
are already dropped at the translation layer for every backend — the Chat
Completions wire drops them too. Not introduced by this PR; documented as a
known limitation in code and CHANGELOG rather than threading new fields through
the internal model (out of scope).
The new frontend-tests job failed on a clean `npm ci`: @preact/preset-vite
injects `preact/devtools` (and `preact/debug`) imports into shared/ files during
transform, and web/vite.config.ts only aliased preact/preact-hooks/jsx, so those
specifiers could not resolve from outside web/. It passed locally only because
the repo root node_modules happened to contain preact. Add devtools/debug
aliases pointing at web/node_modules; reproduced the failure by hiding root
preact and confirmed all 7 web test files pass with the aliases.
@icebear0828 icebear0828 merged commit 12c7361 into dev May 30, 2026
3 checks passed
@icebear0828 icebear0828 deleted the feat/third-party-wire-protocol branch May 30, 2026 15:31
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.

fix: 第三方后端并行 tool call 转 Anthropic 时 content_block index 碰撞导致参数丢失

1 participant