Skip to content

Commit 8940833

Browse files
authored
improvement(agent): allow variable references in reasoning effort, verbosity, and thinking level (#6233)
* improvement(agent): allow variable references in reasoning effort and verbosity Reasoning Effort and Verbosity were select-only dropdowns, so a workflow could not sweep them from a variable or an upstream block the way it already can with the model. Both become editable comboboxes, matching the model field directly above them and the managed-agent selectors. - switch both subblocks to `combobox`, keeping their fetched per-model option lists intact - keep them visible when `model` itself holds a reference, since the concrete model id is only known at execution time and cannot be matched against the static capability list - normalize the resolved level in the provider chokepoint so a reference that resolves to `"High"` or to nothing behaves sanely instead of hitting a provider 400 * improvement(agent): allow variable references in thinking level Extends the same treatment to Thinking Level so all three model-tuning fields behave consistently, and logs a level a model does not declare. - switch `thinkingLevel` to `combobox` with the reference-aware condition - normalize it alongside the other two; an empty resolve now takes the deliberate "send nothing" path rather than the incoherent half-state it hit before, and stays distinct from an explicit `none` - warn when a level is not one the model declares, still forwarding it: Sim's per-model lists drive the pickers and can lag a provider, and a sweep needs the provider's own error rather than a silent fallback to the default * improvement(agent): report a model level the sanitizer discards A model bound to a variable or block reference only resolves at execution time, so a run whose reference landed on a model outside Sim's catalogue had its requested level cleared with no signal and quietly fell back to that model's default. Dropping stays the safe default — a provider with no such parameter rejects the whole request — but it is now reported. - log the field, model, and value whenever an unsupported-field level is cleared - cover both diagnostics, including that they stay quiet for a declared level and for the `auto` / `none` sentinels * fix(providers): redact resolved level content from sanitizer diagnostics The model-level fields accept environment and block references, so an unrecognized level is not necessarily a mistyped level — it is whatever the reference resolved to, which can be secret content. The diagnostics added for dropped and undeclared levels echoed it straight into server logs. - log a level only when the catalogue declares it somewhere, or it is an `auto` / `none` sentinel; anything else is reported by length alone - stop discarding levels for a model the catalogue has never seen. Absent is unknown, not known-incapable, and a reference is exactly how a newly released model arrives before Sim catalogues it — the provider decides instead. Models the catalogue knows, and every dynamic-provider id, keep the protective drop * fix(providers): redact the level in Anthropic's unsupported-thinking warning Forwarding an undeclared level is deliberate, but it means the Anthropic adapter receives it and interpolates it straight into its "not supported, ignoring" warning. Since the field is reference-bound, that value can be whatever a mistyped `{{ENV_VAR}}` or block reference resolved to — so the redaction added for the sanitizer's own diagnostics was leaking one layer downstream. - promote the level renderer to `providers/utils` as `describeModelLevel`, the single gate every site echoing a caller-supplied level goes through - use it in Anthropic's warning and in both sanitizer diagnostics * refactor(providers): drop the sanitizer's level diagnostics The two warnings logged server-side, where the workflow author who set the level never sees them, and the surprising case they described — a level discarded for a model newer than the catalogue — is now fixed at the source rather than narrated. They also carried the redaction that leaked resolved content before it was caught, so removing them removes that surface entirely. Levels still normalize, and still drop for a catalogued model that does not take the field. `describeModelLevel` stays for Anthropic's unsupported-thinking warning, which is a pre-existing log this feature newly exposes to resolved reference content.
1 parent d05289c commit 8940833

13 files changed

Lines changed: 441 additions & 23 deletions

File tree

apps/sim/blocks/blocks.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
22

33
vi.unmock('@/blocks/registry')
44

5+
import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility'
56
import { generateRouterPrompt } from '@/blocks/blocks/router'
67
import {
78
getAllBlocks,
@@ -842,6 +843,41 @@ describe.concurrent('Blocks Module', () => {
842843
expect(modelSubBlock?.commandSearchable).toBe(true)
843844
})
844845

846+
/** Each model-tuning field with a model that accepts it and one that does not. */
847+
const AGENT_MODEL_LEVEL_FIELDS = [
848+
{ id: 'reasoningEffort', capable: 'gpt-5.1', incapable: 'claude-sonnet-5' },
849+
{ id: 'verbosity', capable: 'gpt-5.1', incapable: 'claude-sonnet-5' },
850+
{ id: 'thinkingLevel', capable: 'claude-sonnet-5', incapable: 'gpt-5.1' },
851+
] as const
852+
853+
it('should let the agent model-tuning fields take a typed reference', () => {
854+
const agentBlock = getBlock('agent')
855+
856+
for (const { id } of AGENT_MODEL_LEVEL_FIELDS) {
857+
const subBlock = agentBlock?.subBlocks.find((sb) => sb.id === id)
858+
// A combobox is editable, so a `<block.output>` / `{{ENV_VAR}}` reference can be
859+
// typed into it; the option list still offers every level the model accepts.
860+
expect(subBlock?.type).toBe('combobox')
861+
expect(typeof subBlock?.condition).toBe('function')
862+
}
863+
})
864+
865+
it('should keep the agent model-tuning fields visible when the model is a reference', () => {
866+
const agentBlock = getBlock('agent')
867+
868+
for (const { id, capable, incapable } of AGENT_MODEL_LEVEL_FIELDS) {
869+
const subBlock = agentBlock?.subBlocks.find((sb) => sb.id === id)
870+
const condition = subBlock?.condition
871+
if (typeof condition !== 'function') throw new Error(`${id} condition is not a function`)
872+
873+
expect(evaluateSubBlockCondition(condition, { model: '<start.model>' })).toBe(true)
874+
expect(evaluateSubBlockCondition(condition, { model: '{{MODEL_ID}}' })).toBe(true)
875+
// Gating on the capability list is unchanged for a literal model.
876+
expect(evaluateSubBlockCondition(condition, { model: capable })).toBe(true)
877+
expect(evaluateSubBlockCondition(condition, { model: incapable })).toBe(false)
878+
}
879+
})
880+
845881
it('should hide generator API keys on hosted only for Fal.ai providers', () => {
846882
for (const blockType of ['image_generator_v2', 'video_generator_v3']) {
847883
const block = getBlock(blockType)

apps/sim/blocks/blocks/agent.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AgentIcon } from '@/components/icons'
33
import type { BlockConfig } from '@/blocks/types'
44
import { AuthMode, IntegrationType } from '@/blocks/types'
55
import {
6+
getModelCapabilityCondition,
67
getModelOptions,
78
getProviderCredentialSubBlocks,
89
normalizeFileInput,
@@ -159,8 +160,8 @@ Return ONLY the JSON array.`,
159160
{
160161
id: 'reasoningEffort',
161162
title: 'Reasoning Effort',
162-
type: 'dropdown',
163-
placeholder: 'Select reasoning effort...',
163+
type: 'combobox',
164+
placeholder: 'Type or select reasoning effort...',
164165
options: [
165166
{ label: 'auto', id: 'auto' },
166167
{ label: 'low', id: 'low' },
@@ -207,16 +208,13 @@ Return ONLY the JSON array.`,
207208
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
208209
},
209210
mode: 'advanced',
210-
condition: {
211-
field: 'model',
212-
value: MODELS_WITH_REASONING_EFFORT,
213-
},
211+
condition: getModelCapabilityCondition(MODELS_WITH_REASONING_EFFORT),
214212
},
215213
{
216214
id: 'verbosity',
217215
title: 'Verbosity',
218-
type: 'dropdown',
219-
placeholder: 'Select verbosity...',
216+
type: 'combobox',
217+
placeholder: 'Type or select verbosity...',
220218
options: [
221219
{ label: 'auto', id: 'auto' },
222220
{ label: 'low', id: 'low' },
@@ -263,16 +261,13 @@ Return ONLY the JSON array.`,
263261
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
264262
},
265263
mode: 'advanced',
266-
condition: {
267-
field: 'model',
268-
value: MODELS_WITH_VERBOSITY,
269-
},
264+
condition: getModelCapabilityCondition(MODELS_WITH_VERBOSITY),
270265
},
271266
{
272267
id: 'thinkingLevel',
273268
title: 'Thinking Level',
274-
type: 'dropdown',
275-
placeholder: 'Select thinking level...',
269+
type: 'combobox',
270+
placeholder: 'Type or select thinking level...',
276271
options: [
277272
{ label: 'none', id: 'none' },
278273
{ label: 'minimal', id: 'minimal' },
@@ -306,10 +301,7 @@ Return ONLY the JSON array.`,
306301
return [noneOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
307302
},
308303
mode: 'advanced',
309-
condition: {
310-
field: 'model',
311-
value: MODELS_WITH_THINKING,
312-
},
304+
condition: getModelCapabilityCondition(MODELS_WITH_THINKING),
313305
},
314306
{
315307
id: 'promptCaching',

apps/sim/blocks/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isOllamaConfigured,
88
} from '@/lib/core/config/env-flags'
99
import { getScopesForService } from '@/lib/oauth/utils'
10+
import { containsReference } from '@/lib/workflows/sanitization/references'
1011
import { buildCanonicalIndex } from '@/lib/workflows/subblocks/visibility'
1112
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
1213
import {
@@ -235,6 +236,23 @@ function shouldRequireApiKeyForModel(model: string): boolean {
235236
return true
236237
}
237238

239+
/**
240+
* Visibility condition for a model-tuning field that only some models accept, such as
241+
* reasoning effort or verbosity. Gates on the capability list, but keeps the field visible
242+
* when `model` itself holds a variable or block reference — the concrete model id is only
243+
* known at execution time then, so matching a reference against a static list would hide
244+
* the field for every workflow that binds its model dynamically.
245+
*/
246+
export function getModelCapabilityCondition(capableModels: string[]) {
247+
return (values?: Record<string, unknown>) => {
248+
const model = typeof values?.model === 'string' ? values.model : ''
249+
if (containsReference(model)) {
250+
return buildModelVisibilityCondition(model, true)
251+
}
252+
return { field: 'model', value: capableModels }
253+
}
254+
}
255+
238256
/**
239257
* Get the API key condition for provider credential subblocks.
240258
* Handles hosted vs self-hosted environments and excludes providers that don't need API key.

apps/sim/executor/variables/resolver.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,3 +1341,50 @@ describe('VariableResolver function context overflow offload', () => {
13411341
expect(result.resolvedInputs.code).toBe('return globals()["__blockRef_0"]')
13421342
})
13431343
})
1344+
1345+
/**
1346+
* The agent block's Reasoning Effort and Verbosity fields are editable comboboxes, so a
1347+
* workflow can bind them to a reference instead of picking a level. These lock in that the
1348+
* generic input resolution actually reaches those two fields.
1349+
*/
1350+
describe('VariableResolver agent model levels', () => {
1351+
it('resolves block, workflow-variable, and env references in reasoning effort and verbosity', async () => {
1352+
const producer = createBlock('producer', 'Producer', BlockType.API)
1353+
const agent = createBlock('agent', 'Agent', BlockType.AGENT, {
1354+
model: 'gpt-5',
1355+
reasoningEffort: '<Producer.result>',
1356+
verbosity: '<variable.Detail>',
1357+
thinkingLevel: '{{THINKING}}',
1358+
})
1359+
const workflow: SerializedWorkflow = {
1360+
version: '1',
1361+
blocks: [producer, agent],
1362+
connections: [],
1363+
loops: {},
1364+
parallels: {},
1365+
}
1366+
1367+
const state = new ExecutionState()
1368+
state.setBlockOutput('producer', { result: 'high' })
1369+
const ctx = {
1370+
blockStates: state.getBlockStates(),
1371+
blockLogs: [],
1372+
environmentVariables: { THINKING: 'medium' },
1373+
workflowVariables: { 'var-1': { id: 'var-1', name: 'Detail', type: 'string', value: 'low' } },
1374+
decisions: { router: new Map(), condition: new Map() },
1375+
loopExecutions: new Map(),
1376+
executedBlocks: new Set(),
1377+
activeExecutionPath: new Set(),
1378+
completedLoops: new Set(),
1379+
metadata: {},
1380+
} as unknown as ExecutionContext
1381+
1382+
const resolver = new VariableResolver(workflow, { THINKING: 'medium' }, state)
1383+
const result = await resolver.resolveInputs(ctx, 'agent', agent.config.params, agent)
1384+
1385+
expect(result.reasoningEffort).toBe('high')
1386+
expect(result.verbosity).toBe('low')
1387+
expect(result.thinkingLevel).toBe('medium')
1388+
expect(result.model).toBe('gpt-5')
1389+
})
1390+
})

apps/sim/lib/workflows/sanitization/references.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22
import {
3+
containsReference,
34
isLikelyReferenceSegment,
45
splitReferenceSegment,
56
} from '@/lib/workflows/sanitization/references'
@@ -53,3 +54,37 @@ describe('isLikelyReferenceSegment', () => {
5354
expect(isLikelyReferenceSegment('<123>')).toBe(false)
5455
})
5556
})
57+
58+
describe('containsReference', () => {
59+
it('detects block and variable references', () => {
60+
expect(containsReference('<start.input>')).toBe(true)
61+
expect(containsReference('<variable.model>')).toBe(true)
62+
expect(containsReference('<loop.index>')).toBe(true)
63+
})
64+
65+
it('detects environment variable placeholders', () => {
66+
expect(containsReference('{{MODEL_ID}}')).toBe(true)
67+
})
68+
69+
it('detects a reference embedded in surrounding text', () => {
70+
expect(containsReference('gpt-<start.suffix>')).toBe(true)
71+
})
72+
73+
it('returns false for literal model ids', () => {
74+
expect(containsReference('gpt-5.1')).toBe(false)
75+
expect(containsReference('claude-sonnet-5')).toBe(false)
76+
expect(containsReference('azure/gpt-5.1-codex')).toBe(false)
77+
})
78+
79+
it('returns false for empty and non-string values', () => {
80+
expect(containsReference('')).toBe(false)
81+
expect(containsReference(undefined)).toBe(false)
82+
expect(containsReference(null)).toBe(false)
83+
expect(containsReference(42)).toBe(false)
84+
})
85+
86+
it('returns false for stray brackets that are not references', () => {
87+
expect(containsReference('a < b')).toBe(false)
88+
expect(containsReference('<123>')).toBe(false)
89+
})
90+
})

apps/sim/lib/workflows/sanitization/references.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ export function isLikelyReferenceSegment(segment: string): boolean {
7777
return true
7878
}
7979

80+
const ENV_VAR_PATTERN = new RegExp(`\\${REFERENCE.ENV_VAR_START}[^}]+\\${REFERENCE.ENV_VAR_END}`)
81+
82+
/**
83+
* Whether a subblock value carries a `<block.path>` / `<variable.name>` reference or a
84+
* `{{ENV_VAR}}` placeholder instead of a literal value — i.e. its real value is only known
85+
* once the workflow runs. Conditions that gate one field on a sibling's literal value use
86+
* this to stay visible while the sibling is bound dynamically.
87+
*/
88+
export function containsReference(value: unknown): boolean {
89+
if (typeof value !== 'string' || !value) {
90+
return false
91+
}
92+
return extractReferencePrefixes(value).length > 0 || ENV_VAR_PATTERN.test(value)
93+
}
94+
8095
export function extractReferencePrefixes(value: string): Array<{ raw: string; prefix: string }> {
8196
if (!value || typeof value !== 'string') {
8297
return []

apps/sim/providers/anthropic/core.thinking.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
import { describe, expect, it } from 'vitest'
1010
import { buildThinkingConfig } from '@/providers/anthropic/core'
11+
import { describeModelLevel } from '@/providers/utils'
1112

1213
describe('buildThinkingConfig', () => {
1314
it('requests summarized display for omitted-display models on agent-events runs', () => {
@@ -55,3 +56,21 @@ describe('buildThinkingConfig', () => {
5556
expect(buildThinkingConfig('gpt-4o', 'high', true)).toBeNull()
5657
})
5758
})
59+
60+
/**
61+
* A thinking level that is not one the model declares reaches this adapter, by design — Sim's
62+
* per-model lists can lag a provider. The adapter logs that it is ignoring it, and since the
63+
* field is reference-bound, the value it logs can be whatever a mistyped `{{ENV_VAR}}` or block
64+
* reference resolved to.
65+
*/
66+
describe('unsupported thinking level logging', () => {
67+
it('returns null for a level the model does not declare', () => {
68+
expect(buildThinkingConfig('claude-sonnet-5', 'sk-proj-abcdef0123456789', false)).toBeNull()
69+
})
70+
71+
it('redacts the level in the ignore warning instead of echoing it', () => {
72+
const secret = 'sk-proj-abcdef0123456789'
73+
expect(describeModelLevel(secret)).toBe(`[redacted ${secret.length} chars]`)
74+
expect(describeModelLevel('high')).toBe('high')
75+
})
76+
})

apps/sim/providers/anthropic/core.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import { adaptAnthropicToolSchema } from '@/providers/tool-schema-adapter'
3131
import { enrichLastModelSegment } from '@/providers/trace-enrichment'
3232
import type { ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types'
3333
import { ProviderError } from '@/providers/types'
34-
import { prepareToolExecution, prepareToolsWithUsageControl } from '@/providers/utils'
34+
import {
35+
describeModelLevel,
36+
prepareToolExecution,
37+
prepareToolsWithUsageControl,
38+
} from '@/providers/utils'
3539

3640
/**
3741
* Configuration for creating an Anthropic provider instance.
@@ -396,7 +400,7 @@ export async function executeAnthropicProviderRequest(
396400
)
397401
} else {
398402
logger.warn(
399-
`Thinking level "${request.thinkingLevel}" not supported for model: ${modelId}, ignoring`
403+
`Thinking level "${describeModelLevel(request.thinkingLevel)}" not supported for model: ${modelId}, ignoring`
400404
)
401405
}
402406
}

0 commit comments

Comments
 (0)