@arcote.tech/arc-ai-claude 0.5.0 → 0.5.5
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 +2 -2
- package/src/index.ts +121 -83
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-ai-claude",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.5",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Claude (Anthropic) adapter for Arc AI framework",
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type-check": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@arcote.tech/arc-ai": "^0.5.
|
|
13
|
+
"@arcote.tech/arc-ai": "^0.5.5",
|
|
14
14
|
"typescript": "^5.0.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type {
|
|
|
2
2
|
LLMProvider,
|
|
3
3
|
CompletionRequest,
|
|
4
4
|
CompletionResult,
|
|
5
|
+
Conversation,
|
|
6
|
+
ConversationTurn,
|
|
7
|
+
AssistantContentBlock,
|
|
5
8
|
StreamChunk,
|
|
6
|
-
ToolCall,
|
|
7
9
|
TokenUsage,
|
|
8
10
|
FinishReason,
|
|
9
11
|
} from "@arcote.tech/arc-ai";
|
|
@@ -44,46 +46,88 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Translate a single ConversationTurn into one Claude Messages API message.
|
|
51
|
+
* Adapter is a pure translator — caller already decided what to send via the
|
|
52
|
+
* Conversation discriminated union. Block ordering is preserved 1:1 inside
|
|
53
|
+
* assistant messages.
|
|
54
|
+
*/
|
|
55
|
+
function turnToMessage(turn: ConversationTurn): unknown {
|
|
56
|
+
if (turn.role === "user") {
|
|
57
|
+
return { role: "user", content: turn.content };
|
|
58
|
+
}
|
|
59
|
+
if (turn.role === "tool_result") {
|
|
60
|
+
return {
|
|
61
|
+
role: "user",
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "tool_result",
|
|
65
|
+
tool_use_id: turn.toolCallId,
|
|
66
|
+
content: turn.content,
|
|
67
|
+
...(turn.isError ? { is_error: true } : {}),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// assistant — emit content blocks in order
|
|
73
|
+
const content: unknown[] = [];
|
|
74
|
+
for (const block of turn.blocks) {
|
|
75
|
+
if (block.type === "text") {
|
|
76
|
+
if (!block.text) continue;
|
|
77
|
+
content.push({ type: "text", text: block.text });
|
|
78
|
+
} else {
|
|
79
|
+
content.push({
|
|
80
|
+
type: "tool_use",
|
|
81
|
+
id: block.id,
|
|
82
|
+
name: block.name,
|
|
83
|
+
input: block.arguments,
|
|
84
|
+
});
|
|
65
85
|
}
|
|
66
|
-
|
|
67
|
-
}
|
|
86
|
+
}
|
|
87
|
+
return { role: "assistant", content };
|
|
88
|
+
}
|
|
68
89
|
|
|
69
|
-
|
|
90
|
+
function buildMessages(conversation: Conversation): unknown[] {
|
|
91
|
+
if (conversation.mode !== "full") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Claude provider does not support continuation mode — set " +
|
|
94
|
+
"`supportsContinuation: false` in the listener and pass " +
|
|
95
|
+
"`Conversation.mode = 'full'` with the full conversation history.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return conversation.turns.map(turnToMessage);
|
|
70
99
|
}
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
function buildBody(
|
|
102
|
+
request: CompletionRequest,
|
|
103
|
+
stream: boolean,
|
|
104
|
+
): Record<string, unknown> {
|
|
105
|
+
const messages = buildMessages(request.conversation);
|
|
74
106
|
|
|
75
107
|
const body: Record<string, unknown> = {
|
|
76
108
|
model: request.model,
|
|
77
109
|
messages,
|
|
78
110
|
max_tokens: request.maxTokens ?? 4096,
|
|
79
111
|
temperature: request.temperature,
|
|
112
|
+
...(stream ? { stream: true } : {}),
|
|
80
113
|
};
|
|
81
114
|
|
|
82
|
-
|
|
115
|
+
// `instructions` is always sent. Empty string means no system prompt.
|
|
116
|
+
if (request.instructions) body.system = request.instructions;
|
|
83
117
|
|
|
84
118
|
const tools = translateTools(request.tools);
|
|
85
119
|
if (tools) body.tools = tools;
|
|
86
120
|
|
|
121
|
+
return body;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── complete ─────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async function complete(
|
|
127
|
+
request: CompletionRequest,
|
|
128
|
+
): Promise<CompletionResult> {
|
|
129
|
+
const body = buildBody(request, false);
|
|
130
|
+
|
|
87
131
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
88
132
|
method: "POST",
|
|
89
133
|
headers: {
|
|
@@ -101,24 +145,22 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
101
145
|
|
|
102
146
|
const data = await response.json();
|
|
103
147
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (block.type === "text") {
|
|
109
|
-
content += block.text;
|
|
148
|
+
const blocks: AssistantContentBlock[] = [];
|
|
149
|
+
for (const block of data.content ?? []) {
|
|
150
|
+
if (block.type === "text" && block.text) {
|
|
151
|
+
blocks.push({ type: "text", text: block.text });
|
|
110
152
|
} else if (block.type === "tool_use") {
|
|
111
|
-
|
|
153
|
+
blocks.push({
|
|
154
|
+
type: "tool_call",
|
|
112
155
|
id: block.id,
|
|
113
156
|
name: block.name,
|
|
114
|
-
arguments: block.input,
|
|
157
|
+
arguments: block.input ?? {},
|
|
115
158
|
});
|
|
116
159
|
}
|
|
117
160
|
}
|
|
118
161
|
|
|
119
162
|
return {
|
|
120
|
-
|
|
121
|
-
toolCalls,
|
|
163
|
+
blocks,
|
|
122
164
|
usage: {
|
|
123
165
|
inputTokens: data.usage?.input_tokens ?? 0,
|
|
124
166
|
outputTokens: data.usage?.output_tokens ?? 0,
|
|
@@ -131,24 +173,13 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
131
173
|
};
|
|
132
174
|
}
|
|
133
175
|
|
|
176
|
+
// ─── streamComplete ───────────────────────────────────────────
|
|
177
|
+
|
|
134
178
|
async function streamComplete(
|
|
135
179
|
request: CompletionRequest,
|
|
136
180
|
onChunk: (chunk: StreamChunk) => void,
|
|
137
181
|
): Promise<CompletionResult> {
|
|
138
|
-
const
|
|
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;
|
|
182
|
+
const body = buildBody(request, true);
|
|
152
183
|
|
|
153
184
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
154
185
|
method: "POST",
|
|
@@ -165,23 +196,24 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
165
196
|
throw new Error(`Claude API error ${response.status}: ${error}`);
|
|
166
197
|
}
|
|
167
198
|
|
|
168
|
-
|
|
199
|
+
// Reconstruct ordered blocks from streamed events. Claude emits
|
|
200
|
+
// content_block_start/delta/stop events with explicit indices, so we
|
|
201
|
+
// anchor our blocks by index for deterministic ordering.
|
|
202
|
+
const orderedBlocks: AssistantContentBlock[] = [];
|
|
169
203
|
let finishReason: FinishReason = "stop";
|
|
170
|
-
|
|
204
|
+
const usage: TokenUsage = {
|
|
171
205
|
inputTokens: 0,
|
|
172
206
|
outputTokens: 0,
|
|
173
207
|
totalTokens: 0,
|
|
174
208
|
cachedTokens: 0,
|
|
175
209
|
reasoningTokens: 0,
|
|
176
210
|
};
|
|
177
|
-
const
|
|
178
|
-
let currentToolId = "";
|
|
179
|
-
let currentToolName = "";
|
|
180
|
-
let currentToolArgs = "";
|
|
211
|
+
const toolArgBuffers = new Map<number, string>();
|
|
181
212
|
|
|
182
213
|
const reader = response.body!.getReader();
|
|
183
214
|
const decoder = new TextDecoder();
|
|
184
215
|
let buffer = "";
|
|
216
|
+
let currentEvent = "";
|
|
185
217
|
|
|
186
218
|
while (true) {
|
|
187
219
|
const { done, value } = await reader.read();
|
|
@@ -191,14 +223,11 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
191
223
|
const lines = buffer.split("\n");
|
|
192
224
|
buffer = lines.pop()!;
|
|
193
225
|
|
|
194
|
-
let currentEvent = "";
|
|
195
|
-
|
|
196
226
|
for (const line of lines) {
|
|
197
227
|
if (line.startsWith("event: ")) {
|
|
198
228
|
currentEvent = line.slice(7).trim();
|
|
199
229
|
continue;
|
|
200
230
|
}
|
|
201
|
-
|
|
202
231
|
if (!line.startsWith("data: ")) continue;
|
|
203
232
|
const data = line.slice(6).trim();
|
|
204
233
|
|
|
@@ -216,11 +245,18 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
case "content_block_start": {
|
|
248
|
+
const idx = parsed.index;
|
|
219
249
|
const block = parsed.content_block;
|
|
220
|
-
if (block?.type === "
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
if (block?.type === "text") {
|
|
251
|
+
orderedBlocks[idx] = { type: "text", text: "" };
|
|
252
|
+
} else if (block?.type === "tool_use") {
|
|
253
|
+
orderedBlocks[idx] = {
|
|
254
|
+
type: "tool_call",
|
|
255
|
+
id: block.id,
|
|
256
|
+
name: block.name,
|
|
257
|
+
arguments: {},
|
|
258
|
+
};
|
|
259
|
+
toolArgBuffers.set(idx, "");
|
|
224
260
|
onChunk({
|
|
225
261
|
type: "tool_call_start",
|
|
226
262
|
toolCall: {
|
|
@@ -234,12 +270,15 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
234
270
|
}
|
|
235
271
|
|
|
236
272
|
case "content_block_delta": {
|
|
273
|
+
const idx = parsed.index;
|
|
237
274
|
const delta = parsed.delta;
|
|
238
|
-
|
|
239
|
-
|
|
275
|
+
const block = orderedBlocks[idx];
|
|
276
|
+
if (delta?.type === "text_delta" && block?.type === "text") {
|
|
277
|
+
block.text += delta.text;
|
|
240
278
|
onChunk({ type: "content_delta", content: delta.text });
|
|
241
279
|
} else if (delta?.type === "input_json_delta") {
|
|
242
|
-
|
|
280
|
+
const existing = toolArgBuffers.get(idx) ?? "";
|
|
281
|
+
toolArgBuffers.set(idx, existing + delta.partial_json);
|
|
243
282
|
onChunk({
|
|
244
283
|
type: "tool_call_delta",
|
|
245
284
|
content: delta.partial_json,
|
|
@@ -249,23 +288,18 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
249
288
|
}
|
|
250
289
|
|
|
251
290
|
case "content_block_stop": {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
name: currentToolName,
|
|
263
|
-
arguments: {},
|
|
264
|
-
});
|
|
291
|
+
const idx = parsed.index;
|
|
292
|
+
const buffered = toolArgBuffers.get(idx);
|
|
293
|
+
if (buffered != null) {
|
|
294
|
+
const block = orderedBlocks[idx];
|
|
295
|
+
if (block?.type === "tool_call") {
|
|
296
|
+
try {
|
|
297
|
+
block.arguments = JSON.parse(buffered);
|
|
298
|
+
} catch {
|
|
299
|
+
block.arguments = {};
|
|
300
|
+
}
|
|
265
301
|
}
|
|
266
|
-
|
|
267
|
-
currentToolName = "";
|
|
268
|
-
currentToolArgs = "";
|
|
302
|
+
toolArgBuffers.delete(idx);
|
|
269
303
|
}
|
|
270
304
|
break;
|
|
271
305
|
}
|
|
@@ -287,9 +321,12 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
287
321
|
}
|
|
288
322
|
}
|
|
289
323
|
|
|
324
|
+
const blocks = orderedBlocks.filter(
|
|
325
|
+
(b): b is AssistantContentBlock => b != null,
|
|
326
|
+
);
|
|
327
|
+
|
|
290
328
|
return {
|
|
291
|
-
|
|
292
|
-
toolCalls,
|
|
329
|
+
blocks,
|
|
293
330
|
usage,
|
|
294
331
|
finishReason,
|
|
295
332
|
};
|
|
@@ -302,6 +339,7 @@ export function claude(config: ClaudeConfig): LLMProvider {
|
|
|
302
339
|
"claude-sonnet-4-6",
|
|
303
340
|
"claude-haiku-4-5-20251001",
|
|
304
341
|
],
|
|
342
|
+
supportsContinuation: false,
|
|
305
343
|
complete,
|
|
306
344
|
streamComplete,
|
|
307
345
|
};
|