@hyorman/copilot-proxy-core 1.0.0

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.
Files changed (58) hide show
  1. package/out/assistants/index.d.ts +15 -0
  2. package/out/assistants/index.js +16 -0
  3. package/out/assistants/index.js.map +1 -0
  4. package/out/assistants/routes.d.ts +16 -0
  5. package/out/assistants/routes.js +597 -0
  6. package/out/assistants/routes.js.map +1 -0
  7. package/out/assistants/runner.d.ts +48 -0
  8. package/out/assistants/runner.js +851 -0
  9. package/out/assistants/runner.js.map +1 -0
  10. package/out/assistants/state.d.ts +81 -0
  11. package/out/assistants/state.js +351 -0
  12. package/out/assistants/state.js.map +1 -0
  13. package/out/assistants/tools.d.ts +4 -0
  14. package/out/assistants/tools.js +8 -0
  15. package/out/assistants/tools.js.map +1 -0
  16. package/out/assistants/types.d.ts +254 -0
  17. package/out/assistants/types.js +5 -0
  18. package/out/assistants/types.js.map +1 -0
  19. package/out/backend.d.ts +24 -0
  20. package/out/backend.js +12 -0
  21. package/out/backend.js.map +1 -0
  22. package/out/index.d.ts +13 -0
  23. package/out/index.js +21 -0
  24. package/out/index.js.map +1 -0
  25. package/out/server.d.ts +12 -0
  26. package/out/server.js +504 -0
  27. package/out/server.js.map +1 -0
  28. package/out/skills/index.d.ts +3 -0
  29. package/out/skills/index.js +4 -0
  30. package/out/skills/index.js.map +1 -0
  31. package/out/skills/manifest.d.ts +25 -0
  32. package/out/skills/manifest.js +96 -0
  33. package/out/skills/manifest.js.map +1 -0
  34. package/out/skills/resolver.d.ts +22 -0
  35. package/out/skills/resolver.js +66 -0
  36. package/out/skills/resolver.js.map +1 -0
  37. package/out/skills/routes.d.ts +3 -0
  38. package/out/skills/routes.js +191 -0
  39. package/out/skills/routes.js.map +1 -0
  40. package/out/skills/state.d.ts +35 -0
  41. package/out/skills/state.js +155 -0
  42. package/out/skills/state.js.map +1 -0
  43. package/out/skills/storage.d.ts +30 -0
  44. package/out/skills/storage.js +171 -0
  45. package/out/skills/storage.js.map +1 -0
  46. package/out/skills/types.d.ts +141 -0
  47. package/out/skills/types.js +8 -0
  48. package/out/skills/types.js.map +1 -0
  49. package/out/toolConvert.d.ts +24 -0
  50. package/out/toolConvert.js +56 -0
  51. package/out/toolConvert.js.map +1 -0
  52. package/out/types.d.ts +291 -0
  53. package/out/types.js +2 -0
  54. package/out/types.js.map +1 -0
  55. package/out/utils.d.ts +28 -0
  56. package/out/utils.js +81 -0
  57. package/out/utils.js.map +1 -0
  58. package/package.json +36 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Assistants API Module
