Commit Graph

857 Commits

Author SHA1 Message Date
Luis Pater
f43aad7637 fix(codex): normalize request session header to Session-Id and preload codex headers
- Replaced legacy `Session_id` usage with canonical `Session-Id` when propagating cache/session state.
- Added ensured request headers for `X-Codex-Window-Id`, `Thread-Id`, `Session-Id`, and `X-Openai-Internal-Codex-Responses-Lite`.
- Removed macOS-only `Session_id` auto-generation fallback path for request header setup.
2026-08-12 18:28:40 +08:00
Luis Pater
133047de66 fix(codex): clear multi-agent-v2 optimization state on namespace conflicts
Closes: #4919
2026-08-12 18:20:13 +08:00
Luis Pater
5b5f428ad9 fix(claude): recover OAuth tool names with duplicated server alias prefixes
- Handle reversed tool names starting with `mcp__<server>__<server>__` by resolving them against the exact alias map before regular matching.
- Prevent failed reverse remapping for malformed/duplicated MCP alias patterns by short-circuiting to the direct alias match path.

Closes: #4916
2026-08-12 17:37:12 +08:00
Luis Pater
a2337beb24 fix(kimi): select upstream request format from source format (Claude/OpenAI)
Closes: #4910
2026-08-12 15:43:19 +08:00
Luis Pater
b08fe3b492 fix(codex): preserve multi-agent-v2 namespace handling across incremental websocket turns
Closes: #4909
2026-08-12 15:04:17 +08:00
Luis Pater
522b4de54a fix(openai): handle premature SSE stream termination with terminal error emission
- Propagate pending terminal errors when image/response data streams close, emitting `error`/`failed` SSE events (or HTTP error responses when no stream started) instead of dropping them.
- Sanitize and normalize streamed terminal errors before writing, and reuse normalized errors for cancellation.
- Improve SSE parsing/frame handling for multiline and cross-chunk payloads, prioritize payload/event-level failures, and avoid mutating emitted frame buffers by cloning chunks before queueing.

Closes: #4904
2026-08-12 13:05:06 +08:00
Luis Pater
a59caebc68 fix(kimi): treat [reasoning unavailable] as unusable reasoning in message normalization
- Introduce a shared `isUsableKimiReasoning` helper and constant for the placeholder string.
- Prevent tool-call normalization from propagating or reusing placeholder/empty reasoning content.
- Preserve and reuse the latest valid assistant reasoning when normalizing `reasoning_content` for tool messages.

Closes: #4899
2026-08-12 01:30:18 +08:00
Luis Pater
db143aebac fix(codex): make input ID sanitization collision-resistant and deterministic
- Track normalized ID state (`occupied`/`preserved`) during preprocessing to avoid remapping valid existing IDs.
- Resolve collisions by appending deterministic hash-based suffixes until a free ID is found.
- Share occupancy tracking across shortening and remapping so sanitized IDs remain stable and idempotent.

Closes: #4891
2026-08-12 01:04:22 +08:00
sususu
189776aab1 fix(claude): validate legacy-model system turns before sending
A caller can put a {"role":"system"} turn inside messages. Models older than
the role=system turn reject it outright, verified against api.anthropic.com on
both /v1/messages and /v1/messages/count_tokens:

    400 role 'system' is not supported on this model

claude-haiku-4-5, claude-sonnet-4-5, claude-sonnet-4-6 and claude-opus-4-6
answer that way, while claude-sonnet-5 and claude-opus-5 accept the turn.
claudeLegacySystemReminderModels already enumerates that boundary, which is why
claudeCodeCLIBetas withholds mid-conversation-system-2026-04-07 for those
models, but caller-provided turns were forwarded unchanged and always spent an
upstream call on a guaranteed rejection.

The native client does not produce the pairing either: it gates the turn on the
model. In 314 captured native requests the turn appears only on
claude-opus-5 and claude-sonnet-5, and on none of the 43 requests addressed to
a model in that set. That is an observation about the captures rather than a
proof about the upstream, so it only corroborates the measured rejection.

Validate the finished body, and only inside the evidence that produced the
rule:

- The check runs after the body is finalized and before
  http.NewRequestWithContext. Payload rules can rewrite model and messages long
  after translation, so an earlier check would not describe what is sent.
- Only Anthropic's first-party origin is covered, matching the reasoning
  shouldUseClaudeUpstreamTokenCount already applies to count_tokens. A
  third-party gateway may map these model IDs onto something that accepts the
  turn and therefore decides for itself.
