Repository navigation
fix(ai,coding-agent): address post-merge review of Anthropic fallback credit - #15702
Conversation
… credit Follow-up to can1357#13157.
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. |
roboomp
left a comment
There was a problem hiding this comment.
Thanks @H4vC, this is a tidy follow-up to #13157. Ranked P1: one fix, scoped to that feature, and the new tests fail on main and pass here. I ran the three changed packages/ai test files locally: 86 pass. The coding-agent test could not run here because the generated tool-views.generated.js is missing.
- Correct as claimed:
AIError.status(...) === 400replaces the/\b400\b/text match; thefallback-credit-*prefix guard; the typedAnthropicFallbackCreditHandle.paramswith the casts removed; thefallbackCreditRequestBodydedupe; token capture only whenstopDetails.type === "refusal"(refusals are already persist-skipped); and the system-prompt/tools/edit-mode guard inTurnRecovery. - Should-fix: the changelog entries credit #13157 rather than this PR and contributor (inline comment).
- Maintainer call:
storedRefusalPrefixstores the prefix thinking unsigned. That trades a signature rejection the code already recovers from for a possible leading-text 400 in tool loops, which nothing retries (inline comment, unverified live). @can1357 should confirm which failure mode is preferable.
| function storedRefusalPrefix(content: AssistantMessage["content"]): AssistantMessage["content"] { | ||
| const stored: AssistantMessage["content"] = []; | ||
| for (const block of refusalContinuationPrefix(content)) { | ||
| if (block.type === "redactedThinking") continue; | ||
| if (block.type === "thinking") { | ||
| stored.push({ type: "thinking", thinking: block.thinking }); |
There was a problem hiding this comment.
Question for maintainer (behaviour trade-off, non-blocking): storedRefusalPrefix turns a known, recoverable failure into an unknown one.
- Before: the stored turn replayed the refused model's signed thinking. If the server rejected it on a tokenless turn, the error matched
INVALID_THINKING_SIGNATURE_PATTERN(anthropic.ts:1952), and the existing signature retry recovered in one round trip. - After: the prefix thinking is stored with no signature.
convertAnthropicMessages(anthropic.ts:5842-5871) then demotes it to text, or drops it.
If the redemption response continues into a tool_use, the next request (the one carrying the tool_result) replays an assistant turn that starts with demoted text, not with a thinking block. With thinking enabled, Anthropic documents a 400 for that case: "a final assistant message must start with a thinking block". [INFERENCE: not verified live for Opus 4.8 / adaptive thinking.] No retry pattern in this file matches that error, so the PR body's "cost is a prompt-cache miss" may understate the risk in a tool loop. The added test covers only a text continuation (anthropic-stream-envelope.test.ts, the "stores the refused thinking unsigned" case), not one that ends in tool_use.
Your PR body already raises this as an open question to @can1357. A maintainer should pick a side before merge.
| - Fixed Anthropic fallback-credit redemption stepping down or dropping its credit token on errors that only mention `400` in their text, such as an overload message ([#13157](https://github.com/can1357/oh-my-pi/pull/13157)) | ||
| - Fixed Anthropic fallback-credit redemption adding the `fallback-credit-2026-06-01` beta beside a newer `fallback-credit-*` beta the refused request already sent ([#13157](https://github.com/can1357/oh-my-pi/pull/13157)) | ||
| - Fixed turns after an Anthropic fallback-credit redemption replaying the refused model's signed thinking, which the fallback model could reject without the credit token; the echoed thinking is now kept unsigned and refused-model redacted thinking is dropped ([#13157](https://github.com/can1357/oh-my-pi/pull/13157)) | ||
| - Fixed the Anthropic fallback-credit continuation sending unsigned thinking blocks in the echoed refusal prefix, which the API rejects ([#13157](https://github.com/can1357/oh-my-pi/pull/13157)) |
There was a problem hiding this comment.
should-fix (convention): This is an external contribution, so per AGENTS.md § Changelog these entries should be attributed to this PR, ([#15702](https://github.com/can1357/oh-my-pi/pull/15702) by [@H4vC](https://github.com/H4vC)), not #13157. Line 10 also goes into root cause ("which the fallback model could reject without the credit token; the echoed thinking is now kept unsigned..."). Changelog entries should say what the user sees; put the mechanism in the commit or PR. The same attribution fix applies to packages/coding-agent/CHANGELOG.md:22.
…ng; credit can1357#15702 in changelogs
What
folowup to pr #13157
Fixes the post-merge review findings on the Anthropic fallback-credit redemption:
refusalstop captures a credit handle, so the frozen request body andfct_token stay out of session files (refusal turns are never persisted).fallback-credit-2026-06-01when anyfallback-credit-*beta is already present (the duplicated-06-01check was dead code).formatEchoedRefusalContentreturns typedContentBlockParam[]and drops blocks with no wire form (including unsigned thinking) instead of passing internal blocks through.AIError.status(error) === 400instead of any error text containing400.AnthropicFallbackCreditHandle.paramsisMessageCreateParamsStreaming; theas/as unknown ascasts andoptions!.fallbackCreditRedemption!are gone. AfallbackCreditRequestBodyhelper replaces the two hand-built frozen bodies.FALLBACK_CREDIT_TTL_MS, with a link to the docs.TurnRecoveryqueues the redemption only when edit mode, system prompt, and tool list are all unchanged after the fallback switch, not just edit mode. ThefallbackCreditHandle!assertion is removed.Why
Post-merge review of #13157. On the open question about replaying refused-model signatures: an earlier revision of this PR stored that thinking unsigned, but review pointed out that a redemption turn ending in
tool_usewould then replay demoted text ahead of the thinking, which Anthropic rejects with a 400 nothing retries. Keeping the signature leaves only the failure the code already recovers from.Not addressed:
packages/catalog/CHANGELOG.mdhas no entry for #13157's catalog changes, but that release section is already published and immutable.Known remaining gap (pre-existing in #13157): a candidate that can redeem credit skips the same-provider signed-thinking check. If redemption is then cancelled because the switch rebuilt the prompt or tools, that request replays the refused model's signatures without a token, and the existing signature-rejection retry recovers in one extra round trip. The broader guard makes this path more common than the edit-mode-only check did.
Testing
bun run check:tspasses (oxlint, oxfmt, types in all packages). No Rust changed.packages/ai:bun test anthropic, 639 pass.packages/coding-agent:agent-session-retry-fallback.test.ts, 125 pass.mainand pass with this change:400keeps the continuation shape and token instead of stepping down;fallback-credit-2026-07-01beta header is sent without-06-01;bun checkpasses