Skip to content

Commit f09f6d2

Browse files
feat(router): support custom model overrides
1 parent 1cd315a commit f09f6d2

13 files changed

Lines changed: 511 additions & 106 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { RouterBlock, RouterV2Block } from '@/blocks/blocks/router'
3+
import type { BlockConfig } from '@/blocks/types'
4+
import { CUSTOM_MODEL_ID } from '@/providers/custom-model'
5+
6+
describe.each([
7+
['legacy Router', RouterBlock],
8+
['Router V2', RouterV2Block],
9+
] as const)('%s custom model configuration', (_label, block: BlockConfig) => {
10+
it('exposes the Super User custom option and JSON configuration', () => {
11+
const modelIndex = block.subBlocks.findIndex((subBlock) => subBlock.id === 'model')
12+
const modelOptions = block.subBlocks[modelIndex].options
13+
const evaluatedModelOptions = typeof modelOptions === 'function' ? modelOptions() : modelOptions
14+
15+
expect(evaluatedModelOptions).toContainEqual(
16+
expect.objectContaining({ id: CUSTOM_MODEL_ID, requiresSuperUser: true })
17+
)
18+
expect(block.subBlocks[modelIndex + 1]).toEqual(
19+
expect.objectContaining({
20+
id: 'customModelConfig',
21+
type: 'code',
22+
language: 'json',
23+
superUserOnly: true,
24+
condition: { field: 'model', value: CUSTOM_MODEL_ID },
25+
required: { field: 'model', value: CUSTOM_MODEL_ID },
26+
})
27+
)
28+
expect(block.inputs.customModelConfig?.schema?.properties.provider.enum).toContain('sim')
29+
})
30+
31+
it('serializes the provider selected by the custom configuration', () => {
32+
expect(
33+
block.tools.config?.tool?.({
34+
model: CUSTOM_MODEL_ID,
35+
customModelConfig: {
36+
provider: 'fireworks',
37+
model: 'accounts/fireworks/models/kimi-k3',
38+
},
39+
} as never)
40+
).toBe('fireworks')
41+
})
42+
43+
it('serializes custom Sim Auto with the fallback provider shape', () => {
44+
expect(
45+
block.tools.config?.tool?.({
46+
model: CUSTOM_MODEL_ID,
47+
customModelConfig: {
48+
provider: 'sim',
49+
model: 'sim-auto',
50+
},
51+
} as never)
52+
).toBe('anthropic')
53+
})
54+
})

apps/sim/blocks/blocks/router.ts

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { ConnectIcon } from '@/components/icons'
2-
import { AuthMode, type BlockConfig } from '@/blocks/types'
2+
import { AuthMode, type BlockConfig, type SubBlockConfig } from '@/blocks/types'
33
import {
4-
getModelOptions,
4+
getCustomModelOptions,
55
getProviderCredentialSubBlocks,
66
PROVIDER_CREDENTIAL_INPUTS,
77
} from '@/blocks/utils'
8-
import { getBaseModelProviders } from '@/providers/models'
8+
import {
9+
CUSTOM_MODEL_CONFIG_DEFAULT,
10+
CUSTOM_MODEL_CONFIG_JSON_SCHEMA,
11+
CUSTOM_MODEL_ID,
12+
isCustomModel,
13+
parseCustomModelConfig,
14+
} from '@/providers/custom-model'
15+
import { getBaseModelProviders, isAutoModel } from '@/providers/models'
916
import type { ProviderId } from '@/providers/types'
1017
import type { ToolResponse } from '@/tools/types'
1118

@@ -41,6 +48,47 @@ interface TargetBlock {
4148
currentState?: any
4249
}
4350

