From d5b77b831be01376ef04c6e4d6df726d5ad0c36a Mon Sep 17 00:00:00 2001 From: Vadym O Date: Wed, 30 Sep 2026 18:53:44 +0800 Subject: [PATCH 1/2] fix(anthropic): give each Claude model its real 128K output maximum Both Anthropic registry entries declared only `defaultMaxOutputTokens` 64000 and no per-model map, so every Claude model resolved a 64000 output ceiling. Anthropic documents 128K synchronous max output for all of them except Haiku 4.5, so the figure was understating the real ceiling by half on nine models. That number is not display metadata. `resolveOutputCeiling` is what the adapter budgets against when a caller declares no `max_tokens`, so a 128K-capable model stopped at 64000 with `stop_reason: max_tokens` and the caller had no way to reach the rest. The same value feeds the combo admission reserve in `checkComboTargetInputAdmission`, where understating a target's output makes its headroom arithmetic wrong in the other direction. ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS carries the per-model maxima and both the OAuth and API-key entries consume it. Each value was read from that model's own overview page on platform.claude.com (/docs/en/models//overview), which states it in the header line and again in the Capabilities table: Fable 5.1, Fable 5, Sonnet 5.5, Sonnet 5, Opus 5.5, Opus 5, Opus 4.8, Opus 4.7, Opus 4.6 and Sonnet 4.6 are all 128K. Deliberately not the 300K Message Batches extended-output beta, which needs `output-300k-2026-03-24` and does not apply to the synchronous route these providers use. Haiku 4.5 is genuinely 64K and is left out of the map on purpose so it keeps inheriting the default, and the default stays 64000 as the floor for any model id the map does not name. Verification: the new test asserts the map covers exactly the seeded models minus Haiku 4.5, and that both providers resolve 128000 per model (64000 for Haiku) through `resolveModelPolicy` and through `resolveOutputCeiling`, which is the resolver the admission path actually calls. It fails on both providers with the map defined but not wired into the entries, and passes once it is. `bun run typecheck` is clean. tests/providers, tests/routing and tests/server/input-admission.test.ts report the same 162 pre-existing failures with and without this change. Co-Authored-By: Claude Code --- src/providers/registry/entries-core.ts | 3 + src/providers/registry/model-seeds.ts | 33 +++++++++- .../anthropic/anthropic-output-maxima.test.ts | 61 +++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests/adapters/anthropic/anthropic-output-maxima.test.ts diff --git a/src/providers/registry/entries-core.ts b/src/providers/registry/entries-core.ts index 51369022bf1..bbbb0cb1c1d 100644 --- a/src/providers/registry/entries-core.ts +++ b/src/providers/registry/entries-core.ts @@ -20,6 +20,7 @@ import { ANTHROPIC_MODEL_CONTEXT_WINDOWS, ANTHROPIC_MODEL_INPUT_MODALITIES, ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS, + ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS, ANTHROPIC_MODEL_REASONING_EFFORTS, ZAI_GLM_52_REASONING_EFFORTS, ZAI_GLM_53_REASONING_EFFORTS, @@ -489,6 +490,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [ // Codex omits max_output_tokens; without a provider budget the Anthropic adapter // falls back to 8192, which truncates long answers with stop_reason=max_tokens. defaultMaxOutputTokens: ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS, + modelMaxOutputTokens: { ...ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS }, defaultModel: "claude-sonnet-5", // Claude fast mode on the subscription lane (Claude Code `/fast`): the OAuth route accepts // `speed` and gates it on account entitlement (usage credits / org enablement), probed live @@ -516,6 +518,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [ modelInputModalities: { ...ANTHROPIC_MODEL_INPUT_MODALITIES }, modelReasoningEfforts: { ...ANTHROPIC_MODEL_REASONING_EFFORTS }, defaultMaxOutputTokens: ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS, + modelMaxOutputTokens: { ...ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS }, defaultModel: "claude-sonnet-5", fastWire: ANTHROPIC_FAST_WIRE, modelSupportsServiceTier: { ...ANTHROPIC_FAST_MODELS }, diff --git a/src/providers/registry/model-seeds.ts b/src/providers/registry/model-seeds.ts index b2a8ce82ed9..9b480828a4a 100644 --- a/src/providers/registry/model-seeds.ts +++ b/src/providers/registry/model-seeds.ts @@ -19,9 +19,38 @@ export const ANTHROPIC_MODEL_INPUT_MODALITIES: Record = Object ANTHROPIC_MODELS.map(id => [id, ["text", "image"]]), ); // Every current Claude family accepts at least 64k output tokens (Haiku 4.5 / Sonnet 4.x -// through Opus 5 and Fable 5). Anthropic caps max_tokens per model server-side, so a -// larger request never over-allocates; it only stops the 8192 truncation. +// through Opus 5 and Fable 5), so this stays the floor for a model id not listed in +// ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS below. export const ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 64_000; +/** + * Per-model synchronous Messages API output maxima, each read from that model's own overview + * page on platform.claude.com (`/docs/en/models//overview`), which states the number in + * its header line and again in the Capabilities table. Deliberately NOT the 300K Message + * Batches extended-output beta, which needs `output-300k-2026-03-24` and does not apply to the + * synchronous route these providers use. + * + * Every seeded Claude model except Haiku 4.5 is 128K. Haiku 4.5 is genuinely 64K and is left + * out so it keeps inheriting ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS. + * + * Without this the 64000 default stood in for all of them, which understates the real ceiling + * by half on every Opus, Sonnet and Fable model. `resolveOutputCeiling` is the value the + * adapter budgets against when a caller declares no `max_tokens`, so a 128K-capable model + * stopped at 64000 with `stop_reason: max_tokens`; the same figure feeds the combo admission + * reserve in `checkComboTargetInputAdmission`, where understating a target's output makes its + * headroom arithmetic wrong. + */ +export const ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS: Record = { + "claude-fable-5-1": 128_000, + "claude-fable-5": 128_000, + "claude-sonnet-5-5": 128_000, + "claude-sonnet-5": 128_000, + "claude-opus-5-5": 128_000, + "claude-opus-5": 128_000, + "claude-opus-4-8": 128_000, + "claude-opus-4-7": 128_000, + "claude-opus-4-6": 128_000, + "claude-sonnet-4-6": 128_000, +}; /** * The effort rungs opencodex exposes for native Anthropic models. Without this the * providers advertised no ladder at all, so every client that keys its effort control off diff --git a/tests/adapters/anthropic/anthropic-output-maxima.test.ts b/tests/adapters/anthropic/anthropic-output-maxima.test.ts new file mode 100644 index 00000000000..1f423102b78 --- /dev/null +++ b/tests/adapters/anthropic/anthropic-output-maxima.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "bun:test"; +import { providerConfigSeed } from "../../../src/providers/derive"; +import { getProviderRegistryEntry } from "../../../src/providers/registry"; +import { + ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS, + ANTHROPIC_MODELS, + ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS, +} from "../../../src/providers/registry/model-seeds"; +import { resolveModelPolicy } from "../../../src/providers/resolved-model-policy"; +import { resolveOutputCeiling } from "../../../src/server/responses/input-admission"; + +// Anthropic's per-model overview pages (platform.claude.com/docs/en/models//overview, +// read 2026-09-30) put the synchronous Messages API maximum at 128K for every seeded Claude +// model except Haiku 4.5, which is 64K. The 300K figure on those pages belongs to the Message +// Batches extended-output beta and does not apply to the synchronous route. +const HIGH_OUTPUT = 128_000; +const HAIKU = "claude-haiku-4-5"; +const PROVIDERS = ["anthropic", "anthropic-apikey"] as const; + +const entry = (name: string) => getProviderRegistryEntry(name)!; + +function policy(providerName: string, modelId: string) { + const registryEntry = entry(providerName); + const authMode = providerName === "anthropic" ? "oauth" : "key"; + return resolveModelPolicy({ + providerName, + modelId, + provider: { adapter: registryEntry.adapter, baseUrl: registryEntry.baseUrl, authMode }, + registryEntry, + transportMatchedRegistry: true, + effectiveAuth: { authMode }, + }); +} + +describe("Anthropic per-model output maxima", () => { + test("the table covers every seeded model except the one that really is 64K", () => { + expect(Object.keys(ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS).sort()) + .toEqual(ANTHROPIC_MODELS.filter(id => id !== HAIKU).sort()); + for (const maximum of Object.values(ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS)) { + expect(maximum).toBe(HIGH_OUTPUT); + } + // Haiku 4.5 carries no entry on purpose, so it must still resolve the default and not 128K. + expect(ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS[HAIKU]).toBeUndefined(); + expect(ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS).toBe(64_000); + }); + + for (const providerName of PROVIDERS) { + test(`${providerName} resolves each model's real ceiling rather than the 64K default`, () => { + expect(entry(providerName).modelMaxOutputTokens).toEqual(ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS); + const provider = providerConfigSeed(entry(providerName)); + for (const modelId of ANTHROPIC_MODELS) { + const expected = modelId === HAIKU ? ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS : HIGH_OUTPUT; + expect(policy(providerName, modelId).model.maxOutputTokens, modelId).toBe(expected); + // The admission path reads the ceiling through its own resolver, so assert that too: + // it is what the adapter budgets against when a caller declares no max_tokens, and what + // the combo reserve is computed from. + expect(resolveOutputCeiling(provider, providerName, modelId)).toBe(expected); + } + }); + } +}); From 646142a34fb78e0f948c758b9df5439fffb1e261 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 23:52:58 +0900 Subject: [PATCH 2/2] test: register Anthropic output maxima regression in layout maps --- scripts/test-layout/layout.json | 1 + tests/fixtures/test-layout-expected.json | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index f41417d755e..9454aec7f0f 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -132,6 +132,7 @@ "anthropic-image-retry-e2e.test.ts": "adapters/anthropic", "anthropic-image-retry.test.ts": "adapters/anthropic", "anthropic-sonnet-5-5-contract.test.ts": "adapters/anthropic", + "anthropic-output-maxima.test.ts": "adapters/anthropic", "anthropic-parallel-tool-disable.test.ts": "adapters/anthropic", "anthropic-pool-toggle-copy.test.ts": "adapters/anthropic", "anthropic-quorum-cache.test.ts": "routing", diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index a74fbe1897e..ac3ad1f2837 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -138,6 +138,7 @@ "anthropic-image-retry-e2e.test.ts": "adapters/anthropic", "anthropic-image-retry.test.ts": "adapters/anthropic", "anthropic-sonnet-5-5-contract.test.ts": "adapters/anthropic", + "anthropic-output-maxima.test.ts": "adapters/anthropic", "anthropic-parallel-tool-disable.test.ts": "adapters/anthropic", "anthropic-pool-toggle-copy.test.ts": "adapters/anthropic", "anthropic-quorum-cache.test.ts": "routing",