Skip to content

Commit 26670e2

Browse files
authored
fix(hosted): fixed hosted providers to exact string match model names rather than check provider names (#2228)
1 parent e52bd57 commit 26670e2

File tree

2 files changed

+94
-15
lines changed

2 files changed

+94
-15
lines changed

apps/sim/providers/utils.test.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
MODELS_WITH_VERBOSITY,
2525
PROVIDERS_WITH_TOOL_USAGE_CONTROL,
2626
prepareToolsWithUsageControl,
27+
shouldBillModelUsage,
2728
supportsTemperature,
2829
supportsToolUsageControl,
2930
transformCustomTool,
@@ -40,6 +41,7 @@ describe('getApiKey', () => {
4041
beforeEach(() => {
4142
vi.clearAllMocks()
4243

44+
// @ts-expect-error - mocking boolean with different value
4345
isHostedSpy.mockReturnValue(false)
4446

4547
module.require = vi.fn(() => ({
@@ -53,6 +55,7 @@ describe('getApiKey', () => {
5355
})
5456

5557
it('should return user-provided key when not in hosted environment', () => {
58+
// @ts-expect-error - mocking boolean with different value
5659
isHostedSpy.mockReturnValue(false)
5760

5861
// For OpenAI
@@ -65,6 +68,7 @@ describe('getApiKey', () => {
6568
})
6669

6770
it('should throw error if no key provided in non-hosted environment', () => {
71+
// @ts-expect-error - mocking boolean with different value
6872
isHostedSpy.mockReturnValue(false)
6973

7074
expect(() => getApiKey('openai', 'gpt-4')).toThrow('API key is required for openai gpt-4')
@@ -80,7 +84,8 @@ describe('getApiKey', () => {
8084
throw new Error('Rotation failed')
8185
})
8286

83-
const key = getApiKey('openai', 'gpt-4', 'user-fallback-key')
87+
// Use gpt-4o which IS in the hosted models list
88+
const key = getApiKey('openai', 'gpt-4o', 'user-fallback-key')
8489
expect(key).toBe('user-fallback-key')
8590
})
8691

@@ -91,7 +96,8 @@ describe('getApiKey', () => {
9196
throw new Error('Rotation failed')
9297
})
9398

94-
expect(() => getApiKey('openai', 'gpt-4')).toThrow('No API key available for openai gpt-4')
99+
// Use gpt-4o which IS in the hosted models list
100+
expect(() => getApiKey('openai', 'gpt-4o')).toThrow('No API key available for openai gpt-4o')
95101
})
96102

97103
it('should require user key for non-OpenAI/Anthropic providers even in hosted environment', () => {
@@ -104,6 +110,30 @@ describe('getApiKey', () => {
104110
'API key is required for other-provider some-model'
105111
)
106112
})
113+
114+
it('should require user key for models NOT in hosted list even if provider matches', () => {
115+
isHostedSpy.mockReturnValue(true)
116+
117+
// Models with version suffixes that are NOT in the hosted list should require user API key
118+
// even though they're from anthropic/openai providers
119+
120+
// User provides their own key - should work
121+
const key1 = getApiKey('anthropic', 'claude-sonnet-4-20250514', 'user-key-anthropic')
122+
expect(key1).toBe('user-key-anthropic')
123+
124+
// No user key - should throw, NOT use server key
125+
expect(() => getApiKey('anthropic', 'claude-sonnet-4-20250514')).toThrow(
126+
'API key is required for anthropic claude-sonnet-4-20250514'
127+
)
128+
129+
// Same for OpenAI versioned models not in list
130+
const key2 = getApiKey('openai', 'gpt-4o-2024-08-06', 'user-key-openai')
131+
expect(key2).toBe('user-key-openai')
132+
133+
expect(() => getApiKey('openai', 'gpt-4o-2024-08-06')).toThrow(
134+
'API key is required for openai gpt-4o-2024-08-06'
135+
)
136+
})
107137
})
108138

109139
describe('Model Capabilities', () => {
@@ -476,6 +506,52 @@ describe('getHostedModels', () => {
476506
})
477507
})
478508

509+
describe('shouldBillModelUsage', () => {
510+
it.concurrent('should return true for exact matches of hosted models', () => {
511+
// OpenAI models
512+
expect(shouldBillModelUsage('gpt-4o')).toBe(true)
513+
expect(shouldBillModelUsage('o1')).toBe(true)
514+
515+
// Anthropic models
516+
expect(shouldBillModelUsage('claude-sonnet-4-0')).toBe(true)
517+
expect(shouldBillModelUsage('claude-opus-4-0')).toBe(true)
518+
519+
// Google models
520+
expect(shouldBillModelUsage('gemini-2.5-pro')).toBe(true)
521+
expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true)
522+
})
523+
524+
it.concurrent('should return false for non-hosted models', () => {
525+
// Other providers
526+
expect(shouldBillModelUsage('deepseek-v3')).toBe(false)
527+
expect(shouldBillModelUsage('grok-4-latest')).toBe(false)
528+
529+
// Unknown models
530+
expect(shouldBillModelUsage('unknown-model')).toBe(false)
531+
})
532+
533+
it.concurrent('should return false for versioned model names not in hosted list', () => {
534+
// Versioned model names that are NOT in the hosted list
535+
// These should NOT be billed (user provides own API key)
536+
expect(shouldBillModelUsage('claude-sonnet-4-20250514')).toBe(false)
537+
expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false)
538+
expect(shouldBillModelUsage('claude-3-5-sonnet-20241022')).toBe(false)
539+
})
540+
541+
it.concurrent('should be case insensitive', () => {
542+
expect(shouldBillModelUsage('GPT-4O')).toBe(true)
543+
expect(shouldBillModelUsage('Claude-Sonnet-4-0')).toBe(true)
544+
expect(shouldBillModelUsage('GEMINI-2.5-PRO')).toBe(true)
545+
})
546+
547+
it.concurrent('should not match partial model names', () => {
548+
// Should not match partial/prefix models
549+
expect(shouldBillModelUsage('gpt-4')).toBe(false) // gpt-4o is hosted, not gpt-4
550+
expect(shouldBillModelUsage('claude-sonnet')).toBe(false)
551+
expect(shouldBillModelUsage('gemini')).toBe(false)
552+
})
553+
})
554+
479555
describe('Provider Management', () => {
480556
describe('getProviderFromModel', () => {
481557
it.concurrent('should return correct provider for known models', () => {

apps/sim/providers/utils.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ export function getHostedModels(): string[] {
619619
*/
620620
export function shouldBillModelUsage(model: string): boolean {
621621
const hostedModels = getHostedModels()
622-
return hostedModels.includes(model)
622+
return hostedModels.some((hostedModel) => model.toLowerCase() === hostedModel.toLowerCase())
623623
}
624624

625625
/**
@@ -643,19 +643,22 @@ export function getApiKey(provider: string, model: string, userProvidedKey?: str
643643
const isGeminiModel = provider === 'google'
644644

645645
if (isHosted && (isOpenAIModel || isClaudeModel || isGeminiModel)) {
646-
try {
647-
// Import the key rotation function
648-
const { getRotatingApiKey } = require('@/lib/core/config/api-keys')
649-
const serverKey = getRotatingApiKey(isGeminiModel ? 'gemini' : provider)
650-
return serverKey
651-
} catch (_error) {
652-
// If server key fails and we have a user key, fallback to that
653-
if (hasUserKey) {
654-
return userProvidedKey!
655-
}
646+
// Only use server key if model is explicitly in our hosted list
647+
const hostedModels = getHostedModels()
648+
const isModelHosted = hostedModels.some((m) => m.toLowerCase() === model.toLowerCase())
649+
650+
if (isModelHosted) {
651+
try {
652+
const { getRotatingApiKey } = require('@/lib/core/config/api-keys')
653+
const serverKey = getRotatingApiKey(isGeminiModel ? 'gemini' : provider)
654+
return serverKey
655+
} catch (_error) {
656+
if (hasUserKey) {
657+
return userProvidedKey!
658+
}
656659

657-
// Otherwise, throw an error
658-
throw new Error(`No API key available for ${provider} ${model}`)
660+
throw new Error(`No API key available for ${provider} ${model}`)
661+
}
659662
}
660663
}
661664

0 commit comments

Comments
 (0)