From 8698db33fa38e9559219c71141f8a922b5cab54e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 00:28:11 -0700 Subject: [PATCH 1/8] improvement(mothership): agent model dropdown validations, recommendation system --- .../workflow/edit-workflow/validation.test.ts | 121 +++++++++++++++++- .../workflow/edit-workflow/validation.ts | 35 ++++- apps/sim/lib/copilot/vfs/serializers.ts | 63 ++++++++- apps/sim/providers/models.ts | 82 ++++++++++++ 4 files changed, 288 insertions(+), 13 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index c5085517182..df7850f5fbe 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts @@ -4,8 +4,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { normalizeConditionRouterIds } from './builders' -const { mockValidateSelectorIds } = vi.hoisted(() => ({ +const { mockValidateSelectorIds, mockGetModelOptions } = vi.hoisted(() => ({ mockValidateSelectorIds: vi.fn(), + mockGetModelOptions: vi.fn(() => []), })) const conditionBlockConfig = { @@ -26,7 +27,24 @@ const routerBlockConfig = { type: 'router_v2', name: 'Router', outputs: {}, - subBlocks: [{ id: 'routes', type: 'router-input' }], + subBlocks: [ + { id: 'routes', type: 'router-input' }, + { id: 'model', type: 'combobox', options: mockGetModelOptions }, + ], +} + +const agentBlockConfig = { + type: 'agent', + name: 'Agent', + outputs: {}, + subBlocks: [{ id: 'model', type: 'combobox', options: mockGetModelOptions }], +} + +const huggingfaceBlockConfig = { + type: 'huggingface', + name: 'HuggingFace', + outputs: {}, + subBlocks: [{ id: 'model', type: 'short-input' }], } vi.mock('@/blocks/registry', () => ({ @@ -37,7 +55,15 @@ vi.mock('@/blocks/registry', () => ({ ? oauthBlockConfig : type === 'router_v2' ? routerBlockConfig - : undefined, + : type === 'agent' + ? agentBlockConfig + : type === 'huggingface' + ? huggingfaceBlockConfig + : undefined, +})) + +vi.mock('@/blocks/utils', () => ({ + getModelOptions: mockGetModelOptions, })) vi.mock('@/lib/copilot/validation/selector-validator', () => ({ @@ -83,6 +109,95 @@ describe('validateInputsForBlock', () => { expect(result.errors).toHaveLength(1) expect(result.errors[0]?.error).toContain('expected a JSON array') }) + + it('accepts known agent model ids', () => { + const result = validateInputsForBlock('agent', { model: 'claude-sonnet-4-6' }, 'agent-1') + + expect(result.errors).toHaveLength(0) + expect(result.validInputs.model).toBe('claude-sonnet-4-6') + }) + + it('rejects hallucinated agent model ids that match a static provider pattern', () => { + const result = validateInputsForBlock('agent', { model: 'claude-sonnet-4.6' }, 'agent-1') + + expect(result.validInputs.model).toBeUndefined() + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.field).toBe('model') + expect(result.errors[0]?.error).toContain('Unknown model id') + expect(result.errors[0]?.error).toContain('claude-sonnet-4-6') + }) + + it('rejects legacy claude-4.5-haiku style ids', () => { + const result = validateInputsForBlock('agent', { model: 'claude-4.5-haiku' }, 'agent-1') + + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.error).toContain('Unknown model id') + }) + + it('allows empty model values', () => { + const result = validateInputsForBlock('agent', { model: '' }, 'agent-1') + + expect(result.errors).toHaveLength(0) + expect(result.validInputs.model).toBe('') + }) + + it('allows custom ollama-prefixed model ids', () => { + const result = validateInputsForBlock('agent', { model: 'ollama/my-private-model' }, 'agent-1') + + expect(result.errors).toHaveLength(0) + expect(result.validInputs.model).toBe('ollama/my-private-model') + }) + + it('validates the model field on router_v2 blocks too', () => { + const valid = validateInputsForBlock('router_v2', { model: 'claude-sonnet-4-6' }, 'router-1') + expect(valid.errors).toHaveLength(0) + expect(valid.validInputs.model).toBe('claude-sonnet-4-6') + + const invalid = validateInputsForBlock('router_v2', { model: 'claude-sonnet-4.6' }, 'router-1') + expect(invalid.validInputs.model).toBeUndefined() + expect(invalid.errors).toHaveLength(1) + expect(invalid.errors[0]?.blockType).toBe('router_v2') + expect(invalid.errors[0]?.field).toBe('model') + expect(invalid.errors[0]?.error).toContain('Unknown model id') + }) + + it("does not apply model validation to blocks whose model field is not Sim's catalog", () => { + const result = validateInputsForBlock( + 'huggingface', + { model: 'mistralai/Mistral-7B-Instruct-v0.3' }, + 'hf-1' + ) + + expect(result.errors).toHaveLength(0) + expect(result.validInputs.model).toBe('mistralai/Mistral-7B-Instruct-v0.3') + }) + + it('accepts date-suffixed variants of known Anthropic ids', () => { + const result = validateInputsForBlock( + 'agent', + { model: 'claude-sonnet-4-5-20250929' }, + 'agent-1' + ) + + expect(result.errors).toHaveLength(0) + expect(result.validInputs.model).toBe('claude-sonnet-4-5-20250929') + }) + + it('trims whitespace around catalog model ids and stores the trimmed value', () => { + const result = validateInputsForBlock('agent', { model: ' gpt-5.4 ' }, 'agent-1') + + expect(result.errors).toHaveLength(0) + expect(result.validInputs.model).toBe('gpt-5.4') + }) + + it('rejects a pattern-matching but uncataloged id even with surrounding whitespace', () => { + const result = validateInputsForBlock('agent', { model: ' gpt-100-ultra ' }, 'agent-1') + + expect(result.validInputs.model).toBeUndefined() + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.error).toContain('gpt-100-ultra') + expect(result.errors[0]?.error).not.toMatch(/\s{2,}/) + }) }) describe('normalizeConditionRouterIds', () => { diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index efab8a24b3e..57c72b3aa8c 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -3,7 +3,9 @@ import { validateSelectorIds } from '@/lib/copilot/validation/selector-validator import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { getBlock } from '@/blocks/registry' import type { SubBlockConfig } from '@/blocks/types' +import { getModelOptions } from '@/blocks/utils' import { EDGE, normalizeName } from '@/executor/constants' +import { isKnownModelId, suggestModelIdsForUnknownModel } from '@/providers/models' import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' import type { EdgeHandleValidationResult, @@ -350,12 +352,37 @@ export function validateValueForSubBlockType( case 'short-input': case 'long-input': case 'combobox': { - // Should be string (combobox allows custom values) + let stringValue: string if (typeof value !== 'string' && typeof value !== 'number') { - // Convert to string but don't error - return { valid: true, value: String(value) } + stringValue = String(value) + } else { + stringValue = typeof value === 'number' ? String(value) : value } - return { valid: true, value } + + const usesProviderCatalog = + fieldName === 'model' && subBlockConfig.options === getModelOptions + + if (usesProviderCatalog) { + const trimmed = stringValue.trim() + if (trimmed !== '' && !isKnownModelId(trimmed)) { + const suggestions = suggestModelIdsForUnknownModel(trimmed) + const suggestionText = + suggestions.length > 0 ? ` Valid options include: ${suggestions.join(', ')}.` : '' + return { + valid: false, + error: { + blockId, + blockType, + field: fieldName, + value, + error: `Unknown model id "${trimmed}" for block "${blockType}". Read components/blocks/${blockType}.json (the model.options array) for valid ids; prefer entries with recommended: true and avoid deprecated: true.${suggestionText}`, + }, + } + } + return { valid: true, value: trimmed } + } + + return { valid: true, value: typeof value === 'string' ? value : stringValue } } // Selector types - allow strings (IDs) or arrays of strings diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index e0c05768800..d947e48cdef 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -320,24 +320,75 @@ export function serializeTableMeta(table: { * Excludes dynamic providers (ollama, vllm, openrouter) whose models are user-configured. * Includes provider ID and whether the model is hosted by Sim (no API key required). */ -function getStaticModelOptionsForVFS(): Array<{ +interface StaticModelOption { id: string provider: string hosted: boolean -}> { + recommended?: boolean + speedOptimized?: boolean + deprecated?: boolean +} + +interface TierFlags { + recommended?: boolean + speedOptimized?: boolean + deprecated?: boolean +} + +const RESELLER_BASE_PREFIX: Record = { + 'azure-openai': 'azure/', + 'azure-anthropic': 'azure-anthropic/', + vertex: 'vertex/', +} + +const DYNAMIC_PROVIDERS_NOTE = { + note: 'The options array above lists Sim\'s static provider catalog. These four providers also accept user-configured models that are NOT enumerated here: the user may have additional ids available at runtime. Any model id prefixed with one of the slashes below is accepted by the server, as is any bare id that does not match a static provider pattern (typically a local Ollama tag like "llama3.1:8b"). The UI dropdown shows the user\'s actual installed models; if the user references one by name, use that id verbatim.', + prefixes: ['ollama/', 'vllm/', 'openrouter/', 'fireworks/'], +} as const + +function getStaticModelOptionsForVFS(): StaticModelOption[] { const hostedProviders = new Set(['openai', 'anthropic', 'google']) const dynamicProviders = new Set(['ollama', 'vllm', 'openrouter', 'fireworks']) - const models: Array<{ id: string; provider: string; hosted: boolean }> = [] + const baseTierFlags = new Map() + for (const providerId of hostedProviders) { + const def = PROVIDER_DEFINITIONS[providerId] + if (!def) continue + for (const model of def.models) { + if (model.recommended || model.speedOptimized || model.deprecated) { + baseTierFlags.set(model.id, { + ...(model.recommended && { recommended: true }), + ...(model.speedOptimized && { speedOptimized: true }), + ...(model.deprecated && { deprecated: true }), + }) + } + } + } + + const models: StaticModelOption[] = [] for (const [providerId, def] of Object.entries(PROVIDER_DEFINITIONS)) { if (dynamicProviders.has(providerId)) continue for (const model of def.models) { - models.push({ + const option: StaticModelOption = { id: model.id, provider: providerId, hosted: hostedProviders.has(providerId), - }) + } + if (model.recommended) option.recommended = true + if (model.speedOptimized) option.speedOptimized = true + if (model.deprecated) option.deprecated = true + + if (!option.recommended && !option.speedOptimized && !option.deprecated) { + const prefix = RESELLER_BASE_PREFIX[providerId] + if (prefix && model.id.startsWith(prefix)) { + const baseId = model.id.slice(prefix.length) + const inherited = baseTierFlags.get(baseId) + if (inherited) Object.assign(option, inherited) + } + } + + models.push(option) } } @@ -378,9 +429,9 @@ export function serializeBlockSchema(block: BlockConfig): string { .map((sb) => { const serialized = serializeSubBlock(sb) - // For model comboboxes with function options, inject static model data with hosting info if (sb.id === 'model' && sb.type === 'combobox' && typeof sb.options === 'function') { serialized.options = getStaticModelOptionsForVFS() + serialized.dynamicProviders = DYNAMIC_PROVIDERS_NOTE } return serialized diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 3d8cd0f8cc6..a1bdb2689ac 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -59,6 +59,9 @@ export interface ModelDefinition { contextWindow?: number /** ISO date string (YYYY-MM-DD) when the model was first publicly released */ releaseDate?: string + recommended?: boolean + speedOptimized?: boolean + deprecated?: boolean } export interface ProviderDefinition { @@ -216,6 +219,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 1050000, releaseDate: '2026-03-05', + recommended: true, }, { id: 'gpt-5.4-mini', @@ -256,6 +260,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 400000, releaseDate: '2026-03-17', + speedOptimized: true, }, // GPT-5.2 family { @@ -504,6 +509,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 128000, releaseDate: '2024-05-13', + deprecated: true, }, ], }, @@ -537,6 +543,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 1000000, releaseDate: '2026-04-16', + recommended: true, }, { id: 'claude-opus-4-6', @@ -577,6 +584,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 1000000, releaseDate: '2026-02-17', + recommended: true, }, { id: 'claude-opus-4-5', @@ -695,6 +703,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 200000, releaseDate: '2025-10-15', + speedOptimized: true, }, { id: 'claude-3-haiku-20240307', @@ -710,6 +719,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 200000, releaseDate: '2024-03-07', + deprecated: true, }, ], }, @@ -1170,6 +1180,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 1048576, releaseDate: '2026-02-19', + recommended: true, }, { id: 'gemini-3.1-flash-lite-preview', @@ -1253,6 +1264,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 1048576, releaseDate: '2025-06-17', + speedOptimized: true, }, { id: 'gemini-2.0-flash', @@ -2697,6 +2709,76 @@ export function getProviderModels(providerId: string): string[] { return PROVIDER_DEFINITIONS[providerId]?.models.map((m) => m.id) || [] } +const DYNAMIC_MODEL_PROVIDERS = ['ollama', 'vllm', 'openrouter', 'fireworks'] as const + +export function isKnownModelId(modelId: string): boolean { + if (!modelId || typeof modelId !== 'string') return false + const trimmed = modelId.trim() + if (!trimmed) return false + const normalized = trimmed.toLowerCase() + + for (const provider of Object.values(PROVIDER_DEFINITIONS)) { + for (const model of provider.models) { + const base = model.id.toLowerCase() + if (normalized === base || normalized.startsWith(`${base}-`)) return true + } + } + + const dynamicPrefixes = [/^ollama\//i, /^vllm\//i, /^openrouter\//i, /^fireworks\//i] + if (dynamicPrefixes.some((re) => re.test(trimmed))) return true + + for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { + if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue + if (provider.modelPatterns?.some((re) => re.test(normalized))) { + return false + } + } + + return true +} + +export function getRecommendedModels(): string[] { + const models: string[] = [] + for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { + if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue + for (const model of provider.models) { + if (model.recommended) models.push(model.id) + } + } + return models +} + +export function suggestModelIdsForUnknownModel(modelId: string, limit = 5): string[] { + if (!modelId || typeof modelId !== 'string') return [] + const normalized = modelId.trim().toLowerCase() + if (!normalized) return [] + + for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { + if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue + if (provider.modelPatterns?.some((re) => re.test(normalized))) { + const recommendedFirst = [...provider.models].sort((a, b) => { + if (a.deprecated && !b.deprecated) return 1 + if (!a.deprecated && b.deprecated) return -1 + if (a.recommended && !b.recommended) return -1 + if (!a.recommended && b.recommended) return 1 + return 0 + }) + return recommendedFirst.map((m) => m.id).slice(0, limit) + } + } + + const recommended = getRecommendedModels() + if (recommended.length > 0) return recommended.slice(0, limit) + + return [ + PROVIDER_DEFINITIONS.anthropic.defaultModel, + PROVIDER_DEFINITIONS.openai.defaultModel, + PROVIDER_DEFINITIONS.google.defaultModel, + ] + .filter(Boolean) + .slice(0, limit) +} + export function getBaseModelProviders(): Record { return Object.entries(PROVIDER_DEFINITIONS) .filter(([providerId]) => !['ollama', 'vllm', 'openrouter'].includes(providerId)) From 87257dc1c27d80bb68983a87da096a807f0c7f25 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 00:32:26 -0700 Subject: [PATCH 2/8] mark a few more models: --- apps/sim/providers/models.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index a1bdb2689ac..6b6a28ba37f 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -2315,6 +2315,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 200000, releaseDate: '2025-09-29', + recommended: true, }, { id: 'bedrock/anthropic.claude-haiku-4-5-20251001-v1:0', @@ -2330,6 +2331,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 200000, releaseDate: '2025-10-15', + speedOptimized: true, }, { id: 'bedrock/anthropic.claude-opus-4-1-20250805-v1:0', From 795670642b236dd79c99cd0b1f3edf756fdd968c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 10:19:44 -0700 Subject: [PATCH 3/8] remove regex based checks' --- .../workflow/edit-workflow/validation.test.ts | 16 ++++-- .../workflow/edit-workflow/validation.ts | 2 +- apps/sim/lib/copilot/vfs/serializers.ts | 6 +-- apps/sim/providers/models.ts | 54 +++++++------------ 4 files changed, 35 insertions(+), 43 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index df7850f5fbe..aa76cff4e51 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts @@ -172,15 +172,25 @@ describe('validateInputsForBlock', () => { expect(result.validInputs.model).toBe('mistralai/Mistral-7B-Instruct-v0.3') }) - it('accepts date-suffixed variants of known Anthropic ids', () => { + it('rejects a bare Ollama-style tag without the provider prefix', () => { + const result = validateInputsForBlock('agent', { model: 'llama3.1:8b' }, 'agent-1') + + expect(result.validInputs.model).toBeUndefined() + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.error).toContain('Unknown model id') + expect(result.errors[0]?.error).toContain('ollama/') + }) + + it('rejects date-pinned ids that are not literally in the catalog', () => { const result = validateInputsForBlock( 'agent', { model: 'claude-sonnet-4-5-20250929' }, 'agent-1' ) - expect(result.errors).toHaveLength(0) - expect(result.validInputs.model).toBe('claude-sonnet-4-5-20250929') + expect(result.validInputs.model).toBeUndefined() + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.error).toContain('Unknown model id') }) it('trims whitespace around catalog model ids and stores the trimmed value', () => { diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index 57c72b3aa8c..2fdf2ca92b8 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -375,7 +375,7 @@ export function validateValueForSubBlockType( blockType, field: fieldName, value, - error: `Unknown model id "${trimmed}" for block "${blockType}". Read components/blocks/${blockType}.json (the model.options array) for valid ids; prefer entries with recommended: true and avoid deprecated: true.${suggestionText}`, + error: `Unknown model id "${trimmed}" for block "${blockType}". Read components/blocks/${blockType}.json (the model.options array) for valid ids; prefer entries with recommended: true and avoid deprecated: true. For user-configured models (Ollama, vLLM, OpenRouter, Fireworks), prefix the id with the provider slash, e.g. "ollama/llama3.1:8b".${suggestionText}`, }, } } diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index d947e48cdef..dd616ed6dd4 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -2,7 +2,7 @@ import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import { isHosted } from '@/lib/core/config/feature-flags' import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' -import { PROVIDER_DEFINITIONS } from '@/providers/models' +import { DYNAMIC_MODEL_PROVIDERS, PROVIDER_DEFINITIONS } from '@/providers/models' import type { ToolConfig } from '@/tools/types' /** @@ -342,8 +342,8 @@ const RESELLER_BASE_PREFIX: Record = { } const DYNAMIC_PROVIDERS_NOTE = { - note: 'The options array above lists Sim\'s static provider catalog. These four providers also accept user-configured models that are NOT enumerated here: the user may have additional ids available at runtime. Any model id prefixed with one of the slashes below is accepted by the server, as is any bare id that does not match a static provider pattern (typically a local Ollama tag like "llama3.1:8b"). The UI dropdown shows the user\'s actual installed models; if the user references one by name, use that id verbatim.', - prefixes: ['ollama/', 'vllm/', 'openrouter/', 'fireworks/'], + note: 'The options array above lists Sim\'s static provider catalog. These providers also accept user-configured models that are NOT enumerated here: the user may have additional ids available at runtime. Any model id prefixed with one of the slashes below is accepted by the server, as is any bare id that does not match a static provider pattern (typically a local Ollama tag like "llama3.1:8b"). The UI dropdown shows the user\'s actual installed models; if the user references one by name, use that id verbatim.', + prefixes: DYNAMIC_MODEL_PROVIDERS.map((p) => `${p}/`), } as const function getStaticModelOptionsForVFS(): StaticModelOption[] { diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 6b6a28ba37f..637f4314dba 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -2711,32 +2711,32 @@ export function getProviderModels(providerId: string): string[] { return PROVIDER_DEFINITIONS[providerId]?.models.map((m) => m.id) || [] } -const DYNAMIC_MODEL_PROVIDERS = ['ollama', 'vllm', 'openrouter', 'fireworks'] as const +export const DYNAMIC_MODEL_PROVIDERS = ['ollama', 'vllm', 'openrouter', 'fireworks'] as const + +export function getAllStaticModelIds(): string[] { + const ids: string[] = [] + for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { + if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue + for (const model of provider.models) ids.push(model.id) + } + return ids +} + +const STATIC_MODEL_ID_SET = new Set(getAllStaticModelIds().map((id) => id.toLowerCase())) export function isKnownModelId(modelId: string): boolean { if (!modelId || typeof modelId !== 'string') return false const trimmed = modelId.trim() if (!trimmed) return false - const normalized = trimmed.toLowerCase() - - for (const provider of Object.values(PROVIDER_DEFINITIONS)) { - for (const model of provider.models) { - const base = model.id.toLowerCase() - if (normalized === base || normalized.startsWith(`${base}-`)) return true - } - } - const dynamicPrefixes = [/^ollama\//i, /^vllm\//i, /^openrouter\//i, /^fireworks\//i] - if (dynamicPrefixes.some((re) => re.test(trimmed))) return true + if (STATIC_MODEL_ID_SET.has(trimmed.toLowerCase())) return true - for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { - if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue - if (provider.modelPatterns?.some((re) => re.test(normalized))) { - return false - } + const lowered = trimmed.toLowerCase() + for (const provider of DYNAMIC_MODEL_PROVIDERS) { + if (lowered.startsWith(`${provider}/`)) return true } - return true + return false } export function getRecommendedModels(): string[] { @@ -2750,25 +2750,7 @@ export function getRecommendedModels(): string[] { return models } -export function suggestModelIdsForUnknownModel(modelId: string, limit = 5): string[] { - if (!modelId || typeof modelId !== 'string') return [] - const normalized = modelId.trim().toLowerCase() - if (!normalized) return [] - - for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { - if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue - if (provider.modelPatterns?.some((re) => re.test(normalized))) { - const recommendedFirst = [...provider.models].sort((a, b) => { - if (a.deprecated && !b.deprecated) return 1 - if (!a.deprecated && b.deprecated) return -1 - if (a.recommended && !b.recommended) return -1 - if (!a.recommended && b.recommended) return 1 - return 0 - }) - return recommendedFirst.map((m) => m.id).slice(0, limit) - } - } - +export function suggestModelIdsForUnknownModel(_modelId: string, limit = 5): string[] { const recommended = getRecommendedModels() if (recommended.length > 0) return recommended.slice(0, limit) From 8d37ecddf739471557a730087161d50c6f54a4f6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 10:32:17 -0700 Subject: [PATCH 4/8] remove dead code --- apps/sim/lib/copilot/vfs/serializers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index dd616ed6dd4..769f9292068 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -348,7 +348,7 @@ const DYNAMIC_PROVIDERS_NOTE = { function getStaticModelOptionsForVFS(): StaticModelOption[] { const hostedProviders = new Set(['openai', 'anthropic', 'google']) - const dynamicProviders = new Set(['ollama', 'vllm', 'openrouter', 'fireworks']) + const dynamicProviders = new Set(DYNAMIC_MODEL_PROVIDERS) const baseTierFlags = new Map() for (const providerId of hostedProviders) { From 223b137455d7668e8f136fbdf54deea949c85fce Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 10:37:25 -0700 Subject: [PATCH 5/8] remove inherited reseller flags --- apps/sim/lib/copilot/vfs/serializers.ts | 37 ------------------------- apps/sim/providers/models.ts | 2 -- 2 files changed, 39 deletions(-) diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 769f9292068..df0946e0361 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -329,18 +329,6 @@ interface StaticModelOption { deprecated?: boolean } -interface TierFlags { - recommended?: boolean - speedOptimized?: boolean - deprecated?: boolean -} - -const RESELLER_BASE_PREFIX: Record = { - 'azure-openai': 'azure/', - 'azure-anthropic': 'azure-anthropic/', - vertex: 'vertex/', -} - const DYNAMIC_PROVIDERS_NOTE = { note: 'The options array above lists Sim\'s static provider catalog. These providers also accept user-configured models that are NOT enumerated here: the user may have additional ids available at runtime. Any model id prefixed with one of the slashes below is accepted by the server, as is any bare id that does not match a static provider pattern (typically a local Ollama tag like "llama3.1:8b"). The UI dropdown shows the user\'s actual installed models; if the user references one by name, use that id verbatim.', prefixes: DYNAMIC_MODEL_PROVIDERS.map((p) => `${p}/`), @@ -350,21 +338,6 @@ function getStaticModelOptionsForVFS(): StaticModelOption[] { const hostedProviders = new Set(['openai', 'anthropic', 'google']) const dynamicProviders = new Set(DYNAMIC_MODEL_PROVIDERS) - const baseTierFlags = new Map() - for (const providerId of hostedProviders) { - const def = PROVIDER_DEFINITIONS[providerId] - if (!def) continue - for (const model of def.models) { - if (model.recommended || model.speedOptimized || model.deprecated) { - baseTierFlags.set(model.id, { - ...(model.recommended && { recommended: true }), - ...(model.speedOptimized && { speedOptimized: true }), - ...(model.deprecated && { deprecated: true }), - }) - } - } - } - const models: StaticModelOption[] = [] for (const [providerId, def] of Object.entries(PROVIDER_DEFINITIONS)) { @@ -378,16 +351,6 @@ function getStaticModelOptionsForVFS(): StaticModelOption[] { if (model.recommended) option.recommended = true if (model.speedOptimized) option.speedOptimized = true if (model.deprecated) option.deprecated = true - - if (!option.recommended && !option.speedOptimized && !option.deprecated) { - const prefix = RESELLER_BASE_PREFIX[providerId] - if (prefix && model.id.startsWith(prefix)) { - const baseId = model.id.slice(prefix.length) - const inherited = baseTierFlags.get(baseId) - if (inherited) Object.assign(option, inherited) - } - } - models.push(option) } } diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 637f4314dba..1bcc3a59d31 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -2315,7 +2315,6 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 200000, releaseDate: '2025-09-29', - recommended: true, }, { id: 'bedrock/anthropic.claude-haiku-4-5-20251001-v1:0', @@ -2331,7 +2330,6 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 200000, releaseDate: '2025-10-15', - speedOptimized: true, }, { id: 'bedrock/anthropic.claude-opus-4-1-20250805-v1:0', From 51409ba8875deef19acca34c9e762a513b2e859c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 10:53:30 -0700 Subject: [PATCH 6/8] fix note --- apps/sim/lib/copilot/vfs/serializers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index df0946e0361..4dc2d3458f1 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -330,7 +330,7 @@ interface StaticModelOption { } const DYNAMIC_PROVIDERS_NOTE = { - note: 'The options array above lists Sim\'s static provider catalog. These providers also accept user-configured models that are NOT enumerated here: the user may have additional ids available at runtime. Any model id prefixed with one of the slashes below is accepted by the server, as is any bare id that does not match a static provider pattern (typically a local Ollama tag like "llama3.1:8b"). The UI dropdown shows the user\'s actual installed models; if the user references one by name, use that id verbatim.', + note: 'The options array above lists Sim\'s static provider catalog. These providers also accept user-configured models that are NOT enumerated here: the user may have additional ids available at runtime (e.g. local Ollama tags). To reference one, prefix the model id with the provider slash below — for example "ollama/llama3.1:8b" instead of the bare "llama3.1:8b". The server rejects bare ids that are not in the catalog; always use the prefix for user-configured models.', prefixes: DYNAMIC_MODEL_PROVIDERS.map((p) => `${p}/`), } as const From df3b2ff3b7eef281da7d7f6f4c81b05c72373734 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 11:06:03 -0700 Subject: [PATCH 7/8] address bugbot comments --- .../workflow/edit-workflow/validation.ts | 18 ++++++++++-------- apps/sim/providers/models.ts | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index 2fdf2ca92b8..22b9526aab4 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -352,17 +352,16 @@ export function validateValueForSubBlockType( case 'short-input': case 'long-input': case 'combobox': { - let stringValue: string - if (typeof value !== 'string' && typeof value !== 'number') { - stringValue = String(value) - } else { - stringValue = typeof value === 'number' ? String(value) : value - } - const usesProviderCatalog = fieldName === 'model' && subBlockConfig.options === getModelOptions if (usesProviderCatalog) { + const stringValue = + typeof value === 'string' + ? value + : typeof value === 'number' + ? String(value) + : String(value) const trimmed = stringValue.trim() if (trimmed !== '' && !isKnownModelId(trimmed)) { const suggestions = suggestModelIdsForUnknownModel(trimmed) @@ -382,7 +381,10 @@ export function validateValueForSubBlockType( return { valid: true, value: trimmed } } - return { valid: true, value: typeof value === 'string' ? value : stringValue } + if (typeof value !== 'string' && typeof value !== 'number') { + return { valid: true, value: String(value) } + } + return { valid: true, value } } // Selector types - allow strings (IDs) or arrays of strings diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 1bcc3a59d31..0d265f620fd 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -2711,7 +2711,7 @@ export function getProviderModels(providerId: string): string[] { export const DYNAMIC_MODEL_PROVIDERS = ['ollama', 'vllm', 'openrouter', 'fireworks'] as const -export function getAllStaticModelIds(): string[] { +function getAllStaticModelIds(): string[] { const ids: string[] = [] for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue @@ -2737,7 +2737,7 @@ export function isKnownModelId(modelId: string): boolean { return false } -export function getRecommendedModels(): string[] { +function getRecommendedModels(): string[] { const models: string[] = [] for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue From de7390b88ab4bbc64fe88cfae44f48196f267ec3 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Apr 2026 18:50:25 -0700 Subject: [PATCH 8/8] code cleanup --- .../tools/server/workflow/edit-workflow/validation.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index 22b9526aab4..2c182cc839c 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -356,12 +356,7 @@ export function validateValueForSubBlockType( fieldName === 'model' && subBlockConfig.options === getModelOptions if (usesProviderCatalog) { - const stringValue = - typeof value === 'string' - ? value - : typeof value === 'number' - ? String(value) - : String(value) + const stringValue = typeof value === 'string' ? value : String(value) const trimmed = stringValue.trim() if (trimmed !== '' && !isKnownModelId(trimmed)) { const suggestions = suggestModelIdsForUnknownModel(trimmed)