fix: add claude-opus-4-5 pattern to THINKING_TOKEN_MAP (#11457)

* fix: add claude-opus-4-5 pattern to THINKING_TOKEN_MAP

Adds missing regex pattern for claude-opus-4-5 models (e.g., claude-opus-4-5-20251101)
to the THINKING_TOKEN_MAP configuration. Without this pattern, the model was not
recognized, causing findTokenLimit() to return undefined and leading to an
AI_InvalidArgumentError when using Google Vertex AI Anthropic provider.

The fix adds the pattern 'claude-opus-4-5.*$': { min: 1024, max: 64_000 } to
match the existing claude-4 thinking token configuration.

Fixes AI_InvalidArgumentError: invalid anthropic provider options caused by
budgetTokens receiving NaN instead of a number.

Signed-off-by: Shuchen Luo (personal linux) <nemo0806@gmail.com>

* refactor: make THINKING_TOKEN_MAP constant private

* fix(reasoning): update claude model token limit regex patterns

- Consolidate claude model regex patterns to be more consistent
- Add comprehensive test cases for various claude model variants
- Ensure case insensitivity and proper handling of edge cases

* fix: format

* feat(models): extend claude model regex patterns to support AWS and GCP formats

Update regex patterns in THINKING_TOKEN_MAP to support additional Claude model ID formats used in AWS Bedrock and GCP Vertex AI
Add comprehensive test cases for new model ID formats and reorganize test suite

* fix: format

---------

Signed-off-by: Shuchen Luo (personal linux) <nemo0806@gmail.com>
Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
Shuchen Luo
2025-11-26 02:47:14 -05:00
committed by GitHub
parent 0d69eeaccf
commit b6b999b635
2 changed files with 228 additions and 24 deletions

View File

@@ -800,20 +800,6 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
})
})
describe('Token limit lookup', () => {
it.each([
['gemini-2.5-flash-lite-latest', { min: 512, max: 24576 }],
['qwen-plus-2025-07-14', { min: 0, max: 38912 }],
['claude-haiku-4', { min: 1024, max: 64000 }]
])('returns configured min/max pairs for %s', (id, expected) => {
expect(findTokenLimit(id)).toEqual(expected)
})
it('returns undefined when regex misses', () => {
expect(findTokenLimit('unknown-model')).toBeUndefined()
})
})
describe('Gemini Models', () => {
describe('isSupportedThinkingTokenGeminiModel', () => {
it('should return true for gemini 2.5 models', () => {
@@ -1201,6 +1187,19 @@ describe('Gemini Models', () => {
})
describe('findTokenLimit', () => {
describe('General token limit lookup', () => {
it.each([
['gemini-2.5-flash-lite-latest', { min: 512, max: 24576 }],
['qwen-plus-2025-07-14', { min: 0, max: 38912 }]
])('returns configured min/max pairs for %s', (id, expected) => {
expect(findTokenLimit(id)).toEqual(expected)
})
it('returns undefined when regex misses', () => {
expect(findTokenLimit('unknown-model')).toBeUndefined()
})
})
const cases: Array<{ modelId: string; expected: { min: number; max: number } }> = [
{ modelId: 'gemini-2.5-flash-lite-exp', expected: { min: 512, max: 24_576 } },
{ modelId: 'gemini-1.5-flash', expected: { min: 0, max: 24_576 } },
@@ -1216,11 +1215,7 @@ describe('findTokenLimit', () => {
{ modelId: 'qwen-plus-ultra', expected: { min: 0, max: 81_920 } },
{ modelId: 'qwen-turbo-pro', expected: { min: 0, max: 38_912 } },
{ modelId: 'qwen-flash-lite', expected: { min: 0, max: 81_920 } },
{ modelId: 'qwen3-7b', expected: { min: 1_024, max: 38_912 } },
{ modelId: 'claude-3.7-sonnet-extended', expected: { min: 1_024, max: 64_000 } },
{ modelId: 'claude-sonnet-4.1', expected: { min: 1_024, max: 64_000 } },
{ modelId: 'claude-sonnet-4-5-20250929', expected: { min: 1_024, max: 64_000 } },
{ modelId: 'claude-opus-4-1-extended', expected: { min: 1_024, max: 32_000 } }
{ modelId: 'qwen3-7b', expected: { min: 1_024, max: 38_912 } }
]
it.each(cases)('returns correct limits for $modelId', ({ modelId, expected }) => {
@@ -1230,6 +1225,207 @@ describe('findTokenLimit', () => {
it('returns undefined for unknown models', () => {
expect(findTokenLimit('unknown-model')).toBeUndefined()
})
describe('Claude models', () => {
describe('Claude 3.7 Sonnet models', () => {
it.each([
'claude-3.7-sonnet',
'claude-3-7-sonnet',
'claude-3.7-sonnet-latest',
'claude-3-7-sonnet-latest',
'claude-3.7-sonnet-20250201',
'claude-3-7-sonnet-20250201',
// Official Claude API IDs
'claude-3-7-sonnet-20250219',
// AWS Bedrock format
'anthropic.claude-3-7-sonnet-20250219-v1:0',
// GCP Vertex AI format
'claude-3-7-sonnet@20250219'
])('should return { min: 1024, max: 64000 } for %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
})
it.each(['CLAUDE-3.7-SONNET', 'Claude-3-7-Sonnet-Latest'])('should be case insensitive for %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
})
})
describe('Claude 4.0 series models', () => {
it.each([
'claude-sonnet-4',
'claude-sonnet-4.0',
'claude-sonnet-4-0',
'claude-sonnet-4-preview',
'claude-sonnet-4.0-preview',
'claude-sonnet-4-20250101',
// Official Claude API IDs
'claude-sonnet-4-20250514',
// AWS Bedrock format
'anthropic.claude-sonnet-4-20250514-v1:0',
// GCP Vertex AI format
'claude-sonnet-4@20250514'
])('should return { min: 1024, max: 64000 } for Sonnet variant %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
})
it.each([
'claude-opus-4',
'claude-opus-4.0',
'claude-opus-4-0',
'claude-opus-4-preview',
'claude-opus-4.0-preview',
'claude-opus-4-20250101',
// Official Claude API IDs
'claude-opus-4-20250514',
// AWS Bedrock format
'anthropic.claude-opus-4-20250514-v1:0',
// GCP Vertex AI format
'claude-opus-4@20250514'
])('should return { min: 1024, max: 32000 } for Opus variant %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 32_000 })
})
it.each(['CLAUDE-SONNET-4', 'Claude-Opus-4-Preview'])('should be case insensitive for %s', (modelId) => {
const expectedSonnet = { min: 1024, max: 64_000 }
const expectedOpus = { min: 1024, max: 32_000 }
const result = findTokenLimit(modelId)
expect(result).toBeDefined()
expect([expectedSonnet, expectedOpus]).toContainEqual(result)
})
})
describe('Claude Opus 4.1 models', () => {
it.each([
'claude-opus-4.1',
'claude-opus-4-1',
'claude-opus-4.1-preview',
'claude-opus-4-1-preview',
'claude-opus-4.1-20250120',
'claude-opus-4-1-20250120',
// Official Claude API IDs
'claude-opus-4-1-20250805',
// AWS Bedrock format
'anthropic.claude-opus-4-1-20250805-v1:0',
// GCP Vertex AI format
'claude-opus-4-1@20250805'
])('should return { min: 1024, max: 32000 } for %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 32_000 })
})
it.each(['CLAUDE-OPUS-4.1', 'Claude-Opus-4-1-Preview'])('should be case insensitive for %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 32_000 })
})
})
describe('Claude 4.5 series models (Haiku, Sonnet, Opus)', () => {
it.each([
'claude-haiku-4.5',
'claude-haiku-4-5',
'claude-haiku-4.5-preview',
'claude-haiku-4-5-preview',
'claude-haiku-4.5-20250929',
'claude-haiku-4-5-20250929',
// Official Claude API IDs
'claude-haiku-4-5-20251001',
// AWS Bedrock format
'anthropic.claude-haiku-4-5-20251001-v1:0',
// GCP Vertex AI format
'claude-haiku-4-5@20251001'
])('should return { min: 1024, max: 64000 } for Haiku variant %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
})
it.each([
'claude-sonnet-4.5',
'claude-sonnet-4-5',
'claude-sonnet-4.5-preview',
'claude-sonnet-4-5-preview',
'claude-sonnet-4.5-20250929',
'claude-sonnet-4-5-20250929',
// Official Claude API IDs
'claude-sonnet-4-5-20250929',
// AWS Bedrock format
'anthropic.claude-sonnet-4-5-20250929-v1:0',
// GCP Vertex AI format
'claude-sonnet-4-5@20250929'
])('should return { min: 1024, max: 64000 } for Sonnet variant %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
})
it.each([
'claude-opus-4.5',
'claude-opus-4-5',
'claude-opus-4.5-preview',
'claude-opus-4-5-preview',
'claude-opus-4.5-20250929',
'claude-opus-4-5-20250929',
// Official Claude API IDs
'claude-opus-4-5-20251101',
// AWS Bedrock format
'anthropic.claude-opus-4-5-20251101-v1:0',
// GCP Vertex AI format
'claude-opus-4-5@20251101'
])('should return { min: 1024, max: 64000 } for Opus variant %s', (modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
})
it.each(['CLAUDE-HAIKU-4.5', 'Claude-Sonnet-4-5-Preview', 'CLAUDE-OPUS-4.5-20250929'])(
'should be case insensitive for %s',
(modelId) => {
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
}
)
})
describe('Claude models that should NOT match', () => {
it.each([
'claude-3-opus',
'claude-3-sonnet',
'claude-3-haiku',
'claude-3.5-sonnet',
'claude-3-5-sonnet',
'claude-2.1',
'claude-instant',
'claude-haiku-4',
'claude-haiku-4.0',
'claude-haiku-4-0',
'claude-opus-4.2',
'claude-opus-4-2',
'claude-sonnet-4.2',
'claude-sonnet-4-2',
// Old Haiku models (no Extended thinking support)
'claude-3-5-haiku-20241022',
'claude-3-5-haiku-latest',
'anthropic.claude-3-5-haiku-20241022-v1:0',
'claude-3-5-haiku@20241022',
'claude-3-haiku-20240307',
'anthropic.claude-3-haiku-20240307-v1:0',
'claude-3-haiku@20240307'
])('should return undefined for older/unsupported model %s', (modelId) => {
expect(findTokenLimit(modelId)).toBeUndefined()
})
})
describe('Edge cases', () => {
it('should handle models with custom suffixes', () => {
expect(findTokenLimit('claude-3.7-sonnet-custom-variant')).toEqual({ min: 1024, max: 64_000 })
expect(findTokenLimit('claude-opus-4.1-custom')).toEqual({ min: 1024, max: 32_000 })
expect(findTokenLimit('claude-sonnet-4.5-custom-variant')).toEqual({ min: 1024, max: 64_000 })
})
it('should NOT match non-existent Claude 4.1 variants (only Opus 4.1 exists)', () => {
// Claude Sonnet 4.1 and Haiku 4.1 do not exist
expect(findTokenLimit('claude-sonnet-4.1')).toBeUndefined()
expect(findTokenLimit('claude-haiku-4.1')).toBeUndefined()
})
it('should not match partial model names', () => {
expect(findTokenLimit('claude-3.7')).toBeUndefined()
expect(findTokenLimit('claude-opus')).toBeUndefined()
expect(findTokenLimit('claude-4.5')).toBeUndefined()
})
})
})
})
describe('isGemini3ThinkingTokenModel', () => {

View File

@@ -547,7 +547,7 @@ export function isReasoningModel(model?: Model): boolean {
return REASONING_REGEX.test(modelId) || false
}
export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
// Gemini models
'gemini-2\\.5-flash-lite.*$': { min: 512, max: 24576 },
'gemini-.*-flash.*$': { min: 0, max: 24576 },
@@ -568,10 +568,18 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
'qwen-flash.*$': { min: 0, max: 81_920 },
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },
// Claude models
'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64_000 },
'claude-(:?haiku|sonnet)-4.*$': { min: 1024, max: 64_000 },
'claude-opus-4-1.*$': { min: 1024, max: 32_000 }
// Claude models (supports AWS Bedrock 'anthropic.' prefix, GCP Vertex AI '@' separator, and '-v1:0' suffix)
'(?:anthropic\\.)?claude-3[.-]7.*sonnet.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
'(?:anthropic\\.)?claude-(:?haiku|sonnet|opus)-4[.-]5.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
'(?:anthropic\\.)?claude-opus-4[.-]1.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 32_000 },
'(?:anthropic\\.)?claude-sonnet-4(?:[.-]0)?(?:[@-](?:\\d{4,}|[a-z][\\w-]*))?(?:-v\\d+:\\d+)?$': {
min: 1024,
max: 64_000
},
'(?:anthropic\\.)?claude-opus-4(?:[.-]0)?(?:[@-](?:\\d{4,}|[a-z][\\w-]*))?(?:-v\\d+:\\d+)?$': {
min: 1024,
max: 32_000
}
}
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {