Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e9215f2
feat(combo): preserve native Messages replay
vadymhimself Oct 19, 2026
723a8c3
fix(combo): never fall back to a translated child for a native-only tool
vadymhimself Oct 19, 2026
fe15584
fix(combo): register the native combo test in a shard and pin the sha…
vadymhimself Oct 24, 2026
8201ecf
test(combo): pin native Messages caller scope and credential isolation
lidge-jun Oct 28, 2026
37a459f
fix(combo): sanitize native Messages failure diagnostics
lidge-jun Oct 28, 2026
560e414
fix(combo): treat inline system tool declarations as native-only
lidge-jun Oct 28, 2026
22a769a
fix(combo): fence unnamed inline tool definitions as native-only
lidge-jun Oct 28, 2026
24f734c
fix(combo): fence inline withdrawal of a top-level tool as native-only
lidge-jun Oct 28, 2026
ce63aa8
Merge remote-tracking branch 'origin/dev' into merge-7374
lidge-jun Oct 29, 2026
16c3f3d
test: pin native combo frame cap to one upstream chunk
lidge-jun Oct 29, 2026
b68dc80
Merge remote-tracking branch 'origin/dev' into merge-7374
lidge-jun Oct 29, 2026
400ab41
Merge remote-tracking branch 'origin/dev' into merge-7374b
lidge-jun Oct 29, 2026
9009d08
Merge upstream dev into feat/combo-native-messages-replay
vadymhimself Nov 3, 2026
656b10f
fix(combo): keep inspecting native prelude frames after a retryable e…
vadymhimself Nov 3, 2026
4e3aac2
fix(combo): commit a native prelude event block without data
vadymhimself Nov 3, 2026
5cb67c7
fix(combo): record native first output only after a content frame
vadymhimself Nov 3, 2026
b256a7e
fix(combo): release judged native body copies
vadymhimself Nov 3, 2026
67223c8
fix(combo): stop charging an ingress copy for the native Messages source
vadymhimself Nov 3, 2026
6b26e58
fix(combo): keep the envelope release note within the protocol-paths …
vadymhimself Nov 3, 2026
3f32aec
Merge upstream/dev into feat/combo-native-messages-replay
vadymhimself Nov 4, 2026
932df89
Merge remote-tracking branch 'upstream/dev' into feat/combo-native-me…
vadymhimself Nov 6, 2026
3df2b15
fix(combo): address review on native Messages combo replay
vadymhimself Nov 6, 2026
863dc3e
fix(combo): fence restricted search and release a failed native child…
vadymhimself Nov 6, 2026
c21156a
Merge remote-tracking branch 'upstream/dev' into feat/combo-native-me…
vadymhimself Nov 6, 2026
e73d594
Merge remote-tracking branch 'upstream/dev' into feat/combo-native-me…
vadymhimself Nov 7, 2026
800bafe
Merge remote-tracking branch 'upstream/dev' into feat/combo-native-me…
vadymhimself Nov 7, 2026
68152bd
fix(combo): keep committed native Messages failures unreplayable and …
vadymhimself Nov 7, 2026
ee0e35f
fix(combo): classify a native Messages preflight chunk before retaini…
vadymhimself Nov 7, 2026
9ee2b41
fix(combo): keep a native Messages refusal committed when output shar…
vadymhimself Nov 8, 2026
48c3014
Preserve native Messages replay boundaries
lidge-jun Nov 8, 2026
31a6ae0
Preserve raw Messages commitment across rewrite failures
lidge-jun Nov 8, 2026
9433415
Handle mixed-case Messages SSE media types
lidge-jun Nov 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ thinking fallback. Adaptive thinking without an explicit effort stays blank. The
request controls; they do not confirm what effort the upstream applied or change the wire body.

