Skip to content

Commit fe52248

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

3 files changed

Lines changed: 117 additions & 7 deletions

File tree

apps/sim/providers/index.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,4 +604,63 @@ describe('executeProviderRequest — model level normalization', () => {
604604

605605
expect(mockLoggerWarn).not.toHaveBeenCalled()
606606
})
607+
608+
/**
609+
* These fields take environment and block references, so a mistyped reference resolves the
610+
* secret into the level. The diagnostics must never echo it.
611+
*/
612+
it('redacts a level that is not a catalogue level before logging it', async () => {
613+
const secret = 'sk-proj-abcdef0123456789'
614+
615+
await executeProviderRequest('openai', {
616+
model: 'gpt-5',
617+
workspaceId: 'ws-1',
618+
reasoningEffort: secret,
619+
})
620+
621+
expect(mockLoggerWarn).toHaveBeenCalledWith(
622+
expect.any(String),
623+
expect.objectContaining({ value: `[redacted ${secret.length} chars]` })
624+
)
625+
const loggedText = JSON.stringify(mockLoggerWarn.mock.calls)
626+
expect(loggedText).not.toContain(secret)
627+
})
628+
629+
it('keeps the auto sentinel readable in a drop diagnostic', async () => {
630+
await executeProviderRequest('anthropic', {
631+
model: 'claude-opus-4-6',
632+
workspaceId: 'ws-1',
633+
reasoningEffort: 'auto',
634+
})
635+
636+
expect(mockLoggerWarn).toHaveBeenCalledWith(
637+
'Model does not support this level; dropping it from the request',
638+
expect.objectContaining({ value: 'auto' })
639+
)
640+
})
641+
642+
/**
643+
* A model the catalogue has never seen is unknown, not known-incapable — which is exactly
644+
* how a newly released model arrives through a reference before Sim catalogues it. The
645+
* provider decides, rather than the level being discarded on a stale list.
646+
*/
647+
it('forwards levels for a model absent from the catalogue', async () => {
648+
await executeProviderRequest('openai', {
649+
model: 'gpt-6-unreleased',
650+
workspaceId: 'ws-1',
651+
reasoningEffort: 'high',
652+
})
653+
654+
expect(sentRequest().reasoningEffort).toBe('high')
655+
})
656+
657+
it('still drops levels for a dynamic-provider model that does not take them', async () => {
658+
await executeProviderRequest('ollama', {
659+
model: 'ollama/llama3',
660+
workspaceId: 'ws-1',
661+
reasoningEffort: 'high',
662+
})
663+
664+
expect(sentRequest().reasoningEffort).toBeUndefined()
665+
})
607666
})

apps/sim/providers/index.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
getReasoningEffortValuesForModel,
2020
getThinkingLevelsForModel,
2121
getVerbosityValuesForModel,
22+
isKnownModelId,
23+
isKnownModelLevelValue,
2224
} from '@/providers/models'
2325
import { getProviderExecutor } from '@/providers/registry'
2426
import {
@@ -66,14 +68,27 @@ const MODEL_LEVEL_SENTINELS = new Set(['auto', 'none'])
6668

6769
type ModelLevelField = 'reasoningEffort' | 'verbosity' | 'thinkingLevel'
6870

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+
6984
/**
7085
* Clears a level whose resolved model does not accept the field at all.
7186
*
7287
* Dropping is the safe default — a provider that has no such parameter rejects the whole
7388
* request — but the discard is reported because the model can be bound to a variable or block
7489
* reference and is therefore only known at execution time. Without this, a run whose reference
75-
* resolved to a model outside Sim's catalogue would quietly fall back to that model's default
76-
* while the caller believed the level applied.
90+
* resolved to a model that does not take the field would quietly fall back to that model's
91+
* default while the caller believed the level applied.
7792
*/
7893
function dropUnsupportedLevel(
7994
field: ModelLevelField,
@@ -84,7 +99,7 @@ function dropUnsupportedLevel(
8499
logger.warn('Model does not support this level; dropping it from the request', {
85100
field,
86101
model,
87-
value,
102+
value: describeLevel(value),
88103
})
89104
}
90105
return undefined
@@ -111,7 +126,7 @@ function warnOnUnrecognizedLevel(
111126
logger.warn('Model level is not one this model declares; forwarding to the provider', {
112127
field,
113128
model,
114-
value,
129+
value: describeLevel(value),
115130
declaredValues,
116131
})
117132
}
@@ -128,23 +143,32 @@ function sanitizeRequest(request: ProviderRequest): ProviderRequest {
128143
sanitizedRequest.temperature = undefined
129144
}
130145

131-
if (model && !supportsReasoningEffort(model)) {
146+
/**
147+
* A model absent from the catalogue is unknown, not known-incapable. Since the model can be
148+
* bound to a reference, that is exactly how a newly released model arrives before Sim has
149+
* catalogued it — so its levels are forwarded and the provider decides, rather than being
150+
* discarded on the strength of a list that has not caught up. Models the catalogue does
151+
* know, and every dynamic-provider id, keep the protective drop.
152+
*/
153+
const isCatalogued = Boolean(model) && isKnownModelId(model)
154+
155+
if (model && isCatalogued && !supportsReasoningEffort(model)) {
132156
sanitizedRequest.reasoningEffort = dropUnsupportedLevel(
133157
'reasoningEffort',
134158
model,
135159
sanitizedRequest.reasoningEffort
136160
)
137161
}
138162

139-
if (model && !supportsVerbosity(model)) {
163+
if (model && isCatalogued && !supportsVerbosity(model)) {
140164
sanitizedRequest.verbosity = dropUnsupportedLevel(
141165
'verbosity',
142166
model,
143167
sanitizedRequest.verbosity
144168
)
145169
}
146170

147-
if (model && !supportsThinking(model)) {
171+
if (model && isCatalogued && !supportsThinking(model)) {
148172
sanitizedRequest.thinkingLevel = dropUnsupportedLevel(
149173
'thinkingLevel',
150174
model,

apps/sim/providers/models.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4553,6 +4553,33 @@ export function getThinkingLevelsForModel(modelId: string): string[] | null {
45534553
return capability?.levels ?? null
45544554
}
45554555

4556+
const ALL_MODEL_LEVEL_VALUES = new Set<string>()
4557+
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
4558+
for (const model of provider.models) {
4559+
for (const value of model.capabilities.reasoningEffort?.values ?? []) {
4560+
ALL_MODEL_LEVEL_VALUES.add(value)
4561+
}
4562+
for (const value of model.capabilities.verbosity?.values ?? []) {
4563+
ALL_MODEL_LEVEL_VALUES.add(value)
4564+
}
4565+
for (const level of model.capabilities.thinking?.levels ?? []) {
4566+
ALL_MODEL_LEVEL_VALUES.add(level)
4567+
}
4568+
}
4569+
}
4570+
4571+
/**
4572+
* Whether a string is a tuning level some model in the catalogue declares, regardless of which.
4573+
*
4574+
* Callers that need to put a caller-supplied level into a log or an error gate on this first.
4575+
* These fields accept variable and environment references, so an unrecognized value is not
4576+
* necessarily a mistyped level — it can be whatever that reference resolved to, up to and
4577+
* including secret content that must never be echoed.
4578+
*/
4579+
export function isKnownModelLevelValue(value: string): boolean {
4580+
return ALL_MODEL_LEVEL_VALUES.has(value)
4581+
}
4582+
45564583
/**
45574584
* Per-provider defaults for thinking stream visibility, used when a model does
45584585
* not declare `capabilities.thinking.streamed` explicitly. Gemini and OpenAI

0 commit comments

Comments
 (0)