diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 9fc956cdd86..ce56507a0c4 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -175,7 +175,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 +and eligible key-auth Anthropic-compatible or first-party 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 diff --git a/docs-site/src/content/docs/guides/protocol-paths.md b/docs-site/src/content/docs/guides/protocol-paths.md index 8b28bc10d83..ffe4788dbb8 100644 --- a/docs-site/src/content/docs/guides/protocol-paths.md +++ b/docs-site/src/content/docs/guides/protocol-paths.md @@ -99,7 +99,7 @@ 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` | 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. | +| `managedMessagesNative` | Send eligible direct Anthropic routes and key-auth Anthropic-compatible or first-party 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. | @@ -136,7 +136,7 @@ These paths still use the internal Responses bridge or are not covered: 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, and Chat combo candidates while - `nativeChatCombos` is off. Messages combos retain the bridge except eligible canonical Anthropic + `nativeChatCombos` is off. Messages combos retain the bridge except eligible Anthropic key-auth compatible or first-party 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 diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index c8c423c49ed..e3ea042a0c8 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -719,7 +719,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 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.managedMessagesNative?` | `boolean` | `false` | Send Messages natively to an eligible direct Anthropic provider or a key-auth Anthropic-compatible or first-party 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. | diff --git a/docs-site/src/content/docs/tr/guides/protocol-paths.md b/docs-site/src/content/docs/tr/guides/protocol-paths.md index e05fd070f04..2e4386b3b90 100644 --- a/docs-site/src/content/docs/tr/guides/protocol-paths.md +++ b/docs-site/src/content/docs/tr/guides/protocol-paths.md @@ -99,7 +99,7 @@ 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` | 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. | +| `managedMessagesNative` | Uygun doğrudan Anthropic rotaları ve varsayılan çabası olmayan strict failover combo içindeki anahtar kimlik doğrulamalı Anthropic uyumlu veya 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. | @@ -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 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. + `nativeChatCombos` kapalıyken Chat combo adayları. Messages combo'larında varsayılan çabası olmayan strict failover içindeki uygun anahtar kimlik doğrulamalı Anthropic uyumlu veya 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. diff --git a/src/server/responses/core-combo-native.ts b/src/server/responses/core-combo-native.ts index 8061dd4d2a6..2fc0268d82f 100644 --- a/src/server/responses/core-combo-native.ts +++ b/src/server/responses/core-combo-native.ts @@ -197,8 +197,10 @@ export function createComboProtocolLanes(input: { if (inbound === "messages") { const combo = getCombo(config, comboId); // ponytail: exact replay supports no combo effort rewrite; the existing bridge owns those policies. + const domain = credentialDomainFor(route.provider); if (!combo || combo.strategy !== "failover" || comboDefaultEffort(config, comboId) - || combo.reasoningEffortMode !== "strict" || !credentialDomainFor(route.provider)?.firstPartyAnthropic + || combo.reasoningEffortMode !== "strict" || !domain + || (route.provider.authMode === "oauth" && !domain.firstPartyAnthropic) || supportedLadderFor({ provider: route.provider, modelId: route.modelId })?.length === 0) return false; } // The concrete selector, as the bridge child's body carries it; the wire model comes from diff --git a/structure/data-planes/protocol-paths.md b/structure/data-planes/protocol-paths.md index c93f3b41046..dd6af0298b4 100644 --- a/structure/data-planes/protocol-paths.md +++ b/structure/data-planes/protocol-paths.md @@ -376,7 +376,7 @@ probe uses the bridge's credential-free affinity headers, existing plan and auth short-lived selection lease; uncertain admission keeps the bridge. Without a runnable plan search alone does not decline native Messages. `count_tokens` and the planner lack runtime facts and remain conservative. -Failover combos reuse this lane for eligible concrete first-party Anthropic children through the existing `core-combo-native.ts` source/dispatcher seam. Only strict failover without a default effort qualifies; policy/JEV/other strategies and bridge-only rewrites retain the bridge. The shared executor inherits the combo's attempt, target budget, prepaid permit and gated callbacks, opening no second tracker/final row. Key source system/messages/tools/cache markers retain the native allowlist, image/tool-id safety; explicit operator TTL wins, otherwise TTL-less combo breakpoints stay unchanged. OAuth keeps its preamble/metadata/tool transformations. Counts/previews still decline logical combos without picking accounts. A source declaring a tool the translated body drops with no out-of-body lane (`hasNativeOnlyTool` in `src/claude/inbound-content-options.ts`: `bash_*`, `text_editor_*`, ...; unrestricted `web_search_*` stays portable, while `web_fetch_*` and a domain- or location-restricted `web_search_*` stay portable only to a first-party Anthropic bridge child) makes every translated child unpickable under any `unrepresentable` policy, so a refused native child surfaces its own failure instead of continuing without the tool; non-combo translated routes are unchanged. [Commit boundaries](../transports/responses-combo-boundaries.md) own failover and preserve upstream read evidence before OAuth framing; `tests/claude-integration/messages-native-combo.test.ts` covers the boundary and mocked prefix preservation, not live cache-hit metrics. +Failover combos reuse this lane for eligible concrete Anthropic children, including key-auth compatible endpoints, through the existing `core-combo-native.ts` source/dispatcher seam. OAuth remains first-party only. Only strict failover without a default effort qualifies; policy/JEV/other strategies and bridge-only rewrites retain the bridge. The shared executor inherits the combo's attempt, target budget, prepaid permit and gated callbacks, opening no second tracker/final row. Key source system/messages/tools/cache markers retain the native allowlist, image/tool-id safety; explicit operator TTL wins, otherwise TTL-less combo breakpoints stay unchanged. OAuth keeps its preamble/metadata/tool transformations. Counts/previews still decline logical combos without picking accounts. A source declaring a tool the translated body drops with no out-of-body lane (`hasNativeOnlyTool` in `src/claude/inbound-content-options.ts`: `bash_*`, `text_editor_*`, ...; unrestricted `web_search_*` stays portable, while `web_fetch_*` and a domain- or location-restricted `web_search_*` stay portable only to a first-party Anthropic bridge child) makes every translated child unpickable under any `unrepresentable` policy, so a refused native child surfaces its own failure instead of continuing without the tool; non-combo translated routes are unchanged. [Commit boundaries](../transports/responses-combo-boundaries.md) own failover and preserve upstream read evidence before OAuth framing; `tests/claude-integration/messages-native-combo.test.ts` covers the boundary and mocked prefix preservation, not live cache-hit metrics. With the switch on, a declined route re-marks its bridge entry with the decline reason (after any `effort-row` / `fast-row`), so the trace says why the bridge was taken; with it off nothing is diff --git a/tests/claude-integration/messages-native-combo.test.ts b/tests/claude-integration/messages-native-combo.test.ts index b5dddcc870d..824d6aa192f 100644 --- a/tests/claude-integration/messages-native-combo.test.ts +++ b/tests/claude-integration/messages-native-combo.test.ts @@ -67,7 +67,7 @@ beforeEach(() => { if (new URL(request.url).hostname === "127.0.0.1") return physicalFetch(request); const body = await request.json() as Rec; seen.push({ url: request.url, body, headers: request.headers }); - const native = new URL(request.url).hostname === "api.anthropic.com"; + const native = ["api.anthropic.com", "compatible.fixture.invalid"].includes(new URL(request.url).hostname); if (native) return answer(body, true); return Response.json({ id: "resp_backup", object: "response", status: "completed", model: "fallback", output: [{ id: "msg_backup", type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "fallback reply", annotations: [] }] }], @@ -90,10 +90,10 @@ function config(on = true): OcxConfig { }, combos: { waterfall: { strategy: "failover", targets: [{ provider: "anth", model: "claude-sonnet-4-6" }, { provider: "backup", model: "fallback" }] } }, protocols: { rollout: { managedMessagesNative: on } } }; } -async function send(options: { stream?: boolean; config?: OcxConfig; messages?: unknown; tools?: unknown; admission?: DataPlaneAdmission; extra?: Rec } = {}) { +async function send(options: { stream?: boolean; config?: OcxConfig; messages?: unknown; tools?: unknown; admission?: DataPlaneAdmission; extra?: Rec; headers?: Record } = {}) { const requestId = `native-combo-${crypto.randomUUID()}`; const response = await handleClaudeMessages(new Request("http://localhost/v1/messages", { - method: "POST", headers: { "content-type": "application/json", authorization: "Bearer fixture-admission", "x-api-key": "fixture-caller" }, + method: "POST", headers: { "content-type": "application/json", authorization: "Bearer fixture-admission", "x-api-key": "fixture-caller", ...options.headers }, body: JSON.stringify({ ...source, stream: options.stream ?? true, ...(options.messages ? { messages: options.messages } : {}), ...(options.tools ? { tools: options.tools } : {}), ...options.extra }), }), options.config ?? config(), { model: "", provider: "" }, { requestId, start: Date.now(), admission: options.admission }); const text = await response.text(); @@ -480,6 +480,132 @@ describe("native Messages candidates in a combo", () => { expect(seen[0]!.body).toEqual({ ...source, model: "claude-sonnet-4-6", stream: true }); } finally { await server.stop(true); } }); + test("real HTTP ingress preserves Messages fields for a compatible key-auth combo child", async () => { + const settings = config(); + settings.providers.anth!.baseUrl = "https://compatible.fixture.invalid/v1"; + const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: request => handleClaudeMessages( + request, settings, { model: "", provider: "" }, { requestId: `http-${crypto.randomUUID()}`, start: Date.now() }, + ) }); + try { + const response = await fetch(new URL("/v1/messages", server.url), { method: "POST", + headers: { "content-type": "application/json" }, body: JSON.stringify({ ...source, stream: true }) }); + expect(response.status).toBe(200); + expect(await response.text()).toContain("native reply"); + expect(seen).toHaveLength(1); + expect(seen[0]!.body).toEqual({ ...source, model: "claude-sonnet-4-6", stream: true }); + expect(seen[0]!.headers.get("x-api-key")).toBe("fixture-managed-key"); + } finally { await server.stop(true); } + }); + for (const transport of ["bearer", "x-api-key"] as const) test(`compatible combo uses its own ${transport} credential`, async () => { + const settings = config(); + settings.providers.anth!.baseUrl = "https://compatible.fixture.invalid/v1"; + settings.providers.anth!.apiKeyTransport = transport; + const { response, rows } = await send({ config: settings }); + expect(response.status).toBe(200); + expect(seen).toHaveLength(1); + expect(seen[0]!.headers.get("authorization")).toBe(transport === "bearer" ? "Bearer fixture-managed-key" : null); + expect(seen[0]!.headers.get("x-api-key")).toBe(transport === "x-api-key" ? "fixture-managed-key" : null); + expect(rows[0]!.spend).toMatchObject({ sends: 1 }); + }); + test("compatible pre-output failure replays the original body to the first-party sibling", async () => { + const settings = config(); + settings.providers.compat = { ...settings.providers.anth!, baseUrl: "https://compatible.fixture.invalid/v1", apiKey: "fixture-compatible-key" }; + settings.combos!.waterfall!.targets = [{ provider: "compat", model: "claude-sonnet-4-6" }, { provider: "anth", model: "claude-sonnet-4-6" }]; + answer = () => seen.length === 1 ? stream(prelude + refusal) : stream(prelude + textStart + textDelta + terminal); + const { response, text, rows } = await send({ config: settings }); + expect(response.status).toBe(200); + expect(text).toContain("native reply"); + expect(seen.map(entry => new URL(entry.url).hostname)).toEqual(["compatible.fixture.invalid", "api.anthropic.com"]); + expect(seen.map(entry => entry.headers.get("x-api-key"))).toEqual(["fixture-compatible-key", "fixture-managed-key"]); + expect(seen.map(entry => entry.body)).toEqual([{ ...source, model: "claude-sonnet-4-6", stream: true }, { ...source, model: "claude-sonnet-4-6", stream: true }]); + expect(rows[0]!.spend).toMatchObject({ sends: 2 }); + }); + test("compatible child refuses opaque-state loss before sending under reject", async () => { + const settings = config(); + settings.providers.anth!.baseUrl = "https://compatible.fixture.invalid/v1"; + settings.combos!.waterfall!.targets = [{ provider: "anth", model: "claude-sonnet-4-6" }]; + settings.protocols!.unrepresentable = "reject"; + const messages = [...source.messages, { role: "assistant", content: [{ type: "redacted_thinking", data: "fixture-opaque" }] }]; + const { response } = await send({ config: settings, messages }); + expect(response.status).toBe(400); + expect(seen).toHaveLength(0); + }); + test("a compatible failure replays original opaque state to a first-party sibling", async () => { + const settings = config(); + settings.providers.compat = { ...settings.providers.anth!, baseUrl: "https://compatible.fixture.invalid/v1" }; + settings.combos!.waterfall!.targets = [{ provider: "compat", model: "claude-sonnet-4-6" }, { provider: "anth", model: "claude-sonnet-4-6" }]; + const messages = [...source.messages, { role: "assistant", content: [{ type: "redacted_thinking", data: "fixture-opaque" }, { type: "text", text: "fixture" }] }]; + answer = () => seen.length === 1 ? stream(prelude + refusal) : stream(prelude + textStart + textDelta + terminal); + const { response } = await send({ config: settings, messages }); + expect(response.status).toBe(200); + expect(seen).toHaveLength(2); + expect(JSON.stringify(seen[0]!.body.messages)).not.toContain("fixture-opaque"); + expect(seen[1]!.body.messages).toEqual(messages); + }); + test("compatible child drops caller betas outside its allowlist", async () => { + const settings = config(); + settings.providers.anth!.baseUrl = "https://compatible.fixture.invalid/v1"; + await send({ config: settings, headers: { "anthropic-beta": "interleaved-thinking-2025-05-14,fixture-unlisted-beta-2099-02-02" } }); + expect(seen).toHaveLength(1); + expect(seen[0]!.headers.get("anthropic-beta")).toBeNull(); + }); + test("compatible child enforces caller scope before a send", async () => { + const settings = config(); + settings.providers.anth!.baseUrl = "https://compatible.fixture.invalid/v1"; + settings.apiKeys = [{ id: "caller", name: "fixture", key: "ocx_data_" + "c".repeat(40), createdAt: "2026-01-01T00:00:00.000Z", allowedProviders: ["backup"] }]; + settings.combos!.waterfall!.targets = [{ provider: "anth", model: "claude-sonnet-4-6" }]; + const { response } = await send({ config: settings, admission: { kind: "configured", keyId: "caller", source: "dedicated" } }); + expect(response.status).toBe(403); + expect(seen).toHaveLength(0); + }); + test("OAuth builder refuses a compatible destination independently", async () => { + const { buildAnthropicMessagesPassthroughRequest } = await import("../../src/adapters/anthropic/passthrough"); + const provider = { ...config().providers.anth!, authMode: "oauth" as const, baseUrl: "https://compatible.fixture.invalid/v1", apiKey: "synthetic-oauth-token" }; + expect(() => buildAnthropicMessagesPassthroughRequest(provider, "claude-sonnet-4-6", source)).toThrow("only to api.anthropic.com"); + expect(seen).toHaveLength(0); + }); + test("compatible combo rotates its key after authentication failure", async () => { + const { saveConfig } = await import("../../src/config"); + const settings = config(); + settings.providers.anth!.baseUrl = "https://compatible.fixture.invalid/v1"; + settings.providers.anth!.apiKeyPool = [{ id: "k1", key: "fixture-managed-key", addedAt: 1 }, { id: "k2", key: "fixture-second-key", addedAt: 2 }]; + settings.combos!.waterfall!.targets = [{ provider: "anth", model: "claude-sonnet-4-6" }]; + saveConfig(settings); + answer = () => seen.length === 1 + ? Response.json({ type: "error", error: { type: "authentication_error", message: "invalid x-api-key" } }, { status: 401 }) + : stream(prelude + textStart + textDelta + terminal); + const { response } = await send({ config: settings }); + expect(response.status).toBe(200); + expect(seen.map(entry => entry.headers.get("x-api-key"))).toEqual(["fixture-managed-key", "fixture-second-key"]); + expect(seen[1]!.body).toEqual(seen[0]!.body); + }); + test("compatible candidate rejects OAuth, pinned effort and combo rewrites before native dispatch", async () => { + const { createComboProtocolLanes } = await import("../../src/server/responses/core-combo-native"); + const { routeConcreteModel } = await import("../../src/router"); + const { createRequestExecutionBudget } = await import("../../src/lib/request-execution-budget"); + const settings = config(); + settings.providers.anth!.baseUrl = "https://compatible.fixture.invalid/v1"; + settings.protocols!.unrepresentable = "reject"; + const target = { provider: "anth", model: "claude-sonnet-4-6" }; + const makeLanes = () => createComboProtocolLanes({ source: { inbound: "messages", envelope: createProtocolEnvelope({ inbound: "messages", body: source, translatorBudget: createTranslatorBudget() }), dispatchNativeChild: async () => new Response() }, + req: new Request("http://localhost/v1/messages"), config: settings, logCtx: { model: "", provider: "" }, admissionScope: undefined, comboId: "waterfall", targets: [target] })!; + const plan = () => makeLanes().nativeChild(target, routeConcreteModel(settings, "anth/claude-sonnet-4-6"), createRequestExecutionBudget()); + expect(plan()).toBeDefined(); + settings.providers.anth!.authMode = "oauth"; + expect(plan()).toBeUndefined(); + settings.providers.anth!.authMode = "key"; + settings.modelPinnedEfforts = { "anth/claude-sonnet-4-6": "low" }; + expect(plan()).toBeUndefined(); + expect(makeLanes().pickable(target)).toBe(false); + settings.modelPinnedEfforts = {}; + settings.combos!.waterfall!.defaultEffort = "low"; + expect(plan()).toBeUndefined(); + expect(makeLanes().pickable(target)).toBe(false); + settings.combos!.waterfall!.defaultEffort = null; + settings.combos!.waterfall!.strategy = "jev"; + expect(plan()).toBeUndefined(); + expect(makeLanes().pickable(target)).toBe(false); + }); for (const streaming of [true, false]) test(`first output waits for a content frame (${streaming ? "stream" : "fold"})`, async () => { const empty = frame("message_delta", { delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 0 } }) + frame("message_stop"); answer = () => stream(prelude + empty);