51+
function getCustomModelSubBlock(): SubBlockConfig {
52+
return {
53+
id: 'customModelConfig',
54+
title: 'Custom Model Configuration',
55+
type: 'code',
56+
language: 'json',
57+
placeholder: 'Enter custom provider and model configuration...',
58+
description:
59+
'Provider, model, credentials, and provider-specific generation settings for routing inference. Use provider "sim" with model "sim-auto" for automatic routing.',
60+
defaultValue: CUSTOM_MODEL_CONFIG_DEFAULT,
61+
superUserOnly: true,
62+
required: {
63+
field: 'model',
64+
value: CUSTOM_MODEL_ID,
65+
},
66+
condition: {
67+
field: 'model',
68+
value: CUSTOM_MODEL_ID,
69+
},
70+
}
71+
}
72+
73+
function resolveRouterTool(params: Record<string, any>): string {
74+
const model = params.model || 'claude-sonnet-5'
75+
if (!model) {
76+
throw new Error('No model selected')
77+
}
78+
if (isCustomModel(model)) {
79+
const customConfig = parseCustomModelConfig(params.customModelConfig)
80+
if (customConfig.provider !== 'sim') return customConfig.provider
81+
82+
return getBaseModelProviders()['claude-sonnet-5']
83+
}
84+
const lookupModel = isAutoModel(model) ? 'claude-sonnet-5' : model
85+
const tool = getBaseModelProviders()[lookupModel as ProviderId]
86+
if (!tool) {
87+
throw new Error(`Invalid model selected: ${model}`)
88+
}
89+
return tool
90+
}
91+
4492
/**
4593
* Generates the system prompt for the legacy router (block-based).
4694
*/
@@ -173,8 +221,9 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
173221
placeholder: 'Type or select a model...',
174222
required: true,
175223
defaultValue: 'claude-sonnet-5',
176-
options: getModelOptions,
224+
options: getCustomModelOptions,
177225
},
226+
getCustomModelSubBlock(),
178227
...getProviderCredentialSubBlocks(),
179228
{
180229
id: 'temperature',
@@ -204,22 +253,17 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
204253
'deepseek_reasoner',
205254
],
206255
config: {
207-
tool: (params: Record<string, any>) => {
208-
const model = params.model || 'gpt-4o'
209-
if (!model) {
210-
throw new Error('No model selected')
211-
}
212-
const tool = getBaseModelProviders()[model as ProviderId]
213-
if (!tool) {
214-
throw new Error(`Invalid model selected: ${model}`)
215-
}
216-
return tool
217-
},
256+
tool: resolveRouterTool,
218257
},
219258
},
220259
inputs: {
221260
prompt: { type: 'string', description: 'Routing prompt content' },
222261
model: { type: 'string', description: 'AI model to use' },
262+
customModelConfig: {
263+
type: 'json',
264+
description: 'Custom provider/model execution configuration for routing inference.',
265+
schema: CUSTOM_MODEL_CONFIG_JSON_SCHEMA,
266+
},
223267
...PROVIDER_CREDENTIAL_INPUTS,
224268
temperature: {
225269
type: 'number',
@@ -300,8 +344,9 @@ export const RouterV2Block: BlockConfig<RouterV2Response> = {
300344
placeholder: 'Type or select a model...',
301345
required: true,
302346
defaultValue: 'claude-sonnet-5',
303-
options: getModelOptions,
347+
options: getCustomModelOptions,
304348
},
349+
getCustomModelSubBlock(),
305350
...getProviderCredentialSubBlocks(),
306351
],
307352
tools: {
@@ -314,23 +359,18 @@ export const RouterV2Block: BlockConfig<RouterV2Response> = {
314359
'deepseek_reasoner',
315360
],
316361
config: {
317-
tool: (params: Record<string, any>) => {
318-
const model = params.model || 'gpt-4o'
319-
if (!model) {
320-
throw new Error('No model selected')
321-
}
322-
const tool = getBaseModelProviders()[model as ProviderId]
323-
if (!tool) {
324-
throw new Error(`Invalid model selected: ${model}`)
325-
}
326-
return tool
327-
},
362+
tool: resolveRouterTool,
328363
},
329364
},
330365
inputs: {
331366
context: { type: 'string', description: 'Context for routing decision' },
332367
routes: { type: 'json', description: 'Route definitions with descriptions' },
333368
model: { type: 'string', description: 'AI model to use' },
369+
customModelConfig: {
370+
type: 'json',
371+
description: 'Custom provider/model execution configuration for routing inference.',
372+
schema: CUSTOM_MODEL_CONFIG_JSON_SCHEMA,
373+
},
334374
...PROVIDER_CREDENTIAL_INPUTS,
335375
},
336376
outputs: {

apps/sim/blocks/utils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ describe('getModelOptions', () => {
336336
expect(ids).not.toContain('fireworks/accounts/fireworks/models/minimax-m2p7')
337337
})
338338

339-
it('adds the super-user custom option only to the Agent model options', () => {
339+
it('adds the super-user custom option only to custom-capable model options', () => {
340340
expect(getModelOptions().some((option) => option.id === 'sim-custom')).toBe(false)
341341
expect(getAgentModelOptions()).toContainEqual({
342342
label: 'Custom',

apps/sim/blocks/utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ export function getModelOptions() {
103103
}
104104

105105
/**
106-
* Agent-only model options. The custom passthrough entry is filtered by the
107-
* editor unless the user has effective super-user mode enabled.
106+
* Model options for blocks that support the custom passthrough contract. The
107+
* editor filters the custom entry unless effective Super User mode is enabled.
108108
*/
109-
export function getAgentModelOptions() {
109+
export function getCustomModelOptions() {
110110
return [
111111
...getModelOptions(),
112112
{
@@ -118,6 +118,8 @@ export function getAgentModelOptions() {
118118
]
119119
}
120120

121+
export const getAgentModelOptions = getCustomModelOptions
122+
121123
/**
122124
* Model options filtered to exact provider/model pairs in Pi's pinned catalog.
123125
* Unresolved or blacklisted models (which `getProviderFromModel` can throw on)

0 commit comments

Comments
 (0)