Skip to content

Commit ac6420b

Browse files
committed
fixed ollama and vllm keys
1 parent d431c8d commit ac6420b

File tree

4 files changed

+201
-10
lines changed

4 files changed

+201
-10
lines changed

apps/sim/providers/utils.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
PROVIDERS_WITH_TOOL_USAGE_CONTROL,
3333
prepareToolExecution,
3434
prepareToolsWithUsageControl,
35+
providerRequiresApiKey,
3536
shouldBillModelUsage,
3637
supportsTemperature,
3738
supportsToolUsageControl,
@@ -1385,4 +1386,37 @@ describe('Provider/Model Blacklist', () => {
13851386
expect(getProviderFromModel('CLAUDE-SONNET-4-5')).toBe('anthropic')
13861387
})
13871388
})
1389+
1390+
describe('providerRequiresApiKey', () => {
1391+
it.concurrent('should return false for Ollama models', () => {
1392+
expect(providerRequiresApiKey('llama3.2:1b')).toBe(false)
1393+
expect(providerRequiresApiKey('codellama:13b')).toBe(false)
1394+
expect(providerRequiresApiKey('phi3:latest')).toBe(false)
1395+
})
1396+
1397+
it.concurrent('should return false for vLLM models', () => {
1398+
expect(providerRequiresApiKey('vllm/mistral-7b')).toBe(false)
1399+
expect(providerRequiresApiKey('vllm/llama-3-8b')).toBe(false)
1400+
})
1401+
1402+
it.concurrent('should return true for OpenAI models', () => {
1403+
expect(providerRequiresApiKey('gpt-4o')).toBe(true)
1404+
expect(providerRequiresApiKey('gpt-4o-mini')).toBe(true)
1405+
expect(providerRequiresApiKey('o3-mini')).toBe(true)
1406+
})
1407+
1408+
it.concurrent('should return true for Anthropic models', () => {
1409+
expect(providerRequiresApiKey('claude-sonnet-4-5')).toBe(true)
1410+
expect(providerRequiresApiKey('claude-opus-4-6')).toBe(true)
1411+
})
1412+
1413+
it.concurrent('should return true for Google models', () => {
1414+
expect(providerRequiresApiKey('gemini-2.0-flash')).toBe(true)
1415+
expect(providerRequiresApiKey('gemini-1.5-pro')).toBe(true)
1416+
})
1417+
1418+
it.concurrent('should return true for xAI models', () => {
1419+
expect(providerRequiresApiKey('grok-2')).toBe(true)
1420+
})
1421+
})
13881422
})

apps/sim/providers/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,15 @@ export function shouldBillModelUsage(model: string): boolean {
634634
return hostedModels.some((hostedModel) => model.toLowerCase() === hostedModel.toLowerCase())
635635
}
636636

