@strand-js/anthropic 0.1.0 → 0.1.2
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 +70 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +322 -0
- package/dist/index.mjs +284 -0
- package/package.json +11 -10
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @strand-js/anthropic
|
|
2
|
+
|
|
3
|
+
Anthropic provider adapter for [Strand](https://github.com/strand-js/strand) — AI state management for React.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @strand-js/anthropic
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Express / Fastify / Hono
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createStrandHandler } from '@strand-js/anthropic'
|
|
17
|
+
|
|
18
|
+
app.post('/api/strand', createStrandHandler({
|
|
19
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
20
|
+
model: 'claude-sonnet-4-6',
|
|
21
|
+
system: 'You are a helpful assistant.',
|
|
22
|
+
}))
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Next.js App Router
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// app/api/strand/route.ts
|
|
29
|
+
import { createStrandRoute } from '@strand-js/anthropic'
|
|
30
|
+
|
|
31
|
+
export const POST = createStrandRoute({
|
|
32
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
33
|
+
model: 'claude-sonnet-4-6',
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With tools
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { z } from 'zod'
|
|
41
|
+
import { tool } from '@strand-js/core'
|
|
42
|
+
|
|
43
|
+
const weatherTool = tool({
|
|
44
|
+
name: 'get_weather',
|
|
45
|
+
description: 'Get weather for a city',
|
|
46
|
+
parameters: z.object({ location: z.string() }),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
createStrandHandler({
|
|
50
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
51
|
+
model: 'claude-sonnet-4-6',
|
|
52
|
+
tools: [weatherTool],
|
|
53
|
+
onToolCall: async (name, args) => fetchWeather(args.location),
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### With auth
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
createStrandHandler({
|
|
61
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
62
|
+
model: 'claude-sonnet-4-6',
|
|
63
|
+
authorize: async (request) => {
|
|
64
|
+
const user = await verifyToken(request.headers.get('authorization'))
|
|
65
|
+
if (!user) throw new Error('Unauthorized')
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
[Full documentation →](https://github.com/strand-js/strand)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ToolDefinition, Session } from '@strand-js/core';
|
|
2
|
+
|
|
3
|
+
interface StrandHandlerConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
model: string;
|
|
6
|
+
system?: string | ((request: Request) => string | Promise<string>);
|
|
7
|
+
tools?: ToolDefinition[];
|
|
8
|
+
onToolCall?: (name: string, args: Record<string, unknown>, ctx: {
|
|
9
|
+
request: Request;
|
|
10
|
+
}) => Promise<unknown>;
|
|
11
|
+
authorize?: (request: Request) => Promise<unknown> | unknown;
|
|
12
|
+
maxMessages?: number;
|
|
13
|
+
maxMessageLength?: number;
|
|
14
|
+
maxSteps?: number;
|
|
15
|
+
onRequest?: (req: Request) => void;
|
|
16
|
+
onFinish?: (session: Session) => void;
|
|
17
|
+
}
|
|
18
|
+
type AnyReq = Record<string, unknown>;
|
|
19
|
+
type AnyRes = {
|
|
20
|
+
setHeader(name: string, value: string): void;
|
|
21
|
+
status(code: number): AnyRes;
|
|
22
|
+
json(body: unknown): void;
|
|
23
|
+
write(chunk: string): void;
|
|
24
|
+
end(): void;
|
|
25
|
+
};
|
|
26
|
+
declare function createStrandHandler(config: StrandHandlerConfig): (req: AnyReq, res: AnyRes) => Promise<void>;
|
|
27
|
+
|
|
28
|
+
declare function createStrandRoute(config: StrandHandlerConfig): (req: Request) => Promise<Response>;
|
|
29
|
+
|
|
30
|
+
export { type StrandHandlerConfig, createStrandHandler, createStrandRoute };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ToolDefinition, Session } from '@strand-js/core';
|
|
2
|
+
|
|
3
|
+
interface StrandHandlerConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
model: string;
|
|
6
|
+
system?: string | ((request: Request) => string | Promise<string>);
|
|
7
|
+
tools?: ToolDefinition[];
|
|
8
|
+
onToolCall?: (name: string, args: Record<string, unknown>, ctx: {
|
|
9
|
+
request: Request;
|
|
10
|
+
}) => Promise<unknown>;
|
|
11
|
+
authorize?: (request: Request) => Promise<unknown> | unknown;
|
|
12
|
+
maxMessages?: number;
|
|
13
|
+
maxMessageLength?: number;
|
|
14
|
+
maxSteps?: number;
|
|
15
|
+
onRequest?: (req: Request) => void;
|
|
16
|
+
onFinish?: (session: Session) => void;
|
|
17
|
+
}
|
|
18
|
+
type AnyReq = Record<string, unknown>;
|
|
19
|
+
type AnyRes = {
|
|
20
|
+
setHeader(name: string, value: string): void;
|
|
21
|
+
status(code: number): AnyRes;
|
|
22
|
+
json(body: unknown): void;
|
|
23
|
+
write(chunk: string): void;
|
|
24
|
+
end(): void;
|
|
25
|
+
};
|
|
26
|
+
declare function createStrandHandler(config: StrandHandlerConfig): (req: AnyReq, res: AnyRes) => Promise<void>;
|
|
27
|
+
|
|
28
|
+
declare function createStrandRoute(config: StrandHandlerConfig): (req: Request) => Promise<Response>;
|
|
29
|
+
|
|
30
|
+
export { type StrandHandlerConfig, createStrandHandler, createStrandRoute };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createStrandHandler: () => createStrandHandler,
|
|
34
|
+
createStrandRoute: () => createStrandRoute
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/handler.ts
|
|
39
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
40
|
+
var import_core2 = require("@strand-js/core");
|
|
41
|
+
|
|
42
|
+
// src/format.ts
|
|
43
|
+
var import_core = require("@strand-js/core");
|
|
44
|
+
function toolToAnthropicTool(tool) {
|
|
45
|
+
const schema = (0, import_core.toolToJsonSchema)(tool);
|
|
46
|
+
return {
|
|
47
|
+
name: tool.name,
|
|
48
|
+
description: tool.description,
|
|
49
|
+
input_schema: schema
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/handler.ts
|
|
54
|
+
function emit(res, eventType, data) {
|
|
55
|
+
res.write(`event: ${eventType}
|
|
56
|
+
data: ${JSON.stringify(data)}
|
|
57
|
+
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
function createStrandHandler(config) {
|
|
61
|
+
const client = new import_sdk.default({ apiKey: config.apiKey });
|
|
62
|
+
const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
|
|
63
|
+
const maxSteps = config.maxSteps ?? 10;
|
|
64
|
+
return async (req, res) => {
|
|
65
|
+
const body = req.body;
|
|
66
|
+
const validation = (0, import_core2.validateMessages)(body?.messages, {
|
|
67
|
+
maxMessages: config.maxMessages,
|
|
68
|
+
maxMessageLength: config.maxMessageLength
|
|
69
|
+
});
|
|
70
|
+
if (!validation.ok) {
|
|
71
|
+
res.status(validation.status).json({ error: validation.error });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (config.authorize) {
|
|
75
|
+
try {
|
|
76
|
+
await config.authorize(req);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const message = err instanceof Error ? err.message : "Unauthorized";
|
|
79
|
+
res.status(401).json({ error: message });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
84
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
85
|
+
res.setHeader("Connection", "keep-alive");
|
|
86
|
+
emit(res, "strand:start", { sessionId: (0, import_core2.generateId)(), requestId: (0, import_core2.generateId)() });
|
|
87
|
+
try {
|
|
88
|
+
const messages = body.messages;
|
|
89
|
+
const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
|
|
90
|
+
const conversation = messages.map((m) => ({
|
|
91
|
+
role: m.role,
|
|
92
|
+
content: m.content
|
|
93
|
+
}));
|
|
94
|
+
let totalInput = 0;
|
|
95
|
+
let totalOutput = 0;
|
|
96
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
97
|
+
const stream = await client.messages.create({
|
|
98
|
+
model: config.model,
|
|
99
|
+
max_tokens: 8192,
|
|
100
|
+
...system ? { system } : {},
|
|
101
|
+
messages: conversation,
|
|
102
|
+
...anthropicTools.length > 0 ? { tools: anthropicTools } : {},
|
|
103
|
+
stream: true
|
|
104
|
+
});
|
|
105
|
+
let stopReason = null;
|
|
106
|
+
let currentTool = null;
|
|
107
|
+
const completedTools = [];
|
|
108
|
+
const assistantContent = [];
|
|
109
|
+
for await (const event of stream) {
|
|
110
|
+
if (event.type === "message_start" && event.message.usage) {
|
|
111
|
+
totalInput += event.message.usage.input_tokens;
|
|
112
|
+
}
|
|
113
|
+
if (event.type === "content_block_start") {
|
|
114
|
+
const block = event.content_block;
|
|
115
|
+
if (block.type === "text") assistantContent.push({ type: "text", text: "", citations: null });
|
|
116
|
+
if (block.type === "tool_use") {
|
|
117
|
+
currentTool = { id: block.id, name: block.name, inputJson: "" };
|
|
118
|
+
emit(res, "strand:tool-start", { toolCallId: block.id, toolName: block.name });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (event.type === "content_block_delta") {
|
|
122
|
+
const delta = event.delta;
|
|
123
|
+
if (delta.type === "text_delta") {
|
|
124
|
+
const last = assistantContent.at(-1);
|
|
125
|
+
if (last?.type === "text") last.text += delta.text;
|
|
126
|
+
emit(res, "strand:text-delta", { delta: delta.text });
|
|
127
|
+
}
|
|
128
|
+
if (delta.type === "input_json_delta" && currentTool) {
|
|
129
|
+
currentTool.inputJson += delta.partial_json;
|
|
130
|
+
emit(res, "strand:tool-input-delta", { toolCallId: currentTool.id, delta: delta.partial_json });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (event.type === "content_block_stop" && currentTool) {
|
|
134
|
+
const input = JSON.parse(currentTool.inputJson || "{}");
|
|
135
|
+
completedTools.push({ id: currentTool.id, name: currentTool.name, input });
|
|
136
|
+
assistantContent.push({ type: "tool_use", id: currentTool.id, name: currentTool.name, input });
|
|
137
|
+
currentTool = null;
|
|
138
|
+
}
|
|
139
|
+
if (event.type === "message_delta") {
|
|
140
|
+
stopReason = event.delta.stop_reason ?? null;
|
|
141
|
+
if (event.usage) totalOutput += event.usage.output_tokens;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (stopReason === "end_turn" || stopReason === "max_tokens") {
|
|
145
|
+
emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
146
|
+
res.end();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (stopReason === "tool_use" && completedTools.length > 0) {
|
|
150
|
+
conversation.push({ role: "assistant", content: assistantContent });
|
|
151
|
+
const toolResults = await Promise.all(
|
|
152
|
+
completedTools.map(async (block) => {
|
|
153
|
+
emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
|
|
154
|
+
try {
|
|
155
|
+
const result = await config.onToolCall?.(block.name, block.input, { request: req });
|
|
156
|
+
emit(res, "strand:tool-result", { toolCallId: block.id, result });
|
|
157
|
+
return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result ?? null) };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const message = err instanceof Error ? err.message : "Tool execution failed";
|
|
160
|
+
emit(res, "strand:tool-error", { toolCallId: block.id, error: message });
|
|
161
|
+
return { type: "tool_result", tool_use_id: block.id, content: message, is_error: true };
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
conversation.push({ role: "user", content: toolResults });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
171
|
+
res.end();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
174
|
+
emit(res, "strand:error", { code: "server_error", message });
|
|
175
|
+
res.end();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/route.ts
|
|
181
|
+
var import_sdk2 = __toESM(require("@anthropic-ai/sdk"));
|
|
182
|
+
var import_core3 = require("@strand-js/core");
|
|
183
|
+
function sseChunk(eventType, data) {
|
|
184
|
+
return new TextEncoder().encode(`event: ${eventType}
|
|
185
|
+
data: ${JSON.stringify(data)}
|
|
186
|
+
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
189
|
+
function createStrandRoute(config) {
|
|
190
|
+
const client = new import_sdk2.default({ apiKey: config.apiKey });
|
|
191
|
+
const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
|
|
192
|
+
const maxSteps = config.maxSteps ?? 10;
|
|
193
|
+
return async (req) => {
|
|
194
|
+
const body = await req.json();
|
|
195
|
+
const validation = (0, import_core3.validateMessages)(body?.messages, {
|
|
196
|
+
maxMessages: config.maxMessages,
|
|
197
|
+
maxMessageLength: config.maxMessageLength
|
|
198
|
+
});
|
|
199
|
+
if (!validation.ok) {
|
|
200
|
+
return new Response(JSON.stringify({ error: validation.error }), {
|
|
201
|
+
status: validation.status,
|
|
202
|
+
headers: { "Content-Type": "application/json" }
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (config.authorize) {
|
|
206
|
+
try {
|
|
207
|
+
await config.authorize(req);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const message = err instanceof Error ? err.message : "Unauthorized";
|
|
210
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
211
|
+
status: 401,
|
|
212
|
+
headers: { "Content-Type": "application/json" }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const messages = body.messages;
|
|
217
|
+
const stream = new ReadableStream({
|
|
218
|
+
async start(controller) {
|
|
219
|
+
const emit2 = (eventType, data) => controller.enqueue(sseChunk(eventType, data));
|
|
220
|
+
emit2("strand:start", { sessionId: (0, import_core3.generateId)(), requestId: (0, import_core3.generateId)() });
|
|
221
|
+
try {
|
|
222
|
+
const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
|
|
223
|
+
const conversation = messages.map((m) => ({
|
|
224
|
+
role: m.role,
|
|
225
|
+
content: m.content
|
|
226
|
+
}));
|
|
227
|
+
let totalInput = 0;
|
|
228
|
+
let totalOutput = 0;
|
|
229
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
230
|
+
const anthropicStream = await client.messages.create({
|
|
231
|
+
model: config.model,
|
|
232
|
+
max_tokens: 8192,
|
|
233
|
+
...system ? { system } : {},
|
|
234
|
+
messages: conversation,
|
|
235
|
+
...anthropicTools.length > 0 ? { tools: anthropicTools } : {},
|
|
236
|
+
stream: true
|
|
237
|
+
});
|
|
238
|
+
let stopReason = null;
|
|
239
|
+
let currentTool = null;
|
|
240
|
+
const completedTools = [];
|
|
241
|
+
const assistantContent = [];
|
|
242
|
+
for await (const event of anthropicStream) {
|
|
243
|
+
if (event.type === "message_start" && event.message.usage) totalInput += event.message.usage.input_tokens;
|
|
244
|
+
if (event.type === "content_block_start") {
|
|
245
|
+
const block = event.content_block;
|
|
246
|
+
if (block.type === "text") assistantContent.push({ type: "text", text: "", citations: null });
|
|
247
|
+
if (block.type === "tool_use") {
|
|
248
|
+
currentTool = { id: block.id, name: block.name, inputJson: "" };
|
|
249
|
+
emit2("strand:tool-start", { toolCallId: block.id, toolName: block.name });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (event.type === "content_block_delta") {
|
|
253
|
+
const delta = event.delta;
|
|
254
|
+
if (delta.type === "text_delta") {
|
|
255
|
+
const last = assistantContent.at(-1);
|
|
256
|
+
if (last?.type === "text") last.text += delta.text;
|
|
257
|
+
emit2("strand:text-delta", { delta: delta.text });
|
|
258
|
+
}
|
|
259
|
+
if (delta.type === "input_json_delta" && currentTool) {
|
|
260
|
+
currentTool.inputJson += delta.partial_json;
|
|
261
|
+
emit2("strand:tool-input-delta", { toolCallId: currentTool.id, delta: delta.partial_json });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (event.type === "content_block_stop" && currentTool) {
|
|
265
|
+
const input = JSON.parse(currentTool.inputJson || "{}");
|
|
266
|
+
completedTools.push({ id: currentTool.id, name: currentTool.name, input });
|
|
267
|
+
assistantContent.push({ type: "tool_use", id: currentTool.id, name: currentTool.name, input });
|
|
268
|
+
currentTool = null;
|
|
269
|
+
}
|
|
270
|
+
if (event.type === "message_delta") {
|
|
271
|
+
stopReason = event.delta.stop_reason ?? null;
|
|
272
|
+
if (event.usage) totalOutput += event.usage.output_tokens;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (stopReason === "end_turn" || stopReason === "max_tokens") {
|
|
276
|
+
emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
if (stopReason === "tool_use" && completedTools.length > 0) {
|
|
280
|
+
conversation.push({ role: "assistant", content: assistantContent });
|
|
281
|
+
const results = await Promise.all(
|
|
282
|
+
completedTools.map(async (block) => {
|
|
283
|
+
emit2("strand:tool-input-done", { toolCallId: block.id, input: block.input });
|
|
284
|
+
try {
|
|
285
|
+
const result = await config.onToolCall?.(block.name, block.input, { request: req });
|
|
286
|
+
emit2("strand:tool-result", { toolCallId: block.id, result });
|
|
287
|
+
return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result ?? null) };
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const message = err instanceof Error ? err.message : "Tool execution failed";
|
|
290
|
+
emit2("strand:tool-error", { toolCallId: block.id, error: message });
|
|
291
|
+
return { type: "tool_result", tool_use_id: block.id, content: message, is_error: true };
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
conversation.push({ role: "user", content: results });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
303
|
+
emit2("strand:error", { code: "server_error", message });
|
|
304
|
+
} finally {
|
|
305
|
+
controller.close();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
return new Response(stream, {
|
|
310
|
+
headers: {
|
|
311
|
+
"Content-Type": "text/event-stream",
|
|
312
|
+
"Cache-Control": "no-cache",
|
|
313
|
+
"Connection": "keep-alive"
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
319
|
+
0 && (module.exports = {
|
|
320
|
+
createStrandHandler,
|
|
321
|
+
createStrandRoute
|
|
322
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/handler.ts
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import { generateId, validateMessages } from "@strand-js/core";
|
|
4
|
+
|
|
5
|
+
// src/format.ts
|
|
6
|
+
import { toolToJsonSchema } from "@strand-js/core";
|
|
7
|
+
function toolToAnthropicTool(tool) {
|
|
8
|
+
const schema = toolToJsonSchema(tool);
|
|
9
|
+
return {
|
|
10
|
+
name: tool.name,
|
|
11
|
+
description: tool.description,
|
|
12
|
+
input_schema: schema
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/handler.ts
|
|
17
|
+
function emit(res, eventType, data) {
|
|
18
|
+
res.write(`event: ${eventType}
|
|
19
|
+
data: ${JSON.stringify(data)}
|
|
20
|
+
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
function createStrandHandler(config) {
|
|
24
|
+
const client = new Anthropic({ apiKey: config.apiKey });
|
|
25
|
+
const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
|
|
26
|
+
const maxSteps = config.maxSteps ?? 10;
|
|
27
|
+
return async (req, res) => {
|
|
28
|
+
const body = req.body;
|
|
29
|
+
const validation = validateMessages(body?.messages, {
|
|
30
|
+
maxMessages: config.maxMessages,
|
|
31
|
+
maxMessageLength: config.maxMessageLength
|
|
32
|
+
});
|
|
33
|
+
if (!validation.ok) {
|
|
34
|
+
res.status(validation.status).json({ error: validation.error });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (config.authorize) {
|
|
38
|
+
try {
|
|
39
|
+
await config.authorize(req);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : "Unauthorized";
|
|
42
|
+
res.status(401).json({ error: message });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
47
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
48
|
+
res.setHeader("Connection", "keep-alive");
|
|
49
|
+
emit(res, "strand:start", { sessionId: generateId(), requestId: generateId() });
|
|
50
|
+
try {
|
|
51
|
+
const messages = body.messages;
|
|
52
|
+
const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
|
|
53
|
+
const conversation = messages.map((m) => ({
|
|
54
|
+
role: m.role,
|
|
55
|
+
content: m.content
|
|
56
|
+
}));
|
|
57
|
+
let totalInput = 0;
|
|
58
|
+
let totalOutput = 0;
|
|
59
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
60
|
+
const stream = await client.messages.create({
|
|
61
|
+
model: config.model,
|
|
62
|
+
max_tokens: 8192,
|
|
63
|
+
...system ? { system } : {},
|
|
64
|
+
messages: conversation,
|
|
65
|
+
...anthropicTools.length > 0 ? { tools: anthropicTools } : {},
|
|
66
|
+
stream: true
|
|
67
|
+
});
|
|
68
|
+
let stopReason = null;
|
|
69
|
+
let currentTool = null;
|
|
70
|
+
const completedTools = [];
|
|
71
|
+
const assistantContent = [];
|
|
72
|
+
for await (const event of stream) {
|
|
73
|
+
if (event.type === "message_start" && event.message.usage) {
|
|
74
|
+
totalInput += event.message.usage.input_tokens;
|
|
75
|
+
}
|
|
76
|
+
if (event.type === "content_block_start") {
|
|
77
|
+
const block = event.content_block;
|
|
78
|
+
if (block.type === "text") assistantContent.push({ type: "text", text: "", citations: null });
|
|
79
|
+
if (block.type === "tool_use") {
|
|
80
|
+
currentTool = { id: block.id, name: block.name, inputJson: "" };
|
|
81
|
+
emit(res, "strand:tool-start", { toolCallId: block.id, toolName: block.name });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (event.type === "content_block_delta") {
|
|
85
|
+
const delta = event.delta;
|
|
86
|
+
if (delta.type === "text_delta") {
|
|
87
|
+
const last = assistantContent.at(-1);
|
|
88
|
+
if (last?.type === "text") last.text += delta.text;
|
|
89
|
+
emit(res, "strand:text-delta", { delta: delta.text });
|
|
90
|
+
}
|
|
91
|
+
if (delta.type === "input_json_delta" && currentTool) {
|
|
92
|
+
currentTool.inputJson += delta.partial_json;
|
|
93
|
+
emit(res, "strand:tool-input-delta", { toolCallId: currentTool.id, delta: delta.partial_json });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (event.type === "content_block_stop" && currentTool) {
|
|
97
|
+
const input = JSON.parse(currentTool.inputJson || "{}");
|
|
98
|
+
completedTools.push({ id: currentTool.id, name: currentTool.name, input });
|
|
99
|
+
assistantContent.push({ type: "tool_use", id: currentTool.id, name: currentTool.name, input });
|
|
100
|
+
currentTool = null;
|
|
101
|
+
}
|
|
102
|
+
if (event.type === "message_delta") {
|
|
103
|
+
stopReason = event.delta.stop_reason ?? null;
|
|
104
|
+
if (event.usage) totalOutput += event.usage.output_tokens;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (stopReason === "end_turn" || stopReason === "max_tokens") {
|
|
108
|
+
emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
109
|
+
res.end();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (stopReason === "tool_use" && completedTools.length > 0) {
|
|
113
|
+
conversation.push({ role: "assistant", content: assistantContent });
|
|
114
|
+
const toolResults = await Promise.all(
|
|
115
|
+
completedTools.map(async (block) => {
|
|
116
|
+
emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
|
|
117
|
+
try {
|
|
118
|
+
const result = await config.onToolCall?.(block.name, block.input, { request: req });
|
|
119
|
+
emit(res, "strand:tool-result", { toolCallId: block.id, result });
|
|
120
|
+
return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result ?? null) };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const message = err instanceof Error ? err.message : "Tool execution failed";
|
|
123
|
+
emit(res, "strand:tool-error", { toolCallId: block.id, error: message });
|
|
124
|
+
return { type: "tool_result", tool_use_id: block.id, content: message, is_error: true };
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
conversation.push({ role: "user", content: toolResults });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
134
|
+
res.end();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
137
|
+
emit(res, "strand:error", { code: "server_error", message });
|
|
138
|
+
res.end();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/route.ts
|
|
144
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
145
|
+
import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
|
|
146
|
+
function sseChunk(eventType, data) {
|
|
147
|
+
return new TextEncoder().encode(`event: ${eventType}
|
|
148
|
+
data: ${JSON.stringify(data)}
|
|
149
|
+
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
function createStrandRoute(config) {
|
|
153
|
+
const client = new Anthropic2({ apiKey: config.apiKey });
|
|
154
|
+
const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
|
|
155
|
+
const maxSteps = config.maxSteps ?? 10;
|
|
156
|
+
return async (req) => {
|
|
157
|
+
const body = await req.json();
|
|
158
|
+
const validation = validateMessages2(body?.messages, {
|
|
159
|
+
maxMessages: config.maxMessages,
|
|
160
|
+
maxMessageLength: config.maxMessageLength
|
|
161
|
+
});
|
|
162
|
+
if (!validation.ok) {
|
|
163
|
+
return new Response(JSON.stringify({ error: validation.error }), {
|
|
164
|
+
status: validation.status,
|
|
165
|
+
headers: { "Content-Type": "application/json" }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (config.authorize) {
|
|
169
|
+
try {
|
|
170
|
+
await config.authorize(req);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const message = err instanceof Error ? err.message : "Unauthorized";
|
|
173
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
174
|
+
status: 401,
|
|
175
|
+
headers: { "Content-Type": "application/json" }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const messages = body.messages;
|
|
180
|
+
const stream = new ReadableStream({
|
|
181
|
+
async start(controller) {
|
|
182
|
+
const emit2 = (eventType, data) => controller.enqueue(sseChunk(eventType, data));
|
|
183
|
+
emit2("strand:start", { sessionId: generateId2(), requestId: generateId2() });
|
|
184
|
+
try {
|
|
185
|
+
const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
|
|
186
|
+
const conversation = messages.map((m) => ({
|
|
187
|
+
role: m.role,
|
|
188
|
+
content: m.content
|
|
189
|
+
}));
|
|
190
|
+
let totalInput = 0;
|
|
191
|
+
let totalOutput = 0;
|
|
192
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
193
|
+
const anthropicStream = await client.messages.create({
|
|
194
|
+
model: config.model,
|
|
195
|
+
max_tokens: 8192,
|
|
196
|
+
...system ? { system } : {},
|
|
197
|
+
messages: conversation,
|
|
198
|
+
...anthropicTools.length > 0 ? { tools: anthropicTools } : {},
|
|
199
|
+
stream: true
|
|
200
|
+
});
|
|
201
|
+
let stopReason = null;
|
|
202
|
+
let currentTool = null;
|
|
203
|
+
const completedTools = [];
|
|
204
|
+
const assistantContent = [];
|
|
205
|
+
for await (const event of anthropicStream) {
|
|
206
|
+
if (event.type === "message_start" && event.message.usage) totalInput += event.message.usage.input_tokens;
|
|
207
|
+
if (event.type === "content_block_start") {
|
|
208
|
+
const block = event.content_block;
|
|
209
|
+
if (block.type === "text") assistantContent.push({ type: "text", text: "", citations: null });
|
|
210
|
+
if (block.type === "tool_use") {
|
|
211
|
+
currentTool = { id: block.id, name: block.name, inputJson: "" };
|
|
212
|
+
emit2("strand:tool-start", { toolCallId: block.id, toolName: block.name });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (event.type === "content_block_delta") {
|
|
216
|
+
const delta = event.delta;
|
|
217
|
+
if (delta.type === "text_delta") {
|
|
218
|
+
const last = assistantContent.at(-1);
|
|
219
|
+
if (last?.type === "text") last.text += delta.text;
|
|
220
|
+
emit2("strand:text-delta", { delta: delta.text });
|
|
221
|
+
}
|
|
222
|
+
if (delta.type === "input_json_delta" && currentTool) {
|
|
223
|
+
currentTool.inputJson += delta.partial_json;
|
|
224
|
+
emit2("strand:tool-input-delta", { toolCallId: currentTool.id, delta: delta.partial_json });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (event.type === "content_block_stop" && currentTool) {
|
|
228
|
+
const input = JSON.parse(currentTool.inputJson || "{}");
|
|
229
|
+
completedTools.push({ id: currentTool.id, name: currentTool.name, input });
|
|
230
|
+
assistantContent.push({ type: "tool_use", id: currentTool.id, name: currentTool.name, input });
|
|
231
|
+
currentTool = null;
|
|
232
|
+
}
|
|
233
|
+
if (event.type === "message_delta") {
|
|
234
|
+
stopReason = event.delta.stop_reason ?? null;
|
|
235
|
+
if (event.usage) totalOutput += event.usage.output_tokens;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (stopReason === "end_turn" || stopReason === "max_tokens") {
|
|
239
|
+
emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
if (stopReason === "tool_use" && completedTools.length > 0) {
|
|
243
|
+
conversation.push({ role: "assistant", content: assistantContent });
|
|
244
|
+
const results = await Promise.all(
|
|
245
|
+
completedTools.map(async (block) => {
|
|
246
|
+
emit2("strand:tool-input-done", { toolCallId: block.id, input: block.input });
|
|
247
|
+
try {
|
|
248
|
+
const result = await config.onToolCall?.(block.name, block.input, { request: req });
|
|
249
|
+
emit2("strand:tool-result", { toolCallId: block.id, result });
|
|
250
|
+
return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result ?? null) };
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const message = err instanceof Error ? err.message : "Tool execution failed";
|
|
253
|
+
emit2("strand:tool-error", { toolCallId: block.id, error: message });
|
|
254
|
+
return { type: "tool_result", tool_use_id: block.id, content: message, is_error: true };
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
conversation.push({ role: "user", content: results });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
266
|
+
emit2("strand:error", { code: "server_error", message });
|
|
267
|
+
} finally {
|
|
268
|
+
controller.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return new Response(stream, {
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "text/event-stream",
|
|
275
|
+
"Cache-Control": "no-cache",
|
|
276
|
+
"Connection": "keep-alive"
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export {
|
|
282
|
+
createStrandHandler,
|
|
283
|
+
createStrandRoute
|
|
284
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strand-js/anthropic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"description": "Anthropic provider adapter for Strand",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"module": "./dist/index.mjs",
|
|
@@ -15,9 +16,16 @@
|
|
|
15
16
|
"files": [
|
|
16
17
|
"dist"
|
|
17
18
|
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"lint": "eslint src"
|
|
25
|
+
},
|
|
18
26
|
"dependencies": {
|
|
19
27
|
"@anthropic-ai/sdk": "^0.40.0",
|
|
20
|
-
"@strand-js/core": "
|
|
28
|
+
"@strand-js/core": "workspace:*"
|
|
21
29
|
},
|
|
22
30
|
"devDependencies": {
|
|
23
31
|
"@types/express": "^5.0.0",
|
|
@@ -28,12 +36,5 @@
|
|
|
28
36
|
},
|
|
29
37
|
"publishConfig": {
|
|
30
38
|
"access": "public"
|
|
31
|
-
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
34
|
-
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"lint": "eslint src"
|
|
38
39
|
}
|
|
39
|
-
}
|
|
40
|
+
}
|