- A confirmed native caller keeps the passthrough. It gates the turn itself, so
  its body is forwarded untouched and the upstream error reaches it unchanged.
- Unknown and future model IDs stay optimistic and are forwarded, matching how
  checkSystemInstructions treats them.
- rebuild_mid_system_message still folds caller turns into the system slot; the
  final check therefore preserves the explicit escape hatch.

Cloaking adds one extra ordering case: it can place a caller's top-level system
prompt into a role=system turn for a modern model before a payload rule changes
the model to legacy. Track only the exact contiguous turns that CPA inserted,
using both their position and the corresponding message-count increase as
provenance. After payload rules settle the model, replay those turns through the
existing legacy <system-reminder> path. Pre-existing caller turns, even if they
have identical content, remain caller-owned and are still rejected. If payload
rules also rewrite the tracked messages, reconciliation fails closed and final
validation returns 400 rather than guessing provenance.

The 400 is request-scoped like claudeCallerSystemBlockError: the invalid
body/model pairing is independent of first-party credential health, so no
credential is cooled or retried.

Translated requests never carry the pairing, because every non-Claude source
format hoists system content into the top-level system field. Tests pin that for
OpenAI, Gemini, Responses and Interactions, and separately drive Execute,
ExecuteStream and CountTokens through an injected first-party transport.
2026-08-11 22:56:41 +08:00
sususu
8638f28db5 fix(claude): drop auto context_management without eligible thinking
Anthropic rejects the injected clear_thinking_20251015 strategy unless
thinking is enabled or adaptive:

  `clear_thinking_20251015` strategy requires `thinking` to be enabled or
  adaptive

Both sides of the automatic injection only special-cased the literal
{"type":"disabled"}, so an absent thinking field slipped through
gjson's empty string and the request was sent with a strategy the API
refuses. Two reachable paths produced it:

- a cloaked caller that never sent thinking at all;
- a caller whose thinking was enabled at injection time and then removed
  by disableThinkingIfToolChoiceForced, which runs between injection and
  reconciliation.

Both now test the accepted values instead of excluding the disabled one,
via a shared claudeThinkingAcceptsClearThinking helper, so reconciliation
also withdraws an object CPA injected itself once thinking disappears.
Caller-owned and payload-rule-owned objects keep their existing
precedence and are never withdrawn.

Verified against api.anthropic.com: context_management with thinking
absent or disabled returns 400, with thinking enabled returns 200. The
existing expectation that forced tool choice retains the automatic object
pinned the rejected shape and is inverted accordingly.
2026-08-11 19:33:40 +08:00
sususu
a8bbbea2b9 fix(claude): recognize and pass through native Haiku helpers
Claude Code issues measured Haiku helper requests that intentionally omit the
claude-code beta and, for the minimal shape, the system field. Treat those
profiles as confirmed native clients so CPA does not cloak or rewrite them.

Accept the native metadata builder optional parent_session_id, require
platform headers only for presence and software baseline rather than exact
equality, derive timeout and version from the same header defaults the emitter
uses, and keep discriminating signals strict: exact beta allowlist, body
shape, lowercase hex CCH, and session binding. Preserve helper transport
headers, omit synthetic stream false on markerless helpers, and gate the
billing and CCH fallback on system presence so a later pipeline-attached system
prompt cannot go upstream unsigned while the measured no-system wire stays
intact.
2026-08-11 19:33:40 +08:00
sususu
f0034ca663 fix(claude): restore cloaked prompt-cache ownership and native shape
Cloaked Responses/Chat/Gemini→Claude traffic was skipping ensureCacheControl
once the first-user cloak marker existed, freezing multi-turn cache hits.
Always ensure section-independent system and rolling-message breakpoints for
non-native callers, keep confirmed Claude Code placement intact, and leave
tools unstamped when a usable system prompt already covers the prefix.

Align the wire shape with the Claude Code 2.1.220/2.1.221/2.1.227 cache-control
constructor: the default is {"type":"ephemeral"} with no ttl. A 1h body ttl is
applied only after placement, only for OAuth credentials, and only onto blocks
that already carry cache_control, so it stays strictly paired with
extended-cache-ttl-2025-04-11. Explicit caller-owned 5m choices survive.

Match the native rolling selector (skip thinking-like assistant tails; final
system string special case), fall back to a tools breakpoint when system is
absent or blank, and leave empty/whitespace string systems unconverted so they
cannot double-stamp tools plus a useless system host.
2026-08-11 19:33:40 +08:00
sususu
516ec3a000 fix(antigravity): harden per-credential transport pooling
Review and live-test follow-ups to the shared upstream transport.

