From 3f855a1b8ebd53d605a465c325e908876249c09f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 5 Apr 2026 10:47:46 -0700 Subject: [PATCH 1/4] fix(blocks): resolve Ollama models incorrectly requiring API key in Docker Server-side validation failed for Ollama models like mistral:latest because the Zustand providers store is empty on the server and getProviderFromModel misidentified them via regex pattern matching (e.g. mistral:latest matched Mistral AI's /^mistral/ pattern). Replace the hardcoded CLOUD_PROVIDER_PREFIXES list with existing data sources: - Provider store (definitive on client, checks all provider buckets) - getBaseModelProviders() from PROVIDER_DEFINITIONS (server-side static cloud model lookup) - Slash convention for dynamic cloud providers (fireworks/, openrouter/, etc.) - isOllamaConfigured feature flag using existing OLLAMA_URL env var Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/utils.test.ts | 303 ++++++++++++++++++++++ apps/sim/blocks/utils.ts | 50 ++-- apps/sim/lib/core/config/feature-flags.ts | 8 + 3 files changed, 335 insertions(+), 26 deletions(-) create mode 100644 apps/sim/blocks/utils.test.ts diff --git a/apps/sim/blocks/utils.test.ts b/apps/sim/blocks/utils.test.ts new file mode 100644 index 0000000000..581a0dc4a7 --- /dev/null +++ b/apps/sim/blocks/utils.test.ts @@ -0,0 +1,303 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockIsHosted, mockIsAzureConfigured, mockIsOllamaConfigured } = vi.hoisted(() => ({ + mockIsHosted: { value: false }, + mockIsAzureConfigured: { value: false }, + mockIsOllamaConfigured: { value: false }, +})) + +const { + mockGetHostedModels, + mockGetProviderModels, + mockGetProviderIcon, + mockGetProviderFromModel, + mockGetBaseModelProviders, +} = vi.hoisted(() => ({ + mockGetHostedModels: vi.fn(() => []), + mockGetProviderModels: vi.fn(() => []), + mockGetProviderIcon: vi.fn(() => null), + mockGetProviderFromModel: vi.fn(() => 'ollama'), + mockGetBaseModelProviders: vi.fn(() => ({})), +})) + +const { mockProviders } = vi.hoisted(() => ({ + mockProviders: { + value: { + base: { models: [] as string[], isLoading: false }, + ollama: { models: [] as string[], isLoading: false }, + vllm: { models: [] as string[], isLoading: false }, + openrouter: { models: [] as string[], isLoading: false }, + fireworks: { models: [] as string[], isLoading: false }, + }, + }, +})) + +vi.mock('@/lib/core/config/feature-flags', () => ({ + get isHosted() { + return mockIsHosted.value + }, + get isAzureConfigured() { + return mockIsAzureConfigured.value + }, + get isOllamaConfigured() { + return mockIsOllamaConfigured.value + }, +})) + +vi.mock('@/providers/models', () => ({ + getHostedModels: mockGetHostedModels, + getProviderModels: mockGetProviderModels, + getProviderIcon: mockGetProviderIcon, + getProviderFromModel: mockGetProviderFromModel, + getBaseModelProviders: mockGetBaseModelProviders, +})) + +vi.mock('@/stores/providers/store', () => ({ + useProvidersStore: { + getState: () => ({ + get providers() { + return mockProviders.value + }, + }), + }, +})) + +vi.mock('@/lib/oauth/utils', () => ({ + getScopesForService: vi.fn(() => []), +})) + +import { getApiKeyCondition } from '@/blocks/utils' + +/** + * Simulates getProviderFromModel behavior: checks known prefix patterns, + * defaults to 'ollama' for unrecognized models (matching real implementation). + */ +function simulateGetProviderFromModel(model: string): string { + const m = model.toLowerCase() + if (m.startsWith('fireworks/')) return 'fireworks' + if (m.startsWith('openrouter/')) return 'openrouter' + if (m.startsWith('vllm/')) return 'vllm' + if (m.startsWith('vertex/')) return 'vertex' + if (m.startsWith('bedrock/')) return 'bedrock' + if (m.startsWith('azure/')) return 'azure-openai' + if (m.startsWith('azure-openai/')) return 'azure-openai' + if (m.startsWith('azure-anthropic/')) return 'azure-anthropic' + if (m.startsWith('groq/')) return 'groq' + if (m.startsWith('cerebras/')) return 'cerebras' + if (/^gpt/.test(m) || /^o\d/.test(m)) return 'openai' + if (/^claude/.test(m)) return 'anthropic' + if (/^gemini/.test(m)) return 'google' + if (/^grok/.test(m)) return 'xai' + if (/^mistral/.test(m) || /^magistral/.test(m)) return 'mistral' + return 'ollama' +} + +const BASE_CLOUD_MODELS: Record = { + 'gpt-4o': 'openai', + 'claude-sonnet-4-5': 'anthropic', + 'gemini-2.5-pro': 'google', + 'mistral-large-latest': 'mistral', +} + +describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { + const evaluateCondition = (model: string): boolean => { + const conditionFn = getApiKeyCondition() + const condition = conditionFn({ model }) + if ('not' in condition && condition.not) return false + if (condition.value === '__no_model_selected__') return false + return true + } + + beforeEach(() => { + vi.clearAllMocks() + mockIsHosted.value = false + mockIsAzureConfigured.value = false + mockIsOllamaConfigured.value = false + mockProviders.value = { + base: { models: [], isLoading: false }, + ollama: { models: [], isLoading: false }, + vllm: { models: [], isLoading: false }, + openrouter: { models: [], isLoading: false }, + fireworks: { models: [], isLoading: false }, + } + mockGetHostedModels.mockReturnValue([]) + mockGetProviderModels.mockReturnValue([]) + mockGetProviderFromModel.mockImplementation(simulateGetProviderFromModel) + mockGetBaseModelProviders.mockReturnValue({}) + }) + + describe('empty or missing model', () => { + it('does not require API key when model is empty', () => { + expect(evaluateCondition('')).toBe(false) + }) + + it('does not require API key when model is whitespace', () => { + expect(evaluateCondition(' ')).toBe(false) + }) + }) + + describe('hosted models', () => { + it('does not require API key for hosted models on hosted platform', () => { + mockIsHosted.value = true + mockGetHostedModels.mockReturnValue(['gpt-4o', 'claude-sonnet-4-5']) + expect(evaluateCondition('gpt-4o')).toBe(false) + expect(evaluateCondition('claude-sonnet-4-5')).toBe(false) + }) + + it('requires API key for non-hosted models on hosted platform', () => { + mockIsHosted.value = true + mockGetHostedModels.mockReturnValue(['gpt-4o']) + expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) + }) + }) + + describe('Vertex AI models', () => { + it('does not require API key for vertex/ prefixed models', () => { + expect(evaluateCondition('vertex/gemini-2.5-pro')).toBe(false) + }) + }) + + describe('Bedrock models', () => { + it('does not require API key for bedrock/ prefixed models', () => { + expect(evaluateCondition('bedrock/anthropic.claude-v2')).toBe(false) + }) + }) + + describe('Azure models', () => { + it('does not require API key for azure/ models when Azure is configured', () => { + mockIsAzureConfigured.value = true + expect(evaluateCondition('azure/gpt-4o')).toBe(false) + expect(evaluateCondition('azure-openai/gpt-4o')).toBe(false) + expect(evaluateCondition('azure-anthropic/claude-sonnet-4-5')).toBe(false) + }) + + it('requires API key for azure/ models when Azure is not configured', () => { + mockIsAzureConfigured.value = false + expect(evaluateCondition('azure/gpt-4o')).toBe(true) + }) + }) + + describe('vLLM models', () => { + it('does not require API key for vllm/ prefixed models', () => { + expect(evaluateCondition('vllm/my-model')).toBe(false) + expect(evaluateCondition('vllm/llama-3-70b')).toBe(false) + }) + }) + + describe('provider store lookup (client-side)', () => { + it('does not require API key when model is in the Ollama store bucket', () => { + mockProviders.value.ollama.models = ['llama3:latest', 'mistral:latest'] + expect(evaluateCondition('llama3:latest')).toBe(false) + expect(evaluateCondition('mistral:latest')).toBe(false) + }) + + it('requires API key when model is in the base store bucket', () => { + mockProviders.value.base.models = ['gpt-4o', 'claude-sonnet-4-5'] + expect(evaluateCondition('gpt-4o')).toBe(true) + expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) + }) + + it('requires API key when model is in the fireworks store bucket', () => { + mockProviders.value.fireworks.models = ['fireworks/llama-3'] + expect(evaluateCondition('fireworks/llama-3')).toBe(true) + }) + + it('requires API key when model is in the openrouter store bucket', () => { + mockProviders.value.openrouter.models = ['openrouter/anthropic/claude'] + expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true) + }) + + it('is case-insensitive for store lookup', () => { + mockProviders.value.ollama.models = ['Llama3:Latest'] + expect(evaluateCondition('llama3:latest')).toBe(false) + }) + }) + + describe('Ollama — OLLAMA_URL env var (server-safe)', () => { + it('does not require API key for unknown models when OLLAMA_URL is set', () => { + mockIsOllamaConfigured.value = true + expect(evaluateCondition('llama3:latest')).toBe(false) + expect(evaluateCondition('phi3:latest')).toBe(false) + expect(evaluateCondition('gemma2:latest')).toBe(false) + expect(evaluateCondition('deepseek-coder:latest')).toBe(false) + }) + + it('does not require API key for Ollama models that match cloud provider regex patterns', () => { + mockIsOllamaConfigured.value = true + expect(evaluateCondition('mistral:latest')).toBe(false) + expect(evaluateCondition('mistral')).toBe(false) + expect(evaluateCondition('mistral-nemo')).toBe(false) + expect(evaluateCondition('gpt2')).toBe(false) + }) + + it('requires API key for known cloud models even when OLLAMA_URL is set', () => { + mockIsOllamaConfigured.value = true + mockGetBaseModelProviders.mockReturnValue(BASE_CLOUD_MODELS) + expect(evaluateCondition('gpt-4o')).toBe(true) + expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) + expect(evaluateCondition('gemini-2.5-pro')).toBe(true) + expect(evaluateCondition('mistral-large-latest')).toBe(true) + }) + + it('requires API key for slash-prefixed cloud models when OLLAMA_URL is set', () => { + mockIsOllamaConfigured.value = true + expect(evaluateCondition('azure/gpt-4o')).toBe(true) + expect(evaluateCondition('fireworks/llama-3')).toBe(true) + expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true) + expect(evaluateCondition('groq/llama-3')).toBe(true) + }) + }) + + describe('cloud provider models that need API key', () => { + it('requires API key for standard cloud models on hosted platform', () => { + mockIsHosted.value = true + mockGetHostedModels.mockReturnValue([]) + expect(evaluateCondition('gpt-4o')).toBe(true) + expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) + expect(evaluateCondition('gemini-2.5-pro')).toBe(true) + expect(evaluateCondition('mistral-large-latest')).toBe(true) + }) + + it('requires API key for prefixed cloud models on hosted platform', () => { + mockIsHosted.value = true + expect(evaluateCondition('fireworks/llama-3')).toBe(true) + expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true) + expect(evaluateCondition('groq/llama-3')).toBe(true) + expect(evaluateCondition('cerebras/gpt-oss-120b')).toBe(true) + }) + + it('requires API key for prefixed cloud models on self-hosted', () => { + mockIsHosted.value = false + expect(evaluateCondition('fireworks/llama-3')).toBe(true) + expect(evaluateCondition('openrouter/anthropic/claude')).toBe(true) + expect(evaluateCondition('groq/llama-3')).toBe(true) + expect(evaluateCondition('cerebras/gpt-oss-120b')).toBe(true) + }) + }) + + describe('self-hosted getProviderFromModel fallback', () => { + it('does not require API key when getProviderFromModel defaults to ollama', () => { + mockIsHosted.value = false + mockIsOllamaConfigured.value = false + expect(evaluateCondition('llama3:latest')).toBe(false) + expect(evaluateCondition('phi3:latest')).toBe(false) + }) + + it('requires API key when getProviderFromModel returns a cloud provider', () => { + mockIsHosted.value = false + mockIsOllamaConfigured.value = false + expect(evaluateCondition('mistral:latest')).toBe(true) + expect(evaluateCondition('gpt2')).toBe(true) + }) + + it('does not run getProviderFromModel fallback on hosted platform', () => { + mockIsHosted.value = true + mockGetHostedModels.mockReturnValue([]) + expect(evaluateCondition('llama3:latest')).toBe(true) + expect(mockGetProviderFromModel).not.toHaveBeenCalled() + }) + }) +}) diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index f1f05efc56..c8b24142fa 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,7 +1,8 @@ -import { isAzureConfigured, isHosted } from '@/lib/core/config/feature-flags' +import { isAzureConfigured, isHosted, isOllamaConfigured } from '@/lib/core/config/feature-flags' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' import { + getBaseModelProviders, getHostedModels, getProviderFromModel, getProviderIcon, @@ -100,11 +101,15 @@ export function resolveOutputType( return resolvedOutputs } -/** - * Helper to get current Ollama models from store - */ -const getCurrentOllamaModels = () => { - return useProvidersStore.getState().providers.ollama.models +function getProviderFromStore(model: string): string | null { + const { providers } = useProvidersStore.getState() + const normalized = model.toLowerCase() + for (const [key, state] of Object.entries(providers)) { + if (state.models.some((m: string) => m.toLowerCase() === normalized)) { + return key + } + } + return null } function buildModelVisibilityCondition(model: string, shouldShow: boolean) { @@ -119,16 +124,14 @@ function shouldRequireApiKeyForModel(model: string): boolean { const normalizedModel = model.trim().toLowerCase() if (!normalizedModel) return false - const hostedModels = getHostedModels() - const isHostedModel = hostedModels.some( - (hostedModel) => hostedModel.toLowerCase() === normalizedModel - ) - if (isHosted && isHostedModel) return false + if (isHosted) { + const hostedModels = getHostedModels() + if (hostedModels.some((m) => m.toLowerCase() === normalizedModel)) return false + } if (normalizedModel.startsWith('vertex/') || normalizedModel.startsWith('bedrock/')) { return false } - if ( isAzureConfigured && (normalizedModel.startsWith('azure/') || @@ -138,30 +141,25 @@ function shouldRequireApiKeyForModel(model: string): boolean { ) { return false } - if (normalizedModel.startsWith('vllm/')) { return false } - const currentOllamaModels = getCurrentOllamaModels() - if (currentOllamaModels.some((ollamaModel) => ollamaModel.toLowerCase() === normalizedModel)) { + const storeProvider = getProviderFromStore(normalizedModel) + if (storeProvider === 'ollama') return false + if (storeProvider) return true + + if (isOllamaConfigured) { + if (normalizedModel.includes('/')) return true + if (normalizedModel in getBaseModelProviders()) return true return false } if (!isHosted) { try { const providerId = getProviderFromModel(model) - if ( - providerId === 'ollama' || - providerId === 'vllm' || - providerId === 'vertex' || - providerId === 'bedrock' - ) { - return false - } - } catch { - // If model resolution fails, fall through and require an API key. - } + if (['ollama', 'vllm', 'vertex', 'bedrock'].includes(providerId)) return false + } catch {} } return true diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index b688924afe..7aeb4c3307 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -122,6 +122,14 @@ export const isInboxEnabled = isTruthy(env.INBOX_ENABLED) */ export const isE2bEnabled = isTruthy(env.E2B_ENABLED) +/** + * Whether Ollama is configured (OLLAMA_URL is set). + * When true, models that are not in the static cloud model list and have no + * slash-prefixed provider namespace are assumed to be Ollama models + * and do not require an API key. + */ +export const isOllamaConfigured = Boolean(env.OLLAMA_URL) + /** * Whether Azure OpenAI / Azure Anthropic credentials are pre-configured at the server level * (via AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_ANTHROPIC_ENDPOINT, etc.). From 28f8186b2b318c676f12600bdd51a0ea06f13bdf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 5 Apr 2026 11:20:11 -0700 Subject: [PATCH 2/4] refactor: remove getProviderFromModel regex fallback from API key validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fallback was the last piece of regex-based matching in the function and only ran for self-hosted without OLLAMA_URL on the server — a path where Ollama models cannot appear in the dropdown anyway. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/utils.test.ts | 67 ++++++----------------------------- apps/sim/blocks/utils.ts | 8 ----- 2 files changed, 11 insertions(+), 64 deletions(-) diff --git a/apps/sim/blocks/utils.test.ts b/apps/sim/blocks/utils.test.ts index 581a0dc4a7..fc445aabc9 100644 --- a/apps/sim/blocks/utils.test.ts +++ b/apps/sim/blocks/utils.test.ts @@ -9,19 +9,13 @@ const { mockIsHosted, mockIsAzureConfigured, mockIsOllamaConfigured } = vi.hoist mockIsOllamaConfigured: { value: false }, })) -const { - mockGetHostedModels, - mockGetProviderModels, - mockGetProviderIcon, - mockGetProviderFromModel, - mockGetBaseModelProviders, -} = vi.hoisted(() => ({ - mockGetHostedModels: vi.fn(() => []), - mockGetProviderModels: vi.fn(() => []), - mockGetProviderIcon: vi.fn(() => null), - mockGetProviderFromModel: vi.fn(() => 'ollama'), - mockGetBaseModelProviders: vi.fn(() => ({})), -})) +const { mockGetHostedModels, mockGetProviderModels, mockGetProviderIcon, mockGetBaseModelProviders } = + vi.hoisted(() => ({ + mockGetHostedModels: vi.fn(() => []), + mockGetProviderModels: vi.fn(() => []), + mockGetProviderIcon: vi.fn(() => null), + mockGetBaseModelProviders: vi.fn(() => ({})), + })) const { mockProviders } = vi.hoisted(() => ({ mockProviders: { @@ -51,7 +45,6 @@ vi.mock('@/providers/models', () => ({ getHostedModels: mockGetHostedModels, getProviderModels: mockGetProviderModels, getProviderIcon: mockGetProviderIcon, - getProviderFromModel: mockGetProviderFromModel, getBaseModelProviders: mockGetBaseModelProviders, })) @@ -71,30 +64,6 @@ vi.mock('@/lib/oauth/utils', () => ({ import { getApiKeyCondition } from '@/blocks/utils' -/** - * Simulates getProviderFromModel behavior: checks known prefix patterns, - * defaults to 'ollama' for unrecognized models (matching real implementation). - */ -function simulateGetProviderFromModel(model: string): string { - const m = model.toLowerCase() - if (m.startsWith('fireworks/')) return 'fireworks' - if (m.startsWith('openrouter/')) return 'openrouter' - if (m.startsWith('vllm/')) return 'vllm' - if (m.startsWith('vertex/')) return 'vertex' - if (m.startsWith('bedrock/')) return 'bedrock' - if (m.startsWith('azure/')) return 'azure-openai' - if (m.startsWith('azure-openai/')) return 'azure-openai' - if (m.startsWith('azure-anthropic/')) return 'azure-anthropic' - if (m.startsWith('groq/')) return 'groq' - if (m.startsWith('cerebras/')) return 'cerebras' - if (/^gpt/.test(m) || /^o\d/.test(m)) return 'openai' - if (/^claude/.test(m)) return 'anthropic' - if (/^gemini/.test(m)) return 'google' - if (/^grok/.test(m)) return 'xai' - if (/^mistral/.test(m) || /^magistral/.test(m)) return 'mistral' - return 'ollama' -} - const BASE_CLOUD_MODELS: Record = { 'gpt-4o': 'openai', 'claude-sonnet-4-5': 'anthropic', @@ -125,7 +94,6 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { } mockGetHostedModels.mockReturnValue([]) mockGetProviderModels.mockReturnValue([]) - mockGetProviderFromModel.mockImplementation(simulateGetProviderFromModel) mockGetBaseModelProviders.mockReturnValue({}) }) @@ -278,26 +246,13 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { }) }) - describe('self-hosted getProviderFromModel fallback', () => { - it('does not require API key when getProviderFromModel defaults to ollama', () => { - mockIsHosted.value = false - mockIsOllamaConfigured.value = false - expect(evaluateCondition('llama3:latest')).toBe(false) - expect(evaluateCondition('phi3:latest')).toBe(false) - }) - - it('requires API key when getProviderFromModel returns a cloud provider', () => { + describe('self-hosted without OLLAMA_URL', () => { + it('requires API key for any model (Ollama models cannot appear without OLLAMA_URL)', () => { mockIsHosted.value = false mockIsOllamaConfigured.value = false - expect(evaluateCondition('mistral:latest')).toBe(true) - expect(evaluateCondition('gpt2')).toBe(true) - }) - - it('does not run getProviderFromModel fallback on hosted platform', () => { - mockIsHosted.value = true - mockGetHostedModels.mockReturnValue([]) expect(evaluateCondition('llama3:latest')).toBe(true) - expect(mockGetProviderFromModel).not.toHaveBeenCalled() + expect(evaluateCondition('mistral:latest')).toBe(true) + expect(evaluateCondition('gpt-4o')).toBe(true) }) }) }) diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index c8b24142fa..609bca5f64 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -4,7 +4,6 @@ import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/block import { getBaseModelProviders, getHostedModels, - getProviderFromModel, getProviderIcon, getProviderModels, } from '@/providers/models' @@ -155,13 +154,6 @@ function shouldRequireApiKeyForModel(model: string): boolean { return false } - if (!isHosted) { - try { - const providerId = getProviderFromModel(model) - if (['ollama', 'vllm', 'vertex', 'bedrock'].includes(providerId)) return false - } catch {} - } - return true } From adfdf8f9606541561e052a4f300f5240a1d669a8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 5 Apr 2026 11:21:23 -0700 Subject: [PATCH 3/4] lint --- apps/sim/blocks/utils.test.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/sim/blocks/utils.test.ts b/apps/sim/blocks/utils.test.ts index fc445aabc9..be3ab81a42 100644 --- a/apps/sim/blocks/utils.test.ts +++ b/apps/sim/blocks/utils.test.ts @@ -9,13 +9,17 @@ const { mockIsHosted, mockIsAzureConfigured, mockIsOllamaConfigured } = vi.hoist mockIsOllamaConfigured: { value: false }, })) -const { mockGetHostedModels, mockGetProviderModels, mockGetProviderIcon, mockGetBaseModelProviders } = - vi.hoisted(() => ({ - mockGetHostedModels: vi.fn(() => []), - mockGetProviderModels: vi.fn(() => []), - mockGetProviderIcon: vi.fn(() => null), - mockGetBaseModelProviders: vi.fn(() => ({})), - })) +const { + mockGetHostedModels, + mockGetProviderModels, + mockGetProviderIcon, + mockGetBaseModelProviders, +} = vi.hoisted(() => ({ + mockGetHostedModels: vi.fn(() => []), + mockGetProviderModels: vi.fn(() => []), + mockGetProviderIcon: vi.fn(() => null), + mockGetBaseModelProviders: vi.fn(() => ({})), +})) const { mockProviders } = vi.hoisted(() => ({ mockProviders: { From 82251158fbb748a2a1496739a070713a04c3d4b3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 5 Apr 2026 11:39:29 -0700 Subject: [PATCH 4/4] fix: handle vLLM models in store provider check vLLM is a local model server like Ollama and should not require an API key. Add vllm to the store provider check as a safety net for models that may not have the vllm/ prefix. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/utils.test.ts | 5 +++++ apps/sim/blocks/utils.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/utils.test.ts b/apps/sim/blocks/utils.test.ts index be3ab81a42..0e147a3c7f 100644 --- a/apps/sim/blocks/utils.test.ts +++ b/apps/sim/blocks/utils.test.ts @@ -172,6 +172,11 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => { expect(evaluateCondition('claude-sonnet-4-5')).toBe(true) }) + it('does not require API key when model is in the vLLM store bucket', () => { + mockProviders.value.vllm.models = ['my-custom-model'] + expect(evaluateCondition('my-custom-model')).toBe(false) + }) + it('requires API key when model is in the fireworks store bucket', () => { mockProviders.value.fireworks.models = ['fireworks/llama-3'] expect(evaluateCondition('fireworks/llama-3')).toBe(true) diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 609bca5f64..a31bd10c72 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -145,7 +145,7 @@ function shouldRequireApiKeyForModel(model: string): boolean { } const storeProvider = getProviderFromStore(normalizedModel) - if (storeProvider === 'ollama') return false + if (storeProvider === 'ollama' || storeProvider === 'vllm') return false if (storeProvider) return true if (isOllamaConfigured) {