@strand-js/anthropic 0.1.1 → 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.
@@ -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 };
@@ -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.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": "0.1.1"
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
+ }