diff --git a/.changeset/fix-anthropic-profile-undefined-model.md b/.changeset/fix-anthropic-profile-undefined-model.md new file mode 100644 index 0000000000..5db03cac00 --- /dev/null +++ b/.changeset/fix-anthropic-profile-undefined-model.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kosong": patch +--- + +Fix a crash when a model config entry lacks the `model` field (e.g. from a malformed TOML key like `[models.kimi-k2.7-code]`): the Anthropic profile matchers now tolerate `undefined` model names and return no profile instead of throwing `TypeError: Cannot read properties of undefined (reading 'toLowerCase')`. diff --git a/packages/kosong/src/providers/anthropic-profile.ts b/packages/kosong/src/providers/anthropic-profile.ts index b5d05934ea..c2c61a3f1a 100644 --- a/packages/kosong/src/providers/anthropic-profile.ts +++ b/packages/kosong/src/providers/anthropic-profile.ts @@ -74,9 +74,10 @@ const VERSION_FIRST_RE = /(\d{1,2})[-._](\d{1,2})[-._](opus|sonnet|haiku)/; const BARE_FAMILY_RE = /(\d{1,2})[-._](opus|sonnet|haiku)/; export function parseAnthropicModelVersion( - model: string, + model: string | undefined, requireClaudeMarker = false, ): AnthropicModelVersion | null { + if (model === undefined) return null; const normalized = model.toLowerCase(); if (requireClaudeMarker && !normalized.includes('claude')) return null; @@ -111,8 +112,9 @@ export function parseAnthropicModelVersion( } export function matchKnownAnthropicModelProfile( - model: string, + model: string | undefined, ): AnthropicModelProfile | undefined { + if (model === undefined) return undefined; const normalized = model.toLowerCase(); if (/mythos[-._]preview/.test(normalized)) return ALWAYS_ADAPTIVE_MAX_PROFILE; @@ -164,7 +166,10 @@ export function inferAnthropicModelProfile(model: string): AnthropicModelProfile * fallback: an Anthropic-protocol endpoint still needs some profile to shape * requests. */ -export function matchUnknownClaudeProfile(model: string): AnthropicModelProfile | undefined { +export function matchUnknownClaudeProfile( + model: string | undefined, +): AnthropicModelProfile | undefined { + if (model === undefined) return undefined; const normalized = model.toLowerCase(); return normalized.includes('claude') || CLAUDE_FAMILY_WORD_RE.test(normalized) ? LATEST_OPUS_PROFILE diff --git a/packages/kosong/test/anthropic.test.ts b/packages/kosong/test/anthropic.test.ts index 6b079a75a6..75e8941c12 100644 --- a/packages/kosong/test/anthropic.test.ts +++ b/packages/kosong/test/anthropic.test.ts @@ -98,6 +98,16 @@ describe('Anthropic model profile matching', () => { expect(matchUnknownClaudeProfile(model)).toBeUndefined(); }, ); + + // A malformed config entry (e.g. an unquoted dotted TOML key like + // `[models.kimi-k2.7-code]`) parses into a nested object that lacks the + // top-level `model` field. The v2 config schema marks `model` optional, so + // the entry reaches profile matching with `undefined` — the matcher must + // degrade to "no profile" instead of crashing the whole getModels call. + it('tolerates an undefined model name from a malformed config entry', () => { + expect(matchKnownAnthropicModelProfile(undefined as unknown as string)).toBeUndefined(); + expect(matchUnknownClaudeProfile(undefined as unknown as string)).toBeUndefined(); + }); }); type AnthropicGenerationState = {