@animalabs/membrane 0.5.79 → 0.5.81
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-keepalive.d.ts +115 -0
- package/dist/cache-keepalive.d.ts.map +1 -0
- package/dist/cache-keepalive.js +0 -0
- package/dist/cache-keepalive.js.map +1 -0
- package/dist/cache-keepalive.test.d.ts +2 -0
- package/dist/cache-keepalive.test.d.ts.map +1 -0
- package/dist/cache-keepalive.test.js +206 -0
- package/dist/cache-keepalive.test.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.d.ts +2 -0
- package/dist/floating-cache-marker.test.d.ts.map +1 -0
- package/dist/floating-cache-marker.test.js +263 -0
- package/dist/floating-cache-marker.test.js.map +1 -0
- 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 +57 -19
- package/dist/formatters/anthropic-xml.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 +29 -0
- package/dist/formatters/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/membrane.d.ts +142 -7
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +881 -270
- 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 +163 -26
- 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/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/config.d.ts +5 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.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 +13 -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 +82 -0
- package/dist/utils/cache-marker-budget.d.ts.map +1 -0
- package/dist/utils/cache-marker-budget.js +189 -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 +3 -2
- package/src/cache-keepalive.test.ts +244 -0
- package/src/cache-keepalive.ts +385 -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 +285 -0
- package/src/formatters/anthropic-xml.ts +62 -20
- package/src/formatters/normalize-tool-pairs.ts +49 -0
- package/src/formatters/types.ts +32 -0
- package/src/index.ts +13 -0
- package/src/membrane.ts +1031 -279
- package/src/providers/anthropic.ts +205 -20
- package/src/providers/bedrock.ts +25 -1
- package/src/providers/gemini.ts +125 -87
- 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/config.ts +6 -0
- 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 +14 -0
- package/src/types/response.ts +109 -8
- package/src/types/streaming.ts +8 -2
- package/src/utils/cache-marker-budget.ts +208 -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,11 @@ 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';
|
|
27
|
+
import { CacheKeepalive, type CacheKeepaliveConfig } from '../cache-keepalive.js';
|
|
25
28
|
|
|
26
29
|
// ============================================================================
|
|
27
30
|
// Model capability gates
|
|
@@ -143,9 +146,30 @@ export interface AnthropicAdapterConfig {
|
|
|
143
146
|
|
|
144
147
|
/** Default headers to include with Anthropic requests */
|
|
145
148
|
defaultHeaders?: ClientOptions['defaultHeaders'];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Live per-request headers, evaluated at request time — for values that
|
|
152
|
+
* change between calls (e.g. household telemetry stamps such as
|
|
153
|
+
* `x-gate-debt-chunks`, read by an inference gateway and stripped there
|
|
154
|
+
* before the vendor ever sees them). Merged over the per-request beta
|
|
155
|
+
* headers for the OUTGOING request only; cache-keepalive replays
|
|
156
|
+
* deliberately resend their recorded headers, so a telemetry stamp is
|
|
157
|
+
* never replayed stale — an unstamped touch is honest, a stale stamp lies.
|
|
158
|
+
* null/undefined/'' values are dropped.
|
|
159
|
+
*/
|
|
160
|
+
dynamicHeaders?: () => Record<string, string | number | null | undefined>;
|
|
146
161
|
|
|
147
162
|
/** Default max tokens */
|
|
148
163
|
defaultMaxTokens?: number;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Prompt-cache keepalive: hold this agent's cached prefix warm across idle
|
|
167
|
+
* gaps by replaying the last request with `max_tokens: 0`, which refreshes
|
|
168
|
+
* the entry's TTL at cache-READ price instead of letting it expire into a
|
|
169
|
+
* 2x cache write on the next wake. See `../cache-keepalive.ts`.
|
|
170
|
+
* Pass `{ enabled: false }` to turn off.
|
|
171
|
+
*/
|
|
172
|
+
cacheKeepalive?: CacheKeepaliveConfig;
|
|
149
173
|
}
|
|
150
174
|
|
|
151
175
|
// ============================================================================
|
|
@@ -154,6 +178,14 @@ export interface AnthropicAdapterConfig {
|
|
|
154
178
|
|
|
155
179
|
export class AnthropicAdapter implements ProviderAdapter {
|
|
156
180
|
readonly name = 'anthropic';
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Verified live 2026-08-25 (claude-haiku-4-5, 4,650-token cached system
|
|
184
|
+
* prompt): call 1 returned input_tokens 8 / cache_creation_input_tokens 4650,
|
|
185
|
+
* call 2 input_tokens 8 / cache_read_input_tokens 4650. `input_tokens` never
|
|
186
|
+
* counts the cached span.
|
|
187
|
+
*/
|
|
188
|
+
readonly usageCacheConvention = 'cache-excluded' as const;
|
|
157
189
|
private client: Anthropic;
|
|
158
190
|
private defaultMaxTokens: number;
|
|
159
191
|
/** Any anthropic-beta value from defaultHeaders (e.g. the oauth beta for
|
|
@@ -161,6 +193,10 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
161
193
|
* the SDK rather than merging, so when we add a per-request beta we must
|
|
162
194
|
* re-carry this one alongside it or auth breaks. */
|
|
163
195
|
private defaultBeta: string | undefined;
|
|
196
|
+
/** Holds idle agents' cached prefixes warm; undefined when disabled. */
|
|
197
|
+
readonly cacheKeepalive: CacheKeepalive | undefined;
|
|
198
|
+
/** Live per-request header source (see AnthropicAdapterConfig.dynamicHeaders). */
|
|
199
|
+
private readonly dynamicHeaders?: () => Record<string, string | number | null | undefined>;
|
|
164
200
|
|
|
165
201
|
constructor(config: AnthropicAdapterConfig = {}) {
|
|
166
202
|
const clientOptions: ClientOptions = {
|
|
@@ -168,6 +204,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
168
204
|
defaultHeaders: config.defaultHeaders,
|
|
169
205
|
};
|
|
170
206
|
this.defaultBeta = extractBetaHeader(config.defaultHeaders);
|
|
207
|
+
this.dynamicHeaders = config.dynamicHeaders;
|
|
171
208
|
|
|
172
209
|
if (config.authToken !== undefined) {
|
|
173
210
|
clientOptions.authToken = config.authToken;
|
|
@@ -178,6 +215,19 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
178
215
|
|
|
179
216
|
this.client = new Anthropic(clientOptions);
|
|
180
217
|
this.defaultMaxTokens = config.defaultMaxTokens ?? 4096;
|
|
218
|
+
|
|
219
|
+
this.cacheKeepalive = config.cacheKeepalive?.enabled === false
|
|
220
|
+
? undefined
|
|
221
|
+
: new CacheKeepalive(
|
|
222
|
+
// Replay path. Deliberately bypasses buildRequest(): the payload is
|
|
223
|
+
// the already-built wire request from a real call, and rebuilding it
|
|
224
|
+
// risks a byte diff that silently converts a 0.1x read into a 2x write.
|
|
225
|
+
async (wire, headers) => await this.client.messages.create(
|
|
226
|
+
wire as unknown as Anthropic.MessageCreateParamsNonStreaming,
|
|
227
|
+
headers ? { headers } : undefined,
|
|
228
|
+
),
|
|
229
|
+
config.cacheKeepalive ?? {},
|
|
230
|
+
);
|
|
181
231
|
}
|
|
182
232
|
|
|
183
233
|
supportsModel(modelId: string): boolean {
|
|
@@ -192,10 +242,15 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
192
242
|
const fullRequest = { ...anthropicRequest, stream: false as const };
|
|
193
243
|
options?.onRequest?.(fullRequest);
|
|
194
244
|
|
|
245
|
+
const headers = this.betaHeaders(request);
|
|
246
|
+
this.cacheKeepalive?.record(
|
|
247
|
+
fullRequest as unknown as Record<string, unknown>, headers, 'complete',
|
|
248
|
+
);
|
|
249
|
+
|
|
195
250
|
try {
|
|
196
251
|
const response = await this.client.messages.create(fullRequest, {
|
|
197
252
|
signal: options?.signal,
|
|
198
|
-
headers: this.
|
|
253
|
+
headers: this.liveHeaders(headers),
|
|
199
254
|
});
|
|
200
255
|
|
|
201
256
|
return this.parseResponse(response, fullRequest);
|
|
@@ -214,6 +269,14 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
214
269
|
const fullRequest = { ...anthropicRequest, stream: true };
|
|
215
270
|
options?.onRequest?.(fullRequest);
|
|
216
271
|
|
|
272
|
+
// Snapshot the primary lane's prefix so it can be held warm across idle
|
|
273
|
+
// gaps. `stream: true` is dropped at replay time (transport, not cache key).
|
|
274
|
+
this.cacheKeepalive?.record(
|
|
275
|
+
fullRequest as unknown as Record<string, unknown>,
|
|
276
|
+
this.betaHeaders(request),
|
|
277
|
+
'stream',
|
|
278
|
+
);
|
|
279
|
+
|
|
217
280
|
// Idle timeout: abort if no SSE event arrives within the deadline.
|
|
218
281
|
// The SDK's timeout only covers the initial HTTP response headers;
|
|
219
282
|
// once streaming starts, a silently dropped connection waits forever.
|
|
@@ -260,7 +323,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
260
323
|
try {
|
|
261
324
|
const stream = await this.client.messages.stream(anthropicRequest, {
|
|
262
325
|
signal: idleAbort.signal,
|
|
263
|
-
headers: this.betaHeaders(request),
|
|
326
|
+
headers: this.liveHeaders(this.betaHeaders(request)),
|
|
264
327
|
});
|
|
265
328
|
|
|
266
329
|
// Accumulate response metadata from SSE events directly, so we can
|
|
@@ -276,6 +339,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
276
339
|
let inferenceGeo: string | undefined;
|
|
277
340
|
let serviceTier: string | undefined;
|
|
278
341
|
let stopReason: string = 'end_turn';
|
|
342
|
+
let sawTerminalEvent = false;
|
|
279
343
|
let stopSequence: string | undefined;
|
|
280
344
|
let stopDetails: unknown;
|
|
281
345
|
|
|
@@ -291,6 +355,43 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
291
355
|
// thinking blocks with no thinking_delta at all (signature only).
|
|
292
356
|
const wrapThinkingTags = options?.wrapThinkingTags === true;
|
|
293
357
|
let thinkingTagOpen = false;
|
|
358
|
+
// Index of a block that has started and not yet been stopped. A stream
|
|
359
|
+
// can end with one still open: measured live against claude-haiku-4-5
|
|
360
|
+
// (2026-08-25), a tool call truncated by max_tokens emits
|
|
361
|
+
// content_block_start + input_json_delta fragments and then goes
|
|
362
|
+
// straight to message_delta — no content_block_stop at all.
|
|
363
|
+
let openBlockIndex = -1;
|
|
364
|
+
|
|
365
|
+
const finalizeContentBlock = (blockIdx: number, sawBlockStop: boolean): void => {
|
|
366
|
+
const block = contentBlocks[blockIdx];
|
|
367
|
+
if (block) {
|
|
368
|
+
if (block.type === 'text') {
|
|
369
|
+
block.text = currentBlockContent;
|
|
370
|
+
} else if (block.type === 'thinking') {
|
|
371
|
+
block.thinking = currentBlockContent;
|
|
372
|
+
if (thinkingTagOpen) {
|
|
373
|
+
callbacks.onChunk('</thinking>\n');
|
|
374
|
+
thinkingTagOpen = false;
|
|
375
|
+
}
|
|
376
|
+
} else if (block.type === 'tool_use') {
|
|
377
|
+
if (!sawBlockStop) {
|
|
378
|
+
// Arguments never finished arriving, so `input` is still the
|
|
379
|
+
// empty object content_block_start carried — a plausible no-arg
|
|
380
|
+
// call that nothing downstream can distinguish from a real one.
|
|
381
|
+
block.unparseableInput = currentBlockInputJson;
|
|
382
|
+
} else if (currentBlockInputJson) {
|
|
383
|
+
try {
|
|
384
|
+
block.input = JSON.parse(currentBlockInputJson);
|
|
385
|
+
} catch {
|
|
386
|
+
// Same fabrication, reached by a block that did stop: keep the
|
|
387
|
+
// raw accumulation and mark it so consumers can refuse.
|
|
388
|
+
block.unparseableInput = currentBlockInputJson;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
callbacks.onContentBlock?.(blockIdx, contentBlocks[blockIdx]);
|
|
394
|
+
};
|
|
294
395
|
|
|
295
396
|
for await (const event of stream) {
|
|
296
397
|
sawEvent = true;
|
|
@@ -316,6 +417,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
316
417
|
|
|
317
418
|
} else if (event.type === 'content_block_start') {
|
|
318
419
|
currentBlockIndex = event.index;
|
|
420
|
+
openBlockIndex = event.index;
|
|
319
421
|
currentBlockContent = '';
|
|
320
422
|
currentBlockInputJson = '';
|
|
321
423
|
contentBlocks[currentBlockIndex] = { ...event.content_block };
|
|
@@ -349,22 +451,8 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
349
451
|
|
|
350
452
|
} else if (event.type === 'content_block_stop') {
|
|
351
453
|
// Finalize block — use event.index for defensive correctness
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (block) {
|
|
355
|
-
if (block.type === 'text') {
|
|
356
|
-
block.text = currentBlockContent;
|
|
357
|
-
} else if (block.type === 'thinking') {
|
|
358
|
-
block.thinking = currentBlockContent;
|
|
359
|
-
if (thinkingTagOpen) {
|
|
360
|
-
callbacks.onChunk('</thinking>\n');
|
|
361
|
-
thinkingTagOpen = false;
|
|
362
|
-
}
|
|
363
|
-
} else if (block.type === 'tool_use' && currentBlockInputJson) {
|
|
364
|
-
try { block.input = JSON.parse(currentBlockInputJson); } catch { /* partial JSON */ }
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
callbacks.onContentBlock?.(blockIdx, contentBlocks[blockIdx]);
|
|
454
|
+
finalizeContentBlock((event as { index: number }).index, true);
|
|
455
|
+
openBlockIndex = -1;
|
|
368
456
|
|
|
369
457
|
} else if (event.type === 'message_delta') {
|
|
370
458
|
// All content blocks are finalized by the time message_delta arrives.
|
|
@@ -376,6 +464,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
376
464
|
stop_details?: unknown;
|
|
377
465
|
};
|
|
378
466
|
stopReason = delta.stop_reason ?? 'end_turn';
|
|
467
|
+
sawTerminalEvent = true;
|
|
379
468
|
stopSequence = delta.stop_sequence ?? undefined;
|
|
380
469
|
// stop_details carries refusal metadata (e.g., category: 'reasoning_extraction')
|
|
381
470
|
stopDetails = delta.stop_details ?? undefined;
|
|
@@ -400,9 +489,22 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
400
489
|
if (idleTimer) clearTimeout(idleTimer);
|
|
401
490
|
options?.signal?.removeEventListener('abort', onExternalAbort);
|
|
402
491
|
|
|
492
|
+
// A block still open here never received its content_block_stop: the
|
|
493
|
+
// turn ended mid-block. Finalize it so the accumulated text is not lost
|
|
494
|
+
// and a truncated tool call is marked rather than persisted as `{}`.
|
|
495
|
+
if (openBlockIndex >= 0) {
|
|
496
|
+
finalizeContentBlock(openBlockIndex, false);
|
|
497
|
+
openBlockIndex = -1;
|
|
498
|
+
}
|
|
499
|
+
|
|
403
500
|
// Force-close the HTTP connection so we don't block on SSE drain
|
|
404
501
|
try { stream.controller.abort(); } catch { /* already closed */ }
|
|
405
502
|
|
|
503
|
+
// message_delta is this adapter's terminal event — the loop breaks on
|
|
504
|
+
// it. Falling out of the for-await without one means the SSE connection
|
|
505
|
+
// closed mid-turn, and stopReason is still its 'end_turn' initialiser.
|
|
506
|
+
assertTerminalEventObserved(sawTerminalEvent, 'Anthropic', fullRequest);
|
|
507
|
+
|
|
406
508
|
return {
|
|
407
509
|
content: contentBlocks,
|
|
408
510
|
stopReason,
|
|
@@ -468,6 +570,19 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
468
570
|
* the SDK replaces same-key defaults instead of merging, and the API
|
|
469
571
|
* accepts comma-separated betas. Undefined when nothing to add, so the
|
|
470
572
|
* defaults apply untouched. */
|
|
573
|
+
/** Base headers + the live dynamicHeaders stamp. Request time only: the
|
|
574
|
+
* keepalive recorder receives the base headers BEFORE this merge, so
|
|
575
|
+
* replayed touches never carry a stale telemetry value. */
|
|
576
|
+
private liveHeaders(base: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
577
|
+
const dyn = this.dynamicHeaders?.();
|
|
578
|
+
if (!dyn) return base;
|
|
579
|
+
const out: Record<string, string> = { ...(base ?? {}) };
|
|
580
|
+
for (const [k, v] of Object.entries(dyn)) {
|
|
581
|
+
if (v !== null && v !== undefined && v !== '') out[k] = String(v);
|
|
582
|
+
}
|
|
583
|
+
return Object.keys(out).length ? out : undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
471
586
|
private betaHeaders(request: ProviderRequest): Record<string, string> | undefined {
|
|
472
587
|
if (!thinkingEnabled(request) || !needsInterleavedThinkingBeta(request.model)) {
|
|
473
588
|
return undefined;
|
|
@@ -634,6 +749,10 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
634
749
|
}
|
|
635
750
|
|
|
636
751
|
private handleError(error: unknown, rawRequest?: unknown): MembraneError {
|
|
752
|
+
// Already-classified failures (e.g. the stream-integrity guards) keep
|
|
753
|
+
// their type and retryability instead of being re-derived from a string.
|
|
754
|
+
if (error instanceof MembraneError) return error;
|
|
755
|
+
|
|
637
756
|
if (error instanceof Anthropic.APIError) {
|
|
638
757
|
// Mid-stream SSE `error` events are rethrown by the SDK as APIError
|
|
639
758
|
// with status === undefined (sdk core/streaming.js), so the HTTP
|
|
@@ -664,7 +783,12 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
664
783
|
return authError(message, error, rawRequest);
|
|
665
784
|
}
|
|
666
785
|
|
|
667
|
-
|
|
786
|
+
// Context-length is a client-side request-shape problem — it only ever
|
|
787
|
+
// arrives as a 400 (invalid_request_error). Without the status guard, a
|
|
788
|
+
// transient 5xx whose body happens to contain "context" or "too long"
|
|
789
|
+
// (e.g. "Internal error: context processing failed") was misclassified
|
|
790
|
+
// as non-retryable context_length, silently suppressing retries.
|
|
791
|
+
if (status === 400 && (message.includes('context') || message.includes('too long'))) {
|
|
668
792
|
return contextLengthError(message, error, rawRequest);
|
|
669
793
|
}
|
|
670
794
|
|
|
@@ -837,6 +961,23 @@ export function toAnthropicContent(blocks: ContentBlock[]): Anthropic.ContentBlo
|
|
|
837
961
|
media_type: block.source.mediaType as 'application/pdf',
|
|
838
962
|
data: block.source.data,
|
|
839
963
|
},
|
|
964
|
+
// Anthropic's document block carries the filename as `title`; it was
|
|
965
|
+
// being dropped, so the model lost the one hint about what the PDF is.
|
|
966
|
+
...(block.filename ? { title: block.filename } : {}),
|
|
967
|
+
});
|
|
968
|
+
break;
|
|
969
|
+
|
|
970
|
+
case 'generated_image':
|
|
971
|
+
// A provider-generated image is a base64 image with a MIME type, which
|
|
972
|
+
// is exactly Anthropic's image block — carrying it across costs nothing
|
|
973
|
+
// and lets an image Gemini produced re-enter Anthropic history.
|
|
974
|
+
result.push({
|
|
975
|
+
type: 'image',
|
|
976
|
+
source: {
|
|
977
|
+
type: 'base64',
|
|
978
|
+
media_type: block.mimeType as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp',
|
|
979
|
+
data: block.data,
|
|
980
|
+
},
|
|
840
981
|
});
|
|
841
982
|
break;
|
|
842
983
|
|
|
@@ -876,12 +1017,53 @@ export function toAnthropicContent(blocks: ContentBlock[]): Anthropic.ContentBlo
|
|
|
876
1017
|
data: (block as any).data,
|
|
877
1018
|
} as any);
|
|
878
1019
|
break;
|
|
1020
|
+
|
|
1021
|
+
default: {
|
|
1022
|
+
// The REQUEST path cannot degrade gracefully: a dropped block reaches
|
|
1023
|
+
// the model as an absence, and it answers about content it was never
|
|
1024
|
+
// shown. `audio` and `video` have no Anthropic Messages representation
|
|
1025
|
+
// at all, and a block type added later would silently join them. Fail
|
|
1026
|
+
// loudly at the boundary instead — the caller can strip or transcode.
|
|
1027
|
+
const unsupportedType = (block as { type?: string }).type ?? 'unknown';
|
|
1028
|
+
throw unsupportedError(
|
|
1029
|
+
`Anthropic has no representation for a "${unsupportedType}" content block, and dropping`
|
|
1030
|
+
+ ' it would send the model a message missing content the caller supplied.'
|
|
1031
|
+
+ ' Remove or convert the block before sending it on this provider.'
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
879
1034
|
}
|
|
880
1035
|
}
|
|
881
1036
|
|
|
882
1037
|
return result;
|
|
883
1038
|
}
|
|
884
1039
|
|
|
1040
|
+
/** Unrecognised response block types warn once each, not once per conversion. */
|
|
1041
|
+
const warnedUnconvertibleResponseBlocks = new Set<string>();
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* The RESPONSE path can degrade gracefully where the request path cannot: the
|
|
1045
|
+
* provider's own block is in hand, so keeping it verbatim on a zero-width
|
|
1046
|
+
* carrier loses nothing recoverable and lets formatters replay it. Warn once
|
|
1047
|
+
* per type so a new provider block type surfaces without flooding the log.
|
|
1048
|
+
*/
|
|
1049
|
+
function preserveUnconvertibleBlock(block: unknown, sourceLabel: string): ContentBlock {
|
|
1050
|
+
const blockType = (block as { type?: string })?.type ?? 'unknown';
|
|
1051
|
+
if (!warnedUnconvertibleResponseBlocks.has(blockType)) {
|
|
1052
|
+
warnedUnconvertibleResponseBlocks.add(blockType);
|
|
1053
|
+
console.warn(
|
|
1054
|
+
`[membrane:${sourceLabel}] no normalized ContentBlock for provider block type`
|
|
1055
|
+
+ ` "${blockType}" — preserving it verbatim as a rawItem carrier so it can be`
|
|
1056
|
+
+ ' replayed, but its content is not visible to normalized consumers.'
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return { type: 'text', text: '', rawItem: block } as ContentBlock;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** Test seam: the once-per-type warn latch is process-wide otherwise. */
|
|
1063
|
+
export function resetUnconvertibleBlockWarnings(): void {
|
|
1064
|
+
warnedUnconvertibleResponseBlocks.clear();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
885
1067
|
/**
|
|
886
1068
|
* Convert Anthropic response content to normalized format
|
|
887
1069
|
*/
|
|
@@ -912,11 +1094,14 @@ export function fromAnthropicContent(blocks: Anthropic.ContentBlock[]): ContentB
|
|
|
912
1094
|
break;
|
|
913
1095
|
|
|
914
1096
|
default:
|
|
915
|
-
// Handle redacted_thinking or unknown types
|
|
916
1097
|
if ((block as any).type === 'redacted_thinking') {
|
|
917
1098
|
// Preserve the encrypted `data` payload — without it the block
|
|
918
1099
|
// cannot be round-tripped and prior reasoning is lost.
|
|
919
1100
|
result.push({ type: 'redacted_thinking', data: (block as any).data } as any);
|
|
1101
|
+
} else {
|
|
1102
|
+
// server_tool_use, web_search_tool_result, search_result, mcp_tool_use
|
|
1103
|
+
// and anything Anthropic adds later used to fall out here silently.
|
|
1104
|
+
result.push(preserveUnconvertibleBlock(block, 'anthropic'));
|
|
920
1105
|
}
|
|
921
1106
|
break;
|
|
922
1107
|
}
|
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;
|