@galaxy-yearn/codex-deepseek-gateway 0.2.0 → 0.2.2
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 +19 -10
- package/README.zh-CN.md +220 -0
- package/config/codex-model-catalog.json +18 -8
- package/config/codex-model-catalog.zh.json +22 -12
- package/package.json +2 -2
- package/src/codex-launch.js +30 -7
- package/src/codex-sessions.js +69 -13
- package/src/config.js +6 -5
- package/src/firecrawl.js +8 -3
- package/src/protocol.js +127 -155
- package/src/reasoning-cache.js +235 -0
- package/src/server.js +184 -129
- package/src/web-search-emulator.js +180 -106
- package/state/reasoning-cache.example.jsonl +1 -0
- package/src/session-store.js +0 -282
- package/state/sessions.example.json +0 -72
package/src/server.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
toProviderChatCompletionsRequest,
|
|
22
22
|
unavailableWebSearchToolShims,
|
|
23
23
|
} from './protocol.js';
|
|
24
|
-
import {
|
|
24
|
+
import { ReasoningCache } from './reasoning-cache.js';
|
|
25
25
|
import { callChatCompletions, callModels, readJsonResponse, relayChatCompletionsResponse } from './upstream.js';
|
|
26
26
|
import {
|
|
27
27
|
annotateMessagePartWithWebCitations,
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
hasAnyToolCalls,
|
|
33
33
|
hasKnownExternalToolCalls,
|
|
34
34
|
hasUnknownExternalToolCalls,
|
|
35
|
-
INTERNAL_WEB_SEARCH_TOOL,
|
|
36
35
|
maxWebSearchRounds,
|
|
37
36
|
prepareWebSearchRequest,
|
|
38
37
|
removeWebSearchInstructions,
|
|
@@ -62,6 +61,14 @@ function getRequestPath(req) {
|
|
|
62
61
|
return url.pathname;
|
|
63
62
|
}
|
|
64
63
|
|
|
64
|
+
function codexRequestKind(req) {
|
|
65
|
+
const value = req.headers['x-codex-turn-metadata'];
|
|
66
|
+
if (typeof value !== 'string') return '';
|
|
67
|
+
const parsed = safeJsonParse(value);
|
|
68
|
+
if (!parsed.ok || !parsed.value || typeof parsed.value !== 'object') return '';
|
|
69
|
+
return String(parsed.value.request_kind || '');
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
function isAuthorized(req, config) {
|
|
66
73
|
if (!config.proxyApiKey) return true;
|
|
67
74
|
const authorization = req.headers.authorization || '';
|
|
@@ -69,29 +76,7 @@ function isAuthorized(req, config) {
|
|
|
69
76
|
return bearerToken === config.proxyApiKey || req.headers['x-api-key'] === config.proxyApiKey;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
function
|
|
73
|
-
const conversation = normalized.conversation ?? request.conversation;
|
|
74
|
-
if (typeof conversation === 'string') return conversation;
|
|
75
|
-
if (conversation && typeof conversation === 'object') return conversation.id || conversation.conversation_id || null;
|
|
76
|
-
return request.conversation_id || null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function historyMessagesFromSession(session) {
|
|
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
|
-
return messages;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function shouldPersistSession(normalized, request, conversationId) {
|
|
90
|
-
if (conversationId) return true;
|
|
91
|
-
return (normalized.store ?? request.store) !== false;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function prependMissingAssistantToolMessages(messages, sessions) {
|
|
79
|
+
function prependMissingAssistantToolMessages(messages, reasoningCache) {
|
|
95
80
|
const existingToolCallIds = new Set();
|
|
96
81
|
const missingToolOutputIds = [];
|
|
97
82
|
for (const message of messages) {
|
|
@@ -111,7 +96,7 @@ function prependMissingAssistantToolMessages(messages, sessions) {
|
|
|
111
96
|
const inserted = new Set(existingToolCallIds);
|
|
112
97
|
for (const callId of missingToolOutputIds) {
|
|
113
98
|
if (inserted.has(callId)) continue;
|
|
114
|
-
const assistantMessage =
|
|
99
|
+
const assistantMessage = reasoningCache.getAssistantMessageForToolCall(callId);
|
|
115
100
|
if (!assistantMessage) continue;
|
|
116
101
|
prefix.push(assistantMessage);
|
|
117
102
|
for (const id of extractToolCallIdsFromMessages([assistantMessage])) inserted.add(id);
|
|
@@ -119,14 +104,14 @@ function prependMissingAssistantToolMessages(messages, sessions) {
|
|
|
119
104
|
return prefix.length ? prefix.concat(messages) : messages;
|
|
120
105
|
}
|
|
121
106
|
|
|
122
|
-
function restoreAssistantReasoningContent(messages,
|
|
107
|
+
function restoreAssistantReasoningContent(messages, reasoningCache) {
|
|
123
108
|
return messages.map((message) => {
|
|
124
109
|
if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls) || !message.tool_calls.length) {
|
|
125
110
|
return message;
|
|
126
111
|
}
|
|
127
112
|
if (typeof message.reasoning_content === 'string' && message.reasoning_content) return message;
|
|
128
113
|
for (const toolCall of message.tool_calls) {
|
|
129
|
-
const stored =
|
|
114
|
+
const stored = reasoningCache.getAssistantMessageForToolCall(toolCall?.id);
|
|
130
115
|
if (typeof stored?.reasoning_content === 'string' && stored.reasoning_content) {
|
|
131
116
|
return { ...message, reasoning_content: stored.reasoning_content };
|
|
132
117
|
}
|
|
@@ -242,16 +227,15 @@ function webToolTimeoutMs(config = {}) {
|
|
|
242
227
|
function combineUsage(completions) {
|
|
243
228
|
const usages = completions.map((completion) => completion?.usage).filter(Boolean);
|
|
244
229
|
if (!usages.length) return null;
|
|
245
|
-
const last = usages[usages.length - 1];
|
|
246
230
|
const sum = (pick) => usages.reduce((total, usage) => total + (Number(pick(usage)) || 0), 0);
|
|
247
|
-
const promptTokens =
|
|
231
|
+
const promptTokens = sum((usage) => usage.prompt_tokens);
|
|
248
232
|
const completionTokens = sum((usage) => usage.completion_tokens);
|
|
249
233
|
return {
|
|
250
234
|
prompt_tokens: promptTokens,
|
|
251
235
|
completion_tokens: completionTokens,
|
|
252
236
|
total_tokens: promptTokens + completionTokens,
|
|
253
237
|
prompt_tokens_details: {
|
|
254
|
-
cached_tokens:
|
|
238
|
+
cached_tokens: sum((usage) => usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens),
|
|
255
239
|
},
|
|
256
240
|
completion_tokens_details: {
|
|
257
241
|
reasoning_tokens: sum((usage) => usage.completion_tokens_details?.reasoning_tokens ?? usage.reasoning_tokens),
|
|
@@ -259,27 +243,53 @@ function combineUsage(completions) {
|
|
|
259
243
|
};
|
|
260
244
|
}
|
|
261
245
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
246
|
+
function lastUsage(usages) {
|
|
247
|
+
for (let index = usages.length - 1; index >= 0; index -= 1) {
|
|
248
|
+
if (usages[index]) return usages[index];
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function withUsageFallback(completion, usage) {
|
|
254
|
+
if (!completion || completion.usage || !usage) return completion;
|
|
255
|
+
return { ...completion, usage };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function toWebSearchUpstreamRequest(chatRequest, config, webTools) {
|
|
259
|
+
const request = toProviderChatCompletionsRequest(chatRequest, config);
|
|
260
|
+
if (
|
|
261
|
+
request.thinking?.type === 'enabled' &&
|
|
262
|
+
request.tool_choice?.type === 'function' &&
|
|
263
|
+
request.tool_choice.function?.name === webTools?.search
|
|
264
|
+
) {
|
|
265
|
+
return { ...request, thinking: { type: 'disabled' }, reasoning_effort: undefined };
|
|
266
|
+
}
|
|
267
|
+
return request;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function logWebSearchUsage(config, stage, usages, finalUsage) {
|
|
271
|
+
if (!config.debugPayload) return;
|
|
272
|
+
const aggregateUsage = combineUsage(usages.map((usage) => ({ usage })));
|
|
273
|
+
if (!aggregateUsage && !finalUsage) return;
|
|
274
|
+
writeDebugPayloadLine(config, `[codex-deepseek-gateway] web search usage ${JSON.stringify({
|
|
275
|
+
stage,
|
|
276
|
+
rounds: usages.length,
|
|
277
|
+
aggregate: aggregateUsage,
|
|
278
|
+
final: finalUsage || null,
|
|
279
|
+
})}\n`);
|
|
268
280
|
}
|
|
269
281
|
|
|
270
|
-
function toolCallsToFinalAnswerRequest(request) {
|
|
282
|
+
function toolCallsToFinalAnswerRequest(request, webTools) {
|
|
271
283
|
return {
|
|
272
284
|
...request,
|
|
273
285
|
tool_choice: undefined,
|
|
274
286
|
tools: undefined,
|
|
275
|
-
messages: removeWebSearchInstructions(request.messages).concat({
|
|
287
|
+
messages: removeWebSearchInstructions(request.messages, webTools).concat({
|
|
276
288
|
role: 'user',
|
|
277
289
|
content: [
|
|
278
|
-
'Web tools are
|
|
279
|
-
'
|
|
280
|
-
'
|
|
281
|
-
'Do not write tool calls, DSML, XML, or JSON tool-call blocks in the answer.',
|
|
282
|
-
'If the available tool results are incomplete, say what is missing and answer only what the provided context supports.',
|
|
290
|
+
'Web tools are now unavailable.',
|
|
291
|
+
'Answer now using only completed tool results as visible assistant text, without tool calls or tool-call markup.',
|
|
292
|
+
'If results are incomplete, state what is missing and do not infer beyond them.',
|
|
283
293
|
].join(' '),
|
|
284
294
|
}),
|
|
285
295
|
};
|
|
@@ -407,6 +417,8 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
407
417
|
const effectiveChatRequest = { ...chatRequest, normalized };
|
|
408
418
|
const state = prepareWebSearchRequest({ normalized, chatRequest: effectiveChatRequest, config });
|
|
409
419
|
const searchConfig = state.config || config;
|
|
420
|
+
const webTools = state.webTools;
|
|
421
|
+
const webCache = state.webCache;
|
|
410
422
|
let currentChatRequest = state.enabled ? state.chatRequest : effectiveChatRequest;
|
|
411
423
|
const completions = [];
|
|
412
424
|
const searches = [];
|
|
@@ -417,8 +429,8 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
417
429
|
const maxRounds = maxWebSearchRounds(searchConfig);
|
|
418
430
|
|
|
419
431
|
async function requestFinalAnswer(baseRequest) {
|
|
420
|
-
currentChatRequest = toolCallsToFinalAnswerRequest(baseRequest);
|
|
421
|
-
const upstreamRequest =
|
|
432
|
+
currentChatRequest = toolCallsToFinalAnswerRequest(baseRequest, webTools);
|
|
433
|
+
const upstreamRequest = toWebSearchUpstreamRequest(currentChatRequest, config, webTools);
|
|
422
434
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
423
435
|
logDebugPayload(config, upstreamRequest, {
|
|
424
436
|
stage: 'web_search_final_answer',
|
|
@@ -444,7 +456,7 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
444
456
|
}
|
|
445
457
|
|
|
446
458
|
for (let round = 0; round <= maxRounds + 1; round += 1) {
|
|
447
|
-
const upstreamRequest =
|
|
459
|
+
const upstreamRequest = toWebSearchUpstreamRequest(currentChatRequest, config, webTools);
|
|
448
460
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
449
461
|
logDebugPayload(config, upstreamRequest, {
|
|
450
462
|
stage: `web_search_round_${round}`,
|
|
@@ -465,9 +477,9 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
465
477
|
const commentaryCalls = bridgedCommentaryToolCallsFromMessage(roundChatMessage, currentChatRequest.tools);
|
|
466
478
|
const routingData = stripBridgedCommentaryFromCompletion(data, currentChatRequest.tools);
|
|
467
479
|
|
|
468
|
-
if (state.enabled && hasKnownExternalToolCalls(routingData, currentChatRequest.tools)) {
|
|
480
|
+
if (state.enabled && hasKnownExternalToolCalls(routingData, currentChatRequest.tools, webTools)) {
|
|
469
481
|
finalCompletion = restoreCommentaryToolCalls(
|
|
470
|
-
knownExternalToolCallsCompletion(routingData, currentChatRequest.tools),
|
|
482
|
+
knownExternalToolCallsCompletion(routingData, currentChatRequest.tools, webTools),
|
|
471
483
|
commentaryCalls,
|
|
472
484
|
);
|
|
473
485
|
completions[completions.length - 1] = finalCompletion;
|
|
@@ -478,15 +490,16 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
478
490
|
break;
|
|
479
491
|
}
|
|
480
492
|
|
|
481
|
-
const wantsInternalWeb = shouldContinueWebSearchLoop(routingData);
|
|
493
|
+
const wantsInternalWeb = shouldContinueWebSearchLoop(routingData, webTools);
|
|
482
494
|
const wantsInternalRound = wantsInternalWeb || commentaryCalls.length > 0;
|
|
483
495
|
const hasToolCalls = hasAnyToolCalls(routingData);
|
|
484
496
|
|
|
485
497
|
if (!wantsInternalRound || round >= maxRounds) {
|
|
486
|
-
if (hasToolCalls && hasUnknownExternalToolCalls(routingData, currentChatRequest.tools)) {
|
|
498
|
+
if (hasToolCalls && hasUnknownExternalToolCalls(routingData, currentChatRequest.tools, webTools)) {
|
|
487
499
|
const unsupportedMessages = unhandledToolMessagesFromCompletion(routingData, undefined, {
|
|
488
500
|
onlyUnknownExternal: true,
|
|
489
501
|
tools: currentChatRequest.tools,
|
|
502
|
+
webTools,
|
|
490
503
|
});
|
|
491
504
|
const finalAnswerError = await requestFinalAnswer({
|
|
492
505
|
...currentChatRequest,
|
|
@@ -511,6 +524,8 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
511
524
|
toolResult = await executeWebSearchCalls({
|
|
512
525
|
completion: data,
|
|
513
526
|
config: searchConfig,
|
|
527
|
+
webTools,
|
|
528
|
+
webCache,
|
|
514
529
|
signal: mergeAbortSignals([controller.signal, clientSignal]),
|
|
515
530
|
onSearchStart,
|
|
516
531
|
onSearchDone,
|
|
@@ -524,18 +539,22 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
524
539
|
...currentChatRequest,
|
|
525
540
|
messages: currentChatRequest.messages.concat(toolResult.messages, commentaryToolMessages(commentaryCalls)),
|
|
526
541
|
tool_choice:
|
|
527
|
-
currentChatRequest.tool_choice?.function?.name ===
|
|
542
|
+
currentChatRequest.tool_choice?.function?.name === webTools.search && toolResult.searches.length
|
|
528
543
|
? 'auto'
|
|
529
544
|
: currentChatRequest.tool_choice,
|
|
530
545
|
};
|
|
531
546
|
}
|
|
532
547
|
|
|
548
|
+
const usages = completions.map((completion) => completion?.usage).filter(Boolean);
|
|
549
|
+
const reportedUsage = finalCompletion?.usage || lastUsage(usages);
|
|
550
|
+
logWebSearchUsage(config, 'web_search', usages, finalCompletion?.usage);
|
|
533
551
|
return {
|
|
534
552
|
ok: true,
|
|
535
553
|
response: finalResponse,
|
|
536
|
-
completion:
|
|
554
|
+
completion: withUsageFallback(finalCompletion, reportedUsage),
|
|
537
555
|
searches,
|
|
538
556
|
openedPages,
|
|
557
|
+
webTools,
|
|
539
558
|
chatRequest: currentChatRequest,
|
|
540
559
|
incompleteReason,
|
|
541
560
|
};
|
|
@@ -543,23 +562,48 @@ async function runWebSearchChatLoop({ rawRequest, normalized, chatRequest, confi
|
|
|
543
562
|
|
|
544
563
|
const STREAM_HEARTBEAT_MS = 10000;
|
|
545
564
|
|
|
565
|
+
function streamHeartbeatMs(config) {
|
|
566
|
+
const value = Number(config?.streamHeartbeatMs);
|
|
567
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
568
|
+
return STREAM_HEARTBEAT_MS;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function startStreamHeartbeat({ config, res, write }) {
|
|
572
|
+
const heartbeatMs = streamHeartbeatMs(config);
|
|
573
|
+
let lastActivityAt = Date.now();
|
|
574
|
+
let timer = null;
|
|
575
|
+
const stop = () => {
|
|
576
|
+
if (timer) clearInterval(timer);
|
|
577
|
+
timer = null;
|
|
578
|
+
res.off('close', stop);
|
|
579
|
+
};
|
|
580
|
+
timer = setInterval(() => {
|
|
581
|
+
if (res.writableEnded || res.destroyed || Date.now() - lastActivityAt < heartbeatMs) return;
|
|
582
|
+
write();
|
|
583
|
+
lastActivityAt = Date.now();
|
|
584
|
+
}, heartbeatMs);
|
|
585
|
+
res.once('close', stop);
|
|
586
|
+
return {
|
|
587
|
+
activity() {
|
|
588
|
+
lastActivityAt = Date.now();
|
|
589
|
+
},
|
|
590
|
+
stop,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
546
594
|
function writeSseEvent(res, event) {
|
|
547
595
|
if (res.writableEnded || res.destroyed) return;
|
|
548
596
|
res.write(serializeResponsesSseEvent(event));
|
|
549
597
|
}
|
|
550
598
|
|
|
551
|
-
function
|
|
552
|
-
writeSseEvent(res, { done: true });
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function webSearchSseWriter({ res, mapper, outputIndexBySearch, includeSources = false }) {
|
|
599
|
+
function webSearchSseWriter({ writeEvent, mapper, outputIndexBySearch, includeSources = false }) {
|
|
556
600
|
return {
|
|
557
601
|
start(search) {
|
|
558
602
|
const outputIndex = mapper.output.length;
|
|
559
603
|
outputIndexBySearch.set(search.id, outputIndex);
|
|
560
604
|
const item = buildWebSearchCallItem(search, { status: 'in_progress', includeSources });
|
|
561
605
|
mapper.output.push(item);
|
|
562
|
-
|
|
606
|
+
writeEvent({
|
|
563
607
|
type: 'response.output_item.added',
|
|
564
608
|
sequence_number: mapper.nextSequence(),
|
|
565
609
|
output_index: outputIndex,
|
|
@@ -570,7 +614,7 @@ function webSearchSseWriter({ res, mapper, outputIndexBySearch, includeSources =
|
|
|
570
614
|
const outputIndex = outputIndexBySearch.get(search.id);
|
|
571
615
|
const item = buildWebSearchCallItem(search, { includeSources });
|
|
572
616
|
if (Number.isInteger(outputIndex)) mapper.output[outputIndex] = item;
|
|
573
|
-
|
|
617
|
+
writeEvent({
|
|
574
618
|
type: 'response.output_item.done',
|
|
575
619
|
sequence_number: mapper.nextSequence(),
|
|
576
620
|
output_index: Number.isInteger(outputIndex) ? outputIndex : mapper.output.indexOf(item),
|
|
@@ -580,17 +624,19 @@ function webSearchSseWriter({ res, mapper, outputIndexBySearch, includeSources =
|
|
|
580
624
|
};
|
|
581
625
|
}
|
|
582
626
|
|
|
583
|
-
async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest, config, res, responseId,
|
|
627
|
+
async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest, config, res, responseId, clientSignal }) {
|
|
584
628
|
const effectiveChatRequest = { ...chatRequest, normalized };
|
|
585
629
|
const state = prepareWebSearchRequest({ normalized, chatRequest: effectiveChatRequest, config });
|
|
586
630
|
if (!state.enabled) return { handled: false };
|
|
587
631
|
|
|
588
632
|
const searchConfig = state.config || config;
|
|
633
|
+
const webTools = state.webTools;
|
|
634
|
+
const webCache = state.webCache;
|
|
589
635
|
const maxRounds = maxWebSearchRounds(searchConfig);
|
|
590
636
|
let currentChatRequest = state.chatRequest;
|
|
591
637
|
|
|
592
638
|
const buildUpstreamRequest = (stage) => {
|
|
593
|
-
const upstreamRequest =
|
|
639
|
+
const upstreamRequest = toWebSearchUpstreamRequest(currentChatRequest, config, webTools);
|
|
594
640
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
595
641
|
logDebugPayload(config, upstreamRequest, { stage, rawRequest, normalized, chatRequest: currentChatRequest });
|
|
596
642
|
return upstreamRequest;
|
|
@@ -613,15 +659,19 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
613
659
|
responseId,
|
|
614
660
|
model: firstUpstreamRequest.model,
|
|
615
661
|
createdAt: Math.floor(Date.now() / 1000),
|
|
616
|
-
previousResponseId,
|
|
617
662
|
normalized,
|
|
618
663
|
config,
|
|
619
664
|
...resolveReasoningStreamMode(config, firstUpstreamRequest),
|
|
620
665
|
holdToolItemEvents: true,
|
|
621
666
|
knownToolNames: chatToolNamesFromTools(firstUpstreamRequest.tools),
|
|
622
667
|
});
|
|
668
|
+
let heartbeat = null;
|
|
669
|
+
const writeEvent = (event) => {
|
|
670
|
+
writeSseEvent(res, event);
|
|
671
|
+
heartbeat?.activity();
|
|
672
|
+
};
|
|
623
673
|
const searchWriter = webSearchSseWriter({
|
|
624
|
-
|
|
674
|
+
writeEvent,
|
|
625
675
|
mapper,
|
|
626
676
|
outputIndexBySearch: new Map(),
|
|
627
677
|
includeSources: shouldIncludeSearchSources(normalized),
|
|
@@ -639,18 +689,20 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
639
689
|
connection: 'keep-alive',
|
|
640
690
|
'x-accel-buffering': 'no',
|
|
641
691
|
});
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
692
|
+
writeEvent(mapper.createdEvent());
|
|
693
|
+
writeEvent(mapper.inProgressEvent());
|
|
694
|
+
heartbeat = startStreamHeartbeat({
|
|
695
|
+
config,
|
|
696
|
+
res,
|
|
697
|
+
write: () => writeEvent(mapper.inProgressEvent()),
|
|
698
|
+
});
|
|
647
699
|
|
|
648
700
|
const endStream = () => {
|
|
649
|
-
|
|
701
|
+
writeEvent({ done: true });
|
|
650
702
|
res.end();
|
|
651
703
|
};
|
|
652
704
|
const failStream = (message) => {
|
|
653
|
-
for (const event of mapper.streamFailed(message))
|
|
705
|
+
for (const event of mapper.streamFailed(message)) writeEvent(event);
|
|
654
706
|
endStream();
|
|
655
707
|
return { handled: true };
|
|
656
708
|
};
|
|
@@ -658,12 +710,18 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
658
710
|
if (mapper.messageItem?.content?.length) {
|
|
659
711
|
annotateMessagePartWithWebCitations(mapper.messageItem.content[0], searches, openedPages);
|
|
660
712
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
713
|
+
const usages = roundUsages.map((completion) => completion.usage).filter(Boolean).concat(mapper.pendingUsage ? [mapper.pendingUsage] : []);
|
|
714
|
+
const reportedUsage = mapper.pendingUsage || lastUsage(usages);
|
|
715
|
+
logWebSearchUsage(config, 'web_search_stream', usages, mapper.pendingUsage);
|
|
716
|
+
for (const event of mapper.finalize(mapper.pendingFinishReason || 'stop', reportedUsage)) {
|
|
717
|
+
writeEvent(event);
|
|
664
718
|
}
|
|
665
719
|
endStream();
|
|
666
|
-
return {
|
|
720
|
+
return {
|
|
721
|
+
handled: true,
|
|
722
|
+
chatRequest: currentChatRequest,
|
|
723
|
+
assistantMessage: mapper.terminalStatus === 'completed' ? mapper.roundAssistantMessage() : null,
|
|
724
|
+
};
|
|
667
725
|
};
|
|
668
726
|
const relayRound = async () => {
|
|
669
727
|
roundEnd = null;
|
|
@@ -680,7 +738,7 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
680
738
|
return;
|
|
681
739
|
}
|
|
682
740
|
for (const responseEvent of mapper.mapChatEvent(event)) {
|
|
683
|
-
|
|
741
|
+
writeEvent(responseEvent);
|
|
684
742
|
}
|
|
685
743
|
},
|
|
686
744
|
});
|
|
@@ -698,7 +756,7 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
698
756
|
mapper.expandParallelToolItems();
|
|
699
757
|
const roundMessage = mapper.roundAssistantMessage();
|
|
700
758
|
const commentaryCalls = bridgedCommentaryToolCallsFromMessage(roundMessage, currentChatRequest.tools);
|
|
701
|
-
for (const event of mapper.convertBridgedCommentaryItems())
|
|
759
|
+
for (const event of mapper.convertBridgedCommentaryItems()) writeEvent(event);
|
|
702
760
|
const completionLike = {
|
|
703
761
|
choices: [{ index: 0, message: roundMessage, finish_reason: mapper.pendingFinishReason || 'stop' }],
|
|
704
762
|
};
|
|
@@ -709,13 +767,13 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
709
767
|
const reason = finalAnswerIncompleteReason(routingCompletion);
|
|
710
768
|
if (reason) {
|
|
711
769
|
const events = mapper.replaceBufferedAssistantText(gatewayIncompleteMessageContent(reason));
|
|
712
|
-
for (const event of events || [])
|
|
770
|
+
for (const event of events || []) writeEvent(event);
|
|
713
771
|
}
|
|
714
772
|
return finishTurn();
|
|
715
773
|
}
|
|
716
774
|
|
|
717
|
-
if (hasKnownExternalToolCalls(routingCompletion, currentChatRequest.tools)) {
|
|
718
|
-
const kept = knownExternalToolCallsCompletion(routingCompletion, currentChatRequest.tools);
|
|
775
|
+
if (hasKnownExternalToolCalls(routingCompletion, currentChatRequest.tools, webTools)) {
|
|
776
|
+
const kept = knownExternalToolCallsCompletion(routingCompletion, currentChatRequest.tools, webTools);
|
|
719
777
|
const keptIds = new Set(
|
|
720
778
|
(kept.choices?.[0]?.message?.tool_calls || []).map((toolCall) => toolCall.id).filter(Boolean),
|
|
721
779
|
);
|
|
@@ -723,33 +781,34 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
723
781
|
return finishTurn();
|
|
724
782
|
}
|
|
725
783
|
|
|
726
|
-
const wantsInternalWeb = shouldContinueWebSearchLoop(routingCompletion);
|
|
784
|
+
const wantsInternalWeb = shouldContinueWebSearchLoop(routingCompletion, webTools);
|
|
727
785
|
const wantsInternalRound = wantsInternalWeb || commentaryCalls.length > 0;
|
|
728
786
|
const hasToolCallsRound = hasAnyToolCalls(routingCompletion);
|
|
729
787
|
|
|
730
788
|
if (!wantsInternalRound || round >= maxRounds) {
|
|
731
|
-
if (hasToolCallsRound && hasUnknownExternalToolCalls(routingCompletion, currentChatRequest.tools)) {
|
|
789
|
+
if (hasToolCallsRound && hasUnknownExternalToolCalls(routingCompletion, currentChatRequest.tools, webTools)) {
|
|
732
790
|
const unsupportedMessages = unhandledToolMessagesFromCompletion(routingCompletion, undefined, {
|
|
733
791
|
onlyUnknownExternal: true,
|
|
734
792
|
tools: currentChatRequest.tools,
|
|
793
|
+
webTools,
|
|
735
794
|
});
|
|
736
795
|
currentChatRequest = toolCallsToFinalAnswerRequest({
|
|
737
796
|
...currentChatRequest,
|
|
738
797
|
messages: currentChatRequest.messages.concat(unsupportedMessages),
|
|
739
|
-
});
|
|
798
|
+
}, webTools);
|
|
740
799
|
} else if (wantsInternalRound || (!hasToolCallsRound && hasWebSearchContext(searches, openedPages) && !hasVisibleAssistantContent(routingCompletion))) {
|
|
741
|
-
currentChatRequest = toolCallsToFinalAnswerRequest(currentChatRequest);
|
|
800
|
+
currentChatRequest = toolCallsToFinalAnswerRequest(currentChatRequest, webTools);
|
|
742
801
|
} else {
|
|
743
802
|
return finishTurn();
|
|
744
803
|
}
|
|
745
804
|
finalAnswerForced = true;
|
|
746
805
|
roundUsages.push({ usage: mapper.pendingUsage });
|
|
747
806
|
mapper.removeToolItems();
|
|
748
|
-
for (const event of mapper.beginNextRound())
|
|
807
|
+
for (const event of mapper.beginNextRound()) writeEvent(event);
|
|
749
808
|
mapper.holdVisibleTextUntilDone();
|
|
750
809
|
} else {
|
|
751
810
|
roundUsages.push({ usage: mapper.pendingUsage });
|
|
752
|
-
for (const event of mapper.beginNextRound())
|
|
811
|
+
for (const event of mapper.beginNextRound()) writeEvent(event);
|
|
753
812
|
const controller = new AbortController();
|
|
754
813
|
const timeout = setTimeout(() => controller.abort(), webToolTimeoutMs(searchConfig));
|
|
755
814
|
let toolResult;
|
|
@@ -757,6 +816,8 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
757
816
|
toolResult = await executeWebSearchCalls({
|
|
758
817
|
completion: completionLike,
|
|
759
818
|
config: searchConfig,
|
|
819
|
+
webTools,
|
|
820
|
+
webCache,
|
|
760
821
|
signal: mergeAbortSignals([controller.signal, clientSignal]),
|
|
761
822
|
onSearchStart(search) {
|
|
762
823
|
if (search.action && search.auto) return;
|
|
@@ -777,7 +838,7 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
777
838
|
...currentChatRequest,
|
|
778
839
|
messages: currentChatRequest.messages.concat(toolResult.messages, commentaryToolMessages(commentaryCalls)),
|
|
779
840
|
tool_choice:
|
|
780
|
-
currentChatRequest.tool_choice?.function?.name ===
|
|
841
|
+
currentChatRequest.tool_choice?.function?.name === webTools.search && toolResult.searches.length
|
|
781
842
|
? 'auto'
|
|
782
843
|
: currentChatRequest.tool_choice,
|
|
783
844
|
};
|
|
@@ -806,15 +867,16 @@ async function runStreamingWebSearchTurn({ rawRequest, normalized, chatRequest,
|
|
|
806
867
|
}
|
|
807
868
|
throw error;
|
|
808
869
|
} finally {
|
|
809
|
-
|
|
870
|
+
heartbeat?.stop();
|
|
810
871
|
}
|
|
811
872
|
}
|
|
812
873
|
|
|
813
|
-
export function createProxyServer({ config = loadConfig(),
|
|
814
|
-
|
|
815
|
-
persistPath: config.
|
|
816
|
-
|
|
817
|
-
|
|
874
|
+
export function createProxyServer({ config = loadConfig(), reasoningCache } = {}) {
|
|
875
|
+
reasoningCache ||= new ReasoningCache({
|
|
876
|
+
persistPath: config.reasoningCacheEnabled === false ? '' : config.reasoningCachePath,
|
|
877
|
+
legacyPath: config.legacyReasoningCachePath,
|
|
878
|
+
maxMessages: config.reasoningCacheMaxMessages,
|
|
879
|
+
maxBytes: config.reasoningCacheMaxBytes,
|
|
818
880
|
});
|
|
819
881
|
let modelCache = null;
|
|
820
882
|
|
|
@@ -852,16 +914,11 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
852
914
|
}
|
|
853
915
|
|
|
854
916
|
const request = parsed.value;
|
|
855
|
-
const normalized = normalizeResponsesRequest(request
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (priorMessages.length) {
|
|
861
|
-
normalized.messages = priorMessages.concat(normalized.messages);
|
|
862
|
-
}
|
|
863
|
-
normalized.messages = prependMissingAssistantToolMessages(normalized.messages, sessions);
|
|
864
|
-
normalized.messages = restoreAssistantReasoningContent(normalized.messages, sessions);
|
|
917
|
+
const normalized = normalizeResponsesRequest(request, {
|
|
918
|
+
restoreDiscoveredTools: codexRequestKind(req) !== 'compaction',
|
|
919
|
+
});
|
|
920
|
+
normalized.messages = prependMissingAssistantToolMessages(normalized.messages, reasoningCache);
|
|
921
|
+
normalized.messages = restoreAssistantReasoningContent(normalized.messages, reasoningCache);
|
|
865
922
|
|
|
866
923
|
const chatRequest = toChatCompletionsRequest(normalized);
|
|
867
924
|
const useWebSearchEmulator = hasTavilyWebSearch(config, normalized);
|
|
@@ -886,17 +943,7 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
886
943
|
}
|
|
887
944
|
|
|
888
945
|
const responseId = generateId('resp');
|
|
889
|
-
const
|
|
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);
|
|
898
|
-
}
|
|
899
|
-
};
|
|
946
|
+
const persistReasoning = (assistantMessage) => reasoningCache.rememberAssistantMessage(assistantMessage);
|
|
900
947
|
|
|
901
948
|
if (useWebSearchEmulator && request.stream) {
|
|
902
949
|
const result = await runStreamingWebSearchTurn({
|
|
@@ -906,11 +953,10 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
906
953
|
config,
|
|
907
954
|
res,
|
|
908
955
|
responseId,
|
|
909
|
-
previousResponseId,
|
|
910
956
|
clientSignal,
|
|
911
957
|
});
|
|
912
958
|
if (result.handled) {
|
|
913
|
-
if (result.assistantMessage)
|
|
959
|
+
if (result.assistantMessage) persistReasoning(result.assistantMessage);
|
|
914
960
|
return;
|
|
915
961
|
}
|
|
916
962
|
} else if (useWebSearchEmulator) {
|
|
@@ -936,12 +982,13 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
936
982
|
const payload = applyWebSearchOutputCompatibility(convertChatCompletionToResponses({
|
|
937
983
|
completion: loop.completion,
|
|
938
984
|
model: upstreamRequest.model,
|
|
939
|
-
previousResponseId,
|
|
940
985
|
normalized,
|
|
941
986
|
responseId,
|
|
942
987
|
config,
|
|
943
|
-
}), loop.searches, normalized, loop.openedPages);
|
|
944
|
-
|
|
988
|
+
}), loop.searches, normalized, loop.openedPages, loop.webTools);
|
|
989
|
+
if (payload.status === 'completed') {
|
|
990
|
+
persistReasoning(assistantMessageFromResponseOutput(payload.output));
|
|
991
|
+
}
|
|
945
992
|
sendJson(res, 200, payload);
|
|
946
993
|
return;
|
|
947
994
|
}
|
|
@@ -969,12 +1016,13 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
969
1016
|
const payload = convertChatCompletionToResponses({
|
|
970
1017
|
completion: data,
|
|
971
1018
|
model,
|
|
972
|
-
previousResponseId,
|
|
973
1019
|
normalized,
|
|
974
1020
|
responseId,
|
|
975
1021
|
config,
|
|
976
1022
|
});
|
|
977
|
-
|
|
1023
|
+
if (payload.status === 'completed') {
|
|
1024
|
+
persistReasoning(assistantMessageFromResponseOutput(payload.output));
|
|
1025
|
+
}
|
|
978
1026
|
sendJson(res, 200, payload);
|
|
979
1027
|
return;
|
|
980
1028
|
}
|
|
@@ -990,7 +1038,6 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
990
1038
|
responseId,
|
|
991
1039
|
model,
|
|
992
1040
|
createdAt: Math.floor(Date.now() / 1000),
|
|
993
|
-
previousResponseId,
|
|
994
1041
|
normalized,
|
|
995
1042
|
config,
|
|
996
1043
|
...resolveReasoningStreamMode(config, upstreamRequest),
|
|
@@ -1005,7 +1052,13 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
1005
1052
|
|
|
1006
1053
|
writeSseEvent(res, mapper.createdEvent());
|
|
1007
1054
|
writeSseEvent(res, mapper.inProgressEvent());
|
|
1008
|
-
|
|
1055
|
+
const heartbeat = startStreamHeartbeat({
|
|
1056
|
+
config,
|
|
1057
|
+
res,
|
|
1058
|
+
write: () => {
|
|
1059
|
+
if (!doneSent) writeSseEvent(res, mapper.inProgressEvent());
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1009
1062
|
|
|
1010
1063
|
try {
|
|
1011
1064
|
await relayChatCompletionsResponse({
|
|
@@ -1019,10 +1072,7 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
1019
1072
|
writeSseEvent(res, responseEvent);
|
|
1020
1073
|
}
|
|
1021
1074
|
if (events.length) {
|
|
1022
|
-
|
|
1023
|
-
} else if (!event?.done && Date.now() - lastEventWriteAt >= STREAM_HEARTBEAT_MS) {
|
|
1024
|
-
writeSseEvent(res, mapper.inProgressEvent());
|
|
1025
|
-
lastEventWriteAt = Date.now();
|
|
1075
|
+
heartbeat.activity();
|
|
1026
1076
|
}
|
|
1027
1077
|
if (event?.done) writeResponsesDone();
|
|
1028
1078
|
},
|
|
@@ -1030,9 +1080,13 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
1030
1080
|
} catch (error) {
|
|
1031
1081
|
if (clientSignal.aborted) return;
|
|
1032
1082
|
throw error;
|
|
1083
|
+
} finally {
|
|
1084
|
+
heartbeat.stop();
|
|
1033
1085
|
}
|
|
1034
1086
|
|
|
1035
|
-
|
|
1087
|
+
if (mapper.terminalStatus === 'completed') {
|
|
1088
|
+
persistReasoning(mapper.assistantMessage());
|
|
1089
|
+
}
|
|
1036
1090
|
}
|
|
1037
1091
|
|
|
1038
1092
|
return http.createServer(async (req, res) => {
|
|
@@ -1098,7 +1152,8 @@ export function createProxyServer({ config = loadConfig(), sessions } = {}) {
|
|
|
1098
1152
|
}
|
|
1099
1153
|
return;
|
|
1100
1154
|
}
|
|
1101
|
-
|
|
1155
|
+
const statusCode = Number.isInteger(error.statusCode) ? error.statusCode : 500;
|
|
1156
|
+
sendJson(res, statusCode, { error: { code: error.code, message: error.message || 'Internal server error' } });
|
|
1102
1157
|
}
|
|
1103
1158
|
});
|
|
1104
1159
|
}
|