An enabled Anthropic account pool prefers native Messages for eligible direct Anthropic routes
and canonical Anthropic children of strict failover combos without a default effort
when neither native rollout flag explicitly disables that path. In Providers → Anthropic →
Account pooling → **How account selection works**, **Preserve native Claude requests** stores
`anthropicAccountPool.nativeMessages` (default true). Turning it off selects the legacy bridge
Expand Down
18 changes: 12 additions & 6 deletions docs-site/src/content/docs/guides/protocol-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ These switches stage the new paths. Each defaults to off except `antigravityDire
| Switch | When on |
| --- | --- |
| `nativeChatCombos` | An eligible Chat candidate inside a combo is sent natively from its own copy of the client body; the other candidates keep the bridge. |
| `managedMessagesNative` | A Messages request whose route is a direct, key-authenticated Anthropic provider is sent as Messages instead of through the bridge. Routes that need bridge-only behaviour (a pinned effort, blocked-skill elision, the web-search sidecar, vision preprocessing, synthetic rows) stay on the bridge. |
| `managedMessagesNativeOAuth` | Native Messages for the unpooled `anthropic` OAuth provider on `api.anthropic.com`. Effective only together with `managedMessagesNative`; a pooled Anthropic OAuth account set stays on the bridge. |
| `managedMessagesNative` | Send eligible direct Anthropic routes and canonical Anthropic children of strict failover combos as Messages. Combo defaults/effort rewrites and bridge-only behaviour (pinned effort, blocked-skill elision, web-search sidecar, vision preprocessing, synthetic rows) retain the bridge. |
| `managedMessagesNativeOAuth` | Extend native Messages to configured Anthropic OAuth instances on `api.anthropic.com`, including stored account pools and eligible failover children. Effective only together with `managedMessagesNative`. |
| `directEncoders` | For a non-Responses upstream, the answer to a Chat or Messages client is encoded directly from the adapter's events instead of through the internal Responses stream. The request side is unchanged. |
| `antigravityDirect` | **On by default**; set it to `false` to turn it off. A Chat or Messages request whose route is one Google Antigravity provider skips the internal Responses body in both directions: the conversation is decoded from the client body straight into what the Google adapter compiles from, and the answer is encoded directly as with `directEncoders`. The path reads `chat -> ir -> other` or `messages -> ir -> other`. Request settings (model, effort, limits, tool choice) still pass through the usual policies, so account rotation, effort rules and usage are unchanged. Combos, policies, a hosted web-search tool, images for a model marked text-only, a model the shadow-call intercept could rewrite, and text the proxy rewrites (a skills catalog, a generated-image link) stay on the bridge, and the log row names the reason. On a Chat request the decoded conversation counts toward the shared translation budget, so a request whose retained IR exceeds it is refused with HTTP 413 and `translation_buffer_limit` before anything goes upstream. |
| `shadowPlan` | At the end of each Chat or Messages request, the plan a preview would have predicted is compared with the path the request took; a disagreement adds `planMismatch: true` to the log row's path record. No second request is sent. |
Expand Down Expand Up @@ -135,8 +135,14 @@ These paths still use the internal Responses bridge or are not covered:
- Chat and Messages requests are still decoded into a Responses-shaped body before the IR, including
on a translated path. The exception is a Chat or Messages request to a single Google Antigravity route,
unless `antigravityDirect` is set to `false`.
- Routing-policy candidates, combos reached through an effort row, combo candidates while
`nativeChatCombos` is off, and Messages combos.
- Routing-policy candidates, combos reached through an effort row, and Chat combo candidates while
`nativeChatCombos` is off. Messages combos retain the bridge except eligible canonical Anthropic
children of strict failover combos without a default effort. Each eligible child starts from the
source Messages envelope, preserving system/messages/tools/cache breakpoints under the existing
native allowlist and credential rules. Without explicit operator retention, TTL-less combo
breakpoints stay TTL-less. Pre-output retryable errors may fall back; any content/tool block
commits the child and forbids replay. Every child enforces the caller's model/provider scope.
Preserved prefixes do not prove live cache hits.
- Direct encoding (`directEncoders`) covers only a concrete, non-Responses route in streaming
delivery. Combo and policy children, routed compaction, run-turn adapters (Cursor, Devin,
coding-agent CLIs, CodeBuddy), and sidecar turns keep the bridge.
Expand All @@ -153,8 +159,8 @@ These paths still use the internal Responses bridge or are not covered:
their current behavior. Headers alone do not authorize requests or establish official support.
The TypeScript Agent SDK's `sdk-ts` user-agent entrypoint is recognized with the same required
identity headers, preserving its allowlisted native feature betas on first-party Anthropic requests.
- Native Chat over OAuth is not planned. Native Messages over Anthropic OAuth covers only an
unpooled account; a pooled account set stays on the bridge.
- Native Chat over OAuth is not planned. Native Messages over Anthropic OAuth includes pooled
accounts under the same native eligibility and credential-binding rules.
- The managed native Messages path forwards a caller's `anthropic-beta` values only from a short
allowlist and only to `api.anthropic.com`, drops
top-level fields outside its allowlist without a feature effect, and does not apply
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ modes, the preview, and the per-request trace.
| --- | --- | --- | --- |
| `protocols.unrepresentable?` | `"legacy" \| "reject"` | `"legacy"` | `legacy` sends a request whose path drops a feature and records the loss in the trace. `reject` refuses it with HTTP 400 before any send, naming only the feature keys. |
| `protocols.rollout.nativeChatCombos?` | `boolean` | `false` | Send an eligible Chat candidate inside a combo natively from its own copy of the client body. |
| `protocols.rollout.managedMessagesNative?` | `boolean` | `false` | Send Messages natively to a direct, key-authenticated Anthropic provider instead of through the internal Responses bridge. |
| `protocols.rollout.managedMessagesNative?` | `boolean` | `false` | Send Messages natively to an eligible direct Anthropic provider or canonical Anthropic child in a strict failover combo without a default effort. Other combo strategies, policy routes and bridge-only rewrites retain the bridge. |
| `protocols.rollout.managedMessagesNativeOAuth?` | `boolean` | `false` | Native Messages for the `anthropic` OAuth provider on `api.anthropic.com`, including stored account pools. Requires `managedMessagesNative`. For the settled `anthropic` provider, an enabled pool supplies true for absent native flags when `anthropicAccountPool.nativeMessages` is absent or true. Explicit false/malformed flags and a false/malformed pool preference remain off. Other providers and pool-off keep explicit rollout behavior. Eligible pooled requests retain shared selection, affinity and bounded pre-output recovery. Recognized JSON-string `metadata.user_id` account UUIDs are aligned with the serving OAuth credential; device and session fields are preserved. |
| `protocols.rollout.directEncoders?` | `boolean` | `false` | Encode Chat and Messages answers from a non-Responses upstream directly from adapter events. |
| `protocols.rollout.antigravityDirect?` | `boolean` | `true` | Send a Chat or Messages request to a single Google Antigravity route without the internal Responses body in either direction. Request settings still pass through the usual policies, and a request the lane cannot carry uses the bridge. Set `false` to turn it off. |
Expand Down
10 changes: 5 additions & 5 deletions docs-site/src/content/docs/tr/guides/protocol-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ varsayılan olarak kapalıdır; kapalı anahtar değişiklik yapmaz.
| Anahtar | Açıkken |
| --- | --- |
| `nativeChatCombos` | Combo içindeki uygun Chat adayı, istemci gövdesinin kendine ait kopyasından yerel gönderilir; diğer adaylar köprüde kalır. |
| `managedMessagesNative` | Doğrudan, anahtarla doğrulanan Anthropic sağlayıcısına giden Messages isteği köprü yerine Messages olarak gönderilir. Sabitlenmiş çaba, engelli beceri çıkarma, web arama yardımcısı, görsel ön işleme veya sentetik satır gibi yalnızca köprüde çalışan davranışlar köprüde kalır. |
| `managedMessagesNativeOAuth` | `api.anthropic.com` üzerindeki havuzsuz `anthropic` OAuth sağlayıcısı için yerel Messages. Yalnızca `managedMessagesNative` ile birlikte etkilidir; Anthropic OAuth hesap havuzu köprüde kalır. |
| `managedMessagesNative` | Uygun doğrudan Anthropic rotaları ve varsayılan çabası olmayan strict failover combo içindeki birinci taraf Anthropic adayları Messages olarak gönderilir. Çaba yeniden yazımı ve yalnızca köprüde çalışan davranışlar köprüde kalır. |
| `managedMessagesNativeOAuth` | `api.anthropic.com` üzerindeki yapılandırılmış Anthropic OAuth hesapları, hesap havuzları ve uygun failover adayları için yerel Messages. Yalnızca `managedMessagesNative` ile birlikte etkilidir. |
| `directEncoders` | Responses dışı üst sunucuda Chat/Messages yanıtı, iç Responses akışı yerine doğrudan bağdaştırıcı olaylarından kodlanır. İstek tarafı değişmez. |
| `antigravityDirect` | **Varsayılan olarak açık**; kapatmak için `false` yapın. Tek Google Antigravity sağlayıcısına giden Chat/Messages, iç Responses gövdesini iki yönde de atlar: konuşma doğrudan Google bağdaştırıcısının derlediği biçime çözülür, yanıt `directEncoders` gibi kodlanır. Yol `chat -> ir -> other` veya `messages -> ir -> other` olur. Model, çaba, sınırlar ve araç seçimi normal ilkelerden geçtiği için hesap dönüşümü, çaba kuralları ve kullanım değişmez. Combo'lar, ilkeler, barındırılan web araması aracı, yalnızca metin modeline gelen görseller, gölge çağrı yakalamasının değiştirebileceği modeller ve proxy'nin yeniden yazdığı metinler (beceri kataloğu, üretilmiş görsel bağlantısı) köprüde kalır; neden günlükte belirtilir. |
| `shadowPlan` | Her Chat/Messages isteğinin sonunda önizlemenin öngöreceği plan gerçek yolla karşılaştırılır. Uyuşmazsa yol kaydına `planMismatch: true` eklenir. İkinci istek gönderilmez. |
Expand Down Expand Up @@ -136,7 +136,7 @@ Aşağıdakiler iç Responses köprüsünü kullanmaya devam eder veya kapsam d
gövdeye çözülür. Tek Google Antigravity rotası istisnadır; ancak
`antigravityDirect: false` ise o da köprü kullanır.
- Yönlendirme ilkesi adayları, çaba satırından ulaşılan combo'lar,
`nativeChatCombos` kapalıyken combo adayları ve Messages combo'ları.
`nativeChatCombos` kapalıyken Chat combo adayları. Messages combo'larında varsayılan çabası olmayan strict failover içindeki uygun birinci taraf Anthropic adayları yerel yolu kullanır; diğerleri köprüde kalır. Her aday çağıranın model/sağlayıcı kapsamını uygular.
- `directEncoders`, yalnızca somut Responses dışı rotalarda akışlı teslimi kapsar.
Combo/ilke alt istekleri, yönlendirilmiş sıkıştırma, tur çalıştıran bağdaştırıcılar
(Cursor, Devin, kod ajanı CLI'ları, CodeBuddy) ve yardımcı servis turları köprüde kalır.
Expand All @@ -154,8 +154,8 @@ Aşağıdakiler iç Responses köprüsünü kullanmaya devam eder veya kapsam d
Başlıklar tek başına istek yetkisi veya resmi destek sağlamaz. TypeScript Agent
SDK'nın `sdk-ts` user-agent giriş noktası da aynı zorunlu kimlik başlıklarıyla
tanınır; birinci taraf Anthropic isteklerinde izin listeli yerel betalarını korur.
- OAuth üzerinden yerel Chat planlanmıyor. Anthropic OAuth ile yerel Messages
yalnızca havuzsuz hesabı kapsar; hesap havuzu köprüde kalır.
- OAuth üzerinden yerel Chat planlanmıyor. Anthropic OAuth ile yerel Messages,
aynı uygunluk ve kimlik bilgisi kurallarıyla hesap havuzlarını da kapsar.
- Yönetilen yerel Messages, çağıranın `anthropic-beta` değerlerinden yalnızca kısa
izin listesindekileri ve yalnızca `api.anthropic.com` hedefine iletir. İzin
listesi dışı üst düzey alanları özellik etkisi kaydetmeden düşürür ve
Expand Down
4 changes: 3 additions & 1 deletion scripts/test-layout/layout-replay.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"combo-replay-refusal-attribution.test.ts": "responses",
"responses-replay-metadata-principal.test.ts": "responses",
"responses-combo-replay-principal.test.ts": "responses",
"responses-translated-replay-principal.test.ts": "responses"
"responses-translated-replay-principal.test.ts": "responses",
"messages-native-combo.test.ts": "claude-integration",
"messages-native-combo-framing.test.ts": "claude-integration"
}
21 changes: 18 additions & 3 deletions src/claude/inbound-content-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,17 @@ export function systemCachePrefix(system: unknown): string | undefined {
return last >= 0 && last < system.length - 1 ? systemToInstructions(system.slice(0, last + 1)) : undefined;
}

function isRestrictedSearch(raw: unknown): boolean {
return isRec(raw) && typeof raw.type === "string" && raw.type.startsWith("web_search")
&& ["allowed_domains", "blocked_domains", "user_location"].some(key => Object.hasOwn(raw, key));
}

export function toolsToResponses(tools: unknown): Rec[] | undefined {
if (!Array.isArray(tools) || tools.length === 0) return undefined;
// The shared hosted-search lane cannot enforce these constraints on every destination or
// sidecar backend. Omit search rather than broaden it; a duplicate declaration cannot opt out.
// Typed originals still travel separately to an Anthropic destination.
const restrictedSearch = tools.some(raw => isRec(raw) && typeof raw.type === "string"
&& raw.type.startsWith("web_search")
&& ["allowed_domains", "blocked_domains", "user_location"].some(key => Object.hasOwn(raw, key)));
const restrictedSearch = tools.some(isRestrictedSearch);
const out: Rec[] = [];
for (const raw of tools) {
if (!isRec(raw)) continue;
Expand Down Expand Up @@ -106,6 +109,18 @@ export function declaredAnthropicServerTools(raw: unknown): Rec[] | undefined {
return tools.length > 0 ? tools : undefined;
}

/** A declared tool the translated body drops and no out-of-body lane carries (bash_*, text_editor_*, ...). */
export function hasNativeOnlyTool(raw: Rec): boolean {
return Array.isArray(raw.tools)
&& raw.tools.some(tool => !toolsToResponses([tool]) && !declaredAnthropicServerTools({ tools: [tool] }));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
Comment thread
vadymhimself marked this conversation as resolved.
Resolved

/** A declared web_fetch or restricted web_search: only the out-of-body lane to first-party Anthropic carries it. */
export function hasAnthropicOnlyServerTool(raw: Rec): boolean {
return Array.isArray(raw.tools) && raw.tools.some(tool => isRestrictedSearch(tool)
|| (isRec(tool) && typeof tool.type === "string" && tool.type.startsWith("web_fetch_")));
}

/** Validate original call/result order without inventing inputs or pairing a later call backwards. */
function replayableServerRun(run: readonly Rec[]): boolean {
const pending = new Map<string, Rec>();
Expand Down
22 changes: 20 additions & 2 deletions src/protocols/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ export interface ProtocolEnvelope {
readonly inbound: Protocol;
/** The request features, scanned from the source body on the first call and cached. */
features(): ReadonlySet<ProtocolFeature>;
/** The source body itself, uncharged, for checks that only read it; a rewrite takes `freshBody()`. */
readBody(): Readonly<Record<string, unknown>>;
/** A structured clone of the source body, charged to the translator budget. */
freshBody(): Record<string, unknown>;
/** Hand back the charge of a `freshBody()` copy no longer needed; later calls are no-ops. */
releaseBody(copy: Record<string, unknown>): void;
}

export function createProtocolEnvelope(input: {
Expand All @@ -28,16 +32,30 @@ export function createProtocolEnvelope(input: {
}): ProtocolEnvelope {
const { inbound, body, translatorBudget } = input;
let features: ReadonlySet<ProtocolFeature> | undefined;
// The copy's own size may change once its holder rewrites it, so the charge is remembered.
const charged = new WeakMap<object, number>();
return {
inbound,
features() {
features ??= featuresFromBody(inbound, body);
return features;
},
readBody() {
return body;
},
freshBody() {
// Charged before the copy exists, so an over-budget request fails without allocating it.
translatorBudget.chargeRetained(jsonUtf8Bytes(body), { kind: "request_copies" });
return structuredClone(body);
const bytes = jsonUtf8Bytes(body);
translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
const copy = structuredClone(body);
charged.set(copy, bytes);
return copy;
},
releaseBody(copy) {
const bytes = charged.get(copy);
if (bytes === undefined) return;
charged.delete(copy);
translatorBudget.releaseRetained(bytes, { kind: "request_copies" });
},
Comment thread
vadymhimself marked this conversation as resolved.
Resolved
};
}
Loading
Loading