Bound the cache with an LRU that closes idle connections on eviction, so
rotating a credential's proxy or supplying a per-request base transport can
no longer leak pools.

Stop deriving a pool scope from Auth.Label: it is documented as an optional
human readable label for logging and carries no uniqueness guarantee, so two
OAuth identities sharing a label would share one TCP/TLS pool. Prefer a
refresh-token digest, which stays stable across access-token rotation and is
available to refresh requests that run before any access token exists.

Replace a typed-nil *http.Transport taken from the request context. It passes
the interface nil check, so leaving it in place made http.Client fall back to
http.DefaultTransport, which advertises h2 over ALPN and breaks the
HTTP/1.1-only fingerprint.

Only widen pool limits: treat MaxIdleConns == 0 and IdleConnTimeout == 0 as
unlimited, and leave a negative MaxIdleConnsPerHost alone because that is how
an operator disables pooling.

Size the cache for large deployments. An unused entry costs under 1 KB and no
goroutines, whereas evicting a live pool forces a fresh TCP + TLS handshake,
so capacity is not the lever for bounding memory.
2026-08-11 18:33:50 +08:00
sususu
c33a33e14a perf(antigravity): reuse native upstream connections 2026-08-11 18:33:50 +08:00
sususu
5fa66293db fix(antigravity): preserve request plugin hook semantics 2026-08-11 16:07:42 +08:00
sususu
cf8c27fe90 perf(antigravity): translate each upstream request once
Execute, executeClaudeNonStream and ExecuteStream all assign the validated
original payload to the request and then translate both values. Since both
translations saw the same bytes, every Antigravity request paid for a second
full scan of the client payload. On a captured 24MB tool-history request that
second pass cost roughly 0.9s of CPU and 651MB of allocations.

Translate once when both inputs share a backing array and hand the caller an
independent duplicate, because later stages edit the working copy in place.
Payloads that genuinely differ still get two translations.
2026-08-11 16:07:42 +08:00
sususu
d5f68856f6 perf(antigravity): batch pre-upstream JSON rewrites 2026-08-11 16:07:42 +08:00
sususu
177c7619b6 test(antigravity): clear credits state in place to fix a cleanup data race
resetAntigravityCreditsRetryState assigned a fresh sync.Map to each
package-level credits variable. Credits hint refreshes run on background
goroutines that outlive the request, so they can still be writing those
maps when a test's cleanup runs. Replacing the variable is an
unsynchronized write to the same memory the goroutine is mutating, which
made `go test -race ./internal/runtime/executor` fail with two data races
in TestAntigravityExecute_NoCreditsWithoutConductorFlag.

Empty each map in place with sync.Map.Clear instead. Clear is safe against
concurrent users, and the reset semantics are unchanged.

The races were pre-existing and reproduce identically on b921b5d0; only
test code changes here. Production code never assigned these variables, so
it was never affected.

  before  go test -race ./internal/runtime/executor  FAIL, 2 data races
  after   go test -race ./internal/runtime/executor  ok, 0 data races
2026-08-11 09:57:15 +08:00
sususu
bb7278a1af perf(antigravity): skip replay index rebuilds no item can observe
The replay request index exists solely so that the next item in a
sequential apply loop observes the mutated payload. Both loops rebuilt it
after every successful mutation, including after the final item, whose
rebuild no successor ever reads.

insertAntigravityReasoningReplayItemsWithSchemas is worse than that: its
index parameter is a local pointer, so a rebuild is discarded the moment
the function returns. applyAntigravityReasoningReplayItems only ever
passes a single eligible item, which made every one of those rebuilds
dead work, and the caller then rebuilt the same payload again.

Rebuild only when a successor still has to read the index. Behavior is
unchanged: 800 differential rows over 400 randomized multi-turn payloads
(570 of them mutating) hash-match the previous implementation byte for
byte, under both nil and populated tool schemas.

BenchmarkApplyAntigravityReasoningReplayItems/indexed (1 MiB, 32 turns):
  before  ~302 ms/op  66309 allocs/op
  after   ~269 ms/op  62153 allocs/op

A real 24 MiB production payload (182 contents, 96 ledger items) shows
the effect the synthetic benchmark understates:
  before  6723 ms/op  223381 allocs/op
  after   4035 ms/op  144789 allocs/op
