Skip to content

fix(combos): keep tool loops on one target and make failover history safe (#7298) - #7354

Merged
lidge-jun merged 3 commits into
devfrom
fix/combo-continuation-safety
Oct 19, 2026
Merged

lidge-jun merged 3 commits into
devfrom
fix/combo-continuation-safety

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

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-sol for the call and deepseek/deepseek-flash for the continuation. The Sol turn left no reasoning item, and DeepSeek's thinking mode refused the bare function_call with 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:

  • Pin the tool loop (preferred path). A request whose input (or Chat messages) 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 in combo-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 records combo-tool-loop-pin. A new user message is never pinned, and compaction/main hard-lock override routes neither record nor read a lane target.
  • Make an unavoidable switch safe (failover path). If the pinned target is in cooldown, disabled or fails, normal selection and failover run, and the new target has to accept history it did not write:
    • DeepSeek / strict plaintext Responses (requiresPlaintextReasoningReplay): every assistant tool-call turn with no reasoning item gets a {type:"reasoning", content:[{type:"reasoning_text", text:" "}]} item at its head (backfillBareToolCallTurnReasoning in src/adapters/openai-responses/reasoning.ts). This is the Responses twin of the Chat adapter's reasoning_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.
    • DeepSeek on the Chat wire already received the DeepSeek thinking mode HTTP 400 (reasoning_text must be passed back) when a routed subagent thinking turn lost its reasoning text #5421 placeholder. A combo-level regression now covers it.
    • Gemini on AI Studio now gets the Gemini-only skip_thought_signature_validator sentinel 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.
    • Anthropic receives the foreign call as an intact tool_use/tool_result pair 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):

Verification

  • Red-first: with the src/ changes stashed, tests/server/server-combo-tool-loop-continuation.test.ts reproduces 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 the google.ts` hunk. All pass with the change.
  • bun test on 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/google plus 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.
  • Full bun run test was 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

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed. (docs-site combos guide; structure/transports/native-reasoning-retention.md, responses-failover.md, providers/google.md)
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. (No credential, auth or logging changes; the pin reads only the existing process-local lane recall.)

Closes #7298

Summary by CodeRabbit

  • New Features
    • Combo tool-call continuations stay with the target that handled the preceding call when it remains eligible. Otherwise, normal target selection and failover apply. New user messages use normal selection.
    • Improved compatibility when replaying tool-call history across Google, DeepSeek, and Anthropic providers.
  • Documentation
    • Added guidance on target selection during tool-call continuations and provider-specific handling of tool-call history.

…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
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 19, 2026 06:30
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T06:36:00.577085Z c0d12c2 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: cd0fc8e7-ac36-4219-aa0a-ddee9d77a7d2
📥 Commits

Reviewing files that changed from the base of the PR and between 0a995db and 4389ef4.

📒 Files selected for processing: 16
  • docs-site/src/content/docs/guides/combos.md
  • scripts/test-layout/layout.json
  • src/adapters/google.ts
  • src/adapters/openai-responses/reasoning.ts
  • src/server/responses/combo-session-recall.ts
  • src/server/responses/combo-tool-loop-pin.ts
  • src/server/responses/core-combo.ts
  • structure/providers/google.md
  • structure/transports/native-reasoning-retention.md
  • structure/transports/responses-failover.md
  • tests/adapters/google/google-ai-studio-foreign-call-signature.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/providers/deepseek-bare-tool-call-replay.test.ts
  • tests/providers/deepseek-inbound-wire.test.ts
  • tests/responses/responses-stateless-dangling-call-repair.test.ts
  • tests/server/server-combo-tool-loop-continuation.test.ts
+----------------------------------------------------------------------------------------------------------------------+
| Adapting old programs to fit new machines usually means adapting new machines to behave like old ones. - Alan Perlis |
+----------------------------------------------------------------------------------------------------------------------+
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Oct 19, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +221 to +222
if ((item as Record<string, unknown>).type === "reasoning") turnHasReasoning = true;
if (isClientToolCallItem(item)) turnHasToolCall = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant