Repository navigation
fix(combos): keep tool loops on one target and make failover history safe (#7298) - #7354
Conversation
…safe (#7298) A combo re-picked its target on every request, so the request carrying a tool result could land on a different model than the one that issued the call. A GPT turn leaves no reasoning item, and DeepSeek's thinking mode refuses a bare tool continuation with HTTP 400 ("The reasoning_text in the thinking mode must be passed back to the API"). - Pin a tool-output continuation to the lane's previous combo target while it is enabled, still a member, out of cooldown and eligible; skip the JEV decision for it and record reason combo-tool-loop-pin. New user turns re-pick as before. - When a switch is unavoidable, give every bare assistant tool-call turn a reasoning_text " " item on strict plaintext-replay Responses destinations (the Responses twin of the #5421 Chat placeholder). - Apply the Gemini-only thought-signature sentinel on AI Studio, as Antigravity and Vertex already do, for calls another model issued. Closes #7298
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 16
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Deterministic PR hygiene checks passed. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c0d12c25c6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ((item as Record<string, unknown>).type === "reasoning") turnHasReasoning = true; | ||
| if (isClientToolCallItem(item)) turnHasToolCall = true; |
There was a problem hiding this comment.
Reorder reasoning that follows a tool call
When reconstructed, resumed, or synthetic history orders a reasoning item after its function_call, this marks the whole run as having reasoning and suppresses the placeholder, while leaving the call before its required reasoning_text. DeepSeek can therefore still reject the continuation with HTTP 400. This ordering is already documented as a supported input in tests/providers/deepseek-reasoning-replay-gaps.test.ts lines 157-161; track whether usable reasoning precedes the first client tool call, and move/backfill it at the turn head when it does not.
Useful? React with 👍 / 👎.
Summary
Fixes #7298. A combo picks a target per request, so in a tool loop the request that carries the tool result could go to a different model than the one that issued the call. In the reported case, JEV picked
openai/gpt-6.1-solfor the call anddeepseek/deepseek-flashfor the continuation. The Sol turn left no reasoning item, and DeepSeek's thinking mode refused the barefunction_callwith HTTP 400:The reasoning_text in the thinking mode must be passed back to the API. The existing guard (mandatoryResponsesReasoningReplayUnavailable) only covers histories with an opaque-only reasoning item and a known serving-identity change, so it never fired here.This PR does both things the issue asked for, because each covers a case the other cannot:
input(or Chatmessages) ends with a tool output now goes to the target that completed the lane's previous turn (src/server/responses/combo-tool-loop-pin.ts). It reuses the existing per-lane recall incombo-session-recall.ts, with the same TTL, ownership and config-generation rules. The pin applies only while that target passes every current filter: provider enabled, still a member of the combo, out of cooldown, metered consent, payload, replay and protocol-lane eligibility. It uses a non-mutating pick preview, so round-robin rotation state is unchanged. It skips the JEV decision call, and the route decision recordscombo-tool-loop-pin. A new user message is never pinned, and compaction/main hard-lock override routes neither record nor read a lane target.requiresPlaintextReasoningReplay): every assistant tool-call turn with no reasoning item gets a{type:"reasoning", content:[{type:"reasoning_text", text:" "}]}item at its head (backfillBareToolCallTurnReasoninginsrc/adapters/openai-responses/reasoning.ts). This is the Responses twin of the Chat adapter'sreasoning_content: " "placeholder from DeepSeek thinking mode HTTP 400 (reasoning_text must be passed back) when a routed subagent thinking turn lost its reasoning text #5421, the same fixed single space, never invented reasoning. Turns that already carry reasoning and turns without tool calls are untouched.skip_thought_signature_validatorsentinel for an unsigned first call, which Antigravity and Vertex already applied. A real signature from history still wins, and non-Gemini model ids never receive it.tool_use/tool_resultpair with no fabricated thinking block. OpenAI targets are unchanged: the placeholder is gated to strict destinations, and the existing blanking of foreign plaintext still applies.Design decisions (HOTL, recorded here for review):
thread-id/session_id, scoped byx-codex-parent-thread-id). Clients that send none get a normal pick and rely on the failover layer.Verification
src/changes stashed,tests/server/server-combo-tool-loop-continuation.test.tsreproduces the exact [Bug] JEV re-decides on a tool continuation and sends a bare tool call to DeepSeek (reasoning_text 400) #7298 response (Provider error 400: ... The \reasoning_text` in the thinking mode must be passed back to the API.), and 5 of its 7 cases fail. The two passing cases pin pre-existing Chat and Anthropic behavior.google-ai-studio-foreign-call-signature.test.ts's sentinel case fails without thegoogle.ts` hunk. All pass with the change.bun teston the 3 new files plus neighbors (server-combo-reasoning-replay-eligibility,deepseek-reasoning-replay,deepseek-reasoning-replay-gaps,google-signature-history-roundtrip,google-vertex-thought-signature,responses-compaction-routing,server-combo-failover-e2e,server-combo-zero-output-failover,test-layout,file-size-ratchet): 452 pass / 0 fail.bun test tests/adapters/googleplus the OpenCode Go/Zen Gemini wire tests: 889 pass. The 4 failures in a combined run (Antigravity model discovery and roster) pass in isolation with this change, which points to cross-file state, not this diff.bun run typecheck,bun run structure:check,bun run privacy:scan: pass.bun run testwas not run locally. This is a shared home machine and the task asked for light local checks, so the full suite is left to CI.Checklist
docs-sitecombos guide;structure/transports/native-reasoning-retention.md,responses-failover.md,providers/google.md)Closes #7298
Summary by CodeRabbit