2026-08-11 09:57:15 +08:00
sususu
984836ba37 refactor(antigravity): route replay merge through the request index
The indexing change left two parallel implementations of the same replay
matching logic: the filter path used the request index while the merge and
insert paths still rescanned the payload. Filter and merge must agree on which
part a ledger item targets, so the duplication was a latent source of signatures
being replayed onto the wrong part.

Move the write path onto the index. Every lookup in the merge path already ran
before the first mutation, so one index describes the whole call; insert rebuilds
it after each mutation to keep sequential semantics. Collapse the thought
signature locator into thoughtSignaturePartIndex, shared by the eligibility check
and the write path.

Delete the superseded payload-scanning implementations along with functions that
had no production callers left: antigravityNeedsSignatureReplayForExistingFunctionCall,
antigravityRequestHasMatchingFunctionResponse, antigravityPayloadHasFunctionCallID,
filterAntigravityReasoningReplayItemsForRequest, insertAntigravityReasoningReplayItems,
mergeAntigravityFunctionCallPartReplay and antigravitySetReplayItemContextHash.

Freeze the pre-index implementations into a dedicated oracle test file. The
differential tests previously called the production functions they were meant to
check, so consolidating the logic would have silently turned them into
self-comparisons.

Malformed non-array parts now fail closed consistently. gjson's Result.Array()
yields a one-element slice for a non-array non-null value, so the old fallback
scans could match a functionCall inside a parts object while the primary ID
lookup could not. The end state was already identical because such a payload
cannot be written to; the behavior is pinned by a test.

Also cover the positional fallback for pre-targetHash cache entries, which had
no test at all, and add a write-path benchmark.
2026-08-11 09:57:15 +08:00
sususu
0e4c0dab73 refactor(antigravity): document replay index lifecycle and align empty-contents semantics
Document that the replay request index is request-scoped, retains no-copy GJSON
results aliasing the payload, and is not safe for concurrent use, and that the
context fingerprints snapshot a running hash lazily in content order.

Replace the fingerprint byte counter with a boolean since it only distinguishes
an empty fingerprint from a hashed one.

Guard reasoningReplayItemsFromRequest on validContents so a valid but empty
contents array keeps returning an empty non-nil slice, exactly as it did before
the index was introduced, and lock that invariant with a test.
2026-08-11 09:57:15 +08:00
sususu
9eedbc27bd perf(antigravity): index reasoning replay requests
Cache request contents, function locations, and incremental context fingerprints so large replay histories are scanned once. Rebuild the request index only after mutations and reuse the precomputed response context hash in replay accumulators.
2026-08-11 09:57:15 +08:00
Luis Pater
5d9b629962 fix(runtime): close per-request uTLS HTTP/2 connections with request context
- Create a fresh uTLS/HTTP-2 connection per request instead of reusing cached connections.
- Pass request context through dialing and TLS handshake to make `RoundTrip` cancelable.
- Wrap response bodies so the underlying HTTP/2 connection is closed when the body is closed, and clean up promptly on request/response failures.

Closes: #4878
2026-08-11 00:58:13 +08:00
Luis Pater
ecc9aa72b3 fix(openai): preserve assistant content when converting Responses tool-call turns
- Merge buffered `tool_calls` into the latest mergeable assistant message instead of always appending a new one.
- Combine deferred reasoning segments across the same assistant turn, while ignoring `[reasoning unavailable]` placeholders.
- Reset merge state on role/tool-output boundaries to keep tool-call attachment behavior correct across turns.

Closes: #4676
2026-08-10 05:13:24 +08:00
Luis Pater
93c378b791 fix(claude): recover malformed OAuth MCP aliases when reverse-remapping Claude tool names
Closes: #4867
2026-08-10 04:32:28 +08:00
Luis Pater
a6825fe992 fix(xai): normalize forced web_search tool_choice during Responses request prep
Closes: #4718
2026-08-09 23:15:46 +08:00
Luis Pater
6710a5af30 Merge pull request #4865 from shoucandanghehe/fix/codex-sequential-cutoff-summary
fix(codex): forward sequential cutoff reasoning summaries
2026-08-09 21:52:06 +08:00
Luis Pater
673bac5fc6 fix(codex): normalize custom_tool_call_output IDs with ctco_ prefix during Codex input sanitization 2026-08-09 21:34:01 +08:00
shoucandanghehe
5314b29da9 fix(codex): forward sequential cutoff reasoning summaries 2026-08-09 18:01:28 +08:00
Luis Pater
2e6b1d83f6 fix(claude): add Claude-compatible thinking replay persistence for multi-turn sessions
- Add Claude replay enablement and scope derivation for API-key compatible Claude requests with isolated model/session keys.
- Restore cached signed reasoning blocks into outbound requests and persist completed assistant turns after execution, including stream handling.
- Introduce a dedicated Claude thinking-replay cache with local + home-KV paths, CAS-based snapshot replace/clear, TTL/size limits, and bounded eviction/expiration management.

