@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 +33 -0
- package/src/errors.ts +48 -0
- package/src/extension.ts +213 -0
- package/src/index.ts +25 -0
- package/src/sse.ts +35 -0
- package/src/stream.ts +382 -0
- package/src/thinking.ts +37 -0
- package/src/translate.ts +290 -0
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
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -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
|
+
}
|
package/src/thinking.ts
ADDED
|
@@ -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
|
+
}
|
package/src/translate.ts
ADDED
|
@@ -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
|
+
}
|