From fa5f89b6443a9b3205e8151469385b9e161cc330 Mon Sep 17 00:00:00 2001 From: Wasim Date: Fri, 24 Jul 2026 00:19:31 +0530 Subject: [PATCH] fix: check both dash-form and dot-form model IDs in getDefaultMaxTokens normalizeModelId converts Anthropic version dashes to dots (e.g., claude-3-7-sonnet -> claude-3.7-sonnet), but getDefaultMaxTokens only checked dash-form patterns. This caused normalized model IDs to silently fall back to the 4096 default instead of their intended values (64000 for Sonnet 4/3.7, 8192 for 3.5 Haiku, 64000 for Haiku 4.5). Fixes stripe/ai#432 --- .../provider/stripe-language-model-v3.ts | 11 +++- llm/ai-sdk/provider/stripe-language-model.ts | 20 ++++--- .../tests/stripe-language-model-v3.test.ts | 58 ++++++++++++++++++ .../tests/stripe-language-model.test.ts | 59 +++++++++++++++++++ 4 files changed, 139 insertions(+), 9 deletions(-) diff --git a/llm/ai-sdk/provider/stripe-language-model-v3.ts b/llm/ai-sdk/provider/stripe-language-model-v3.ts index 9ce96218..b9bd5b83 100644 --- a/llm/ai-sdk/provider/stripe-language-model-v3.ts +++ b/llm/ai-sdk/provider/stripe-language-model-v3.ts @@ -185,15 +185,22 @@ export class StripeLanguageModelV3 implements LanguageModelV3 { const model = modelId.substring('anthropic/'.length); + // Check both dash-form (claude-3-7-sonnet) and dot-form (claude-3.7-sonnet) + // since normalizeModelId converts version dashes to dots if ( model.includes('sonnet-4') || model.includes('claude-3-7-sonnet') || - model.includes('haiku-4-5') + model.includes('claude-3.7-sonnet') || + model.includes('haiku-4-5') || + model.includes('haiku-4.5') ) { return 64000; } else if (model.includes('opus-4')) { return 32000; - } else if (model.includes('claude-3-5-haiku')) { + } else if ( + model.includes('claude-3-5-haiku') || + model.includes('claude-3.5-haiku') + ) { return 8192; } else { return 4096; diff --git a/llm/ai-sdk/provider/stripe-language-model.ts b/llm/ai-sdk/provider/stripe-language-model.ts index 38c15f09..34c6fe16 100644 --- a/llm/ai-sdk/provider/stripe-language-model.ts +++ b/llm/ai-sdk/provider/stripe-language-model.ts @@ -208,19 +208,25 @@ export class StripeLanguageModel implements LanguageModelV2 { const model = modelId.substring('anthropic/'.length); // Claude Sonnet 4 models (including variants like sonnet-4-1) and 3.7 Sonnet - if (model.includes('sonnet-4') || - model.includes('claude-3-7-sonnet') || - model.includes('haiku-4-5')) { + // Check both dash-form (claude-3-7-sonnet) and dot-form (claude-3.7-sonnet) + // since normalizeModelId converts version dashes to dots + if (model.includes('sonnet-4') || + model.includes('claude-3-7-sonnet') || + model.includes('claude-3.7-sonnet') || + model.includes('haiku-4-5') || + model.includes('haiku-4.5')) { return 64000; // 64K tokens - } + } // Claude Opus 4 models (including variants like opus-4-1) else if (model.includes('opus-4')) { return 32000; // 32K tokens - } + } // Claude 3.5 Haiku - else if (model.includes('claude-3-5-haiku')) { + // Check both dash-form (claude-3-5-haiku) and dot-form (claude-3.5-haiku) + else if (model.includes('claude-3-5-haiku') || + model.includes('claude-3.5-haiku')) { return 8192; // 8K tokens - } + } // Default fallback for other Anthropic models else { return 4096; diff --git a/llm/ai-sdk/provider/tests/stripe-language-model-v3.test.ts b/llm/ai-sdk/provider/tests/stripe-language-model-v3.test.ts index 30958186..25827fed 100644 --- a/llm/ai-sdk/provider/tests/stripe-language-model-v3.test.ts +++ b/llm/ai-sdk/provider/tests/stripe-language-model-v3.test.ts @@ -422,6 +422,64 @@ describe('StripeLanguageModelV3', () => { expect(args.max_tokens).toBeUndefined(); }); + it('should apply correct defaults for normalized (dot-form) model IDs', () => { + const testCases = [ + {modelId: 'anthropic/claude-3.7-sonnet', expected: 64000}, + {modelId: 'anthropic/claude-3.5-haiku', expected: 8192}, + {modelId: 'anthropic/claude-haiku-4.5', expected: 64000}, + {modelId: 'anthropic/claude-opus-4', expected: 32000}, + {modelId: 'anthropic/claude-sonnet-4', expected: 64000}, + ]; + + testCases.forEach(({modelId, expected}) => { + const m = new StripeLanguageModelV3( + modelId, + {customerId: 'cus_test'}, + { + provider: 'stripe', + baseURL: 'https://llm.stripe.com', + headers: () => ({}), + } + ); + + const options: LanguageModelV3CallOptions = { + prompt: [], + }; + + // @ts-expect-error - Accessing private method for testing + const {args} = m.getArgs(options); + expect(args.max_tokens).toBe(expected); + }); + }); + + it('should apply correct defaults for dash-form model IDs (pre-normalization)', () => { + const testCases = [ + {modelId: 'anthropic/claude-3-7-sonnet', expected: 64000}, + {modelId: 'anthropic/claude-3-5-haiku', expected: 8192}, + {modelId: 'anthropic/claude-haiku-4-5', expected: 64000}, + ]; + + testCases.forEach(({modelId, expected}) => { + const m = new StripeLanguageModelV3( + modelId, + {customerId: 'cus_test'}, + { + provider: 'stripe', + baseURL: 'https://llm.stripe.com', + headers: () => ({}), + } + ); + + const options: LanguageModelV3CallOptions = { + prompt: [], + }; + + // @ts-expect-error - Accessing private method for testing + const {args} = m.getArgs(options); + expect(args.max_tokens).toBe(expected); + }); + }); + it('should allow user-provided maxOutputTokens to override default', () => { const sonnetModel = new StripeLanguageModelV3( 'anthropic/claude-sonnet-4', diff --git a/llm/ai-sdk/provider/tests/stripe-language-model.test.ts b/llm/ai-sdk/provider/tests/stripe-language-model.test.ts index 502380d4..3e89a4f0 100644 --- a/llm/ai-sdk/provider/tests/stripe-language-model.test.ts +++ b/llm/ai-sdk/provider/tests/stripe-language-model.test.ts @@ -421,6 +421,65 @@ describe('StripeLanguageModel', () => { expect(args.max_tokens).toBeUndefined(); }); + it('should apply correct defaults for normalized (dot-form) model IDs', () => { + // These are the IDs that normalizeModelId produces + const testCases = [ + {modelId: 'anthropic/claude-3.7-sonnet', expected: 64000}, + {modelId: 'anthropic/claude-3.5-haiku', expected: 8192}, + {modelId: 'anthropic/claude-haiku-4.5', expected: 64000}, + {modelId: 'anthropic/claude-opus-4', expected: 32000}, + {modelId: 'anthropic/claude-sonnet-4', expected: 64000}, + ]; + + testCases.forEach(({modelId, expected}) => { + const m = new StripeLanguageModel( + modelId, + {customerId: 'cus_test'}, + { + provider: 'stripe', + baseURL: 'https://llm.stripe.com', + headers: () => ({}), + } + ); + + const options: LanguageModelV2CallOptions = { + prompt: [], + }; + + // @ts-expect-error - Accessing private method for testing + const {args} = m.getArgs(options); + expect(args.max_tokens).toBe(expected); + }); + }); + + it('should apply correct defaults for dash-form model IDs (pre-normalization)', () => { + const testCases = [ + {modelId: 'anthropic/claude-3-7-sonnet', expected: 64000}, + {modelId: 'anthropic/claude-3-5-haiku', expected: 8192}, + {modelId: 'anthropic/claude-haiku-4-5', expected: 64000}, + ]; + + testCases.forEach(({modelId, expected}) => { + const m = new StripeLanguageModel( + modelId, + {customerId: 'cus_test'}, + { + provider: 'stripe', + baseURL: 'https://llm.stripe.com', + headers: () => ({}), + } + ); + + const options: LanguageModelV2CallOptions = { + prompt: [], + }; + + // @ts-expect-error - Accessing private method for testing + const {args} = m.getArgs(options); + expect(args.max_tokens).toBe(expected); + }); + }); + it('should allow user-provided maxOutputTokens to override default', () => { const sonnetModel = new StripeLanguageModel( 'anthropic/claude-sonnet-4',