Closes: #4856
2026-08-09 04:36:39 +08:00
sususu
9992920984 Avoid copying large payloads in Antigravity reads
gjson.GetBytes copies the matched subtree before returning it, so every read of
request.contents on a multi-megabyte request allocated another copy of it. The
reasoning replay, executor, signature validation and session identity paths all
read whole arrays this way, and hasExplicitSession parsed the entire body with
gjson.ParseBytes on every request.

Use the no-copy helpers for these reads. Each call site only reads from the
result and writes through sjson, which allocates a new buffer, so the payload
stays immutable while the results are alive.
2026-08-09 00:40:57 +08:00
Luis Pater
197f520426 fix(codex): normalize custom_tool_call IDs with ctc_ prefix during Codex input sanitization 2026-08-08 23:25:11 +08:00
Luis Pater
01a21b77f4 fix(cliproxy): delegate OpenAI-compatible OAuth refresh to plugin auth providers
- Added a plugin refresh-compat executor wrapper that forwards normal OpenAI-compat execution paths while routing `Refresh` to plugin `AuthProvider`/Home refresh logic.
- Updated refresh lookup to use the effective executor key from auth metadata so namespaced compatibility providers can resolve their refresh executors correctly.
- Changed OpenAI-compat registration to wrap built-in executors with the plugin-refresh wrapper when a matching plugin auth provider exists, while preserving bare executors otherwise.
- Made `OpenAICompatExecutor.Refresh` fail fast for OAuth-style credentials (with refresh tokens) instead of silently returning unchanged auth.

Closes: #4719
2026-08-08 06:08:23 +08:00
Luis Pater
36936340a3 fix(kimi): canonicalize K2.7 Code model aliases to official Kimi-For-Coding IDs
Closes: #4605
2026-08-08 05:08:42 +08:00
Luis Pater
4b3cc55cdc fix(cliproxy): centralize client error status mapping and apply context cancellation/deadline HTTP codes
Closes: #4601
2026-08-08 04:53:34 +08:00
jizhenggang
c30e60a11b Reduce Codex request amplification for large payloads
Reuse immutable request bytes during input inspection and avoid a redundant websocket clone while preserving the required outbound envelope.

Constraint: Preserve request immutability and one final WebSocket request-body allocation
Rejected: In-place JSON mutation | request buffers are shared across translation and execution stages
Confidence: high
Scope-risk: narrow
Directive: Do not retain no-copy gjson results or mutate their backing payload while results are in use
Tested: GOTOOLCHAIN=local go test ./...; targeted 8 MiB allocation benchmarks
Not-tested: Live Codex upstream network traffic
2026-08-07 19:16:06 +08:00
Luis Pater
dd67f56f26 fix(xai): remap bad-credentials 403s to unauthorized
- normalize xAI `403` bad-credentials responses to `401` before conductor retry handling
- preserve websocket error headers while applying the normalized status/retry hints
- share bad-credentials detection across flat and nested payload shapes for HTTP/websocket paths

Closes: #4046
2026-08-07 06:48:09 +08:00
Luis Pater
0a95fa62a1 feat(compat): preserve Claude thinking/tool-call content for is-compat OpenAI compatibility models
- Add `is-compat` support to OpenAI compatibility model config, capabilities, hashing, and example config.
- Propagate `IsCompat` through API-key model resolution and switch OpenAI-compat executor translation to compatibility-aware routing.
- Keep Claude assistant thinking content in compatibility mode while keeping default behavior unchanged when `is-compat` is disabled.

Closes: #4776
2026-08-07 06:26:33 +08:00
Luis Pater
5e25566c24 fix(codex): normalize reasoning and function_call item IDs during input sanitization 2026-08-07 05:52:20 +08:00
sususu
fe28d582f4 fix(openai): expose only client-fault streaming errors
Forward an upstream failure to Responses websocket and SSE clients only when the request itself is at fault. Credential, quota and transport failures now close the stream silently so the client reconnects and retries; a fresh websocket carries no server-side transcript, so reconnecting already implies a full context resend and needs no extra close-code signal.

