@intx/inference-discovery-anthropic 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/package.json +16 -0
- package/src/auth.ts +17 -0
- package/src/endpoint.ts +19 -0
- package/src/index.ts +308 -0
- package/src/media.ts +35 -0
- package/src/plugin.test.ts +681 -0
- package/src/reasoning.ts +64 -0
- package/src/request-body.ts +637 -0
- package/src/sse.fixture-contract.test.ts +51 -0
- package/src/sse.test.ts +188 -0
- package/src/sse.ts +273 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/reasoning.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface AnthropicReasoningTrace {
|
|
2
|
+
blockType: "thinking" | "redacted_thinking";
|
|
3
|
+
fieldPath: string;
|
|
4
|
+
// For "thinking" blocks: the first slice of the surfaced text. For
|
|
5
|
+
// "redacted_thinking" blocks: a short prefix of the encrypted data
|
|
6
|
+
// field, just enough to confirm the round-trip is exercising real
|
|
7
|
+
// bytes.
|
|
8
|
+
sample: string;
|
|
9
|
+
// Present on either block type when the response carries it.
|
|
10
|
+
signature?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SAMPLE_PREFIX_LENGTH = 80;
|
|
14
|
+
|
|
15
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function asString(value: unknown): string | null {
|
|
20
|
+
return typeof value === "string" ? value : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Surfaces the first thinking-class block from an Anthropic /v1/messages
|
|
24
|
+
// response so the observed-vs-documented notes in the discovery doc can
|
|
25
|
+
// pin the exact wire shape Anthropic returned. We capture the block
|
|
26
|
+
// type, the field path, a short sample of the contents, and the
|
|
27
|
+
// signature when present.
|
|
28
|
+
export function extractReasoningTrace(
|
|
29
|
+
parsed: unknown,
|
|
30
|
+
): AnthropicReasoningTrace | null {
|
|
31
|
+
if (!isRecord(parsed)) return null;
|
|
32
|
+
const content = parsed.content;
|
|
33
|
+
if (!Array.isArray(content)) return null;
|
|
34
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
35
|
+
const block = content[i];
|
|
36
|
+
if (!isRecord(block)) continue;
|
|
37
|
+
const type = block.type;
|
|
38
|
+
if (type === "thinking") {
|
|
39
|
+
const text = asString(block.thinking);
|
|
40
|
+
if (text === null) continue;
|
|
41
|
+
const trace: AnthropicReasoningTrace = {
|
|
42
|
+
blockType: "thinking",
|
|
43
|
+
fieldPath: `content[${String(i)}].thinking`,
|
|
44
|
+
sample: text.slice(0, SAMPLE_PREFIX_LENGTH),
|
|
45
|
+
};
|
|
46
|
+
const signature = asString(block.signature);
|
|
47
|
+
if (signature !== null) trace.signature = signature;
|
|
48
|
+
return trace;
|
|
49
|
+
}
|
|
50
|
+
if (type === "redacted_thinking") {
|
|
51
|
+
const data = asString(block.data);
|
|
52
|
+
if (data === null) continue;
|
|
53
|
+
const trace: AnthropicReasoningTrace = {
|
|
54
|
+
blockType: "redacted_thinking",
|
|
55
|
+
fieldPath: `content[${String(i)}].data`,
|
|
56
|
+
sample: data.slice(0, SAMPLE_PREFIX_LENGTH),
|
|
57
|
+
};
|
|
58
|
+
const signature = asString(block.signature);
|
|
59
|
+
if (signature !== null) trace.signature = signature;
|
|
60
|
+
return trace;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
resolveMediaPath,
|
|
4
|
+
type Capability,
|
|
5
|
+
type CapabilityIntent,
|
|
6
|
+
type MediaRef,
|
|
7
|
+
type ToolDecl,
|
|
8
|
+
} from "@intx/inference-discovery/catalog";
|
|
9
|
+
import { mediaTypeFor } from "./media";
|
|
10
|
+
|
|
11
|
+
const PLAIN_MAX_TOKENS = 512;
|
|
12
|
+
const THINKING_BUDGET_TOKENS = 1024;
|
|
13
|
+
const THINKING_MAX_TOKENS = THINKING_BUDGET_TOKENS + 1024;
|
|
14
|
+
|
|
15
|
+
// Tool versions for Anthropic's server-side tools. These are wire-shape
|
|
16
|
+
// markers — bumping them is a deliberate decision that should be
|
|
17
|
+
// reflected in regenerated fixtures.
|
|
18
|
+
const CODE_EXECUTION_TOOL_TYPE = "code_execution_20250522";
|
|
19
|
+
const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
|
|
20
|
+
|
|
21
|
+
export interface AnthropicTextBlock {
|
|
22
|
+
type: "text";
|
|
23
|
+
text: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AnthropicImageBlock {
|
|
27
|
+
type: "image";
|
|
28
|
+
source: {
|
|
29
|
+
type: "base64";
|
|
30
|
+
media_type: string;
|
|
31
|
+
data: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AnthropicDocumentBlock {
|
|
36
|
+
type: "document";
|
|
37
|
+
source:
|
|
38
|
+
| { type: "base64"; media_type: "application/pdf"; data: string }
|
|
39
|
+
| { type: "file"; file_id: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AnthropicToolUseBlock {
|
|
43
|
+
type: "tool_use";
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
input: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AnthropicToolResultBlock {
|
|
50
|
+
type: "tool_result";
|
|
51
|
+
tool_use_id: string;
|
|
52
|
+
content: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AnthropicThinkingBlock {
|
|
56
|
+
type: "thinking";
|
|
57
|
+
thinking: string;
|
|
58
|
+
signature?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AnthropicRedactedThinkingBlock {
|
|
62
|
+
type: "redacted_thinking";
|
|
63
|
+
data: string;
|
|
64
|
+
signature?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Blocks this plug-in constructs for outgoing request bodies. Also the
|
|
68
|
+
// type assistant-echo paths produce: blocks Anthropic returns that
|
|
69
|
+
// aren't enumerated here (server_tool_use, web_search_tool_result,
|
|
70
|
+
// code_execution_tool_use, citation blocks inside text, …) are
|
|
71
|
+
// forwarded verbatim because the wire round-trip is what matters; the
|
|
72
|
+
// runtime values just don't match the static union. The single
|
|
73
|
+
// quarantined cast lives in extractAssistantContentBlocks.
|
|
74
|
+
export type AnthropicContentBlock =
|
|
75
|
+
| AnthropicTextBlock
|
|
76
|
+
| AnthropicImageBlock
|
|
77
|
+
| AnthropicDocumentBlock
|
|
78
|
+
| AnthropicToolUseBlock
|
|
79
|
+
| AnthropicToolResultBlock
|
|
80
|
+
| AnthropicThinkingBlock
|
|
81
|
+
| AnthropicRedactedThinkingBlock;
|
|
82
|
+
|
|
83
|
+
export interface AnthropicMessage {
|
|
84
|
+
role: "user" | "assistant";
|
|
85
|
+
content: string | AnthropicContentBlock[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface AnthropicToolDecl {
|
|
89
|
+
name: string;
|
|
90
|
+
description: string;
|
|
91
|
+
input_schema: ToolDecl["parameters"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AnthropicServerTool {
|
|
95
|
+
type: string;
|
|
96
|
+
name: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type AnthropicTool = AnthropicToolDecl | AnthropicServerTool;
|
|
100
|
+
|
|
101
|
+
export interface AnthropicThinkingConfig {
|
|
102
|
+
type: "enabled";
|
|
103
|
+
budget_tokens: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AnthropicRequestBody {
|
|
107
|
+
model: string;
|
|
108
|
+
max_tokens: number;
|
|
109
|
+
messages: AnthropicMessage[];
|
|
110
|
+
tools?: AnthropicTool[];
|
|
111
|
+
thinking?: AnthropicThinkingConfig;
|
|
112
|
+
stream?: true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readMediaBase64(ref: MediaRef): string {
|
|
116
|
+
return readFileSync(resolveMediaPath(ref)).toString("base64");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function expectSingleMedia(intent: CapabilityIntent): MediaRef {
|
|
120
|
+
if (intent.media === undefined || intent.media.length === 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
"anthropic: media-input capability requires intent.media to be non-empty",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (intent.media.length !== 1) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`anthropic: media-input capability expects exactly one media reference, got ${String(intent.media.length)}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const [media] = intent.media;
|
|
131
|
+
if (media === undefined) {
|
|
132
|
+
throw new Error("anthropic: media-input capability: media[0] is undefined");
|
|
133
|
+
}
|
|
134
|
+
return media;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function expectSingleTool(intent: CapabilityIntent): ToolDecl {
|
|
138
|
+
if (intent.tools === undefined || intent.tools.length === 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"anthropic: function-calling capability requires intent.tools to be non-empty",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (intent.tools.length !== 1) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`anthropic: function-calling capability expects exactly one tool, got ${String(intent.tools.length)}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const [tool] = intent.tools;
|
|
149
|
+
if (tool === undefined) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"anthropic: function-calling capability: tools[0] is undefined",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return tool;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function userTextMessage(prompt: string): AnthropicMessage {
|
|
158
|
+
return { role: "user", content: prompt };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function plainTextBody(
|
|
162
|
+
model: string,
|
|
163
|
+
intent: CapabilityIntent,
|
|
164
|
+
opts: { stream: boolean },
|
|
165
|
+
): AnthropicRequestBody {
|
|
166
|
+
const body: AnthropicRequestBody = {
|
|
167
|
+
model,
|
|
168
|
+
max_tokens: PLAIN_MAX_TOKENS,
|
|
169
|
+
messages: [userTextMessage(intent.prompt)],
|
|
170
|
+
};
|
|
171
|
+
if (opts.stream) body.stream = true;
|
|
172
|
+
return body;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function functionToolDecl(decl: ToolDecl): AnthropicToolDecl {
|
|
176
|
+
return {
|
|
177
|
+
name: decl.name,
|
|
178
|
+
description: decl.description,
|
|
179
|
+
input_schema: decl.parameters,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function functionCallingBody(
|
|
184
|
+
model: string,
|
|
185
|
+
intent: CapabilityIntent,
|
|
186
|
+
opts: { stream: boolean; thinking: boolean },
|
|
187
|
+
): AnthropicRequestBody {
|
|
188
|
+
const decl = expectSingleTool(intent);
|
|
189
|
+
const body: AnthropicRequestBody = {
|
|
190
|
+
model,
|
|
191
|
+
max_tokens: opts.thinking ? THINKING_MAX_TOKENS : PLAIN_MAX_TOKENS,
|
|
192
|
+
messages: [userTextMessage(intent.prompt)],
|
|
193
|
+
tools: [functionToolDecl(decl)],
|
|
194
|
+
};
|
|
195
|
+
if (opts.thinking) {
|
|
196
|
+
body.thinking = { type: "enabled", budget_tokens: THINKING_BUDGET_TOKENS };
|
|
197
|
+
}
|
|
198
|
+
if (opts.stream) body.stream = true;
|
|
199
|
+
return body;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function visionBody(
|
|
203
|
+
model: string,
|
|
204
|
+
intent: CapabilityIntent,
|
|
205
|
+
opts: { stream: boolean },
|
|
206
|
+
): AnthropicRequestBody {
|
|
207
|
+
const media = expectSingleMedia(intent);
|
|
208
|
+
if (media.kind !== "image") {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`anthropic vision-input: expected media.kind=image, got ${media.kind}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const body: AnthropicRequestBody = {
|
|
214
|
+
model,
|
|
215
|
+
max_tokens: PLAIN_MAX_TOKENS,
|
|
216
|
+
messages: [
|
|
217
|
+
{
|
|
218
|
+
role: "user",
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "image",
|
|
222
|
+
source: {
|
|
223
|
+
type: "base64",
|
|
224
|
+
media_type: mediaTypeFor(media),
|
|
225
|
+
data: readMediaBase64(media),
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{ type: "text", text: intent.prompt },
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
if (opts.stream) body.stream = true;
|
|
234
|
+
return body;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function documentBody(
|
|
238
|
+
model: string,
|
|
239
|
+
intent: CapabilityIntent,
|
|
240
|
+
opts: { stream: boolean },
|
|
241
|
+
): AnthropicRequestBody {
|
|
242
|
+
const media = expectSingleMedia(intent);
|
|
243
|
+
if (media.kind !== "document") {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`anthropic document-input: expected media.kind=document, got ${media.kind}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const mediaType = mediaTypeFor(media);
|
|
249
|
+
if (mediaType !== "application/pdf") {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`anthropic document-input: only application/pdf is supported, got ${mediaType}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const body: AnthropicRequestBody = {
|
|
255
|
+
model,
|
|
256
|
+
max_tokens: PLAIN_MAX_TOKENS,
|
|
257
|
+
messages: [
|
|
258
|
+
{
|
|
259
|
+
role: "user",
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: "document",
|
|
263
|
+
source: {
|
|
264
|
+
type: "base64",
|
|
265
|
+
media_type: "application/pdf",
|
|
266
|
+
data: readMediaBase64(media),
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{ type: "text", text: intent.prompt },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
if (opts.stream) body.stream = true;
|
|
275
|
+
return body;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function codeExecutionBody(
|
|
279
|
+
model: string,
|
|
280
|
+
intent: CapabilityIntent,
|
|
281
|
+
opts: { stream: boolean },
|
|
282
|
+
): AnthropicRequestBody {
|
|
283
|
+
const body: AnthropicRequestBody = {
|
|
284
|
+
model,
|
|
285
|
+
max_tokens: PLAIN_MAX_TOKENS,
|
|
286
|
+
messages: [userTextMessage(intent.prompt)],
|
|
287
|
+
tools: [{ type: CODE_EXECUTION_TOOL_TYPE, name: "code_execution" }],
|
|
288
|
+
};
|
|
289
|
+
if (opts.stream) body.stream = true;
|
|
290
|
+
return body;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function reasoningBody(
|
|
294
|
+
model: string,
|
|
295
|
+
intent: CapabilityIntent,
|
|
296
|
+
opts: { stream: boolean },
|
|
297
|
+
): AnthropicRequestBody {
|
|
298
|
+
const body: AnthropicRequestBody = {
|
|
299
|
+
model,
|
|
300
|
+
max_tokens: THINKING_MAX_TOKENS,
|
|
301
|
+
messages: [userTextMessage(intent.prompt)],
|
|
302
|
+
thinking: { type: "enabled", budget_tokens: THINKING_BUDGET_TOKENS },
|
|
303
|
+
};
|
|
304
|
+
if (opts.stream) body.stream = true;
|
|
305
|
+
return body;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function groundingBody(
|
|
309
|
+
model: string,
|
|
310
|
+
intent: CapabilityIntent,
|
|
311
|
+
opts: { stream: boolean },
|
|
312
|
+
): AnthropicRequestBody {
|
|
313
|
+
const body: AnthropicRequestBody = {
|
|
314
|
+
model,
|
|
315
|
+
max_tokens: PLAIN_MAX_TOKENS,
|
|
316
|
+
messages: [userTextMessage(intent.prompt)],
|
|
317
|
+
tools: [{ type: WEB_SEARCH_TOOL_TYPE, name: "web_search" }],
|
|
318
|
+
};
|
|
319
|
+
if (opts.stream) body.stream = true;
|
|
320
|
+
return body;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function redactedThinkingTurn1Body(
|
|
324
|
+
model: string,
|
|
325
|
+
intent: CapabilityIntent,
|
|
326
|
+
opts: { stream: boolean },
|
|
327
|
+
): AnthropicRequestBody {
|
|
328
|
+
const body: AnthropicRequestBody = {
|
|
329
|
+
model,
|
|
330
|
+
max_tokens: THINKING_MAX_TOKENS,
|
|
331
|
+
messages: [userTextMessage(intent.prompt)],
|
|
332
|
+
thinking: { type: "enabled", budget_tokens: THINKING_BUDGET_TOKENS },
|
|
333
|
+
};
|
|
334
|
+
if (opts.stream) body.stream = true;
|
|
335
|
+
return body;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function buildRequestBody(opts: {
|
|
339
|
+
model: string;
|
|
340
|
+
capability: Capability;
|
|
341
|
+
intent: CapabilityIntent;
|
|
342
|
+
}): AnthropicRequestBody {
|
|
343
|
+
switch (opts.capability) {
|
|
344
|
+
case "plain-text":
|
|
345
|
+
return plainTextBody(opts.model, opts.intent, { stream: false });
|
|
346
|
+
case "plain-text-streaming":
|
|
347
|
+
return plainTextBody(opts.model, opts.intent, { stream: true });
|
|
348
|
+
case "function-calling":
|
|
349
|
+
return functionCallingBody(opts.model, opts.intent, {
|
|
350
|
+
stream: false,
|
|
351
|
+
thinking: false,
|
|
352
|
+
});
|
|
353
|
+
case "function-calling-multi-turn":
|
|
354
|
+
return functionCallingBody(opts.model, opts.intent, {
|
|
355
|
+
stream: false,
|
|
356
|
+
thinking: false,
|
|
357
|
+
});
|
|
358
|
+
case "function-calling-multi-turn-streaming":
|
|
359
|
+
return functionCallingBody(opts.model, opts.intent, {
|
|
360
|
+
stream: true,
|
|
361
|
+
thinking: false,
|
|
362
|
+
});
|
|
363
|
+
case "function-calling-with-thinking":
|
|
364
|
+
return functionCallingBody(opts.model, opts.intent, {
|
|
365
|
+
stream: false,
|
|
366
|
+
thinking: true,
|
|
367
|
+
});
|
|
368
|
+
case "function-calling-with-thinking-streaming":
|
|
369
|
+
return functionCallingBody(opts.model, opts.intent, {
|
|
370
|
+
stream: true,
|
|
371
|
+
thinking: true,
|
|
372
|
+
});
|
|
373
|
+
case "redacted-thinking":
|
|
374
|
+
return redactedThinkingTurn1Body(opts.model, opts.intent, {
|
|
375
|
+
stream: false,
|
|
376
|
+
});
|
|
377
|
+
case "redacted-thinking-streaming":
|
|
378
|
+
return redactedThinkingTurn1Body(opts.model, opts.intent, {
|
|
379
|
+
stream: true,
|
|
380
|
+
});
|
|
381
|
+
case "files-api-reference":
|
|
382
|
+
case "files-api-reference-streaming":
|
|
383
|
+
throw new Error(
|
|
384
|
+
`anthropic: capability ${opts.capability} is multipart; ` +
|
|
385
|
+
"use iterateCaptureSteps from the plug-in, not buildRequestBody.",
|
|
386
|
+
);
|
|
387
|
+
case "vision-input":
|
|
388
|
+
return visionBody(opts.model, opts.intent, { stream: false });
|
|
389
|
+
case "vision-input-streaming":
|
|
390
|
+
return visionBody(opts.model, opts.intent, { stream: true });
|
|
391
|
+
case "document-input":
|
|
392
|
+
return documentBody(opts.model, opts.intent, { stream: false });
|
|
393
|
+
case "document-input-streaming":
|
|
394
|
+
return documentBody(opts.model, opts.intent, { stream: true });
|
|
395
|
+
case "code-execution":
|
|
396
|
+
return codeExecutionBody(opts.model, opts.intent, { stream: false });
|
|
397
|
+
case "code-execution-streaming":
|
|
398
|
+
return codeExecutionBody(opts.model, opts.intent, { stream: true });
|
|
399
|
+
case "reasoning-content":
|
|
400
|
+
return reasoningBody(opts.model, opts.intent, { stream: false });
|
|
401
|
+
case "reasoning-content-streaming":
|
|
402
|
+
return reasoningBody(opts.model, opts.intent, { stream: true });
|
|
403
|
+
case "grounding":
|
|
404
|
+
return groundingBody(opts.model, opts.intent, { stream: false });
|
|
405
|
+
case "grounding-streaming":
|
|
406
|
+
return groundingBody(opts.model, opts.intent, { stream: true });
|
|
407
|
+
case "audio-input":
|
|
408
|
+
case "audio-input-streaming":
|
|
409
|
+
case "video-input":
|
|
410
|
+
case "video-input-streaming":
|
|
411
|
+
case "image-output":
|
|
412
|
+
case "image-output-streaming":
|
|
413
|
+
case "safety-classification":
|
|
414
|
+
case "safety-classification-streaming":
|
|
415
|
+
case "structured-output":
|
|
416
|
+
case "structured-output-streaming":
|
|
417
|
+
throw new Error(
|
|
418
|
+
`anthropic: capability ${opts.capability} is not supported by any Anthropic model`,
|
|
419
|
+
);
|
|
420
|
+
default: {
|
|
421
|
+
const exhaustive: never = opts.capability;
|
|
422
|
+
throw new Error(`anthropic: unhandled capability ${String(exhaustive)}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Multi-turn helpers.
|
|
428
|
+
|
|
429
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
430
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function extractAssistantContentBlocks(
|
|
434
|
+
parsed: unknown,
|
|
435
|
+
): AnthropicContentBlock[] {
|
|
436
|
+
if (!isRecord(parsed)) {
|
|
437
|
+
throw new Error("anthropic multi-turn: turn-1 response is not an object");
|
|
438
|
+
}
|
|
439
|
+
const content = parsed.content;
|
|
440
|
+
if (!Array.isArray(content)) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
"anthropic multi-turn: turn-1 response has no content array",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
for (const block of content) {
|
|
446
|
+
if (!isRecord(block) || typeof block.type !== "string") {
|
|
447
|
+
throw new Error(
|
|
448
|
+
"anthropic multi-turn: turn-1 response content[] entry is not a block",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Anthropic may return assistant content blocks (server_tool_use, web_search_tool_result, code_execution_tool_use, citation blocks) that this plug-in does not enumerate. The wire round-trip is what matters; we forward verbatim. The runtime guard above only verifies each entry is an object with a string type field.
|
|
453
|
+
return content as AnthropicContentBlock[];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function findFirstToolUse(blocks: readonly AnthropicContentBlock[]): {
|
|
457
|
+
id: string;
|
|
458
|
+
name: string;
|
|
459
|
+
} {
|
|
460
|
+
for (const block of blocks) {
|
|
461
|
+
if (block.type === "tool_use") {
|
|
462
|
+
return { id: block.id, name: block.name };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
throw new Error(
|
|
466
|
+
"anthropic multi-turn: turn-1 response had no tool_use content block",
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
interface ToolFollowUp {
|
|
471
|
+
toolName: string;
|
|
472
|
+
content: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
interface UserFollowUp {
|
|
476
|
+
content: string;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function partitionFollowUps(intent: CapabilityIntent): {
|
|
480
|
+
tool?: ToolFollowUp;
|
|
481
|
+
user?: UserFollowUp;
|
|
482
|
+
} {
|
|
483
|
+
const out: { tool?: ToolFollowUp; user?: UserFollowUp } = {};
|
|
484
|
+
if (intent.followUp === undefined) return out;
|
|
485
|
+
for (const step of intent.followUp) {
|
|
486
|
+
if (step.role === "tool" && out.tool === undefined) {
|
|
487
|
+
out.tool = { toolName: step.toolName, content: step.content };
|
|
488
|
+
} else if (step.role === "user" && out.user === undefined) {
|
|
489
|
+
out.user = { content: step.content };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return out;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// When an intent has no tool-role followUp (true for
|
|
496
|
+
// function-calling-with-thinking, whose INTENTS record declares only a
|
|
497
|
+
// prompt and a tool decl), fall back to the tool name from intent.tools
|
|
498
|
+
// with an empty JSON object payload. Matches the deriveToolFollowUp
|
|
499
|
+
// fallback in @intx/inference-discovery-google-genai.
|
|
500
|
+
function deriveToolFollowUp(intent: CapabilityIntent): ToolFollowUp {
|
|
501
|
+
const followUps = partitionFollowUps(intent);
|
|
502
|
+
if (followUps.tool !== undefined) return followUps.tool;
|
|
503
|
+
const tools = intent.tools;
|
|
504
|
+
if (tools === undefined || tools.length === 0) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
"anthropic multi-turn: intent has neither followUp.tool nor tools",
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const [tool] = tools;
|
|
510
|
+
if (tool === undefined) {
|
|
511
|
+
throw new Error("anthropic multi-turn: intent.tools[0] is undefined");
|
|
512
|
+
}
|
|
513
|
+
return { toolName: tool.name, content: "{}" };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function buildFunctionCallingTurn2Body(opts: {
|
|
517
|
+
model: string;
|
|
518
|
+
capability: Capability;
|
|
519
|
+
intent: CapabilityIntent;
|
|
520
|
+
turn1Body: AnthropicRequestBody;
|
|
521
|
+
turn1Response: unknown;
|
|
522
|
+
}): AnthropicRequestBody {
|
|
523
|
+
const assistantBlocks = extractAssistantContentBlocks(opts.turn1Response);
|
|
524
|
+
const toolUse = findFirstToolUse(assistantBlocks);
|
|
525
|
+
const toolFollowUp = deriveToolFollowUp(opts.intent);
|
|
526
|
+
const messages: AnthropicMessage[] = [
|
|
527
|
+
...opts.turn1Body.messages,
|
|
528
|
+
{ role: "assistant", content: assistantBlocks },
|
|
529
|
+
{
|
|
530
|
+
role: "user",
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "tool_result",
|
|
534
|
+
tool_use_id: toolUse.id,
|
|
535
|
+
content: toolFollowUp.content,
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
];
|
|
540
|
+
const body: AnthropicRequestBody = {
|
|
541
|
+
model: opts.model,
|
|
542
|
+
max_tokens: opts.turn1Body.max_tokens,
|
|
543
|
+
messages,
|
|
544
|
+
};
|
|
545
|
+
if (opts.turn1Body.tools !== undefined) body.tools = opts.turn1Body.tools;
|
|
546
|
+
if (opts.turn1Body.thinking !== undefined)
|
|
547
|
+
body.thinking = opts.turn1Body.thinking;
|
|
548
|
+
if (opts.turn1Body.stream === true) body.stream = true;
|
|
549
|
+
return body;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function buildRedactedThinkingTurn2Body(opts: {
|
|
553
|
+
model: string;
|
|
554
|
+
intent: CapabilityIntent;
|
|
555
|
+
turn1Body: AnthropicRequestBody;
|
|
556
|
+
turn1Response: unknown;
|
|
557
|
+
}): AnthropicRequestBody {
|
|
558
|
+
const assistantBlocks = extractAssistantContentBlocks(opts.turn1Response);
|
|
559
|
+
const followUps = partitionFollowUps(opts.intent);
|
|
560
|
+
const userFollowUp = followUps.user;
|
|
561
|
+
if (userFollowUp === undefined) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
"anthropic redacted-thinking: intent.followUp must include a user entry",
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
const messages: AnthropicMessage[] = [
|
|
567
|
+
...opts.turn1Body.messages,
|
|
568
|
+
{ role: "assistant", content: assistantBlocks },
|
|
569
|
+
{ role: "user", content: userFollowUp.content },
|
|
570
|
+
];
|
|
571
|
+
const body: AnthropicRequestBody = {
|
|
572
|
+
model: opts.model,
|
|
573
|
+
max_tokens: opts.turn1Body.max_tokens,
|
|
574
|
+
messages,
|
|
575
|
+
};
|
|
576
|
+
if (opts.turn1Body.thinking !== undefined)
|
|
577
|
+
body.thinking = opts.turn1Body.thinking;
|
|
578
|
+
if (opts.turn1Body.stream === true) body.stream = true;
|
|
579
|
+
return body;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function buildFilesApiGenerateBody(opts: {
|
|
583
|
+
model: string;
|
|
584
|
+
fileId: string;
|
|
585
|
+
intent: CapabilityIntent;
|
|
586
|
+
stream: boolean;
|
|
587
|
+
}): AnthropicRequestBody {
|
|
588
|
+
const body: AnthropicRequestBody = {
|
|
589
|
+
model: opts.model,
|
|
590
|
+
max_tokens: PLAIN_MAX_TOKENS,
|
|
591
|
+
messages: [
|
|
592
|
+
{
|
|
593
|
+
role: "user",
|
|
594
|
+
content: [
|
|
595
|
+
{
|
|
596
|
+
type: "document",
|
|
597
|
+
source: { type: "file", file_id: opts.fileId },
|
|
598
|
+
},
|
|
599
|
+
{ type: "text", text: opts.intent.prompt },
|
|
600
|
+
],
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
};
|
|
604
|
+
if (opts.stream) body.stream = true;
|
|
605
|
+
return body;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Capability-keyed model-supports check. Anthropic's three current
|
|
609
|
+
// models (Sonnet, Opus, Haiku) all expose the same surface; if that
|
|
610
|
+
// stops being true, this gate is where to encode the divergence.
|
|
611
|
+
const SUPPORTED_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
|
|
612
|
+
"plain-text",
|
|
613
|
+
"plain-text-streaming",
|
|
614
|
+
"function-calling",
|
|
615
|
+
"function-calling-multi-turn",
|
|
616
|
+
"function-calling-multi-turn-streaming",
|
|
617
|
+
"function-calling-with-thinking",
|
|
618
|
+
"function-calling-with-thinking-streaming",
|
|
619
|
+
"vision-input",
|
|
620
|
+
"vision-input-streaming",
|
|
621
|
+
"document-input",
|
|
622
|
+
"document-input-streaming",
|
|
623
|
+
"code-execution",
|
|
624
|
+
"code-execution-streaming",
|
|
625
|
+
"reasoning-content",
|
|
626
|
+
"reasoning-content-streaming",
|
|
627
|
+
"grounding",
|
|
628
|
+
"grounding-streaming",
|
|
629
|
+
"files-api-reference",
|
|
630
|
+
"files-api-reference-streaming",
|
|
631
|
+
"redacted-thinking",
|
|
632
|
+
"redacted-thinking-streaming",
|
|
633
|
+
]);
|
|
634
|
+
|
|
635
|
+
export function isSupportedCapability(capability: Capability): boolean {
|
|
636
|
+
return SUPPORTED_CAPABILITIES.has(capability);
|
|
637
|
+
}
|