fix(kosong): fall back to nearest lower catalogued minor for max tokens (#1463)

* fix(kosong): fall back to nearest lower catalogued minor for Claude max tokens

An uncatalogued Claude minor version (e.g. claude-opus-4-8) previously
dropped straight to the family/major baseline ceiling, so Opus 4.8
resolved to 32k max output tokens instead of the 128k it supports.
Walk the minor version down to the nearest catalogued entry before
using the family baseline, so a newer minor inherits its predecessor's
documented ceiling.

* fix(kosong): catalogue Opus 4.8's documented 128k output ceiling

Known models should resolve from explicit table entries; the
nearest-lower-minor fallback now only covers minors that are not yet
catalogued. Fable 5 already has an explicit entry.
This commit is contained in:
Luyu Cheng 2026-07-07 15:11:12 +08:00 committed by GitHub
parent 928726ace1
commit 03e78ae190
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 29 additions and 10 deletions

View file

@ -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.

View file

@ -151,15 +151,16 @@ const ANTHROPIC_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = {
* can silently truncate mid-`tool_use`.
*
* Keys are `<family>-<major>[-<minor>]`. 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<Record<string, number>> = {
// 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}`];
}

View file

@ -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', () => {