@galaxy-yearn/codex-deepseek-gateway 0.1.7 → 0.2.1
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 +57 -37
- package/README.zh-CN.md +213 -0
- package/config/codex-model-catalog.json +32 -28
- package/config/codex-model-catalog.zh.json +36 -32
- package/package.json +2 -1
- package/src/codex-launch.js +30 -8
- package/src/codex-sessions.js +69 -13
- package/src/common.js +18 -3
- package/src/config.js +13 -2
- package/src/firecrawl.js +15 -19
- package/src/model-map.js +6 -20
- package/src/protocol.js +1395 -572
- package/src/reasoning-cache.js +235 -0
- package/src/server.js +525 -378
- package/src/tavily.js +2 -11
- package/src/upstream.js +14 -12
- package/src/web-search-emulator.js +15 -16
- package/state/reasoning-cache.example.jsonl +1 -0
- package/src/session-store.js +0 -67
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
|
-
import {
|
|
24
|
+
import { ReasoningCache } from './reasoning-cache.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,
|
|
@@ -52,6 +62,14 @@ function getRequestPath(req) {
|
|
|
52
62
|
return url.pathname;
|
|
53
63
|
}
|
|
54
64
|
|
|
65
|
+
function codexRequestKind(req) {
|
|
66
|
+
const value = req.headers['x-codex-turn-metadata'];
|
|
67
|
+
if (typeof value !== 'string') return '';
|
|
68
|
+
const parsed = safeJsonParse(value);
|
|
69
|
+
if (!parsed.ok || !parsed.value || typeof parsed.value !== 'object') return '';
|
|
70
|
+
return String(parsed.value.request_kind || '');
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
function isAuthorized(req, config) {
|
|
56
74
|
if (!config.proxyApiKey) return true;
|
|
57
75
|
const authorization = req.headers.authorization || '';
|
|
@@ -59,34 +77,7 @@ function isAuthorized(req, config) {
|
|
|
59
77
|
return bearerToken === config.proxyApiKey || req.headers['x-api-key'] === config.proxyApiKey;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
|
-
function
|
|
63
|
-
const conversation = normalized.conversation ?? request.conversation;
|
|
64
|
-
if (typeof conversation === 'string') return conversation;
|
|
65
|
-
if (conversation && typeof conversation === 'object') return conversation.id || conversation.conversation_id || null;
|
|
66
|
-
return request.conversation_id || null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function historyMessagesFromSession(session) {
|
|
70
|
-
if (!session?.history?.length) return [];
|
|
71
|
-
const messages = [];
|
|
72
|
-
for (const turn of session.history) {
|
|
73
|
-
if (Array.isArray(turn.historyMessages)) {
|
|
74
|
-
messages.push(...turn.historyMessages);
|
|
75
|
-
continue;
|
|
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
|
-
}
|
|
86
|
-
return messages;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function prependMissingAssistantToolMessages(messages, sessions) {
|
|
80
|
+
function prependMissingAssistantToolMessages(messages, reasoningCache) {
|
|
90
81
|
const existingToolCallIds = new Set();
|
|
91
82
|
const missingToolOutputIds = [];
|
|
92
83
|
for (const message of messages) {
|
|
@@ -106,7 +97,7 @@ function prependMissingAssistantToolMessages(messages, sessions) {
|
|
|
106
97
|
const inserted = new Set(existingToolCallIds);
|
|
107
98
|
for (const callId of missingToolOutputIds) {
|
|
108
99
|
if (inserted.has(callId)) continue;
|
|
109
|
-
const assistantMessage =
|
|
100
|
+
const assistantMessage = reasoningCache.getAssistantMessageForToolCall(callId);
|
|
110
101
|
if (!assistantMessage) continue;
|
|
111
102
|
prefix.push(assistantMessage);
|
|
112
103
|
for (const id of extractToolCallIdsFromMessages([assistantMessage])) inserted.add(id);
|
|
@@ -114,6 +105,22 @@ function prependMissingAssistantToolMessages(messages, sessions) {
|
|
|
114
105
|
return prefix.length ? prefix.concat(messages) : messages;
|
|
115
106
|
}
|
|
116
107
|
|
|
108
|
+
function restoreAssistantReasoningContent(messages, reasoningCache) {
|
|
109
|
+
return messages.map((message) => {
|
|
110
|
+
if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls) || !message.tool_calls.length) {
|
|
111
|
+
return message;
|
|
112
|
+
}
|
|
113
|
+
if (typeof message.reasoning_content === 'string' && message.reasoning_content) return message;
|
|
114
|
+
for (const toolCall of message.tool_calls) {
|
|
115
|
+
const stored = reasoningCache.getAssistantMessageForToolCall(toolCall?.id);
|
|
116
|
+
if (typeof stored?.reasoning_content === 'string' && stored.reasoning_content) {
|
|
117
|
+
return { ...message, reasoning_content: stored.reasoning_content };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return message;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
function summarizeMessage(message, index) {
|
|
118
125
|
return {
|
|
119
126
|
index,
|
|
@@ -126,16 +133,66 @@ function summarizeMessage(message, index) {
|
|
|
126
133
|
};
|
|
127
134
|
}
|
|
128
135
|
|
|
129
|
-
function
|
|
136
|
+
function summarizeTool(tool, index) {
|
|
137
|
+
if (!tool || typeof tool !== 'object') {
|
|
138
|
+
return { index, type: typeof tool };
|
|
139
|
+
}
|
|
140
|
+
const fn = tool.type === 'function' && tool.function && typeof tool.function === 'object'
|
|
141
|
+
? tool.function
|
|
142
|
+
: null;
|
|
143
|
+
return {
|
|
144
|
+
index,
|
|
145
|
+
type: tool.type,
|
|
146
|
+
name: fn?.name || tool.name || tool.tool_name || tool.server_label,
|
|
147
|
+
namespace: fn?.namespace || tool.namespace,
|
|
148
|
+
child_tools: Array.isArray(tool.tools) ? tool.tools.length : undefined,
|
|
149
|
+
has_parameters: Boolean(fn?.parameters || tool.parameters || tool.input_schema),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function summarizeTools(tools) {
|
|
154
|
+
return Array.isArray(tools) ? tools.map(summarizeTool) : [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const DEBUG_PAYLOAD_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
158
|
+
|
|
159
|
+
function rotateDebugPayloadLog(logPath, maxBytes) {
|
|
160
|
+
try {
|
|
161
|
+
if (statSync(logPath).size < maxBytes) return;
|
|
162
|
+
rmSync(`${logPath}.1`, { force: true });
|
|
163
|
+
renameSync(logPath, `${logPath}.1`);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeDebugPayloadLine(config, line) {
|
|
169
|
+
process.stderr.write(line);
|
|
170
|
+
if (!config.debugPayloadLogPath) return;
|
|
171
|
+
try {
|
|
172
|
+
const logPath = resolve(config.debugPayloadLogPath);
|
|
173
|
+
rotateDebugPayloadLog(logPath, config.debugPayloadLogMaxBytes || DEBUG_PAYLOAD_LOG_MAX_BYTES);
|
|
174
|
+
appendFileSync(logPath, line, 'utf8');
|
|
175
|
+
} catch (error) {
|
|
176
|
+
process.stderr.write(`[codex-deepseek-gateway] failed to write debug payload log: ${error.message || error}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function logDebugPayload(config, request, context = {}) {
|
|
130
181
|
if (!config.debugPayload) return;
|
|
131
182
|
const summary = {
|
|
183
|
+
stage: context.stage || 'upstream',
|
|
132
184
|
model: request.model,
|
|
133
185
|
stream: request.stream,
|
|
134
186
|
thinking: request.thinking,
|
|
135
187
|
reasoning_effort: request.reasoning_effort,
|
|
188
|
+
codex_tools: summarizeTools(context.rawRequest?.tools),
|
|
189
|
+
normalized_tools: summarizeTools(context.normalized?.tools),
|
|
190
|
+
chat_tools: summarizeTools(context.chatRequest?.tools),
|
|
191
|
+
upstream_tools: summarizeTools(request.tools),
|
|
192
|
+
tool_choice: request.tool_choice,
|
|
136
193
|
messages: Array.isArray(request.messages) ? request.messages.map(summarizeMessage) : [],
|
|
137
194
|
};
|
|
138
|
-
|
|
195
|
+
writeDebugPayloadLine(config, `[codex-deepseek-gateway] upstream request ${JSON.stringify(summary)}\n`);
|
|
139
196
|
}
|
|
140
197
|
|
|
141
198
|
function resolveReasoningStreamMode(config, upstreamRequest) {
|
|
@@ -168,29 +225,26 @@ function webToolTimeoutMs(config = {}) {
|
|
|
168
225
|
return Math.max(tavilyTimeout, firecrawlTimeout);
|
|
169
226
|
}
|
|
170
227
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
if (!
|
|
228
|
+
function combineUsage(completions) {
|
|
229
|
+
const usages = completions.map((completion) => completion?.usage).filter(Boolean);
|
|
230
|
+
if (!usages.length) return null;
|
|
231
|
+
const last = usages[usages.length - 1];
|
|
232
|
+
const sum = (pick) => usages.reduce((total, usage) => total + (Number(pick(usage)) || 0), 0);
|
|
233
|
+
const promptTokens = Number(last.prompt_tokens) || 0;
|
|
234
|
+
const completionTokens = sum((usage) => usage.completion_tokens);
|
|
174
235
|
return {
|
|
175
|
-
prompt_tokens:
|
|
176
|
-
completion_tokens:
|
|
177
|
-
total_tokens:
|
|
178
|
-
reasoning_tokens: (left.reasoning_tokens || 0) + (right.reasoning_tokens || 0),
|
|
236
|
+
prompt_tokens: promptTokens,
|
|
237
|
+
completion_tokens: completionTokens,
|
|
238
|
+
total_tokens: promptTokens + completionTokens,
|
|
179
239
|
prompt_tokens_details: {
|
|
180
|
-
cached_tokens:
|
|
240
|
+
cached_tokens: last.prompt_tokens_details?.cached_tokens ?? last.prompt_cache_hit_tokens ?? 0,
|
|
181
241
|
},
|
|
182
242
|
completion_tokens_details: {
|
|
183
|
-
reasoning_tokens:
|
|
184
|
-
(left.completion_tokens_details?.reasoning_tokens || 0) +
|
|
185
|
-
(right.completion_tokens_details?.reasoning_tokens || 0),
|
|
243
|
+
reasoning_tokens: sum((usage) => usage.completion_tokens_details?.reasoning_tokens ?? usage.reasoning_tokens),
|
|
186
244
|
},
|
|
187
245
|
};
|
|
188
246
|
}
|
|
189
247
|
|
|
190
|
-
function combineUsage(completions) {
|
|
191
|
-
return completions.reduce((usage, completion) => addUsage(usage, completion?.usage), null);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
248
|
function cloneCompletionWithUsage(completion, usage) {
|
|
195
249
|
if (!usage) return completion;
|
|
196
250
|
return {
|
|
@@ -202,16 +256,14 @@ function cloneCompletionWithUsage(completion, usage) {
|
|
|
202
256
|
function toolCallsToFinalAnswerRequest(request) {
|
|
203
257
|
return {
|
|
204
258
|
...request,
|
|
205
|
-
tool_choice:
|
|
259
|
+
tool_choice: undefined,
|
|
206
260
|
tools: undefined,
|
|
207
261
|
messages: removeWebSearchInstructions(request.messages).concat({
|
|
208
262
|
role: 'user',
|
|
209
263
|
content: [
|
|
210
|
-
'Web tools are
|
|
211
|
-
'
|
|
212
|
-
'
|
|
213
|
-
'Do not write tool calls, DSML, XML, or JSON tool-call blocks in the answer.',
|
|
214
|
-
'If the available tool results are incomplete, say what is missing and answer only what the provided context supports.',
|
|
264
|
+
'Web tools are now unavailable.',
|
|
265
|
+
'Answer now using only completed tool results as visible assistant text, without tool calls or tool-call markup.',
|
|
266
|
+
'If results are incomplete, state what is missing and do not infer beyond them.',
|
|
215
267
|
].join(' '),
|
|
216
268
|
}),
|
|
217
269
|
};
|
|
@@ -245,6 +297,12 @@ function hasWebSearchContext(searches, openedPages) {
|
|
|
245
297
|
return searches.length > 0 || openedPages.length > 0;
|
|
246
298
|
}
|
|
247
299
|
|
|
300
|
+
function gatewayIncompleteMessageContent(reason) {
|
|
301
|
+
return reason === 'pseudo_tool_call_text_after_web_limit'
|
|
302
|
+
? 'Gateway incomplete: after web tools were disabled, the model wrote a tool call as text instead of producing a final answer.'
|
|
303
|
+
: 'Gateway incomplete: the model did not produce visible assistant content after web tool results and a final-answer request.';
|
|
304
|
+
}
|
|
305
|
+
|
|
248
306
|
function completionWithGatewayIncompleteMessage(completion, reason = 'no_visible_assistant_content') {
|
|
249
307
|
const next = completion == null ? {} : JSON.parse(JSON.stringify(completion));
|
|
250
308
|
if (!Array.isArray(next.choices) || !next.choices.length) {
|
|
@@ -252,9 +310,7 @@ function completionWithGatewayIncompleteMessage(completion, reason = 'no_visible
|
|
|
252
310
|
}
|
|
253
311
|
const choice = next.choices[0];
|
|
254
312
|
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.';
|
|
313
|
+
const content = gatewayIncompleteMessageContent(reason);
|
|
258
314
|
choice.message = {
|
|
259
315
|
role: 'assistant',
|
|
260
316
|
content,
|
|
@@ -276,24 +332,62 @@ function completionWithoutToolCalls(completion) {
|
|
|
276
332
|
return next;
|
|
277
333
|
}
|
|
278
334
|
|
|
279
|
-
function
|
|
280
|
-
payload.status = 'incomplete';
|
|
281
|
-
payload.incomplete_details = { reason };
|
|
282
|
-
return payload;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function callUpstreamJson({ upstreamRequest, config }) {
|
|
335
|
+
async function callUpstreamJson({ upstreamRequest, config, signal }) {
|
|
286
336
|
const response = await callChatCompletions({
|
|
287
337
|
baseUrl: config.upstreamBaseUrl,
|
|
288
338
|
apiKey: config.upstreamApiKey,
|
|
289
339
|
request: upstreamRequest,
|
|
290
340
|
timeoutMs: config.upstreamTimeoutMs,
|
|
341
|
+
signal,
|
|
291
342
|
});
|
|
292
343
|
const data = await readJsonResponse(response);
|
|
293
|
-
return {
|
|
344
|
+
return {
|
|
345
|
+
response,
|
|
346
|
+
data: resolveEmittedToolCallNamesInCompletion(
|
|
347
|
+
expandParallelToolCallsInCompletion(data),
|
|
348
|
+
chatToolNamesFromTools(upstreamRequest.tools),
|
|
349
|
+
),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function clientAbortControllerFor(res) {
|
|
354
|
+
const controller = new AbortController();
|
|
355
|
+
res.on('close', () => {
|
|
356
|
+
if (!res.writableEnded) controller.abort();
|
|
357
|
+
});
|
|
358
|
+
return controller;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function mergeAbortSignals(signals) {
|
|
362
|
+
const active = signals.filter(Boolean);
|
|
363
|
+
if (!active.length) return undefined;
|
|
364
|
+
if (active.length === 1) return active[0];
|
|
365
|
+
return AbortSignal.any(active);
|
|
294
366
|
}
|
|
295
367
|
|
|
296
|
-
|
|
368
|
+
function commentaryToolMessages(commentaryCalls) {
|
|
369
|
+
return (Array.isArray(commentaryCalls) ? commentaryCalls : []).map((toolCall) => ({
|
|
370
|
+
role: 'tool',
|
|
371
|
+
tool_call_id: toolCall.id || generateId('call'),
|
|
372
|
+
content: 'Delivered to the user.',
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function restoreCommentaryToolCalls(completion, commentaryCalls) {
|
|
377
|
+
if (!Array.isArray(commentaryCalls) || !commentaryCalls.length) return completion;
|
|
378
|
+
const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
|
|
379
|
+
if (!choice?.message) return completion;
|
|
380
|
+
const existing = Array.isArray(choice.message.tool_calls) ? choice.message.tool_calls : [];
|
|
381
|
+
return {
|
|
382
|
+
...completion,
|
|
383
|
+
choices: [
|
|
384
|
+
{ ...choice, message: { ...choice.message, tool_calls: [...commentaryCalls, ...existing] } },
|
|
385
|
+
...completion.choices.slice(1),
|
|
386
|
+
],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, config, clientSignal, onSearchStart, onSearchDone }) {
|
|
297
391
|
const effectiveChatRequest = { ...chatRequest, normalized };
|
|
298
392
|
const state = prepareWebSearchRequest({ normalized, chatRequest: effectiveChatRequest, config });
|
|
299
393
|
const searchConfig = state.config || config;
|
|
@@ -310,9 +404,16 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
310
404
|
currentChatRequest = toolCallsToFinalAnswerRequest(baseRequest);
|
|
311
405
|
const upstreamRequest = toProviderChatCompletionsRequest(currentChatRequest, config);
|
|
312
406
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
407
|
+
logDebugPayload(config, upstreamRequest, {
|
|
408
|
+
stage: 'web_search_final_answer',
|
|
409
|
+
rawRequest,
|
|
410
|
+
normalized,
|
|
411
|
+
chatRequest: currentChatRequest,
|
|
412
|
+
});
|
|
313
413
|
const { response, data } = await callUpstreamJson({
|
|
314
414
|
upstreamRequest: disableStreaming(upstreamRequest),
|
|
315
415
|
config,
|
|
416
|
+
signal: clientSignal,
|
|
316
417
|
});
|
|
317
418
|
finalResponse = response;
|
|
318
419
|
if (!response.ok) return { ok: false, status: response.status, data };
|
|
@@ -329,17 +430,30 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
329
430
|
for (let round = 0; round <= maxRounds + 1; round += 1) {
|
|
330
431
|
const upstreamRequest = toProviderChatCompletionsRequest(currentChatRequest, config);
|
|
331
432
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
433
|
+
logDebugPayload(config, upstreamRequest, {
|
|
434
|
+
stage: `web_search_round_${round}`,
|
|
435
|
+
rawRequest,
|
|
436
|
+
normalized,
|
|
437
|
+
chatRequest: currentChatRequest,
|
|
438
|
+
});
|
|
332
439
|
const { response, data } = await callUpstreamJson({
|
|
333
440
|
upstreamRequest: disableStreaming(upstreamRequest),
|
|
334
441
|
config,
|
|
442
|
+
signal: clientSignal,
|
|
335
443
|
});
|
|
336
444
|
finalResponse = response;
|
|
337
445
|
if (!response.ok) return { ok: false, status: response.status, data };
|
|
338
446
|
completions.push(data);
|
|
339
447
|
finalCompletion = data;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
448
|
+
const roundChatMessage = Array.isArray(data?.choices) ? data.choices[0]?.message : null;
|
|
449
|
+
const commentaryCalls = bridgedCommentaryToolCallsFromMessage(roundChatMessage, currentChatRequest.tools);
|
|
450
|
+
const routingData = stripBridgedCommentaryFromCompletion(data, currentChatRequest.tools);
|
|
451
|
+
|
|
452
|
+
if (state.enabled && hasKnownExternalToolCalls(routingData, currentChatRequest.tools)) {
|
|
453
|
+
finalCompletion = restoreCommentaryToolCalls(
|
|
454
|
+
knownExternalToolCallsCompletion(routingData, currentChatRequest.tools),
|
|
455
|
+
commentaryCalls,
|
|
456
|
+
);
|
|
343
457
|
completions[completions.length - 1] = finalCompletion;
|
|
344
458
|
break;
|
|
345
459
|
}
|
|
@@ -348,12 +462,13 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
348
462
|
break;
|
|
349
463
|
}
|
|
350
464
|
|
|
351
|
-
const wantsInternalWeb = shouldContinueWebSearchLoop(
|
|
352
|
-
const
|
|
465
|
+
const wantsInternalWeb = shouldContinueWebSearchLoop(routingData);
|
|
466
|
+
const wantsInternalRound = wantsInternalWeb || commentaryCalls.length > 0;
|
|
467
|
+
const hasToolCalls = hasAnyToolCalls(routingData);
|
|
353
468
|
|
|
354
|
-
if (!
|
|
355
|
-
if (hasToolCalls && hasUnknownExternalToolCalls(
|
|
356
|
-
const unsupportedMessages = unhandledToolMessagesFromCompletion(
|
|
469
|
+
if (!wantsInternalRound || round >= maxRounds) {
|
|
470
|
+
if (hasToolCalls && hasUnknownExternalToolCalls(routingData, currentChatRequest.tools)) {
|
|
471
|
+
const unsupportedMessages = unhandledToolMessagesFromCompletion(routingData, undefined, {
|
|
357
472
|
onlyUnknownExternal: true,
|
|
358
473
|
tools: currentChatRequest.tools,
|
|
359
474
|
});
|
|
@@ -365,7 +480,7 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
365
480
|
break;
|
|
366
481
|
}
|
|
367
482
|
|
|
368
|
-
if (
|
|
483
|
+
if (wantsInternalRound || (!hasToolCalls && hasWebSearchContext(searches, openedPages) && !hasVisibleAssistantContent(routingData))) {
|
|
369
484
|
const finalAnswerError = await requestFinalAnswer(currentChatRequest);
|
|
370
485
|
if (finalAnswerError) return finalAnswerError;
|
|
371
486
|
break;
|
|
@@ -380,7 +495,7 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
380
495
|
toolResult = await executeWebSearchCalls({
|
|
381
496
|
completion: data,
|
|
382
497
|
config: searchConfig,
|
|
383
|
-
signal: controller.signal,
|
|
498
|
+
signal: mergeAbortSignals([controller.signal, clientSignal]),
|
|
384
499
|
onSearchStart,
|
|
385
500
|
onSearchDone,
|
|
386
501
|
});
|
|
@@ -391,7 +506,7 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
391
506
|
openedPages.push(...(toolResult.openedPages || []));
|
|
392
507
|
currentChatRequest = {
|
|
393
508
|
...currentChatRequest,
|
|
394
|
-
messages: currentChatRequest.messages.concat(toolResult.messages),
|
|
509
|
+
messages: currentChatRequest.messages.concat(toolResult.messages, commentaryToolMessages(commentaryCalls)),
|
|
395
510
|
tool_choice:
|
|
396
511
|
currentChatRequest.tool_choice?.function?.name === INTERNAL_WEB_SEARCH_TOOL
|
|
397
512
|
? 'auto'
|
|
@@ -410,170 +525,16 @@ async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchS
|
|
|
410
525
|
};
|
|
411
526
|
}
|
|
412
527
|
|
|
413
|
-
const
|
|
528
|
+
const STREAM_HEARTBEAT_MS = 10000;
|
|
414
529
|
|
|
415
|
-
function
|
|
416
|
-
const value =
|
|
417
|
-
if (
|
|
418
|
-
|
|
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
|
-
};
|
|
530
|
+
function streamHeartbeatMs(config) {
|
|
531
|
+
const value = Number(config?.streamHeartbeatMs);
|
|
532
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
533
|
+
return STREAM_HEARTBEAT_MS;
|
|
574
534
|
}
|
|
575
535
|
|
|
576
536
|
function writeSseEvent(res, event) {
|
|
537
|
+
if (res.writableEnded || res.destroyed) return;
|
|
577
538
|
res.write(serializeResponsesSseEvent(event));
|
|
578
539
|
}
|
|
579
540
|
|
|
@@ -609,7 +570,246 @@ function webSearchSseWriter({ res, mapper, outputIndexBySearch, includeSources =
|
|
|
609
570
|
};
|
|
610
571
|
}
|
|
611
572
|
|
|
612
|
-
|
|
573
|
+
async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest, config, res, responseId, clientSignal }) {
|
|
574
|
+
const effectiveChatRequest = { ...chatRequest, normalized };
|
|
575
|
+
const state = prepareWebSearchRequest({ normalized, chatRequest: effectiveChatRequest, config });
|
|
576
|
+
if (!state.enabled) return { handled: false };
|
|
577
|
+
|
|
578
|
+
const searchConfig = state.config || config;
|
|
579
|
+
const maxRounds = maxWebSearchRounds(searchConfig);
|
|
580
|
+
let currentChatRequest = state.chatRequest;
|
|
581
|
+
|
|
582
|
+
const buildUpstreamRequest = (stage) => {
|
|
583
|
+
const upstreamRequest = toProviderChatCompletionsRequest(currentChatRequest, config);
|
|
584
|
+
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
585
|
+
logDebugPayload(config, upstreamRequest, { stage, rawRequest, normalized, chatRequest: currentChatRequest });
|
|
586
|
+
return upstreamRequest;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const firstUpstreamRequest = buildUpstreamRequest('web_search_stream_round_0');
|
|
590
|
+
let upstreamResponse = await callChatCompletions({
|
|
591
|
+
baseUrl: config.upstreamBaseUrl,
|
|
592
|
+
apiKey: config.upstreamApiKey,
|
|
593
|
+
request: firstUpstreamRequest,
|
|
594
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
595
|
+
signal: clientSignal,
|
|
596
|
+
});
|
|
597
|
+
if (!upstreamResponse.ok) {
|
|
598
|
+
sendJson(res, upstreamResponse.status, await readJsonResponse(upstreamResponse));
|
|
599
|
+
return { handled: true };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const mapper = new ResponsesStreamMapper({
|
|
603
|
+
responseId,
|
|
604
|
+
model: firstUpstreamRequest.model,
|
|
605
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
606
|
+
normalized,
|
|
607
|
+
config,
|
|
608
|
+
...resolveReasoningStreamMode(config, firstUpstreamRequest),
|
|
609
|
+
holdToolItemEvents: true,
|
|
610
|
+
knownToolNames: chatToolNamesFromTools(firstUpstreamRequest.tools),
|
|
611
|
+
});
|
|
612
|
+
const searchWriter = webSearchSseWriter({
|
|
613
|
+
res,
|
|
614
|
+
mapper,
|
|
615
|
+
outputIndexBySearch: new Map(),
|
|
616
|
+
includeSources: shouldIncludeSearchSources(normalized),
|
|
617
|
+
});
|
|
618
|
+
const streamSearchItems = new Set();
|
|
619
|
+
const roundUsages = [];
|
|
620
|
+
const searches = [];
|
|
621
|
+
const openedPages = [];
|
|
622
|
+
let finalAnswerForced = false;
|
|
623
|
+
let roundEnd = null;
|
|
624
|
+
|
|
625
|
+
res.writeHead(200, {
|
|
626
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
627
|
+
'cache-control': 'no-cache, no-transform',
|
|
628
|
+
connection: 'keep-alive',
|
|
629
|
+
'x-accel-buffering': 'no',
|
|
630
|
+
});
|
|
631
|
+
writeSseEvent(res, mapper.createdEvent());
|
|
632
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
633
|
+
const heartbeat = setInterval(() => {
|
|
634
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
635
|
+
}, STREAM_HEARTBEAT_MS);
|
|
636
|
+
|
|
637
|
+
const endStream = () => {
|
|
638
|
+
writeSseDone(res);
|
|
639
|
+
res.end();
|
|
640
|
+
};
|
|
641
|
+
const failStream = (message) => {
|
|
642
|
+
for (const event of mapper.streamFailed(message)) writeSseEvent(res, event);
|
|
643
|
+
endStream();
|
|
644
|
+
return { handled: true };
|
|
645
|
+
};
|
|
646
|
+
const finishTurn = () => {
|
|
647
|
+
if (mapper.messageItem?.content?.length) {
|
|
648
|
+
annotateMessagePartWithWebCitations(mapper.messageItem.content[0], searches, openedPages);
|
|
649
|
+
}
|
|
650
|
+
const combinedUsage = combineUsage(roundUsages.concat([{ usage: mapper.pendingUsage }]));
|
|
651
|
+
for (const event of mapper.finalize(mapper.pendingFinishReason || 'stop', combinedUsage)) {
|
|
652
|
+
writeSseEvent(res, event);
|
|
653
|
+
}
|
|
654
|
+
endStream();
|
|
655
|
+
return {
|
|
656
|
+
handled: true,
|
|
657
|
+
chatRequest: currentChatRequest,
|
|
658
|
+
assistantMessage: mapper.terminalStatus === 'completed' ? mapper.roundAssistantMessage() : null,
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
const relayRound = async () => {
|
|
662
|
+
roundEnd = null;
|
|
663
|
+
mapper.markRoundStart();
|
|
664
|
+
await relayChatCompletionsResponse({
|
|
665
|
+
upstreamResponse,
|
|
666
|
+
res,
|
|
667
|
+
passThrough: false,
|
|
668
|
+
writeHeaders: false,
|
|
669
|
+
endResponse: false,
|
|
670
|
+
onStreamChunk(event) {
|
|
671
|
+
if (event?.done) {
|
|
672
|
+
roundEnd = { eof: Boolean(event.eof) };
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
for (const responseEvent of mapper.mapChatEvent(event)) {
|
|
676
|
+
writeSseEvent(res, responseEvent);
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
await relayRound();
|
|
684
|
+
for (let round = 0; ; round += 1) {
|
|
685
|
+
if (clientSignal?.aborted) {
|
|
686
|
+
return { handled: true };
|
|
687
|
+
}
|
|
688
|
+
if (!mapper.pendingFinishReason && roundEnd?.eof) {
|
|
689
|
+
return failStream('upstream stream ended before completion');
|
|
690
|
+
}
|
|
691
|
+
mapper.expandParallelToolItems();
|
|
692
|
+
const roundMessage = mapper.roundAssistantMessage();
|
|
693
|
+
const commentaryCalls = bridgedCommentaryToolCallsFromMessage(roundMessage, currentChatRequest.tools);
|
|
694
|
+
for (const event of mapper.convertBridgedCommentaryItems()) writeSseEvent(res, event);
|
|
695
|
+
const completionLike = {
|
|
696
|
+
choices: [{ index: 0, message: roundMessage, finish_reason: mapper.pendingFinishReason || 'stop' }],
|
|
697
|
+
};
|
|
698
|
+
const routingCompletion = stripBridgedCommentaryFromCompletion(completionLike, currentChatRequest.tools);
|
|
699
|
+
|
|
700
|
+
if (finalAnswerForced) {
|
|
701
|
+
mapper.removeToolItems();
|
|
702
|
+
const reason = finalAnswerIncompleteReason(routingCompletion);
|
|
703
|
+
if (reason) {
|
|
704
|
+
const events = mapper.replaceBufferedAssistantText(gatewayIncompleteMessageContent(reason));
|
|
705
|
+
for (const event of events || []) writeSseEvent(res, event);
|
|
706
|
+
}
|
|
707
|
+
return finishTurn();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (hasKnownExternalToolCalls(routingCompletion, currentChatRequest.tools)) {
|
|
711
|
+
const kept = knownExternalToolCallsCompletion(routingCompletion, currentChatRequest.tools);
|
|
712
|
+
const keptIds = new Set(
|
|
713
|
+
(kept.choices?.[0]?.message?.tool_calls || []).map((toolCall) => toolCall.id).filter(Boolean),
|
|
714
|
+
);
|
|
715
|
+
mapper.removeToolItems((item) => !keptIds.has(item.call_id));
|
|
716
|
+
return finishTurn();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const wantsInternalWeb = shouldContinueWebSearchLoop(routingCompletion);
|
|
720
|
+
const wantsInternalRound = wantsInternalWeb || commentaryCalls.length > 0;
|
|
721
|
+
const hasToolCallsRound = hasAnyToolCalls(routingCompletion);
|
|
722
|
+
|
|
723
|
+
if (!wantsInternalRound || round >= maxRounds) {
|
|
724
|
+
if (hasToolCallsRound && hasUnknownExternalToolCalls(routingCompletion, currentChatRequest.tools)) {
|
|
725
|
+
const unsupportedMessages = unhandledToolMessagesFromCompletion(routingCompletion, undefined, {
|
|
726
|
+
onlyUnknownExternal: true,
|
|
727
|
+
tools: currentChatRequest.tools,
|
|
728
|
+
});
|
|
729
|
+
currentChatRequest = toolCallsToFinalAnswerRequest({
|
|
730
|
+
...currentChatRequest,
|
|
731
|
+
messages: currentChatRequest.messages.concat(unsupportedMessages),
|
|
732
|
+
});
|
|
733
|
+
} else if (wantsInternalRound || (!hasToolCallsRound && hasWebSearchContext(searches, openedPages) && !hasVisibleAssistantContent(routingCompletion))) {
|
|
734
|
+
currentChatRequest = toolCallsToFinalAnswerRequest(currentChatRequest);
|
|
735
|
+
} else {
|
|
736
|
+
return finishTurn();
|
|
737
|
+
}
|
|
738
|
+
finalAnswerForced = true;
|
|
739
|
+
roundUsages.push({ usage: mapper.pendingUsage });
|
|
740
|
+
mapper.removeToolItems();
|
|
741
|
+
for (const event of mapper.beginNextRound()) writeSseEvent(res, event);
|
|
742
|
+
mapper.holdVisibleTextUntilDone();
|
|
743
|
+
} else {
|
|
744
|
+
roundUsages.push({ usage: mapper.pendingUsage });
|
|
745
|
+
for (const event of mapper.beginNextRound()) writeSseEvent(res, event);
|
|
746
|
+
const controller = new AbortController();
|
|
747
|
+
const timeout = setTimeout(() => controller.abort(), webToolTimeoutMs(searchConfig));
|
|
748
|
+
let toolResult;
|
|
749
|
+
try {
|
|
750
|
+
toolResult = await executeWebSearchCalls({
|
|
751
|
+
completion: completionLike,
|
|
752
|
+
config: searchConfig,
|
|
753
|
+
signal: mergeAbortSignals([controller.signal, clientSignal]),
|
|
754
|
+
onSearchStart(search) {
|
|
755
|
+
if (search.action && search.auto) return;
|
|
756
|
+
streamSearchItems.add(search);
|
|
757
|
+
searchWriter.start(search);
|
|
758
|
+
},
|
|
759
|
+
onSearchDone(search) {
|
|
760
|
+
if (!streamSearchItems.has(search)) return;
|
|
761
|
+
searchWriter.done(search);
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
} finally {
|
|
765
|
+
clearTimeout(timeout);
|
|
766
|
+
}
|
|
767
|
+
searches.push(...toolResult.searches);
|
|
768
|
+
openedPages.push(...(toolResult.openedPages || []));
|
|
769
|
+
currentChatRequest = {
|
|
770
|
+
...currentChatRequest,
|
|
771
|
+
messages: currentChatRequest.messages.concat(toolResult.messages, commentaryToolMessages(commentaryCalls)),
|
|
772
|
+
tool_choice:
|
|
773
|
+
currentChatRequest.tool_choice?.function?.name === INTERNAL_WEB_SEARCH_TOOL
|
|
774
|
+
? 'auto'
|
|
775
|
+
: currentChatRequest.tool_choice,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (clientSignal?.aborted) {
|
|
780
|
+
return { handled: true };
|
|
781
|
+
}
|
|
782
|
+
const upstreamRequest = buildUpstreamRequest(`web_search_stream_round_${round + 1}`);
|
|
783
|
+
upstreamResponse = await callChatCompletions({
|
|
784
|
+
baseUrl: config.upstreamBaseUrl,
|
|
785
|
+
apiKey: config.upstreamApiKey,
|
|
786
|
+
request: upstreamRequest,
|
|
787
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
788
|
+
signal: clientSignal,
|
|
789
|
+
});
|
|
790
|
+
if (!upstreamResponse.ok) {
|
|
791
|
+
const data = await readJsonResponse(upstreamResponse);
|
|
792
|
+
return failStream(data?.error?.message || `upstream error ${upstreamResponse.status}`);
|
|
793
|
+
}
|
|
794
|
+
await relayRound();
|
|
795
|
+
}
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (clientSignal?.aborted) {
|
|
798
|
+
return { handled: true };
|
|
799
|
+
}
|
|
800
|
+
throw error;
|
|
801
|
+
} finally {
|
|
802
|
+
clearInterval(heartbeat);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export function createProxyServer({ config = loadConfig(), reasoningCache } = {}) {
|
|
807
|
+
reasoningCache ||= new ReasoningCache({
|
|
808
|
+
persistPath: config.reasoningCacheEnabled === false ? '' : config.reasoningCachePath,
|
|
809
|
+
legacyPath: config.legacyReasoningCachePath,
|
|
810
|
+
maxMessages: config.reasoningCacheMaxMessages,
|
|
811
|
+
maxBytes: config.reasoningCacheMaxBytes,
|
|
812
|
+
});
|
|
613
813
|
let modelCache = null;
|
|
614
814
|
|
|
615
815
|
async function getModelList() {
|
|
@@ -646,19 +846,22 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
646
846
|
}
|
|
647
847
|
|
|
648
848
|
const request = parsed.value;
|
|
649
|
-
const normalized = normalizeResponsesRequest(request
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (priorSession?.history?.length) {
|
|
655
|
-
const priorMessages = historyMessagesFromSession(priorSession);
|
|
656
|
-
normalized.messages = priorMessages.concat(normalized.messages);
|
|
657
|
-
}
|
|
658
|
-
normalized.messages = prependMissingAssistantToolMessages(normalized.messages, sessions);
|
|
849
|
+
const normalized = normalizeResponsesRequest(request, {
|
|
850
|
+
restoreDiscoveredTools: codexRequestKind(req) !== 'compaction',
|
|
851
|
+
});
|
|
852
|
+
normalized.messages = prependMissingAssistantToolMessages(normalized.messages, reasoningCache);
|
|
853
|
+
normalized.messages = restoreAssistantReasoningContent(normalized.messages, reasoningCache);
|
|
659
854
|
|
|
660
855
|
const chatRequest = toChatCompletionsRequest(normalized);
|
|
661
856
|
const useWebSearchEmulator = hasTavilyWebSearch(config, normalized);
|
|
857
|
+
if (!useWebSearchEmulator) {
|
|
858
|
+
const existingToolNames = new Set(chatToolNamesFromTools(chatRequest.tools));
|
|
859
|
+
const webShims = unavailableWebSearchToolShims(normalized.tools)
|
|
860
|
+
.filter((tool) => !existingToolNames.has(tool.function.name));
|
|
861
|
+
if (webShims.length) chatRequest.tools = [...(chatRequest.tools ?? []), ...webShims];
|
|
862
|
+
}
|
|
863
|
+
const clientAbort = clientAbortControllerFor(res);
|
|
864
|
+
const clientSignal = clientAbort.signal;
|
|
662
865
|
let upstreamRequest = toProviderChatCompletionsRequest(chatRequest, config);
|
|
663
866
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || normalized.model;
|
|
664
867
|
|
|
@@ -672,119 +875,64 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
672
875
|
}
|
|
673
876
|
|
|
674
877
|
const responseId = generateId('resp');
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
if (useWebSearchEmulator) {
|
|
678
|
-
const streamWebSearch = Boolean(request.stream);
|
|
679
|
-
let streamMapper = null;
|
|
680
|
-
let streamSearchWriter = null;
|
|
681
|
-
const streamSearchItems = new Set();
|
|
682
|
-
if (streamWebSearch) {
|
|
683
|
-
const createdAt = Math.floor(Date.now() / 1000);
|
|
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());
|
|
707
|
-
}
|
|
878
|
+
const persistReasoning = (assistantMessage) => reasoningCache.rememberAssistantMessage(assistantMessage);
|
|
708
879
|
|
|
880
|
+
if (useWebSearchEmulator && request.stream) {
|
|
881
|
+
const result = await runStreamingWebSearchTurn({
|
|
882
|
+
rawRequest: request,
|
|
883
|
+
normalized,
|
|
884
|
+
chatRequest,
|
|
885
|
+
config,
|
|
886
|
+
res,
|
|
887
|
+
responseId,
|
|
888
|
+
clientSignal,
|
|
889
|
+
});
|
|
890
|
+
if (result.handled) {
|
|
891
|
+
if (result.assistantMessage) persistReasoning(result.assistantMessage);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
} else if (useWebSearchEmulator) {
|
|
709
895
|
const loop = await runWebSearchChatLoop({
|
|
896
|
+
rawRequest: request,
|
|
710
897
|
normalized,
|
|
711
898
|
chatRequest,
|
|
712
899
|
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,
|
|
900
|
+
clientSignal,
|
|
726
901
|
});
|
|
727
902
|
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
903
|
sendJson(res, loop.status || 500, loop.data);
|
|
739
904
|
return;
|
|
740
905
|
}
|
|
741
906
|
upstreamRequest = toProviderChatCompletionsRequest(loop.chatRequest, config);
|
|
742
907
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || normalized.model;
|
|
743
|
-
|
|
908
|
+
logDebugPayload(config, upstreamRequest, {
|
|
909
|
+
stage: 'web_search_compatibility_payload',
|
|
910
|
+
rawRequest: request,
|
|
911
|
+
normalized,
|
|
912
|
+
chatRequest: loop.chatRequest,
|
|
913
|
+
});
|
|
744
914
|
const payload = applyWebSearchOutputCompatibility(convertChatCompletionToResponses({
|
|
745
915
|
completion: loop.completion,
|
|
746
|
-
model,
|
|
747
|
-
previousResponseId,
|
|
916
|
+
model: upstreamRequest.model,
|
|
748
917
|
normalized,
|
|
749
918
|
responseId,
|
|
750
919
|
config,
|
|
751
920
|
}), loop.searches, normalized, loop.openedPages);
|
|
752
|
-
if (
|
|
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;
|
|
921
|
+
if (payload.status === 'completed') {
|
|
922
|
+
persistReasoning(assistantMessageFromResponseOutput(payload.output));
|
|
770
923
|
}
|
|
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();
|
|
924
|
+
sendJson(res, 200, payload);
|
|
778
925
|
return;
|
|
779
926
|
}
|
|
780
927
|
|
|
781
|
-
logDebugPayload(config, upstreamRequest);
|
|
928
|
+
logDebugPayload(config, upstreamRequest, { rawRequest: request, normalized, chatRequest });
|
|
782
929
|
|
|
783
930
|
const upstreamResponse = await callChatCompletions({
|
|
784
931
|
baseUrl: config.upstreamBaseUrl,
|
|
785
932
|
apiKey: config.upstreamApiKey,
|
|
786
933
|
request: upstreamRequest,
|
|
787
934
|
timeoutMs: config.upstreamTimeoutMs,
|
|
935
|
+
signal: clientSignal,
|
|
788
936
|
});
|
|
789
937
|
|
|
790
938
|
const model = upstreamRequest.model;
|
|
@@ -800,21 +948,13 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
800
948
|
const payload = convertChatCompletionToResponses({
|
|
801
949
|
completion: data,
|
|
802
950
|
model,
|
|
803
|
-
previousResponseId,
|
|
804
951
|
normalized,
|
|
805
952
|
responseId,
|
|
806
953
|
config,
|
|
807
954
|
});
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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);
|
|
955
|
+
if (payload.status === 'completed') {
|
|
956
|
+
persistReasoning(assistantMessageFromResponseOutput(payload.output));
|
|
957
|
+
}
|
|
818
958
|
sendJson(res, 200, payload);
|
|
819
959
|
return;
|
|
820
960
|
}
|
|
@@ -830,51 +970,55 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
830
970
|
responseId,
|
|
831
971
|
model,
|
|
832
972
|
createdAt: Math.floor(Date.now() / 1000),
|
|
833
|
-
previousResponseId,
|
|
834
973
|
normalized,
|
|
835
974
|
config,
|
|
836
|
-
...(
|
|
837
|
-
|
|
838
|
-
return {
|
|
839
|
-
bufferOutputUntilDone: upstreamRequest?.thinking?.type === 'enabled',
|
|
840
|
-
...reasoningMode,
|
|
841
|
-
};
|
|
842
|
-
})(),
|
|
975
|
+
...resolveReasoningStreamMode(config, upstreamRequest),
|
|
976
|
+
knownToolNames: chatToolNamesFromTools(upstreamRequest.tools),
|
|
843
977
|
});
|
|
844
978
|
let doneSent = false;
|
|
845
979
|
const writeResponsesDone = () => {
|
|
846
980
|
if (doneSent) return;
|
|
847
981
|
doneSent = true;
|
|
848
|
-
res
|
|
982
|
+
writeSseEvent(res, { done: true });
|
|
849
983
|
};
|
|
850
984
|
|
|
851
|
-
res
|
|
852
|
-
res
|
|
985
|
+
writeSseEvent(res, mapper.createdEvent());
|
|
986
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
987
|
+
let lastEventWriteAt = Date.now();
|
|
988
|
+
const heartbeatMs = streamHeartbeatMs(config);
|
|
989
|
+
const heartbeat = setInterval(() => {
|
|
990
|
+
if (doneSent || Date.now() - lastEventWriteAt < heartbeatMs) return;
|
|
991
|
+
writeSseEvent(res, mapper.inProgressEvent());
|
|
992
|
+
lastEventWriteAt = Date.now();
|
|
993
|
+
}, heartbeatMs);
|
|
853
994
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
995
|
+
try {
|
|
996
|
+
await relayChatCompletionsResponse({
|
|
997
|
+
upstreamResponse,
|
|
998
|
+
res,
|
|
999
|
+
passThrough: false,
|
|
1000
|
+
writeHeaders: false,
|
|
1001
|
+
onStreamChunk(event) {
|
|
1002
|
+
const events = mapper.mapChatEvent(event);
|
|
1003
|
+
for (const responseEvent of events) {
|
|
1004
|
+
writeSseEvent(res, responseEvent);
|
|
1005
|
+
}
|
|
1006
|
+
if (events.length) {
|
|
1007
|
+
lastEventWriteAt = Date.now();
|
|
1008
|
+
}
|
|
1009
|
+
if (event?.done) writeResponsesDone();
|
|
1010
|
+
},
|
|
1011
|
+
});
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (clientSignal.aborted) return;
|
|
1014
|
+
throw error;
|
|
1015
|
+
} finally {
|
|
1016
|
+
clearInterval(heartbeat);
|
|
1017
|
+
}
|
|
867
1018
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
upstreamRequest,
|
|
872
|
-
inputMessages: currentInputMessages,
|
|
873
|
-
assistantMessage: mapper.assistantMessage(),
|
|
874
|
-
createdAt: Date.now(),
|
|
875
|
-
});
|
|
876
|
-
sessions.set(responseId, nextSession);
|
|
877
|
-
sessions.setConversation(conversationId, responseId);
|
|
1019
|
+
if (mapper.terminalStatus === 'completed') {
|
|
1020
|
+
persistReasoning(mapper.assistantMessage());
|
|
1021
|
+
}
|
|
878
1022
|
}
|
|
879
1023
|
|
|
880
1024
|
return http.createServer(async (req, res) => {
|
|
@@ -917,6 +1061,7 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
917
1061
|
apiKey: config.upstreamApiKey,
|
|
918
1062
|
request: upstreamRequest,
|
|
919
1063
|
timeoutMs: config.upstreamTimeoutMs,
|
|
1064
|
+
signal: clientAbortControllerFor(res).signal,
|
|
920
1065
|
});
|
|
921
1066
|
await relayChatCompletionsResponse({ upstreamResponse, res, passThrough: true });
|
|
922
1067
|
return;
|
|
@@ -924,6 +1069,7 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
924
1069
|
|
|
925
1070
|
sendJson(res, 404, { error: { message: 'Not found' } });
|
|
926
1071
|
} catch (error) {
|
|
1072
|
+
if (res.destroyed) return;
|
|
927
1073
|
if (res.headersSent) {
|
|
928
1074
|
if (!res.writableEnded) {
|
|
929
1075
|
res.write(serializeResponsesSseEvent({
|
|
@@ -938,7 +1084,8 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
938
1084
|
}
|
|
939
1085
|
return;
|
|
940
1086
|
}
|
|
941
|
-
|
|
1087
|
+
const statusCode = Number.isInteger(error.statusCode) ? error.statusCode : 500;
|
|
1088
|
+
sendJson(res, statusCode, { error: { code: error.code, message: error.message || 'Internal server error' } });
|
|
942
1089
|
}
|
|
943
1090
|
});
|
|
944
1091
|
}
|