@dex-ai/anthropic 0.1.6

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/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@dex-ai/anthropic",
3
+ "version": "0.1.6",
4
+ "description": "Anthropic Messages API provider for @dex-ai/sdk.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ }
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "scripts": {
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@dex-ai/sdk": "^0.1.1",
20
+ "zod-to-json-schema": "^3.24.0"
21
+ },
22
+ "devDependencies": {
23
+ "zod": "^3.23.8"
24
+ },
25
+ "peerDependencies": {
26
+ "zod": "^3.23.0"
27
+ },
28
+ "sideEffects": false,
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ }
33
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,48 @@
1
+ /** Anthropic-specific error class. */
2
+
3
+ export class AnthropicError extends Error {
4
+ readonly status: number;
5
+ readonly code: string | null;
6
+ readonly type: string | null;
7
+
8
+ constructor(
9
+ status: number,
10
+ code: string | null,
11
+ type: string | null,
12
+ message: string,
13
+ ) {
14
+ super(message);
15
+ this.name = "AnthropicError";
16
+ this.status = status;
17
+ this.code = code;
18
+ this.type = type;
19
+ }
20
+
21
+ static async fromResponse(res: Response): Promise<AnthropicError> {
22
+ let body: string;
23
+ try {
24
+ body = await res.text();
25
+ } catch {
26
+ body = "";
27
+ }
28
+
29
+ let code: string | null = null;
30
+ let type: string | null = null;
31
+ let message = `${res.status} ${res.statusText}`;
32
+
33
+ try {
34
+ const json = JSON.parse(body) as {
35
+ error?: { message?: string; type?: string };
36
+ type?: string;
37
+ };
38
+ if (json.error) {
39
+ message = json.error.message ?? message;
40
+ type = json.error.type ?? json.type ?? null;
41
+ }
42
+ } catch {
43
+ if (body) message += ` — ${body.slice(0, 200)}`;
44
+ }
45
+
46
+ return new AnthropicError(res.status, code, type, message);
47
+ }
48
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Anthropic Extension — provides models via the Messages API.
3
+ */
4
+
5
+ import type {
6
+ Extension,
7
+ Model,
8
+ StreamPart,
9
+ ModelRequest,
10
+ ThinkingLevel,
11
+ } from "@dex-ai/sdk";
12
+ import { createAnthropicStream } from "./stream";
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Options */
16
+ /* ------------------------------------------------------------------ */
17
+
18
+ export interface AnthropicExtensionOptions {
19
+ /** API key. Falls back to ANTHROPIC_API_KEY. */
20
+ readonly apiKey?: string;
21
+ /** Base URL (without trailing slash). Falls back to ANTHROPIC_BASE_URL → https://api.anthropic.com */
22
+ readonly baseUrl?: string;
23
+ /** Extension name (used as provider name in Agent.create). Default: "anthropic". */
24
+ readonly name?: string;
25
+ /** Model IDs to register statically. Discovered models are added alongside these. */
26
+ readonly models?: ReadonlyArray<string | ModelConfig>;
27
+ /** Messages endpoint path appended to baseUrl. Default: "/v1/messages". */
28
+ readonly messagesPath?: string;
29
+ /**
30
+ * Models endpoint path for discovery.
31
+ * Default: "/v1/models". Set to null to disable discovery.
32
+ */
33
+ readonly modelsPath?: string | null;
34
+ /** Anthropic API version header. Default: "2023-06-01". */
35
+ readonly anthropicVersion?: string;
36
+ /** Emit raw-chunk parts during streaming. Default: false. */
37
+ readonly rawChunks?: boolean;
38
+ /** Additional headers sent with every request. */
39
+ readonly headers?: Record<string, string>;
40
+ /** Fetch override (for retry wrappers, testing). */
41
+ readonly fetch?: (url: string, init: RequestInit) => Promise<Response>;
42
+ }
43
+
44
+ export interface ModelConfig {
45
+ readonly id: string;
46
+ readonly name?: string;
47
+ readonly contextWindow?: number;
48
+ readonly maxTokens?: number;
49
+ readonly reasoning?: boolean;
50
+ readonly thinkingLevels?: ReadonlyArray<ThinkingLevel>;
51
+ readonly input?: ReadonlyArray<"text" | "image">;
52
+ }
53
+
54
+ /* ------------------------------------------------------------------ */
55
+ /* Helpers */
56
+ /* ------------------------------------------------------------------ */
57
+
58
+ function resolveBaseUrl(opts: AnthropicExtensionOptions): string {
59
+ const raw =
60
+ opts.baseUrl ||
61
+ process.env.ANTHROPIC_BASE_URL ||
62
+ "https://api.anthropic.com";
63
+ return raw.replace(/\/+$/, "");
64
+ }
65
+
66
+ function resolveApiKey(opts: AnthropicExtensionOptions): string {
67
+ const key =
68
+ opts.apiKey ||
69
+ process.env.ANTHROPIC_API_KEY;
70
+ if (!key) {
71
+ throw new Error(
72
+ "@dex-ai/anthropic: no API key found. Set apiKey or ANTHROPIC_API_KEY.",
73
+ );
74
+ }
75
+ return key;
76
+ }
77
+
78
+ /* ------------------------------------------------------------------ */
79
+ /* Extension factory */
80
+ /* ------------------------------------------------------------------ */
81
+
82
+ export function anthropicExtension(
83
+ opts: AnthropicExtensionOptions = {},
84
+ ): Extension {
85
+ const extName = opts.name ?? "anthropic";
86
+ const baseUrl = resolveBaseUrl(opts);
87
+ const apiKey = resolveApiKey(opts);
88
+ const rawChunks = opts.rawChunks ?? false;
89
+ const messagesPath = opts.messagesPath ?? "/v1/messages";
90
+ const anthropicVersion = opts.anthropicVersion ?? "2023-06-01";
91
+ const doFetch =
92
+ opts.fetch ?? ((url: string, init: RequestInit) => fetch(url, init));
93
+
94
+ function createStreamFn(
95
+ modelId: string,
96
+ modelMaxTokens?: number,
97
+ ): (req: ModelRequest) => AsyncIterable<StreamPart> {
98
+ return createAnthropicStream(
99
+ {
100
+ baseUrl,
101
+ apiKey,
102
+ modelId,
103
+ providerName: extName,
104
+ rawChunks,
105
+ messagesPath,
106
+ anthropicVersion,
107
+ doFetch,
108
+ ...(opts.headers !== undefined ? { headers: opts.headers } : {}),
109
+ },
110
+ modelMaxTokens,
111
+ );
112
+ }
113
+
114
+ // Static models from config
115
+ const staticModels: Model[] = (opts.models ?? []).map((m) => {
116
+ const cfg: ModelConfig = typeof m === "string" ? { id: m } : m;
117
+ return {
118
+ id: cfg.id,
119
+ ...(cfg.name !== undefined ? { name: cfg.name } : {}),
120
+ ...(cfg.contextWindow !== undefined
121
+ ? { contextWindow: cfg.contextWindow }
122
+ : {}),
123
+ ...(cfg.maxTokens !== undefined ? { maxTokens: cfg.maxTokens } : {}),
124
+ ...(cfg.reasoning !== undefined ? { reasoning: cfg.reasoning } : {}),
125
+ ...(cfg.thinkingLevels !== undefined
126
+ ? { thinkingLevels: cfg.thinkingLevels }
127
+ : {}),
128
+ ...(cfg.input !== undefined ? { input: cfg.input } : {}),
129
+ stream: createStreamFn(cfg.id, cfg.maxTokens),
130
+ };
131
+ });
132
+
133
+ const allModels: Model[] = [...staticModels];
134
+
135
+ return {
136
+ name: extName,
137
+ get models() {
138
+ return allModels;
139
+ },
140
+
141
+ async init() {
142
+ const modelsPath =
143
+ opts.modelsPath !== null ? (opts.modelsPath ?? "/v1/models") : null;
144
+ if (!modelsPath) return;
145
+
146
+ try {
147
+ const modelsUrl = modelsPath.startsWith("http")
148
+ ? modelsPath
149
+ : `${baseUrl}${modelsPath}`;
150
+
151
+ const res = await doFetch(modelsUrl, {
152
+ method: "GET",
153
+ headers: {
154
+ "x-api-key": apiKey,
155
+ "anthropic-version": anthropicVersion,
156
+ ...(opts.headers !== undefined ? opts.headers : {}),
157
+ },
158
+ });
159
+ if (!res.ok) return; // silently skip if discovery fails
160
+
161
+ const json = (await res.json()) as any;
162
+ const modelList: Array<{
163
+ id: string;
164
+ display_name?: string;
165
+ max_tokens?: number;
166
+ max_input_tokens?: number;
167
+ capabilities?: {
168
+ thinking?: { supported?: boolean };
169
+ image_input?: { supported?: boolean };
170
+ };
171
+ }> = json.data ?? (Array.isArray(json) ? json : []);
172
+
173
+ // Add discovered models not already statically configured
174
+ const staticIds = new Set(staticModels.map((m) => m.id));
175
+ for (const m of modelList) {
176
+ if (!m.id || staticIds.has(m.id)) continue;
177
+
178
+ const hasThinking = m.capabilities?.thinking?.supported ?? false;
179
+ const hasImage = m.capabilities?.image_input?.supported ?? false;
180
+ const inputModes: ("text" | "image")[] = hasImage
181
+ ? ["text", "image"]
182
+ : ["text"];
183
+
184
+ allModels.push({
185
+ id: m.id,
186
+ ...(m.display_name ? { name: m.display_name } : {}),
187
+ ...(m.max_input_tokens
188
+ ? { contextWindow: m.max_input_tokens }
189
+ : {}),
190
+ ...(m.max_tokens ? { maxTokens: m.max_tokens } : {}),
191
+ reasoning: hasThinking,
192
+ ...(hasThinking
193
+ ? {
194
+ thinkingLevels: [
195
+ "off",
196
+ "min",
197
+ "low",
198
+ "med",
199
+ "high",
200
+ "max",
201
+ ] as ThinkingLevel[],
202
+ }
203
+ : {}),
204
+ input: inputModes,
205
+ stream: createStreamFn(m.id, m.max_tokens),
206
+ });
207
+ }
208
+ } catch {
209
+ // Discovery failed — proceed with static models only
210
+ }
211
+ },
212
+ };
213
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export { anthropicExtension } from "./extension";
2
+ export type { AnthropicExtensionOptions, ModelConfig } from "./extension";
3
+
4
+ import type { ProviderDescriptor } from "@dex-ai/sdk";
5
+ import { anthropicExtension } from "./extension";
6
+
7
+ export const descriptor: ProviderDescriptor = {
8
+ type: "anthropic",
9
+ label: "Anthropic",
10
+ fields: [
11
+ { key: "apiKey", label: "API Key", required: true, secret: true, envVar: "ANTHROPIC_API_KEY", hint: "sk-ant-... (or press Enter to use ANTHROPIC_API_KEY env var)" },
12
+ { key: "baseUrl", label: "Base URL", required: false, default: "https://api.anthropic.com", hint: "Custom endpoint for proxies" },
13
+ { key: "models", label: "Models", required: false, hint: "Comma-separated model IDs, or leave blank to auto-discover" },
14
+ ],
15
+ create(config) {
16
+ const models = config.models as string[] | undefined;
17
+ return anthropicExtension({
18
+ ...(config.name ? { name: config.name as string } : {}),
19
+ ...(config.apiKey ? { apiKey: config.apiKey as string } : {}),
20
+ ...(config.baseUrl ? { baseUrl: config.baseUrl as string } : {}),
21
+ ...(models ? { models } : {}),
22
+ ...(models?.length ? { modelsPath: null } : {}),
23
+ });
24
+ },
25
+ };
package/src/sse.ts ADDED
@@ -0,0 +1,35 @@
1
+ /** Minimal SSE line parser for streaming API responses. */
2
+
3
+ export interface SSEFrame {
4
+ data: string;
5
+ }
6
+
7
+ /** Parse a ReadableStream<Uint8Array> into SSE frames. Yields each `data:` payload (trimmed of the prefix). */
8
+ export async function* parseSSE(
9
+ body: ReadableStream<Uint8Array>,
10
+ ): AsyncGenerator<SSEFrame> {
11
+ const reader = body.getReader();
12
+ const decoder = new TextDecoder();
13
+ let buffer = "";
14
+ try {
15
+ while (true) {
16
+ const { done, value } = await reader.read();
17
+ if (done) break;
18
+ buffer += decoder.decode(value, { stream: true });
19
+ let nl: number;
20
+ while ((nl = buffer.indexOf("\n")) !== -1) {
21
+ const line = buffer.slice(0, nl).replace(/\r$/, "");
22
+ buffer = buffer.slice(nl + 1);
23
+ if (!line) continue;
24
+ if (line.startsWith("data: ")) {
25
+ yield { data: line.slice(6) };
26
+ } else if (line.startsWith("data:")) {
27
+ yield { data: line.slice(5) };
28
+ }
29
+ // ignore comments / other fields
30
+ }
31
+ }
32
+ } finally {
33
+ reader.releaseLock();
34
+ }
35
+ }
package/src/stream.ts ADDED
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Anthropic Messages API streaming: POST /v1/messages → StreamPart async iterable.
3
+ */
4
+
5
+ import type {
6
+ StreamPart,
7
+ ModelRequest,
8
+ FinishReason,
9
+ Usage,
10
+ ResponseMeta,
11
+ Content,
12
+ Message,
13
+ } from "@dex-ai/sdk";
14
+ import { parseSSE } from "./sse";
15
+ import { AnthropicError } from "./errors";
16
+ import {
17
+ messagesToAnthropic,
18
+ toolToAnthropic,
19
+ toolChoiceToAnthropic,
20
+ } from "./translate";
21
+ import { thinkingToAnthropicConfig } from "./thinking";
22
+
23
+ /* ------------------------------------------------------------------ */
24
+ /* Types */
25
+ /* ------------------------------------------------------------------ */
26
+
27
+ interface ToolBlockAccumulator {
28
+ id: string;
29
+ name: string;
30
+ inputJson: string;
31
+ }
32
+
33
+ interface StreamOptions {
34
+ baseUrl: string;
35
+ apiKey: string;
36
+ modelId: string;
37
+ providerName: string;
38
+ rawChunks: boolean;
39
+ messagesPath: string;
40
+ anthropicVersion: string;
41
+ doFetch: (url: string, init: RequestInit) => Promise<Response>;
42
+ headers?: Record<string, string> | undefined;
43
+ }
44
+
45
+ /* ------------------------------------------------------------------ */
46
+ /* FinishReason mapping */
47
+ /* ------------------------------------------------------------------ */
48
+
49
+ function mapStopReason(reason: string | undefined | null): FinishReason {
50
+ switch (reason) {
51
+ case "end_turn":
52
+ return "stop";
53
+ case "tool_use":
54
+ return "tool-calls";
55
+ case "max_tokens":
56
+ return "length";
57
+ case "stop_sequence":
58
+ return "stop";
59
+ default:
60
+ return "stop";
61
+ }
62
+ }
63
+
64
+ /* ------------------------------------------------------------------ */
65
+ /* createAnthropicStream */
66
+ /* ------------------------------------------------------------------ */
67
+
68
+ export function createAnthropicStream(
69
+ opts: StreamOptions,
70
+ modelMaxTokens?: number,
71
+ ): (req: ModelRequest) => AsyncIterable<StreamPart> {
72
+ const {
73
+ baseUrl,
74
+ apiKey,
75
+ modelId,
76
+ providerName,
77
+ rawChunks,
78
+ messagesPath,
79
+ anthropicVersion,
80
+ doFetch,
81
+ } = opts;
82
+ const url = `${baseUrl}${messagesPath}`;
83
+
84
+ return async function* stream(req: ModelRequest): AsyncIterable<StreamPart> {
85
+ const startedAt = Date.now();
86
+
87
+ // Build Anthropic request body
88
+ const { system, messages } = messagesToAnthropic(req.messages, {
89
+ ...(req.cacheBreakpoints
90
+ ? { cacheBreakpoints: req.cacheBreakpoints }
91
+ : {}),
92
+ });
93
+ const body: Record<string, unknown> = {
94
+ model: modelId,
95
+ messages,
96
+ max_tokens: req.maxTokens ?? modelMaxTokens ?? 8192,
97
+ stream: true,
98
+ };
99
+
100
+ if (system.length > 0) body.system = system;
101
+ if (req.temperature !== undefined) body.temperature = req.temperature;
102
+ if (req.topP !== undefined) body.top_p = req.topP;
103
+ if (req.stopSequences?.length) body.stop_sequences = req.stopSequences;
104
+
105
+ // Tools
106
+ if (req.tools?.length) {
107
+ const tools = req.tools.map(toolToAnthropic);
108
+ // Cache breakpoint on last tool — caches entire system+tools prefix
109
+ if (tools.length > 0) {
110
+ tools[tools.length - 1] = {
111
+ ...tools[tools.length - 1]!,
112
+ cache_control: { type: "ephemeral" },
113
+ };
114
+ }
115
+ body.tools = tools;
116
+ const tc = toolChoiceToAnthropic(req.toolChoice);
117
+ if (tc !== undefined) body.tool_choice = tc;
118
+ }
119
+
120
+ // Extended thinking
121
+ const thinkingConfig = thinkingToAnthropicConfig(
122
+ req.thinking,
123
+ modelMaxTokens,
124
+ );
125
+ if (thinkingConfig) {
126
+ body.thinking = thinkingConfig;
127
+ }
128
+
129
+ // Provider options pass-through
130
+ if (req.providerOptions) Object.assign(body, req.providerOptions);
131
+
132
+ const init: RequestInit = {
133
+ method: "POST",
134
+ headers: {
135
+ "Content-Type": "application/json",
136
+ "x-api-key": apiKey,
137
+ "anthropic-version": anthropicVersion,
138
+ ...opts.headers,
139
+ },
140
+ body: JSON.stringify(body),
141
+ };
142
+ if (req.signal) (init as { signal: AbortSignal }).signal = req.signal;
143
+
144
+ let res: Response;
145
+ try {
146
+ res = await doFetch(url, init);
147
+ } catch (err: unknown) {
148
+ if (err instanceof Error && err.name === "AbortError") {
149
+ yield { type: "abort", reason: err };
150
+ return;
151
+ }
152
+ throw err;
153
+ }
154
+
155
+ if (!res.ok) {
156
+ const error = await AnthropicError.fromResponse(res);
157
+ yield { type: "error", error, recoverable: false };
158
+ return;
159
+ }
160
+
161
+ if (!res.body) {
162
+ yield {
163
+ type: "error",
164
+ error: new Error("anthropic stream: empty response body"),
165
+ recoverable: false,
166
+ };
167
+ return;
168
+ }
169
+
170
+ const meta: ResponseMeta = { providerName, modelId, startedAt };
171
+ yield { type: "response-start", meta };
172
+ yield { type: "message-start", role: "assistant" };
173
+
174
+ // Accumulation state
175
+ const textParts: string[] = [];
176
+ const reasoningParts: string[] = [];
177
+ const toolBlocks: Map<number, ToolBlockAccumulator> = new Map();
178
+ const usage: {
179
+ inputTokens: number;
180
+ outputTokens: number;
181
+ cachedInputTokens?: number;
182
+ cacheCreationInputTokens?: number;
183
+ } = {
184
+ inputTokens: 0,
185
+ outputTokens: 0,
186
+ };
187
+ let finishReason: FinishReason = "stop";
188
+ let currentBlockIdx = -1;
189
+ let responseId: string | undefined;
190
+
191
+ try {
192
+ for await (const frame of parseSSE(res.body)) {
193
+ if (rawChunks) {
194
+ yield { type: "raw-chunk", providerName, data: frame.data };
195
+ }
196
+
197
+ let event: any;
198
+ try {
199
+ event = JSON.parse(frame.data);
200
+ } catch {
201
+ continue;
202
+ }
203
+
204
+ const eventType = event.type as string | undefined;
205
+
206
+ switch (eventType) {
207
+ case "message_start": {
208
+ if (event.message?.id) responseId = event.message.id as string;
209
+ if (event.message?.usage) {
210
+ const u = event.message.usage as Record<string, unknown>;
211
+ usage.inputTokens = (u.input_tokens as number) ?? 0;
212
+ if (u.cache_read_input_tokens !== undefined) {
213
+ usage.cachedInputTokens = u.cache_read_input_tokens as number;
214
+ }
215
+ if (u.cache_creation_input_tokens !== undefined) {
216
+ usage.cacheCreationInputTokens =
217
+ u.cache_creation_input_tokens as number;
218
+ }
219
+ }
220
+ break;
221
+ }
222
+
223
+ case "content_block_start": {
224
+ currentBlockIdx = (event.index as number) ?? 0;
225
+ const block = event.content_block as
226
+ | { type: string; id?: string; name?: string }
227
+ | undefined;
228
+ if (block?.type === "tool_use") {
229
+ toolBlocks.set(currentBlockIdx, {
230
+ id: block.id ?? "",
231
+ name: block.name ?? "",
232
+ inputJson: "",
233
+ });
234
+ }
235
+ break;
236
+ }
237
+
238
+ case "content_block_delta": {
239
+ const delta = event.delta as
240
+ | {
241
+ type: string;
242
+ text?: string;
243
+ thinking?: string;
244
+ partial_json?: string;
245
+ }
246
+ | undefined;
247
+ if (!delta) break;
248
+
249
+ if (delta.type === "text_delta" && delta.text) {
250
+ textParts.push(delta.text);
251
+ yield { type: "text-delta", delta: delta.text };
252
+ } else if (delta.type === "thinking_delta" && delta.thinking) {
253
+ reasoningParts.push(delta.thinking);
254
+ yield { type: "reasoning-delta", delta: delta.thinking };
255
+ } else if (
256
+ delta.type === "input_json_delta" &&
257
+ delta.partial_json !== undefined
258
+ ) {
259
+ const idx = (event.index as number) ?? currentBlockIdx;
260
+ const tool = toolBlocks.get(idx);
261
+ if (tool) {
262
+ tool.inputJson += delta.partial_json;
263
+ yield {
264
+ type: "tool-call-delta",
265
+ toolCallId: tool.id,
266
+ toolName: tool.name,
267
+ inputDelta: delta.partial_json,
268
+ };
269
+ }
270
+ }
271
+ break;
272
+ }
273
+
274
+ case "content_block_stop": {
275
+ const idx = (event.index as number) ?? currentBlockIdx;
276
+ const tool = toolBlocks.get(idx);
277
+ if (tool) {
278
+ let input: unknown = {};
279
+ try {
280
+ input = tool.inputJson ? JSON.parse(tool.inputJson) : {};
281
+ } catch {
282
+ input = tool.inputJson;
283
+ }
284
+ yield {
285
+ type: "tool-call",
286
+ toolCallId: tool.id,
287
+ toolName: tool.name,
288
+ input,
289
+ };
290
+ }
291
+ break;
292
+ }
293
+
294
+ case "message_delta": {
295
+ const d = event.delta as { stop_reason?: string } | undefined;
296
+ if (d?.stop_reason) {
297
+ finishReason = mapStopReason(d.stop_reason);
298
+ }
299
+ const u = event.usage as { output_tokens?: number } | undefined;
300
+ if (u?.output_tokens !== undefined) {
301
+ usage.outputTokens = u.output_tokens;
302
+ }
303
+ break;
304
+ }
305
+
306
+ case "message_stop": {
307
+ // End of stream
308
+ break;
309
+ }
310
+
311
+ case "error": {
312
+ const errMsg =
313
+ (event.error as { message?: string })?.message ??
314
+ JSON.stringify(event);
315
+ yield { type: "error", error: new Error(errMsg) };
316
+ break;
317
+ }
318
+
319
+ default:
320
+ // ping, unknown events — ignore
321
+ break;
322
+ }
323
+ }
324
+ } catch (err: unknown) {
325
+ if (err instanceof Error && err.name === "AbortError") {
326
+ yield { type: "abort", reason: err };
327
+ return;
328
+ }
329
+ yield { type: "error", error: err, recoverable: false };
330
+ return;
331
+ }
332
+
333
+ // Assemble final message
334
+ const contentParts: Content[] = [];
335
+ if (reasoningParts.length > 0) {
336
+ contentParts.push({ type: "reasoning", text: reasoningParts.join("") });
337
+ }
338
+ if (textParts.length > 0) {
339
+ contentParts.push({ type: "text", text: textParts.join("") });
340
+ }
341
+ for (const [, tool] of toolBlocks) {
342
+ let input: unknown = {};
343
+ try {
344
+ input = tool.inputJson ? JSON.parse(tool.inputJson) : {};
345
+ } catch {
346
+ input = tool.inputJson;
347
+ }
348
+ contentParts.push({
349
+ type: "tool-call",
350
+ toolCallId: tool.id,
351
+ toolName: tool.name,
352
+ input,
353
+ });
354
+ }
355
+
356
+ const finalUsage: Usage = {
357
+ inputTokens: usage.inputTokens,
358
+ outputTokens: usage.outputTokens,
359
+ totalTokens: usage.inputTokens + usage.outputTokens,
360
+ ...(usage.cachedInputTokens !== undefined
361
+ ? { cachedInputTokens: usage.cachedInputTokens }
362
+ : {}),
363
+ ...(usage.cacheCreationInputTokens !== undefined
364
+ ? { cacheCreationInputTokens: usage.cacheCreationInputTokens }
365
+ : {}),
366
+ };
367
+
368
+ const message: Message = { role: "assistant", content: contentParts };
369
+ yield { type: "message-stop", message };
370
+ yield { type: "finish", reason: finishReason, usage: finalUsage };
371
+ yield {
372
+ type: "response-stop",
373
+ meta: {
374
+ ...meta,
375
+ ...(responseId !== undefined ? { id: responseId } : {}),
376
+ endedAt: Date.now(),
377
+ },
378
+ usage: finalUsage,
379
+ finishReason,
380
+ };
381
+ };
382
+ }
@@ -0,0 +1,37 @@
1
+ /** ThinkingLevel → Anthropic thinking config. */
2
+
3
+ import type { ThinkingLevel } from "@dex-ai/sdk";
4
+
5
+ export interface AnthropicThinkingConfig {
6
+ type: "enabled";
7
+ budget_tokens: number;
8
+ }
9
+
10
+ /**
11
+ * Map SDK ThinkingLevel to Anthropic's thinking config object.
12
+ * Returns undefined when thinking is disabled (field should be omitted).
13
+ */
14
+ export function thinkingToAnthropicConfig(
15
+ thinking: ThinkingLevel | { readonly budgetTokens: number } | undefined,
16
+ maxTokens?: number,
17
+ ): AnthropicThinkingConfig | undefined {
18
+ if (thinking === undefined || thinking === "off") return undefined;
19
+
20
+ if (typeof thinking === "object" && "budgetTokens" in thinking) {
21
+ if (thinking.budgetTokens <= 0) return undefined;
22
+ return { type: "enabled", budget_tokens: thinking.budgetTokens };
23
+ }
24
+
25
+ const effective = maxTokens ?? 16384;
26
+ const pct: Record<ThinkingLevel, number> = {
27
+ off: 0,
28
+ min: 0.1,
29
+ low: 0.25,
30
+ med: 0.5,
31
+ high: 0.75,
32
+ max: 1.0,
33
+ };
34
+ const budget = Math.round(effective * (pct[thinking] ?? 0.5));
35
+ if (budget <= 0) return undefined;
36
+ return { type: "enabled", budget_tokens: budget };
37
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Translate SDK types ↔ Anthropic Messages API wire format.
3
+ */
4
+
5
+ import type {
6
+ Content,
7
+ Message,
8
+ ToolChoice,
9
+ ToolOutput,
10
+ AnyTool,
11
+ } from "@dex-ai/sdk";
12
+ import { resolveJsonSchema } from "@dex-ai/sdk";
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Anthropic wire types */
16
+ /* ------------------------------------------------------------------ */
17
+
18
+ export type AnthropicCacheControl = { type: "ephemeral" };
19
+
20
+ export type AnthropicContentBlock =
21
+ | { type: "text"; text: string; cache_control?: AnthropicCacheControl }
22
+ | {
23
+ type: "image";
24
+ source: { type: "base64"; media_type: string; data: string };
25
+ cache_control?: AnthropicCacheControl;
26
+ }
27
+ | { type: "tool_use"; id: string; name: string; input: unknown }
28
+ | {
29
+ type: "tool_result";
30
+ tool_use_id: string;
31
+ content: string;
32
+ is_error?: boolean;
33
+ };
34
+
35
+ export type AnthropicSystemBlock = {
36
+ type: "text";
37
+ text: string;
38
+ cache_control?: AnthropicCacheControl;
39
+ };
40
+
41
+ export interface AnthropicMessage {
42
+ role: "user" | "assistant";
43
+ content: AnthropicContentBlock[];
44
+ }
45
+
46
+ export interface AnthropicTool {
47
+ name: string;
48
+ description: string | undefined;
49
+ input_schema: unknown;
50
+ cache_control?: AnthropicCacheControl;
51
+ }
52
+
53
+ export type AnthropicToolChoice =
54
+ | { type: "auto" }
55
+ | { type: "any" }
56
+ | { type: "none" }
57
+ | { type: "tool"; name: string };
58
+
59
+ /* ------------------------------------------------------------------ */
60
+ /* SDK Content → Anthropic content block */
61
+ /* ------------------------------------------------------------------ */
62
+
63
+ function contentToAnthropic(c: Content): AnthropicContentBlock | null {
64
+ switch (c.type) {
65
+ case "text": {
66
+ const cc = (c as { cacheControl?: { type: string } }).cacheControl;
67
+ return {
68
+ type: "text",
69
+ text: c.text,
70
+ ...(cc ? { cache_control: { type: "ephemeral" as const } } : {}),
71
+ };
72
+ }
73
+ case "image": {
74
+ let b64: string;
75
+ if (c.image instanceof URL) {
76
+ // URLs not directly supported — would need to download
77
+ // For now, treat as empty placeholder
78
+ b64 = "";
79
+ } else if (typeof c.image === "string") {
80
+ b64 = c.image.startsWith("data:")
81
+ ? (c.image.split(",")[1] ?? c.image)
82
+ : c.image;
83
+ } else {
84
+ // c.image is typed as Uint8Array, but after JSON round-trip (session
85
+ // resume) it may be a plain object: {0: 72, 1: 101, ...} or
86
+ // {type: "Buffer", data: [...]}. Handle both cases.
87
+ const raw = c.image as unknown;
88
+ let buf: Buffer;
89
+ if (
90
+ ArrayBuffer.isView(raw) ||
91
+ raw instanceof ArrayBuffer ||
92
+ Array.isArray(raw)
93
+ ) {
94
+ buf = Buffer.from(raw as any);
95
+ } else if (
96
+ typeof raw === "object" &&
97
+ raw !== null &&
98
+ "type" in raw &&
99
+ (raw as any).type === "Buffer" &&
100
+ Array.isArray((raw as any).data)
101
+ ) {
102
+ // Deserialized Node Buffer: {type: "Buffer", data: [...]}
103
+ buf = Buffer.from((raw as any).data);
104
+ } else if (typeof raw === "object" && raw !== null) {
105
+ // Deserialized Uint8Array: plain object with numeric keys
106
+ const values = Object.values(raw as Record<string, number>);
107
+ buf = Buffer.from(values);
108
+ } else {
109
+ buf = Buffer.from(raw as any);
110
+ }
111
+ b64 = buf.toString("base64");
112
+ }
113
+ return {
114
+ type: "image",
115
+ source: {
116
+ type: "base64",
117
+ media_type: c.mediaType ?? "image/png",
118
+ data: b64,
119
+ },
120
+ };
121
+ }
122
+ case "tool-call":
123
+ return {
124
+ type: "tool_use",
125
+ id: c.toolCallId,
126
+ name: c.toolName,
127
+ input: c.input ?? {},
128
+ };
129
+ case "tool-result": {
130
+ const text = toolOutputToString(c.output);
131
+ const isError =
132
+ c.output.type === "error-text" || c.output.type === "error-json";
133
+ return {
134
+ type: "tool_result",
135
+ tool_use_id: c.toolCallId,
136
+ content: text,
137
+ ...(isError ? { is_error: true } : {}),
138
+ };
139
+ }
140
+ case "file": {
141
+ const label = c.name ?? "untitled";
142
+ return { type: "text", text: `[file: ${label} (${c.mediaType})]` };
143
+ }
144
+ case "reasoning":
145
+ // Skip reasoning in outbound messages — Anthropic handles it internally
146
+ return null;
147
+ default:
148
+ return null;
149
+ }
150
+ }
151
+
152
+ function toolOutputToString(output: ToolOutput): string {
153
+ switch (output.type) {
154
+ case "text":
155
+ case "error-text":
156
+ return typeof output.value === "string"
157
+ ? output.value
158
+ : JSON.stringify(output.value);
159
+ case "json":
160
+ case "error-json":
161
+ return JSON.stringify(output.value);
162
+ case "content": {
163
+ return output.value
164
+ .filter((p) => p.type === "text")
165
+ .map((p) => (p as { type: "text"; text: string }).text)
166
+ .join("\n");
167
+ }
168
+ default:
169
+ return "";
170
+ }
171
+ }
172
+
173
+ /* ------------------------------------------------------------------ */
174
+ /* SDK Message[] → Anthropic { system, messages } */
175
+ /* ------------------------------------------------------------------ */
176
+
177
+ export function messagesToAnthropic(
178
+ messages: ReadonlyArray<Message>,
179
+ opts?: { cacheBreakpoints?: ReadonlyArray<number> },
180
+ ): {
181
+ system: AnthropicSystemBlock[];
182
+ messages: AnthropicMessage[];
183
+ } {
184
+ const systemBlocks: AnthropicSystemBlock[] = [];
185
+ const out: AnthropicMessage[] = [];
186
+
187
+ // Track which SDK message index maps to which output message index.
188
+ // Needed to apply cacheBreakpoints to the correct output position.
189
+ const sdkIdxToOutIdx: number[] = []; // sdkIdxToOutIdx[sdkIdx] = outIdx
190
+
191
+ for (let sdkIdx = 0; sdkIdx < messages.length; sdkIdx++) {
192
+ const m = messages[sdkIdx]!;
193
+
194
+ if (m.role === "system") {
195
+ // Collect system messages into system blocks array
196
+ for (const c of m.content) {
197
+ if (c.type !== "text") continue;
198
+ const block: AnthropicSystemBlock = {
199
+ type: "text",
200
+ text: c.text,
201
+ ...((c as { cacheControl?: { type: string } }).cacheControl
202
+ ? { cache_control: { type: "ephemeral" as const } }
203
+ : {}),
204
+ };
205
+ systemBlocks.push(block);
206
+ }
207
+ sdkIdxToOutIdx.push(-1); // system messages don't map to out
208
+ continue;
209
+ }
210
+
211
+ // Map role: tool → user (tool_result goes inside user turn)
212
+ const role: "user" | "assistant" =
213
+ m.role === "assistant" ? "assistant" : "user";
214
+
215
+ // Convert content
216
+ const blocks: AnthropicContentBlock[] = [];
217
+ for (const c of m.content) {
218
+ const block = contentToAnthropic(c);
219
+ if (block) blocks.push(block);
220
+ }
221
+
222
+ if (blocks.length === 0) {
223
+ sdkIdxToOutIdx.push(-1);
224
+ continue;
225
+ }
226
+
227
+ // Anthropic requires alternating user/assistant. Merge consecutive same-role.
228
+ const last = out[out.length - 1];
229
+ if (last && last.role === role) {
230
+ last.content.push(...blocks);
231
+ sdkIdxToOutIdx.push(out.length - 1); // merged into existing
232
+ } else {
233
+ sdkIdxToOutIdx.push(out.length);
234
+ out.push({ role, content: blocks });
235
+ }
236
+ }
237
+
238
+ // Apply cache breakpoints to the last content block of marked messages
239
+ if (opts?.cacheBreakpoints && opts.cacheBreakpoints.length > 0) {
240
+ for (const breakIdx of opts.cacheBreakpoints) {
241
+ if (breakIdx < 0 || breakIdx >= sdkIdxToOutIdx.length) continue;
242
+ const outIdx = sdkIdxToOutIdx[breakIdx]!;
243
+ if (outIdx < 0) continue;
244
+ const msg = out[outIdx];
245
+ if (!msg || msg.content.length === 0) continue;
246
+
247
+ const lastBlock = msg.content[msg.content.length - 1]!;
248
+ msg.content[msg.content.length - 1] = {
249
+ ...lastBlock,
250
+ cache_control: { type: "ephemeral" },
251
+ } as AnthropicContentBlock;
252
+ }
253
+ }
254
+
255
+ return { system: systemBlocks, messages: out };
256
+ }
257
+
258
+ /* ------------------------------------------------------------------ */
259
+ /* SDK Tool → Anthropic tool */
260
+ /* ------------------------------------------------------------------ */
261
+
262
+ export function toolToAnthropic(tool: AnyTool): AnthropicTool {
263
+ const schema = tool.parameters;
264
+ const jsonSchema = schema
265
+ ? resolveJsonSchema(schema)
266
+ : { type: "object", properties: {} };
267
+
268
+ return {
269
+ name: tool.name,
270
+ description: tool.description,
271
+ input_schema: jsonSchema,
272
+ };
273
+ }
274
+
275
+ /* ------------------------------------------------------------------ */
276
+ /* SDK ToolChoice → Anthropic tool_choice */
277
+ /* ------------------------------------------------------------------ */
278
+
279
+ export function toolChoiceToAnthropic(
280
+ choice: ToolChoice | undefined,
281
+ ): AnthropicToolChoice | undefined {
282
+ if (choice === undefined) return undefined;
283
+ if (choice === "auto") return { type: "auto" };
284
+ if (choice === "required") return { type: "any" };
285
+ if (choice === "none") return { type: "none" };
286
+ if (typeof choice === "object" && "toolName" in choice) {
287
+ return { type: "tool", name: choice.toolName };
288
+ }
289
+ return undefined;
290
+ }