3
+ *
4
+ * Exports all assistants-related functionality:
5
+ * - Types for Assistant, Thread, Message, Run, RunStep, StreamEvent
6
+ * - State management with persistence support
7
+ * - Run execution engine with streaming and native tool calling
8
+ * - Tool utilities for ID generation and format conversion
9
+ * - Express routes
10
+ */
11
+ export * from './types.js';
12
+ export { state, SerializedState, PendingToolContext } from './state.js';
13
+ export { executeRun, executeRunNonStreaming, requestRunCancellation, isRunActive, continueRunWithToolOutputs, continueRunWithToolOutputsNonStreaming, setRunnerBackend, } from './runner.js';
14
+ export { generateToolCallId, } from './tools.js';
15
+ export { default as assistantsRouter } from './routes.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Assistants API Module
3
+ *
4
+ * Exports all assistants-related functionality:
5
+ * - Types for Assistant, Thread, Message, Run, RunStep, StreamEvent
6
+ * - State management with persistence support
7
+ * - Run execution engine with streaming and native tool calling
8
+ * - Tool utilities for ID generation and format conversion
9
+ * - Express routes
10
+ */
11
+ export * from './types.js';
12
+ export { state } from './state.js';
13
+ export { executeRun, executeRunNonStreaming, requestRunCancellation, isRunActive, continueRunWithToolOutputs, continueRunWithToolOutputsNonStreaming, setRunnerBackend, } from './runner.js';
14
+ export { generateToolCallId, } from './tools.js';
15
+ export { default as assistantsRouter } from './routes.js';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/assistants/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,KAAK,EAAuC,MAAM,YAAY,CAAC;AACxE,OAAO,EACL,UAAU,EACV,sBAAsB,EACtB,sBAAsB,EACtB,WAAW,EACX,0BAA0B,EAC1B,sCAAsC,EACtC,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Express Routes for OpenAI Assistants API
3
+ *
4
+ * Implements all CRUD operations for:
5
+ * - /v1/assistants
6
+ * - /v1/threads
7
+ * - /v1/threads/:thread_id/messages
8
+ * - /v1/threads/:thread_id/runs
9
+ *
10
+ * Future extensibility:
11
+ * - /v1/threads/runs (create thread and run)
12
+ * - /v1/threads/:thread_id/runs/:run_id/steps
13
+ * - /v1/threads/:thread_id/runs/:run_id/submit_tool_outputs
14
+ */
15
+ declare const router: import("express-serve-static-core").Router;
16
+ export default router;
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Express Routes for OpenAI Assistants API
3
+ *
4
+ * Implements all CRUD operations for:
5
+ * - /v1/assistants
6
+ * - /v1/threads
7
+ * - /v1/threads/:thread_id/messages
8
+ * - /v1/threads/:thread_id/runs
9
+ *
10
+ * Future extensibility:
11
+ * - /v1/threads/runs (create thread and run)
12
+ * - /v1/threads/:thread_id/runs/:run_id/steps
13
+ * - /v1/threads/:thread_id/runs/:run_id/submit_tool_outputs
14
+ */
15
+ import { Router } from 'express';
16
+ import { state } from './state.js';
17
+ import { executeRun, executeRunNonStreaming, requestRunCancellation, continueRunWithToolOutputs, continueRunWithToolOutputsNonStreaming } from './runner.js';
18
+ import { errorResponse, notFoundError, createMessage } from '../utils.js';
19
+ const router = Router();
20
+ // ==================== Validation Helpers ======================================
21
+ function validateRequired(body, fields) {
22
+ for (const field of fields) {
23
+ if (body[field] === undefined || body[field] === null) {
24
+ return `Missing required field: ${String(field)}`;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ function parsePaginationParams(query) {
30
+ return {
31
+ limit: query.limit ? Math.min(parseInt(query.limit, 10), 100) : 20,
32
+ order: query.order ?? 'desc',
33
+ after: query.after,
34
+ before: query.before
35
+ };
36
+ }
37
+ // ==================== Assistants Routes ====================
38
+ // Create assistant
39
+ router.post('/v1/assistants', (req, res) => {
40
+ const body = req.body;
41
+ const validationError = validateRequired(body, ['model']);
42
+ if (validationError) {
43
+ return res.status(400).json(errorResponse(validationError, 'invalid_request_error', 'model'));
44
+ }
45
+ const assistant = {
46
+ id: state.generateAssistantId(),
47
+ object: 'assistant',
48
+ created_at: Math.floor(Date.now() / 1000),
49
+ name: body.name ?? null,
50
+ description: body.description ?? null,
51
+ model: body.model,
52
+ instructions: body.instructions ?? null,
53
+ tools: body.tools ?? [],
54
+ skills: body.skills ?? [],
55
+ metadata: body.metadata ?? {}
56
+ };
57
+ state.createAssistant(assistant);
58
+ res.status(201).json(assistant);
59
+ });
60
+ // List assistants
61
+ router.get('/v1/assistants', (req, res) => {
62
+ const params = parsePaginationParams(req.query);
63
+ const result = state.listAssistants(params);
64
+ res.json(result);
65
+ });
66
+ // Get assistant
67
+ router.get('/v1/assistants/:assistant_id', (req, res) => {
68
+ const assistant = state.getAssistant(req.params.assistant_id);
69
+ if (!assistant) {
70
+ return res.status(404).json(notFoundError('assistant'));
71
+ }
72
+ res.json(assistant);
73
+ });
74
+ // Update assistant (POST for OpenAI compatibility)
75
+ router.post('/v1/assistants/:assistant_id', (req, res) => {
76
+ const body = req.body;
77
+ const updated = state.updateAssistant(req.params.assistant_id, body);
78
+ if (!updated) {
79
+ return res.status(404).json(notFoundError('assistant'));
80
+ }
81
+ res.json(updated);
82
+ });
83
+ // Delete assistant
84
+ router.delete('/v1/assistants/:assistant_id', (req, res) => {
85
+ const deleted = state.deleteAssistant(req.params.assistant_id);
86
+ res.json({
87
+ id: req.params.assistant_id,
88
+ object: 'assistant.deleted',
89
+ deleted
90
+ });
91
+ });
92
+ // ==================== Threads Routes ====================
93
+ // Create thread
94
+ router.post('/v1/threads', (req, res) => {
95
+ const body = (req.body || {});
96
+ const thread = {
97
+ id: state.generateThreadId(),
98
+ object: 'thread',
99
+ created_at: Math.floor(Date.now() / 1000),
100
+ metadata: body.metadata ?? {}
101
+ };
102
+ state.createThread(thread);
103
+ // Add initial messages if provided
104
+ if (body.messages && Array.isArray(body.messages)) {
105
+ for (const msg of body.messages) {
106
+ const content = typeof msg.content === 'string'
107
+ ? msg.content
108
+ : JSON.stringify(msg.content);
109
+ const message = createMessage({
110
+ threadId: thread.id,
111
+ messageId: state.generateMessageId(),
112
+ content,
113
+ role: msg.role || 'user',
114
+ attachments: msg.attachments ?? [],
115
+ metadata: msg.metadata ?? {}
116
+ });
117
+ state.addMessage(thread.id, message);
118
+ }
119
+ }
120
+ res.status(201).json(thread);
121
+ });
122
+ // Get thread
123
+ router.get('/v1/threads/:thread_id', (req, res) => {
124
+ const thread = state.getThread(req.params.thread_id);
125
+ if (!thread) {
126
+ return res.status(404).json(notFoundError('thread'));
127
+ }
128
+ res.json(thread);
129
+ });
130
+ // Update thread (POST for OpenAI compatibility)
131
+ router.post('/v1/threads/:thread_id', (req, res) => {
132
+ const updated = state.updateThread(req.params.thread_id, req.body);
133
+ if (!updated) {
134
+ return res.status(404).json(notFoundError('thread'));
135
+ }
136
+ res.json(updated);
137
+ });
138
+ // Delete thread
139
+ router.delete('/v1/threads/:thread_id', (req, res) => {
140
+ const deleted = state.deleteThread(req.params.thread_id);
141
+ res.json({
142
+ id: req.params.thread_id,
143
+ object: 'thread.deleted',
144
+ deleted
145
+ });
146
+ });
147
+ // ==================== Messages Routes ====================
148
+ // Create message
149
+ router.post('/v1/threads/:thread_id/messages', (req, res) => {
150
+ const { thread_id } = req.params;
151
+ const thread = state.getThread(thread_id);
152
+ if (!thread) {
153
+ return res.status(404).json(notFoundError('thread'));
154
+ }
155
+ const body = req.body;
156
+ const validationError = validateRequired(body, ['role', 'content']);
157
+ if (validationError) {
158
+ return res.status(400).json(errorResponse(validationError));
159
+ }
160
+ const content = typeof body.content === 'string'
161
+ ? body.content
162
+ : JSON.stringify(body.content);
163
+ const message = createMessage({
164
+ threadId: thread_id,
165
+ messageId: state.generateMessageId(),
166
+ content,
167
+ role: body.role,
168
+ attachments: body.attachments ?? [],
169
+ metadata: body.metadata ?? {}
170
+ });
171
+ state.addMessage(thread_id, message);
172
+ res.status(201).json(message);
173
+ });
174
+ // List messages
175
+ router.get('/v1/threads/:thread_id/messages', (req, res) => {
176
+ const { thread_id } = req.params;
177
+ const thread = state.getThread(thread_id);
178
+ if (!thread) {
179
+ return res.status(404).json(notFoundError('thread'));
180
+ }
181
+ const params = {
182
+ ...parsePaginationParams(req.query),
183
+ run_id: req.query.run_id
184
+ };
185
+ const result = state.getMessages(thread_id, params);
186
+ res.json(result);
187
+ });
188
+ // Get message
189
+ router.get('/v1/threads/:thread_id/messages/:message_id', (req, res) => {
190
+ const { thread_id, message_id } = req.params;
191
+ const thread = state.getThread(thread_id);
192
+ if (!thread) {
193
+ return res.status(404).json(notFoundError('thread'));
194
+ }
195
+ const message = state.getMessage(thread_id, message_id);
196
+ if (!message) {
197
+ return res.status(404).json(notFoundError('message'));
198
+ }
199
+ res.json(message);
200
+ });
201
+ // Update message (POST for OpenAI compatibility) - only metadata can be updated
202
+ router.post('/v1/threads/:thread_id/messages/:message_id', (req, res) => {
203
+ const { thread_id, message_id } = req.params;
204
+ const thread = state.getThread(thread_id);
205
+ if (!thread) {
206
+ return res.status(404).json(notFoundError('thread'));
207
+ }
208
+ // Only metadata updates allowed
209
+ const updated = state.updateMessage(thread_id, message_id, {
210
+ metadata: req.body.metadata
211
+ });
212
+ if (!updated) {
213
+ return res.status(404).json(notFoundError('message'));
214
+ }
215
+ res.json(updated);
216
+ });
217
+ // Delete message
218
+ router.delete('/v1/threads/:thread_id/messages/:message_id', (req, res) => {
219
+ const { thread_id, message_id } = req.params;
220
+ const thread = state.getThread(thread_id);
221
+ if (!thread) {
222
+ return res.status(404).json(notFoundError('thread'));
223
+ }
224
+ const deleted = state.deleteMessage(thread_id, message_id);
225
+ res.json({
226
+ id: message_id,
227
+ object: 'thread.message.deleted',
228
+ deleted
229
+ });
230
+ });
231
+ // ==================== Runs Routes ====================
232
+ // Create run
233
+ router.post('/v1/threads/:thread_id/runs', async (req, res) => {
234
+ const { thread_id } = req.params;
235
+ const thread = state.getThread(thread_id);
236
+ if (!thread) {
237
+ return res.status(404).json(notFoundError('thread'));
238
+ }
239
+ const body = req.body;
240
+ const validationError = validateRequired(body, ['assistant_id']);
241
+ if (validationError) {
242
+ return res.status(400).json(errorResponse(validationError, 'invalid_request_error', 'assistant_id'));
243
+ }
244
+ const assistant = state.getAssistant(body.assistant_id);
245
+ if (!assistant) {
246
+ return res.status(404).json(notFoundError('assistant'));
247
+ }
248
+ // Add additional messages if provided
249
+ if (body.additional_messages && Array.isArray(body.additional_messages)) {
250
+ for (const msg of body.additional_messages) {
251
+ const content = typeof msg.content === 'string'
252
+ ? msg.content
253
+ : JSON.stringify(msg.content);
254
+ const message = createMessage({
255
+ threadId: thread_id,
256
+ messageId: state.generateMessageId(),
257
+ content,
258
+ role: msg.role || 'user',
259
+ attachments: msg.attachments ?? [],
260
+ metadata: msg.metadata ?? {}
261
+ });
262
+ state.addMessage(thread_id, message);
263
+ }
264
+ }
265
+ const run = {
266
+ id: state.generateRunId(),
267
+ object: 'thread.run',
268
+ created_at: Math.floor(Date.now() / 1000),
269
+ thread_id,
270
+ assistant_id: body.assistant_id,
271
+ status: 'queued',
272
+ required_action: null,
273
+ last_error: null,
274
+ expires_at: Math.floor(Date.now() / 1000) + 600, // 10 minutes
275
+ started_at: null,
276
+ cancelled_at: null,
277
+ failed_at: null,
278
+ completed_at: null,
279
+ incomplete_details: null,
280
+ model: body.model ?? assistant.model,
281
+ instructions: body.instructions ?? null,
282
+ tools: body.tools ?? assistant.tools,
283
+ skills: body.skills ?? assistant.skills,
284
+ metadata: body.metadata ?? {},
285
+ usage: null
286
+ };
287
+ state.addRun(thread_id, run);
288
+ // Check if streaming is requested
289
+ if (body.stream) {
290
+ // Streaming mode: use SSE
291
+ res.setHeader('Content-Type', 'text/event-stream');
292
+ res.setHeader('Cache-Control', 'no-cache');
293
+ res.setHeader('Connection', 'keep-alive');
294
+ res.setHeader('X-Accel-Buffering', 'no');
295
+ // Execute run with streaming
296
+ (async () => {
297
+ try {
298
+ const generator = executeRun(thread_id, run.id, true);
299
+ for await (const event of generator) {
300
+ if (event.event === 'done') {
301
+ res.write(`event: done\ndata: [DONE]\n\n`);
302
+ }
303
+ else {
304
+ res.write(`event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
305
+ }
306
+ }
307
+ }
308
+ catch (err) {
309
+ console.error('Streaming run error:', err);
310
+ res.write(`event: error\ndata: ${JSON.stringify({ error: { message: 'Stream error' } })}\n\n`);
311
+ }
312
+ finally {
313
+ res.end();
314
+ }
315
+ })();
316
+ }
317
+ else {
318
+ // Non-streaming mode: return immediately, execute async
319
+ res.status(201).json(run);
320
+ // Execute in background (don't await)
321
+ executeRunNonStreaming(thread_id, run.id).catch(err => {
322
+ console.error('Run execution failed:', err);
323
+ });
324
+ }
325
+ });
326
+ // List runs
327
+ router.get('/v1/threads/:thread_id/runs', (req, res) => {
328
+ const { thread_id } = req.params;
329
+ const thread = state.getThread(thread_id);
330
+ if (!thread) {
331
+ return res.status(404).json(notFoundError('thread'));
332
+ }
333
+ const params = parsePaginationParams(req.query);
334
+ const result = state.getRuns(thread_id, params);
335
+ res.json(result);
336
+ });
337
+ // Get run
338
+ router.get('/v1/threads/:thread_id/runs/:run_id', (req, res) => {
339
+ const { thread_id, run_id } = req.params;
340
+ const thread = state.getThread(thread_id);
341
+ if (!thread) {
342
+ return res.status(404).json(notFoundError('thread'));
343
+ }
344
+ const run = state.getRun(thread_id, run_id);
345
+ if (!run) {
346
+ return res.status(404).json(notFoundError('run'));
347
+ }
348
+ res.json(run);
349
+ });
350
+ // Update run (POST for OpenAI compatibility) - only metadata can be updated
351
+ router.post('/v1/threads/:thread_id/runs/:run_id', (req, res) => {
352
+ const { thread_id, run_id } = req.params;
353
+ const thread = state.getThread(thread_id);
354
+ if (!thread) {
355
+ return res.status(404).json(notFoundError('thread'));
356
+ }
357
+ const updated = state.updateRun(thread_id, run_id, {
358
+ metadata: req.body.metadata
359
+ });
360
+ if (!updated) {
361
+ return res.status(404).json(notFoundError('run'));
362
+ }
363
+ res.json(updated);
364
+ });
365
+ // Cancel run
366
+ router.post('/v1/threads/:thread_id/runs/:run_id/cancel', (req, res) => {
367
+ const { thread_id, run_id } = req.params;
368
+ const thread = state.getThread(thread_id);
369
+ if (!thread) {
370
+ return res.status(404).json(notFoundError('thread'));
371
+ }
372
+ const run = state.getRun(thread_id, run_id);
373
+ if (!run) {
374
+ return res.status(404).json(notFoundError('run'));
375
+ }
376
+ // Check if run can be cancelled
377
+ const cancellableStatuses = ['queued', 'in_progress', 'requires_action'];
378
+ if (!cancellableStatuses.includes(run.status)) {
379
+ return res.status(400).json(errorResponse(`Cannot cancel run with status: ${run.status}`, 'invalid_request_error', 'status'));
380
+ }
381
+ // Request cancellation
382
+ requestRunCancellation(thread_id, run_id);
383
+ const updated = state.updateRun(thread_id, run_id, {
384
+ status: 'cancelling',
385
+ cancelled_at: Math.floor(Date.now() / 1000)
386
+ });
387
+ // After a short delay, mark as cancelled
388
+ setTimeout(() => {
389
+ const currentRun = state.getRun(thread_id, run_id);
390
+ if (currentRun?.status === 'cancelling') {
391
+ state.updateRun(thread_id, run_id, { status: 'cancelled' });
392
+ }
393
+ }, 100);
394
+ res.json(updated);
395
+ });
396
+ // ==================== Create Thread and Run ====================
397
+ router.post('/v1/threads/runs', async (req, res) => {
398
+ const body = req.body;
399
+ const validationError = validateRequired(body, ['assistant_id']);
400
+ if (validationError) {
401
+ return res.status(400).json(errorResponse(validationError, 'invalid_request_error', 'assistant_id'));
402
+ }
403
+ const assistant = state.getAssistant(body.assistant_id);
404
+ if (!assistant) {
405
+ return res.status(404).json(notFoundError('assistant'));
406
+ }
407
+ // Create thread
408
+ const threadBody = body.thread ?? {};
409
+ const thread = {
410
+ id: state.generateThreadId(),
411
+ object: 'thread',
412
+ created_at: Math.floor(Date.now() / 1000),
413
+ metadata: threadBody.metadata ?? {}
414
+ };
415
+ state.createThread(thread);
416
+ // Add initial messages if provided
417
+ if (threadBody.messages && Array.isArray(threadBody.messages)) {
418
+ for (const msg of threadBody.messages) {
419
+ const content = typeof msg.content === 'string'
420
+ ? msg.content
421
+ : JSON.stringify(msg.content);
422
+ const message = createMessage({
423
+ threadId: thread.id,
424
+ messageId: state.generateMessageId(),
425
+ content,
426
+ role: msg.role || 'user',
427
+ attachments: msg.attachments ?? [],
428
+ metadata: msg.metadata ?? {}
429
+ });
430
+ state.addMessage(thread.id, message);
431
+ }
432
+ }
433
+ // Create run
434
+ const run = {
435
+ id: state.generateRunId(),
436
+ object: 'thread.run',
437
+ created_at: Math.floor(Date.now() / 1000),
438
+ thread_id: thread.id,
439
+ assistant_id: body.assistant_id,
440
+ status: 'queued',
441
+ required_action: null,
442
+ last_error: null,
443
+ expires_at: Math.floor(Date.now() / 1000) + 600,
444
+ started_at: null,
445
+ cancelled_at: null,
446
+ failed_at: null,
447
+ completed_at: null,
448
+ incomplete_details: null,
449
+ model: body.model ?? assistant.model,
450
+ instructions: body.instructions ?? null,
451
+ tools: body.tools ?? assistant.tools,
452
+ skills: body.skills ?? assistant.skills,
453
+ metadata: body.metadata ?? {},
454
+ usage: null
455
+ };
456
+ state.addRun(thread.id, run);
457
+ // Check if streaming is requested
458
+ if (body.stream) {
459
+ // Streaming mode: use SSE
460
+ res.setHeader('Content-Type', 'text/event-stream');
461
+ res.setHeader('Cache-Control', 'no-cache');
462
+ res.setHeader('Connection', 'keep-alive');
463
+ res.setHeader('X-Accel-Buffering', 'no');
464
+ // Execute run with streaming
465
+ (async () => {
466
+ try {
467
+ const generator = executeRun(thread.id, run.id, true);
468
+ for await (const event of generator) {
469
+ if (event.event === 'done') {
470
+ res.write(`event: done\ndata: [DONE]\n\n`);
471
+ }
472
+ else {
473
+ res.write(`event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
474
+ }
475
+ }
476
+ }
477
+ catch (err) {
478
+ console.error('Streaming run error:', err);
479
+ res.write(`event: error\ndata: ${JSON.stringify({ error: { message: 'Stream error' } })}\n\n`);
480
+ }
481
+ finally {
482
+ res.end();
483
+ }
484
+ })();
485
+ }
486
+ else {
487
+ // Non-streaming mode: return immediately, execute async
488
+ res.status(201).json(run);
489
+ // Execute in background
490
+ executeRunNonStreaming(thread.id, run.id).catch(err => {
491
+ console.error('Run execution failed:', err);
492
+ });
493
+ }
494
+ });
495
+ // ==================== Submit Tool Outputs ====================
496
+ router.post('/v1/threads/:thread_id/runs/:run_id/submit_tool_outputs', async (req, res) => {
497
+ const { thread_id, run_id } = req.params;
498
+ const thread = state.getThread(thread_id);
499
+ if (!thread) {
500
+ return res.status(404).json(notFoundError('thread'));
501
+ }
502
+ const run = state.getRun(thread_id, run_id);
503
+ if (!run) {
504
+ return res.status(404).json(notFoundError('run'));
505
+ }
506
+ // Check if run is in requires_action status
507
+ if (run.status !== 'requires_action') {
508
+ return res.status(400).json(errorResponse(`Run is not in requires_action status. Current status: ${run.status}`, 'invalid_request_error', 'status'));
509
+ }
510
+ const body = req.body;
511
+ const validationError = validateRequired(body, ['tool_outputs']);
512
+ if (validationError) {
513
+ return res.status(400).json(errorResponse(validationError, 'invalid_request_error', 'tool_outputs'));
514
+ }
515
+ // Validate that all required tool calls are provided
516
+ const requiredToolCallIds = new Set(run.required_action?.submit_tool_outputs.tool_calls.map(tc => tc.id) ?? []);
517
+ const providedToolCallIds = new Set(body.tool_outputs.map(o => o.tool_call_id));
518
+ for (const requiredId of requiredToolCallIds) {
519
+ if (!providedToolCallIds.has(requiredId)) {
520
+ return res.status(400).json(errorResponse(`Missing output for tool call: ${requiredId}`, 'invalid_request_error', 'tool_outputs'));
521
+ }
522
+ }
523
+ // Check if streaming is requested
524
+ if (body.stream) {
525
+ res.setHeader('Content-Type', 'text/event-stream');
526
+ res.setHeader('Cache-Control', 'no-cache');
527
+ res.setHeader('Connection', 'keep-alive');
528
+ res.setHeader('X-Accel-Buffering', 'no');
529
+ (async () => {
530
+ try {
531
+ const generator = continueRunWithToolOutputs(thread_id, run_id, body.tool_outputs, true);
532
+ for await (const event of generator) {
533
+ if (event.event === 'done') {
534
+ res.write(`event: done\ndata: [DONE]\n\n`);
535
+ }
536
+ else {
537
+ res.write(`event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
538
+ }
539
+ }
540
+ }
541
+ catch (err) {
542
+ console.error('Streaming continue error:', err);
543
+ res.write(`event: error\ndata: ${JSON.stringify({ error: { message: 'Stream error' } })}\n\n`);
544
+ }
545
+ finally {
546
+ res.end();
547
+ }
548
+ })();
549
+ }
550
+ else {
551
+ // Non-streaming mode: return the run immediately, execute async
552
+ const updatedRun = state.updateRun(thread_id, run_id, {
553
+ status: 'in_progress',
554
+ required_action: null
555
+ });
556
+ res.json(updatedRun);
557
+ // Continue execution in background
558
+ continueRunWithToolOutputsNonStreaming(thread_id, run_id, body.tool_outputs).catch(err => {
559
+ console.error('Continue run failed:', err);
560
+ });
561
+ }
562
+ });
563
+ // ==================== Run Steps ====================
564
+ // List run steps
565
+ router.get('/v1/threads/:thread_id/runs/:run_id/steps', (req, res) => {
566
+ const { thread_id, run_id } = req.params;
567
+ const thread = state.getThread(thread_id);
568
+ if (!thread) {
569
+ return res.status(404).json(notFoundError('thread'));
570
+ }
571
+ const run = state.getRun(thread_id, run_id);
572
+ if (!run) {
573
+ return res.status(404).json(notFoundError('run'));
574
+ }
575
+ const params = parsePaginationParams(req.query);
576
+ const result = state.getRunSteps(run_id, params);
577
+ res.json(result);
578
+ });
579
+ // Get run step
580
+ router.get('/v1/threads/:thread_id/runs/:run_id/steps/:step_id', (req, res) => {
581
+ const { thread_id, run_id, step_id } = req.params;
582
+ const thread = state.getThread(thread_id);
583
+ if (!thread) {
584
+ return res.status(404).json(notFoundError('thread'));
585
+ }
586
+ const run = state.getRun(thread_id, run_id);
587
+ if (!run) {
588
+ return res.status(404).json(notFoundError('run'));
589
+ }
590
+ const step = state.getRunStep(run_id, step_id);
591
+ if (!step) {
592
+ return res.status(404).json(notFoundError('run step'));
593
+ }
594
+ res.json(step);
595
+ });
596
+ export default router;
597
+ //# sourceMappingURL=routes.js.map