Skip to content

Commit 4f19e41

Browse files
committed
fix(agent): map the non-OpenAI finish reasons compatible vendors actually emit
1 parent 7576b19 commit 4f19e41

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

apps/sim/providers/finish-reason.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,39 @@ describe('normalizeFinishReason', () => {
6666
expect(normalizeFinishReason('pause_turn')).toBe('other')
6767
})
6868

69+
/**
70+
* Values confirmed against each vendor's own enum by a per-provider documentation
71+
* sweep. Several are the routine success path on open-weight models, so leaving
72+
* them unclassified would misreport healthy runs.
73+
*/
74+
it('classifies the non-OpenAI vocabularies of compatible vendors', () => {
75+
expect(normalizeFinishReason('model_length')).toBe('length') // Mistral
76+
expect(normalizeFinishReason('eos')).toBe('stop') // Together
77+
expect(normalizeFinishReason('eos_token')).toBe('stop') // LiteLLM/HuggingFace
78+
expect(normalizeFinishReason('error')).toBe('error') // Mistral, OpenRouter, Together
79+
expect(normalizeFinishReason('insufficient_system_resource')).toBe('error') // DeepSeek
80+
expect(normalizeFinishReason('network_error')).toBe('error') // Z.ai
81+
expect(normalizeFinishReason('sensitive')).toBe('content_filter') // Z.ai
82+
})
83+
84+
it('classifies the Gemini and Vertex values absent from the TS enum', () => {
85+
expect(normalizeFinishReason('MODEL_ARMOR')).toBe('content_filter') // Vertex only
86+
expect(normalizeFinishReason('ESCALATION')).toBe('content_filter')
87+
expect(normalizeFinishReason('MALFORMED_RESPONSE')).toBe('error')
88+
expect(normalizeFinishReason('MISSING_THOUGHT_SIGNATURE')).toBe('error')
89+
})
90+
91+
/**
92+
* A server-aborted tool loop is not a request to execute tools, and a repetition
93+
* cutoff is a normal finish rather than a failure — both would mislead a workflow
94+
* branching on the value.
95+
*/
96+
it('does not overclaim on aborted or degenerate stops', () => {
97+
expect(normalizeFinishReason('too_many_tool_calls')).toBe('other')
98+
expect(normalizeFinishReason('repetition')).toBe('other')
99+
expect(normalizeFinishReason('abort')).toBe('other')
100+
})
101+
69102
it('degrades an unrecognized value to other rather than throwing', () => {
70103
expect(normalizeFinishReason('some_future_reason')).toBe('other')
71104
expect(normalizeFinishReason('OTHER')).toBe('other')

apps/sim/providers/finish-reason.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type AgentFinishReason =
1616
| 'tool_calls'
1717
/** Blocked or refused by a safety system. */
1818
| 'content_filter'
19-
/** The provider reported the generation itself as malformed. */
19+
/** The provider reported the generation itself as failed or malformed. */
2020
| 'error'
2121
/** Reported, but not a case this vocabulary distinguishes. */
2222
| 'other'
@@ -41,6 +41,37 @@ const NORMALIZED_BY_RAW = new Map<string, AgentFinishReason>([
4141
// OpenAI Responses reports truncation through `incomplete_details.reason`.
4242
['max_output_tokens', 'length'],
4343

44+
/** Mistral separates the model's own max length from the caller's `max_tokens`. */
45+
['model_length', 'length'],
46+
47+
/**
48+
* Natural end-of-sequence. Together and HuggingFace-backed proxies report the
49+
* model's EOS token separately from a caller-supplied stop sequence, so without
50+
* these the routine success path on open-weight models is unclassified.
51+
*/
52+
['eos', 'stop'],
53+
['eos_token', 'stop'],
54+
55+
/**
56+
* A generation the provider itself reported as failed. Mistral, OpenRouter,
57+
* Together and Fireworks all spell this `error`; the DeepSeek and Z.ai values are
58+
* the same class with a stated cause.
59+
*/
60+
['error', 'error'],
61+
['insufficient_system_resource', 'error'],
62+
['network_error', 'error'],
63+
64+
/** Z.ai (GLM) sensitive-content block. */
65+
['sensitive', 'content_filter'],
66+
67+
/**
68+
* Cut short with no outcome to report, on vLLM and NIM-hosted models. vLLM treats
69+
* a repetition cutoff as a normal finish rather than a failure, so it is not
70+
* `error`.
71+
*/
72+
['abort', 'other'],
73+
['repetition', 'other'],
74+
4475
// Anthropic Messages, shared by Bedrock's Converse API.
4576
['end_turn', 'stop'],
4677
['stop_sequence', 'stop'],
@@ -72,6 +103,12 @@ const NORMALIZED_BY_RAW = new Map<string, AgentFinishReason>([
72103
['image_recitation', 'content_filter'],
73104
['malformed_function_call', 'error'],
74105
['unexpected_tool_call', 'error'],
106+
['malformed_response', 'error'],
107+
['missing_thought_signature', 'error'],
108+
['model_armor', 'content_filter'],
109+
['escalation', 'content_filter'],
110+
/** A runaway tool loop the server aborted, not a request to execute tools. */
111+
['too_many_tool_calls', 'other'],
75112
['language', 'other'],
76113
['other', 'other'],
77114
['no_image', 'other'],

0 commit comments

Comments
 (0)