Skip to content

Commit 8f4f24f

Browse files
committed
Add sim-auto: automatic model routing for the agent block (hosted)
- 'Auto' model option (Sim wordmark icon, hosted-only, new-block default; runtime fallback for unset models stays claude-sonnet-5) - Resolver (lib/model-router): classifies each execution via mothership's /api/model-router and routes over two ladders — text: fireworks/glm-5.2 -> fireworks/kimi-k3 (new static hosted Fireworks catalog entries on the platform FIREWORKS_API_KEY); attachments: claude-haiku-4-5 -> gpt-5.5 (Fireworks OSS endpoints reject images). Trivial tasks skip the router; 5-min decision cache; 2s timeout; never fails the workflow - Hidden identity preamble on every auto run (English by default, don't volunteer the underlying model) - Fireworks executor: wire-name map for catalog ids (glm-5.2 -> glm-5p2), pricing keyed on the full catalog id - Billing: routing cost applied to non-streaming output cost only when mothership marks the call billable (bill-model-router flag, default off) - sim-auto special-cases: API-key condition, serialization tool lookup, edit-workflow validation (hosted), VFS model options projection
1 parent 1ce2aef commit 8f4f24f

14 files changed

Lines changed: 749 additions & 9 deletions

File tree

apps/sim/blocks/blocks/agent.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { AgentIcon } from '@/components/icons'
3+
import { isHosted } from '@/lib/core/config/env-flags'
34
import type { BlockConfig } from '@/blocks/types'
45
import { AuthMode, IntegrationType } from '@/blocks/types'
56
import {
@@ -20,6 +21,8 @@ import {
2021
getReasoningEffortValuesForModel,
2122
getThinkingLevelsForModel,
2223
getVerbosityValuesForModel,
24+
isAutoModel,
25+
SIM_AUTO_MODEL_ID,
2326
supportsTemperature,
2427
} from '@/providers/models'
2528
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -132,7 +135,9 @@ Return ONLY the JSON array.`,
132135
type: 'combobox',
133136
placeholder: 'Type or select a model...',
134137
required: true,
135-
defaultValue: 'claude-sonnet-5',
138+
// Hosted Sim defaults new agent blocks to the automatic model; the
139+
// runtime fallback for blocks with no model set stays AGENT.DEFAULT_MODEL.
140+
defaultValue: isHosted ? SIM_AUTO_MODEL_ID : 'claude-sonnet-5',
136141
options: getModelOptions,
137142
commandSearchable: true,
138143
},
@@ -522,6 +527,11 @@ Return ONLY the JSON array.`,
522527
if (!model) {
523528
throw new Error('No model selected')
524529
}
530+
// sim-auto resolves to a concrete model at execution time; this
531+
// serialization-time lookup only needs a stable provider tool id.
532+
if (isAutoModel(model)) {
533+
return 'anthropic_chat'
534+
}
525535
const tool = getBaseModelProviders()[model]
526536
if (!tool) {
527537
throw new Error(`Invalid model selected: ${model}`)

apps/sim/blocks/utils.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ vi.mock('@/providers/models', () => ({
4040
getProviderModels: mockGetProviderModels,
4141
getProviderIcon: mockGetProviderIcon,
4242
getBaseModelProviders: mockGetBaseModelProviders,
43+
SIM_AUTO_MODEL_ID: 'sim-auto',
44+
isAutoModel: (model: string) => model.trim().toLowerCase() === 'sim-auto',
4345
}))
4446

4547
vi.mock('@/providers/utils', () => ({

apps/sim/blocks/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { toError } from '@sim/utils/errors'
2+
import { SimAutoIcon } from '@/components/icons'
23
import {
34
isAzureConfigured,
45
isCohereConfigured,
@@ -14,7 +15,9 @@ import {
1415
getModelSunsetStatus,
1516
getProviderIcon,
1617
getProviderModels,
18+
isAutoModel,
1719
orderModelIdsByReleaseDate,
20+
SIM_AUTO_MODEL_ID,
1821
} from '@/providers/models'
1922
import { isPiSupportedModel } from '@/providers/pi-providers'
2023
import { getProviderFromModel } from '@/providers/utils'
@@ -75,12 +78,18 @@ export function getModelOptions() {
7578
])
7679
)
7780

78-
return allModels
81+
const options = allModels
7982
.filter((model) => getModelSunsetStatus(model) !== 'deprecated')
8083
.map((model) => {
8184
const icon = getProviderIcon(model)
8285
return { label: model, id: model, ...(icon && { icon }) }
8386
})
87+
88+
if (isHosted) {
89+
options.unshift({ label: 'Auto', id: SIM_AUTO_MODEL_ID, icon: SimAutoIcon })
90+
}
91+
92+
return options
8493
}
8594

8695
/**
@@ -183,6 +192,9 @@ function shouldRequireApiKeyForModel(model: string): boolean {
183192
const normalizedModel = model.trim().toLowerCase()
184193
if (!normalizedModel) return false
185194

195+
// The auto pseudo-model resolves server-side to a hosted pool model.
196+
if (isAutoModel(normalizedModel)) return false
197+
186198
if (isHosted) {
187199
const hostedModels = getHostedModels()
188200
if (hostedModels.some((m) => m.toLowerCase() === normalizedModel)) return false

apps/sim/components/icons.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7558,6 +7558,31 @@ export function SixtyfourIcon(props: SVGProps<SVGSVGElement>) {
75587558
)
75597559
}
75607560

7561+
/**
7562+
* The "sim" brand wordmark (v1.0 brand guide simLogotype paths — the same mark
7563+
* the navbar/login header renders), inked with the theme-adaptive
7564+
* `--text-body`. Used as the icon for the Auto model option; the wide viewBox
7565+
* letterboxes itself inside square icon slots.
7566+
*/
7567+
export function SimAutoIcon(props: SVGProps<SVGSVGElement>) {
7568+
return (
7569+
<svg
7570+
{...props}
7571+
viewBox='0 0 441 212'
7572+
fill='none'
7573+
xmlns='http://www.w3.org/2000/svg'
7574+
aria-hidden='true'
7575+
>
7576+
<g fill='var(--text-body)'>
7577+
<path d='M0 160.9H29.51C29.51 169.08 32.46 175.61 38.37 180.48C44.27 185.12 52.25 187.44 62.31 187.44C73.24 187.44 81.65 185.34 87.56 181.14C93.46 176.71 96.41 170.85 96.41 163.55C96.41 158.24 94.77 153.82 91.49 150.28C88.43 146.74 82.75 143.86 74.44 141.65L46.24 135.01C32.03 131.47 21.42 126.05 14.43 118.75C7.65 111.45 4.26 101.83 4.26 89.88C4.26 79.93 6.78 71.3 11.81 64C17.05 56.7 24.16 51.06 33.12 47.08C42.3 43.09 52.8 41.1 64.6 41.1C76.41 41.1 86.57 43.2 95.1 47.41C103.84 51.61 110.62 57.47 115.43 64.99C120.46 72.52 123.08 81.48 123.3 91.87H93.79C93.57 83.47 90.84 76.94 85.59 72.3C80.34 67.65 73.02 65.33 63.62 65.33C54 65.33 46.57 67.43 41.32 71.63C36.07 75.83 33.45 81.59 33.45 88.89C33.45 99.73 41.32 107.14 57.06 111.12L85.26 118.09C98.81 121.19 108.98 126.28 115.76 133.35C122.53 140.21 125.92 149.61 125.92 161.56C125.92 171.74 123.19 180.7 117.73 188.44C112.26 195.96 104.72 201.82 95.1 206.03C85.7 210.01 74.55 212 61.65 212C42.85 212 27.87 207.35 16.72 198.06C5.57 188.77 0 176.38 0 160.9Z' />
7578+
<path d='M232.8 212H202.13L202.13 49.76H229.54V77.39C232.8 68.34 239.11 60.66 247.81 54.7C256.73 48.52 267.5 45.43 280.12 45.43C294.26 45.43 306.01 49.29 315.36 57.02C324.72 64.75 330.81 75.01 333.64 87.82H328.09C330.27 75.01 336.25 64.75 346.04 57.02C355.83 49.29 367.9 45.43 382.26 45.43C400.54 45.43 414.89 50.84 425.34 61.66C435.78 72.47 441 87.26 441 106.03V212H410.98V113.65C410.98 100.84 407.71 91.02 401.19 84.17C394.88 77.11 386.29 73.58 375.41 73.58C367.79 73.58 361.05 75.34 355.17 78.88C349.52 82.19 345.06 87.04 341.8 93.45C338.53 99.85 336.9 107.36 336.9 115.97V212H306.55V113.32C306.55 100.51 303.4 90.8 297.09 84.17C290.78 77.33 282.19 73.91 271.31 73.91C263.69 73.91 256.95 75.67 251.08 79.21C245.42 82.52 240.96 87.38 237.7 93.78C234.43 99.96 232.8 107.36 232.8 115.97V212Z' />
7579+
<path d='M184.83 20.55C184.83 31.9 175.64 41.1 164.29 41.1C152.95 41.1 143.76 31.9 143.76 20.55C143.76 9.2 152.95 0 164.29 0C175.64 0 184.83 9.2 184.83 20.55Z' />
7580+
<path d='M179.43 212H149.16V49.76C153.76 51.91 158.88 53.12 164.29 53.12C169.7 53.12 174.83 51.91 179.43 49.76V212Z' />
7581+
</g>
7582+
</svg>
7583+
)
7584+
}
7585+
75617586
export function SimTriggerIcon(props: SVGProps<SVGSVGElement>) {
75627587
return (
75637588
<svg

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import { truncate } from '@sim/utils/string'
77
import { and, eq, inArray, isNull } from 'drizzle-orm'
88
import { normalizeStringRecord, normalizeWorkflowVariables } from '@/lib/core/utils/records'
99
import { createMcpToolId } from '@/lib/mcp/utils'
10+
import {
11+
type AutoRoutingResult,
12+
type AutoRoutingSignals,
13+
resolveAutoModel,
14+
SIM_AUTO_SYSTEM_PREAMBLE,
15+
} from '@/lib/model-router/resolve'
1016
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
1117
import { hydrateUserFilesWithBase64 } from '@/lib/uploads/utils/user-file-base64.server'
1218
import { resolveCustomBlockToolBinding } from '@/lib/workflows/custom-blocks/operations'
@@ -46,6 +52,7 @@ import {
4652
shouldUseLargeFilePath,
4753
supportsFileAttachments,
4854
} from '@/providers/attachments'
55+
import { isAutoModel } from '@/providers/models'
4956
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
5057
import type { SerializedBlock } from '@/serializer/types'
5158
import { filterSchemaForLLM, type ToolSchema } from '@/tools/params'
@@ -77,7 +84,32 @@ export class AgentBlockHandler implements BlockHandler {
7784
await this.validateToolPermissions(ctx, filteredInputs.tools || [])
7885

7986
const responseFormat = parseResponseFormat(filteredInputs.responseFormat)
80-
const model = filteredInputs.model || AGENT.DEFAULT_MODEL
87+
const configuredModel = filteredInputs.model || AGENT.DEFAULT_MODEL
88+
89+
let model = configuredModel
90+
let autoRouting: AutoRoutingResult | null = null
91+
if (isAutoModel(configuredModel)) {
92+
autoRouting = await resolveAutoModel({
93+
ctx,
94+
blockId: block.id,
95+
signals: this.buildAutoRoutingSignals(filteredInputs, responseFormat),
96+
fallbackModel: AGENT.DEFAULT_MODEL,
97+
})
98+
model = autoRouting.model
99+
logger.info('Resolved sim-auto model', {
100+
blockId: block.id,
101+
model,
102+
tier: autoRouting.tier,
103+
decidedBy: autoRouting.decidedBy,
104+
})
105+
// Hidden identity preamble for every auto execution (fallback included):
106+
// keeps pool models in English by default and off the topic of which
107+
// underlying model they are. Applied after signal building so the
108+
// preamble never influences classification.
109+
filteredInputs.systemPrompt = [SIM_AUTO_SYSTEM_PREAMBLE, filteredInputs.systemPrompt]
110+
.filter(Boolean)
111+
.join('\n\n')
112+
}
81113

82114
await validateModelProvider(ctx.userId, ctx.workspaceId, model, ctx)
83115

@@ -126,6 +158,14 @@ export class AgentBlockHandler implements BlockHandler {
126158

127159
const result = await this.executeProviderRequest(ctx, providerRequest, block, responseFormat)
128160

161+
// Routing cost lands on non-streaming outputs only for now; streaming
162+
// outputs assemble cost at stream end where there is no hook yet. The
163+
// charge is gated server-side by mothership's bill-model-router flag
164+
// (default off), so this asymmetry currently bills nobody.
165+
if (autoRouting && autoRouting.billableRoutingCost > 0 && !this.isStreamingExecution(result)) {
166+
this.applyRoutingCost(result as BlockOutput, autoRouting.billableRoutingCost)
167+
}
168+
129169
if (this.isStreamingExecution(result)) {
130170
if (filteredInputs.memoryType && filteredInputs.memoryType !== 'none') {
131171
return this.wrapStreamForMemoryPersistence(
@@ -144,6 +184,52 @@ export class AgentBlockHandler implements BlockHandler {
144184
return result
145185
}
146186

187+
/**
188+
* Derives the compact routing signals for sim-auto resolution from the
189+
* block's resolved inputs. Excerpts only — the resolver truncates further
190+
* and mothership re-clamps server-side.
191+
*/
192+
private buildAutoRoutingSignals(inputs: AgentInputs, responseFormat: any): AutoRoutingSignals {
193+
const lastMessage =
194+
typeof inputs.userPrompt === 'string'
195+
? inputs.userPrompt
196+
: inputs.userPrompt != null
197+
? stringifyJSON(inputs.userPrompt)
198+
: (inputs.messages?.at(-1)?.content ?? '')
199+
const systemPrompt = inputs.systemPrompt ?? ''
200+
const normalizedFiles = normalizeFileInput(inputs.files)
201+
const approxChars =
202+
systemPrompt.length +
203+
lastMessage.length +
204+
(inputs.messages ? stringifyJSON(inputs.messages).length : 0)
205+
206+
return {
207+
systemPrompt,
208+
lastMessage,
209+
messageCount: (inputs.messages?.length ?? 0) + (inputs.userPrompt ? 1 : 0),
210+
toolNames: (inputs.tools ?? []).map((t) => t.title || t.type || 'tool'),
211+
hasAttachments: Array.isArray(normalizedFiles)
212+
? normalizedFiles.length > 0
213+
: Boolean(normalizedFiles),
214+
hasResponseFormat: Boolean(responseFormat),
215+
approxInputTokens: Math.ceil(approxChars / 4),
216+
}
217+
}
218+
219+
/**
220+
* Adds the billable sim-auto routing charge to a non-streaming output's
221+
* cost breakdown as a distinct `routing` component.
222+
*/
223+
private applyRoutingCost(output: BlockOutput, routingCost: number): void {
224+
const target = output as { cost?: Record<string, number> }
225+
if (target.cost && typeof target.cost.total === 'number') {
226+
target.cost.routing = routingCost
227+
target.cost.total += routingCost
228+
} else {
229+
target.cost = { input: 0, output: 0, routing: routingCost, total: routingCost }
230+
}
231+
}
232+
147233
private async validateToolPermissions(ctx: ExecutionContext, tools: ToolInput[]): Promise<void> {
148234
if (!Array.isArray(tools) || tools.length === 0) return
149235

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { toError } from '@sim/utils/errors'
33
import { omit } from '@sim/utils/object'
44
import { validateSelectorIds } from '@/lib/copilot/validation/selector-validator'
5+
import { isHosted as isHostedDeployment } from '@/lib/core/config/env-flags'
56
import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access'
67
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
78
import { getCustomToolById } from '@/lib/workflows/custom-tools/operations'
@@ -16,7 +17,7 @@ import { getBlock } from '@/blocks/registry'
1617
import type { SubBlockConfig } from '@/blocks/types'
1718
import { getModelOptions } from '@/blocks/utils'
1819
import { BlockType, EDGE, normalizeName } from '@/executor/constants'
19-
import { isKnownModelId, suggestModelIdsForUnknownModel } from '@/providers/models'
20+
import { isAutoModel, isKnownModelId, suggestModelIdsForUnknownModel } from '@/providers/models'
2021
import { isPiByokOnlyMode } from '@/providers/pi-providers'
2122
import { getTool } from '@/tools/utils'
2223
import { TRIGGER_RUNTIME_SUBBLOCK_IDS, TRIGGER_WEBHOOK_URL_FIELD } from '@/triggers/constants'
@@ -557,6 +558,12 @@ export function validateValueForSubBlockType(
557558
if (usesProviderCatalog) {
558559
const stringValue = typeof value === 'string' ? value : String(value)
559560
const trimmed = stringValue.trim()
561+
// sim-auto is a valid model value on hosted Sim only (mirrors the
562+
// options array the agent reads: it is absent from self-hosted
563+
// snapshots, so writes of it there are rejected as unknown).
564+
if (trimmed !== '' && isAutoModel(trimmed) && isHostedDeployment) {
565+
return { valid: true, value: trimmed.toLowerCase() }
566+
}
560567
if (trimmed !== '' && !isKnownModelId(trimmed)) {
561568
const suggestions = suggestModelIdsForUnknownModel(trimmed)
562569
const suggestionText =

apps/sim/lib/copilot/vfs/serializers.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { getBlock } from '@/blocks'
1212
import { isCustomBlockType } from '@/blocks/custom/build-config'
1313
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
1414
import { isHiddenUnder } from '@/blocks/visibility/context'
15-
import { DYNAMIC_MODEL_PROVIDERS, PROVIDER_DEFINITIONS } from '@/providers/models'
15+
import {
16+
DYNAMIC_MODEL_PROVIDERS,
17+
PROVIDER_DEFINITIONS,
18+
SIM_AUTO_MODEL_ID,
19+
} from '@/providers/models'
1620
import type { ToolConfig, ToolHostingCondition } from '@/tools/types'
1721

1822
/** The service-account alternative to OAuth for a service, when it offers one. */
@@ -500,6 +504,18 @@ function getStaticModelOptionsForVFS(): StaticModelOption[] {
500504

501505
const models: StaticModelOption[] = []
502506

507+
// Hosted-only automatic model: presence in this options array is what
508+
// licenses the build agent to write it (its prompt guidance is conditioned
509+
// on presence), so self-hosted snapshots never carry it.
510+
if (isHosted) {
511+
models.push({
512+
id: SIM_AUTO_MODEL_ID,
513+
provider: 'sim',
514+
hosted: true,
515+
recommended: true,
516+
})
517+
}
518+
503519
for (const [providerId, def] of Object.entries(PROVIDER_DEFINITIONS)) {
504520
if (dynamicProviders.has(providerId)) continue
505521
for (const model of def.models) {

0 commit comments

Comments
 (0)