@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.
- package/LICENSE +176 -0
- package/dist/actions.d.ts +16 -0
- package/dist/actions.js +200 -0
- package/dist/adapter.d.ts +40 -0
- package/dist/adapter.js +31 -0
- package/dist/assembly.d.ts +75 -0
- package/dist/assembly.js +133 -0
- package/dist/audit-collector.d.ts +10 -0
- package/dist/audit-collector.js +139 -0
- package/dist/auth.d.ts +24 -0
- package/{src/auth.ts → dist/auth.js} +13 -19
- package/dist/authz-extension.d.ts +46 -0
- package/dist/authz-extension.js +184 -0
- package/dist/correlation.d.ts +26 -0
- package/dist/correlation.js +39 -0
- package/dist/default-director.d.ts +111 -0
- package/dist/default-director.js +228 -0
- package/dist/director.d.ts +6 -0
- package/dist/director.js +56 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +83 -0
- package/dist/gates.d.ts +28 -0
- package/dist/gates.js +103 -0
- package/dist/harness.d.ts +147 -0
- package/dist/harness.js +1407 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +44 -0
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.js +917 -0
- package/dist/providers/google-genai-files.d.ts +48 -0
- package/dist/providers/google-genai-files.js +205 -0
- package/dist/providers/google-genai.d.ts +5 -0
- package/dist/providers/google-genai.js +1205 -0
- package/dist/providers/index.d.ts +38 -0
- package/dist/providers/index.js +56 -0
- package/dist/providers/openai.d.ts +9 -0
- package/dist/providers/openai.js +903 -0
- package/dist/reactor.d.ts +50 -0
- package/dist/reactor.js +1233 -0
- package/dist/retry-policy.d.ts +31 -0
- package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
- package/dist/sse.d.ts +1 -0
- package/dist/sse.js +63 -0
- package/dist/state.d.ts +23 -0
- package/dist/state.js +100 -0
- package/dist/tool-name.d.ts +6 -0
- package/dist/tool-name.js +110 -0
- package/dist/transform.d.ts +11 -0
- package/dist/transform.js +132 -0
- package/dist/transforms/index.d.ts +2 -0
- package/dist/transforms/index.js +1 -0
- package/dist/transforms/size-cap.d.ts +12 -0
- package/dist/transforms/size-cap.js +80 -0
- package/dist/turns.d.ts +21 -0
- package/dist/turns.js +135 -0
- package/package.json +22 -6
- package/src/actions.ts +0 -245
- package/src/adapter.ts +0 -57
- package/src/assembly.test.ts +0 -728
- package/src/assembly.ts +0 -250
- package/src/audit-collector.test.ts +0 -332
- package/src/audit-collector.ts +0 -172
- package/src/auth.test.ts +0 -117
- package/src/authz-extension.test.ts +0 -269
- package/src/authz-extension.ts +0 -145
- package/src/correlation.ts +0 -61
- package/src/default-director.test.ts +0 -314
- package/src/default-director.ts +0 -344
- package/src/director.ts +0 -87
- package/src/errors.test.ts +0 -133
- package/src/errors.ts +0 -115
- package/src/gates.ts +0 -128
- package/src/harness.test.ts +0 -655
- package/src/harness.ts +0 -1571
- package/src/index.ts +0 -76
- package/src/providers/anthropic.test.ts +0 -771
- package/src/providers/anthropic.ts +0 -810
- package/src/providers/google-genai-files.ts +0 -289
- package/src/providers/google-genai.ts +0 -1518
- package/src/providers/openai.ts +0 -719
- package/src/providers/registry.ts +0 -33
- package/src/reactor.test.ts +0 -3660
- package/src/reactor.ts +0 -1058
- package/src/scheduler.test.ts +0 -41
- package/src/sse.test.ts +0 -133
- package/src/sse.ts +0 -76
- package/src/state.ts +0 -135
- package/src/transform.test.ts +0 -207
- package/src/transform.ts +0 -159
- package/src/transforms/index.ts +0 -2
- package/src/transforms/size-cap.test.ts +0 -172
- package/src/transforms/size-cap.ts +0 -110
- package/src/turns.ts +0 -54
- package/tsconfig.json +0 -4
- 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
|
-
}
|