feat(api-keys): per-key wire protocol + fix parallel tool_use blocks#611
Merged
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
源自与 cc-switch 的对照调研。两块独立改动 + 一块测试基建:(1) 修一个第三方后端多工具并行场景下会丢工具入参的真 bug(#610);(2) 给第三方 API key 增加上游协议选择(Chat Completions 默认 / 原生 Responses 可选);(3) 把此前不在任何自动化里跑的 web 前端测试接进 CI 门禁。
Changes
fix(translation) — 并行 tool call 块索引碰撞(#610)
codex-to-anthropic流式状态机让多个tool_use块抢同一个 content block index,deltas 混入同一块、第二个工具入参丢失(Claude Code 多工具并行必现)。callId → 块索引映射,functionCallStart分配并自增 index,delta/done 按 callId 路由(src/translation/codex-to-anthropic.ts、tests/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。src/proxy/responses-upstream.ts、src/proxy/adapter-factory.ts、src/auth/api-key-pool.ts、src/routes/api-keys.ts、shared/hooks/use-api-keys.ts、web/src/components/ApiKeyManager.tsx)。ci(quality) — web 测试接入门禁
web/独立 package(vitest jsdom)的组件测试此前不在npm test内。新增test:web脚本 +frontend-testsjob(.github/workflows/ci-quality.yml、package.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
/responses的 E2E 尚未做(没有可用的 Responses-capable 第三方 key)。建议有 key 后补一轮真实连测再大规模启用wire: "responses"。.claude/gitignored,不在本 PR)已同步加了 web 测试步骤;仓库内的门禁由本 PR 的 CI job 提供。Linked Issues
Closes #610