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