@arcote.tech/arc-ai-claude 0.4.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,19 @@
1
+ {
2
+ "name": "@arcote.tech/arc-ai-claude",
3
+ "type": "module",
4
+ "version": "0.4.6",
5
+ "private": false,
6
+ "description": "Claude (Anthropic) adapter for Arc AI framework",
7
+ "main": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "scripts": {
10
+ "type-check": "tsc --noEmit"
11
+ },
12
+ "peerDependencies": {
13
+ "@arcote.tech/arc-ai": "^0.4.6",
14
+ "typescript": "^5.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "latest"
18
+ }
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,308 @@
1
+ import type {
2
+ LLMProvider,
3
+ CompletionRequest,
4
+ CompletionResult,
5
+ StreamChunk,
6
+ ToolCall,
7
+ TokenUsage,
8
+ FinishReason,
9
+ } from "@arcote.tech/arc-ai";
10
+
11
+ // ─── Config ──────────────────────────────────────────────────────
12
+
13
+ export interface ClaudeConfig {
14
+ apiKey: string;
15
+ apiVersion?: string;
16
+ }
17
+
18
+ // ─── Adapter ─────────────────────────────────────────────────────
19
+
20
+ export function claude(config: ClaudeConfig): LLMProvider {
21
+ const apiVersion = config.apiVersion ?? "2023-06-01";
22
+
23
+ function translateTools(
24
+ tools: CompletionRequest["tools"],
25
+ ): unknown[] | undefined {
26
+ if (!tools || tools.length === 0) return undefined;
27
+ return tools.map((t) => ({
28
+ name: t.name,
29
+ description: t.description,
30
+ input_schema: t.parameters,
31
+ }));
32
+ }
33
+
34
+ function mapFinishReason(stopReason: string): FinishReason {
35
+ switch (stopReason) {
36
+ case "end_turn":
37
+ return "stop";
38
+ case "tool_use":
39
+ return "tool_call";
40
+ case "max_tokens":
41
+ return "max_tokens";
42
+ default:
43
+ return "stop";
44
+ }
45
+ }
46
+
47
+ function buildMessages(messages: CompletionRequest["messages"]) {
48
+ const systemMessages = messages.filter((m) => m.role === "system");
49
+ const nonSystemMessages = messages.filter((m) => m.role !== "system");
50
+
51
+ const system = systemMessages.map((m) => m.content).join("\n\n") || undefined;
52
+
53
+ const claudeMessages = nonSystemMessages.map((m) => {
54
+ if (m.role === "tool") {
55
+ return {
56
+ role: "user" as const,
57
+ content: [
58
+ {
59
+ type: "tool_result" as const,
60
+ tool_use_id: m.toolCallId,
61
+ content: m.content,
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ return { role: m.role as "user" | "assistant", content: m.content };
67
+ });
68
+
69
+ return { system, messages: claudeMessages };
70
+ }
71
+
72
+ async function complete(request: CompletionRequest): Promise<CompletionResult> {
73
+ const { system, messages } = buildMessages(request.messages);
74
+
75
+ const body: Record<string, unknown> = {
76
+ model: request.model,
77
+ messages,
78
+ max_tokens: request.maxTokens ?? 4096,
79
+ temperature: request.temperature,
80
+ };
81
+
82
+ if (system) body.system = system;
83
+
84
+ const tools = translateTools(request.tools);
85
+ if (tools) body.tools = tools;
86
+
87
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
88
+ method: "POST",
89
+ headers: {
90
+ "Content-Type": "application/json",
91
+ "x-api-key": config.apiKey,
92
+ "anthropic-version": apiVersion,
93
+ },
94
+ body: JSON.stringify(body),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ const error = await response.text();
99
+ throw new Error(`Claude API error ${response.status}: ${error}`);
100
+ }
101
+
102
+ const data = await response.json() as any;
103
+
104
+ let content = "";
105
+ const toolCalls: ToolCall[] = [];
106
+
107
+ for (const block of data.content) {
108
+ if (block.type === "text") {
109
+ content += block.text;
110
+ } else if (block.type === "tool_use") {
111
+ toolCalls.push({
112
+ id: block.id,
113
+ name: block.name,
114
+ arguments: block.input,
115
+ });
116
+ }
117
+ }
118
+
119
+ return {
120
+ content,
121
+ toolCalls,
122
+ usage: {
123
+ inputTokens: data.usage?.input_tokens ?? 0,
124
+ outputTokens: data.usage?.output_tokens ?? 0,
125
+ totalTokens:
126
+ (data.usage?.input_tokens ?? 0) + (data.usage?.output_tokens ?? 0),
127
+ cachedTokens: data.usage?.cache_read_input_tokens ?? 0,
128
+ reasoningTokens: 0,
129
+ },
130
+ finishReason: mapFinishReason(data.stop_reason),
131
+ };
132
+ }
133
+
134
+ async function streamComplete(
135
+ request: CompletionRequest,
136
+ onChunk: (chunk: StreamChunk) => void,
137
+ ): Promise<CompletionResult> {
138
+ const { system, messages } = buildMessages(request.messages);
139
+
140
+ const body: Record<string, unknown> = {
141
+ model: request.model,
142
+ messages,
143
+ max_tokens: request.maxTokens ?? 4096,
144
+ temperature: request.temperature,
145
+ stream: true,
146
+ };
147
+
148
+ if (system) body.system = system;
149
+
150
+ const tools = translateTools(request.tools);
151
+ if (tools) body.tools = tools;
152
+
153
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
154
+ method: "POST",
155
+ headers: {
156
+ "Content-Type": "application/json",
157
+ "x-api-key": config.apiKey,
158
+ "anthropic-version": apiVersion,
159
+ },
160
+ body: JSON.stringify(body),
161
+ });
162
+
163
+ if (!response.ok) {
164
+ const error = await response.text();
165
+ throw new Error(`Claude API error ${response.status}: ${error}`);
166
+ }
167
+
168
+ let content = "";
169
+ let finishReason: FinishReason = "stop";
170
+ let usage: TokenUsage = {
171
+ inputTokens: 0,
172
+ outputTokens: 0,
173
+ totalTokens: 0,
174
+ cachedTokens: 0,
175
+ reasoningTokens: 0,
176
+ };
177
+ const toolCalls: ToolCall[] = [];
178
+ let currentToolId = "";
179
+ let currentToolName = "";
180
+ let currentToolArgs = "";
181
+
182
+ const reader = response.body!.getReader();
183
+ const decoder = new TextDecoder();
184
+ let buffer = "";
185
+
186
+ while (true) {
187
+ const { done, value } = await reader.read();
188
+ if (done) break;
189
+
190
+ buffer += decoder.decode(value, { stream: true });
191
+ const lines = buffer.split("\n");
192
+ buffer = lines.pop()!;
193
+
194
+ let currentEvent = "";
195
+
196
+ for (const line of lines) {
197
+ if (line.startsWith("event: ")) {
198
+ currentEvent = line.slice(7).trim();
199
+ continue;
200
+ }
201
+
202
+ if (!line.startsWith("data: ")) continue;
203
+ const data = line.slice(6).trim();
204
+
205
+ try {
206
+ const parsed = JSON.parse(data);
207
+
208
+ switch (currentEvent) {
209
+ case "message_start": {
210
+ const u = parsed.message?.usage;
211
+ if (u) {
212
+ usage.inputTokens = u.input_tokens ?? 0;
213
+ usage.cachedTokens = u.cache_read_input_tokens ?? 0;
214
+ }
215
+ break;
216
+ }
217
+
218
+ case "content_block_start": {
219
+ const block = parsed.content_block;
220
+ if (block?.type === "tool_use") {
221
+ currentToolId = block.id;
222
+ currentToolName = block.name;
223
+ currentToolArgs = "";
224
+ onChunk({
225
+ type: "tool_call_start",
226
+ toolCall: {
227
+ id: block.id,
228
+ name: block.name,
229
+ arguments: {},
230
+ },
231
+ });
232
+ }
233
+ break;
234
+ }
235
+
236
+ case "content_block_delta": {
237
+ const delta = parsed.delta;
238
+ if (delta?.type === "text_delta") {
239
+ content += delta.text;
240
+ onChunk({ type: "content_delta", content: delta.text });
241
+ } else if (delta?.type === "input_json_delta") {
242
+ currentToolArgs += delta.partial_json;
243
+ onChunk({
244
+ type: "tool_call_delta",
245
+ content: delta.partial_json,
246
+ });
247
+ }
248
+ break;
249
+ }
250
+
251
+ case "content_block_stop": {
252
+ if (currentToolId) {
253
+ try {
254
+ toolCalls.push({
255
+ id: currentToolId,
256
+ name: currentToolName,
257
+ arguments: JSON.parse(currentToolArgs),
258
+ });
259
+ } catch {
260
+ toolCalls.push({
261
+ id: currentToolId,
262
+ name: currentToolName,
263
+ arguments: {},
264
+ });
265
+ }
266
+ currentToolId = "";
267
+ currentToolName = "";
268
+ currentToolArgs = "";
269
+ }
270
+ break;
271
+ }
272
+
273
+ case "message_delta": {
274
+ if (parsed.delta?.stop_reason) {
275
+ finishReason = mapFinishReason(parsed.delta.stop_reason);
276
+ }
277
+ if (parsed.usage) {
278
+ usage.outputTokens = parsed.usage.output_tokens ?? 0;
279
+ usage.totalTokens = usage.inputTokens + usage.outputTokens;
280
+ }
281
+ break;
282
+ }
283
+ }
284
+ } catch {
285
+ // Skip malformed JSON
286
+ }
287
+ }
288
+ }
289
+
290
+ return {
291
+ content,
292
+ toolCalls,
293
+ usage,
294
+ finishReason,
295
+ };
296
+ }
297
+
298
+ return {
299
+ name: "claude",
300
+ models: [
301
+ "claude-opus-4-6",
302
+ "claude-sonnet-4-6",
303
+ "claude-haiku-4-5-20251001",
304
+ ],
305
+ complete,
306
+ streamComplete,
307
+ };
308
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../../../tsconfig.json",
3
+ "include": ["src/**/*"]
4
+ }