@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
|
@@ -20,8 +20,10 @@ import {
|
|
|
20
20
|
authError,
|
|
21
21
|
serverError,
|
|
22
22
|
abortError,
|
|
23
|
+
unsupportedError,
|
|
23
24
|
} from '../types/index.js';
|
|
24
25
|
import { flattenRootSchemaUnion } from './anthropic-tool-schema.js';
|
|
26
|
+
import { assertTerminalEventObserved } from './utils.js';
|
|
25
27
|
import { CacheKeepalive, type CacheKeepaliveConfig } from '../cache-keepalive.js';
|
|
26
28
|
|
|
27
29
|
// ============================================================================
|
|
@@ -128,6 +130,18 @@ export function thinkingEnabled(request: ProviderRequest): boolean {
|
|
|
128
130
|
// Adapter Configuration
|
|
129
131
|
// ============================================================================
|
|
130
132
|
|
|
133
|
+
/**
|
|
134
|
+
* What the dynamicHeaders callback is told about the request it stamps.
|
|
135
|
+
* `lane` names the transport shape: 'stream' is the conversational turn loop,
|
|
136
|
+
* 'complete' the non-streamed lane (compression, side-calls, keepalive
|
|
137
|
+
* touches). A stamp that describes WHY the agent's turn fired belongs on the
|
|
138
|
+
* stream lane only — a compression call running in the background is not the
|
|
139
|
+
* turn, and stamping it with the turn's cause would lie to the ledger.
|
|
140
|
+
*/
|
|
141
|
+
export interface DynamicHeadersContext {
|
|
142
|
+
lane: 'stream' | 'complete';
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
export interface AnthropicAdapterConfig {
|
|
132
146
|
/** API key (defaults to ANTHROPIC_API_KEY env var) */
|
|
133
147
|
apiKey?: string | null;
|
|
@@ -144,6 +158,18 @@ export interface AnthropicAdapterConfig {
|
|
|
144
158
|
|
|
145
159
|
/** Default headers to include with Anthropic requests */
|
|
146
160
|
defaultHeaders?: ClientOptions['defaultHeaders'];
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Live per-request headers, evaluated at request time — for values that
|
|
164
|
+
* change between calls (e.g. household telemetry stamps such as
|
|
165
|
+
* `x-gate-debt-chunks`, read by an inference gateway and stripped there
|
|
166
|
+
* before the vendor ever sees them). Merged over the per-request beta
|
|
167
|
+
* headers for the OUTGOING request only; cache-keepalive replays
|
|
168
|
+
* deliberately resend their recorded headers, so a telemetry stamp is
|
|
169
|
+
* never replayed stale — an unstamped touch is honest, a stale stamp lies.
|
|
170
|
+
* null/undefined/'' values are dropped.
|
|
171
|
+
*/
|
|
172
|
+
dynamicHeaders?: (ctx?: DynamicHeadersContext) => Record<string, string | number | null | undefined>;
|
|
147
173
|
|
|
148
174
|
/** Default max tokens */
|
|
149
175
|
defaultMaxTokens?: number;
|
|
@@ -164,6 +190,14 @@ export interface AnthropicAdapterConfig {
|
|
|
164
190
|
|
|
165
191
|
export class AnthropicAdapter implements ProviderAdapter {
|
|
166
192
|
readonly name = 'anthropic';
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Verified live 2026-08-25 (claude-haiku-4-5, 4,650-token cached system
|
|
196
|
+
* prompt): call 1 returned input_tokens 8 / cache_creation_input_tokens 4650,
|
|
197
|
+
* call 2 input_tokens 8 / cache_read_input_tokens 4650. `input_tokens` never
|
|
198
|
+
* counts the cached span.
|
|
199
|
+
*/
|
|
200
|
+
readonly usageCacheConvention = 'cache-excluded' as const;
|
|
167
201
|
private client: Anthropic;
|
|
168
202
|
private defaultMaxTokens: number;
|
|
169
203
|
/** Any anthropic-beta value from defaultHeaders (e.g. the oauth beta for
|
|
@@ -173,6 +207,8 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
173
207
|
private defaultBeta: string | undefined;
|
|
174
208
|
/** Holds idle agents' cached prefixes warm; undefined when disabled. */
|
|
175
209
|
readonly cacheKeepalive: CacheKeepalive | undefined;
|
|
210
|
+
/** Live per-request header source (see AnthropicAdapterConfig.dynamicHeaders). */
|
|
211
|
+
private readonly dynamicHeaders?: (ctx?: DynamicHeadersContext) => Record<string, string | number | null | undefined>;
|
|
176
212
|
|
|
177
213
|
constructor(config: AnthropicAdapterConfig = {}) {
|
|
178
214
|
const clientOptions: ClientOptions = {
|
|
@@ -180,6 +216,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
180
216
|
defaultHeaders: config.defaultHeaders,
|
|
181
217
|
};
|
|
182
218
|
this.defaultBeta = extractBetaHeader(config.defaultHeaders);
|
|
219
|
+
this.dynamicHeaders = config.dynamicHeaders;
|
|
183
220
|
|
|
184
221
|
if (config.authToken !== undefined) {
|
|
185
222
|
clientOptions.authToken = config.authToken;
|
|
@@ -225,7 +262,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
225
262
|
try {
|
|
226
263
|
const response = await this.client.messages.create(fullRequest, {
|
|
227
264
|
signal: options?.signal,
|
|
228
|
-
headers,
|
|
265
|
+
headers: this.liveHeaders(headers, 'complete'),
|
|
229
266
|
});
|
|
230
267
|
|
|
231
268
|
return this.parseResponse(response, fullRequest);
|
|
@@ -298,7 +335,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
298
335
|
try {
|
|
299
336
|
const stream = await this.client.messages.stream(anthropicRequest, {
|
|
300
337
|
signal: idleAbort.signal,
|
|
301
|
-
headers: this.betaHeaders(request),
|
|
338
|
+
headers: this.liveHeaders(this.betaHeaders(request), 'stream'),
|
|
302
339
|
});
|
|
303
340
|
|
|
304
341
|
// Accumulate response metadata from SSE events directly, so we can
|
|
@@ -314,6 +351,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
314
351
|
let inferenceGeo: string | undefined;
|
|
315
352
|
let serviceTier: string | undefined;
|
|
316
353
|
let stopReason: string = 'end_turn';
|
|
354
|
+
let sawTerminalEvent = false;
|
|
317
355
|
let stopSequence: string | undefined;
|
|
318
356
|
let stopDetails: unknown;
|
|
319
357
|
|
|
@@ -329,6 +367,43 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
329
367
|
// thinking blocks with no thinking_delta at all (signature only).
|
|
330
368
|
const wrapThinkingTags = options?.wrapThinkingTags === true;
|
|
331
369
|
let thinkingTagOpen = false;
|
|
370
|
+
// Index of a block that has started and not yet been stopped. A stream
|
|
371
|
+
// can end with one still open: measured live against claude-haiku-4-5
|
|
372
|
+
// (2026-08-25), a tool call truncated by max_tokens emits
|
|
373
|
+
// content_block_start + input_json_delta fragments and then goes
|
|
374
|
+
// straight to message_delta — no content_block_stop at all.
|
|
375
|
+
let openBlockIndex = -1;
|
|
376
|
+
|
|
377
|
+
const finalizeContentBlock = (blockIdx: number, sawBlockStop: boolean): void => {
|
|
378
|
+
const block = contentBlocks[blockIdx];
|
|
379
|
+
if (block) {
|
|
380
|
+
if (block.type === 'text') {
|
|
381
|
+
block.text = currentBlockContent;
|
|
382
|
+
} else if (block.type === 'thinking') {
|
|
383
|
+
block.thinking = currentBlockContent;
|
|
384
|
+
if (thinkingTagOpen) {
|
|
385
|
+
callbacks.onChunk('</thinking>\n');
|
|
386
|
+
thinkingTagOpen = false;
|
|
387
|
+
}
|
|
388
|
+
} else if (block.type === 'tool_use') {
|
|
389
|
+
if (!sawBlockStop) {
|
|
390
|
+
// Arguments never finished arriving, so `input` is still the
|
|
391
|
+
// empty object content_block_start carried — a plausible no-arg
|
|
392
|
+
// call that nothing downstream can distinguish from a real one.
|
|
393
|
+
block.unparseableInput = currentBlockInputJson;
|
|
394
|
+
} else if (currentBlockInputJson) {
|
|
395
|
+
try {
|
|
396
|
+
block.input = JSON.parse(currentBlockInputJson);
|
|
397
|
+
} catch {
|
|
398
|
+
// Same fabrication, reached by a block that did stop: keep the
|
|
399
|
+
// raw accumulation and mark it so consumers can refuse.
|
|
400
|
+
block.unparseableInput = currentBlockInputJson;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
callbacks.onContentBlock?.(blockIdx, contentBlocks[blockIdx]);
|
|
406
|
+
};
|
|
332
407
|
|
|
333
408
|
for await (const event of stream) {
|
|
334
409
|
sawEvent = true;
|
|
@@ -354,6 +429,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
354
429
|
|
|
355
430
|
} else if (event.type === 'content_block_start') {
|
|
356
431
|
currentBlockIndex = event.index;
|
|
432
|
+
openBlockIndex = event.index;
|
|
357
433
|
currentBlockContent = '';
|
|
358
434
|
currentBlockInputJson = '';
|
|
359
435
|
contentBlocks[currentBlockIndex] = { ...event.content_block };
|
|
@@ -387,22 +463,8 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
387
463
|
|
|
388
464
|
} else if (event.type === 'content_block_stop') {
|
|
389
465
|
// Finalize block — use event.index for defensive correctness
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (block) {
|
|
393
|
-
if (block.type === 'text') {
|
|
394
|
-
block.text = currentBlockContent;
|
|
395
|
-
} else if (block.type === 'thinking') {
|
|
396
|
-
block.thinking = currentBlockContent;
|
|
397
|
-
if (thinkingTagOpen) {
|
|
398
|
-
callbacks.onChunk('</thinking>\n');
|
|
399
|
-
thinkingTagOpen = false;
|
|
400
|
-
}
|
|
401
|
-
} else if (block.type === 'tool_use' && currentBlockInputJson) {
|
|
402
|
-
try { block.input = JSON.parse(currentBlockInputJson); } catch { /* partial JSON */ }
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
callbacks.onContentBlock?.(blockIdx, contentBlocks[blockIdx]);
|
|
466
|
+
finalizeContentBlock((event as { index: number }).index, true);
|
|
467
|
+
openBlockIndex = -1;
|
|
406
468
|
|
|
407
469
|
} else if (event.type === 'message_delta') {
|
|
408
470
|
// All content blocks are finalized by the time message_delta arrives.
|
|
@@ -414,6 +476,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
414
476
|
stop_details?: unknown;
|
|
415
477
|
};
|
|
416
478
|
stopReason = delta.stop_reason ?? 'end_turn';
|
|
479
|
+
sawTerminalEvent = true;
|
|
417
480
|
stopSequence = delta.stop_sequence ?? undefined;
|
|
418
481
|
// stop_details carries refusal metadata (e.g., category: 'reasoning_extraction')
|
|
419
482
|
stopDetails = delta.stop_details ?? undefined;
|
|
@@ -438,9 +501,22 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
438
501
|
if (idleTimer) clearTimeout(idleTimer);
|
|
439
502
|
options?.signal?.removeEventListener('abort', onExternalAbort);
|
|
440
503
|
|
|
504
|
+
// A block still open here never received its content_block_stop: the
|
|
505
|
+
// turn ended mid-block. Finalize it so the accumulated text is not lost
|
|
506
|
+
// and a truncated tool call is marked rather than persisted as `{}`.
|
|
507
|
+
if (openBlockIndex >= 0) {
|
|
508
|
+
finalizeContentBlock(openBlockIndex, false);
|
|
509
|
+
openBlockIndex = -1;
|
|
510
|
+
}
|
|
511
|
+
|
|
441
512
|
// Force-close the HTTP connection so we don't block on SSE drain
|
|
442
513
|
try { stream.controller.abort(); } catch { /* already closed */ }
|
|
443
514
|
|
|
515
|
+
// message_delta is this adapter's terminal event — the loop breaks on
|
|
516
|
+
// it. Falling out of the for-await without one means the SSE connection
|
|
517
|
+
// closed mid-turn, and stopReason is still its 'end_turn' initialiser.
|
|
518
|
+
assertTerminalEventObserved(sawTerminalEvent, 'Anthropic', fullRequest);
|
|
519
|
+
|
|
444
520
|
return {
|
|
445
521
|
content: contentBlocks,
|
|
446
522
|
stopReason,
|
|
@@ -506,6 +582,19 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
506
582
|
* the SDK replaces same-key defaults instead of merging, and the API
|
|
507
583
|
* accepts comma-separated betas. Undefined when nothing to add, so the
|
|
508
584
|
* defaults apply untouched. */
|
|
585
|
+
/** Base headers + the live dynamicHeaders stamp. Request time only: the
|
|
586
|
+
* keepalive recorder receives the base headers BEFORE this merge, so
|
|
587
|
+
* replayed touches never carry a stale telemetry value. */
|
|
588
|
+
private liveHeaders(base: Record<string, string> | undefined, lane: DynamicHeadersContext['lane']): Record<string, string> | undefined {
|
|
589
|
+
const dyn = this.dynamicHeaders?.({ lane });
|
|
590
|
+
if (!dyn) return base;
|
|
591
|
+
const out: Record<string, string> = { ...(base ?? {}) };
|
|
592
|
+
for (const [k, v] of Object.entries(dyn)) {
|
|
593
|
+
if (v !== null && v !== undefined && v !== '') out[k] = String(v);
|
|
594
|
+
}
|
|
595
|
+
return Object.keys(out).length ? out : undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
509
598
|
private betaHeaders(request: ProviderRequest): Record<string, string> | undefined {
|
|
510
599
|
if (!thinkingEnabled(request) || !needsInterleavedThinkingBeta(request.model)) {
|
|
511
600
|
return undefined;
|
|
@@ -672,6 +761,10 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
672
761
|
}
|
|
673
762
|
|
|
674
763
|
private handleError(error: unknown, rawRequest?: unknown): MembraneError {
|
|
764
|
+
// Already-classified failures (e.g. the stream-integrity guards) keep
|
|
765
|
+
// their type and retryability instead of being re-derived from a string.
|
|
766
|
+
if (error instanceof MembraneError) return error;
|
|
767
|
+
|
|
675
768
|
if (error instanceof Anthropic.APIError) {
|
|
676
769
|
// Mid-stream SSE `error` events are rethrown by the SDK as APIError
|
|
677
770
|
// with status === undefined (sdk core/streaming.js), so the HTTP
|
|
@@ -880,6 +973,23 @@ export function toAnthropicContent(blocks: ContentBlock[]): Anthropic.ContentBlo
|
|
|
880
973
|
media_type: block.source.mediaType as 'application/pdf',
|
|
881
974
|
data: block.source.data,
|
|
882
975
|
},
|
|
976
|
+
// Anthropic's document block carries the filename as `title`; it was
|
|
977
|
+
// being dropped, so the model lost the one hint about what the PDF is.
|
|
978
|
+
...(block.filename ? { title: block.filename } : {}),
|
|
979
|
+
});
|
|
980
|
+
break;
|
|
981
|
+
|
|
982
|
+
case 'generated_image':
|
|
983
|
+
// A provider-generated image is a base64 image with a MIME type, which
|
|
984
|
+
// is exactly Anthropic's image block — carrying it across costs nothing
|
|
985
|
+
// and lets an image Gemini produced re-enter Anthropic history.
|
|
986
|
+
result.push({
|
|
987
|
+
type: 'image',
|
|
988
|
+
source: {
|
|
989
|
+
type: 'base64',
|
|
990
|
+
media_type: block.mimeType as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp',
|
|
991
|
+
data: block.data,
|
|
992
|
+
},
|
|
883
993
|
});
|
|
884
994
|
break;
|
|
885
995
|
|
|
@@ -919,12 +1029,53 @@ export function toAnthropicContent(blocks: ContentBlock[]): Anthropic.ContentBlo
|
|
|
919
1029
|
data: (block as any).data,
|
|
920
1030
|
} as any);
|
|
921
1031
|
break;
|
|
1032
|
+
|
|
1033
|
+
default: {
|
|
1034
|
+
// The REQUEST path cannot degrade gracefully: a dropped block reaches
|
|
1035
|
+
// the model as an absence, and it answers about content it was never
|
|
1036
|
+
// shown. `audio` and `video` have no Anthropic Messages representation
|
|
1037
|
+
// at all, and a block type added later would silently join them. Fail
|
|
1038
|
+
// loudly at the boundary instead — the caller can strip or transcode.
|
|
1039
|
+
const unsupportedType = (block as { type?: string }).type ?? 'unknown';
|
|
1040
|
+
throw unsupportedError(
|
|
1041
|
+
`Anthropic has no representation for a "${unsupportedType}" content block, and dropping`
|
|
1042
|
+
+ ' it would send the model a message missing content the caller supplied.'
|
|
1043
|
+
+ ' Remove or convert the block before sending it on this provider.'
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
922
1046
|
}
|
|
923
1047
|
}
|
|
924
1048
|
|
|
925
1049
|
return result;
|
|
926
1050
|
}
|
|
927
1051
|
|
|
1052
|
+
/** Unrecognised response block types warn once each, not once per conversion. */
|
|
1053
|
+
const warnedUnconvertibleResponseBlocks = new Set<string>();
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* The RESPONSE path can degrade gracefully where the request path cannot: the
|
|
1057
|
+
* provider's own block is in hand, so keeping it verbatim on a zero-width
|
|
1058
|
+
* carrier loses nothing recoverable and lets formatters replay it. Warn once
|
|
1059
|
+
* per type so a new provider block type surfaces without flooding the log.
|
|
1060
|
+
*/
|
|
1061
|
+
function preserveUnconvertibleBlock(block: unknown, sourceLabel: string): ContentBlock {
|
|
1062
|
+
const blockType = (block as { type?: string })?.type ?? 'unknown';
|
|
1063
|
+
if (!warnedUnconvertibleResponseBlocks.has(blockType)) {
|
|
1064
|
+
warnedUnconvertibleResponseBlocks.add(blockType);
|
|
1065
|
+
console.warn(
|
|
1066
|
+
`[membrane:${sourceLabel}] no normalized ContentBlock for provider block type`
|
|
1067
|
+
+ ` "${blockType}" — preserving it verbatim as a rawItem carrier so it can be`
|
|
1068
|
+
+ ' replayed, but its content is not visible to normalized consumers.'
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
return { type: 'text', text: '', rawItem: block } as ContentBlock;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Test seam: the once-per-type warn latch is process-wide otherwise. */
|
|
1075
|
+
export function resetUnconvertibleBlockWarnings(): void {
|
|
1076
|
+
warnedUnconvertibleResponseBlocks.clear();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
928
1079
|
/**
|
|
929
1080
|
* Convert Anthropic response content to normalized format
|
|
930
1081
|
*/
|
|
@@ -955,11 +1106,14 @@ export function fromAnthropicContent(blocks: Anthropic.ContentBlock[]): ContentB
|
|
|
955
1106
|
break;
|
|
956
1107
|
|
|
957
1108
|
default:
|
|
958
|
-
// Handle redacted_thinking or unknown types
|
|
959
1109
|
if ((block as any).type === 'redacted_thinking') {
|
|
960
1110
|
// Preserve the encrypted `data` payload — without it the block
|
|
961
1111
|
// cannot be round-tripped and prior reasoning is lost.
|
|
962
1112
|
result.push({ type: 'redacted_thinking', data: (block as any).data } as any);
|
|
1113
|
+
} else {
|
|
1114
|
+
// server_tool_use, web_search_tool_result, search_result, mcp_tool_use
|
|
1115
|
+
// and anything Anthropic adds later used to fall out here silently.
|
|
1116
|
+
result.push(preserveUnconvertibleBlock(block, 'anthropic'));
|
|
963
1117
|
}
|
|
964
1118
|
break;
|
|
965
1119
|
}
|
package/src/providers/bedrock.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
serverError,
|
|
21
21
|
abortError,
|
|
22
22
|
} from '../types/index.js';
|
|
23
|
-
import { createCombinedSignal } from './utils.js';
|
|
23
|
+
import { createCombinedSignal, isDeadlineAbort, deadlineTimeoutError, assertTerminalEventObserved } from './utils.js';
|
|
24
24
|
import {
|
|
25
25
|
INTERLEAVED_THINKING_BETA,
|
|
26
26
|
needsInterleavedThinkingBeta,
|
|
@@ -273,6 +273,15 @@ async function signRequest(
|
|
|
273
273
|
export class BedrockAdapter implements ProviderAdapter {
|
|
274
274
|
readonly name = 'bedrock';
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Bedrock serves Anthropic models over the Anthropic Messages payload shape
|
|
278
|
+
* and this adapter reads Anthropic's own field names
|
|
279
|
+
* (`cache_read_input_tokens` / `cache_creation_input_tokens`), so it inherits
|
|
280
|
+
* the convention verified live against api.anthropic.com on 2026-08-25.
|
|
281
|
+
* Derived from the wire contract, not measured against Bedrock directly.
|
|
282
|
+
*/
|
|
283
|
+
readonly usageCacheConvention = 'cache-excluded' as const;
|
|
284
|
+
|
|
276
285
|
private accessKeyId: string;
|
|
277
286
|
private secretAccessKey: string;
|
|
278
287
|
private sessionToken?: string;
|
|
@@ -610,6 +619,7 @@ export class BedrockAdapter implements ProviderAdapter {
|
|
|
610
619
|
let cacheCreationTokens: number | undefined;
|
|
611
620
|
let cacheReadTokens: number | undefined;
|
|
612
621
|
let stopReason: string = 'end_turn';
|
|
622
|
+
let sawTerminalEvent = false;
|
|
613
623
|
let stopSequence: string | undefined;
|
|
614
624
|
let fullText = '';
|
|
615
625
|
|
|
@@ -782,6 +792,7 @@ export class BedrockAdapter implements ProviderAdapter {
|
|
|
782
792
|
}
|
|
783
793
|
callbacks.onContentBlock?.(blockIdx, contentBlocks[blockIdx]);
|
|
784
794
|
} else if (eventData.type === 'message_delta') {
|
|
795
|
+
sawTerminalEvent = true;
|
|
785
796
|
if (eventData.usage) {
|
|
786
797
|
outputTokens = eventData.usage.output_tokens;
|
|
787
798
|
// message_delta carries cumulative cache metrics — use as
|
|
@@ -833,6 +844,11 @@ export class BedrockAdapter implements ProviderAdapter {
|
|
|
833
844
|
);
|
|
834
845
|
}
|
|
835
846
|
|
|
847
|
+
// The empty-response guard above only catches a stream that produced
|
|
848
|
+
// NOTHING; a stream truncated after any content passed it and was
|
|
849
|
+
// reported as a clean end_turn. message_delta is the terminal event.
|
|
850
|
+
assertTerminalEventObserved(sawTerminalEvent, 'Bedrock', { modelId, ...request });
|
|
851
|
+
|
|
836
852
|
// Build response from accumulated data
|
|
837
853
|
finalMessage = {
|
|
838
854
|
id: 'msg_stream',
|
|
@@ -917,6 +933,14 @@ export class BedrockAdapter implements ProviderAdapter {
|
|
|
917
933
|
}
|
|
918
934
|
|
|
919
935
|
private handleError(error: unknown, rawRequest?: unknown): MembraneError {
|
|
936
|
+
// A deadline abort is a timeout and stays one. Collapsing it into a bare
|
|
937
|
+
// abortError() here is what erased the identity before Membrane's
|
|
938
|
+
// caller-signal > timeout > error ladder could read it.
|
|
939
|
+
if (isDeadlineAbort(error)) return deadlineTimeoutError(error, rawRequest);
|
|
940
|
+
// Already-classified failures (e.g. the stream-integrity guards) keep
|
|
941
|
+
// their type and retryability instead of being re-derived from a string.
|
|
942
|
+
if (error instanceof MembraneError) return error;
|
|
943
|
+
|
|
920
944
|
if (error instanceof BedrockError) {
|
|
921
945
|
const status = error.status;
|
|
922
946
|
const message = error.message;
|
package/src/providers/gemini.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
abortError,
|
|
29
29
|
networkError,
|
|
30
30
|
} from '../types/index.js';
|
|
31
|
-
import { createCombinedSignal } from './utils.js';
|
|
31
|
+
import { createCombinedSignal, isDeadlineAbort, deadlineTimeoutError, throwOnStreamErrorFrame, assertTerminalEventObserved } from './utils.js';
|
|
32
32
|
|
|
33
33
|
// ============================================================================
|
|
34
34
|
// Gemini API Types
|
|
@@ -77,11 +77,58 @@ interface GeminiResponse {
|
|
|
77
77
|
candidatesTokenCount?: number;
|
|
78
78
|
totalTokenCount?: number;
|
|
79
79
|
cachedContentTokenCount?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Reasoning ("thinking") tokens. DISJOINT from candidatesTokenCount and
|
|
82
|
+
* billed at the output rate, so generated output is candidates + thoughts.
|
|
83
|
+
* Live receipt 2026-08-25, gemini-3.5-flash-lite, thinkingBudget 512:
|
|
84
|
+
* prompt 35, candidates 2, thoughts 228, total 265 — 35+2+228 === 265.
|
|
85
|
+
*/
|
|
86
|
+
thoughtsTokenCount?: number;
|
|
80
87
|
};
|
|
81
88
|
modelVersion?: string;
|
|
82
89
|
error?: { code: number; message: string; status: string };
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Map Gemini's `usageMetadata` onto membrane's usage shape.
|
|
94
|
+
*
|
|
95
|
+
* `thoughtsTokenCount` is disjoint from `candidatesTokenCount` and billed at
|
|
96
|
+
* the output rate, so generated output is the SUM of the two; reading only
|
|
97
|
+
* candidates reported a thinking turn at a fraction of its real size. The
|
|
98
|
+
* reconciliation check makes the next such omission loud rather than silent:
|
|
99
|
+
* Google's own total is the independent witness, and a mismatch means a
|
|
100
|
+
* usageMetadata field membrane does not read is carrying tokens.
|
|
101
|
+
*/
|
|
102
|
+
function geminiUsageToProviderUsage(
|
|
103
|
+
usageMetadata: GeminiResponse['usageMetadata']
|
|
104
|
+
): ProviderResponse['usage'] {
|
|
105
|
+
const promptTokens = usageMetadata?.promptTokenCount ?? 0;
|
|
106
|
+
const candidatesTokens = usageMetadata?.candidatesTokenCount ?? 0;
|
|
107
|
+
const thoughtsTokens = usageMetadata?.thoughtsTokenCount;
|
|
108
|
+
const totalTokens = usageMetadata?.totalTokenCount;
|
|
109
|
+
|
|
110
|
+
if (usageMetadata && totalTokens != null) {
|
|
111
|
+
const accountedTokens = promptTokens + candidatesTokens + (thoughtsTokens ?? 0);
|
|
112
|
+
if (accountedTokens !== totalTokens) {
|
|
113
|
+
console.warn(
|
|
114
|
+
`[membrane:gemini] usageMetadata does not reconcile: promptTokenCount(${promptTokens})`
|
|
115
|
+
+ ` + candidatesTokenCount(${candidatesTokens}) + thoughtsTokenCount(${thoughtsTokens ?? 0})`
|
|
116
|
+
+ ` = ${accountedTokens}, but totalTokenCount = ${totalTokens}.`
|
|
117
|
+
+ ' Some billed tokens are in a usageMetadata field membrane does not read.'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
inputTokens: promptTokens,
|
|
124
|
+
outputTokens: candidatesTokens + (thoughtsTokens ?? 0),
|
|
125
|
+
...(thoughtsTokens != null ? { thinkingTokens: thoughtsTokens } : {}),
|
|
126
|
+
cacheReadTokens: usageMetadata?.cachedContentTokenCount
|
|
127
|
+
? usageMetadata.cachedContentTokenCount
|
|
128
|
+
: undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
// ============================================================================
|
|
86
133
|
// Adapter Configuration
|
|
87
134
|
// ============================================================================
|
|
@@ -103,6 +150,18 @@ export interface GeminiAdapterConfig {
|
|
|
103
150
|
|
|
104
151
|
export class GeminiAdapter implements ProviderAdapter {
|
|
105
152
|
readonly name = 'gemini';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* NOT ESTABLISHED. Google documents `cachedContentTokenCount` but the probes
|
|
156
|
+
* available on 2026-08-25 could not produce a cache hit to measure against:
|
|
157
|
+
* three identical 10,893-token calls to gemini-3.5-flash-lite never reported
|
|
158
|
+
* the field (implicit caching did not trigger), and explicit `cachedContents`
|
|
159
|
+
* is refused on the free tier (429,
|
|
160
|
+
* TotalCachedContentStorageTokensPerModelFreeTier limit=0). Declared honestly
|
|
161
|
+
* rather than guessed — membrane passes the counts through and warns once if
|
|
162
|
+
* a cache read ever arrives.
|
|
163
|
+
*/
|
|
164
|
+
readonly usageCacheConvention = 'unknown' as const;
|
|
106
165
|
private apiKey: string;
|
|
107
166
|
private baseURL: string;
|
|
108
167
|
private defaultMaxTokens: number;
|
|
@@ -188,11 +247,65 @@ export class GeminiAdapter implements ProviderAdapter {
|
|
|
188
247
|
const decoder = new TextDecoder();
|
|
189
248
|
let accumulated = '';
|
|
190
249
|
let finishReason = 'STOP';
|
|
250
|
+
let sawTerminalEvent = false;
|
|
191
251
|
let toolCalls: { name: string; args: Record<string, unknown> }[] = [];
|
|
192
252
|
let images: { data: string; mimeType: string }[] = [];
|
|
193
253
|
let lastUsage: GeminiResponse['usageMetadata'] | undefined;
|
|
254
|
+
// The resolved model Google actually served, echoed on stream frames.
|
|
255
|
+
// Reporting the requested id instead hides alias/auto-upgrade routing.
|
|
256
|
+
let lastModelVersion: string | undefined;
|
|
194
257
|
let buffer = '';
|
|
195
258
|
|
|
259
|
+
// One frame handler for both the streaming lines and the trailing
|
|
260
|
+
// buffer — the two used to carry byte-identical copies of this logic,
|
|
261
|
+
// so any fix (error frames, terminal observation) had to be made twice.
|
|
262
|
+
const processDataLine = (dataLine: string): void => {
|
|
263
|
+
let parsed: GeminiResponse;
|
|
264
|
+
try {
|
|
265
|
+
parsed = JSON.parse(dataLine) as GeminiResponse;
|
|
266
|
+
} catch {
|
|
267
|
+
return; // Ignore parse errors in stream chunks
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throwOnStreamErrorFrame(parsed, 'Gemini', geminiRequest);
|
|
271
|
+
|
|
272
|
+
const candidate = parsed.candidates?.[0];
|
|
273
|
+
|
|
274
|
+
if (candidate?.content?.parts) {
|
|
275
|
+
for (const part of candidate.content.parts) {
|
|
276
|
+
if (part.text) {
|
|
277
|
+
accumulated += part.text;
|
|
278
|
+
callbacks.onChunk(part.text);
|
|
279
|
+
}
|
|
280
|
+
if (part.inlineData) {
|
|
281
|
+
images.push({
|
|
282
|
+
data: part.inlineData.data,
|
|
283
|
+
mimeType: part.inlineData.mimeType,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (part.functionCall) {
|
|
287
|
+
toolCalls.push({
|
|
288
|
+
name: part.functionCall.name,
|
|
289
|
+
args: part.functionCall.args,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (candidate?.finishReason) {
|
|
296
|
+
finishReason = candidate.finishReason;
|
|
297
|
+
sawTerminalEvent = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (parsed.usageMetadata) {
|
|
301
|
+
lastUsage = parsed.usageMetadata;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (parsed.modelVersion) {
|
|
305
|
+
lastModelVersion = parsed.modelVersion;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
196
309
|
while (true) {
|
|
197
310
|
const { done, value } = await reader.read();
|
|
198
311
|
if (done) break;
|
|
@@ -206,42 +319,7 @@ export class GeminiAdapter implements ProviderAdapter {
|
|
|
206
319
|
if (!line.startsWith('data: ')) continue;
|
|
207
320
|
const data = line.slice(6).trim();
|
|
208
321
|
if (!data || data === '[DONE]') continue;
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const parsed = JSON.parse(data) as GeminiResponse;
|
|
212
|
-
const candidate = parsed.candidates?.[0];
|
|
213
|
-
|
|
214
|
-
if (candidate?.content?.parts) {
|
|
215
|
-
for (const part of candidate.content.parts) {
|
|
216
|
-
if (part.text) {
|
|
217
|
-
accumulated += part.text;
|
|
218
|
-
callbacks.onChunk(part.text);
|
|
219
|
-
}
|
|
220
|
-
if (part.inlineData) {
|
|
221
|
-
images.push({
|
|
222
|
-
data: part.inlineData.data,
|
|
223
|
-
mimeType: part.inlineData.mimeType,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
if (part.functionCall) {
|
|
227
|
-
toolCalls.push({
|
|
228
|
-
name: part.functionCall.name,
|
|
229
|
-
args: part.functionCall.args,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (candidate?.finishReason) {
|
|
236
|
-
finishReason = candidate.finishReason;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (parsed.usageMetadata) {
|
|
240
|
-
lastUsage = parsed.usageMetadata;
|
|
241
|
-
}
|
|
242
|
-
} catch {
|
|
243
|
-
// Ignore parse errors in stream chunks
|
|
244
|
-
}
|
|
322
|
+
processDataLine(data);
|
|
245
323
|
}
|
|
246
324
|
}
|
|
247
325
|
|
|
@@ -250,56 +328,18 @@ export class GeminiAdapter implements ProviderAdapter {
|
|
|
250
328
|
const remaining = buffer.trim();
|
|
251
329
|
const dataLine = remaining.startsWith('data: ') ? remaining.slice(6).trim() : remaining;
|
|
252
330
|
if (dataLine && dataLine !== '[DONE]') {
|
|
253
|
-
|
|
254
|
-
const parsed = JSON.parse(dataLine) as GeminiResponse;
|
|
255
|
-
const candidate = parsed.candidates?.[0];
|
|
256
|
-
|
|
257
|
-
if (candidate?.content?.parts) {
|
|
258
|
-
for (const part of candidate.content.parts) {
|
|
259
|
-
if (part.text) {
|
|
260
|
-
accumulated += part.text;
|
|
261
|
-
callbacks.onChunk(part.text);
|
|
262
|
-
}
|
|
263
|
-
if (part.inlineData) {
|
|
264
|
-
images.push({
|
|
265
|
-
data: part.inlineData.data,
|
|
266
|
-
mimeType: part.inlineData.mimeType,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
if (part.functionCall) {
|
|
270
|
-
toolCalls.push({
|
|
271
|
-
name: part.functionCall.name,
|
|
272
|
-
args: part.functionCall.args,
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (candidate?.finishReason) {
|
|
279
|
-
finishReason = candidate.finishReason;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (parsed.usageMetadata) {
|
|
283
|
-
lastUsage = parsed.usageMetadata;
|
|
284
|
-
}
|
|
285
|
-
} catch {
|
|
286
|
-
// Final buffer wasn't valid JSON — nothing to do
|
|
287
|
-
}
|
|
331
|
+
processDataLine(dataLine);
|
|
288
332
|
}
|
|
289
333
|
}
|
|
290
334
|
|
|
335
|
+
assertTerminalEventObserved(sawTerminalEvent, 'Gemini', geminiRequest);
|
|
336
|
+
|
|
291
337
|
return {
|
|
292
338
|
content: this.buildContentBlocks(accumulated, toolCalls, images),
|
|
293
339
|
stopReason: this.mapFinishReason(finishReason),
|
|
294
340
|
stopSequence: undefined,
|
|
295
|
-
usage:
|
|
296
|
-
|
|
297
|
-
outputTokens: lastUsage?.candidatesTokenCount ?? 0,
|
|
298
|
-
cacheReadTokens: lastUsage?.cachedContentTokenCount
|
|
299
|
-
? lastUsage.cachedContentTokenCount
|
|
300
|
-
: undefined,
|
|
301
|
-
},
|
|
302
|
-
model: request.model,
|
|
341
|
+
usage: geminiUsageToProviderUsage(lastUsage),
|
|
342
|
+
model: lastModelVersion ?? request.model,
|
|
303
343
|
rawRequest: geminiRequest,
|
|
304
344
|
raw: { finishReason, usage: lastUsage },
|
|
305
345
|
};
|
|
@@ -563,13 +603,7 @@ export class GeminiAdapter implements ProviderAdapter {
|
|
|
563
603
|
content: this.buildContentBlocks(text, toolCalls, images),
|
|
564
604
|
stopReason: this.mapFinishReason(candidate?.finishReason),
|
|
565
605
|
stopSequence: undefined,
|
|
566
|
-
usage:
|
|
567
|
-
inputTokens: response.usageMetadata?.promptTokenCount ?? 0,
|
|
568
|
-
outputTokens: response.usageMetadata?.candidatesTokenCount ?? 0,
|
|
569
|
-
cacheReadTokens: response.usageMetadata?.cachedContentTokenCount
|
|
570
|
-
? response.usageMetadata.cachedContentTokenCount
|
|
571
|
-
: undefined,
|
|
572
|
-
},
|
|
606
|
+
usage: geminiUsageToProviderUsage(response.usageMetadata),
|
|
573
607
|
model: response.modelVersion ?? requestedModel,
|
|
574
608
|
rawRequest,
|
|
575
609
|
raw: response,
|
|
@@ -630,6 +664,10 @@ export class GeminiAdapter implements ProviderAdapter {
|
|
|
630
664
|
// --------------------------------------------------------------------------
|
|
631
665
|
|
|
632
666
|
private handleError(error: unknown, rawRequest?: unknown): MembraneError {
|
|
667
|
+
// A deadline abort is a timeout and stays one. Collapsing it into a bare
|
|
668
|
+
// abortError() here is what erased the identity before Membrane's
|
|
669
|
+
// caller-signal > timeout > error ladder could read it.
|
|
670
|
+
if (isDeadlineAbort(error)) return deadlineTimeoutError(error, rawRequest);
|
|
633
671
|
if (error instanceof MembraneError) return error;
|
|
634
672
|
|
|
635
673
|
if (error instanceof Error) {
|