praxis-agent 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/interactive.js +2 -0
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +11 -1
- package/dist/core/runtime.d.ts +12 -1
- package/dist/core/runtime.js +25 -0
- package/dist/providers/anthropic-compatible.js +85 -11
- package/dist/providers/fallback-provider.js +9 -0
- package/dist/providers/openai-compatible.js +92 -8
- package/dist/providers/provider-errors.d.ts +3 -0
- package/dist/providers/provider-errors.js +9 -0
- package/package.json +1 -1
package/dist/cli/interactive.js
CHANGED
|
@@ -1294,6 +1294,8 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1294
1294
|
text: `API retry ${event.attempt}/${event.maxRetries} · ${event.error}`,
|
|
1295
1295
|
});
|
|
1296
1296
|
break;
|
|
1297
|
+
case 'terminal':
|
|
1298
|
+
break;
|
|
1297
1299
|
case 'elicitation-complete':
|
|
1298
1300
|
append({
|
|
1299
1301
|
kind: 'notice',
|
package/dist/cli/protocol.d.ts
CHANGED
package/dist/cli/protocol.js
CHANGED
|
@@ -1666,6 +1666,7 @@ export class StreamJsonOutput {
|
|
|
1666
1666
|
turnThinking = [];
|
|
1667
1667
|
turnCalls = [];
|
|
1668
1668
|
turnUsage = emptyUsage();
|
|
1669
|
+
turnTerminalReason;
|
|
1669
1670
|
turnActive = false;
|
|
1670
1671
|
assistantFlushed = true;
|
|
1671
1672
|
contentStarted = false;
|
|
@@ -1884,6 +1885,11 @@ export class StreamJsonOutput {
|
|
|
1884
1885
|
this.turnUsage = event.usage;
|
|
1885
1886
|
return;
|
|
1886
1887
|
}
|
|
1888
|
+
if (event.type === 'terminal') {
|
|
1889
|
+
this.ensureTurn();
|
|
1890
|
+
this.turnTerminalReason = event.reason;
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1887
1893
|
if (event.type === 'permission-decision') {
|
|
1888
1894
|
this.flushAssistant();
|
|
1889
1895
|
return;
|
|
@@ -2111,6 +2117,7 @@ export class StreamJsonOutput {
|
|
|
2111
2117
|
this.turnThinking = [];
|
|
2112
2118
|
this.turnCalls = [];
|
|
2113
2119
|
this.turnUsage = emptyUsage();
|
|
2120
|
+
this.turnTerminalReason = undefined;
|
|
2114
2121
|
this.turnActive = true;
|
|
2115
2122
|
this.assistantFlushed = false;
|
|
2116
2123
|
this.contentStarted = false;
|
|
@@ -2224,7 +2231,10 @@ export class StreamJsonOutput {
|
|
|
2224
2231
|
event: {
|
|
2225
2232
|
type: 'message_delta',
|
|
2226
2233
|
delta: {
|
|
2227
|
-
stop_reason: this.
|
|
2234
|
+
stop_reason: this.turnTerminalReason === 'prompt_too_long'
|
|
2235
|
+
? 'model_context_window_exceeded'
|
|
2236
|
+
: (this.turnTerminalReason ??
|
|
2237
|
+
(this.turnCalls.length > 0 ? 'tool_use' : 'end_turn')),
|
|
2228
2238
|
stop_sequence: null,
|
|
2229
2239
|
},
|
|
2230
2240
|
usage: { output_tokens: this.turnUsage.outputTokens },
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface ModelWebSearch {
|
|
|
78
78
|
blockedDomains?: readonly string[];
|
|
79
79
|
maxUses: number;
|
|
80
80
|
}
|
|
81
|
+
export type ModelTerminalReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'prompt_too_long';
|
|
81
82
|
export type ModelStreamEvent = {
|
|
82
83
|
type: 'text-delta';
|
|
83
84
|
delta: string;
|
|
@@ -102,6 +103,9 @@ export type ModelStreamEvent = {
|
|
|
102
103
|
} | {
|
|
103
104
|
type: 'usage';
|
|
104
105
|
usage: ModelUsage;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'terminal';
|
|
108
|
+
reason: ModelTerminalReason;
|
|
105
109
|
} | {
|
|
106
110
|
type: 'api-retry';
|
|
107
111
|
attempt: number;
|
|
@@ -135,6 +139,8 @@ export interface ModelProviderCapabilities {
|
|
|
135
139
|
};
|
|
136
140
|
contextWindowTokens?: number;
|
|
137
141
|
maxOutputTokens?: number;
|
|
142
|
+
/** The provider emits exactly one terminal event as its final stream event. */
|
|
143
|
+
terminalReasons?: boolean;
|
|
138
144
|
}
|
|
139
145
|
export interface ModelProvider {
|
|
140
146
|
readonly capabilities: ModelProviderCapabilities;
|
|
@@ -170,6 +176,9 @@ export type RuntimeEvent = {
|
|
|
170
176
|
} | {
|
|
171
177
|
type: 'usage';
|
|
172
178
|
usage: ModelUsage;
|
|
179
|
+
} | {
|
|
180
|
+
type: 'terminal';
|
|
181
|
+
reason: ModelTerminalReason;
|
|
173
182
|
} | {
|
|
174
183
|
type: 'tool-call';
|
|
175
184
|
call: ModelToolCall;
|
|
@@ -480,8 +489,10 @@ export declare class ModelProviderError extends Error {
|
|
|
480
489
|
readonly retryable: boolean;
|
|
481
490
|
readonly status?: number;
|
|
482
491
|
readonly retryDelayMs?: number;
|
|
492
|
+
readonly kind?: ProviderErrorKind;
|
|
483
493
|
constructor(message: string, options: {
|
|
484
494
|
retryable: boolean;
|
|
495
|
+
kind?: ProviderErrorKind;
|
|
485
496
|
status?: number;
|
|
486
497
|
retryDelayMs?: number;
|
|
487
498
|
cause?: unknown;
|
|
@@ -492,7 +503,7 @@ export declare class AgentRunCancelledError extends Error {
|
|
|
492
503
|
constructor();
|
|
493
504
|
}
|
|
494
505
|
export type RuntimeEventSink = (event: RuntimeEvent) => void;
|
|
495
|
-
export type ProviderErrorKind = 'authentication_failed' | 'billing_error' | 'rate_limit' | 'invalid_request' | 'server_error' | 'unknown' | 'max_output_tokens';
|
|
506
|
+
export type ProviderErrorKind = 'authentication_failed' | 'billing_error' | 'rate_limit' | 'invalid_request' | 'server_error' | 'timeout' | 'overloaded' | 'api_error' | 'prompt_too_long' | 'transport_error' | 'cancelled' | 'unknown' | 'max_output_tokens';
|
|
496
507
|
export declare function modelProviderErrorKind(error: ModelProviderError): ProviderErrorKind;
|
|
497
508
|
export declare class AgentRuntime {
|
|
498
509
|
private readonly provider;
|
package/dist/core/runtime.js
CHANGED
|
@@ -20,9 +20,12 @@ export class ModelProviderError extends Error {
|
|
|
20
20
|
retryable;
|
|
21
21
|
status;
|
|
22
22
|
retryDelayMs;
|
|
23
|
+
kind;
|
|
23
24
|
constructor(message, options) {
|
|
24
25
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
25
26
|
this.retryable = options.retryable;
|
|
27
|
+
if (options.kind !== undefined)
|
|
28
|
+
this.kind = options.kind;
|
|
26
29
|
if (options.status !== undefined)
|
|
27
30
|
this.status = options.status;
|
|
28
31
|
if (options.retryDelayMs !== undefined)
|
|
@@ -37,6 +40,8 @@ export class AgentRunCancelledError extends Error {
|
|
|
37
40
|
}
|
|
38
41
|
const emptyUsage = () => ({ inputTokens: 0, outputTokens: 0 });
|
|
39
42
|
export function modelProviderErrorKind(error) {
|
|
43
|
+
if (error.kind !== undefined)
|
|
44
|
+
return error.kind;
|
|
40
45
|
if (error.status === 401 || error.status === 403)
|
|
41
46
|
return 'authentication_failed';
|
|
42
47
|
if (error.status === 402)
|
|
@@ -341,6 +346,7 @@ export class AgentRuntime {
|
|
|
341
346
|
let turnUsage = emptyUsage();
|
|
342
347
|
let streaming = false;
|
|
343
348
|
const toolCalls = [];
|
|
349
|
+
let terminalReason;
|
|
344
350
|
const apiStartedAt = request.collectMetrics ? performance.now() : 0;
|
|
345
351
|
let turnApiDurationMs = 0;
|
|
346
352
|
let turnApiDurationWithoutRetriesMs;
|
|
@@ -349,6 +355,9 @@ export class AgentRuntime {
|
|
|
349
355
|
for await (const event of this.provider.complete(providerRequest)) {
|
|
350
356
|
if (request.signal?.aborted)
|
|
351
357
|
return this.cancel();
|
|
358
|
+
if (terminalReason !== undefined) {
|
|
359
|
+
throw new ModelProviderError(`Provider emitted ${event.type} after terminal reason ${terminalReason}`, { retryable: false });
|
|
360
|
+
}
|
|
352
361
|
if (event.type === 'api-retry') {
|
|
353
362
|
this.emit(event);
|
|
354
363
|
continue;
|
|
@@ -414,6 +423,10 @@ export class AgentRuntime {
|
|
|
414
423
|
toolCalls.push(event.call);
|
|
415
424
|
this.emit(event);
|
|
416
425
|
}
|
|
426
|
+
else if (event.type === 'terminal') {
|
|
427
|
+
terminalReason = event.reason;
|
|
428
|
+
this.emit(event);
|
|
429
|
+
}
|
|
417
430
|
else {
|
|
418
431
|
turnUsage = event.usage;
|
|
419
432
|
this.emit(event);
|
|
@@ -427,6 +440,18 @@ export class AgentRuntime {
|
|
|
427
440
|
unrecordedDurationApiMs = addApiDurationMetric(turnApiDurationMs, unrecordedDurationApiMs, 'durationApiMs');
|
|
428
441
|
}
|
|
429
442
|
}
|
|
443
|
+
if (this.provider.capabilities.terminalReasons === true &&
|
|
444
|
+
terminalReason === undefined) {
|
|
445
|
+
throw new ModelProviderError('Provider stream ended without a terminal reason', { retryable: true });
|
|
446
|
+
}
|
|
447
|
+
if (terminalReason === 'tool_use' && toolCalls.length === 0) {
|
|
448
|
+
throw new ModelProviderError('Provider reported tool_use without a completed tool call', { retryable: false });
|
|
449
|
+
}
|
|
450
|
+
if (terminalReason !== undefined &&
|
|
451
|
+
terminalReason !== 'tool_use' &&
|
|
452
|
+
toolCalls.length > 0) {
|
|
453
|
+
throw new ModelProviderError(`Provider reported ${terminalReason} with completed tool calls`, { retryable: false });
|
|
454
|
+
}
|
|
430
455
|
if (request.collectMetrics) {
|
|
431
456
|
const turnApiDurationWithoutRetriesMsResolved = turnApiDurationWithoutRetriesMs ?? turnApiDurationMs;
|
|
432
457
|
durationApiWithoutRetriesMs = addApiDurationMetric(turnApiDurationWithoutRetriesMsResolved, durationApiWithoutRetriesMs, 'durationApiWithoutRetriesMs');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ModelProviderError, } from '../core/runtime.js';
|
|
2
|
+
import { transportFailureKind } from './provider-errors.js';
|
|
2
3
|
function isRecord(value) {
|
|
3
4
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
5
|
}
|
|
@@ -61,6 +62,52 @@ function readErrorMessage(value, status) {
|
|
|
61
62
|
}
|
|
62
63
|
return `Provider request failed with HTTP ${status}`;
|
|
63
64
|
}
|
|
65
|
+
function isPromptTooLongError(type, message) {
|
|
66
|
+
return (['prompt_too_long', 'context_length_exceeded'].includes(type) ||
|
|
67
|
+
/prompt\s+(?:is\s+)?too long|context.{0,80}(?:exceed|too long)|maximum context length/iu.test(message));
|
|
68
|
+
}
|
|
69
|
+
function anthropicErrorKind(value, status) {
|
|
70
|
+
const error = isRecord(value) && isRecord(value.error) ? value.error : value;
|
|
71
|
+
const type = isRecord(error) && typeof error.type === 'string' ? error.type : '';
|
|
72
|
+
const message = isRecord(error) && typeof error.message === 'string' ? error.message : '';
|
|
73
|
+
if (isPromptTooLongError(type, message))
|
|
74
|
+
return 'prompt_too_long';
|
|
75
|
+
if (type === 'authentication_error' || status === 401 || status === 403)
|
|
76
|
+
return 'authentication_failed';
|
|
77
|
+
if (type === 'billing_error' || status === 402)
|
|
78
|
+
return 'billing_error';
|
|
79
|
+
if (type === 'rate_limit_error' || status === 429)
|
|
80
|
+
return 'rate_limit';
|
|
81
|
+
if (type === 'overloaded_error' || status === 529)
|
|
82
|
+
return 'overloaded';
|
|
83
|
+
if (type === 'api_error')
|
|
84
|
+
return 'api_error';
|
|
85
|
+
if (status === 408)
|
|
86
|
+
return 'timeout';
|
|
87
|
+
if (type === 'invalid_request_error')
|
|
88
|
+
return 'invalid_request';
|
|
89
|
+
if (status !== undefined && status >= 400 && status < 500)
|
|
90
|
+
return 'invalid_request';
|
|
91
|
+
if (status !== undefined && status >= 500)
|
|
92
|
+
return 'server_error';
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
function anthropicStopReason(value) {
|
|
96
|
+
if (value === 'end_turn' || value === 'stop_sequence' || value === 'refusal')
|
|
97
|
+
return 'end_turn';
|
|
98
|
+
if (value === 'tool_use')
|
|
99
|
+
return 'tool_use';
|
|
100
|
+
if (value === 'max_tokens')
|
|
101
|
+
return 'max_tokens';
|
|
102
|
+
if (value === 'model_context_window_exceeded')
|
|
103
|
+
return 'prompt_too_long';
|
|
104
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
105
|
+
throw new ModelProviderError('Provider message delta is missing stop reason', {
|
|
106
|
+
retryable: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
throw new ModelProviderError(`Provider returned unsupported stop reason ${value}`, { retryable: false });
|
|
110
|
+
}
|
|
64
111
|
function completedToolCall(state, index) {
|
|
65
112
|
const pending = state.tools.get(index);
|
|
66
113
|
if (!pending)
|
|
@@ -105,12 +152,14 @@ function parseSseEvent(data, state, maxToolArgumentsBytes, maxToolCallsPerRespon
|
|
|
105
152
|
const message = typeof error.message === 'string'
|
|
106
153
|
? error.message
|
|
107
154
|
: 'Provider stream returned an error';
|
|
155
|
+
const kind = anthropicErrorKind(error);
|
|
108
156
|
const retryable = [
|
|
109
157
|
'api_error',
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
|
|
113
|
-
|
|
158
|
+
'overloaded',
|
|
159
|
+
'rate_limit',
|
|
160
|
+
'timeout',
|
|
161
|
+
].includes(kind);
|
|
162
|
+
throw new ModelProviderError(message, { kind, retryable });
|
|
114
163
|
}
|
|
115
164
|
if (value.type === 'message_start') {
|
|
116
165
|
if (state.messageStarted || !isRecord(value.message)) {
|
|
@@ -344,6 +393,17 @@ function parseSseEvent(data, state, maxToolArgumentsBytes, maxToolCallsPerRespon
|
|
|
344
393
|
throw new ModelProviderError('Provider returned an invalid message delta', { retryable: false });
|
|
345
394
|
}
|
|
346
395
|
state.messageDeltaSeen = true;
|
|
396
|
+
if (value.delta !== undefined && !isRecord(value.delta)) {
|
|
397
|
+
throw new ModelProviderError('Provider returned an invalid message delta', {
|
|
398
|
+
retryable: false,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (isRecord(value.delta) && value.delta.stop_reason !== undefined) {
|
|
402
|
+
if (state.terminalReason !== undefined) {
|
|
403
|
+
throw new ModelProviderError('Provider returned multiple terminal stop reasons', { retryable: false });
|
|
404
|
+
}
|
|
405
|
+
state.terminalReason = anthropicStopReason(value.delta.stop_reason);
|
|
406
|
+
}
|
|
347
407
|
if (typeof value.usage.output_tokens === 'number') {
|
|
348
408
|
state.outputTokens = value.usage.output_tokens;
|
|
349
409
|
state.usageSeen = true;
|
|
@@ -360,6 +420,11 @@ function parseSseEvent(data, state, maxToolArgumentsBytes, maxToolCallsPerRespon
|
|
|
360
420
|
if (!state.messageDeltaSeen) {
|
|
361
421
|
throw new ModelProviderError('Provider stopped before the terminal message delta', { retryable: false });
|
|
362
422
|
}
|
|
423
|
+
if (state.terminalReason === undefined) {
|
|
424
|
+
throw new ModelProviderError('Provider message delta is missing stop reason', {
|
|
425
|
+
retryable: false,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
363
428
|
state.terminal = true;
|
|
364
429
|
const events = [];
|
|
365
430
|
if (state.usageSeen) {
|
|
@@ -381,6 +446,7 @@ function parseSseEvent(data, state, maxToolArgumentsBytes, maxToolCallsPerRespon
|
|
|
381
446
|
});
|
|
382
447
|
state.usageSeen = false;
|
|
383
448
|
}
|
|
449
|
+
events.push({ type: 'terminal', reason: state.terminalReason });
|
|
384
450
|
return events;
|
|
385
451
|
}
|
|
386
452
|
return [];
|
|
@@ -532,6 +598,7 @@ export class AnthropicCompatibleProvider {
|
|
|
532
598
|
? {}
|
|
533
599
|
: { contextWindowTokens: options.contextWindowTokens }),
|
|
534
600
|
maxOutputTokens: this.maxOutputTokens,
|
|
601
|
+
terminalReasons: true,
|
|
535
602
|
};
|
|
536
603
|
this.thinking = validateThinking(options.thinking);
|
|
537
604
|
this.anthropicVersion = options.anthropicVersion ?? '2023-06-01';
|
|
@@ -618,10 +685,14 @@ export class AnthropicCompatibleProvider {
|
|
|
618
685
|
response = await this.fetchImplementation(this.endpoint, requestInit);
|
|
619
686
|
}
|
|
620
687
|
catch (error) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
688
|
+
const kind = transportFailureKind(error, request.signal);
|
|
689
|
+
throw new ModelProviderError(kind === 'cancelled'
|
|
690
|
+
? 'Provider request cancelled'
|
|
691
|
+
: kind === 'timeout'
|
|
692
|
+
? 'Provider request timed out'
|
|
693
|
+
: 'Provider transport failed', {
|
|
694
|
+
kind,
|
|
695
|
+
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
625
696
|
cause: error,
|
|
626
697
|
});
|
|
627
698
|
}
|
|
@@ -669,12 +740,14 @@ export class AnthropicCompatibleProvider {
|
|
|
669
740
|
payload = null;
|
|
670
741
|
}
|
|
671
742
|
throw new ModelProviderError(readErrorMessage(payload, response.status), {
|
|
743
|
+
kind: anthropicErrorKind(payload, response.status),
|
|
672
744
|
retryable: isRetryableStatus(response.status),
|
|
673
745
|
status: response.status,
|
|
674
746
|
});
|
|
675
747
|
}
|
|
676
748
|
if (!response.body) {
|
|
677
749
|
throw new ModelProviderError('Provider response has no body', {
|
|
750
|
+
kind: 'transport_error',
|
|
678
751
|
retryable: true,
|
|
679
752
|
});
|
|
680
753
|
}
|
|
@@ -734,11 +807,12 @@ export class AnthropicCompatibleProvider {
|
|
|
734
807
|
}
|
|
735
808
|
}
|
|
736
809
|
catch (error) {
|
|
737
|
-
if (error instanceof ModelProviderError
|
|
810
|
+
if (error instanceof ModelProviderError)
|
|
738
811
|
throw error;
|
|
739
|
-
|
|
812
|
+
const kind = transportFailureKind(error, request.signal);
|
|
740
813
|
throw new ModelProviderError('Provider stream failed', {
|
|
741
|
-
|
|
814
|
+
kind,
|
|
815
|
+
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
742
816
|
cause: error,
|
|
743
817
|
});
|
|
744
818
|
}
|
|
@@ -35,7 +35,11 @@ export class FallbackModelProvider {
|
|
|
35
35
|
// cannot duplicate text/tool events when retried.
|
|
36
36
|
const events = [];
|
|
37
37
|
let attemptDurationMs;
|
|
38
|
+
let terminalSeen = false;
|
|
38
39
|
for await (const event of provider.complete(request)) {
|
|
40
|
+
if (terminalSeen) {
|
|
41
|
+
throw new ModelProviderError(`Provider emitted ${event.type} after its terminal event`, { retryable: false });
|
|
42
|
+
}
|
|
39
43
|
if (event.type === 'api-attempt-duration') {
|
|
40
44
|
// Consume the underlying attempt timing rather than replaying it
|
|
41
45
|
// so nested wrappers report a single retry-free duration.
|
|
@@ -51,8 +55,13 @@ export class FallbackModelProvider {
|
|
|
51
55
|
attemptDurationMs = durationMs;
|
|
52
56
|
continue;
|
|
53
57
|
}
|
|
58
|
+
if (event.type === 'terminal')
|
|
59
|
+
terminalSeen = true;
|
|
54
60
|
events.push(event);
|
|
55
61
|
}
|
|
62
|
+
if (provider.capabilities.terminalReasons === true && !terminalSeen) {
|
|
63
|
+
throw new ModelProviderError('Provider stream ended without a terminal reason', { retryable: true });
|
|
64
|
+
}
|
|
56
65
|
yield {
|
|
57
66
|
type: 'api-attempt-duration',
|
|
58
67
|
durationMs: attemptDurationMs ??
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ModelProviderError, } from '../core/runtime.js';
|
|
2
|
+
import { transportFailureKind } from './provider-errors.js';
|
|
2
3
|
function isRecord(value) {
|
|
3
4
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
5
|
}
|
|
@@ -13,6 +14,48 @@ function readErrorMessage(value, status) {
|
|
|
13
14
|
}
|
|
14
15
|
return `Provider request failed with HTTP ${status}`;
|
|
15
16
|
}
|
|
17
|
+
function openAiFinishReason(value) {
|
|
18
|
+
if (value === 'stop' || value === 'content_filter')
|
|
19
|
+
return 'end_turn';
|
|
20
|
+
if (value === 'tool_calls' || value === 'function_call')
|
|
21
|
+
return 'tool_use';
|
|
22
|
+
if (value === 'length')
|
|
23
|
+
return 'max_tokens';
|
|
24
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
25
|
+
throw new ModelProviderError('Provider stream is missing finish reason', {
|
|
26
|
+
retryable: false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
throw new ModelProviderError(`Provider returned unsupported finish reason ${value}`, { retryable: false });
|
|
30
|
+
}
|
|
31
|
+
function openAiErrorKind(value, status) {
|
|
32
|
+
const error = isRecord(value) && isRecord(value.error) ? value.error : value;
|
|
33
|
+
const type = isRecord(error) && typeof error.type === 'string' ? error.type : '';
|
|
34
|
+
const code = isRecord(error) && typeof error.code === 'string' ? error.code : '';
|
|
35
|
+
const message = isRecord(error) && typeof error.message === 'string' ? error.message : '';
|
|
36
|
+
if (['context_length_exceeded', 'prompt_too_long'].includes(type) ||
|
|
37
|
+
['context_length_exceeded', 'prompt_too_long'].includes(code) ||
|
|
38
|
+
/prompt\s+(?:is\s+)?too long|context.{0,80}(?:exceed|too long)|maximum context length/iu.test(message)) {
|
|
39
|
+
return 'prompt_too_long';
|
|
40
|
+
}
|
|
41
|
+
if (status === 401 || status === 403)
|
|
42
|
+
return 'authentication_failed';
|
|
43
|
+
if (status === 402)
|
|
44
|
+
return 'billing_error';
|
|
45
|
+
if (status === 408)
|
|
46
|
+
return 'timeout';
|
|
47
|
+
if (type === 'rate_limit_error' || status === 429)
|
|
48
|
+
return 'rate_limit';
|
|
49
|
+
if (type === 'overloaded_error' || status === 529)
|
|
50
|
+
return 'overloaded';
|
|
51
|
+
if (type === 'api_error' || type === 'server_error')
|
|
52
|
+
return 'api_error';
|
|
53
|
+
if (status !== undefined && status >= 400 && status < 500)
|
|
54
|
+
return 'invalid_request';
|
|
55
|
+
if (status !== undefined && status >= 500)
|
|
56
|
+
return 'server_error';
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
16
59
|
function completedToolCallEvents(pending) {
|
|
17
60
|
const events = [...pending.calls.entries()]
|
|
18
61
|
.sort(([left], [right]) => left - right)
|
|
@@ -42,9 +85,18 @@ function completedToolCallEvents(pending) {
|
|
|
42
85
|
}
|
|
43
86
|
function parseSseEvent(data, pendingTools, maxToolArgumentsBytes, maxToolCallsPerResponse, maxToolMetadataBytes) {
|
|
44
87
|
if (data === '[DONE]') {
|
|
88
|
+
if (pendingTools.terminalReason === undefined) {
|
|
89
|
+
throw new ModelProviderError('Provider stream is missing finish reason', {
|
|
90
|
+
retryable: false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
45
93
|
pendingTools.done = true;
|
|
46
94
|
pendingTools.terminal = true;
|
|
47
|
-
|
|
95
|
+
pendingTools.terminalEmitted = true;
|
|
96
|
+
return [
|
|
97
|
+
...completedToolCallEvents(pendingTools),
|
|
98
|
+
{ type: 'terminal', reason: pendingTools.terminalReason },
|
|
99
|
+
];
|
|
48
100
|
}
|
|
49
101
|
let value;
|
|
50
102
|
try {
|
|
@@ -58,6 +110,16 @@ function parseSseEvent(data, pendingTools, maxToolArgumentsBytes, maxToolCallsPe
|
|
|
58
110
|
}
|
|
59
111
|
if (!isRecord(value))
|
|
60
112
|
return [];
|
|
113
|
+
if (isRecord(value.error)) {
|
|
114
|
+
const kind = openAiErrorKind(value);
|
|
115
|
+
const message = typeof value.error.message === 'string'
|
|
116
|
+
? value.error.message
|
|
117
|
+
: 'Provider stream returned an error';
|
|
118
|
+
throw new ModelProviderError(message, {
|
|
119
|
+
kind,
|
|
120
|
+
retryable: ['api_error', 'overloaded', 'rate_limit', 'timeout'].includes(kind),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
61
123
|
const events = [];
|
|
62
124
|
const choices = value.choices;
|
|
63
125
|
if (Array.isArray(choices)) {
|
|
@@ -106,7 +168,11 @@ function parseSseEvent(data, pendingTools, maxToolArgumentsBytes, maxToolCallsPe
|
|
|
106
168
|
if (isRecord(first) &&
|
|
107
169
|
first.finish_reason !== null &&
|
|
108
170
|
first.finish_reason !== undefined) {
|
|
171
|
+
if (pendingTools.terminalReason !== undefined) {
|
|
172
|
+
throw new ModelProviderError('Provider returned multiple finish reasons', { retryable: false });
|
|
173
|
+
}
|
|
109
174
|
pendingTools.terminal = true;
|
|
175
|
+
pendingTools.terminalReason = openAiFinishReason(first.finish_reason);
|
|
110
176
|
if (pendingTools.calls.size > 0) {
|
|
111
177
|
events.push(...completedToolCallEvents(pendingTools));
|
|
112
178
|
}
|
|
@@ -219,6 +285,7 @@ export class OpenAICompatibleProvider {
|
|
|
219
285
|
tools: true,
|
|
220
286
|
images: true,
|
|
221
287
|
thinking: { modes: ['disabled'], maxTokens: false },
|
|
288
|
+
terminalReasons: true,
|
|
222
289
|
...(options.contextWindowTokens === undefined
|
|
223
290
|
? {}
|
|
224
291
|
: { contextWindowTokens: options.contextWindowTokens }),
|
|
@@ -279,10 +346,14 @@ export class OpenAICompatibleProvider {
|
|
|
279
346
|
response = await this.fetchImplementation(this.endpoint, requestInit);
|
|
280
347
|
}
|
|
281
348
|
catch (error) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
349
|
+
const kind = transportFailureKind(error, request.signal);
|
|
350
|
+
throw new ModelProviderError(kind === 'cancelled'
|
|
351
|
+
? 'Provider request cancelled'
|
|
352
|
+
: kind === 'timeout'
|
|
353
|
+
? 'Provider request timed out'
|
|
354
|
+
: 'Provider transport failed', {
|
|
355
|
+
kind,
|
|
356
|
+
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
286
357
|
cause: error,
|
|
287
358
|
});
|
|
288
359
|
}
|
|
@@ -330,12 +401,14 @@ export class OpenAICompatibleProvider {
|
|
|
330
401
|
payload = null;
|
|
331
402
|
}
|
|
332
403
|
throw new ModelProviderError(readErrorMessage(payload, response.status), {
|
|
404
|
+
kind: openAiErrorKind(payload, response.status),
|
|
333
405
|
retryable: isRetryableStatus(response.status),
|
|
334
406
|
status: response.status,
|
|
335
407
|
});
|
|
336
408
|
}
|
|
337
409
|
if (!response.body) {
|
|
338
410
|
throw new ModelProviderError('Provider response has no body', {
|
|
411
|
+
kind: 'transport_error',
|
|
339
412
|
retryable: true,
|
|
340
413
|
});
|
|
341
414
|
}
|
|
@@ -347,6 +420,7 @@ export class OpenAICompatibleProvider {
|
|
|
347
420
|
metadataBytes: 0,
|
|
348
421
|
done: false,
|
|
349
422
|
terminal: false,
|
|
423
|
+
terminalEmitted: false,
|
|
350
424
|
};
|
|
351
425
|
let streamEnded = false;
|
|
352
426
|
try {
|
|
@@ -383,13 +457,23 @@ export class OpenAICompatibleProvider {
|
|
|
383
457
|
if (!pendingTools.terminal) {
|
|
384
458
|
throw new ModelProviderError('Provider stream ended before a terminal event', { retryable: true });
|
|
385
459
|
}
|
|
460
|
+
if (!pendingTools.terminalEmitted) {
|
|
461
|
+
if (pendingTools.terminalReason === undefined) {
|
|
462
|
+
throw new ModelProviderError('Provider stream is missing finish reason', {
|
|
463
|
+
retryable: false,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
pendingTools.terminalEmitted = true;
|
|
467
|
+
yield { type: 'terminal', reason: pendingTools.terminalReason };
|
|
468
|
+
}
|
|
386
469
|
}
|
|
387
470
|
catch (error) {
|
|
388
|
-
if (error instanceof ModelProviderError
|
|
471
|
+
if (error instanceof ModelProviderError)
|
|
389
472
|
throw error;
|
|
390
|
-
|
|
473
|
+
const kind = transportFailureKind(error, request.signal);
|
|
391
474
|
throw new ModelProviderError('Provider stream failed', {
|
|
392
|
-
|
|
475
|
+
kind,
|
|
476
|
+
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
393
477
|
cause: error,
|
|
394
478
|
});
|
|
395
479
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function transportFailureKind(error, signal) {
|
|
2
|
+
const reason = signal?.reason;
|
|
3
|
+
if ((error instanceof Error && error.name === 'TimeoutError') ||
|
|
4
|
+
(reason instanceof Error && reason.name === 'TimeoutError')) {
|
|
5
|
+
return 'timeout';
|
|
6
|
+
}
|
|
7
|
+
return signal?.aborted ? 'cancelled' : 'transport_error';
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=provider-errors.js.map
|