@galaxy-yearn/codex-deepseek-gateway 0.1.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/README.md +207 -0
- package/bin/codex-deepseek-gateway.js +375 -0
- package/config/gateway.example.json +12 -0
- package/config/model-aliases.example.json +10 -0
- package/package.json +34 -0
- package/src/codex-config.js +83 -0
- package/src/common.js +141 -0
- package/src/config.js +31 -0
- package/src/local-config.js +51 -0
- package/src/model-map.js +152 -0
- package/src/protocol.js +1740 -0
- package/src/server.js +350 -0
- package/src/session-store.js +67 -0
- package/src/upstream.js +155 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { generateId, safeJsonParse } from './common.js';
|
|
5
|
+
import { listModels, mergeModelLists, normalizeModelList } from './model-map.js';
|
|
6
|
+
import {
|
|
7
|
+
assistantMessageFromResponseOutput,
|
|
8
|
+
convertChatCompletionToResponses,
|
|
9
|
+
extractToolCallIdsFromMessages,
|
|
10
|
+
normalizeResponsesRequest,
|
|
11
|
+
ResponsesStreamMapper,
|
|
12
|
+
serializeResponsesSseEvent,
|
|
13
|
+
toChatCompletionsRequest,
|
|
14
|
+
toProviderChatCompletionsRequest,
|
|
15
|
+
} from './protocol.js';
|
|
16
|
+
import { SessionStore } from './session-store.js';
|
|
17
|
+
import { callChatCompletions, callModels, readJsonResponse, relayChatCompletionsResponse } from './upstream.js';
|
|
18
|
+
|
|
19
|
+
function sendJson(res, statusCode, payload, headers = {}) {
|
|
20
|
+
res.writeHead(statusCode, {
|
|
21
|
+
'content-type': 'application/json; charset=utf-8',
|
|
22
|
+
...headers,
|
|
23
|
+
});
|
|
24
|
+
res.end(JSON.stringify(payload));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readRequestBody(req) {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
30
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getRequestPath(req) {
|
|
34
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
35
|
+
return url.pathname;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isAuthorized(req, config) {
|
|
39
|
+
if (!config.proxyApiKey) return true;
|
|
40
|
+
const authorization = req.headers.authorization || '';
|
|
41
|
+
const bearerToken = authorization.startsWith('Bearer ') ? authorization.slice(7) : '';
|
|
42
|
+
return bearerToken === config.proxyApiKey || req.headers['x-api-key'] === config.proxyApiKey;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function conversationIdFromRequest(request, normalized) {
|
|
46
|
+
const conversation = normalized.conversation ?? request.conversation;
|
|
47
|
+
if (typeof conversation === 'string') return conversation;
|
|
48
|
+
if (conversation && typeof conversation === 'object') return conversation.id || conversation.conversation_id || null;
|
|
49
|
+
return request.conversation_id || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function historyMessagesFromSession(session) {
|
|
53
|
+
if (!session?.history?.length) return [];
|
|
54
|
+
const messages = [];
|
|
55
|
+
for (const turn of session.history) {
|
|
56
|
+
if (Array.isArray(turn.inputMessages)) {
|
|
57
|
+
messages.push(...turn.inputMessages);
|
|
58
|
+
} else if (Array.isArray(turn.chatRequest?.messages)) {
|
|
59
|
+
messages.push(...turn.chatRequest.messages);
|
|
60
|
+
}
|
|
61
|
+
if (turn.assistantMessage) {
|
|
62
|
+
messages.push(turn.assistantMessage);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return messages;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function prependMissingAssistantToolMessages(messages, sessions) {
|
|
69
|
+
const existingToolCallIds = new Set();
|
|
70
|
+
const missingToolOutputIds = [];
|
|
71
|
+
for (const message of messages) {
|
|
72
|
+
if (message?.role === 'assistant' && Array.isArray(message.tool_calls)) {
|
|
73
|
+
for (const toolCall of message.tool_calls) {
|
|
74
|
+
if (toolCall?.id) existingToolCallIds.add(toolCall.id);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (message?.role === 'tool' && message.tool_call_id && !existingToolCallIds.has(message.tool_call_id)) {
|
|
79
|
+
missingToolOutputIds.push(message.tool_call_id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!missingToolOutputIds.length) return messages;
|
|
84
|
+
const prefix = [];
|
|
85
|
+
const inserted = new Set(existingToolCallIds);
|
|
86
|
+
for (const callId of missingToolOutputIds) {
|
|
87
|
+
if (inserted.has(callId)) continue;
|
|
88
|
+
const assistantMessage = sessions.getAssistantMessageForToolCall(callId);
|
|
89
|
+
if (!assistantMessage) continue;
|
|
90
|
+
prefix.push(assistantMessage);
|
|
91
|
+
for (const id of extractToolCallIdsFromMessages([assistantMessage])) inserted.add(id);
|
|
92
|
+
}
|
|
93
|
+
return prefix.length ? prefix.concat(messages) : messages;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function summarizeMessage(message, index) {
|
|
97
|
+
return {
|
|
98
|
+
index,
|
|
99
|
+
role: message?.role,
|
|
100
|
+
content_type: Array.isArray(message?.content) ? 'array' : typeof message?.content,
|
|
101
|
+
has_reasoning_content: typeof message?.reasoning_content === 'string',
|
|
102
|
+
reasoning_content_length: typeof message?.reasoning_content === 'string' ? message.reasoning_content.length : undefined,
|
|
103
|
+
tool_call_ids: Array.isArray(message?.tool_calls) ? message.tool_calls.map((toolCall) => toolCall?.id).filter(Boolean) : undefined,
|
|
104
|
+
tool_call_id: message?.tool_call_id,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function logDebugPayload(config, request) {
|
|
109
|
+
if (!config.debugPayload) return;
|
|
110
|
+
const summary = {
|
|
111
|
+
model: request.model,
|
|
112
|
+
stream: request.stream,
|
|
113
|
+
thinking: request.thinking,
|
|
114
|
+
reasoning_effort: request.reasoning_effort,
|
|
115
|
+
messages: Array.isArray(request.messages) ? request.messages.map(summarizeMessage) : [],
|
|
116
|
+
};
|
|
117
|
+
process.stderr.write(`[codex-deepseek-gateway] upstream request ${JSON.stringify(summary)}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveReasoningStreamMode(config, upstreamRequest) {
|
|
121
|
+
if (String(config.codexHideAgentReasoning).toLowerCase() === 'true') {
|
|
122
|
+
return { emitReasoningSummary: false, emitReasoningText: false };
|
|
123
|
+
}
|
|
124
|
+
const thinkingEnabled = upstreamRequest?.thinking?.type === 'enabled';
|
|
125
|
+
if (thinkingEnabled) {
|
|
126
|
+
return { emitReasoningSummary: true, emitReasoningText: false };
|
|
127
|
+
}
|
|
128
|
+
return { emitReasoningSummary: false, emitReasoningText: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createProxyServer({ config = loadConfig(), sessions = new SessionStore() } = {}) {
|
|
132
|
+
let modelCache = null;
|
|
133
|
+
|
|
134
|
+
async function getModelList() {
|
|
135
|
+
const localModels = listModels(config);
|
|
136
|
+
if (!config.fetchUpstreamModels || !config.upstreamApiKey) return localModels;
|
|
137
|
+
if (modelCache && modelCache.expiresAt > Date.now()) return modelCache.data;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const response = await callModels({
|
|
141
|
+
baseUrl: config.upstreamBaseUrl,
|
|
142
|
+
apiKey: config.upstreamApiKey,
|
|
143
|
+
timeoutMs: config.modelsTimeoutMs || 5000,
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) return localModels;
|
|
146
|
+
const data = await readJsonResponse(response);
|
|
147
|
+
const upstreamModels = normalizeModelList(data, config);
|
|
148
|
+
const models = mergeModelLists(upstreamModels, localModels);
|
|
149
|
+
modelCache = {
|
|
150
|
+
data: models,
|
|
151
|
+
expiresAt: Date.now() + (config.modelsCacheMs || 60000),
|
|
152
|
+
};
|
|
153
|
+
return models;
|
|
154
|
+
} catch {
|
|
155
|
+
return localModels;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleResponsesWithState(req, res) {
|
|
160
|
+
const raw = await readRequestBody(req);
|
|
161
|
+
const parsed = safeJsonParse(raw);
|
|
162
|
+
if (!parsed.ok || !parsed.value || typeof parsed.value !== 'object') {
|
|
163
|
+
sendJson(res, 400, { error: { message: 'Invalid JSON body' } });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const request = parsed.value;
|
|
168
|
+
const normalized = normalizeResponsesRequest(request);
|
|
169
|
+
const currentInputMessages = normalized.messages.slice();
|
|
170
|
+
const previousResponseId = normalized.previous_response_id || request.previous_response_id || null;
|
|
171
|
+
const conversationId = conversationIdFromRequest(request, normalized);
|
|
172
|
+
const priorSession = previousResponseId ? sessions.get(previousResponseId) : conversationId ? sessions.getConversation(conversationId) : null;
|
|
173
|
+
if (priorSession?.history?.length) {
|
|
174
|
+
const priorMessages = historyMessagesFromSession(priorSession);
|
|
175
|
+
normalized.messages = priorMessages.concat(normalized.messages);
|
|
176
|
+
}
|
|
177
|
+
normalized.messages = prependMissingAssistantToolMessages(normalized.messages, sessions);
|
|
178
|
+
|
|
179
|
+
const chatRequest = toChatCompletionsRequest(normalized);
|
|
180
|
+
const upstreamRequest = toProviderChatCompletionsRequest(chatRequest, config);
|
|
181
|
+
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || normalized.model;
|
|
182
|
+
logDebugPayload(config, upstreamRequest);
|
|
183
|
+
|
|
184
|
+
if (!upstreamRequest.model) {
|
|
185
|
+
sendJson(res, 400, { error: { message: 'Missing model' } });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!config.upstreamApiKey) {
|
|
189
|
+
sendJson(res, 500, { error: { message: 'Missing UPSTREAM_API_KEY' } });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const responseId = generateId('resp');
|
|
194
|
+
const nextSession = { id: responseId, history: [] };
|
|
195
|
+
|
|
196
|
+
const upstreamResponse = await callChatCompletions({
|
|
197
|
+
baseUrl: config.upstreamBaseUrl,
|
|
198
|
+
apiKey: config.upstreamApiKey,
|
|
199
|
+
request: upstreamRequest,
|
|
200
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const model = upstreamRequest.model;
|
|
204
|
+
|
|
205
|
+
if (!upstreamResponse.ok) {
|
|
206
|
+
const data = await readJsonResponse(upstreamResponse);
|
|
207
|
+
sendJson(res, upstreamResponse.status, data);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!upstreamRequest.stream) {
|
|
212
|
+
const data = await readJsonResponse(upstreamResponse);
|
|
213
|
+
const payload = convertChatCompletionToResponses({
|
|
214
|
+
completion: data,
|
|
215
|
+
model,
|
|
216
|
+
previousResponseId,
|
|
217
|
+
normalized,
|
|
218
|
+
responseId,
|
|
219
|
+
});
|
|
220
|
+
nextSession.history.push({
|
|
221
|
+
request: normalized,
|
|
222
|
+
chatRequest,
|
|
223
|
+
upstreamRequest,
|
|
224
|
+
inputMessages: currentInputMessages,
|
|
225
|
+
assistantMessage: assistantMessageFromResponseOutput(payload.output),
|
|
226
|
+
createdAt: Date.now(),
|
|
227
|
+
});
|
|
228
|
+
sessions.set(responseId, nextSession);
|
|
229
|
+
sessions.setConversation(conversationId, responseId);
|
|
230
|
+
sendJson(res, 200, payload);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
res.writeHead(200, {
|
|
235
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
236
|
+
'cache-control': 'no-cache, no-transform',
|
|
237
|
+
connection: 'keep-alive',
|
|
238
|
+
'x-accel-buffering': 'no',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const mapper = new ResponsesStreamMapper({
|
|
242
|
+
responseId,
|
|
243
|
+
model,
|
|
244
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
245
|
+
previousResponseId,
|
|
246
|
+
normalized,
|
|
247
|
+
...(() => {
|
|
248
|
+
const reasoningMode = resolveReasoningStreamMode(config, upstreamRequest);
|
|
249
|
+
return {
|
|
250
|
+
bufferOutputUntilDone: upstreamRequest?.thinking?.type === 'enabled',
|
|
251
|
+
...reasoningMode,
|
|
252
|
+
};
|
|
253
|
+
})(),
|
|
254
|
+
});
|
|
255
|
+
let doneSent = false;
|
|
256
|
+
const writeResponsesDone = () => {
|
|
257
|
+
if (doneSent) return;
|
|
258
|
+
doneSent = true;
|
|
259
|
+
res.write(serializeResponsesSseEvent({ done: true }));
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
res.write(serializeResponsesSseEvent(mapper.createdEvent()));
|
|
263
|
+
res.write(serializeResponsesSseEvent(mapper.inProgressEvent()));
|
|
264
|
+
|
|
265
|
+
await relayChatCompletionsResponse({
|
|
266
|
+
upstreamResponse,
|
|
267
|
+
res,
|
|
268
|
+
passThrough: false,
|
|
269
|
+
writeHeaders: false,
|
|
270
|
+
onStreamChunk(event) {
|
|
271
|
+
const events = mapper.mapChatEvent(event);
|
|
272
|
+
for (const responseEvent of events) {
|
|
273
|
+
res.write(serializeResponsesSseEvent(responseEvent));
|
|
274
|
+
}
|
|
275
|
+
if (event?.done) writeResponsesDone();
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
nextSession.history.push({
|
|
280
|
+
request: normalized,
|
|
281
|
+
chatRequest,
|
|
282
|
+
upstreamRequest,
|
|
283
|
+
inputMessages: currentInputMessages,
|
|
284
|
+
assistantMessage: mapper.assistantMessage(),
|
|
285
|
+
createdAt: Date.now(),
|
|
286
|
+
});
|
|
287
|
+
sessions.set(responseId, nextSession);
|
|
288
|
+
sessions.setConversation(conversationId, responseId);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return http.createServer(async (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const path = getRequestPath(req);
|
|
294
|
+
|
|
295
|
+
if (req.method === 'GET' && path === '/health') {
|
|
296
|
+
sendJson(res, 200, { ok: true });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (path.startsWith('/v1/') && !isAuthorized(req, config)) {
|
|
301
|
+
sendJson(res, 401, { error: { message: 'Unauthorized' } });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (req.method === 'GET' && path === '/v1/models') {
|
|
306
|
+
sendJson(res, 200, { object: 'list', data: await getModelList() });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (req.method === 'POST' && path === '/v1/responses') {
|
|
311
|
+
await handleResponsesWithState(req, res);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (req.method === 'POST' && path === '/v1/chat/completions') {
|
|
316
|
+
const body = safeJsonParse(await readRequestBody(req));
|
|
317
|
+
if (!body.ok) {
|
|
318
|
+
sendJson(res, 400, { error: { message: 'Invalid JSON body' } });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const upstreamRequest = body.value;
|
|
322
|
+
if (!config.upstreamApiKey) {
|
|
323
|
+
sendJson(res, 500, { error: { message: 'Missing UPSTREAM_API_KEY' } });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const upstreamResponse = await callChatCompletions({
|
|
327
|
+
baseUrl: config.upstreamBaseUrl,
|
|
328
|
+
apiKey: config.upstreamApiKey,
|
|
329
|
+
request: upstreamRequest,
|
|
330
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
331
|
+
});
|
|
332
|
+
await relayChatCompletionsResponse({ upstreamResponse, res, passThrough: true });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
sendJson(res, 404, { error: { message: 'Not found' } });
|
|
337
|
+
} catch (error) {
|
|
338
|
+
sendJson(res, 500, { error: { message: error.message || 'Internal server error' } });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
344
|
+
if (isMain) {
|
|
345
|
+
const config = loadConfig();
|
|
346
|
+
const server = createProxyServer({ config });
|
|
347
|
+
server.listen(config.port, config.host, () => {
|
|
348
|
+
process.stdout.write(`listening on http://${config.host}:${config.port}\n`);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class SessionStore {
|
|
2
|
+
constructor({ maxToolCallMessages = 1000 } = {}) {
|
|
3
|
+
this.map = new Map();
|
|
4
|
+
this.toolCallMessages = new Map();
|
|
5
|
+
this.conversations = new Map();
|
|
6
|
+
this.maxToolCallMessages = maxToolCallMessages;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get(id) {
|
|
10
|
+
return this.map.get(id) || null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
set(id, value) {
|
|
14
|
+
this.map.set(id, value);
|
|
15
|
+
for (const turn of Array.isArray(value?.history) ? value.history : []) {
|
|
16
|
+
this.indexAssistantMessage(turn.assistantMessage);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getConversation(id) {
|
|
22
|
+
const responseId = this.conversations.get(id);
|
|
23
|
+
return responseId ? this.get(responseId) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setConversation(id, responseId) {
|
|
27
|
+
if (id && responseId) this.conversations.set(String(id), responseId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
indexAssistantMessage(message) {
|
|
31
|
+
if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls)) return;
|
|
32
|
+
for (const toolCall of message.tool_calls) {
|
|
33
|
+
if (!toolCall?.id) continue;
|
|
34
|
+
this.toolCallMessages.set(toolCall.id, cloneAssistantMessage(message));
|
|
35
|
+
while (this.toolCallMessages.size > this.maxToolCallMessages) {
|
|
36
|
+
const oldestKey = this.toolCallMessages.keys().next().value;
|
|
37
|
+
this.toolCallMessages.delete(oldestKey);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getAssistantMessageForToolCall(callId) {
|
|
43
|
+
return this.toolCallMessages.get(callId) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
delete(id) {
|
|
47
|
+
return this.map.delete(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
clear() {
|
|
51
|
+
this.map.clear();
|
|
52
|
+
this.toolCallMessages.clear();
|
|
53
|
+
this.conversations.clear();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cloneAssistantMessage(message) {
|
|
58
|
+
return {
|
|
59
|
+
...message,
|
|
60
|
+
tool_calls: Array.isArray(message.tool_calls)
|
|
61
|
+
? message.tool_calls.map((toolCall) => ({
|
|
62
|
+
...toolCall,
|
|
63
|
+
function: toolCall.function ? { ...toolCall.function } : toolCall.function,
|
|
64
|
+
}))
|
|
65
|
+
: undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
package/src/upstream.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { joinUrl, SseParser, safeJsonParse } from './common.js';
|
|
2
|
+
|
|
3
|
+
function buildHeaders(apiKey, extra = {}) {
|
|
4
|
+
return {
|
|
5
|
+
Authorization: `Bearer ${apiKey}`,
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
...extra,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function readJsonResponse(response) {
|
|
12
|
+
const text = await response.text();
|
|
13
|
+
const parsed = safeJsonParse(text);
|
|
14
|
+
return parsed.ok ? parsed.value : { raw: text };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function mergeAbortSignals(...signals) {
|
|
18
|
+
const activeSignals = signals.filter(Boolean);
|
|
19
|
+
if (!activeSignals.length) return undefined;
|
|
20
|
+
if (activeSignals.length === 1) return activeSignals[0];
|
|
21
|
+
if (typeof AbortSignal?.any === 'function') {
|
|
22
|
+
return AbortSignal.any(activeSignals);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
for (const signal of activeSignals) {
|
|
27
|
+
if (signal.aborted) {
|
|
28
|
+
controller.abort(signal.reason);
|
|
29
|
+
return controller.signal;
|
|
30
|
+
}
|
|
31
|
+
signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
|
|
32
|
+
}
|
|
33
|
+
return controller.signal;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function callChatCompletions({
|
|
37
|
+
baseUrl,
|
|
38
|
+
apiKey,
|
|
39
|
+
request,
|
|
40
|
+
timeoutMs,
|
|
41
|
+
signal,
|
|
42
|
+
}) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
const finalSignal = mergeAbortSignals(signal, controller.signal);
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(joinUrl(baseUrl, '/chat/completions'), {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: buildHeaders(apiKey),
|
|
50
|
+
body: JSON.stringify(request),
|
|
51
|
+
signal: finalSignal,
|
|
52
|
+
});
|
|
53
|
+
return response;
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function callModels({
|
|
60
|
+
baseUrl,
|
|
61
|
+
apiKey,
|
|
62
|
+
timeoutMs,
|
|
63
|
+
signal,
|
|
64
|
+
}) {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
67
|
+
const finalSignal = mergeAbortSignals(signal, controller.signal);
|
|
68
|
+
try {
|
|
69
|
+
return await fetch(joinUrl(baseUrl, '/models'), {
|
|
70
|
+
method: 'GET',
|
|
71
|
+
headers: buildHeaders(apiKey),
|
|
72
|
+
signal: finalSignal,
|
|
73
|
+
});
|
|
74
|
+
} finally {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function relayChatCompletionsResponse({
|
|
80
|
+
upstreamResponse,
|
|
81
|
+
res,
|
|
82
|
+
onStreamChunk,
|
|
83
|
+
passThrough = true,
|
|
84
|
+
writeHeaders = true,
|
|
85
|
+
}) {
|
|
86
|
+
const contentType = upstreamResponse.headers.get('content-type') || '';
|
|
87
|
+
const isStream = contentType.includes('text/event-stream');
|
|
88
|
+
|
|
89
|
+
if (!isStream) {
|
|
90
|
+
const data = await readJsonResponse(upstreamResponse);
|
|
91
|
+
if (typeof onStreamChunk === 'function') {
|
|
92
|
+
onStreamChunk({ data });
|
|
93
|
+
onStreamChunk({ done: true });
|
|
94
|
+
}
|
|
95
|
+
if (passThrough) {
|
|
96
|
+
res.writeHead(upstreamResponse.status, {
|
|
97
|
+
'content-type': 'application/json; charset=utf-8',
|
|
98
|
+
});
|
|
99
|
+
res.end(JSON.stringify(data));
|
|
100
|
+
} else {
|
|
101
|
+
res.end();
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (writeHeaders) {
|
|
107
|
+
res.writeHead(upstreamResponse.status, {
|
|
108
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
109
|
+
'cache-control': 'no-cache, no-transform',
|
|
110
|
+
connection: 'keep-alive',
|
|
111
|
+
'x-accel-buffering': 'no',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const reader = upstreamResponse.body?.getReader?.();
|
|
116
|
+
if (!reader) {
|
|
117
|
+
res.end();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parser = new SseParser();
|
|
122
|
+
let sawDone = false;
|
|
123
|
+
const emitDone = () => {
|
|
124
|
+
if (sawDone) return;
|
|
125
|
+
sawDone = true;
|
|
126
|
+
if (typeof onStreamChunk === 'function') onStreamChunk({ done: true });
|
|
127
|
+
if (passThrough) res.write(`data: [DONE]\n\n`);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
while (true) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done) break;
|
|
133
|
+
const events = parser.push(value);
|
|
134
|
+
for (const event of events) {
|
|
135
|
+
if (event.done) {
|
|
136
|
+
emitDone();
|
|
137
|
+
res.end();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (typeof onStreamChunk === 'function') onStreamChunk(event);
|
|
141
|
+
if (passThrough) res.write(`data: ${event.data}\n\n`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const event of parser.end()) {
|
|
146
|
+
if (event.done) {
|
|
147
|
+
emitDone();
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
if (typeof onStreamChunk === 'function') onStreamChunk(event);
|
|
151
|
+
if (passThrough) res.write(`data: ${event.data}\n\n`);
|
|
152
|
+
}
|
|
153
|
+
emitDone();
|
|
154
|
+
res.end();
|
|
155
|
+
}
|