@codeworksh/aikit 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -81
- package/{agent → dist/pack/agent}/codemode/drivers/drivers.d.mts +1 -1
- package/dist/pack/codemode-BEDXG7WN.d.mts +8736 -0
- package/{index.d.mts → dist/pack/index.d.mts} +1 -1
- package/dist/pack/index.mjs +8226 -0
- package/dist/pack/index.mjs.map +1 -0
- package/package.json +31 -24
- package/src/agent/agent.ts +790 -0
- package/src/agent/codemode/codemode.ts +561 -0
- package/src/agent/codemode/drivers/drivers.ts +1 -0
- package/src/agent/codemode/drivers/quickjs-wasi-driver.ts +149 -0
- package/src/agent/codemode/index.ts +1 -0
- package/src/agent/loop.ts +903 -0
- package/src/agent.ts +11 -0
- package/src/event/event.ts +277 -0
- package/src/index.ts +6 -0
- package/src/llm.ts +31 -0
- package/src/message/message.ts +311 -0
- package/src/model/catalog.ts +94 -0
- package/src/model/model.ts +250 -0
- package/src/model/transform.ts +59 -0
- package/src/provider/provider.ts +60 -0
- package/src/provider/providers/anthropic/index.ts +792 -0
- package/src/provider/providers/github/github.ts +34 -0
- package/src/provider/providers/openai/completions.ts +953 -0
- package/src/provider/providers/utils.ts +7 -0
- package/src/provider/register.ts +6 -0
- package/src/provider/stream.ts +278 -0
- package/src/stream.ts +72 -0
- package/src/utils/eventstream.ts +95 -0
- package/src/utils/helpers.ts +24 -0
- package/src/utils/jsonparse.ts +32 -0
- package/src/utils/overflow.ts +121 -0
- package/src/utils/sanitize.ts +25 -0
- package/src/utils/validation.ts +87 -0
- package/LICENSE +0 -21
- package/codemode-SiETGR3z.d.mts +0 -8410
- package/index.mjs +0 -3381
- package/index.mjs.map +0 -1
- /package/{agent → dist/pack/agent}/codemode/drivers/drivers.mjs +0 -0
- /package/{agent → dist/pack/agent}/codemode/drivers/drivers.mjs.map +0 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import type {
|
|
3
|
+
ContentBlockParam,
|
|
4
|
+
MessageCreateParamsStreaming,
|
|
5
|
+
MessageParam,
|
|
6
|
+
} from "@anthropic-ai/sdk/resources/messages.js";
|
|
7
|
+
import { Message } from "../../../message/message";
|
|
8
|
+
import { Model } from "../../../model/model";
|
|
9
|
+
import { AssistantMessageEventStream } from "../../../utils/eventstream";
|
|
10
|
+
import { parseStreamingJson } from "../../../utils/jsonparse";
|
|
11
|
+
import { sanitizeSurrogates } from "../../../utils/sanitize";
|
|
12
|
+
import { Stream } from "../../stream";
|
|
13
|
+
import { getEnvApiKey } from "../utils";
|
|
14
|
+
|
|
15
|
+
export interface AnthropicOptions extends Stream.Options {
|
|
16
|
+
thinkingEnabled?: boolean;
|
|
17
|
+
thinkingBudgetTokens?: number;
|
|
18
|
+
interleavedThinking?: boolean;
|
|
19
|
+
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
|
|
20
|
+
client?: Anthropic;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveCacheRetention(cacheRetention?: Stream.CacheRetention): Stream.CacheRetention {
|
|
24
|
+
if (cacheRetention) {
|
|
25
|
+
return cacheRetention;
|
|
26
|
+
}
|
|
27
|
+
if (typeof process !== "undefined" && process.env.CODEWORK_CACHE_RETENTION === "long") {
|
|
28
|
+
return "long";
|
|
29
|
+
}
|
|
30
|
+
return "short";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCacheControl(
|
|
34
|
+
baseUrl: string,
|
|
35
|
+
cacheRetention?: Stream.CacheRetention,
|
|
36
|
+
): {
|
|
37
|
+
retention: Stream.CacheRetention;
|
|
38
|
+
cacheControl?: { type: "ephemeral"; ttl?: "1h" };
|
|
39
|
+
} {
|
|
40
|
+
const retention = resolveCacheRetention(cacheRetention);
|
|
41
|
+
if (retention === "none") {
|
|
42
|
+
return { retention };
|
|
43
|
+
}
|
|
44
|
+
const ttl = retention === "long" && baseUrl.includes("api.anthropic.com") ? "1h" : undefined;
|
|
45
|
+
return {
|
|
46
|
+
retention,
|
|
47
|
+
cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const claudeCodeVersion = "2.1.2";
|
|
52
|
+
const claudeCodeTools = [
|
|
53
|
+
"Read",
|
|
54
|
+
"Write",
|
|
55
|
+
"Edit",
|
|
56
|
+
"Bash",
|
|
57
|
+
"Grep",
|
|
58
|
+
"Glob",
|
|
59
|
+
"AskUserQuestion",
|
|
60
|
+
"EnterPlanMode",
|
|
61
|
+
"ExitPlanMode",
|
|
62
|
+
"KillShell",
|
|
63
|
+
"NotebookEdit",
|
|
64
|
+
"Skill",
|
|
65
|
+
"Task",
|
|
66
|
+
"TaskOutput",
|
|
67
|
+
"TodoWrite",
|
|
68
|
+
"WebFetch",
|
|
69
|
+
"WebSearch",
|
|
70
|
+
];
|
|
71
|
+
const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));
|
|
72
|
+
|
|
73
|
+
const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;
|
|
74
|
+
const fromClaudeCodeName = (name: string, tools?: Message.Tool[]) => {
|
|
75
|
+
if (tools && tools.length > 0) {
|
|
76
|
+
const lowerName = name.toLowerCase();
|
|
77
|
+
const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);
|
|
78
|
+
if (matchedTool) return matchedTool.name;
|
|
79
|
+
}
|
|
80
|
+
return name;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function convertContentBlocks(content: (Message.TextContent | Message.ImageContent)[]):
|
|
84
|
+
| string
|
|
85
|
+
| Array<
|
|
86
|
+
| { type: "text"; text: string }
|
|
87
|
+
| {
|
|
88
|
+
type: "image";
|
|
89
|
+
source: {
|
|
90
|
+
type: "base64";
|
|
91
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
92
|
+
data: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
> {
|
|
96
|
+
const hasImages = content.some((c) => c.type === "image");
|
|
97
|
+
if (!hasImages) {
|
|
98
|
+
return sanitizeSurrogates(content.map((c) => (c as Message.TextContent).text).join("\n"));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const blocks = content.map((block) => {
|
|
102
|
+
if (block.type === "text") {
|
|
103
|
+
return {
|
|
104
|
+
type: "text" as const,
|
|
105
|
+
text: sanitizeSurrogates(block.text),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
type: "image" as const,
|
|
110
|
+
source: {
|
|
111
|
+
type: "base64" as const,
|
|
112
|
+
media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
113
|
+
data: block.data,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!blocks.some((b) => b.type === "text")) {
|
|
119
|
+
blocks.unshift({
|
|
120
|
+
type: "text" as const,
|
|
121
|
+
text: "(see attached image)",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return blocks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
|
|
129
|
+
const merged: Record<string, string> = {};
|
|
130
|
+
for (const headers of headerSources) {
|
|
131
|
+
if (headers) {
|
|
132
|
+
Object.assign(merged, headers);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return merged;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isOAuthToken(apiKey: string): boolean {
|
|
139
|
+
return apiKey.includes("sk-ant-oat");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createClient(
|
|
143
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.anthropicMessages>,
|
|
144
|
+
apiKey: string,
|
|
145
|
+
interleavedThinking: boolean,
|
|
146
|
+
optionsHeaders?: Record<string, string>,
|
|
147
|
+
): { client: Anthropic; isOAuthToken: boolean } {
|
|
148
|
+
const betaFeatures = ["fine-grained-tool-streaming-2025-05-14"];
|
|
149
|
+
if (interleavedThinking) {
|
|
150
|
+
betaFeatures.push("interleaved-thinking-2025-05-14");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const oauthToken = isOAuthToken(apiKey);
|
|
154
|
+
if (oauthToken) {
|
|
155
|
+
const defaultHeaders = mergeHeaders(
|
|
156
|
+
{
|
|
157
|
+
accept: "application/json",
|
|
158
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
159
|
+
"anthropic-beta": `claude-code-20250219,oauth-2025-04-20,${betaFeatures.join(",")}`,
|
|
160
|
+
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
|
161
|
+
"x-app": "cli",
|
|
162
|
+
},
|
|
163
|
+
model.headers,
|
|
164
|
+
optionsHeaders,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
client: new Anthropic({
|
|
169
|
+
apiKey: null,
|
|
170
|
+
authToken: apiKey,
|
|
171
|
+
baseURL: model.baseUrl,
|
|
172
|
+
defaultHeaders,
|
|
173
|
+
dangerouslyAllowBrowser: true,
|
|
174
|
+
}),
|
|
175
|
+
isOAuthToken: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const defaultHeaders = mergeHeaders(
|
|
180
|
+
{
|
|
181
|
+
accept: "application/json",
|
|
182
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
183
|
+
"anthropic-beta": betaFeatures.join(","),
|
|
184
|
+
},
|
|
185
|
+
model.headers,
|
|
186
|
+
optionsHeaders,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
client: new Anthropic({
|
|
191
|
+
apiKey,
|
|
192
|
+
baseURL: model.baseUrl,
|
|
193
|
+
dangerouslyAllowBrowser: true,
|
|
194
|
+
defaultHeaders,
|
|
195
|
+
}),
|
|
196
|
+
isOAuthToken: false,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeToolCallId(id: string): string {
|
|
201
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function assertReplayableAssistant(message: Message.AssistantMessage): void {
|
|
205
|
+
let sawToolCall = false;
|
|
206
|
+
let sawNonTerminalToolCall = false;
|
|
207
|
+
let sawTerminalToolCall = false;
|
|
208
|
+
|
|
209
|
+
for (const part of message.parts) {
|
|
210
|
+
if (part.type === "image") {
|
|
211
|
+
throw new Error("Anthropic replay does not support assistant image parts");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (part.type === "toolCall") {
|
|
215
|
+
sawToolCall = true;
|
|
216
|
+
if (part.status === "pending" || part.status === "running") {
|
|
217
|
+
sawNonTerminalToolCall = true;
|
|
218
|
+
} else {
|
|
219
|
+
sawTerminalToolCall = true;
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (sawToolCall) {
|
|
225
|
+
throw new Error("Assistant parts after a toolCall are not replayable for Anthropic");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (sawToolCall && message.stopReason !== "toolUse") {
|
|
230
|
+
throw new Error("Assistant message with tool calls must have stopReason 'toolUse'");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!sawToolCall && message.stopReason === "toolUse") {
|
|
234
|
+
throw new Error("Assistant message with stopReason 'toolUse' must contain at least one toolCall");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (sawTerminalToolCall && sawNonTerminalToolCall) {
|
|
238
|
+
throw new Error("Assistant message cannot mix terminal and non-terminal toolCall states");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildSyntheticToolResult(
|
|
243
|
+
block: Extract<Message.ToolCall, { status: "pending" | "running" }>,
|
|
244
|
+
): ContentBlockParam {
|
|
245
|
+
return {
|
|
246
|
+
type: "tool_result",
|
|
247
|
+
tool_use_id: block.callID,
|
|
248
|
+
content: convertContentBlocks([
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: ["<result>", "No Result Provided", "</result>"].join("\n"),
|
|
252
|
+
},
|
|
253
|
+
]),
|
|
254
|
+
is_error: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function convertMessages(
|
|
259
|
+
messages: Message.Message[],
|
|
260
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.anthropicMessages>,
|
|
261
|
+
isOAuthToken: boolean,
|
|
262
|
+
cacheControl?: { type: "ephemeral"; ttl?: "1h" },
|
|
263
|
+
): MessageParam[] {
|
|
264
|
+
const params: MessageParam[] = [];
|
|
265
|
+
const transformedMessages = Message.transformMessages(messages, model, normalizeToolCallId);
|
|
266
|
+
|
|
267
|
+
for (const msg of transformedMessages) {
|
|
268
|
+
if (msg.role === "user") {
|
|
269
|
+
const blocks: ContentBlockParam[] = msg.parts.map((item) => {
|
|
270
|
+
if (item.type === "text") {
|
|
271
|
+
return {
|
|
272
|
+
type: "text",
|
|
273
|
+
text: sanitizeSurrogates(item.text),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
type: "image",
|
|
278
|
+
source: {
|
|
279
|
+
type: "base64",
|
|
280
|
+
media_type: item.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
281
|
+
data: item.data,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
let filteredBlocks = !model.input.includes("image") ? blocks.filter((b) => b.type !== "image") : blocks;
|
|
286
|
+
filteredBlocks = filteredBlocks.filter((b) => (b.type === "text" ? b.text.trim().length > 0 : true));
|
|
287
|
+
if (filteredBlocks.length === 0) continue;
|
|
288
|
+
params.push({
|
|
289
|
+
role: "user",
|
|
290
|
+
content: filteredBlocks,
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
assertReplayableAssistant(msg);
|
|
300
|
+
|
|
301
|
+
const assistantBlocks: ContentBlockParam[] = [];
|
|
302
|
+
const toolResults: ContentBlockParam[] = [];
|
|
303
|
+
|
|
304
|
+
for (const block of msg.parts) {
|
|
305
|
+
if (block.type === "text") {
|
|
306
|
+
if (block.text.trim().length === 0) continue;
|
|
307
|
+
assistantBlocks.push({
|
|
308
|
+
type: "text",
|
|
309
|
+
text: sanitizeSurrogates(block.text),
|
|
310
|
+
});
|
|
311
|
+
} else if (block.type === "thinking") {
|
|
312
|
+
if (block.thinking.trim().length === 0) continue;
|
|
313
|
+
if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
|
|
314
|
+
assistantBlocks.push({
|
|
315
|
+
type: "text",
|
|
316
|
+
text: sanitizeSurrogates(block.thinking),
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
assistantBlocks.push({
|
|
320
|
+
type: "thinking",
|
|
321
|
+
thinking: sanitizeSurrogates(block.thinking),
|
|
322
|
+
signature: block.thinkingSignature,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
} else if (block.type === "toolCall") {
|
|
326
|
+
assistantBlocks.push({
|
|
327
|
+
type: "tool_use",
|
|
328
|
+
id: block.callID,
|
|
329
|
+
name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
|
|
330
|
+
input: block.arguments ?? {},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (block.status === "pending" || block.status === "running") {
|
|
334
|
+
toolResults.push(buildSyntheticToolResult(block));
|
|
335
|
+
} else {
|
|
336
|
+
toolResults.push({
|
|
337
|
+
type: "tool_result",
|
|
338
|
+
tool_use_id: block.callID,
|
|
339
|
+
content: convertContentBlocks(block.result.content),
|
|
340
|
+
is_error: block.result.isError,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (assistantBlocks.length > 0) {
|
|
347
|
+
params.push({
|
|
348
|
+
role: "assistant",
|
|
349
|
+
content: assistantBlocks,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (toolResults.length > 0) {
|
|
354
|
+
params.push({
|
|
355
|
+
role: "user",
|
|
356
|
+
content: toolResults,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (cacheControl && params.length > 0) {
|
|
362
|
+
const lastMessage = params[params.length - 1]!;
|
|
363
|
+
if (lastMessage.role === "user") {
|
|
364
|
+
if (Array.isArray(lastMessage.content)) {
|
|
365
|
+
const lastBlock = lastMessage.content[lastMessage.content.length - 1];
|
|
366
|
+
if (
|
|
367
|
+
lastBlock &&
|
|
368
|
+
(lastBlock.type === "text" || lastBlock.type === "image" || lastBlock.type === "tool_result")
|
|
369
|
+
) {
|
|
370
|
+
(lastBlock as { cache_control?: { type: "ephemeral"; ttl?: "1h" } }).cache_control = cacheControl;
|
|
371
|
+
}
|
|
372
|
+
} else if (typeof lastMessage.content === "string") {
|
|
373
|
+
lastMessage.content = [
|
|
374
|
+
{
|
|
375
|
+
type: "text",
|
|
376
|
+
text: lastMessage.content,
|
|
377
|
+
cache_control: cacheControl,
|
|
378
|
+
},
|
|
379
|
+
] as any;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return params;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function convertTools(tools: Message.Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
|
|
388
|
+
if (!tools) return [];
|
|
389
|
+
|
|
390
|
+
return tools.map((tool) => {
|
|
391
|
+
const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,
|
|
395
|
+
description: tool.description,
|
|
396
|
+
input_schema: {
|
|
397
|
+
type: "object" as const,
|
|
398
|
+
properties: jsonSchema.properties || {},
|
|
399
|
+
required: jsonSchema.required || [],
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type AnthropicStopReason = Anthropic.Messages.StopReason | "sensitive";
|
|
406
|
+
|
|
407
|
+
function mapStopReason(reason: AnthropicStopReason): Message.StopReason {
|
|
408
|
+
switch (reason) {
|
|
409
|
+
case "end_turn":
|
|
410
|
+
return "stop";
|
|
411
|
+
case "max_tokens":
|
|
412
|
+
return "length";
|
|
413
|
+
case "tool_use":
|
|
414
|
+
return "toolUse";
|
|
415
|
+
case "pause_turn":
|
|
416
|
+
case "stop_sequence":
|
|
417
|
+
return "stop";
|
|
418
|
+
case "refusal":
|
|
419
|
+
case "sensitive":
|
|
420
|
+
return "error";
|
|
421
|
+
default:
|
|
422
|
+
throw new Error("Unhandled Anthropic stop reason");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function buildParams(
|
|
427
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.anthropicMessages>,
|
|
428
|
+
context: Message.Context,
|
|
429
|
+
isOAuthToken: boolean,
|
|
430
|
+
options?: AnthropicOptions,
|
|
431
|
+
): MessageCreateParamsStreaming {
|
|
432
|
+
const { cacheControl } = getCacheControl(model.baseUrl, options?.cacheRetention);
|
|
433
|
+
const params: MessageCreateParamsStreaming = {
|
|
434
|
+
model: model.id,
|
|
435
|
+
messages: convertMessages(context.messages, model, isOAuthToken, cacheControl),
|
|
436
|
+
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
|
|
437
|
+
stream: true,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if (isOAuthToken) {
|
|
441
|
+
params.system = [
|
|
442
|
+
{
|
|
443
|
+
type: "text",
|
|
444
|
+
text: "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
445
|
+
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
446
|
+
},
|
|
447
|
+
];
|
|
448
|
+
if (context.systemPrompt) {
|
|
449
|
+
params.system.push({
|
|
450
|
+
type: "text",
|
|
451
|
+
text: sanitizeSurrogates(context.systemPrompt),
|
|
452
|
+
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
} else if (context.systemPrompt) {
|
|
456
|
+
params.system = [
|
|
457
|
+
{
|
|
458
|
+
type: "text",
|
|
459
|
+
text: sanitizeSurrogates(context.systemPrompt),
|
|
460
|
+
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (options?.temperature !== undefined && !options?.thinkingEnabled) {
|
|
466
|
+
params.temperature = options.temperature;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (context.tools) {
|
|
470
|
+
params.tools = convertTools(context.tools, isOAuthToken);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (options?.thinkingEnabled && model.reasoning) {
|
|
474
|
+
params.thinking = {
|
|
475
|
+
type: "enabled",
|
|
476
|
+
budget_tokens: options.thinkingBudgetTokens || 1024,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (options?.metadata) {
|
|
481
|
+
const userId = options.metadata.user_id;
|
|
482
|
+
if (typeof userId === "string") {
|
|
483
|
+
params.metadata = { user_id: userId };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (options?.toolChoice) {
|
|
488
|
+
params.tool_choice = typeof options.toolChoice === "string" ? { type: options.toolChoice } : options.toolChoice;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return params;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
type Block = (Message.ThinkingContent | Message.TextContent | (Message.ToolCall & { partialJson: string })) & {
|
|
495
|
+
index: number;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
export const streamAnthropic: Stream.StreamFunction<
|
|
499
|
+
typeof Model.KnownProtocolEnum.anthropicMessages,
|
|
500
|
+
AnthropicOptions
|
|
501
|
+
> = (
|
|
502
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.anthropicMessages>,
|
|
503
|
+
context: Message.Context,
|
|
504
|
+
options?: AnthropicOptions,
|
|
505
|
+
): AssistantMessageEventStream => {
|
|
506
|
+
const stream = new AssistantMessageEventStream();
|
|
507
|
+
|
|
508
|
+
void (async () => {
|
|
509
|
+
const output = Message.createAssistantMessage({
|
|
510
|
+
role: "assistant",
|
|
511
|
+
parts: [],
|
|
512
|
+
protocol: model.protocol,
|
|
513
|
+
provider: model.provider,
|
|
514
|
+
model: model.id,
|
|
515
|
+
usage: {
|
|
516
|
+
input: 0,
|
|
517
|
+
output: 0,
|
|
518
|
+
cacheRead: 0,
|
|
519
|
+
cacheWrite: 0,
|
|
520
|
+
totalTokens: 0,
|
|
521
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
522
|
+
},
|
|
523
|
+
stopReason: "stop",
|
|
524
|
+
time: {
|
|
525
|
+
created: Date.now(),
|
|
526
|
+
completed: Date.now(),
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
let client: Anthropic;
|
|
532
|
+
let isOAuthToken: boolean;
|
|
533
|
+
|
|
534
|
+
if (options?.client) {
|
|
535
|
+
client = options.client;
|
|
536
|
+
isOAuthToken = false;
|
|
537
|
+
} else {
|
|
538
|
+
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider);
|
|
539
|
+
if (!apiKey) {
|
|
540
|
+
throw new Error(`No API key for provider: ${model.provider.id}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const created = createClient(model, apiKey, options?.interleavedThinking ?? true, options?.headers);
|
|
544
|
+
client = created.client;
|
|
545
|
+
isOAuthToken = created.isOAuthToken;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let params = buildParams(model, context, isOAuthToken, options);
|
|
549
|
+
const nextParams = await options?.onPayload?.(params, model);
|
|
550
|
+
if (nextParams !== undefined) {
|
|
551
|
+
params = nextParams as MessageCreateParamsStreaming;
|
|
552
|
+
}
|
|
553
|
+
const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal });
|
|
554
|
+
stream.push({ type: "start", partial: output });
|
|
555
|
+
|
|
556
|
+
const blocks = output.parts as Block[];
|
|
557
|
+
|
|
558
|
+
for await (const event of anthropicStream) {
|
|
559
|
+
if (event.type === "message_start") {
|
|
560
|
+
output.responseId = event.message.id;
|
|
561
|
+
// Capture initial token usage from message_start event
|
|
562
|
+
// This ensures we have input token counts even if the stream is aborted early
|
|
563
|
+
output.usage.input = event.message.usage.input_tokens || 0;
|
|
564
|
+
output.usage.output = event.message.usage.output_tokens || 0;
|
|
565
|
+
output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;
|
|
566
|
+
output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;
|
|
567
|
+
// Anthropic doesn't provide total_tokens, compute from components
|
|
568
|
+
output.usage.totalTokens =
|
|
569
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
570
|
+
Model.calculateCost(model, output.usage);
|
|
571
|
+
} else if (event.type === "content_block_start") {
|
|
572
|
+
if (event.content_block.type === "text") {
|
|
573
|
+
const block: Block = { type: "text", text: "", index: event.index };
|
|
574
|
+
output.parts.push(block); // push the block to parts
|
|
575
|
+
// `contentIndex` points to the index of the block within parts
|
|
576
|
+
// Useful for replaying message and corresponding parts based on stream.
|
|
577
|
+
stream.push({
|
|
578
|
+
type: "text.start",
|
|
579
|
+
partIndex: output.parts.length - 1,
|
|
580
|
+
partial: output,
|
|
581
|
+
});
|
|
582
|
+
} else if (event.content_block.type === "thinking") {
|
|
583
|
+
const block: Block = {
|
|
584
|
+
type: "thinking",
|
|
585
|
+
thinking: "",
|
|
586
|
+
thinkingSignature: "",
|
|
587
|
+
index: event.index,
|
|
588
|
+
};
|
|
589
|
+
output.parts.push(block);
|
|
590
|
+
stream.push({
|
|
591
|
+
type: "thinking.start",
|
|
592
|
+
partIndex: output.parts.length - 1,
|
|
593
|
+
partial: output,
|
|
594
|
+
});
|
|
595
|
+
} else if (event.content_block.type === "redacted_thinking") {
|
|
596
|
+
const block: Block = {
|
|
597
|
+
type: "thinking",
|
|
598
|
+
thinking: "[Reasoning redacted]",
|
|
599
|
+
thinkingSignature: event.content_block.data,
|
|
600
|
+
redacted: true,
|
|
601
|
+
index: event.index,
|
|
602
|
+
};
|
|
603
|
+
output.parts.push(block);
|
|
604
|
+
stream.push({
|
|
605
|
+
type: "thinking.start",
|
|
606
|
+
partIndex: output.parts.length - 1,
|
|
607
|
+
partial: output,
|
|
608
|
+
});
|
|
609
|
+
} else if (event.content_block.type === "tool_use") {
|
|
610
|
+
const block: Block = {
|
|
611
|
+
type: "toolCall",
|
|
612
|
+
callID: event.content_block.id,
|
|
613
|
+
name: isOAuthToken
|
|
614
|
+
? fromClaudeCodeName(event.content_block.name, context.tools)
|
|
615
|
+
: event.content_block.name,
|
|
616
|
+
arguments: (event.content_block.input as Record<string, any>) ?? {},
|
|
617
|
+
partialJson: "",
|
|
618
|
+
status: "pending",
|
|
619
|
+
time: {
|
|
620
|
+
start: Date.now(),
|
|
621
|
+
end: Date.now(),
|
|
622
|
+
},
|
|
623
|
+
index: event.index,
|
|
624
|
+
};
|
|
625
|
+
output.parts.push(block);
|
|
626
|
+
stream.push({
|
|
627
|
+
type: "toolcall.start",
|
|
628
|
+
partIndex: output.parts.length - 1,
|
|
629
|
+
partial: output,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
} else if (event.type === "content_block_delta") {
|
|
633
|
+
// *_delta updates existing block part
|
|
634
|
+
// filtered based on event identifier `event.index` (not to confuse with array indices)
|
|
635
|
+
// index persisted on block(part) with `index`
|
|
636
|
+
const partIndex = blocks.findIndex((b) => b.index === event.index);
|
|
637
|
+
const block = blocks[partIndex];
|
|
638
|
+
if (!block) continue; // block doesn't exist for delta, move on
|
|
639
|
+
|
|
640
|
+
if (event.delta.type === "text_delta" && block.type === "text") {
|
|
641
|
+
block.text += event.delta.text; // mutates block, implicitly mutating block in output.parts
|
|
642
|
+
stream.push({
|
|
643
|
+
type: "text.delta",
|
|
644
|
+
partIndex: partIndex,
|
|
645
|
+
delta: event.delta.text,
|
|
646
|
+
partial: output,
|
|
647
|
+
});
|
|
648
|
+
} else if (event.delta.type === "thinking_delta" && block.type === "thinking") {
|
|
649
|
+
block.thinking += event.delta.thinking;
|
|
650
|
+
stream.push({
|
|
651
|
+
type: "thinking.delta",
|
|
652
|
+
partIndex: partIndex,
|
|
653
|
+
delta: event.delta.thinking,
|
|
654
|
+
partial: output,
|
|
655
|
+
});
|
|
656
|
+
} else if (event.delta.type === "input_json_delta" && block.type === "toolCall") {
|
|
657
|
+
block.partialJson += event.delta.partial_json;
|
|
658
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
659
|
+
stream.push({
|
|
660
|
+
type: "toolcall.delta",
|
|
661
|
+
partIndex: partIndex,
|
|
662
|
+
delta: event.delta.partial_json,
|
|
663
|
+
partial: output,
|
|
664
|
+
});
|
|
665
|
+
} else if (event.delta.type === "signature_delta" && block.type === "thinking") {
|
|
666
|
+
block.thinkingSignature = block.thinkingSignature || "";
|
|
667
|
+
block.thinkingSignature += event.delta.signature;
|
|
668
|
+
}
|
|
669
|
+
} else if (event.type === "content_block_stop") {
|
|
670
|
+
const partIndex = blocks.findIndex((b) => b.index === event.index);
|
|
671
|
+
const block = blocks[partIndex];
|
|
672
|
+
if (block) {
|
|
673
|
+
delete (block as any).index; // clear block index; used for internal loop state
|
|
674
|
+
if (block.type === "text") {
|
|
675
|
+
stream.push({
|
|
676
|
+
type: "text.end",
|
|
677
|
+
partIndex: partIndex,
|
|
678
|
+
content: block.text,
|
|
679
|
+
partial: output,
|
|
680
|
+
});
|
|
681
|
+
} else if (block.type === "thinking") {
|
|
682
|
+
stream.push({
|
|
683
|
+
type: "thinking.end",
|
|
684
|
+
partIndex: partIndex,
|
|
685
|
+
content: block.thinking,
|
|
686
|
+
partial: output,
|
|
687
|
+
});
|
|
688
|
+
} else if (block.type === "toolCall") {
|
|
689
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
690
|
+
delete (block as any).partialJson; // clear partialJson; used for internal loop state
|
|
691
|
+
stream.push({
|
|
692
|
+
type: "toolcall.end",
|
|
693
|
+
partIndex: partIndex,
|
|
694
|
+
toolCall: block,
|
|
695
|
+
partial: output,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} else if (event.type === "message_delta") {
|
|
700
|
+
if (event.delta.stop_reason) {
|
|
701
|
+
output.stopReason = mapStopReason(event.delta.stop_reason);
|
|
702
|
+
}
|
|
703
|
+
// Only update usage fields if present (not null).
|
|
704
|
+
// Preserves input_tokens from message_start when proxies omit it in message_delta.
|
|
705
|
+
if (event.usage.input_tokens != null) {
|
|
706
|
+
output.usage.input = event.usage.input_tokens;
|
|
707
|
+
}
|
|
708
|
+
if (event.usage.output_tokens != null) {
|
|
709
|
+
output.usage.output = event.usage.output_tokens;
|
|
710
|
+
}
|
|
711
|
+
if (event.usage.cache_read_input_tokens != null) {
|
|
712
|
+
output.usage.cacheRead = event.usage.cache_read_input_tokens;
|
|
713
|
+
}
|
|
714
|
+
if (event.usage.cache_creation_input_tokens != null) {
|
|
715
|
+
output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
|
|
716
|
+
}
|
|
717
|
+
// Anthropic doesn't provide total_tokens, compute from components
|
|
718
|
+
output.usage.totalTokens =
|
|
719
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
720
|
+
Model.calculateCost(model, output.usage);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (options?.signal?.aborted) {
|
|
725
|
+
throw new Error("Request was aborted");
|
|
726
|
+
}
|
|
727
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
728
|
+
throw new Error("An unknown error occurred");
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
output.time.completed = Date.now();
|
|
732
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
733
|
+
stream.end();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
for (const block of output.parts) delete (block as { index?: number }).index;
|
|
736
|
+
output.time.completed = Date.now();
|
|
737
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
738
|
+
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
739
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
740
|
+
stream.end();
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
|
|
744
|
+
return stream;
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
export const streamSimpleAnthropic: Stream.StreamFunction<
|
|
748
|
+
typeof Model.KnownProtocolEnum.anthropicMessages,
|
|
749
|
+
Stream.SimpleOptions
|
|
750
|
+
> = (
|
|
751
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.anthropicMessages>,
|
|
752
|
+
context: Message.Context,
|
|
753
|
+
options?: Stream.SimpleOptions,
|
|
754
|
+
): AssistantMessageEventStream => {
|
|
755
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
|
|
756
|
+
if (!apiKey) {
|
|
757
|
+
throw new Error(`No API key for provider: ${model.provider.id}`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const base = Stream.buildBaseOptions(model, options, apiKey);
|
|
761
|
+
if (!options?.reasoning) {
|
|
762
|
+
return streamAnthropic(model, context, {
|
|
763
|
+
...base,
|
|
764
|
+
thinkingEnabled: false,
|
|
765
|
+
} satisfies AnthropicOptions);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const adjusted = Stream.adjustMaxTokensForThinking(
|
|
769
|
+
base.maxTokens || 0,
|
|
770
|
+
model.maxTokens,
|
|
771
|
+
options.reasoning,
|
|
772
|
+
options.thinkingBudgets,
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
return streamAnthropic(model, context, {
|
|
776
|
+
...base,
|
|
777
|
+
maxTokens: adjusted.maxTokens,
|
|
778
|
+
thinkingEnabled: true,
|
|
779
|
+
thinkingBudgetTokens: adjusted.thinkingBudget,
|
|
780
|
+
} satisfies AnthropicOptions);
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const anthropicProtocolProvider: Stream.ProtocolProvider<
|
|
784
|
+
typeof Model.KnownProtocolEnum.anthropicMessages,
|
|
785
|
+
AnthropicOptions
|
|
786
|
+
> = {
|
|
787
|
+
protocol: Model.KnownProtocolEnum.anthropicMessages,
|
|
788
|
+
stream: streamAnthropic,
|
|
789
|
+
streamSimple: streamSimpleAnthropic,
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
export default anthropicProtocolProvider;
|