Classify the failure from the upstream error body instead of the attached status. Codex reports the same cyber_policy rejection as 400 on the stream error path and as 502 through the websocket disconnect channel, so a status-only whitelist hid most of them. Treat cyber_policy as a request error so it stops credential failover without suspending the credential, and treat 413 as request-scoped because a payload that exceeds the upstream frame limit fails identically on every credential and would otherwise burn the whole pool.

Report an unroutable model as 400 invalid_request_error instead of 502 so streaming clients receive an actionable message instead of retrying forever. Keep the upstream reason in the request-log websocket timeline when the client only observes a closed connection, and stop logging expected connection-teardown races as warnings.
2026-08-06 20:49:23 +08:00
Luis Pater
dcee14dd3c feat(compat): preserve compat-mode thinking/signature blocks for API-key models
- Added `is-compat` model metadata plumbing from config through executor and helpers, including hash computation.
- Introduced a compatibility-aware translation path (`TranslateRequestWithAPIKeyModelCompatibility`) and wired it into Claude/Gemini/Codex/Interactions request flows.
- Updated Claude message sanitization/translation behavior to keep empty-thinking compatibility blocks (including signatures) when `is-compat` is enabled, while keeping default behavior unchanged.
2026-08-06 17:19:24 +08:00
Luis Pater
e5ea945ed9 feat(codex): add model-level is-compat flag to rewrite MultiAgentV2 agent_message for Responses-compatible endpoints
Closes: #4801
2026-08-06 04:49:28 +08:00
DefinitelyNotSpammy
533b69e3e0 fix(claude): skip context management when thinking is disabled 2026-08-05 18:25:40 +08:00
Luis Pater
e400d7191d feat(xai): bump client version to 0.2.120 and include Grok Shell auth headers on XAI requests 2026-08-05 07:28:55 +08:00
Luis Pater
690f93dc14 fix(openai): drop OpenAI stream chunks after [DONE]
- Track terminal `[DONE]` state in translator and ignore all subsequent SSE payloads
- Stop OpenAI-compatible stream processing once `[DONE]` is received, only emit synthetic done when missing
- Add tests covering translator and executor behavior to prevent post-DONE trailing chunks from being forwarded

Closes: #4783
2026-08-05 05:13:09 +08:00
Luis Pater
42eef103d6 feat(antigravity): obfuscate sensitive words in system instructions
Closes: #4696 #4723 #4732
2026-08-05 00:27:32 +08:00
Luis Pater
9b8d97441e fix(responses): preserve original request model on response.created/response.in_progress payloads 2026-08-04 18:37:01 +08:00
sususu
8cf1d46f06 fix(usage): account for Claude thinking tokens 2026-08-03 22:30:00 +08:00
Supra4E8C
a88197f845 Merge pull request #4698 from router-for-me/fix/home-401-refresh-recovery
fix(auth): retry Home OAuth requests once after upstream 401
2026-08-03 22:16:00 +08:00
sususu
9b1142399c fix(claude): rebuild the Responses reasoning chain
A Responses caller that asked for reasoning summaries got an empty chain
of thought back: every reasoning item carried a signature and no text, so
replaying it produced a Claude thinking block with an empty thinking
field. The cause was the beta list, not the translator. Cloaked requests
always sent redact-thinking-2026-02-12, which makes Anthropic withhold
the summary text even when thinking.display is summarized.

Native Claude Code 2.1.220 treats the two as mutually exclusive: the beta
is only appended while thinking summaries are off, and the request
builder removes it again whenever a display value is attached. An
isolated 2.1.220 profile with showThinkingSummaries enabled confirms it
on the wire, still on cc_entrypoint=cli, sending display=summarized
without the beta and receiving thinking text. A direct A/B against
claude-opus-4-8 pins the effect down: with the beta the thinking text is
empty for every display value, without it and with display=summarized the
text comes back. The beta is now dropped whenever the request carries
thinking.display, which is exactly the native rule.

Two translation gaps kept the chain lossy on the way back. redacted_thinking
blocks had no Responses representation at all and vanished, even though
Anthropic requires them to be replayed verbatim; they now ride in
encrypted_content behind a marker prefix and are restored as
redacted_thinking blocks. Reasoning text was only read from summary[],
so a caller whose SDK models the text in content[] lost it; content[] is
now used as a fallback, and only as a fallback so a client that mirrors
both arrays does not replay the text twice.

An item whose encrypted_content is missing or belongs to another provider
is still dropped rather than replayed, because Anthropic rejects a
thinking block without a signature and there is nothing to synthesize.
2026-08-03 21:46:27 +08:00