@framers/agentos 0.10.29 → 0.10.31
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/README.md +2 -2
- package/dist/cognition/memory/retrieval/graph/graphrag/GraphRAGEngine.d.ts.map +1 -1
- package/dist/cognition/memory/retrieval/graph/graphrag/GraphRAGEngine.js +18 -3
- package/dist/cognition/memory/retrieval/graph/graphrag/GraphRAGEngine.js.map +1 -1
- package/dist/cognition/rag/vector_stores/PostgresVectorStore.d.ts +2 -0
- package/dist/cognition/rag/vector_stores/PostgresVectorStore.d.ts.map +1 -1
- package/dist/cognition/rag/vector_stores/PostgresVectorStore.js +25 -7
- package/dist/cognition/rag/vector_stores/PostgresVectorStore.js.map +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts +80 -0
- package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js +297 -34
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
- package/dist/core/storage/SqlStorageAdapter.d.ts.map +1 -1
- package/dist/core/storage/SqlStorageAdapter.js +22 -7
- package/dist/core/storage/SqlStorageAdapter.js.map +1 -1
- package/package.json +1 -1
|
@@ -18,16 +18,31 @@ export interface OpenRouterProviderConfig {
|
|
|
18
18
|
requestTimeout?: number;
|
|
19
19
|
streamRequestTimeout?: number;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* OpenRouter's error envelope. It arrives as an HTTP error body, as the
|
|
23
|
+
* body of a 200 whose provider failed after accepting the request, on a
|
|
24
|
+
* choice (`finish_reason: 'error'`), or as a mid-stream SSE event. `code`
|
|
25
|
+
* is the HTTP status the failure maps to; `metadata.error_type` is
|
|
26
|
+
* OpenRouter's stable typed code (the OpenRouter error reference).
|
|
27
|
+
*/
|
|
28
|
+
export interface OpenRouterErrorEnvelope {
|
|
29
|
+
code?: number | string;
|
|
30
|
+
message?: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
}
|
|
21
33
|
interface OpenRouterChatChoice {
|
|
22
34
|
index: number;
|
|
23
35
|
message?: {
|
|
24
36
|
role: ChatMessage['role'];
|
|
25
37
|
content: string | null;
|
|
26
38
|
tool_calls?: ChatMessage['tool_calls'];
|
|
39
|
+
/** Set when the model declined as output (`finish_reason: 'content_filter'`). */
|
|
40
|
+
refusal?: string | null;
|
|
27
41
|
};
|
|
28
42
|
delta?: {
|
|
29
43
|
role?: ChatMessage['role'];
|
|
30
44
|
content?: string | null;
|
|
45
|
+
refusal?: string | null;
|
|
31
46
|
tool_calls?: Array<{
|
|
32
47
|
index: number;
|
|
33
48
|
id?: string;
|
|
@@ -40,6 +55,8 @@ interface OpenRouterChatChoice {
|
|
|
40
55
|
};
|
|
41
56
|
finish_reason: string | null;
|
|
42
57
|
logprobs?: unknown;
|
|
58
|
+
/** Present with `finish_reason: 'error'`: the provider failed mid-generation. */
|
|
59
|
+
error?: OpenRouterErrorEnvelope;
|
|
43
60
|
}
|
|
44
61
|
interface OpenRouterChatCompletionAPIResponse {
|
|
45
62
|
id: string;
|
|
@@ -66,6 +83,8 @@ interface OpenRouterChatCompletionAPIResponse {
|
|
|
66
83
|
cached_tokens?: number;
|
|
67
84
|
};
|
|
68
85
|
};
|
|
86
|
+
/** A 200 whose body reports a failure instead of choices, or a mid-stream error event. */
|
|
87
|
+
error?: OpenRouterErrorEnvelope;
|
|
69
88
|
}
|
|
70
89
|
export declare function defaultOpenRouterProviderPrefs(env?: NodeJS.ProcessEnv): Record<string, unknown> | undefined;
|
|
71
90
|
/**
|
|
@@ -78,6 +97,37 @@ export declare function defaultOpenRouterProviderPrefs(env?: NodeJS.ProcessEnv):
|
|
|
78
97
|
* downstream cache log light up for OpenRouter turns without caller changes.
|
|
79
98
|
*/
|
|
80
99
|
export declare function mapOpenRouterUsage(apiUsage: OpenRouterChatCompletionAPIResponse['usage']): ModelUsage | undefined;
|
|
100
|
+
/** How OpenRouter declined a request, read from its error envelope. */
|
|
101
|
+
export interface OpenRouterDecline {
|
|
102
|
+
/**
|
|
103
|
+
* AgentOS's content-policy codes: `content_filter` for a model's own
|
|
104
|
+
* refusal, `content_policy_violation` for a filter around the model. Both
|
|
105
|
+
* are in `isContentPolicyRefusal`'s and the health registry's sets.
|
|
106
|
+
*/
|
|
107
|
+
code: 'content_filter' | 'content_policy_violation';
|
|
108
|
+
/** What OpenRouter reported, kept for diagnostics. */
|
|
109
|
+
nativeType: 'refusal' | 'content_policy_violation' | 'moderation' | 'in_body_403' | 'content_filter_finish';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Classify an OpenRouter error envelope as a content decline, or not.
|
|
113
|
+
*
|
|
114
|
+
* - `metadata.error_type` `refusal`: the model refused (code `content_filter`).
|
|
115
|
+
* - `metadata.error_type` `content_policy_violation`: a filter flagged the
|
|
116
|
+
* input or output.
|
|
117
|
+
* - The documented moderation metadata (`reasons` + `flagged_input`) with no
|
|
118
|
+
* `error_type`.
|
|
119
|
+
* - With `inBody`, a numeric code 403 and no `error_type`: the error reference
|
|
120
|
+
* gives every in-body policy decline the code 403 once the HTTP line was
|
|
121
|
+
* already committed. Never applied to an HTTP 403 response, which without a
|
|
122
|
+
* decline type is a guardrail or permission block (`permission_denied`).
|
|
123
|
+
*
|
|
124
|
+
* @param error - The `error` object from an OpenRouter body, choice or event.
|
|
125
|
+
* @param opts.inBody - True when the error sits inside an HTTP 200 body.
|
|
126
|
+
* @returns The decline, or `undefined` when the error is not one.
|
|
127
|
+
*/
|
|
128
|
+
export declare function classifyOpenRouterDecline(error: unknown, opts?: {
|
|
129
|
+
inBody?: boolean;
|
|
130
|
+
}): OpenRouterDecline | undefined;
|
|
81
131
|
export declare class OpenRouterProvider implements IProvider {
|
|
82
132
|
readonly providerId: string;
|
|
83
133
|
isInitialized: boolean;
|
|
@@ -181,6 +231,34 @@ export declare class OpenRouterProvider implements IProvider {
|
|
|
181
231
|
* jittered exponential backoff; 4xx request errors throw immediately.
|
|
182
232
|
*/
|
|
183
233
|
private makeApiRequest;
|
|
234
|
+
/**
|
|
235
|
+
* The error a declined request raises. Code `content_filter` or
|
|
236
|
+
* `content_policy_violation` is what `isContentPolicyRefusal` and the
|
|
237
|
+
* health registry's exemption read, so the walkers move on and no breaker
|
|
238
|
+
* opens. The message is fixed: no HTTP status digits and no upstream text,
|
|
239
|
+
* which retry classifiers grep for. The response status, the upstream
|
|
240
|
+
* message (redacted, 300 chars), the provider, the refusal text (300), the
|
|
241
|
+
* partial text (2,000) and the billed usage ride `details`.
|
|
242
|
+
*/
|
|
243
|
+
private declineError;
|
|
244
|
+
/**
|
|
245
|
+
* A 200 body that reports a failure instead of an answer throws before
|
|
246
|
+
* mapping, in this order: a body holding only `error` and no
|
|
247
|
+
* choice; a choice ended by `finish_reason: 'error'` with its own error
|
|
248
|
+
* object; a choice ended by `finish_reason: 'content_filter'` (the model
|
|
249
|
+
* declined as output). A choice with `finish_reason: 'error'` and no error
|
|
250
|
+
* object is not a documented shape and is mapped as before.
|
|
251
|
+
*/
|
|
252
|
+
private throwOnInBodyError;
|
|
253
|
+
/**
|
|
254
|
+
* An error reported inside an HTTP 200 body: a decline becomes
|
|
255
|
+
* the decline error; anything else becomes the error an HTTP response with
|
|
256
|
+
* that code produces today, so `isRetryableError` and the breaker treat it
|
|
257
|
+
* by its code. The exception is a 401 or 403 that is not a decline: it is
|
|
258
|
+
* still retryable, but it carries no status, so it counts as a transient
|
|
259
|
+
* failure and not as a rejected key.
|
|
260
|
+
*/
|
|
261
|
+
private inBodyError;
|
|
184
262
|
/**
|
|
185
263
|
* Masks the configured API keys (the raw config value and each key in a
|
|
186
264
|
* comma-separated pool) and any base-URL credentials out of text bound
|
|
@@ -221,6 +299,8 @@ export declare class OpenRouterProvider implements IProvider {
|
|
|
221
299
|
* @returns A plain description that is safe to log or serialize.
|
|
222
300
|
*/
|
|
223
301
|
private describeError;
|
|
302
|
+
/** The usage a raw SSE line reports, when it is a data line that carries one. */
|
|
303
|
+
private usageOfSseLine;
|
|
224
304
|
private parseSseStream;
|
|
225
305
|
}
|
|
226
306
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpenRouterProvider.d.ts","sourceRoot":"","sources":["../../../../../src/core/llm/providers/implementations/OpenRouterProvider.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AAGH,OAAO,EACL,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,uBAAuB,EACvB,SAAS,EACT,UAAU,EACV,wBAAwB,EACxB,yBAAyB,EAE1B,MAAM,cAAc,CAAC;AAQtB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,UAAU,oBAAoB;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE;QACR,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,UAAU,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"OpenRouterProvider.d.ts","sourceRoot":"","sources":["../../../../../src/core/llm/providers/implementations/OpenRouterProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -161,6 +161,43 @@ function parseErrorBody(text) {
|
|
|
161
161
|
return text;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Classify an OpenRouter error envelope as a content decline, or not.
|
|
166
|
+
*
|
|
167
|
+
* - `metadata.error_type` `refusal`: the model refused (code `content_filter`).
|
|
168
|
+
* - `metadata.error_type` `content_policy_violation`: a filter flagged the
|
|
169
|
+
* input or output.
|
|
170
|
+
* - The documented moderation metadata (`reasons` + `flagged_input`) with no
|
|
171
|
+
* `error_type`.
|
|
172
|
+
* - With `inBody`, a numeric code 403 and no `error_type`: the error reference
|
|
173
|
+
* gives every in-body policy decline the code 403 once the HTTP line was
|
|
174
|
+
* already committed. Never applied to an HTTP 403 response, which without a
|
|
175
|
+
* decline type is a guardrail or permission block (`permission_denied`).
|
|
176
|
+
*
|
|
177
|
+
* @param error - The `error` object from an OpenRouter body, choice or event.
|
|
178
|
+
* @param opts.inBody - True when the error sits inside an HTTP 200 body.
|
|
179
|
+
* @returns The decline, or `undefined` when the error is not one.
|
|
180
|
+
*/
|
|
181
|
+
export function classifyOpenRouterDecline(error, opts = {}) {
|
|
182
|
+
if (!error || typeof error !== 'object')
|
|
183
|
+
return undefined;
|
|
184
|
+
const env = error;
|
|
185
|
+
const meta = env.metadata && typeof env.metadata === 'object' ? env.metadata : undefined;
|
|
186
|
+
const errorType = typeof meta?.error_type === 'string' ? meta.error_type : undefined;
|
|
187
|
+
if (errorType === 'refusal')
|
|
188
|
+
return { code: 'content_filter', nativeType: 'refusal' };
|
|
189
|
+
if (errorType === 'content_policy_violation') {
|
|
190
|
+
return { code: 'content_policy_violation', nativeType: 'content_policy_violation' };
|
|
191
|
+
}
|
|
192
|
+
if (errorType !== undefined)
|
|
193
|
+
return undefined;
|
|
194
|
+
if (Array.isArray(meta?.reasons) && typeof meta?.flagged_input === 'string') {
|
|
195
|
+
return { code: 'content_policy_violation', nativeType: 'moderation' };
|
|
196
|
+
}
|
|
197
|
+
if (opts.inBody && env.code === 403)
|
|
198
|
+
return { code: 'content_policy_violation', nativeType: 'in_body_403' };
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
164
201
|
export class OpenRouterProvider {
|
|
165
202
|
constructor() {
|
|
166
203
|
this.providerId = 'openrouter';
|
|
@@ -451,6 +488,7 @@ export class OpenRouterProvider {
|
|
|
451
488
|
throw error;
|
|
452
489
|
apiResponseData = await this.makeApiRequest('/chat/completions', 'POST', options.requestTimeout ?? this.config.requestTimeout, payload);
|
|
453
490
|
}
|
|
491
|
+
this.throwOnInBodyError(apiResponseData, modelId);
|
|
454
492
|
return this.mapApiToCompletionResponse(apiResponseData, modelId);
|
|
455
493
|
}
|
|
456
494
|
/**
|
|
@@ -578,40 +616,95 @@ export class OpenRouterProvider {
|
|
|
578
616
|
}
|
|
579
617
|
const accumulatedToolCalls = new Map();
|
|
580
618
|
const abortSignal = options.abortSignal;
|
|
619
|
+
// `usage` is what a refused turn billed before the abort; a consumer
|
|
620
|
+
// metering final chunks would otherwise lose it.
|
|
621
|
+
const abortChunk = (message, usage) => ({
|
|
622
|
+
id: `openrouter-abort-${Date.now()}`,
|
|
623
|
+
object: 'chat.completion.chunk',
|
|
624
|
+
created: Math.floor(Date.now() / 1000),
|
|
625
|
+
modelId,
|
|
626
|
+
choices: [],
|
|
627
|
+
error: { message, type: 'abort' },
|
|
628
|
+
isFinal: true,
|
|
629
|
+
...(usage ? { usage } : {}),
|
|
630
|
+
});
|
|
581
631
|
if (abortSignal?.aborted) {
|
|
582
|
-
|
|
632
|
+
// The response is already open, and parseSseStream, which closes it,
|
|
633
|
+
// never runs on this path.
|
|
634
|
+
stream.destroy?.();
|
|
635
|
+
yield abortChunk('Stream aborted prior to first chunk');
|
|
583
636
|
return;
|
|
584
637
|
}
|
|
585
638
|
const abortHandler = () => { };
|
|
586
639
|
abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
640
|
+
// Text yielded so far (for the decline's partialText); the
|
|
641
|
+
// decline held back when a content_filter finish arrives, while the
|
|
642
|
+
// trailing usage chunk is read; a read error seen during that wait.
|
|
643
|
+
let yieldedText = '';
|
|
644
|
+
let held = null;
|
|
645
|
+
let readError;
|
|
646
|
+
const isDecline = (e) => e instanceof OpenRouterProviderError && (e.code === 'content_filter' || e.code === 'content_policy_violation');
|
|
647
|
+
try {
|
|
648
|
+
try {
|
|
649
|
+
for await (const rawChunk of this.parseSseStream(stream)) {
|
|
650
|
+
if (abortSignal?.aborted) {
|
|
651
|
+
// The line that shows the abort is usually the usage line a held
|
|
652
|
+
// decline was waiting for: read what it reports before leaving.
|
|
653
|
+
if (held)
|
|
654
|
+
held.usage = this.usageOfSseLine(rawChunk) ?? held.usage;
|
|
655
|
+
yield abortChunk('Stream aborted by caller', held?.usage);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (!rawChunk.startsWith('data: '))
|
|
659
|
+
continue;
|
|
660
|
+
const jsonData = rawChunk.substring('data: '.length);
|
|
661
|
+
if (jsonData.trim() === '[DONE]')
|
|
662
|
+
break;
|
|
663
|
+
// The parse has its own catch, so a decline thrown while HANDLING
|
|
664
|
+
// a chunk is never swallowed as a parse failure. A line that is not
|
|
665
|
+
// JSON, or is JSON but not an object, is skipped as before.
|
|
666
|
+
let apiChunk;
|
|
667
|
+
try {
|
|
668
|
+
apiChunk = JSON.parse(jsonData);
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
console.warn('OpenRouterProvider: Failed to parse stream chunk JSON, skipping chunk. Data:', jsonData, 'Error:', error);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (!apiChunk || typeof apiChunk !== 'object') {
|
|
675
|
+
console.warn('OpenRouterProvider: Stream chunk is not a JSON object, skipping chunk. Data:', jsonData);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
604
678
|
// OpenRouter reports upstream failures MID-STREAM as an SSE data
|
|
605
|
-
// event carrying an `error` object
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
// which made every mid-stream failure look identical to callers'
|
|
610
|
-
// retry/fallback routing. Surface the actual message + code and
|
|
611
|
-
// terminate the stream.
|
|
679
|
+
// event carrying an `error` object. A content decline throws typed
|
|
680
|
+
// (the walkers move on before any output; after output the stream
|
|
681
|
+
// ends with an error part). Any other error still surfaces as an
|
|
682
|
+
// upstream_error chunk and ends the stream.
|
|
612
683
|
if (apiChunk.error && typeof apiChunk.error === 'object') {
|
|
684
|
+
// The stream is an HTTP 200, so the in-body 403 row applies here too.
|
|
685
|
+
const decline = classifyOpenRouterDecline(apiChunk.error, { inBody: true });
|
|
686
|
+
if (decline) {
|
|
687
|
+
throw this.declineError(modelId, decline, {
|
|
688
|
+
httpStatus: typeof apiChunk.error.code === 'number' ? apiChunk.error.code : undefined,
|
|
689
|
+
error: apiChunk.error,
|
|
690
|
+
partialText: yieldedText,
|
|
691
|
+
// The event's own usage when it reports one, else what the
|
|
692
|
+
// held finish or the usage chunk reported.
|
|
693
|
+
usage: mapOpenRouterUsage(apiChunk.usage) ?? held?.usage,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
613
696
|
const errMessage = apiChunk.error.message || 'OpenRouter mid-stream error';
|
|
614
697
|
const errCode = apiChunk.error.code;
|
|
698
|
+
const decorated = errCode !== undefined ? `[${errCode}] ${errMessage}` : errMessage;
|
|
699
|
+
if (held) {
|
|
700
|
+
// The answer already ended on a content_filter finish. A later
|
|
701
|
+
// upstream failure is the end of the read: it neither replaces
|
|
702
|
+
// the held decline nor loses the usage read so far.
|
|
703
|
+
if (apiChunk.usage)
|
|
704
|
+
held.usage = mapOpenRouterUsage(apiChunk.usage);
|
|
705
|
+
readError = new Error(decorated);
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
615
708
|
yield {
|
|
616
709
|
id: apiChunk.id ?? `openrouter-error-${Date.now()}`,
|
|
617
710
|
object: 'chat.completion.chunk',
|
|
@@ -620,25 +713,78 @@ export class OpenRouterProvider {
|
|
|
620
713
|
choices: [],
|
|
621
714
|
isFinal: true,
|
|
622
715
|
error: {
|
|
623
|
-
message:
|
|
716
|
+
message: decorated,
|
|
624
717
|
type: 'upstream_error',
|
|
625
718
|
},
|
|
626
719
|
};
|
|
627
720
|
break;
|
|
628
721
|
}
|
|
629
|
-
|
|
722
|
+
if (held) {
|
|
723
|
+
// After a content_filter finish only the trailing usage-only
|
|
724
|
+
// chunk is wanted; nothing more is yielded.
|
|
725
|
+
if (apiChunk.usage)
|
|
726
|
+
held.usage = mapOpenRouterUsage(apiChunk.usage);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const choice = apiChunk.choices?.[0];
|
|
730
|
+
if (choice && choice.finish_reason === 'content_filter') {
|
|
731
|
+
held = {
|
|
732
|
+
decline: { code: 'content_filter', nativeType: 'content_filter_finish' },
|
|
733
|
+
refusal: choice.delta?.refusal ?? choice.message?.refusal ?? null,
|
|
734
|
+
usage: mapOpenRouterUsage(apiChunk.usage),
|
|
735
|
+
};
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
// A chunk of an unexpected shape (no choices array, a malformed
|
|
739
|
+
// tool call) is logged and skipped, as it was when the parse and
|
|
740
|
+
// the mapping shared one catch.
|
|
741
|
+
let mapped;
|
|
742
|
+
try {
|
|
743
|
+
mapped = this.mapApiToStreamChunkResponse(apiChunk, modelId, accumulatedToolCalls);
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
console.warn('OpenRouterProvider: Failed to map stream chunk, skipping chunk. Data:', jsonData, 'Error:', this.describeError(error));
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (mapped.responseTextDelta)
|
|
750
|
+
yieldedText += mapped.responseTextDelta;
|
|
751
|
+
yield mapped;
|
|
630
752
|
// Don't break on finish_reason: with stream_options.include_usage,
|
|
631
753
|
// OpenRouter (like OpenAI) emits a trailing usage-only chunk AFTER
|
|
632
|
-
// the finish_reason chunk and BEFORE [DONE].
|
|
633
|
-
//
|
|
634
|
-
// [DONE] marker check above is the right termination signal.
|
|
754
|
+
// the finish_reason chunk and BEFORE [DONE]. The [DONE] marker
|
|
755
|
+
// check above is the right termination signal.
|
|
635
756
|
}
|
|
636
|
-
|
|
637
|
-
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
if (isDecline(error))
|
|
760
|
+
throw error;
|
|
761
|
+
// A read error after a content_filter finish does not lose the
|
|
762
|
+
// decline: it is thrown below with the usage held so far.
|
|
763
|
+
if (held)
|
|
764
|
+
readError = error;
|
|
765
|
+
else
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
if (held) {
|
|
769
|
+
// An abort during the wait wins over the decline. The loop only
|
|
770
|
+
// checks the signal when a line arrives, so a read that ended
|
|
771
|
+
// without one is checked here.
|
|
772
|
+
if (abortSignal?.aborted) {
|
|
773
|
+
yield abortChunk('Stream aborted by caller', held.usage);
|
|
774
|
+
return;
|
|
638
775
|
}
|
|
776
|
+
throw this.declineError(modelId, held.decline, {
|
|
777
|
+
httpStatus: 200,
|
|
778
|
+
refusal: held.refusal,
|
|
779
|
+
partialText: yieldedText,
|
|
780
|
+
usage: held.usage,
|
|
781
|
+
readError,
|
|
782
|
+
});
|
|
639
783
|
}
|
|
640
784
|
}
|
|
641
|
-
|
|
785
|
+
finally {
|
|
786
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
787
|
+
}
|
|
642
788
|
}
|
|
643
789
|
async generateEmbeddings(modelId, texts, options) {
|
|
644
790
|
this.ensureInitialized();
|
|
@@ -947,7 +1093,8 @@ export class OpenRouterProvider {
|
|
|
947
1093
|
}
|
|
948
1094
|
if (errorData?.error && typeof errorData.error === 'object') {
|
|
949
1095
|
errorMessage = errorData.error.message || errorMessage;
|
|
950
|
-
|
|
1096
|
+
const metaType = errorData.error.metadata?.error_type;
|
|
1097
|
+
errorType = (typeof metaType === 'string' ? metaType : undefined) || errorData.error.type || errorType;
|
|
951
1098
|
}
|
|
952
1099
|
else if (typeof errorData === 'string' && errorData.trim()) {
|
|
953
1100
|
errorMessage = errorData;
|
|
@@ -959,6 +1106,16 @@ export class OpenRouterProvider {
|
|
|
959
1106
|
else if (error instanceof Error) {
|
|
960
1107
|
errorMessage = error.message;
|
|
961
1108
|
}
|
|
1109
|
+
// A content decline is a verdict on the request, not provider
|
|
1110
|
+
// health: throw it typed at once (no in-provider retry, no key
|
|
1111
|
+
// cooldown) so the walkers move on and the breaker stays closed.
|
|
1112
|
+
const decline = classifyOpenRouterDecline(errorData?.error);
|
|
1113
|
+
if (decline) {
|
|
1114
|
+
throw this.declineError(String(body?.model ?? ''), decline, {
|
|
1115
|
+
httpStatus: statusCode,
|
|
1116
|
+
error: errorData.error,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
962
1119
|
// A throttled (429) or credit-exhausted (402) key must not be
|
|
963
1120
|
// reused by the next attempt/request — cool it down in the pool.
|
|
964
1121
|
if ((statusCode === 402 || statusCode === 429) && this.keyPool?.hasKeys) {
|
|
@@ -996,6 +1153,98 @@ export class OpenRouterProvider {
|
|
|
996
1153
|
// the compiler + any future refactor honest.
|
|
997
1154
|
throw lastError ?? new OpenRouterProviderError('OpenRouter request failed.', 'API_REQUEST_FAILED');
|
|
998
1155
|
}
|
|
1156
|
+
/**
|
|
1157
|
+
* The error a declined request raises. Code `content_filter` or
|
|
1158
|
+
* `content_policy_violation` is what `isContentPolicyRefusal` and the
|
|
1159
|
+
* health registry's exemption read, so the walkers move on and no breaker
|
|
1160
|
+
* opens. The message is fixed: no HTTP status digits and no upstream text,
|
|
1161
|
+
* which retry classifiers grep for. The response status, the upstream
|
|
1162
|
+
* message (redacted, 300 chars), the provider, the refusal text (300), the
|
|
1163
|
+
* partial text (2,000) and the billed usage ride `details`.
|
|
1164
|
+
*/
|
|
1165
|
+
declineError(modelId, decline, details) {
|
|
1166
|
+
const meta = details.error?.metadata;
|
|
1167
|
+
const bounded = (value, max) => typeof value === 'string' && value.length > 0 ? this.redactSecrets(value).slice(0, max) : undefined;
|
|
1168
|
+
return new OpenRouterProviderError(`OpenRouter declined the request on ${modelId || 'the requested model'} (${decline.nativeType}).`, decline.code, undefined, decline.nativeType, {
|
|
1169
|
+
httpStatus: details.httpStatus,
|
|
1170
|
+
upstreamMessage: bounded(details.error?.message, 300),
|
|
1171
|
+
providerName: typeof meta?.provider_name === 'string' ? meta.provider_name : undefined,
|
|
1172
|
+
providerCode: typeof meta?.provider_code === 'string' ? meta.provider_code : undefined,
|
|
1173
|
+
refusal: bounded(details.refusal, 300),
|
|
1174
|
+
partialText: bounded(details.partialText, 2000),
|
|
1175
|
+
usage: details.usage,
|
|
1176
|
+
...(details.readError !== undefined ? { readError: this.describeError(details.readError) } : {}),
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* A 200 body that reports a failure instead of an answer throws before
|
|
1181
|
+
* mapping, in this order: a body holding only `error` and no
|
|
1182
|
+
* choice; a choice ended by `finish_reason: 'error'` with its own error
|
|
1183
|
+
* object; a choice ended by `finish_reason: 'content_filter'` (the model
|
|
1184
|
+
* declined as output). A choice with `finish_reason: 'error'` and no error
|
|
1185
|
+
* object is not a documented shape and is mapped as before.
|
|
1186
|
+
*/
|
|
1187
|
+
throwOnInBodyError(body, modelId) {
|
|
1188
|
+
const choices = Array.isArray(body.choices) ? body.choices : [];
|
|
1189
|
+
const first = choices[0];
|
|
1190
|
+
const usage = mapOpenRouterUsage(body.usage);
|
|
1191
|
+
if (body.error && typeof body.error === 'object' && choices.length === 0) {
|
|
1192
|
+
throw this.inBodyError(body.error, modelId, { responseId: body.id, usage });
|
|
1193
|
+
}
|
|
1194
|
+
if (first && first.finish_reason === 'error' && first.error && typeof first.error === 'object') {
|
|
1195
|
+
throw this.inBodyError(first.error, modelId, {
|
|
1196
|
+
responseId: body.id,
|
|
1197
|
+
usage,
|
|
1198
|
+
partialText: first.message?.content ?? null,
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
if (first && first.finish_reason === 'content_filter') {
|
|
1202
|
+
throw this.declineError(modelId, { code: 'content_filter', nativeType: 'content_filter_finish' }, {
|
|
1203
|
+
httpStatus: 200,
|
|
1204
|
+
refusal: first.message?.refusal ?? null,
|
|
1205
|
+
partialText: first.message?.content ?? null,
|
|
1206
|
+
usage,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* An error reported inside an HTTP 200 body: a decline becomes
|
|
1212
|
+
* the decline error; anything else becomes the error an HTTP response with
|
|
1213
|
+
* that code produces today, so `isRetryableError` and the breaker treat it
|
|
1214
|
+
* by its code. The exception is a 401 or 403 that is not a decline: it is
|
|
1215
|
+
* still retryable, but it carries no status, so it counts as a transient
|
|
1216
|
+
* failure and not as a rejected key.
|
|
1217
|
+
*/
|
|
1218
|
+
inBodyError(error, modelId, extra) {
|
|
1219
|
+
const code = typeof error.code === 'number' ? error.code : undefined;
|
|
1220
|
+
const decline = classifyOpenRouterDecline(error, { inBody: true });
|
|
1221
|
+
if (decline) {
|
|
1222
|
+
return this.declineError(modelId, decline, {
|
|
1223
|
+
httpStatus: code,
|
|
1224
|
+
error,
|
|
1225
|
+
partialText: extra.partialText,
|
|
1226
|
+
usage: extra.usage,
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
// The body is untyped network input: a message that is not a string
|
|
1230
|
+
// must not reach the redaction's string calls.
|
|
1231
|
+
const message = this.redactSecrets(typeof error.message === 'string' && error.message ? error.message : 'OpenRouter reported an error in a 200 response');
|
|
1232
|
+
const metaType = typeof error.metadata?.error_type === 'string' ? error.metadata.error_type : undefined;
|
|
1233
|
+
// The 200 means the configured key was accepted, so a 401 or 403 inside
|
|
1234
|
+
// the body describes an upstream attempt (a BYOK key, a failover leg),
|
|
1235
|
+
// not this account. Its code stays in the message, where the retry
|
|
1236
|
+
// classifiers read it, and in details; the error carries no status and
|
|
1237
|
+
// no `[NNN]` prefix, so the breaker counts one transient failure instead
|
|
1238
|
+
// of opening its auth policy (one failure, 30 minutes) on one response.
|
|
1239
|
+
const inBodyAuth = code === 401 || code === 403;
|
|
1240
|
+
return new OpenRouterProviderError(code === undefined ? message : inBodyAuth ? `OpenRouter in-body error ${code}: ${message}` : `[${code}] ${message}`, 'API_REQUEST_FAILED', inBodyAuth ? undefined : code, metaType ?? 'UNKNOWN_API_ERROR', {
|
|
1241
|
+
responseId: extra.responseId,
|
|
1242
|
+
...(inBodyAuth ? { httpStatus: code } : {}),
|
|
1243
|
+
responseData: this.redactResponseData(error),
|
|
1244
|
+
partialText: typeof extra.partialText === 'string' ? this.redactSecrets(extra.partialText).slice(0, 2000) : undefined,
|
|
1245
|
+
usage: extra.usage,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
999
1248
|
/**
|
|
1000
1249
|
* Masks the configured API keys (the raw config value and each key in a
|
|
1001
1250
|
* comma-separated pool) and any base-URL credentials out of text bound
|
|
@@ -1078,6 +1327,20 @@ export class OpenRouterProvider {
|
|
|
1078
1327
|
}
|
|
1079
1328
|
return { message: this.redactSecrets(String(error)) };
|
|
1080
1329
|
}
|
|
1330
|
+
/** The usage a raw SSE line reports, when it is a data line that carries one. */
|
|
1331
|
+
usageOfSseLine(rawChunk) {
|
|
1332
|
+
if (!rawChunk.startsWith('data: '))
|
|
1333
|
+
return undefined;
|
|
1334
|
+
try {
|
|
1335
|
+
const parsed = JSON.parse(rawChunk.substring('data: '.length));
|
|
1336
|
+
return parsed && typeof parsed === 'object'
|
|
1337
|
+
? mapOpenRouterUsage(parsed.usage)
|
|
1338
|
+
: undefined;
|
|
1339
|
+
}
|
|
1340
|
+
catch {
|
|
1341
|
+
return undefined;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1081
1344
|
async *parseSseStream(stream) {
|
|
1082
1345
|
let buffer = '';
|
|
1083
1346
|
const readableStream = stream;
|