@intx/inference 0.1.2 → 0.2.2

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.
Files changed (97) hide show
  1. package/LICENSE +176 -0
  2. package/dist/actions.d.ts +16 -0
  3. package/dist/actions.js +200 -0
  4. package/dist/adapter.d.ts +38 -0
  5. package/dist/adapter.js +31 -0
  6. package/dist/assembly.d.ts +68 -0
  7. package/dist/assembly.js +132 -0
  8. package/dist/audit-collector.d.ts +10 -0
  9. package/dist/audit-collector.js +139 -0
  10. package/dist/auth.d.ts +24 -0
  11. package/{src/auth.ts → dist/auth.js} +13 -19
  12. package/dist/authz-extension.d.ts +32 -0
  13. package/dist/authz-extension.js +100 -0
  14. package/dist/correlation.d.ts +25 -0
  15. package/dist/correlation.js +32 -0
  16. package/dist/default-director.d.ts +111 -0
  17. package/dist/default-director.js +199 -0
  18. package/dist/director.d.ts +6 -0
  19. package/dist/director.js +56 -0
  20. package/dist/errors.d.ts +18 -0
  21. package/dist/errors.js +83 -0
  22. package/dist/gates.d.ts +27 -0
  23. package/dist/gates.js +80 -0
  24. package/dist/harness.d.ts +147 -0
  25. package/dist/harness.js +1319 -0
  26. package/dist/index.d.ts +37 -0
  27. package/dist/index.js +21 -0
  28. package/dist/manifest.d.ts +31 -0
  29. package/dist/manifest.js +44 -0
  30. package/dist/providers/anthropic.d.ts +33 -0
  31. package/dist/providers/anthropic.js +670 -0
  32. package/dist/providers/google-genai-files.d.ts +48 -0
  33. package/dist/providers/google-genai-files.js +205 -0
  34. package/dist/providers/google-genai.d.ts +3 -0
  35. package/dist/providers/google-genai.js +1196 -0
  36. package/dist/providers/index.d.ts +38 -0
  37. package/dist/providers/index.js +56 -0
  38. package/dist/providers/openai.d.ts +3 -0
  39. package/dist/providers/openai.js +609 -0
  40. package/dist/reactor.d.ts +50 -0
  41. package/dist/reactor.js +920 -0
  42. package/dist/retry-policy.d.ts +31 -0
  43. package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
  44. package/dist/sse.d.ts +1 -0
  45. package/dist/sse.js +63 -0
  46. package/dist/state.d.ts +23 -0
  47. package/dist/state.js +100 -0
  48. package/dist/tool-name.d.ts +6 -0
  49. package/dist/tool-name.js +110 -0
  50. package/dist/transform.d.ts +11 -0
  51. package/dist/transform.js +117 -0
  52. package/dist/transforms/index.d.ts +2 -0
  53. package/dist/transforms/index.js +1 -0
  54. package/dist/transforms/size-cap.d.ts +12 -0
  55. package/dist/transforms/size-cap.js +80 -0
  56. package/dist/turns.d.ts +21 -0
  57. package/dist/turns.js +135 -0
  58. package/package.json +21 -6
  59. package/src/actions.ts +0 -245
  60. package/src/adapter.ts +0 -57
  61. package/src/assembly.test.ts +0 -728
  62. package/src/assembly.ts +0 -250
  63. package/src/audit-collector.test.ts +0 -332
  64. package/src/audit-collector.ts +0 -172
  65. package/src/auth.test.ts +0 -117
  66. package/src/authz-extension.test.ts +0 -269
  67. package/src/authz-extension.ts +0 -145
  68. package/src/correlation.ts +0 -61
  69. package/src/default-director.test.ts +0 -314
  70. package/src/default-director.ts +0 -344
  71. package/src/director.ts +0 -87
  72. package/src/errors.test.ts +0 -133
  73. package/src/errors.ts +0 -115
  74. package/src/gates.ts +0 -128
  75. package/src/harness.test.ts +0 -655
  76. package/src/harness.ts +0 -1571
  77. package/src/index.ts +0 -76
  78. package/src/providers/anthropic.test.ts +0 -771
  79. package/src/providers/anthropic.ts +0 -810
  80. package/src/providers/google-genai-files.ts +0 -289
  81. package/src/providers/google-genai.ts +0 -1518
  82. package/src/providers/openai.ts +0 -719
  83. package/src/providers/registry.ts +0 -33
  84. package/src/reactor.test.ts +0 -3660
  85. package/src/reactor.ts +0 -1058
  86. package/src/scheduler.test.ts +0 -41
  87. package/src/sse.test.ts +0 -133
  88. package/src/sse.ts +0 -76
  89. package/src/state.ts +0 -135
  90. package/src/transform.test.ts +0 -207
  91. package/src/transform.ts +0 -159
  92. package/src/transforms/index.ts +0 -2
  93. package/src/transforms/size-cap.test.ts +0 -172
  94. package/src/transforms/size-cap.ts +0 -110
  95. package/src/turns.ts +0 -54
  96. package/tsconfig.json +0 -4
  97. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,670 @@
