@strand-js/openai 0.1.1 → 0.1.3

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,321 @@
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_openai = __toESM(require("openai"));
40
+ var import_core2 = require("@strand-js/core");
41
+
42
+ // src/format.ts
43
+ var import_core = require("@strand-js/core");
44
+ function toolToOpenAITool(tool) {
45
+ const schema = (0, import_core.toolToJsonSchema)(tool);
46
+ return {
47
+ type: "function",
48
+ function: {
49
+ name: tool.name,
50
+ description: tool.description,
51
+ parameters: schema
52
+ }
53
+ };
54
+ }
55
+
56
+ // src/handler.ts
57
+ function emit(res, eventType, data) {
58
+ res.write(`event: ${eventType}
59
+ data: ${JSON.stringify(data)}
60
+
61
+ `);
62
+ }
63
+ function createStrandHandler(config) {
64
+ const client = new import_openai.default({ apiKey: config.apiKey });
65
+ const openAITools = (config.tools ?? []).map(toolToOpenAITool);
66
+ const maxSteps = config.maxSteps ?? 10;
67
+ return async (req, res) => {
68
+ const body = req.body;
69
+ const validation = (0, import_core2.validateMessages)(body?.messages, {
70
+ maxMessages: config.maxMessages,
71
+ maxMessageLength: config.maxMessageLength
72
+ });
73
+ if (!validation.ok) {
74
+ res.status(validation.status).json({ error: validation.error });
75
+ return;
76
+ }
77
+ if (config.authorize) {
78
+ try {
79
+ await config.authorize(req);
80
+ } catch (err) {
81
+ const message = err instanceof Error ? err.message : "Unauthorized";
82
+ res.status(401).json({ error: message });
83
+ return;
84
+ }
85
+ }
86
+ res.setHeader("Content-Type", "text/event-stream");
87
+ res.setHeader("Cache-Control", "no-cache");
88
+ res.setHeader("Connection", "keep-alive");
89
+ emit(res, "strand:start", { sessionId: (0, import_core2.generateId)(), requestId: (0, import_core2.generateId)() });
90
+ try {
91
+ const messages = body.messages;
92
+ const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
93
+ const conversation = [
94
+ ...system ? [{ role: "system", content: system }] : [],
95
+ ...messages.map((m) => ({ role: m.role, content: m.content }))
96
+ ];
97
+ let totalInput = 0;
98
+ let totalOutput = 0;
99
+ for (let step = 0; step < maxSteps; step++) {
100
+ const stream = await client.chat.completions.create({
101
+ model: config.model,
102
+ messages: conversation,
103
+ ...openAITools.length > 0 ? { tools: openAITools } : {},
104
+ stream: true,
105
+ stream_options: { include_usage: true }
106
+ });
107
+ let finishReason = null;
108
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
109
+ for await (const chunk of stream) {
110
+ const choice = chunk.choices[0];
111
+ if (!choice) {
112
+ if (chunk.usage) {
113
+ totalInput += chunk.usage.prompt_tokens;
114
+ totalOutput += chunk.usage.completion_tokens;
115
+ }
116
+ continue;
117
+ }
118
+ const delta = choice.delta;
119
+ if (delta.content) emit(res, "strand:text-delta", { delta: delta.content });
120
+ if (delta.tool_calls) {
121
+ for (const tc of delta.tool_calls) {
122
+ if (tc.id) {
123
+ toolCallAccumulator.set(tc.index, { id: tc.id, name: tc.function?.name ?? "", argsJson: "" });
124
+ emit(res, "strand:tool-start", { toolCallId: tc.id, toolName: tc.function?.name ?? "" });
125
+ }
126
+ if (tc.function?.arguments) {
127
+ const acc = toolCallAccumulator.get(tc.index);
128
+ if (acc) {
129
+ acc.argsJson += tc.function.arguments;
130
+ emit(res, "strand:tool-input-delta", { toolCallId: acc.id, delta: tc.function.arguments });
131
+ }
132
+ }
133
+ }
134
+ }
135
+ if (choice.finish_reason) finishReason = choice.finish_reason;
136
+ }
137
+ if (finishReason === "stop") {
138
+ emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
139
+ res.end();
140
+ return;
141
+ }
142
+ if (finishReason === "tool_calls" && toolCallAccumulator.size > 0) {
143
+ const completedTools = Array.from(toolCallAccumulator.values()).map((acc) => ({
144
+ id: acc.id,
145
+ name: acc.name,
146
+ input: JSON.parse(acc.argsJson || "{}")
147
+ }));
148
+ conversation.push({
149
+ role: "assistant",
150
+ content: null,
151
+ tool_calls: completedTools.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))
152
+ });
153
+ const toolResults = await Promise.all(
154
+ completedTools.map(async (block) => {
155
+ emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
156
+ try {
157
+ const result = await config.onToolCall?.(block.name, block.input, { request: req });
158
+ emit(res, "strand:tool-result", { toolCallId: block.id, result });
159
+ return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
160
+ } catch (err) {
161
+ const message = err instanceof Error ? err.message : "Tool execution failed";
162
+ emit(res, "strand:tool-error", { toolCallId: block.id, error: message });
163
+ return { role: "tool", tool_call_id: block.id, content: message };
164
+ }
165
+ })
166
+ );
167
+ conversation.push(...toolResults);
168
+ continue;
169
+ }
170
+ break;
171
+ }
172
+ emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
173
+ res.end();
174
+ } catch (err) {
175
+ const message = err instanceof Error ? err.message : "Internal server error";
176
+ emit(res, "strand:error", { code: "server_error", message });
177
+ res.end();
178
+ }
179
+ };
180
+ }
181
+
182
+ // src/route.ts
183
+ var import_openai2 = __toESM(require("openai"));
184
+ var import_core3 = require("@strand-js/core");
185
+ function sseChunk(eventType, data) {
186
+ return new TextEncoder().encode(`event: ${eventType}
187
+ data: ${JSON.stringify(data)}
188
+
189
+ `);
190
+ }
191
+ function createStrandRoute(config) {
192
+ const client = new import_openai2.default({ apiKey: config.apiKey });
193
+ const openAITools = (config.tools ?? []).map(toolToOpenAITool);
194
+ const maxSteps = config.maxSteps ?? 10;
195
+ return async (req) => {
196
+ const body = await req.json();
197
+ const validation = (0, import_core3.validateMessages)(body?.messages, {
198
+ maxMessages: config.maxMessages,
199
+ maxMessageLength: config.maxMessageLength
200
+ });
201
+ if (!validation.ok) {
202
+ return new Response(JSON.stringify({ error: validation.error }), {
203
+ status: validation.status,
204
+ headers: { "Content-Type": "application/json" }
205
+ });
206
+ }
207
+ if (config.authorize) {
208
+ try {
209
+ await config.authorize(req);
210
+ } catch (err) {
211
+ const message = err instanceof Error ? err.message : "Unauthorized";
212
+ return new Response(JSON.stringify({ error: message }), {
213
+ status: 401,
214
+ headers: { "Content-Type": "application/json" }
215
+ });
216
+ }
217
+ }
218
+ const messages = body.messages;
219
+ const stream = new ReadableStream({
220
+ async start(controller) {
221
+ const emit2 = (eventType, data) => controller.enqueue(sseChunk(eventType, data));
222
+ emit2("strand:start", { sessionId: (0, import_core3.generateId)(), requestId: (0, import_core3.generateId)() });
223
+ try {
224
+ const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
225
+ const conversation = [
226
+ ...system ? [{ role: "system", content: system }] : [],
227
+ ...messages.map((m) => ({ role: m.role, content: m.content }))
228
+ ];
229
+ let totalInput = 0;
230
+ let totalOutput = 0;
231
+ for (let step = 0; step < maxSteps; step++) {
232
+ const openAIStream = await client.chat.completions.create({
233
+ model: config.model,
234
+ messages: conversation,
235
+ ...openAITools.length > 0 ? { tools: openAITools } : {},
236
+ stream: true,
237
+ stream_options: { include_usage: true }
238
+ });
239
+ let finishReason = null;
240
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
241
+ for await (const chunk of openAIStream) {
242
+ const choice = chunk.choices[0];
243
+ if (!choice) {
244
+ if (chunk.usage) {
245
+ totalInput += chunk.usage.prompt_tokens;
246
+ totalOutput += chunk.usage.completion_tokens;
247
+ }
248
+ continue;
249
+ }
250
+ const delta = choice.delta;
251
+ if (delta.content) emit2("strand:text-delta", { delta: delta.content });
252
+ if (delta.tool_calls) {
253
+ for (const tc of delta.tool_calls) {
254
+ if (tc.id) {
255
+ toolCallAccumulator.set(tc.index, { id: tc.id, name: tc.function?.name ?? "", argsJson: "" });
256
+ emit2("strand:tool-start", { toolCallId: tc.id, toolName: tc.function?.name ?? "" });
257
+ }
258
+ if (tc.function?.arguments) {
259
+ const acc = toolCallAccumulator.get(tc.index);
260
+ if (acc) {
261
+ acc.argsJson += tc.function.arguments;
262
+ emit2("strand:tool-input-delta", { toolCallId: acc.id, delta: tc.function.arguments });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ if (choice.finish_reason) finishReason = choice.finish_reason;
268
+ }
269
+ if (finishReason === "stop") {
270
+ emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
271
+ break;
272
+ }
273
+ if (finishReason === "tool_calls" && toolCallAccumulator.size > 0) {
274
+ const completedTools = Array.from(toolCallAccumulator.values()).map((acc) => ({
275
+ id: acc.id,
276
+ name: acc.name,
277
+ input: JSON.parse(acc.argsJson || "{}")
278
+ }));
279
+ conversation.push({
280
+ role: "assistant",
281
+ content: null,
282
+ tool_calls: completedTools.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))
283
+ });
284
+ const results = await Promise.all(
285
+ completedTools.map(async (block) => {
286
+ emit2("strand:tool-input-done", { toolCallId: block.id, input: block.input });
287
+ try {
288
+ const result = await config.onToolCall?.(block.name, block.input, { request: req });
289
+ emit2("strand:tool-result", { toolCallId: block.id, result });
290
+ return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
291
+ } catch (err) {
292
+ const message = err instanceof Error ? err.message : "Tool execution failed";
293
+ emit2("strand:tool-error", { toolCallId: block.id, error: message });
294
+ return { role: "tool", tool_call_id: block.id, content: message };
295
+ }
296
+ })
297
+ );
298
+ conversation.push(...results);
299
+ continue;
300
+ }
301
+ emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
302
+ break;
303
+ }
304
+ } catch (err) {
305
+ const message = err instanceof Error ? err.message : "Internal server error";
306
+ emit2("strand:error", { code: "server_error", message });
307
+ } finally {
308
+ controller.close();
309
+ }
310
+ }
311
+ });
312
+ return new Response(stream, {
313
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }
314
+ });
315
+ };
316
+ }
317
+ // Annotate the CommonJS export names for ESM import in node:
318
+ 0 && (module.exports = {
319
+ createStrandHandler,
320
+ createStrandRoute
321
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,283 @@
1
+ // src/handler.ts
2
+ import OpenAI from "openai";
3
+ import { generateId, validateMessages } from "@strand-js/core";
4
+
5
+ // src/format.ts
6
+ import { toolToJsonSchema } from "@strand-js/core";
7
+ function toolToOpenAITool(tool) {
8
+ const schema = toolToJsonSchema(tool);
9
+ return {
10
+ type: "function",
11
+ function: {
12
+ name: tool.name,
13
+ description: tool.description,
14
+ parameters: schema
15
+ }
16
+ };
17
+ }
18
+
19
+ // src/handler.ts
20
+ function emit(res, eventType, data) {
21
+ res.write(`event: ${eventType}
22
+ data: ${JSON.stringify(data)}
23
+
24
+ `);
25
+ }
26
+ function createStrandHandler(config) {
27
+ const client = new OpenAI({ apiKey: config.apiKey });
28
+ const openAITools = (config.tools ?? []).map(toolToOpenAITool);
29
+ const maxSteps = config.maxSteps ?? 10;
30
+ return async (req, res) => {
31
+ const body = req.body;
32
+ const validation = validateMessages(body?.messages, {
33
+ maxMessages: config.maxMessages,
34
+ maxMessageLength: config.maxMessageLength
35
+ });
36
+ if (!validation.ok) {
37
+ res.status(validation.status).json({ error: validation.error });
38
+ return;
39
+ }
40
+ if (config.authorize) {
41
+ try {
42
+ await config.authorize(req);
43
+ } catch (err) {
44
+ const message = err instanceof Error ? err.message : "Unauthorized";
45
+ res.status(401).json({ error: message });
46
+ return;
47
+ }
48
+ }
49
+ res.setHeader("Content-Type", "text/event-stream");
50
+ res.setHeader("Cache-Control", "no-cache");
51
+ res.setHeader("Connection", "keep-alive");
52
+ emit(res, "strand:start", { sessionId: generateId(), requestId: generateId() });
53
+ try {
54
+ const messages = body.messages;
55
+ const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
56
+ const conversation = [
57
+ ...system ? [{ role: "system", content: system }] : [],
58
+ ...messages.map((m) => ({ role: m.role, content: m.content }))
59
+ ];
60
+ let totalInput = 0;
61
+ let totalOutput = 0;
62
+ for (let step = 0; step < maxSteps; step++) {
63
+ const stream = await client.chat.completions.create({
64
+ model: config.model,
65
+ messages: conversation,
66
+ ...openAITools.length > 0 ? { tools: openAITools } : {},
67
+ stream: true,
68
+ stream_options: { include_usage: true }
69
+ });
70
+ let finishReason = null;
71
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
72
+ for await (const chunk of stream) {
73
+ const choice = chunk.choices[0];
74
+ if (!choice) {
75
+ if (chunk.usage) {
76
+ totalInput += chunk.usage.prompt_tokens;
77
+ totalOutput += chunk.usage.completion_tokens;
78
+ }
79
+ continue;
80
+ }
81
+ const delta = choice.delta;
82
+ if (delta.content) emit(res, "strand:text-delta", { delta: delta.content });
83
+ if (delta.tool_calls) {
84
+ for (const tc of delta.tool_calls) {
85
+ if (tc.id) {
86
+ toolCallAccumulator.set(tc.index, { id: tc.id, name: tc.function?.name ?? "", argsJson: "" });
87
+ emit(res, "strand:tool-start", { toolCallId: tc.id, toolName: tc.function?.name ?? "" });
88
+ }
89
+ if (tc.function?.arguments) {
90
+ const acc = toolCallAccumulator.get(tc.index);
91
+ if (acc) {
92
+ acc.argsJson += tc.function.arguments;
93
+ emit(res, "strand:tool-input-delta", { toolCallId: acc.id, delta: tc.function.arguments });
94
+ }
95
+ }
96
+ }
97
+ }
98
+ if (choice.finish_reason) finishReason = choice.finish_reason;
99
+ }
100
+ if (finishReason === "stop") {
101
+ emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
102
+ res.end();
103
+ return;
104
+ }
105
+ if (finishReason === "tool_calls" && toolCallAccumulator.size > 0) {
106
+ const completedTools = Array.from(toolCallAccumulator.values()).map((acc) => ({
107
+ id: acc.id,
108
+ name: acc.name,
109
+ input: JSON.parse(acc.argsJson || "{}")
110
+ }));
111
+ conversation.push({
112
+ role: "assistant",
113
+ content: null,
114
+ tool_calls: completedTools.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))
115
+ });
116
+ const toolResults = await Promise.all(
117
+ completedTools.map(async (block) => {
118
+ emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
119
+ try {
120
+ const result = await config.onToolCall?.(block.name, block.input, { request: req });
121
+ emit(res, "strand:tool-result", { toolCallId: block.id, result });
122
+ return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
123
+ } catch (err) {
124
+ const message = err instanceof Error ? err.message : "Tool execution failed";
125
+ emit(res, "strand:tool-error", { toolCallId: block.id, error: message });
126
+ return { role: "tool", tool_call_id: block.id, content: message };
127
+ }
128
+ })
129
+ );
130
+ conversation.push(...toolResults);
131
+ continue;
132
+ }
133
+ break;
134
+ }
135
+ emit(res, "strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
136
+ res.end();
137
+ } catch (err) {
138
+ const message = err instanceof Error ? err.message : "Internal server error";
139
+ emit(res, "strand:error", { code: "server_error", message });
140
+ res.end();
141
+ }
142
+ };
143
+ }
144
+
145
+ // src/route.ts
146
+ import OpenAI2 from "openai";
147
+ import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
148
+ function sseChunk(eventType, data) {
149
+ return new TextEncoder().encode(`event: ${eventType}
150
+ data: ${JSON.stringify(data)}
151
+
152
+ `);
153
+ }
154
+ function createStrandRoute(config) {
155
+ const client = new OpenAI2({ apiKey: config.apiKey });
156
+ const openAITools = (config.tools ?? []).map(toolToOpenAITool);
157
+ const maxSteps = config.maxSteps ?? 10;
158
+ return async (req) => {
159
+ const body = await req.json();
160
+ const validation = validateMessages2(body?.messages, {
161
+ maxMessages: config.maxMessages,
162
+ maxMessageLength: config.maxMessageLength
163
+ });
164
+ if (!validation.ok) {
165
+ return new Response(JSON.stringify({ error: validation.error }), {
166
+ status: validation.status,
167
+ headers: { "Content-Type": "application/json" }
168
+ });
169
+ }
170
+ if (config.authorize) {
171
+ try {
172
+ await config.authorize(req);
173
+ } catch (err) {
174
+ const message = err instanceof Error ? err.message : "Unauthorized";
175
+ return new Response(JSON.stringify({ error: message }), {
176
+ status: 401,
177
+ headers: { "Content-Type": "application/json" }
178
+ });
179
+ }
180
+ }
181
+ const messages = body.messages;
182
+ const stream = new ReadableStream({
183
+ async start(controller) {
184
+ const emit2 = (eventType, data) => controller.enqueue(sseChunk(eventType, data));
185
+ emit2("strand:start", { sessionId: generateId2(), requestId: generateId2() });
186
+ try {
187
+ const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
188
+ const conversation = [
189
+ ...system ? [{ role: "system", content: system }] : [],
190
+ ...messages.map((m) => ({ role: m.role, content: m.content }))
191
+ ];
192
+ let totalInput = 0;
193
+ let totalOutput = 0;
194
+ for (let step = 0; step < maxSteps; step++) {
195
+ const openAIStream = await client.chat.completions.create({
196
+ model: config.model,
197
+ messages: conversation,
198
+ ...openAITools.length > 0 ? { tools: openAITools } : {},
199
+ stream: true,
200
+ stream_options: { include_usage: true }
201
+ });
202
+ let finishReason = null;
203
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
204
+ for await (const chunk of openAIStream) {
205
+ const choice = chunk.choices[0];
206
+ if (!choice) {
207
+ if (chunk.usage) {
208
+ totalInput += chunk.usage.prompt_tokens;
209
+ totalOutput += chunk.usage.completion_tokens;
210
+ }
211
+ continue;
212
+ }
213
+ const delta = choice.delta;
214
+ if (delta.content) emit2("strand:text-delta", { delta: delta.content });
215
+ if (delta.tool_calls) {
216
+ for (const tc of delta.tool_calls) {
217
+ if (tc.id) {
218
+ toolCallAccumulator.set(tc.index, { id: tc.id, name: tc.function?.name ?? "", argsJson: "" });
219
+ emit2("strand:tool-start", { toolCallId: tc.id, toolName: tc.function?.name ?? "" });
220
+ }
221
+ if (tc.function?.arguments) {
222
+ const acc = toolCallAccumulator.get(tc.index);
223
+ if (acc) {
224
+ acc.argsJson += tc.function.arguments;
225
+ emit2("strand:tool-input-delta", { toolCallId: acc.id, delta: tc.function.arguments });
226
+ }
227
+ }
228
+ }
229
+ }
230
+ if (choice.finish_reason) finishReason = choice.finish_reason;
231
+ }
232
+ if (finishReason === "stop") {
233
+ emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
234
+ break;
235
+ }
236
+ if (finishReason === "tool_calls" && toolCallAccumulator.size > 0) {
237
+ const completedTools = Array.from(toolCallAccumulator.values()).map((acc) => ({
238
+ id: acc.id,
239
+ name: acc.name,
240
+ input: JSON.parse(acc.argsJson || "{}")
241
+ }));
242
+ conversation.push({
243
+ role: "assistant",
244
+ content: null,
245
+ tool_calls: completedTools.map((tc) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))
246
+ });
247
+ const results = await Promise.all(
248
+ completedTools.map(async (block) => {
249
+ emit2("strand:tool-input-done", { toolCallId: block.id, input: block.input });
250
+ try {
251
+ const result = await config.onToolCall?.(block.name, block.input, { request: req });
252
+ emit2("strand:tool-result", { toolCallId: block.id, result });
253
+ return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
254
+ } catch (err) {
255
+ const message = err instanceof Error ? err.message : "Tool execution failed";
256
+ emit2("strand:tool-error", { toolCallId: block.id, error: message });
257
+ return { role: "tool", tool_call_id: block.id, content: message };
258
+ }
259
+ })
260
+ );
261
+ conversation.push(...results);
262
+ continue;
263
+ }
264
+ emit2("strand:done", { usage: { input: totalInput, output: totalOutput, total: totalInput + totalOutput } });
265
+ break;
266
+ }
267
+ } catch (err) {
268
+ const message = err instanceof Error ? err.message : "Internal server error";
269
+ emit2("strand:error", { code: "server_error", message });
270
+ } finally {
271
+ controller.close();
272
+ }
273
+ }
274
+ });
275
+ return new Response(stream, {
276
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }
277
+ });
278
+ };
279
+ }
280
+ export {
281
+ createStrandHandler,
282
+ createStrandRoute
283
+ };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@strand-js/openai",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
+ "license": "MIT",
4
5
  "description": "OpenAI provider adapter for Strand",
5
6
  "main": "./dist/index.js",
6
7
  "module": "./dist/index.mjs",
@@ -17,7 +18,7 @@
17
18
  ],
18
19
  "dependencies": {
19
20
  "openai": "^4.77.0",
20
- "@strand-js/core": "0.1.1"
21
+ "@strand-js/core": "0.1.3"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/express": "^5.0.0",