diff --git a/.changeset/opus-4-8-max-tokens.md b/.changeset/opus-4-8-max-tokens.md new file mode 100644 index 000000000..3ccd00278 --- /dev/null +++ b/.changeset/opus-4-8-max-tokens.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix newer Claude minor versions (e.g. Opus 4.8) defaulting to the family-baseline max output tokens; an uncatalogued minor now reuses the nearest earlier known version's limit. diff --git a/packages/kosong/src/providers/anthropic.ts b/packages/kosong/src/providers/anthropic.ts index 682bf3210..791175e12 100644 --- a/packages/kosong/src/providers/anthropic.ts +++ b/packages/kosong/src/providers/anthropic.ts @@ -151,15 +151,16 @@ const ANTHROPIC_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = { * can silently truncate mid-`tool_use`. * * Keys are `-[-]`. Lookups try the most specific - * key first, then fall back to the family/major-only entry, so an - * unrecognized minor version (e.g. a future `opus-4-10`) gets the - * family's baseline rather than the generic fallback. + * key first, then the nearest lower catalogued minor of the same + * family/major (a not-yet-catalogued `opus-4-8` reuses `opus-4-7`'s + * ceiling), and finally the family/major-only baseline entry. */ const CEILING_BY_FAMILY_VERSION: Readonly> = { // Claude Fable 5 documents a 128k output ceiling. 'fable-5': 128000, - // Claude Opus per minor version. 4.6 and 4.7 raised the cap to 128k; + // Claude Opus per minor version. 4.6 through 4.8 document a 128k cap; // 4.5 ships at 64k; 4.1 and the dated 4.0 release stay at 32k. + 'opus-4-8': 128000, 'opus-4-7': 128000, 'opus-4-6': 128000, 'opus-4-5': 64000, @@ -269,8 +270,16 @@ function parseClaudeFamilyVersion(model: string, requireClaudeMarker: boolean): function lookupClaudeCeiling(version: ClaudeVersion): number | undefined { const { family, major, minor } = version; if (minor !== null) { - const exact = CEILING_BY_FAMILY_VERSION[`${family}-${major}-${minor}`]; - if (exact !== undefined) return exact; + // Exact minor first, then walk down to the nearest catalogued minor: + // a newer minor release inherits at least its predecessor's ceiling + // (Anthropic has never lowered the cap within a major), so a + // not-yet-catalogued 4.8 reuses 4.7's value instead of dropping to + // the family baseline. The regex caps minors at two digits, so this + // walk is bounded. + for (let candidate = minor; candidate >= 0; candidate--) { + const ceiling = CEILING_BY_FAMILY_VERSION[`${family}-${major}-${candidate}`]; + if (ceiling !== undefined) return ceiling; + } } return CEILING_BY_FAMILY_VERSION[`${family}-${major}`]; } diff --git a/packages/kosong/test/anthropic.test.ts b/packages/kosong/test/anthropic.test.ts index 10540d41f..494ea94d1 100644 --- a/packages/kosong/test/anthropic.test.ts +++ b/packages/kosong/test/anthropic.test.ts @@ -2619,6 +2619,7 @@ describe('AnthropicChatProvider', () => { describe('resolveDefaultMaxTokens', () => { it('returns per-version Messages-API caps for known Claude 4 models', () => { expect(resolveDefaultMaxTokens('claude-fable-5')).toBe(128000); + expect(resolveDefaultMaxTokens('claude-opus-4-8')).toBe(128000); expect(resolveDefaultMaxTokens('claude-opus-4-7')).toBe(128000); expect(resolveDefaultMaxTokens('claude-opus-4-6')).toBe(128000); expect(resolveDefaultMaxTokens('claude-opus-4-5-20251101')).toBe(64000); @@ -2650,6 +2651,7 @@ describe('resolveDefaultMaxTokens', () => { }); it('matches dotted version separators', () => { + expect(resolveDefaultMaxTokens('claude-opus-4.8')).toBe(128000); expect(resolveDefaultMaxTokens('claude-opus-4.7')).toBe(128000); expect(resolveDefaultMaxTokens('claude-opus-4.6')).toBe(128000); expect(resolveDefaultMaxTokens('claude-sonnet-4.6')).toBe(64000); @@ -2672,12 +2674,15 @@ describe('resolveDefaultMaxTokens', () => { expect(resolveDefaultMaxTokens('anthropic.claude-3-5-sonnet-20240620-v1:0')).toBe(8192); }); - it('falls back to family-only ceiling for unknown minor versions', () => { - // Future opus-4-X release: minor not in table, falls back to opus-4 = 32000. - // Better to under-quote and fail loudly than over-quote a model we can't verify. - expect(resolveDefaultMaxTokens('claude-opus-4-10')).toBe(32000); + it('falls back to the nearest lower catalogued minor for unknown minors', () => { + // opus-4-9/4-10 are not in the table; they reuse opus-4-8's 128k + // ceiling (a newer minor inherits at least its predecessor's cap). + expect(resolveDefaultMaxTokens('claude-opus-4-9')).toBe(128000); + expect(resolveDefaultMaxTokens('claude-opus-4-10')).toBe(128000); expect(resolveDefaultMaxTokens('claude-sonnet-4-9')).toBe(64000); expect(resolveDefaultMaxTokens('claude-haiku-4-9')).toBe(64000); + // A gap between catalogued minors also resolves to the nearest lower one. + expect(resolveDefaultMaxTokens('claude-opus-4-3')).toBe(32000); }); it('matches case-insensitively', () => {