1
+ import { type } from "arktype";
2
+ import { CitationBlock as CitationBlockType } from "@intx/types/runtime";
3
+ import { CREDENTIAL_SENTINEL } from "../auth.js";
4
+ import { ProtocolMismatchError } from "../errors.js";
5
+ import { decodeToolName, encodeToolName, } from "../tool-name.js";
6
+ // Anthropic's tool-name constraint is `^[a-zA-Z0-9_-]{1,128}$`, which rejects
7
+ // the raw package-qualified names for the same out-of-charset characters as
8
+ // the OpenAI family.
9
+ const ANTHROPIC_TOOL_NAME_LIMIT = {
10
+ provider: "anthropic",
11
+ maxLength: 128,
12
+ };
13
+ // ---------------------------------------------------------------------------
14
+ // Request building
15
+ // ---------------------------------------------------------------------------
16
+ function buildRequest(messages, model, options) {
17
+ rejectUnsupportedResponseFormat(options.responseFormat);
18
+ const systemMessages = messages.filter((m) => m.role === "system");
19
+ const conversationMessages = messages.filter((m) => m.role !== "system");
20
+ const systemText = systemMessages
21
+ .flatMap((m) => m.content
22
+ .filter((b) => b.type === "text")
23
+ .map((b) => b.text))
24
+ .join("\n\n");
25
+ const effectiveSystem = options.systemPrompt
26
+ ? options.systemPrompt
27
+ : systemText || undefined;
28
+ const body = {
29
+ model,
30
+ max_tokens: options.maxTokens ?? 4096,
31
+ messages: conversationMessages.map((msg, i) => {
32
+ // Place a cache breakpoint on the last user message so all prior
33
+ // turns are cached on the next request.
34
+ const isLastUser = msg.role !== "assistant" &&
35
+ conversationMessages.slice(i + 1).every((m) => m.role === "assistant");
36
+ return toAnthropicMessage(msg, isLastUser);
37
+ }),
38
+ stream: true,
39
+ };
40
+ if (effectiveSystem) {
41
+ body["system"] = [
42
+ {
43
+ type: "text",
44
+ text: effectiveSystem,
45
+ cache_control: { type: "ephemeral" },
46
+ },
47
+ ];
48
+ }
49
+ if (options.thinking?.enabled) {
50
+ body["thinking"] = {
51
+ type: "enabled",
52
+ budget_tokens: options.thinking.budgetTokens ?? 1024,
53
+ };
54
+ }
55
+ if (options.tools !== undefined && options.tools.length > 0) {
56
+ const tools = options.tools.map((t) => ({
57
+ name: encodeToolName(t.name, ANTHROPIC_TOOL_NAME_LIMIT),
58
+ description: t.description,
59
+ input_schema: t.inputSchema,
60
+ }));
61
+ const lastTool = tools[tools.length - 1];
62
+ if (lastTool !== undefined) {
63
+ lastTool["cache_control"] = { type: "ephemeral" };
64
+ }
65
+ body["tools"] = tools;
66
+ }
67
+ if (options.temperature !== undefined) {
68
+ body["temperature"] = options.temperature;
69
+ }
70
+ return {
71
+ url: "/v1/messages",
72
+ headers: {
73
+ "content-type": "application/json",
74
+ "x-api-key": CREDENTIAL_SENTINEL,
75
+ "anthropic-version": "2023-06-01",
76
+ },
77
+ body: JSON.stringify(body),
78
+ };
79
+ }
80
+ // Anthropic's Messages API has no native structured-outputs surface.
81
+ // `text` is the default and a no-op. `json` and `json-schema` raise
82
+ // here at the marshaling boundary rather than silently dropping the
83
+ // field; the codebase prefers loud failure at the wire boundary over
84
+ // a forward-synthesis shim (a hidden tool whose input_schema mirrors
85
+ // the requested schema) because no other adapter shim synthesizes
86
+ // requests the caller didn't author.
87
+ function rejectUnsupportedResponseFormat(format) {
88
+ if (format === undefined)
89
+ return;
90
+ if (format.kind === "text")
91
+ return;
92
+ throw new Error(`Anthropic adapter does not support structured outputs ` +
93
+ `(responseFormat.kind="${format.kind}").`);
94
+ }
95
+ function toAnthropicMessage(msg, cacheLastBlock) {
96
+ const role = msg.role === "assistant" ? "assistant" : "user";
97
+ const content = msg.content.map(toAnthropicBlock);
98
+ if (cacheLastBlock) {
99
+ const lastBlock = content[content.length - 1];
100
+ if (lastBlock !== undefined) {
101
+ lastBlock["cache_control"] = { type: "ephemeral" };
102
+ }
103
+ }
104
+ return { role, content };
105
+ }
106
+ // Marshal a MediaSource into Anthropic's nested `source` shape.
107
+ // Base64 sources carry the mimeType on the wire as `media_type`.
108
+ // File-reference and URL sources carry no mimeType: Anthropic
109
+ // identifies file-reference content by id alone (encoded server-side
110
+ // at upload time) and infers URL-sourced content from the response of
111
+ // the fetch it performs. The MediaSource's mimeType is intentionally
112
+ // dropped at this layer for both. The internal `mimeType` requirement
113
+ // on the non-base64 variants keeps callers honest about what they
114
+ // have in hand even when the provider doesn't need it.
115
+ function toAnthropicMediaSource(source) {
116
+ if (source.kind === "base64") {
117
+ return {
118
+ type: "base64",
119
+ media_type: source.mimeType,
120
+ data: source.data,
121
+ };
122
+ }
123
+ if (source.kind === "file-reference") {
124
+ return {
125
+ type: "file",
126
+ file_id: source.reference,
127
+ };
128
+ }
129
+ if (source.kind === "url") {
130
+ return {
131
+ type: "url",
132
+ url: source.url,
133
+ };
134
+ }
135
+ // Exhaustiveness: a new MediaSource variant added without a case
136
+ // here fails this compile-time check.
137
+ source;
138
+ throw new Error(`unreachable: unknown MediaSource kind`);
139
+ }
140
+ // Map an Anthropic-streamed citation onto the internal CitationBlock.
141
+ // `textOffset` is intentionally not populated for any variant:
142
+ // Anthropic's offsets are document-relative (page numbers, doc char
143
+ // offsets, doc block indices) rather than text-relative — they don't
144
+ // correspond to UTF-16 positions in the preceding TextBlock that
145
+ // `CitationBlock.textOffset` describes. Computing text-relative
146
+ // offsets from `cited_text` substring search produces wrong answers
147
+ // whenever `cited_text` is paraphrased, appears multiple times, or
148
+ // spans wire-chunk boundaries; better to leave the field unset than
149
+ // guess.
150
+ //
151
+ // `encrypted_index` (web_search_result_location) has no echo-back
152
+ // target in CitationBlock today and is intentionally dropped at this
153
+ // layer. When echo-back of citation context lands, this is the layer
154
+ // to preserve it from.
155
+ function toCitationBlock(wire, index) {
156
+ if (wire.cited_text === undefined) {
157
+ throw new ProtocolMismatchError(`anthropic parseResponse: citation at block ${index} missing required \`cited_text\``, wire);
158
+ }
159
+ const citedText = wire.cited_text;
160
+ const source = {};
161
+ if (wire.title !== undefined)
162
+ source.title = wire.title;
163
+ if (wire.url !== undefined)
164
+ source.uri = wire.url;
165
+ if (wire.document_title !== undefined && source.title === undefined) {
166
+ source.title = wire.document_title;
167
+ }
168
+ if (wire.document_index !== undefined) {
169
+ source.documentRef = { index: wire.document_index };
170
+ }
171
+ switch (wire.type) {
172
+ case "web_search_result_location":
173
+ return { type: "citation", citedText, source };
174
+ case "page_location": {
175
+ // Anthropic page numbers are 1-indexed and inclusive on both
176
+ // ends per the documented PDF citation shape.
177
+ const start = wire.start_page_number;
178
+ const end = wire.end_page_number;
179
+ if (start === undefined || end === undefined) {
180
+ throw new ProtocolMismatchError(`anthropic parseResponse: page_location citation at block ${index} missing start_page_number or end_page_number`, wire);
181
+ }
182
+ return {
183
+ type: "citation",
184
+ citedText,
185
+ source,
186
+ location: { kind: "page", start, end },
187
+ };
188
+ }
189
+ case "char_location": {
190
+ const start = wire.start_char_index;
191
+ const end = wire.end_char_index;
192
+ if (start === undefined || end === undefined) {
193
+ throw new ProtocolMismatchError(`anthropic parseResponse: char_location citation at block ${index} missing start_char_index or end_char_index`, wire);
194
+ }
195
+ return {
196
+ type: "citation",
197
+ citedText,
198
+ source,
199
+ location: { kind: "char", start, end },
200
+ };
201
+ }
202
+ case "content_block_location": {
203
+ const start = wire.start_block_index;
204
+ const end = wire.end_block_index;
205
+ if (start === undefined || end === undefined) {
206
+ throw new ProtocolMismatchError(`anthropic parseResponse: content_block_location citation at block ${index} missing start_block_index or end_block_index`, wire);
207
+ }
208
+ return {
209
+ type: "citation",
210
+ citedText,
211
+ source,
212
+ location: { kind: "content-block", start, end },
213
+ };
214
+ }
215
+ default:
216
+ throw new ProtocolMismatchError(`anthropic parseResponse: unrecognized citation variant "${wire.type}" at block ${index}`, wire);
217
+ }
218
+ }
219
+ function toAnthropicBlock(block) {
220
+ switch (block.type) {
221
+ case "text":
222
+ return { type: "text", text: block.text };
223
+ case "thinking":
224
+ return {
225
+ type: "thinking",
226
+ thinking: block.thinking,
227
+ ...(block.signature !== undefined
228
+ ? { signature: block.signature }
229
+ : {}),
230
+ };
231
+ case "redacted_thinking":
232
+ // The opaque `data` blob must echo back verbatim on every
233
+ // follow-up turn that includes this block as context. Any
234
+ // mutation (truncation, base64-decoding-and-reencoding,
235
+ // whitespace normalization) produces a 400 from Anthropic with
236
+ // "messages.N.content.M.redacted_thinking: Field required" or
237
+ // a context-corruption error on subsequent turns. The
238
+ // RedactedThinkingBlock type carries it as `string` (opaque
239
+ // base64); pass through untouched.
240
+ return { type: "redacted_thinking", data: block.data };
241
+ case "image":
242
+ return { type: "image", source: toAnthropicMediaSource(block.source) };
243
+ case "document":
244
+ return { type: "document", source: toAnthropicMediaSource(block.source) };
245
+ case "audio":
246
+ case "video":
247
+ throw new Error(`Anthropic adapter does not yet handle ${block.type} content blocks.`);
248
+ case "citation":
249
+ throw new Error("Anthropic adapter does not yet emit citation content blocks.");
250
+ case "code_execution_request":
251
+ case "code_execution_result":
252
+ throw new Error(`Anthropic adapter does not yet emit ${block.type} content blocks.`);
253
+ case "refusal":
254
+ // Refusal blocks are an OpenAI strict-mode output shape. Echoing
255
+ // one back into an Anthropic request has no defined wire shape;
256
+ // surface the mismatch at the marshaling site rather than fall
257
+ // through to a silent drop.
258
+ throw new Error("Anthropic adapter does not handle refusal content blocks; " +
259
+ "they are emitted by OpenAI strict-mode structured outputs.");
260
+ case "tool_call":
261
+ return {
262
+ type: "tool_use",
263
+ id: block.id,
264
+ name: encodeToolName(block.name, ANTHROPIC_TOOL_NAME_LIMIT),
265
+ input: block.arguments,
266
+ };
267
+ case "tool_result":
268
+ return {
269
+ type: "tool_result",
270
+ tool_use_id: block.callId,
271
+ content: block.content.map((c) => {
272
+ if (c.type === "text") {
273
+ return { type: "text", text: c.text };
274
+ }
275
+ if (c.type === "image") {
276
+ return {
277
+ type: "image",
278
+ source: toAnthropicMediaSource(c.source),
279
+ };
280
+ }
281
+ // Anthropic's tool_result.content accepts only `text` and
282
+ // `image` blocks today. `document` in particular is rejected
283
+ // at the API edge; surface the failure at the marshaling
284
+ // site with the specific block type so the failure shows
285
+ // where the wrong block type was authored, not as an opaque
286
+ // HTTP 400 a round-trip later. The ContentBlock union allows
287
+ // these so the type system can grow uniformly; the wire
288
+ // surface lags.
289
+ throw new Error(`Anthropic adapter does not handle ${c.type} content blocks ` +
290
+ `inside tool_result.content; the API accepts only text and ` +
291
+ `image here.`);
292
+ }),
293
+ ...(block.isError ? { is_error: true } : {}),
294
+ };
295
+ }
296
+ }
297
+ // ---------------------------------------------------------------------------
298
+ // Response parsing
299
+ //
300
+ // The harness passes one SSE data payload per call. The parser accumulates
301
+ // no state — all partial state lives in the harness. The parser emits events
302
+ // for the fragments it sees; the harness updates the PartialMessage and
303
+ // injects it into the returned events.
304
+ //
305
+ // Because the harness owns partial state, the parser cannot construct the
306
+ // correct `partial` field. We emit raw delta events with a placeholder empty
307
+ // partial — the harness will replace it before forwarding. This is the
308
+ // design: adapters are pure translators, the harness owns all state.
309
+ // ---------------------------------------------------------------------------
310
+ const EMPTY_PARTIAL = { text: "" };
311
+ // Anthropic's SSE protocol guarantees `index` on every content_block_*
312
+ // event. Parsing it as required (not optional) means the type system
313
+ // carries the guarantee through to every emission site below — no
314
+ // defensive `?? 0` fallback that would silently route real protocol
315
+ // violations to block 0 and corrupt the `blockIndexToCallId` cache the
316
+ // parser uses to resolve input_json_delta lookups across multiple
317
+ // tool_use blocks at distinct indices. A malformed upstream missing
318
+ // `index` surfaces as a ProtocolMismatchError via the schema-validation
319
+ // throw site, with the offending payload preserved in `error.raw` for
320
+ // inspection.
321
+ // Anthropic's wire shape for a single citation, streamed inside a
322
+ // `citations_delta`. The `type` discriminator selects the location
323
+ // model:
324
+ // - web_search_result_location: URL + title, no document offsets
325
+ // - page_location: 1-indexed page numbers (inclusive start/end)
326
+ // - char_location: 0-indexed character offsets into the document
327
+ // - content_block_location: index into the document's content blocks
328
+ // Fields not relevant to a given variant are absent; the union is
329
+ // flat at the wire level. `encrypted_index` (web_search) is recorded
330
+ // only on the wire — it has no echo-back target in the internal
331
+ // CitationBlock today, so the adapter drops it.
332
+ const AnthropicCitation = type({
333
+ type: "string",
334
+ "cited_text?": "string",
335
+ "url?": "string",
336
+ "title?": "string",
337
+ "encrypted_index?": "string",
338
+ "document_index?": "number",
339
+ "document_title?": "string",
340
+ "start_page_number?": "number",
341
+ "end_page_number?": "number",
342
+ "start_char_index?": "number",
343
+ "end_char_index?": "number",
344
+ "start_block_index?": "number",
345
+ "end_block_index?": "number",
346
+ });
347
+ const ContentBlockDelta = type({
348
+ type: "'content_block_delta'",
349
+ index: "number",
350
+ delta: {
351
+ type: "string",
352
+ "text?": "string",
353
+ "thinking?": "string",
354
+ "partial_json?": "string",
355
+ "signature?": "string",
356
+ "citation?": AnthropicCitation,
357
+ },
358
+ });
359
+ const ContentBlockStart = type({
360
+ type: "'content_block_start'",
361
+ index: "number",
362
+ // Anthropic sends either content_block (snake_case) or contentBlock
363
+ // (camelCase). `data` is optional on the shared shape because only
364
+ // redacted_thinking blocks carry it; the redacted_thinking branch in
365
+ // the parser asserts presence and throws ProtocolMismatchError when
366
+ // it is missing, rather than synthesizing an empty string that would
367
+ // round-trip back to Anthropic as a corrupted block.
368
+ "content_block?": {
369
+ type: "string",
370
+ "id?": "string",
371
+ "name?": "string",
372
+ "data?": "string",
373
+ },
374
+ "contentBlock?": {
375
+ type: "string",
376
+ "id?": "string",
377
+ "name?": "string",
378
+ "data?": "string",
379
+ },
380
+ });
381
+ const ContentBlockStop = type({
382
+ type: "'content_block_stop'",
383
+ index: "number",
384
+ });
385
+ const MessageDelta = type({
386
+ type: "'message_delta'",
387
+ "usage?": { "output_tokens?": "number" },
388
+ });
389
+ const MessageStart = type({
390
+ type: "'message_start'",
391
+ "message?": {
392
+ "usage?": {
393
+ "input_tokens?": "number",
394
+ "output_tokens?": "number",
395
+ "cache_read_input_tokens?": "number",
396
+ "cache_creation_input_tokens?": "number",
397
+ },
398
+ },
399
+ });
400
+ const MessageStop = type({ type: "'message_stop'" });
401
+ const Ping = type({ type: "'ping'" });
402
+ const AnthropicSSEEvent = ContentBlockDelta.or(ContentBlockStart)
403
+ .or(ContentBlockStop)
404
+ .or(MessageDelta)
405
+ .or(MessageStart)
406
+ .or(MessageStop)
407
+ .or(Ping);
408
+ function parseResponse(sseData, blockIndexToCallId, source) {
409
+ // Same protocol-mismatch posture as the openai adapter: a JSON parse
410
+ // failure or arktype rejection means the upstream emitted bytes that
411
+ // violate the Anthropic streaming protocol. Surface through
412
+ // ProtocolMismatchError so the harness's stream-error catch emits
413
+ // an inference.error with category "protocol_mismatch" carrying the
414
+ // offending data in error.raw, rather than dropping the chunk
415
+ // silently.
416
+ let parsed;
417
+ try {
418
+ parsed = JSON.parse(sseData);
419
+ }
420
+ catch (cause) {
421
+ const message = cause instanceof Error ? cause.message : String(cause);
422
+ throw new ProtocolMismatchError(`anthropic parseResponse: malformed JSON in SSE data payload: ${message}`, sseData);
423
+ }
424
+ const event = AnthropicSSEEvent(parsed);
425
+ if (event instanceof type.errors) {
426
+ throw new ProtocolMismatchError(`anthropic parseResponse: SSE event failed schema validation: ${event.summary}`, parsed);
427
+ }
428
+ // The seq field is a placeholder 0 — the harness assigns real sequence numbers.
429
+ const seq = 0;
430
+ switch (event.type) {
431
+ case "content_block_delta": {
432
+ const { delta, index } = event;
433
+ if (delta.type === "text_delta") {
434
+ const token = delta.text ?? "";
435
+ return [
436
+ {
437
+ type: "inference.text.delta",
438
+ seq,
439
+ data: { token, partial: EMPTY_PARTIAL, index },
440
+ },
441
+ ];
442
+ }
443
+ if (delta.type === "thinking_delta") {
444
+ const token = delta.thinking ?? "";
445
+ return [
446
+ {
447
+ type: "inference.thinking.delta",
448
+ seq,
449
+ data: { token, partial: EMPTY_PARTIAL, index },
450
+ },
451
+ ];
452
+ }
453
+ if (delta.type === "signature_delta") {
454
+ // Anthropic emits the cryptographic signature for a thinking block
455
+ // in a dedicated signature_delta event after the block's
456
+ // thinking_delta stream. The signature must be echoed back on any
457
+ // follow-up turn that includes the thinking block as context —
458
+ // otherwise the API rejects the request with
459
+ // "messages.N.content.M.thinking.signature: Field required".
460
+ const signature = delta.signature ?? "";
461
+ return [
462
+ {
463
+ type: "inference.thinking.signature",
464
+ seq,
465
+ data: { signature, index },
466
+ },
467
+ ];
468
+ }
469
+ if (delta.type === "input_json_delta") {
470
+ const callId = blockIndexToCallId.get(index);
471
+ if (callId === undefined) {
472
+ throw new ProtocolMismatchError(`anthropic parseResponse: input_json_delta for content block ${index} with no preceding tool_use start`, event);
473
+ }
474
+ const fragment = delta.partial_json ?? "";
475
+ return [
476
+ {
477
+ type: "inference.tool_call.delta",
478
+ seq,
479
+ data: {
480
+ callId,
481
+ argumentFragment: fragment,
482
+ partial: EMPTY_PARTIAL,
483
+ index,
484
+ },
485
+ },
486
+ ];
487
+ }
488
+ if (delta.type === "citations_delta") {
489
+ const wireCitation = delta.citation;
490
+ if (wireCitation === undefined) {
491
+ throw new ProtocolMismatchError(`anthropic parseResponse: citations_delta missing citation payload at block ${index}`, event);
492
+ }
493
+ const citation = toCitationBlock(wireCitation, index);
494
+ return [
495
+ {
496
+ type: "inference.citation",
497
+ seq,
498
+ data: { citation, index },
499
+ },
500
+ ];
501
+ }
502
+ return [];
503
+ }
504
+ case "content_block_start": {
505
+ const block = event.content_block ?? event.contentBlock;
506
+ if (block === undefined)
507
+ return [];
508
+ if (block.type === "tool_use") {
509
+ const { index } = event;
510
+ const callId = block.id ?? String(index);
511
+ blockIndexToCallId.set(index, callId);
512
+ const name = decodeToolName(block.name ?? "");
513
+ return [
514
+ {
515
+ type: "inference.tool_call.start",
516
+ seq,
517
+ data: { callId, name, partial: EMPTY_PARTIAL, index },
518
+ },
519
+ ];
520
+ }
521
+ if (block.type === "thinking") {
522
+ // Anchor the thinking block in the harness's per-index map
523
+ // via an empty thinking.delta. Anthropic can stream a
524
+ // signature_delta for a thinking block whose visible text is
525
+ // empty (redacted-adjacent flow); without this anchor, the
526
+ // signature would arrive at the harness with no preceding
527
+ // thinking entry at the same index and the per-index router
528
+ // would (correctly) reject it as a protocol violation. The
529
+ // empty-token delta is the parser-side analogue of the wire's
530
+ // `content_block_start` for thinking — it carries no visible
531
+ // content but reserves the index.
532
+ const { index } = event;
533
+ return [
534
+ {
535
+ type: "inference.thinking.delta",
536
+ seq,
537
+ data: { token: "", partial: EMPTY_PARTIAL, index },
538
+ },
539
+ ];
540
+ }
541
+ if (block.type === "redacted_thinking") {
542
+ // Anthropic delivers redacted_thinking as a one-shot inside
543
+ // content_block_start (no delta stream). The opaque `data`
544
+ // blob must echo back verbatim on every follow-up turn —
545
+ // mutating or synthesizing it corrupts the conversation
546
+ // context. A start event missing `data` is a protocol
547
+ // violation, not a default-to-empty case.
548
+ const { index } = event;
549
+ if (block.data === undefined) {
550
+ throw new ProtocolMismatchError(`anthropic parseResponse: content_block_start of type redacted_thinking ` +
551
+ `at index ${String(index)} missing required \`data\` field`, event);
552
+ }
553
+ return [
554
+ {
555
+ type: "inference.thinking.redacted",
556
+ seq,
557
+ data: {
558
+ redactedThinking: {
559
+ type: "redacted_thinking",
560
+ data: block.data,
561
+ },
562
+ index,
563
+ },
564
+ },
565
+ ];
566
+ }
567
+ // Non-tool_use, non-redacted_thinking content_block_start events
568
+ // (text, thinking) emit nothing here by design: each
569
+ // content_block_delta arrives with a typed delta (text_delta,
570
+ // thinking_delta, signature_delta) that the switch above
571
+ // discriminates on directly, so an upfront start emission would
572
+ // be redundant. Tool calls are the exception because their
573
+ // callId arrives only in the start event and must be cached
574
+ // against the block index for subsequent input_json_delta
575
+ // lookups; redacted_thinking is the exception because the block
576
+ // is delivered start-only with no follow-on deltas.
577
+ return [];
578
+ }
579
+ case "content_block_stop": {
580
+ // The harness handles finalizing tool calls when it sees this — we
581
+ // emit nothing here; the harness knows which blocks are complete.
582
+ return [];
583
+ }
584
+ case "message_delta": {
585
+ const outputTokens = event.usage?.output_tokens ?? 0;
586
+ const inferenceUsage = {
587
+ input: 0,
588
+ output: outputTokens,
589
+ cacheRead: 0,
590
+ cacheWrite: 0,
591
+ thinking: 0,
592
+ };
593
+ return [
594
+ {
595
+ type: "inference.usage",
596
+ seq,
597
+ data: { usage: inferenceUsage, source },
598
+ },
599
+ ];
600
+ }
601
+ case "message_start": {
602
+ blockIndexToCallId.clear();
603
+ const msgUsage = event.message?.usage;
604
+ if (msgUsage === undefined)
605
+ return [];
606
+ const inferenceUsage = {
607
+ input: msgUsage.input_tokens ?? 0,
608
+ output: msgUsage.output_tokens ?? 0,
609
+ cacheRead: msgUsage.cache_read_input_tokens ?? 0,
610
+ cacheWrite: msgUsage.cache_creation_input_tokens ?? 0,
611
+ thinking: 0,
612
+ };
613
+ return [
614
+ {
615
+ type: "inference.usage",
616
+ seq,
617
+ data: { usage: inferenceUsage, source },
618
+ },
619
+ ];
620
+ }
621
+ case "message_stop":
622
+ case "ping":
623
+ return [];
624
+ }
625
+ }
626
+ function extractRetryAfterMs(headers) {
627
+ const raw = headers.get("retry-after");
628
+ if (raw === null)
629
+ return undefined;
630
+ const seconds = Number(raw);
631
+ if (!Number.isFinite(seconds) || seconds <= 0)
632
+ return undefined;
633
+ return Math.ceil(seconds * 1000);
634
+ }
635
+ function extractPacingDelayMs(headers) {
636
+ // Check all rate limit dimensions and return the longest wait needed
637
+ const delays = [];
638
+ for (const prefix of [
639
+ "anthropic-ratelimit-requests",
640
+ "anthropic-ratelimit-input-tokens",
641
+ "anthropic-ratelimit-output-tokens",
642
+ "anthropic-ratelimit-tokens",
643
+ ]) {
644
+ const remaining = headers.get(`${prefix}-remaining`);
645
+ if (remaining === null)
646
+ continue;
647
+ const n = Number(remaining);
648
+ if (!Number.isFinite(n) || n > 0)
649
+ continue;
650
+ const reset = headers.get(`${prefix}-reset`);
651
+ if (reset === null)
652
+ continue;
653
+ const resetTime = Date.parse(reset);
654
+ if (Number.isNaN(resetTime))
655
+ continue;
656
+ const delayMs = resetTime - Date.now();
657
+ if (delayMs > 0)
658
+ delays.push(delayMs);
659
+ }
660
+ return delays.length > 0 ? Math.max(...delays) : undefined;
661
+ }
662
+ export function createAnthropicAdapter(source) {
663
+ const blockIndexToCallId = new Map();
664
+ return {
665
+ buildRequest,
666
+ parseResponse: (sseData) => parseResponse(sseData, blockIndexToCallId, source),
667
+ extractRetryAfterMs,
668
+ extractPacingDelayMs,
669
+ };
670
+ }