@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.
- package/out/assistants/index.d.ts +15 -0
- package/out/assistants/index.js +16 -0
- package/out/assistants/index.js.map +1 -0
- package/out/assistants/routes.d.ts +16 -0
- package/out/assistants/routes.js +597 -0
- package/out/assistants/routes.js.map +1 -0
- package/out/assistants/runner.d.ts +48 -0
- package/out/assistants/runner.js +851 -0
- package/out/assistants/runner.js.map +1 -0
- package/out/assistants/state.d.ts +81 -0
- package/out/assistants/state.js +351 -0
- package/out/assistants/state.js.map +1 -0
- package/out/assistants/tools.d.ts +4 -0
- package/out/assistants/tools.js +8 -0
- package/out/assistants/tools.js.map +1 -0
- package/out/assistants/types.d.ts +254 -0
- package/out/assistants/types.js +5 -0
- package/out/assistants/types.js.map +1 -0
- package/out/backend.d.ts +24 -0
- package/out/backend.js +12 -0
- package/out/backend.js.map +1 -0
- package/out/index.d.ts +13 -0
- package/out/index.js +21 -0
- package/out/index.js.map +1 -0
- package/out/server.d.ts +12 -0
- package/out/server.js +504 -0
- package/out/server.js.map +1 -0
- package/out/skills/index.d.ts +3 -0
- package/out/skills/index.js +4 -0
- package/out/skills/index.js.map +1 -0
- package/out/skills/manifest.d.ts +25 -0
- package/out/skills/manifest.js +96 -0
- package/out/skills/manifest.js.map +1 -0
- package/out/skills/resolver.d.ts +22 -0
- package/out/skills/resolver.js +66 -0
- package/out/skills/resolver.js.map +1 -0
- package/out/skills/routes.d.ts +3 -0
- package/out/skills/routes.js +191 -0
- package/out/skills/routes.js.map +1 -0
- package/out/skills/state.d.ts +35 -0
- package/out/skills/state.js +155 -0
- package/out/skills/state.js.map +1 -0
- package/out/skills/storage.d.ts +30 -0
- package/out/skills/storage.js +171 -0
- package/out/skills/storage.js.map +1 -0
- package/out/skills/types.d.ts +141 -0
- package/out/skills/types.js +8 -0
- package/out/skills/types.js.map +1 -0
- package/out/toolConvert.d.ts +24 -0
- package/out/toolConvert.js +56 -0
- package/out/toolConvert.js.map +1 -0
- package/out/types.d.ts +291 -0
- package/out/types.js +2 -0
- package/out/types.js.map +1 -0
- package/out/utils.d.ts +28 -0
- package/out/utils.js +81 -0
- package/out/utils.js.map +1 -0
- 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
|