Skip to content

Commit a4c7abb

Browse files
committed
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
1 parent fe52248 commit a4c7abb

5 files changed

Lines changed: 83 additions & 23 deletions

File tree

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
}

apps/sim/providers/index.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
getThinkingLevelsForModel,
2121
getVerbosityValuesForModel,
2222
isKnownModelId,
23-
isKnownModelLevelValue,
2423
} from '@/providers/models'
2524
import { getProviderExecutor } from '@/providers/registry'
2625
import {
@@ -29,6 +28,7 @@ import {
2928
} from '@/providers/runtime-context'
3029
import type { ProviderId, ProviderRequest, ProviderResponse } from '@/providers/types'
3130
import {
31+
describeModelLevel,
3232
generateStructuredOutputInstructions,
3333
sumToolCosts,
3434
supportsPromptCaching,
@@ -59,28 +59,10 @@ function normalizeModelLevel(value: string | undefined): string | undefined {
5959
return normalized || undefined
6060
}
6161

62-
/**
63-
* Levels the pickers offer on top of what a model declares. `auto` means "say nothing" and
64-
* `none` means "explicitly off"; every provider adapter special-cases them, so neither is
65-
* an unrecognized level.
66-
*/
6762
const MODEL_LEVEL_SENTINELS = new Set(['auto', 'none'])
6863

6964
type ModelLevelField = 'reasoningEffort' | 'verbosity' | 'thinkingLevel'
7065

71-
/**
72-
* Renders a level for a log line.
73-
*
74-
* These fields accept variable and environment references, so an unrecognized value is not
75-
* necessarily a mistyped level — it is whatever the reference resolved to, which may be secret
76-
* content. Only a level the catalogue declares somewhere is safe to echo; anything else is
77-
* reported by length alone, which is enough to tell a stray level from a resolved blob.
78-
*/
79-
function describeLevel(value: string): string {
80-
const isSafe = MODEL_LEVEL_SENTINELS.has(value) || isKnownModelLevelValue(value)
81-
return isSafe ? value : `[redacted ${value.length} chars]`
82-
}
83-
8466
/**
8567
* Clears a level whose resolved model does not accept the field at all.
8668
*
@@ -99,7 +81,7 @@ function dropUnsupportedLevel(
9981
logger.warn('Model does not support this level; dropping it from the request', {
10082
field,
10183
model,
102-
value: describeLevel(value),
84+
value: describeModelLevel(value),
10385
})
10486
}
10587
return undefined
@@ -126,7 +108,7 @@ function warnOnUnrecognizedLevel(
126108
logger.warn('Model level is not one this model declares; forwarding to the provider', {
127109
field,
128110
model,
129-
value: describeLevel(value),
111+
value: describeModelLevel(value),
130112
declaredValues,
131113
})
132114
}

apps/sim/providers/utils.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing'
22
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
33
import {
44
calculateCost,
5+
describeModelLevel,
56
extractAndParseJSON,
67
filterBlacklistedModels,
78
formatCost,
@@ -1767,3 +1768,32 @@ describe('prepareToolExecution invoker identity hand-off', () => {
17671768
expect(executionParams._context.executionId).toBeUndefined()
17681769
})
17691770
})
1771+
1772+
/**
1773+
* The agent block's tuning-level fields accept variable and environment references, so any
1774+
* message that echoes a caller-supplied level can otherwise carry whatever that reference
1775+
* resolved to — including secret content.
1776+
*/
1777+
describe('describeModelLevel', () => {
1778+
it('echoes a level the catalogue declares', () => {
1779+
expect(describeModelLevel('high')).toBe('high')
1780+
expect(describeModelLevel('minimal')).toBe('minimal')
1781+
expect(describeModelLevel('xhigh')).toBe('xhigh')
1782+
})
1783+
1784+
it('echoes the auto and none sentinels', () => {
1785+
expect(describeModelLevel('auto')).toBe('auto')
1786+
expect(describeModelLevel('none')).toBe('none')
1787+
})
1788+
1789+
it('redacts anything else to a length', () => {
1790+
const secret = 'sk-proj-abcdef0123456789'
1791+
expect(describeModelLevel(secret)).toBe(`[redacted ${secret.length} chars]`)
1792+
expect(describeModelLevel(secret)).not.toContain('abcdef')
1793+
})
1794+
1795+
it('reports an absent level without throwing', () => {
1796+
expect(describeModelLevel(undefined)).toBe('(unset)')
1797+
expect(describeModelLevel('')).toBe('(unset)')
1798+
})
1799+
})

apps/sim/providers/utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
getReasoningEffortValuesForModel as getReasoningEffortValuesForModelFromDefinitions,
4545
getThinkingLevelsForModel as getThinkingLevelsForModelFromDefinitions,
4646
getVerbosityValuesForModel as getVerbosityValuesForModelFromDefinitions,
47+
isKnownModelLevelValue,
4748
PROVIDER_DEFINITIONS,
4849
supportsTemperature as supportsTemperatureFromDefinitions,
4950
supportsToolUsageControl as supportsToolUsageControlFromDefinitions,
@@ -1381,6 +1382,30 @@ export function supportsTemperature(model: string): boolean {
13811382
return supportsTemperatureFromDefinitions(model)
13821383
}
13831384

1385+
/**
1386+
* Levels the pickers offer on top of what a model declares. `auto` means "say nothing" and
1387+
* `none` means "explicitly off"; provider adapters special-case both, so neither is an
1388+
* unrecognized level.
1389+
*/
1390+
const MODEL_LEVEL_SENTINELS = new Set(['auto', 'none'])
1391+
1392+
/**
1393+
* Renders a tuning level for a log line or an error message.
1394+
*
1395+
* The agent block's reasoning effort, verbosity, and thinking level fields accept variable and
1396+
* environment references, so an unrecognized level is not necessarily a mistyped level — it is
1397+
* whatever the reference resolved to, up to and including secret content. Only a level the
1398+
* catalogue declares somewhere is safe to echo; anything else is reported by length alone,
1399+
* which still distinguishes a stray level from a resolved blob.
1400+
*
1401+
* Every site that puts a caller-supplied level into a message must go through this.
1402+
*/
1403+
export function describeModelLevel(value: string | undefined): string {
1404+
if (!value) return '(unset)'
1405+
const isSafe = MODEL_LEVEL_SENTINELS.has(value) || isKnownModelLevelValue(value)
1406+
return isSafe ? value : `[redacted ${value.length} chars]`
1407+
}
1408+
13841409
export function supportsReasoningEffort(model: string): boolean {
13851410
return MODELS_WITH_REASONING_EFFORT.includes(model.toLowerCase())
13861411
}

0 commit comments

Comments
 (0)