637+
/**
638+
* Checks whether a given model's provider requires a user-supplied API key.
639+
* Local providers (Ollama, vLLM) don't need one.
640+
*/
641+
export function providerRequiresApiKey(model: string): boolean {
642+
const provider = getProviderFromModel(model)
643+
return provider !== 'ollama' && provider !== 'vllm'
644+
}
645+
637646
/**
638647
* Get an API key for a specific provider, handling rotation and fallbacks
639648
* For use server-side only

apps/sim/serializer/index.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from '@/lib/workflows/subblocks/visibility'
1515
import { getBlock } from '@/blocks'
1616
import type { SubBlockConfig } from '@/blocks/types'
17-
import { getProviderFromModel } from '@/providers/utils'
17+
import { providerRequiresApiKey } from '@/providers/utils'
1818
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
1919
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
2020
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
@@ -525,15 +525,13 @@ export class Serializer {
525525
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlockConfig.id]
526526
const fieldValue = canonicalId ? params[canonicalId] : params[subBlockConfig.id]
527527
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
528-
if (subBlockConfig.id === 'apiKey' && params.model) {
529-
try {
530-
const provider = getProviderFromModel(params.model as string)
531-
if (provider === 'ollama' || provider === 'vllm') {
532-
return
533-
}
534-
} catch {
535-
// If provider resolution fails, continue with validation
536-
}
528+
// Skip API key validation for local providers that don't require one (Ollama, vLLM)
529+
if (
530+
subBlockConfig.id === 'apiKey' &&
531+
params.model &&
532+
!providerRequiresApiKey(params.model as string)
533+
) {
534+
return
537535
}
538536
missingFields.push(subBlockConfig.title || subBlockConfig.id)
539537
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*
4+
* Tests for API key validation with local providers (Ollama, vLLM).
5+
* Verifies that the serializer skips API key validation for providers
6+
* that don't require one, while still enforcing it for cloud providers.
7+
*/
8+
import { loggerMock } from '@sim/testing/mocks'
9+
import { describe, expect, it, vi } from 'vitest'
10+
import { Serializer } from '@/serializer/index'
11+
12+
const agentBlockConfig = {
13+
name: 'Agent',
14+
category: 'blocks',
15+
bgColor: '#2196F3',
16+
tools: {
17+
access: ['openai_chat'],
18+
config: {
19+
tool: () => {
20+
throw new Error('Invalid model selected')
21+
},
22+
},
23+
},
24+
subBlocks: [
25+
{ id: 'model', type: 'combobox', title: 'Model', required: true },
26+
{ id: 'apiKey', type: 'short-input', title: 'API Key', required: true },
27+
{ id: 'messages', type: 'messages-input', title: 'Messages' },
28+
],
29+
inputs: {
30+
model: { type: 'string' },
31+
apiKey: { type: 'string' },
32+
messages: { type: 'json' },
33+
},
34+
}
35+
36+
vi.mock('@/blocks', () => ({
37+
getBlock: (type: string) => {
38+
if (type === 'agent') return agentBlockConfig
39+
return null
40+
},
41+
getAllBlocks: () => [agentBlockConfig],
42+
}))
43+
44+
vi.mock('@/tools/utils', () => ({
45+
getTool: () => null,
46+
}))
47+
48+
vi.mock('@sim/logger', () => loggerMock)
49+
50+
vi.mock('@/providers/utils', () => ({
51+
providerRequiresApiKey: (model: string) => {
52+
if (model.startsWith('vllm/')) return false
53+
if (model.startsWith('gpt') || model.startsWith('o1') || model.startsWith('o3')) return true
54+
if (model.includes('claude')) return true
55+
if (model.includes('gemini')) return true
56+
if (model.startsWith('grok')) return true
57+
// Unknown models default to ollama — no API key required
58+
return false
59+
},
60+
}))
61+
62+
function createAgentBlock(model: string, apiKey: string | null = null): any {
63+
return {
64+
id: 'agent-1',
65+
type: 'agent',
66+
name: 'Agent',
67+
position: { x: 0, y: 0 },
68+
subBlocks: {
69+
model: { value: model },
70+
apiKey: { value: apiKey },
71+
messages: { value: [] },
72+
},
73+
outputs: {},
74+
enabled: true,
75+
}
76+
}
77+
78+
describe('API key validation for local providers', () => {
79+
it.concurrent('should not require API key for Ollama models', () => {
80+
const serializer = new Serializer()
81+
const block = createAgentBlock('llama3.2:1b')
82+
83+
expect(() => {
84+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
85+
}).not.toThrow()
86+
})
87+
88+
it.concurrent('should not require API key for other Ollama model names', () => {
89+
const serializer = new Serializer()
90+
const block = createAgentBlock('mistral:7b')
91+
92+
expect(() => {
93+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
94+
}).not.toThrow()
95+
})
96+
97+
it.concurrent('should not require API key for vLLM models', () => {
98+
const serializer = new Serializer()
99+
const block = createAgentBlock('vllm/mistral-7b')
100+
101+
expect(() => {
102+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
103+
}).not.toThrow()
104+
})
105+
106+
it.concurrent('should require API key for OpenAI models', () => {
107+
const serializer = new Serializer()
108+
const block = createAgentBlock('gpt-4o')
109+
110+
expect(() => {
111+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
112+
}).toThrow('Agent is missing required fields: API Key')
113+
})
114+
115+
it.concurrent('should require API key for Anthropic models', () => {
116+
const serializer = new Serializer()
117+
const block = createAgentBlock('claude-sonnet-4-5')
118+
119+
expect(() => {
120+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
121+
}).toThrow('Agent is missing required fields: API Key')
122+
})
123+
124+
it.concurrent('should require API key for Google models', () => {
125+
const serializer = new Serializer()
126+
const block = createAgentBlock('gemini-2.0-flash')
127+
128+
expect(() => {
129+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
130+
}).toThrow('Agent is missing required fields: API Key')
131+
})
132+
133+
it.concurrent('should pass validation when API key is provided for cloud provider', () => {
134+
const serializer = new Serializer()
135+
const block = createAgentBlock('gpt-4o', 'sk-test-key')
136+
137+
expect(() => {
138+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
139+
}).not.toThrow()
140+
})
141+
142+
it.concurrent('should pass validation for Ollama even with empty string API key', () => {
143+
const serializer = new Serializer()
144+
const block = createAgentBlock('llama3.2:1b', '')
145+
146+
expect(() => {
147+
serializer.serializeWorkflow({ 'agent-1': block }, [], {}, undefined, true)
148+
}).not.toThrow()
149+
})
150+
})

0 commit comments

Comments
 (0)