@intx/inference 0.1.2 → 0.3.0

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