fix(ai): skip no-op thinking repair resends - #4389
Conversation
The invalid-signature and blocks-immutable 400 arms of the Anthropic thinking-replay repair fired unconditionally, but a repair can only change the request when native thinking/redacted_thinking blocks are actually in flight. Without them the rebuilt replay is byte-identical, so each resend drew the same deterministic rejection and burned the repair budget (ANTHROPIC_MAX_THINKING_REPAIRS) on no-ops before the error finally surfaced — up to two wasted full-size round trips. The masked proxy-rejection arm already carried this guard; it is now the shared gate for every trigger class, and a rejection that provably cannot be repaired surfaces immediately. The forced-tool_choice composition test relied on the removed no-op: prepareParams strips native thinking proactively whenever tool choice is forced, so its 'repair' resend never changed a byte and its signature assertions were vacuous. It now exercises the repair against a fast-mode fallback rebuild, which genuinely coexists with replayed thinking, and asserts the first attempt really carried the signature. Regenerate the issue-3670 eval source binding for anthropic.ts.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f72e4776e
ℹ️ 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".
| "providerSourceBlobOid": "a654fcc1d261547c5fa525763058a57cb3dca230", | ||
| "providerSourceSha256": "c1fa95dcafdbb5e7f4bcc1863c73656bc0184c6614f7b4ac81f382a1bfacf449", | ||
| "providerSourceSha256": "8debae1adf66b013b4ce3b03c396915a4867bf551ac111acd1951827ec461188", |
There was a problem hiding this comment.
Bind the artifact to the changed provider blob
When this commit is checked out, HEAD:packages/ai/src/providers/anthropic.ts has blob OID f32963c12f37671f2158042ff87382cb9b72e502, but the regenerated artifact still records the parent blob a654fcc1d261547c5fa525763058a57cb3dca230. Consequently, anthropic-cache-eval.integration.test.ts builds an artifact using currentSourceIdentity() and unconditionally fails at expect(artifact).toEqual(derivedArtifact) (and later validateSource) even though the SHA-256 was updated; update providerSourceBlobOid alongside it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 61588d3 — regenerated the artifact at the committed HEAD so providerSourceBlobOid now records f32963c12f37671f2158042ff87382cb9b72e502 (matching HEAD:packages/ai/src/providers/anthropic.ts). Both cache-eval integration tests pass against the committed identity.
The artifact was regenerated before the provider change was committed, so providerSourceBlobOid still recorded the parent blob while the SHA-256 was already updated. Regenerated at the committed HEAD so both identity fields match HEAD:packages/ai/src/providers/anthropic.ts.
|
감사합니다, @jason931225. 이 문제는 dev 대상 PR #4388에서 먼저 수정되어 #4389는
— |
Problem
The invalid-signature and blocks-immutable 400 arms of the Anthropic thinking-replay repair (
streamAnthropic) fire unconditionally. But the repair can only change the request when nativethinking/redacted_thinkingblocks are actually in flight — without them,prepareParams()rebuilds a byte-identical replay. Each resend draws the same deterministic 400 and burns the repair budget (ANTHROPIC_MAX_THINKING_REPAIRS = 2) on no-ops before the error finally surfaces: up to two wasted full-size round trips per rejection.The masked proxy-rejection arm already carries exactly this guard ("without signed thinking blocks in flight there is nothing to repair and the error must surface") — the other two classes were missing it.
Change
hasNativeThinkingBlocks(params.messages)is now the shared gate for all three trigger classes. A rejection that provably cannot be repaired surfaces immediately.Test fallout (pre-existing vacuous fixture)
keeps thinking repair active when a later forced-tool_choice fallback rebuilds paramsrelied on the removed no-op:prepareParamsproactively strips native thinking whenever tool choice is forced ("A forced tool choice stripsthinkingfrom the request"), so that fixture's attempt 1 never carriedsig_history, its "repair" resend never changed a byte, and itsnot.toContain("sig_history")assertions were vacuously true. Debug capture of its first request body confirms: only the text block goes to the wire.The test's real intent — repair state survives a later fallback rebuild — is preserved with a fallback that genuinely coexists with replayed thinking: fast mode (
serviceTier: "priority"→speed400 → rebuild). The reshaped test additionally asserts the first attempt really carried the signature, so it can never go vacuous again.Tests
anthropic.ts) viaWRITE_ISSUE_3670_EVAL=1, same as 31cea9dbun test packages/ai/test/: 2316 pass / 0 fail (337 env-gated skips). Biome +tsc --noEmitclean.