@animalabs/membrane 0.5.80 → 0.5.82
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/cache-wire-receipt.d.ts +13 -0
- package/dist/cache-wire-receipt.d.ts.map +1 -0
- package/dist/cache-wire-receipt.js +108 -0
- package/dist/cache-wire-receipt.js.map +1 -0
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/process.d.ts +3 -1
- package/dist/context/process.d.ts.map +1 -1
- package/dist/context/process.js +230 -49
- package/dist/context/process.js.map +1 -1
- package/dist/context/types.d.ts +30 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +16 -0
- package/dist/context/types.js.map +1 -1
- package/dist/floating-cache-marker.test.js +21 -0
- package/dist/floating-cache-marker.test.js.map +1 -1
- package/dist/formatters/anthropic-xml.d.ts +2 -0
- package/dist/formatters/anthropic-xml.d.ts.map +1 -1
- package/dist/formatters/anthropic-xml.js +64 -23
- package/dist/formatters/anthropic-xml.js.map +1 -1
- package/dist/formatters/native.d.ts.map +1 -1
- package/dist/formatters/native.js +16 -3
- package/dist/formatters/native.js.map +1 -1
- package/dist/formatters/normalize-tool-pairs.d.ts +7 -0
- package/dist/formatters/normalize-tool-pairs.d.ts.map +1 -1
- package/dist/formatters/normalize-tool-pairs.js +45 -0
- package/dist/formatters/normalize-tool-pairs.js.map +1 -1
- package/dist/formatters/types.d.ts +31 -0
- package/dist/formatters/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/membrane.d.ts +137 -8
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +823 -310
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +142 -25
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts +8 -0
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +24 -1
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/providers/gemini.d.ts +11 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +104 -82
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +2 -0
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts +7 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +99 -48
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +5 -0
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +107 -44
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +16 -0
- package/dist/providers/openai-responses-api.d.ts.map +1 -1
- package/dist/providers/openai-responses-api.js +38 -10
- package/dist/providers/openai-responses-api.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +2 -0
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +8 -1
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -44
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +9 -0
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +118 -62
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/utils.d.ts +74 -2
- package/dist/providers/utils.d.ts.map +1 -1
- package/dist/providers/utils.js +184 -3
- package/dist/providers/utils.js.map +1 -1
- package/dist/registry/default-pricing.d.ts +22 -0
- package/dist/registry/default-pricing.d.ts.map +1 -1
- package/dist/registry/default-pricing.js +162 -28
- package/dist/registry/default-pricing.js.map +1 -1
- package/dist/types/content.d.ts +13 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js.map +1 -1
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +26 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/provider.d.ts +50 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/request.d.ts +6 -0
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/response.d.ts +85 -4
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/response.js.map +1 -1
- package/dist/types/streaming.d.ts +6 -1
- package/dist/types/streaming.d.ts.map +1 -1
- package/dist/utils/cache-marker-budget.d.ts +91 -0
- package/dist/utils/cache-marker-budget.d.ts.map +1 -0
- package/dist/utils/cache-marker-budget.js +208 -0
- package/dist/utils/cache-marker-budget.js.map +1 -0
- package/dist/utils/cost.d.ts +22 -0
- package/dist/utils/cost.d.ts.map +1 -1
- package/dist/utils/cost.js +55 -0
- package/dist/utils/cost.js.map +1 -1
- package/dist/utils/thinking-carriers.d.ts +80 -0
- package/dist/utils/thinking-carriers.d.ts.map +1 -0
- package/dist/utils/thinking-carriers.js +115 -0
- package/dist/utils/thinking-carriers.js.map +1 -0
- package/dist/utils/tool-parser.d.ts +24 -0
- package/dist/utils/tool-parser.d.ts.map +1 -1
- package/dist/utils/tool-parser.js +367 -161
- package/dist/utils/tool-parser.js.map +1 -1
- package/dist/utils/usage.d.ts +71 -0
- package/dist/utils/usage.d.ts.map +1 -0
- package/dist/utils/usage.js +161 -0
- package/dist/utils/usage.js.map +1 -0
- package/dist/yielding-stream.d.ts +8 -0
- package/dist/yielding-stream.d.ts.map +1 -1
- package/dist/yielding-stream.js +49 -3
- package/dist/yielding-stream.js.map +1 -1
- package/package.json +1 -1
- package/src/cache-wire-receipt.ts +125 -0
- package/src/context/index.ts +1 -0
- package/src/context/process.ts +305 -59
- package/src/context/types.ts +38 -0
- package/src/floating-cache-marker.test.ts +24 -0
- package/src/formatters/anthropic-xml.ts +66 -23
- package/src/formatters/native.ts +23 -2
- package/src/formatters/normalize-tool-pairs.ts +49 -0
- package/src/formatters/types.ts +35 -0
- package/src/index.ts +6 -0
- package/src/membrane.ts +974 -313
- package/src/providers/anthropic.ts +173 -19
- package/src/providers/bedrock.ts +25 -1
- package/src/providers/gemini.ts +125 -87
- package/src/providers/index.ts +1 -0
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-compatible.ts +102 -45
- package/src/providers/openai-completions.ts +112 -46
- package/src/providers/openai-responses-api.ts +45 -15
- package/src/providers/openai-responses.ts +8 -1
- package/src/providers/openai.ts +95 -42
- package/src/providers/openrouter.ts +125 -63
- package/src/providers/utils.ts +227 -6
- package/src/registry/default-pricing.ts +165 -28
- package/src/types/content.ts +14 -0
- package/src/types/errors.ts +28 -0
- package/src/types/index.ts +6 -0
- package/src/types/provider.ts +55 -0
- package/src/types/request.ts +7 -0
- package/src/types/response.ts +109 -8
- package/src/types/streaming.ts +8 -2
- package/src/utils/cache-marker-budget.ts +237 -0
- package/src/utils/cost.ts +64 -0
- package/src/utils/thinking-carriers.ts +125 -0
- package/src/utils/tool-parser.ts +473 -174
- package/src/utils/usage.ts +184 -0
- package/src/yielding-stream.ts +54 -3
package/src/providers/openai.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
abortError,
|
|
30
30
|
networkError,
|
|
31
31
|
} from '../types/index.js';
|
|
32
|
-
import { safeParseJson, createCombinedSignal, SSELineParser } from './utils.js';
|
|
32
|
+
import { safeParseJson, createCombinedSignal, SSELineParser, isDeadlineAbort, deadlineTimeoutError, throwOnStreamErrorFrame, assertTerminalEventObserved } from './utils.js';
|
|
33
33
|
|
|
34
34
|
// ============================================================================
|
|
35
35
|
// Types
|
|
@@ -188,6 +188,14 @@ function noStopSupport(model: string): boolean {
|
|
|
188
188
|
|
|
189
189
|
export class OpenAIAdapter implements ProviderAdapter {
|
|
190
190
|
readonly name = 'openai';
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Verified live 2026-08-25 (gpt-4o-mini, 1,732-token prompt): prompt_tokens
|
|
194
|
+
* stayed at 1732 across a cache hit reporting
|
|
195
|
+
* prompt_tokens_details.cached_tokens 1664 — the cached span is a SUBSET of
|
|
196
|
+
* prompt_tokens, not an addition to it.
|
|
197
|
+
*/
|
|
198
|
+
readonly usageCacheConvention = 'cache-inclusive' as const;
|
|
191
199
|
private apiKey: string;
|
|
192
200
|
private baseURL: string;
|
|
193
201
|
private organization?: string;
|
|
@@ -263,61 +271,98 @@ export class OpenAIAdapter implements ProviderAdapter {
|
|
|
263
271
|
const sseParser = new SSELineParser();
|
|
264
272
|
let accumulated = '';
|
|
265
273
|
let finishReason = 'stop';
|
|
274
|
+
let sawTerminalEvent = false;
|
|
266
275
|
let toolCalls: OpenAIToolCall[] = [];
|
|
267
276
|
let streamUsage: OpenAIResponse['usage'] | undefined;
|
|
277
|
+
// The model the provider actually served, echoed on every SSE
|
|
278
|
+
// chunk. Reporting the requested id instead hides alias
|
|
279
|
+
// resolution (and, on OpenRouter, which provider it routed to).
|
|
280
|
+
let servedModel: string | undefined;
|
|
281
|
+
|
|
282
|
+
// One frame handler for both the streamed lines and the EOF flush — the
|
|
283
|
+
// trailing buffer carries real terminal frames, not leftovers.
|
|
284
|
+
const processDataLine = (data: string): void => {
|
|
285
|
+
if (data === '[DONE]') {
|
|
286
|
+
sawTerminalEvent = true;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
268
289
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
// Parse first; only JSON noise is ignorable. Everything after the
|
|
291
|
+
// parse must NOT be swallowed by the catch below.
|
|
292
|
+
let parsed: Record<string, any>;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(data);
|
|
295
|
+
} catch {
|
|
296
|
+
return; // Ignore parse errors (partial/keep-alive lines)
|
|
297
|
+
}
|
|
275
298
|
|
|
276
|
-
|
|
277
|
-
if (data === '[DONE]') continue;
|
|
299
|
+
throwOnStreamErrorFrame(parsed, 'OpenAI', openAIRequest);
|
|
278
300
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const delta = parsed.choices?.[0]?.delta;
|
|
301
|
+
try {
|
|
302
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
282
303
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
304
|
+
if (delta?.content) {
|
|
305
|
+
accumulated += delta.content;
|
|
306
|
+
callbacks.onChunk(delta.content);
|
|
307
|
+
}
|
|
287
308
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
309
|
+
// Handle streaming tool calls
|
|
310
|
+
if (delta?.tool_calls) {
|
|
311
|
+
for (const tc of delta.tool_calls) {
|
|
312
|
+
const index = tc.index ?? 0;
|
|
313
|
+
if (!toolCalls[index]) {
|
|
314
|
+
toolCalls[index] = {
|
|
315
|
+
id: tc.id ?? '',
|
|
316
|
+
type: 'function',
|
|
317
|
+
function: { name: '', arguments: '' },
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (tc.id) toolCalls[index].id = tc.id;
|
|
321
|
+
if (tc.function?.name) toolCalls[index].function.name = tc.function.name;
|
|
322
|
+
if (tc.function?.arguments) {
|
|
323
|
+
toolCalls[index].function.arguments += tc.function.arguments;
|
|
304
324
|
}
|
|
305
325
|
}
|
|
326
|
+
}
|
|
306
327
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
328
|
+
if (parsed.choices?.[0]?.finish_reason) {
|
|
329
|
+
finishReason = parsed.choices[0].finish_reason;
|
|
330
|
+
sawTerminalEvent = true;
|
|
331
|
+
}
|
|
310
332
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
} catch {
|
|
316
|
-
// Ignore parse errors in stream
|
|
333
|
+
// Capture usage data (comes in final chunk with stream_options.include_usage)
|
|
334
|
+
if (parsed.usage) {
|
|
335
|
+
streamUsage = parsed.usage;
|
|
317
336
|
}
|
|
337
|
+
|
|
338
|
+
if (parsed.model) {
|
|
339
|
+
servedModel = parsed.model;
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
// Ignore parse errors in stream
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
while (true) {
|
|
347
|
+
const { done, value } = await reader.read();
|
|
348
|
+
if (done) break;
|
|
349
|
+
|
|
350
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
351
|
+
for (const data of sseParser.feed(chunk)) {
|
|
352
|
+
processDataLine(data);
|
|
318
353
|
}
|
|
319
354
|
}
|
|
320
355
|
|
|
356
|
+
// A final `data:` line that arrived without its trailing newline is still
|
|
357
|
+
// buffered here. Servers and proxies do close right after writing the
|
|
358
|
+
// last event, so dropping it would report a finished turn as a dropped
|
|
359
|
+
// connection at the guard below.
|
|
360
|
+
for (const data of sseParser.flush()) {
|
|
361
|
+
processDataLine(data);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
assertTerminalEventObserved(sawTerminalEvent, 'OpenAI', openAIRequest);
|
|
365
|
+
|
|
321
366
|
// Build response with accumulated data
|
|
322
367
|
const message: OpenAIMessage = {
|
|
323
368
|
role: 'assistant',
|
|
@@ -328,7 +373,7 @@ export class OpenAIAdapter implements ProviderAdapter {
|
|
|
328
373
|
message.tool_calls = toolCalls;
|
|
329
374
|
}
|
|
330
375
|
|
|
331
|
-
return this.parseStreamedResponse(message, finishReason, request.model, streamUsage, openAIRequest);
|
|
376
|
+
return this.parseStreamedResponse(message, finishReason, servedModel ?? request.model, streamUsage, openAIRequest);
|
|
332
377
|
|
|
333
378
|
} catch (error) {
|
|
334
379
|
throw this.handleError(error, openAIRequest);
|
|
@@ -650,6 +695,14 @@ export class OpenAIAdapter implements ProviderAdapter {
|
|
|
650
695
|
}
|
|
651
696
|
|
|
652
697
|
private handleError(error: unknown, rawRequest?: unknown): MembraneError {
|
|
698
|
+
// A deadline abort is a timeout and stays one. Collapsing it into a bare
|
|
699
|
+
// abortError() here is what erased the identity before Membrane's
|
|
700
|
+
// caller-signal > timeout > error ladder could read it.
|
|
701
|
+
if (isDeadlineAbort(error)) return deadlineTimeoutError(error, rawRequest);
|
|
702
|
+
// Already-classified failures (e.g. the stream-integrity guards) keep
|
|
703
|
+
// their type and retryability instead of being re-derived from a string.
|
|
704
|
+
if (error instanceof MembraneError) return error;
|
|
705
|
+
|
|
653
706
|
if (error instanceof Error) {
|
|
654
707
|
const message = error.message;
|
|
655
708
|
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
StreamCallbacks,
|
|
13
13
|
ContentBlock,
|
|
14
14
|
ToolDefinition,
|
|
15
|
+
UsageCacheConvention,
|
|
15
16
|
} from '../types/index.js';
|
|
16
17
|
import {
|
|
17
18
|
MembraneError,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
abortError,
|
|
23
24
|
networkError,
|
|
24
25
|
} from '../types/index.js';
|
|
25
|
-
import { safeParseJson, createCombinedSignal, SSELineParser } from './utils.js';
|
|
26
|
+
import { safeParseJson, createCombinedSignal, SSELineParser, isDeadlineAbort, deadlineTimeoutError, throwOnStreamErrorFrame, assertTerminalEventObserved } from './utils.js';
|
|
26
27
|
|
|
27
28
|
// ============================================================================
|
|
28
29
|
// Types
|
|
@@ -144,8 +145,33 @@ export interface OpenRouterAdapterConfig {
|
|
|
144
145
|
// OpenRouter Adapter
|
|
145
146
|
// ============================================================================
|
|
146
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Which convention the tokens OpenRouter just handed back are in. OpenRouter
|
|
150
|
+
* passes the routed provider's usage payload through, so an Anthropic-routed
|
|
151
|
+
* call reports `cache_read_input_tokens` (disjoint from prompt_tokens) while an
|
|
152
|
+
* OpenAI-routed one reports `prompt_tokens_details.cached_tokens` (a subset of
|
|
153
|
+
* it). The field that carried the number therefore identifies the convention.
|
|
154
|
+
*/
|
|
155
|
+
function resolveRoutedCacheConvention(
|
|
156
|
+
anthropicShapedCacheRead: number | undefined,
|
|
157
|
+
resolvedCacheRead: number | undefined,
|
|
158
|
+
): UsageCacheConvention | undefined {
|
|
159
|
+
if (resolvedCacheRead == null) return undefined;
|
|
160
|
+
return anthropicShapedCacheRead != null ? 'cache-excluded' : 'cache-inclusive';
|
|
161
|
+
}
|
|
162
|
+
|
|
147
163
|
export class OpenRouterAdapter implements ProviderAdapter {
|
|
148
164
|
readonly name = 'openrouter';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* OpenRouter fronts BOTH conventions: it passes through Anthropic's
|
|
168
|
+
* `cache_read_input_tokens` (cache-excluded) or OpenAI's
|
|
169
|
+
* `prompt_tokens_details.cached_tokens` (cache-inclusive) depending on which
|
|
170
|
+
* provider it routed to. The convention is therefore a per-response fact —
|
|
171
|
+
* each parse sets `usage.cacheConvention` from the field it actually read,
|
|
172
|
+
* and this adapter-level value is only the no-cache-tokens fallback.
|
|
173
|
+
*/
|
|
174
|
+
readonly usageCacheConvention = 'unknown' as const;
|
|
149
175
|
private apiKey: string;
|
|
150
176
|
private baseURL: string;
|
|
151
177
|
private httpReferer: string;
|
|
@@ -218,80 +244,102 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
218
244
|
const sseParser = new SSELineParser();
|
|
219
245
|
let accumulated = '';
|
|
220
246
|
let finishReason = 'stop';
|
|
247
|
+
let sawTerminalEvent = false;
|
|
221
248
|
let toolCalls: OpenRouterToolCall[] = [];
|
|
222
249
|
let streamUsage: OpenRouterResponse['usage'] | undefined;
|
|
250
|
+
// The model the provider actually served, echoed on every SSE
|
|
251
|
+
// chunk. Reporting the requested id instead hides alias
|
|
252
|
+
// resolution (and, on OpenRouter, which provider it routed to).
|
|
253
|
+
let servedModel: string | undefined;
|
|
254
|
+
|
|
255
|
+
// One frame handler for both the streamed lines and the EOF flush — the
|
|
256
|
+
// trailing buffer carries real terminal frames, not leftovers.
|
|
257
|
+
const processDataLine = (data: string): void => {
|
|
258
|
+
if (data === '[DONE]') {
|
|
259
|
+
sawTerminalEvent = true;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
223
262
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (data === '[DONE]') continue;
|
|
233
|
-
|
|
234
|
-
// Parse first; only JSON noise is ignorable. Everything after the
|
|
235
|
-
// parse must NOT be swallowed by the catch below.
|
|
236
|
-
let parsed: Record<string, any>;
|
|
237
|
-
try {
|
|
238
|
-
parsed = JSON.parse(data);
|
|
239
|
-
} catch {
|
|
240
|
-
continue; // Ignore parse errors (partial/keep-alive lines)
|
|
241
|
-
}
|
|
263
|
+
// Parse first; only JSON noise is ignorable. Everything after the
|
|
264
|
+
// parse must NOT be swallowed by the catch below.
|
|
265
|
+
let parsed: Record<string, any>;
|
|
266
|
+
try {
|
|
267
|
+
parsed = JSON.parse(data);
|
|
268
|
+
} catch {
|
|
269
|
+
return; // Ignore parse errors (partial/keep-alive lines)
|
|
270
|
+
}
|
|
242
271
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const err = parsed.error as { code?: number | string; message?: string };
|
|
249
|
-
throw new Error(
|
|
250
|
-
`OpenRouter stream error${err.code !== undefined ? ` (${err.code})` : ''}: ${err.message ?? JSON.stringify(err)}`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
272
|
+
// OpenRouter delivers mid-stream failures (e.g. upstream 429s) as an
|
|
273
|
+
// SSE data line with an `error` payload. Silently ignoring it would
|
|
274
|
+
// yield a fake-successful empty completion — surface it instead so
|
|
275
|
+
// retry logic can handle it. Shared with every other SSE adapter.
|
|
276
|
+
throwOnStreamErrorFrame(parsed, 'OpenRouter', openRouterRequest);
|
|
253
277
|
|
|
254
|
-
|
|
255
|
-
|
|
278
|
+
try {
|
|
279
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
256
280
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
281
|
+
if (delta?.content) {
|
|
282
|
+
accumulated += delta.content;
|
|
283
|
+
callbacks.onChunk(delta.content);
|
|
284
|
+
}
|
|
261
285
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
286
|
+
// Handle streaming tool calls
|
|
287
|
+
if (delta?.tool_calls) {
|
|
288
|
+
for (const tc of delta.tool_calls) {
|
|
289
|
+
const index = tc.index ?? 0;
|
|
290
|
+
if (!toolCalls[index]) {
|
|
291
|
+
toolCalls[index] = {
|
|
292
|
+
id: tc.id ?? '',
|
|
293
|
+
type: 'function',
|
|
294
|
+
function: { name: '', arguments: '' },
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (tc.id) toolCalls[index].id = tc.id;
|
|
298
|
+
if (tc.function?.name) toolCalls[index].function.name = tc.function.name;
|
|
299
|
+
if (tc.function?.arguments) {
|
|
300
|
+
toolCalls[index].function.arguments += tc.function.arguments;
|
|
278
301
|
}
|
|
279
302
|
}
|
|
303
|
+
}
|
|
280
304
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
305
|
+
if (parsed.choices?.[0]?.finish_reason) {
|
|
306
|
+
finishReason = parsed.choices[0].finish_reason;
|
|
307
|
+
sawTerminalEvent = true;
|
|
308
|
+
}
|
|
284
309
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
// Capture usage data (comes in final chunk when stream_options.include_usage is set)
|
|
311
|
+
if (parsed.usage) {
|
|
312
|
+
streamUsage = parsed.usage;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (parsed.model) {
|
|
316
|
+
servedModel = parsed.model;
|
|
291
317
|
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
// Ignore parse errors
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
while (true) {
|
|
324
|
+
const { done, value } = await reader.read();
|
|
325
|
+
if (done) break;
|
|
326
|
+
|
|
327
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
328
|
+
for (const data of sseParser.feed(chunk)) {
|
|
329
|
+
processDataLine(data);
|
|
292
330
|
}
|
|
293
331
|
}
|
|
294
332
|
|
|
333
|
+
// A final `data:` line that arrived without its trailing newline is still
|
|
334
|
+
// buffered here. Servers and proxies do close right after writing the
|
|
335
|
+
// last event, so dropping it would report a finished turn as a dropped
|
|
336
|
+
// connection at the guard below.
|
|
337
|
+
for (const data of sseParser.flush()) {
|
|
338
|
+
processDataLine(data);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
assertTerminalEventObserved(sawTerminalEvent, 'OpenRouter', openRouterRequest);
|
|
342
|
+
|
|
295
343
|
// Build response with accumulated data
|
|
296
344
|
const message: OpenRouterMessage = {
|
|
297
345
|
role: 'assistant',
|
|
@@ -302,7 +350,7 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
302
350
|
message.tool_calls = toolCalls;
|
|
303
351
|
}
|
|
304
352
|
|
|
305
|
-
return this.parseStreamedResponse(message, finishReason, request.model, streamUsage, openRouterRequest);
|
|
353
|
+
return this.parseStreamedResponse(message, finishReason, servedModel ?? request.model, streamUsage, openRouterRequest);
|
|
306
354
|
|
|
307
355
|
} catch (error) {
|
|
308
356
|
throw this.handleError(error, openRouterRequest);
|
|
@@ -602,8 +650,10 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
602
650
|
// Anthropic: cache_creation_input_tokens, cache_read_input_tokens
|
|
603
651
|
// OpenAI: prompt_tokens_details.cached_tokens
|
|
604
652
|
const cacheCreationTokens = response.usage?.cache_creation_input_tokens;
|
|
605
|
-
const
|
|
653
|
+
const anthropicShapedCacheRead = response.usage?.cache_read_input_tokens;
|
|
654
|
+
const cacheReadTokens = anthropicShapedCacheRead
|
|
606
655
|
?? response.usage?.prompt_tokens_details?.cached_tokens;
|
|
656
|
+
const cacheConvention = resolveRoutedCacheConvention(anthropicShapedCacheRead, cacheReadTokens);
|
|
607
657
|
|
|
608
658
|
return {
|
|
609
659
|
content: this.messageToContent(message),
|
|
@@ -614,6 +664,7 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
614
664
|
outputTokens: response.usage?.completion_tokens ?? 0,
|
|
615
665
|
cacheCreationTokens: cacheCreationTokens ?? undefined,
|
|
616
666
|
cacheReadTokens: cacheReadTokens ?? undefined,
|
|
667
|
+
cacheConvention,
|
|
617
668
|
},
|
|
618
669
|
model: response.model ?? requestedModel,
|
|
619
670
|
rawRequest,
|
|
@@ -630,8 +681,10 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
630
681
|
): ProviderResponse {
|
|
631
682
|
// Extract cache tokens if available from stream usage
|
|
632
683
|
const cacheCreationTokens = streamUsage?.cache_creation_input_tokens;
|
|
633
|
-
const
|
|
684
|
+
const anthropicShapedCacheRead = streamUsage?.cache_read_input_tokens;
|
|
685
|
+
const cacheReadTokens = anthropicShapedCacheRead
|
|
634
686
|
?? streamUsage?.prompt_tokens_details?.cached_tokens;
|
|
687
|
+
const cacheConvention = resolveRoutedCacheConvention(anthropicShapedCacheRead, cacheReadTokens);
|
|
635
688
|
|
|
636
689
|
return {
|
|
637
690
|
content: this.messageToContent(message),
|
|
@@ -642,6 +695,7 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
642
695
|
outputTokens: streamUsage?.completion_tokens ?? 0,
|
|
643
696
|
cacheCreationTokens: cacheCreationTokens ?? undefined,
|
|
644
697
|
cacheReadTokens: cacheReadTokens ?? undefined,
|
|
698
|
+
cacheConvention,
|
|
645
699
|
},
|
|
646
700
|
model: requestedModel,
|
|
647
701
|
rawRequest,
|
|
@@ -696,6 +750,14 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
696
750
|
}
|
|
697
751
|
|
|
698
752
|
private handleError(error: unknown, rawRequest?: unknown): MembraneError {
|
|
753
|
+
// A deadline abort is a timeout and stays one. Collapsing it into a bare
|
|
754
|
+
// abortError() here is what erased the identity before Membrane's
|
|
755
|
+
// caller-signal > timeout > error ladder could read it.
|
|
756
|
+
if (isDeadlineAbort(error)) return deadlineTimeoutError(error, rawRequest);
|
|
757
|
+
// Already-classified failures (e.g. the stream-integrity guards) keep
|
|
758
|
+
// their type and retryability instead of being re-derived from a string.
|
|
759
|
+
if (error instanceof MembraneError) return error;
|
|
760
|
+
|
|
699
761
|
if (error instanceof Error) {
|
|
700
762
|
const message = error.message;
|
|
701
763
|
|