@galaxy-yearn/codex-deepseek-gateway 0.1.7 → 0.2.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 +54 -36
- package/config/codex-model-catalog.json +20 -24
- package/config/codex-model-catalog.zh.json +20 -24
- package/package.json +2 -1
- package/src/codex-launch.js +0 -1
- package/src/common.js +18 -3
- package/src/config.js +12 -2
- package/src/firecrawl.js +7 -16
- package/src/model-map.js +6 -20
- package/src/protocol.js +1319 -450
- package/src/server.js +510 -350
- package/src/session-store.js +228 -13
- package/src/tavily.js +2 -11
- package/src/upstream.js +14 -12
- package/src/web-search-emulator.js +12 -11
- package/state/sessions.example.json +72 -0
package/src/server.js
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
+
import { appendFileSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
2
4
|
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { loadConfig } from './config.js';
|
|
4
6
|
import { generateId, safeJsonParse, toText } from './common.js';
|
|
5
7
|
import { listModels, mergeModelLists, normalizeModelList } from './model-map.js';
|
|
6
8
|
import {
|
|
7
9
|
assistantMessageFromResponseOutput,
|
|
10
|
+
bridgedCommentaryToolCallsFromMessage,
|
|
11
|
+
chatToolNamesFromTools,
|
|
8
12
|
convertChatCompletionToResponses,
|
|
13
|
+
expandParallelToolCallsInCompletion,
|
|
9
14
|
extractToolCallIdsFromMessages,
|
|
10
15
|
normalizeResponsesRequest,
|
|
16
|
+
resolveEmittedToolCallNamesInCompletion,
|
|
11
17
|
ResponsesStreamMapper,
|
|
12
18
|
serializeResponsesSseEvent,
|
|
19
|
+
stripBridgedCommentaryFromCompletion,
|
|
13
20
|
toChatCompletionsRequest,
|
|
14
21
|
toProviderChatCompletionsRequest,
|
|
22
|
+
unavailableWebSearchToolShims,
|
|
15
23
|
} from './protocol.js';
|
|
16
24
|
import { SessionStore } from './session-store.js';
|
|
17
25
|
import { callChatCompletions, callModels, readJsonResponse, relayChatCompletionsResponse } from './upstream.js';
|
|
18
26
|
import {
|
|
27
|
+
annotateMessagePartWithWebCitations,
|
|
19
28
|
applyWebSearchOutputCompatibility,
|
|
20
29
|
buildWebSearchCallItem,
|
|
21
30
|
containsWebSearchTool,
|
|
@@ -34,6 +43,7 @@ import {
|
|
|
34
43
|
} from './web-search-emulator.js';
|
|
35
44
|
|
|
36
45
|
function sendJson(res, statusCode, payload, headers = {}) {
|
|
46
|
+
if (res.destroyed || res.writableEnded) return;
|
|
37
47
|
res.writeHead(statusCode, {
|
|
38
48
|
'content-type': 'application/json; charset=utf-8',
|
|
39
49
|
...headers,
|
|
@@ -67,25 +77,20 @@ function conversationIdFromRequest(request, normalized) {
|
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
function historyMessagesFromSession(session) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (Array.isArray(turn.inputMessages)) {
|
|
78
|
-
messages.push(...turn.inputMessages);
|
|
79
|
-
} else if (Array.isArray(turn.chatRequest?.messages)) {
|
|
80
|
-
messages.push(...turn.chatRequest.messages);
|
|
81
|
-
}
|
|
82
|
-
if (turn.assistantMessage) {
|
|
83
|
-
messages.push(turn.assistantMessage);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
80
|
+
return Array.isArray(session?.messages) ? session.messages : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sessionMessages(chatMessages, assistantMessage) {
|
|
84
|
+
const messages = Array.isArray(chatMessages) ? chatMessages.slice() : [];
|
|
85
|
+
if (assistantMessage) messages.push(assistantMessage);
|
|
86
86
|
return messages;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function shouldPersistSession(normalized, request, conversationId) {
|
|
90
|
+
if (conversationId) return true;
|
|
91
|
+
return (normalized.store ?? request.store) !== false;
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
function prependMissingAssistantToolMessages(messages, sessions) {
|
|
90
95
|
const existingToolCallIds = new Set();
|
|
91
96
|
const missingToolOutputIds = [];
|
|
@@ -114,6 +119,22 @@ function prependMissingAssistantToolMessages(messages, sessions) {
|
|
|
114
119
|
return prefix.length ? prefix.concat(messages) : messages;
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
function restoreAssistantReasoningContent(messages, sessions) {
|
|
123
|
+
return messages.map((message) => {
|
|
124
|
+
if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls) || !message.tool_calls.length) {
|
|
125
|
+
return message;
|
|
126
|
+
}
|
|
127
|
+
if (typeof message.reasoning_content === 'string' && message.reasoning_content) return message;
|
|
128
|
+
for (const toolCall of message.tool_calls) {
|
|
129
|
+
const stored = sessions.getAssistantMessageForToolCall(toolCall?.id);
|
|
130
|
+
if (typeof stored?.reasoning_content === 'string' && stored.reasoning_content) {
|
|
131
|
+
return { ...message, reasoning_content: stored.reasoning_content };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return message;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
117
138
|
function summarizeMessage(message, index) {
|
|
118
139
|
return {
|
|
119
140
|
index,
|
|
@@ -126,16 +147,66 @@ function summarizeMessage(message, index) {
|
|
|
126
147
|
};
|
|
127
148
|
}
|
|
128
149
|
|
|
129
|
-
function
|
|
150
|
+
function summarizeTool(tool, index) {
|
|
151
|
+
if (!tool || typeof tool !== 'object') {
|
|
152
|
+
return { index, type: typeof tool };
|
|
153
|
+
}
|
|
154
|
+
const fn = tool.type === 'function' && tool.function && typeof tool.function === 'object'
|
|
155
|
+
? tool.function
|
|
156
|
+
: null;
|
|
157
|
+
return {
|
|
158
|
+
index,
|
|
159
|
+
type: tool.type,
|
|
160
|
+
name: fn?.name || tool.name || tool.tool_name || tool.server_label,
|
|
161
|
+
namespace: fn?.namespace || tool.namespace,
|
|
162
|
+
child_tools: Array.isArray(tool.tools) ? tool.tools.length : undefined,
|
|
163
|
+
has_parameters: Boolean(fn?.parameters || tool.parameters || tool.input_schema),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function summarizeTools(tools) {
|
|
168
|
+
return Array.isArray(tools) ? tools.map(summarizeTool) : [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const DEBUG_PAYLOAD_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
172
|
+
|
|
173
|
+
function rotateDebugPayloadLog(logPath, maxBytes) {
|
|
174
|
+
try {
|
|
175
|
+
if (statSync(logPath).size < maxBytes) return;
|
|
176
|
+
rmSync(`${logPath}.1`, { force: true });
|
|
177
|
+
renameSync(logPath, `${logPath}.1`);
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function writeDebugPayloadLine(config, line) {
|
|
183
|
+
process.stderr.write(line);
|
|
184
|
+
if (!config.debugPayloadLogPath) return;
|
|
185
|
+
try {
|
|
186
|
+
const logPath = resolve(config.debugPayloadLogPath);
|
|
187
|
+
rotateDebugPayloadLog(logPath, config.debugPayloadLogMaxBytes || DEBUG_PAYLOAD_LOG_MAX_BYTES);
|
|
188
|
+
appendFileSync(logPath, line, 'utf8');
|
|
189
|
+
} catch (error) {
|
|
190
|
+
process.stderr.write(`[codex-deepseek-gateway] failed to write debug payload log: ${error.message || error}\n`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function logDebugPayload(config, request, context = {}) {
|
|
130
195
|
if (!config.debugPayload) return;
|
|
131
196
|
const summary = {
|
|
197
|
+
stage: context.stage || 'upstream',
|
|
132
198
|
model: request.model,
|
|
133
199
|
stream: request.stream,
|
|
134
200
|
thinking: request.thinking,
|
|
135
201
|
reasoning_effort: request.reasoning_effort,
|
|
202
|
+
codex_tools: summarizeTools(context.rawRequest?.tools),
|
|
203
|
+
normalized_tools: summarizeTools(context.normalized?.tools),
|
|
204
|
+
chat_tools: summarizeTools(context.chatRequest?.tools),
|
|
205
|
+
upstream_tools: summarizeTools(request.tools),
|
|
206
|
+
tool_choice: request.tool_choice,
|
|
136
207
|
messages: Array.isArray(request.messages) ? request.messages.map(summarizeMessage) : [],
|
|
137
208
|
};
|
|
138
|
-
|
|
209
|
+
writeDebugPayloadLine(config, `[codex-deepseek-gateway] upstream request ${JSON.stringify(summary)}\n`);
|
|
139
210
|
}
|
|
140
211
|
|
|
141
212
|
function resolveReasoningStreamMode(config, upstreamRequest) {
|
|
@@ -168,29 +239,26 @@ function webToolTimeoutMs(config = {}) {
|
|
|
168
239
|
return Math.max(tavilyTimeout, firecrawlTimeout);
|
|
169
240
|
}
|
|
170
241
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
if (!
|
|
242
|
+
function combineUsage(completions) {
|
|
243
|
+
const usages = completions.map((completion) => completion?.usage).filter(Boolean);
|
|
244
|
+
if (!usages.length) return null;
|
|
245
|
+
const last = usages[usages.length - 1];
|
|
246
|
+
const sum = (pick) => usages.reduce((total, usage) => total + (Number(pick(usage)) || 0), 0);
|
|
247
|
+
const promptTokens = Number(last.prompt_tokens) || 0;
|
|
248
|
+
const completionTokens = sum((usage) => usage.completion_tokens);
|
|
174
249
|
return {
|
|
175
|
-
prompt_tokens:
|
|
176
|
-
completion_tokens:
|
|
177
|
-
total_tokens:
|
|
178
|
-
reasoning_tokens: (left.reasoning_tokens || 0) + (right.reasoning_tokens || 0),
|
|
250
|
+
prompt_tokens: promptTokens,
|
|
251
|
+
completion_tokens: completionTokens,
|
|
252
|
+
total_tokens: promptTokens + completionTokens,
|
|
179
253
|
prompt_tokens_details: {
|
|
180
|
-
cached_tokens:
|
|
254
|
+
cached_tokens: last.prompt_tokens_details?.cached_tokens ?? last.prompt_cache_hit_tokens ?? 0,
|
|
181
255
|
},
|
|
182
256
|
completion_tokens_details: {
|
|
183
|
-
reasoning_tokens:
|
|
184
|
-
(left.completion_tokens_details?.reasoning_tokens || 0) +
|
|
185
|
-
(right.completion_tokens_details?.reasoning_tokens || 0),
|
|
257
|
+
reasoning_tokens: sum((usage) => usage.completion_tokens_details?.reasoning_tokens ?? usage.reasoning_tokens),
|
|
186
258
|
},
|
|
187
259
|
};
|
|
188
260
|
}
|
|
189
261
|
|
|
190
|
-
function combineUsage(completions) {
|
|
191
|
-
return completions.reduce((usage, completion) => addUsage(usage, completion?.usage), null);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
262
|
function cloneCompletionWithUsage(completion, usage) {
|
|
195
263
|
if (!usage) return completion;
|
|
196
264
|
return {
|
|
@@ -202,7 +270,7 @@ function cloneCompletionWithUsage(completion, usage) {
|
|
|
202
270
|
function toolCallsToFinalAnswerRequest(request) {
|
|
203
271
|
return {
|
|
204
272
|
...request,
|
|
205
|
-
tool_choice:
|
|
273
|
+
tool_choice: undefined,
|
|
206
274
|
tools: undefined,
|
|
207
275
|
messages: removeWebSearchInstructions(request.messages).concat({
|
|
208
276
|
role: 'user',
|
|
@@ -245,6 +313,12 @@ function hasWebSearchContext(searches, openedPages) {
|
|
|
245
313
|
return searches.length > 0 || openedPages.length > 0;
|
|
246
314
|
}
|
|
247
315
|
|
|
316
|
+
function gatewayIncompleteMessageContent(reason) {
|
|
317
|
+
return reason === 'pseudo_tool_call_text_after_web_limit'
|
|
318
|
+
? 'Gateway incomplete: after web tools were disabled, the model wrote a tool call as text instead of producing a final answer.'
|
|
319
|
+
: 'Gateway incomplete: the model did not produce visible assistant content after web tool results and a final-answer request.';
|
|
320
|
+
}
|
|
321
|
+
|
|
248
322
|
function completionWithGatewayIncompleteMessage(completion, reason = 'no_visible_assistant_content') {
|
|
249
323
|
const next = completion == null ? {} : JSON.parse(JSON.stringify(completion));
|
|
250
324
|
if (!Array.isArray(next.choices) || !next.choices.length) {
|
|
@@ -252,9 +326,7 @@ function completionWithGatewayIncompleteMessage(completion, reason = 'no_visible
|
|
|
252
326
|
}
|
|
253
327
|
const choice = next.choices[0];
|
|
254
328
|
const previousMessage = choice.message || {};
|
|
255
|
-
const content = reason
|
|
256
|
-
? 'Gateway incomplete: after web tools were disabled, the model wrote a tool call as text instead of producing a final answer.'
|
|
257
|
-
: 'Gateway incomplete: the model did not produce visible assistant content after web tool results and a final-answer request.';
|
|
329
|
+
const content = gatewayIncompleteMessageContent(reason);
|
|
258
330
|
choice.message = {
|
|
259
331
|
role: 'assistant',
|
|
260
332
|
content,
|
|
@@ -276,24 +348,62 @@ function completionWithoutToolCalls(completion) {
|
|
|
276
348
|
return next;
|
|
277
349
|
}
|
|
278
350
|
|
|
279
|
-
function
|
|
280
|
-
payload.status = 'incomplete';
|
|
281
|
-
payload.incomplete_details = { reason };
|
|
282
|
-
return payload;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function callUpstreamJson({ upstreamRequest, config }) {
|
|
351
|
+
async function callUpstreamJson({ upstreamRequest, config, signal }) {
|
|
286
352
|
const response = await callChatCompletions({
|
|
287
353
|
baseUrl: config.upstreamBaseUrl,
|
|
288
354
|
apiKey: config.upstreamApiKey,
|
|
289
355
|
request: upstreamRequest,
|
|
290
356
|
timeoutMs: config.upstreamTimeoutMs,
|
|
357
|
+
signal,
|
|
291
358
|
});
|
|
292
359
|
const data = await readJsonResponse(response);
|
|
293
|
-
return {
|
|
360
|
+
return {
|
|
361
|
+
response,
|
|
362
|
+
data: resolveEmittedToolCallNamesInCompletion(
|
|
363
|
+
expandParallelToolCallsInCompletion(data),
|
|
364
|
+
chatToolNamesFromTools(upstreamRequest.tools),
|
|
365
|
+
),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function clientAbortControllerFor(res) {
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
res.on('close', () => {
|
|
372
|
+
if (!res.writableEnded) controller.abort();
|
|
373
|
+
});
|
|
374
|
+
return controller;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function mergeAbortSignals(signals) {
|
|
378
|
+
const active = signals.filter(Boolean);
|
|
379
|
+
if (!active.length) return undefined;
|
|
380
|
+
if (active.length === 1) return active[0];
|
|
381
|
+
return AbortSignal.any(active);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function commentaryToolMessages(commentaryCalls) {
|
|
385
|
+
return (Array.isArray(commentaryCalls) ? commentaryCalls : []).map((toolCall) => ({
|
|
386
|
+
role: 'tool',
|
|
387
|
+
tool_call_id: toolCall.id || generateId('call'),
|
|
388
|
+
content: 'Delivered to the user.',
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function restoreCommentaryToolCalls(completion, commentaryCalls) {
|
|
393
|
+
if (!Array.isArray(commentaryCalls) || !commentaryCalls.length) return completion;
|
|
394
|
+
const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
|
|
395
|
+
if (!choice?.message) return completion;
|
|
396
|
+
const existing = Array.isArray(choice.message.tool_calls) ? choice.message.tool_calls : [];
|
|
397
|
+
return {
|
|
398
|
+
...completion,
|
|
399
|
+
choices: [
|
|
400
|
+
{ ...choice, message: { ...choice.message, tool_calls: [...commentaryCalls, ...existing] } },
|
|
401
|
+
...completion.choices.slice(1),
|
|
402
|
+
],
|
|
403
|
+
};
|
|
294
404
|
}
|
|
295
405
|
|
|
296
|
-
async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchStart, onSearchDone }) {
|
|
406
|
+
async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, config, clientSignal, onSearchStart, onSearchDone }) {
|
|
297
407
|
const effectiveChatRequest = { ...chatRequest, normalized };
|
|
298
408
|
const state = prepareWebSearchRequest({ normalized, chatRequest: effectiveChatRequest, config });
|
|
299
409
|
const searchConfig = state.config || config;
|
|
@@ -310,9 +420,16 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
310
420
|
currentChatRequest = toolCallsToFinalAnswerRequest(baseRequest);
|
|
311
421
|
const upstreamRequest = toProviderChatCompletionsRequest(currentChatRequest, config);
|
|
312
422
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
423
|
+
logDebugPayload(config, upstreamRequest, {
|
|
424
|
+
stage: 'web_search_final_answer',
|
|
425
|
+
rawRequest,
|
|
426
|
+
normalized,
|
|
427
|
+
chatRequest: currentChatRequest,
|
|
428
|
+
});
|
|
313
429
|
const { response, data } = await callUpstreamJson({
|
|
314
430
|
upstreamRequest: disableStreaming(upstreamRequest),
|
|
315
431
|
config,
|
|
432
|
+
signal: clientSignal,
|
|
316
433
|
});
|
|
317
434
|
finalResponse = response;
|
|
318
435
|
if (!response.ok) return { ok: false, status: response.status, data };
|
|
@@ -329,17 +446,30 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
329
446
|
for (let round = 0; round <= maxRounds + 1; round += 1) {
|
|
330
447
|
const upstreamRequest = toProviderChatCompletionsRequest(currentChatRequest, config);
|
|
331
448
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
449
|
+
logDebugPayload(config, upstreamRequest, {
|
|
450
|
+
stage: `web_search_round_${round}`,
|
|
451
|
+
rawRequest,
|
|
452
|
+
normalized,
|
|
453
|
+
chatRequest: currentChatRequest,
|
|
454
|
+
});
|
|
332
455
|
const { response, data } = await callUpstreamJson({
|
|
333
456
|
upstreamRequest: disableStreaming(upstreamRequest),
|
|
334
457
|
config,
|
|
458
|
+
signal: clientSignal,
|
|
335
459
|
});
|
|
336
460
|
finalResponse = response;
|
|
337
461
|
if (!response.ok) return { ok: false, status: response.status, data };
|
|
338
462
|
completions.push(data);
|
|
339
463
|
finalCompletion = data;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
464
|
+
const roundChatMessage = Array.isArray(data?.choices) ? data.choices[0]?.message : null;
|
|
465
|
+
const commentaryCalls = bridgedCommentaryToolCallsFromMessage(roundChatMessage, currentChatRequest.tools);
|
|
466
|
+
const routingData = stripBridgedCommentaryFromCompletion(data, currentChatRequest.tools);
|
|
467
|
+
|
|
468
|
+
if (state.enabled && hasKnownExternalToolCalls(routingData, currentChatRequest.tools)) {
|
|
469
|
+
finalCompletion = restoreCommentaryToolCalls(
|
|
470
|
+
knownExternalToolCallsCompletion(routingData, currentChatRequest.tools),
|
|
471
|
+
commentaryCalls,
|
|
472
|
+
);
|
|
343
473
|
completions[completions.length - 1] = finalCompletion;
|
|
344
474
|
break;
|
|
345
475
|
}
|
|
@@ -348,12 +478,13 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
348
478
|
break;
|
|
349
479
|
}
|
|
350
480
|
|
|
351
|
-
const wantsInternalWeb = shouldContinueWebSearchLoop(
|
|
352
|
-
const
|
|
481
|
+
const wantsInternalWeb = shouldContinueWebSearchLoop(routingData);
|
|
482
|
+
const wantsInternalRound = wantsInternalWeb || commentaryCalls.length > 0;
|
|
483
|
+
const hasToolCalls = hasAnyToolCalls(routingData);
|
|
353
484
|
|
|
354
|
-
if (!
|
|
355
|
-
if (hasToolCalls && hasUnknownExternalToolCalls(
|
|
356
|
-
const unsupportedMessages = unhandledToolMessagesFromCompletion(
|
|
485
|
+
if (!wantsInternalRound || round >= maxRounds) {
|
|
486
|
+
if (hasToolCalls && hasUnknownExternalToolCalls(routingData, currentChatRequest.tools)) {
|
|
487
|
+
const unsupportedMessages = unhandledToolMessagesFromCompletion(routingData, undefined, {
|
|
357
488
|
onlyUnknownExternal: true,
|
|
358
489
|
tools: currentChatRequest.tools,
|
|
359
490
|
});
|
|
@@ -365,7 +496,7 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
365
496
|
break;
|
|
366
497
|
}
|
|
367
498
|
|
|
368
|
-
if (
|
|
499
|
+
if (wantsInternalRound || (!hasToolCalls && hasWebSearchContext(searches, openedPages) && !hasVisibleAssistantContent(routingData))) {
|
|
369
500
|
const finalAnswerError = await requestFinalAnswer(currentChatRequest);
|
|
370
501
|
if (finalAnswerError) return finalAnswerError;
|
|
371
502
|
break;
|
|
@@ -380,7 +511,7 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
380
511
|
toolResult = await executeWebSearchCalls({
|
|
381
512
|
completion: data,
|
|
382
513
|
config: searchConfig,
|
|
383
|
-
signal: controller.signal,
|
|
514
|
+
signal: mergeAbortSignals([controller.signal, clientSignal]),
|
|
384
515
|
onSearchStart,
|
|
385
516
|
onSearchDone,
|
|
386
517
|
});
|
|
@@ -391,7 +522,7 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
391
522
|
openedPages.push(...(toolResult.openedPages || []));
|
|
392
523
|
currentChatRequest = {
|
|
393
524
|
...currentChatRequest,
|
|
394
|
-
messages: currentChatRequest.messages.concat(toolResult.messages),
|
|
525
|
+
messages: currentChatRequest.messages.concat(toolResult.messages, commentaryToolMessages(commentaryCalls)),
|
|
395
526
|
tool_choice:
|
|
396
527
|
currentChatRequest.tool_choice?.function?.name === INTERNAL_WEB_SEARCH_TOOL
|
|
397
528
|
? 'auto'
|
|
@@ -410,170 +541,10 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
410
541
|
};
|
|
411
542
|
}
|
|
412
543
|
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
function splitReplayText(text, maxChars = REPLAY_REASONING_DELTA_CHARS) {
|
|
416
|
-
const value = String(text ?? '');
|
|
417
|
-
if (!value) return [];
|
|
418
|
-
const chunks = [];
|
|
419
|
-
for (let index = 0; index < value.length; index += maxChars) {
|
|
420
|
-
chunks.push(value.slice(index, index + maxChars));
|
|
421
|
-
}
|
|
422
|
-
return chunks;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function replayReasoningItemEvents({ item, outputIndex, mapper }) {
|
|
426
|
-
const events = [];
|
|
427
|
-
const summaries = Array.isArray(item.summary) ? item.summary : [];
|
|
428
|
-
const content = Array.isArray(item.content) ? item.content : [];
|
|
429
|
-
const addedItem = {
|
|
430
|
-
...item,
|
|
431
|
-
status: 'in_progress',
|
|
432
|
-
summary: [],
|
|
433
|
-
content: content.map((part) => (part?.type === 'reasoning_text' ? { ...part, text: '' } : part)),
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
events.push({
|
|
437
|
-
type: 'response.output_item.added',
|
|
438
|
-
sequence_number: mapper.nextSequence(),
|
|
439
|
-
output_index: outputIndex,
|
|
440
|
-
item: addedItem,
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
for (const [summaryIndex, part] of summaries.entries()) {
|
|
444
|
-
if (part?.type !== 'summary_text') continue;
|
|
445
|
-
const text = String(part.text ?? '');
|
|
446
|
-
events.push({
|
|
447
|
-
type: 'response.reasoning_summary_part.added',
|
|
448
|
-
sequence_number: mapper.nextSequence(),
|
|
449
|
-
output_index: outputIndex,
|
|
450
|
-
item_id: item.id,
|
|
451
|
-
summary_index: summaryIndex,
|
|
452
|
-
part: { ...part, text: '' },
|
|
453
|
-
});
|
|
454
|
-
for (const delta of splitReplayText(text)) {
|
|
455
|
-
events.push({
|
|
456
|
-
type: 'response.reasoning_summary_text.delta',
|
|
457
|
-
sequence_number: mapper.nextSequence(),
|
|
458
|
-
output_index: outputIndex,
|
|
459
|
-
item_id: item.id,
|
|
460
|
-
summary_index: summaryIndex,
|
|
461
|
-
delta,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
events.push({
|
|
465
|
-
type: 'response.reasoning_summary_text.done',
|
|
466
|
-
sequence_number: mapper.nextSequence(),
|
|
467
|
-
output_index: outputIndex,
|
|
468
|
-
item_id: item.id,
|
|
469
|
-
summary_index: summaryIndex,
|
|
470
|
-
text,
|
|
471
|
-
});
|
|
472
|
-
events.push({
|
|
473
|
-
type: 'response.reasoning_summary_part.done',
|
|
474
|
-
sequence_number: mapper.nextSequence(),
|
|
475
|
-
output_index: outputIndex,
|
|
476
|
-
item_id: item.id,
|
|
477
|
-
summary_index: summaryIndex,
|
|
478
|
-
part,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
events.push({
|
|
483
|
-
type: 'response.output_item.done',
|
|
484
|
-
sequence_number: mapper.nextSequence(),
|
|
485
|
-
output_index: outputIndex,
|
|
486
|
-
item,
|
|
487
|
-
});
|
|
488
|
-
return events;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function outputEventsFromPayload(payload, mapper, { skipTypes = new Set() } = {}) {
|
|
492
|
-
const events = [];
|
|
493
|
-
const output = Array.isArray(payload.output) ? payload.output : [];
|
|
494
|
-
for (const [outputIndex, item] of output.entries()) {
|
|
495
|
-
if (skipTypes.has(item?.type)) continue;
|
|
496
|
-
mapper.output[outputIndex] = item;
|
|
497
|
-
if (item?.type === 'reasoning') {
|
|
498
|
-
events.push(...replayReasoningItemEvents({ item, outputIndex, mapper }));
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
events.push({
|
|
502
|
-
type: 'response.output_item.added',
|
|
503
|
-
sequence_number: mapper.nextSequence(),
|
|
504
|
-
output_index: outputIndex,
|
|
505
|
-
item,
|
|
506
|
-
});
|
|
507
|
-
if (item.type === 'message' && Array.isArray(item.content)) {
|
|
508
|
-
for (const [contentIndex, part] of item.content.entries()) {
|
|
509
|
-
events.push({
|
|
510
|
-
type: 'response.content_part.added',
|
|
511
|
-
sequence_number: mapper.nextSequence(),
|
|
512
|
-
output_index: outputIndex,
|
|
513
|
-
content_index: contentIndex,
|
|
514
|
-
item_id: item.id,
|
|
515
|
-
part: part.type === 'output_text' ? { ...part, text: '', annotations: [] } : part,
|
|
516
|
-
});
|
|
517
|
-
if (part.type === 'output_text' && part.text) {
|
|
518
|
-
events.push({
|
|
519
|
-
type: 'response.output_text.delta',
|
|
520
|
-
sequence_number: mapper.nextSequence(),
|
|
521
|
-
output_index: outputIndex,
|
|
522
|
-
content_index: contentIndex,
|
|
523
|
-
item_id: item.id,
|
|
524
|
-
delta: part.text,
|
|
525
|
-
logprobs: [],
|
|
526
|
-
});
|
|
527
|
-
for (const [annotationIndex, annotation] of (Array.isArray(part.annotations) ? part.annotations : []).entries()) {
|
|
528
|
-
events.push({
|
|
529
|
-
type: 'response.output_text.annotation.added',
|
|
530
|
-
sequence_number: mapper.nextSequence(),
|
|
531
|
-
output_index: outputIndex,
|
|
532
|
-
content_index: contentIndex,
|
|
533
|
-
item_id: item.id,
|
|
534
|
-
annotation_index: annotationIndex,
|
|
535
|
-
annotation,
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
events.push({
|
|
539
|
-
type: 'response.output_text.done',
|
|
540
|
-
sequence_number: mapper.nextSequence(),
|
|
541
|
-
output_index: outputIndex,
|
|
542
|
-
content_index: contentIndex,
|
|
543
|
-
item_id: item.id,
|
|
544
|
-
text: part.text,
|
|
545
|
-
logprobs: [],
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
events.push({
|
|
549
|
-
type: 'response.content_part.done',
|
|
550
|
-
sequence_number: mapper.nextSequence(),
|
|
551
|
-
output_index: outputIndex,
|
|
552
|
-
content_index: contentIndex,
|
|
553
|
-
item_id: item.id,
|
|
554
|
-
part,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
events.push({
|
|
559
|
-
type: 'response.output_item.done',
|
|
560
|
-
sequence_number: mapper.nextSequence(),
|
|
561
|
-
output_index: outputIndex,
|
|
562
|
-
item,
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
return events;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function completedEventFromPayload(payload, mapper) {
|
|
569
|
-
return {
|
|
570
|
-
type: payload.status === 'incomplete' ? 'response.incomplete' : 'response.completed',
|
|
571
|
-
sequence_number: mapper.nextSequence(),
|
|
572
|
-
response: payload,
|
|
573
|
-
};
|
|
574
|
-
}
|
|
544
|
+
const STREAM_HEARTBEAT_MS = 10000;
|
|
575
545
|
|
|
576
546
|
function writeSseEvent(res, event) {
|
|
547
|
+
if (res.writableEnded || res.destroyed) return;
|
|
577
548
|
res.write(serializeResponsesSseEvent(event));
|
|
578
549
|
}
|
|
579
550
|
|
|
@@ -609,7 +580,242 @@ function webSearchSseWriter({ res, mapper, outputIndexBySearch, includeSources =
|
|
|
609
580
|
};
|
|
610
581
|
}
|
|
611
582
|
|
|
612
|
-
|
|
583
|
+
async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest, config, res, responseId, previousResponseId, clientSignal }) {
|
|
584
|
+
const effectiveChatRequest = { ...chatRequest, normalized };
|
|
585
|
+
const state = prepareWebSearchRequest({ normalized, chatRequest: effectiveChatRequest, config });
|
|
586
|
+
if (!state.enabled) return { handled: false };
|
|
587
|
+
|
|
588
|
+
const searchConfig = state.config || config;
|
|
589
|
+
const maxRounds = maxWebSearchRounds(searchConfig);
|
|
590
|
+
let currentChatRequest = state.chatRequest;
|
|
591
|
+
|
|
592
|
+
const buildUpstreamRequest = (stage) => {
|
|
593
|
+
const upstreamRequest = toProviderChatCompletionsRequest(currentChatRequest, config);
|
|
594
|
+
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
595
|
+
logDebugPayload(config, upstreamRequest, { stage, rawRequest, normalized, chatRequest: currentChatRequest });
|
|
596
|
+
return upstreamRequest;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const firstUpstreamRequest = buildUpstreamRequest('web_search_stream_round_0');
|
|
600
|
+
let upstreamResponse = await callChatCompletions({
|
|
601
|
+
baseUrl: config.upstreamBaseUrl,
|
|
602
|
+
apiKey: config.upstreamApiKey,
|
|
603
|
+
request: firstUpstreamRequest,
|
|
604
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
605
|
+
signal: clientSignal,
|
|
606
|
+
});
|
|
607
|
+
if (!upstreamResponse.ok) {
|
|
608
|
+
sendJson(res, upstreamResponse.status, await readJsonResponse(upstreamResponse));
|
|
609
|
+
return { handled: true };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const mapper = new ResponsesStreamMapper({
|
|
613
|
+
responseId,
|
|
614
|
+
model: firstUpstreamRequest.model,
|
|
615
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
616
|
+
previousResponseId,
|
|
617
|
+
normalized,
|
|
618
|
+
config,
|
|
619
|
+
...resolveReasoningStreamMode(config, firstUpstreamRequest),
|
|
620
|
+
holdToolItemEvents: true,
|
|
621
|
+
knownToolNames: chatToolNamesFromTools(firstUpstreamRequest.tools),
|
|
622
|
+
});
|
|
623
|
+
const searchWriter = webSearchSseWriter({
|
|
624
|
+
res,
|
|
625
|
+
mapper,
|
|
626
|
+
outputIndexBySearch: new Map(),
|
|
627
|
+
includeSources: shouldIncludeSearchSources(normalized),
|
|
628
|
+
});
|
|
629
|
+
const streamSearchItems = new Set();
|
|
630
|
+
const roundUsages = [];
|
|
631
|
+
const searches = [];
|
|
632
|
+
const openedPages = [];
|
|
633
|
+
let finalAnswerForced = false;
|
|
634
|
+
let roundEnd = null;
|
|
635
|
+
|
|
636
|
+
res.writeHead(200, {
|
|
637
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
638
|
+
'cache-control': 'no-cache, no-transform',
|
|
639
|
+
connection: 'keep-alive',
|
|
640
|
+
'x-accel-buffering': 'no',
|
|
641
|
+
});
|
|
642
|
+
writeSseEvent(res, mapper.createdEvent());
|
|
643
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
644
|
+
const heartbeat = setInterval(() => {
|
|
645
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
646
|
+
}, STREAM_HEARTBEAT_MS);
|
|
647
|
+
|
|
648
|
+
const endStream = () => {
|
|
649
|
+
writeSseDone(res);
|
|
650
|
+
res.end();
|
|
651
|
+
};
|
|
652
|
+
const failStream = (message) => {
|
|
653
|
+
for (const event of mapper.streamFailed(message)) writeSseEvent(res, event);
|
|
654
|
+
endStream();
|
|
655
|
+
return { handled: true };
|
|
656
|
+
};
|
|
657
|
+
const finishTurn = () => {
|
|
658
|
+
if (mapper.messageItem?.content?.length) {
|
|
659
|
+
annotateMessagePartWithWebCitations(mapper.messageItem.content[0], searches, openedPages);
|
|
660
|
+
}
|
|
661
|
+
const combinedUsage = combineUsage(roundUsages.concat([{ usage: mapper.pendingUsage }]));
|
|
662
|
+
for (const event of mapper.finalize(mapper.pendingFinishReason || 'stop', combinedUsage)) {
|
|
663
|
+
writeSseEvent(res, event);
|
|
664
|
+
}
|
|
665
|
+
endStream();
|
|
666
|
+
return { handled: true, chatRequest: currentChatRequest, assistantMessage: mapper.roundAssistantMessage() };
|
|
667
|
+
};
|
|
668
|
+
const relayRound = async () => {
|
|
669
|
+
roundEnd = null;
|
|
670
|
+
mapper.markRoundStart();
|
|
671
|
+
await relayChatCompletionsResponse({
|
|
672
|
+
upstreamResponse,
|
|
673
|
+
res,
|
|
674
|
+
passThrough: false,
|
|
675
|
+
writeHeaders: false,
|
|
676
|
+
endResponse: false,
|
|
677
|
+
onStreamChunk(event) {
|
|
678
|
+
if (event?.done) {
|
|
679
|
+
roundEnd = { eof: Boolean(event.eof) };
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
for (const responseEvent of mapper.mapChatEvent(event)) {
|
|
683
|
+
writeSseEvent(res, responseEvent);
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
await relayRound();
|
|
691
|
+
for (let round = 0; ; round += 1) {
|
|
692
|
+
if (clientSignal?.aborted) {
|
|
693
|
+
return { handled: true };
|
|
694
|
+
}
|
|
695
|
+
if (!mapper.pendingFinishReason && roundEnd?.eof) {
|
|
696
|
+
return failStream('upstream stream ended before completion');
|
|
697
|
+
}
|
|
698
|
+
mapper.expandParallelToolItems();
|
|
699
|
+
const roundMessage = mapper.roundAssistantMessage();
|
|
700
|
+
const commentaryCalls = bridgedCommentaryToolCallsFromMessage(roundMessage, currentChatRequest.tools);
|
|
701
|
+
for (const event of mapper.convertBridgedCommentaryItems()) writeSseEvent(res, event);
|
|
702
|
+
const completionLike = {
|
|
703
|
+
choices: [{ index: 0, message: roundMessage, finish_reason: mapper.pendingFinishReason || 'stop' }],
|
|
704
|
+
};
|
|
705
|
+
const routingCompletion = stripBridgedCommentaryFromCompletion(completionLike, currentChatRequest.tools);
|
|
706
|
+
|
|
707
|
+
if (finalAnswerForced) {
|
|
708
|
+
mapper.removeToolItems();
|
|
709
|
+
const reason = finalAnswerIncompleteReason(routingCompletion);
|
|
710
|
+
if (reason) {
|
|
711
|
+
const events = mapper.replaceBufferedAssistantText(gatewayIncompleteMessageContent(reason));
|
|
712
|
+
for (const event of events || []) writeSseEvent(res, event);
|
|
713
|
+
}
|
|
714
|
+
return finishTurn();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (hasKnownExternalToolCalls(routingCompletion, currentChatRequest.tools)) {
|
|
718
|
+
const kept = knownExternalToolCallsCompletion(routingCompletion, currentChatRequest.tools);
|
|
719
|
+
const keptIds = new Set(
|
|
720
|
+
(kept.choices?.[0]?.message?.tool_calls || []).map((toolCall) => toolCall.id).filter(Boolean),
|
|
721
|
+
);
|
|
722
|
+
mapper.removeToolItems((item) => !keptIds.has(item.call_id));
|
|
723
|
+
return finishTurn();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const wantsInternalWeb = shouldContinueWebSearchLoop(routingCompletion);
|
|
727
|
+
const wantsInternalRound = wantsInternalWeb || commentaryCalls.length > 0;
|
|
728
|
+
const hasToolCallsRound = hasAnyToolCalls(routingCompletion);
|
|
729
|
+
|
|
730
|
+
if (!wantsInternalRound || round >= maxRounds) {
|
|
731
|
+
if (hasToolCallsRound && hasUnknownExternalToolCalls(routingCompletion, currentChatRequest.tools)) {
|
|
732
|
+
const unsupportedMessages = unhandledToolMessagesFromCompletion(routingCompletion, undefined, {
|
|
733
|
+
onlyUnknownExternal: true,
|
|
734
|
+
tools: currentChatRequest.tools,
|
|
735
|
+
});
|
|
736
|
+
currentChatRequest = toolCallsToFinalAnswerRequest({
|
|
737
|
+
...currentChatRequest,
|
|
738
|
+
messages: currentChatRequest.messages.concat(unsupportedMessages),
|
|
739
|
+
});
|
|
740
|
+
} else if (wantsInternalRound || (!hasToolCallsRound && hasWebSearchContext(searches, openedPages) && !hasVisibleAssistantContent(routingCompletion))) {
|
|
741
|
+
currentChatRequest = toolCallsToFinalAnswerRequest(currentChatRequest);
|
|
742
|
+
} else {
|
|
743
|
+
return finishTurn();
|
|
744
|
+
}
|
|
745
|
+
finalAnswerForced = true;
|
|
746
|
+
roundUsages.push({ usage: mapper.pendingUsage });
|
|
747
|
+
mapper.removeToolItems();
|
|
748
|
+
for (const event of mapper.beginNextRound()) writeSseEvent(res, event);
|
|
749
|
+
mapper.holdVisibleTextUntilDone();
|
|
750
|
+
} else {
|
|
751
|
+
roundUsages.push({ usage: mapper.pendingUsage });
|
|
752
|
+
for (const event of mapper.beginNextRound()) writeSseEvent(res, event);
|
|
753
|
+
const controller = new AbortController();
|
|
754
|
+
const timeout = setTimeout(() => controller.abort(), webToolTimeoutMs(searchConfig));
|
|
755
|
+
let toolResult;
|
|
756
|
+
try {
|
|
757
|
+
toolResult = await executeWebSearchCalls({
|
|
758
|
+
completion: completionLike,
|
|
759
|
+
config: searchConfig,
|
|
760
|
+
signal: mergeAbortSignals([controller.signal, clientSignal]),
|
|
761
|
+
onSearchStart(search) {
|
|
762
|
+
if (search.action && search.auto) return;
|
|
763
|
+
streamSearchItems.add(search);
|
|
764
|
+
searchWriter.start(search);
|
|
765
|
+
},
|
|
766
|
+
onSearchDone(search) {
|
|
767
|
+
if (!streamSearchItems.has(search)) return;
|
|
768
|
+
searchWriter.done(search);
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
} finally {
|
|
772
|
+
clearTimeout(timeout);
|
|
773
|
+
}
|
|
774
|
+
searches.push(...toolResult.searches);
|
|
775
|
+
openedPages.push(...(toolResult.openedPages || []));
|
|
776
|
+
currentChatRequest = {
|
|
777
|
+
...currentChatRequest,
|
|
778
|
+
messages: currentChatRequest.messages.concat(toolResult.messages, commentaryToolMessages(commentaryCalls)),
|
|
779
|
+
tool_choice:
|
|
780
|
+
currentChatRequest.tool_choice?.function?.name === INTERNAL_WEB_SEARCH_TOOL
|
|
781
|
+
? 'auto'
|
|
782
|
+
: currentChatRequest.tool_choice,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (clientSignal?.aborted) {
|
|
787
|
+
return { handled: true };
|
|
788
|
+
}
|
|
789
|
+
const upstreamRequest = buildUpstreamRequest(`web_search_stream_round_${round + 1}`);
|
|
790
|
+
upstreamResponse = await callChatCompletions({
|
|
791
|
+
baseUrl: config.upstreamBaseUrl,
|
|
792
|
+
apiKey: config.upstreamApiKey,
|
|
793
|
+
request: upstreamRequest,
|
|
794
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
795
|
+
signal: clientSignal,
|
|
796
|
+
});
|
|
797
|
+
if (!upstreamResponse.ok) {
|
|
798
|
+
const data = await readJsonResponse(upstreamResponse);
|
|
799
|
+
return failStream(data?.error?.message || `upstream error ${upstreamResponse.status}`);
|
|
800
|
+
}
|
|
801
|
+
await relayRound();
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
if (clientSignal?.aborted) {
|
|
805
|
+
return { handled: true };
|
|
806
|
+
}
|
|
807
|
+
throw error;
|
|
808
|
+
} finally {
|
|
809
|
+
clearInterval(heartbeat);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
814
|
+
sessions ||= new SessionStore({
|
|
815
|
+
persistPath: config.sessionStoreEnabled === false ? '' : config.sessionStorePath,
|
|
816
|
+
maxSessions: config.sessionStoreMaxSessions,
|
|
817
|
+
maxBytes: config.sessionStoreMaxBytes,
|
|
818
|
+
});
|
|
613
819
|
let modelCache = null;
|
|
614
820
|
|
|
615
821
|
async function getModelList() {
|
|
@@ -647,18 +853,26 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
647
853
|
|
|
648
854
|
const request = parsed.value;
|
|
649
855
|
const normalized = normalizeResponsesRequest(request);
|
|
650
|
-
const currentInputMessages = normalized.messages.slice();
|
|
651
856
|
const previousResponseId = normalized.previous_response_id || request.previous_response_id || null;
|
|
652
857
|
const conversationId = conversationIdFromRequest(request, normalized);
|
|
653
858
|
const priorSession = previousResponseId ? sessions.get(previousResponseId) : conversationId ? sessions.getConversation(conversationId) : null;
|
|
654
|
-
|
|
655
|
-
|
|
859
|
+
const priorMessages = historyMessagesFromSession(priorSession);
|
|
860
|
+
if (priorMessages.length) {
|
|
656
861
|
normalized.messages = priorMessages.concat(normalized.messages);
|
|
657
862
|
}
|
|
658
863
|
normalized.messages = prependMissingAssistantToolMessages(normalized.messages, sessions);
|
|
864
|
+
normalized.messages = restoreAssistantReasoningContent(normalized.messages, sessions);
|
|
659
865
|
|
|
660
866
|
const chatRequest = toChatCompletionsRequest(normalized);
|
|
661
867
|
const useWebSearchEmulator = hasTavilyWebSearch(config, normalized);
|
|
868
|
+
if (!useWebSearchEmulator) {
|
|
869
|
+
const existingToolNames = new Set(chatToolNamesFromTools(chatRequest.tools));
|
|
870
|
+
const webShims = unavailableWebSearchToolShims(normalized.tools)
|
|
871
|
+
.filter((tool) => !existingToolNames.has(tool.function.name));
|
|
872
|
+
if (webShims.length) chatRequest.tools = [...(chatRequest.tools ?? []), ...webShims];
|
|
873
|
+
}
|
|
874
|
+
const clientAbort = clientAbortControllerFor(res);
|
|
875
|
+
const clientSignal = clientAbort.signal;
|
|
662
876
|
let upstreamRequest = toProviderChatCompletionsRequest(chatRequest, config);
|
|
663
877
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || normalized.model;
|
|
664
878
|
|
|
@@ -672,119 +886,74 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
672
886
|
}
|
|
673
887
|
|
|
674
888
|
const responseId = generateId('resp');
|
|
675
|
-
const nextSession = { id: responseId,
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
streamMapper = new ResponsesStreamMapper({
|
|
685
|
-
responseId,
|
|
686
|
-
model: upstreamRequest.model,
|
|
687
|
-
createdAt,
|
|
688
|
-
previousResponseId,
|
|
689
|
-
normalized,
|
|
690
|
-
emitReasoningSummary: true,
|
|
691
|
-
emitReasoningText: false,
|
|
692
|
-
});
|
|
693
|
-
streamSearchWriter = webSearchSseWriter({
|
|
694
|
-
res,
|
|
695
|
-
mapper: streamMapper,
|
|
696
|
-
outputIndexBySearch: new Map(),
|
|
697
|
-
includeSources: shouldIncludeSearchSources(normalized),
|
|
698
|
-
});
|
|
699
|
-
res.writeHead(200, {
|
|
700
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
701
|
-
'cache-control': 'no-cache, no-transform',
|
|
702
|
-
connection: 'keep-alive',
|
|
703
|
-
'x-accel-buffering': 'no',
|
|
704
|
-
});
|
|
705
|
-
writeSseEvent(res, streamMapper.createdEvent());
|
|
706
|
-
writeSseEvent(res, streamMapper.inProgressEvent());
|
|
889
|
+
const nextSession = { id: responseId, messages: [] };
|
|
890
|
+
const persistSession = shouldPersistSession(normalized, request, conversationId);
|
|
891
|
+
const persistTurn = (chatMessages, assistantMessage) => {
|
|
892
|
+
if (persistSession) {
|
|
893
|
+
nextSession.messages = sessionMessages(chatMessages, assistantMessage);
|
|
894
|
+
sessions.set(responseId, nextSession);
|
|
895
|
+
sessions.setConversation(conversationId, responseId);
|
|
896
|
+
} else {
|
|
897
|
+
sessions.rememberAssistantMessage(assistantMessage);
|
|
707
898
|
}
|
|
899
|
+
};
|
|
708
900
|
|
|
901
|
+
if (useWebSearchEmulator && request.stream) {
|
|
902
|
+
const result = await runStreamingWebSearchTurn({
|
|
903
|
+
rawRequest: request,
|
|
904
|
+
normalized,
|
|
905
|
+
chatRequest,
|
|
906
|
+
config,
|
|
907
|
+
res,
|
|
908
|
+
responseId,
|
|
909
|
+
previousResponseId,
|
|
910
|
+
clientSignal,
|
|
911
|
+
});
|
|
912
|
+
if (result.handled) {
|
|
913
|
+
if (result.assistantMessage) persistTurn(result.chatRequest.messages, result.assistantMessage);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
} else if (useWebSearchEmulator) {
|
|
709
917
|
const loop = await runWebSearchChatLoop({
|
|
918
|
+
rawRequest: request,
|
|
710
919
|
normalized,
|
|
711
920
|
chatRequest,
|
|
712
921
|
config,
|
|
713
|
-
|
|
714
|
-
? (search) => {
|
|
715
|
-
if (search.action && search.auto) return;
|
|
716
|
-
streamSearchItems.add(search);
|
|
717
|
-
streamSearchWriter.start(search);
|
|
718
|
-
}
|
|
719
|
-
: undefined,
|
|
720
|
-
onSearchDone: streamWebSearch
|
|
721
|
-
? (search) => {
|
|
722
|
-
if (!streamSearchItems.has(search)) return;
|
|
723
|
-
streamSearchWriter.done(search);
|
|
724
|
-
}
|
|
725
|
-
: undefined,
|
|
922
|
+
clientSignal,
|
|
726
923
|
});
|
|
727
924
|
if (!loop.ok) {
|
|
728
|
-
if (streamWebSearch) {
|
|
729
|
-
writeSseEvent(res, {
|
|
730
|
-
type: 'response.failed',
|
|
731
|
-
sequence_number: streamMapper.nextSequence(),
|
|
732
|
-
response: streamMapper.response('failed'),
|
|
733
|
-
});
|
|
734
|
-
writeSseDone(res);
|
|
735
|
-
res.end();
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
925
|
sendJson(res, loop.status || 500, loop.data);
|
|
739
926
|
return;
|
|
740
927
|
}
|
|
741
928
|
upstreamRequest = toProviderChatCompletionsRequest(loop.chatRequest, config);
|
|
742
929
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || normalized.model;
|
|
743
|
-
|
|
930
|
+
logDebugPayload(config, upstreamRequest, {
|
|
931
|
+
stage: 'web_search_compatibility_payload',
|
|
932
|
+
rawRequest: request,
|
|
933
|
+
normalized,
|
|
934
|
+
chatRequest: loop.chatRequest,
|
|
935
|
+
});
|
|
744
936
|
const payload = applyWebSearchOutputCompatibility(convertChatCompletionToResponses({
|
|
745
937
|
completion: loop.completion,
|
|
746
|
-
model,
|
|
938
|
+
model: upstreamRequest.model,
|
|
747
939
|
previousResponseId,
|
|
748
940
|
normalized,
|
|
749
941
|
responseId,
|
|
750
942
|
config,
|
|
751
943
|
}), loop.searches, normalized, loop.openedPages);
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
nextSession.history.push({
|
|
756
|
-
request: normalized,
|
|
757
|
-
chatRequest: loop.chatRequest,
|
|
758
|
-
upstreamRequest,
|
|
759
|
-
inputMessages: currentInputMessages,
|
|
760
|
-
historyMessages: loop.chatRequest.messages.concat(assistantMessage),
|
|
761
|
-
assistantMessage,
|
|
762
|
-
createdAt: Date.now(),
|
|
763
|
-
});
|
|
764
|
-
sessions.set(responseId, nextSession);
|
|
765
|
-
sessions.setConversation(conversationId, responseId);
|
|
766
|
-
|
|
767
|
-
if (!streamWebSearch) {
|
|
768
|
-
sendJson(res, 200, payload);
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
for (const event of outputEventsFromPayload(payload, streamMapper, { skipTypes: new Set(['web_search_call']) })) {
|
|
773
|
-
writeSseEvent(res, event);
|
|
774
|
-
}
|
|
775
|
-
writeSseEvent(res, completedEventFromPayload(payload, streamMapper));
|
|
776
|
-
writeSseDone(res);
|
|
777
|
-
res.end();
|
|
944
|
+
persistTurn(loop.chatRequest.messages, assistantMessageFromResponseOutput(payload.output));
|
|
945
|
+
sendJson(res, 200, payload);
|
|
778
946
|
return;
|
|
779
947
|
}
|
|
780
948
|
|
|
781
|
-
logDebugPayload(config, upstreamRequest);
|
|
949
|
+
logDebugPayload(config, upstreamRequest, { rawRequest: request, normalized, chatRequest });
|
|
782
950
|
|
|
783
951
|
const upstreamResponse = await callChatCompletions({
|
|
784
952
|
baseUrl: config.upstreamBaseUrl,
|
|
785
953
|
apiKey: config.upstreamApiKey,
|
|
786
954
|
request: upstreamRequest,
|
|
787
955
|
timeoutMs: config.upstreamTimeoutMs,
|
|
956
|
+
signal: clientSignal,
|
|
788
957
|
});
|
|
789
958
|
|
|
790
959
|
const model = upstreamRequest.model;
|
|
@@ -805,16 +974,7 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
805
974
|
responseId,
|
|
806
975
|
config,
|
|
807
976
|
});
|
|
808
|
-
|
|
809
|
-
request: normalized,
|
|
810
|
-
chatRequest,
|
|
811
|
-
upstreamRequest,
|
|
812
|
-
inputMessages: currentInputMessages,
|
|
813
|
-
assistantMessage: assistantMessageFromResponseOutput(payload.output),
|
|
814
|
-
createdAt: Date.now(),
|
|
815
|
-
});
|
|
816
|
-
sessions.set(responseId, nextSession);
|
|
817
|
-
sessions.setConversation(conversationId, responseId);
|
|
977
|
+
persistTurn(chatRequest.messages, assistantMessageFromResponseOutput(payload.output));
|
|
818
978
|
sendJson(res, 200, payload);
|
|
819
979
|
return;
|
|
820
980
|
}
|
|
@@ -833,48 +993,46 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
833
993
|
previousResponseId,
|
|
834
994
|
normalized,
|
|
835
995
|
config,
|
|
836
|
-
...(
|
|
837
|
-
|
|
838
|
-
return {
|
|
839
|
-
bufferOutputUntilDone: upstreamRequest?.thinking?.type === 'enabled',
|
|
840
|
-
...reasoningMode,
|
|
841
|
-
};
|
|
842
|
-
})(),
|
|
996
|
+
...resolveReasoningStreamMode(config, upstreamRequest),
|
|
997
|
+
knownToolNames: chatToolNamesFromTools(upstreamRequest.tools),
|
|
843
998
|
});
|
|
844
999
|
let doneSent = false;
|
|
845
1000
|
const writeResponsesDone = () => {
|
|
846
1001
|
if (doneSent) return;
|
|
847
1002
|
doneSent = true;
|
|
848
|
-
res
|
|
1003
|
+
writeSseEvent(res, { done: true });
|
|
849
1004
|
};
|
|
850
1005
|
|
|
851
|
-
res
|
|
852
|
-
res
|
|
1006
|
+
writeSseEvent(res, mapper.createdEvent());
|
|
1007
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
1008
|
+
let lastEventWriteAt = Date.now();
|
|
853
1009
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1010
|
+
try {
|
|
1011
|
+
await relayChatCompletionsResponse({
|
|
1012
|
+
upstreamResponse,
|
|
1013
|
+
res,
|
|
1014
|
+
passThrough: false,
|
|
1015
|
+
writeHeaders: false,
|
|
1016
|
+
onStreamChunk(event) {
|
|
1017
|
+
const events = mapper.mapChatEvent(event);
|
|
1018
|
+
for (const responseEvent of events) {
|
|
1019
|
+
writeSseEvent(res, responseEvent);
|
|
1020
|
+
}
|
|
1021
|
+
if (events.length) {
|
|
1022
|
+
lastEventWriteAt = Date.now();
|
|
1023
|
+
} else if (!event?.done && Date.now() - lastEventWriteAt >= STREAM_HEARTBEAT_MS) {
|
|
1024
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
1025
|
+
lastEventWriteAt = Date.now();
|
|
1026
|
+
}
|
|
1027
|
+
if (event?.done) writeResponsesDone();
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
if (clientSignal.aborted) return;
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
867
1034
|
|
|
868
|
-
|
|
869
|
-
request: normalized,
|
|
870
|
-
chatRequest,
|
|
871
|
-
upstreamRequest,
|
|
872
|
-
inputMessages: currentInputMessages,
|
|
873
|
-
assistantMessage: mapper.assistantMessage(),
|
|
874
|
-
createdAt: Date.now(),
|
|
875
|
-
});
|
|
876
|
-
sessions.set(responseId, nextSession);
|
|
877
|
-
sessions.setConversation(conversationId, responseId);
|
|
1035
|
+
persistTurn(chatRequest.messages, mapper.assistantMessage());
|
|
878
1036
|
}
|
|
879
1037
|
|
|
880
1038
|
return http.createServer(async (req, res) => {
|
|
@@ -917,6 +1075,7 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
917
1075
|
apiKey: config.upstreamApiKey,
|
|
918
1076
|
request: upstreamRequest,
|
|
919
1077
|
timeoutMs: config.upstreamTimeoutMs,
|
|
1078
|
+
signal: clientAbortControllerFor(res).signal,
|
|
920
1079
|
});
|
|
921
1080
|
await relayChatCompletionsResponse({ upstreamResponse, res, passThrough: true });
|
|
922
1081
|
return;
|
|
@@ -924,6 +1083,7 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
924
1083
|
|
|
925
1084
|
sendJson(res, 404, { error: { message: 'Not found' } });
|
|
926
1085
|
} catch (error) {
|
|
1086
|
+
if (res.destroyed) return;
|
|
927
1087
|
if (res.headersSent) {
|
|
928
1088
|
if (!res.writableEnded) {
|
|
929
1089
|
res.write(serializeResponsesSseEvent({
|