Skip to content

Commit c77f940

Browse files
committed
fix(agent): harden fallback models after pre-landing review
Executor: hydrate the primary before the secret registries settle and fork again, re-forking per fallback provider, so file provenance stays in the result registry; project the fall-through warn against the failed attempt's error registry and reinstate it before a post-skip rethrow; prime a streaming candidate's first chunk when another candidate follows, so a tool-loop startup failure still falls back; never fall back on a deep-research follow-up turn; strip the sim-auto identity preamble from a named fallback's messages; treat a row key still in {{VAR}} form as no key; stop starting candidates after an abort; record failed models on every exit, clearing them when a retry succeeds, and only when the names project safely. Helpers: row tuning applies only while its field is shown; the editor's row transforms are pure functions; changing a row's model to another provider drops the key reference; Bedrock honors NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS. Editor: legacy Combobox to match the block editor, a row's own model is never disabled in its own picker, move controls only for two or more rows, the shared dashed add-row button, Create Secret wording, non-reference keys never rendered. Trace: one Failed model row per fallback. Export sanitizer uses the strict whole-reference check.
1 parent 54b8fb6 commit c77f940

16 files changed

Lines changed: 878 additions & 285 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,8 +694,8 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa
694694
if (span.tries !== undefined) metaEntries.push({ label: 'Tries', value: String(span.tries) })
695695
if (span.provider) metaEntries.push({ label: 'Provider', value: span.provider })
696696
if (span.model) metaEntries.push({ label: 'Model', value: span.model })
697-
if (span.modelFallbacks?.length) {
698-
metaEntries.push({ label: 'Fell back from', value: span.modelFallbacks.join(', ') })
697+
for (const failedModel of span.modelFallbacks ?? []) {
698+
metaEntries.push({ label: 'Failed model', value: failedModel })
699699
}
700700
if (span.finishReason) metaEntries.push({ label: 'Finish reason', value: span.finishReason })
701701
const ttftFormatted = formatTtft(span.ttft)

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/model-fallback-list/model-fallback-list.test.tsx‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ vi.mock('@sim/emcn', () => ({
3535
{children}
3636
</button>
3737
),
38-
ChipCombobox: ({
38+
Combobox: ({
3939
options,
4040
value,
4141
placeholder,
@@ -155,23 +155,35 @@ describe('ModelFallbackList', () => {
155155
expect(html).toContain('3rd choice')
156156
expect(html).not.toContain('Auto')
157157
expect(html).not.toContain('denied-model')
158-
/** The primary is never offered; a model already chosen is offered but disabled. */
158+
/** The primary is never offered. A model another row holds is disabled there, never in its own row. */
159159
expect(html).not.toContain('>claude-sonnet-5<')
160-
expect(html).toContain('data-disabled="true">gpt-5<')
160+
expect(html.match(/data-disabled="true">gpt-5</g)).toHaveLength(1)
161+
expect(html.match(/>gpt-5</g)).toHaveLength(2)
162+
expect(html).toContain('aria-label="Move up"')
163+
})
164+
165+
it('renders no move controls for a single row and never shows a non-reference key', () => {
166+
subBlockValues.fallbackModels = [
167+
{ id: 'r1', model: 'openrouter/x', apiKey: 'sk-raw-through-socket' },
168+
]
169+
const html = render()
170+
expect(html).not.toContain('aria-label="Move up"')
171+
expect(html).not.toContain('sk-raw-through-socket')
172+
expect(html).toContain('data-combobox="Select a secret" data-value=""')
161173
})
162174

163175
it('asks for an environment variable only when the row model needs its own key', () => {
164176
subBlockValues.fallbackModels = [{ id: 'r1', model: 'gpt-5' }]
165-
expect(render()).not.toContain('data-combobox="Select an environment variable"')
177+
expect(render()).not.toContain('data-combobox="Select a secret"')
166178

167179
subBlockValues.fallbackModels = [
168180
{ id: 'r1', model: 'openrouter/x', apiKey: '{{OPENROUTER_API_KEY}}' },
169181
]
170182
const html = render()
171-
expect(html).toContain('data-combobox="Select an environment variable"')
183+
expect(html).toContain('data-combobox="Select a secret"')
172184
expect(html).toContain('data-value="{{OPENROUTER_API_KEY}}"')
173185
expect(html).toContain('OPENROUTER_API_KEY')
174-
expect(html).toContain('Create variable')
186+
expect(html).toContain('Create Secret')
175187
})
176188

177189
it('shows a tuning field only for the knobs the helper says need one', () => {

0 commit comments

Comments
 (0)