@galaxy-yearn/codex-deepseek-gateway 0.1.0 → 0.1.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/LICENSE +21 -0
- package/README.md +50 -11
- package/bin/codex-deepseek-gateway.js +7 -0
- package/config/gateway.example.json +11 -0
- package/package.json +14 -4
- package/src/config.js +28 -0
- package/src/firecrawl.js +369 -0
- package/src/local-config.js +1 -1
- package/src/protocol.js +10 -11
- package/src/server.js +492 -2
- package/src/tavily.js +249 -0
- package/src/web-search-emulator.js +660 -0
package/src/protocol.js
CHANGED
|
@@ -25,6 +25,8 @@ const TOOL_OUTPUT_TYPES = new Set([
|
|
|
25
25
|
'code_interpreter_call_output',
|
|
26
26
|
'image_generation_call_output',
|
|
27
27
|
]);
|
|
28
|
+
const CHAT_HISTORY_IGNORED_TOOL_ITEM_TYPES = new Set(['web_search_call', 'web_search_call_output']);
|
|
29
|
+
const EMULATED_HOSTED_TOOL_TYPES = new Set(['web_search', 'web_search_preview']);
|
|
28
30
|
|
|
29
31
|
function jsonString(value) {
|
|
30
32
|
if (typeof value === 'string') return value;
|
|
@@ -288,6 +290,11 @@ function extractMessagesFromResponsesInput(input) {
|
|
|
288
290
|
if (normalized) messages.push(normalized);
|
|
289
291
|
continue;
|
|
290
292
|
}
|
|
293
|
+
if (CHAT_HISTORY_IGNORED_TOOL_ITEM_TYPES.has(item.type)) {
|
|
294
|
+
flushPendingUserContent();
|
|
295
|
+
flushPendingAssistantToolMessage();
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
291
298
|
if (TOOL_OUTPUT_TYPES.has(item.type) || String(item.type || '').endsWith('_call_output')) {
|
|
292
299
|
flushPendingUserContent();
|
|
293
300
|
flushPendingAssistantToolMessage();
|
|
@@ -339,6 +346,9 @@ function extractMessagesFromResponsesInput(input) {
|
|
|
339
346
|
|
|
340
347
|
function normalizeTool(tool) {
|
|
341
348
|
if (!isObject(tool)) return tool;
|
|
349
|
+
if (typeof tool.type === 'string' && EMULATED_HOSTED_TOOL_TYPES.has(tool.type)) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
342
352
|
if (tool.type === 'function' && isObject(tool.function)) {
|
|
343
353
|
return {
|
|
344
354
|
...tool,
|
|
@@ -360,17 +370,6 @@ function normalizeTool(tool) {
|
|
|
360
370
|
}),
|
|
361
371
|
};
|
|
362
372
|
}
|
|
363
|
-
if (typeof tool.type === 'string') {
|
|
364
|
-
return {
|
|
365
|
-
type: 'function',
|
|
366
|
-
function: omitUndefined({
|
|
367
|
-
name: sanitizeFunctionName(tool.name || tool.type),
|
|
368
|
-
description: tool.description || `Gateway shim for Responses tool type ${tool.type}.`,
|
|
369
|
-
parameters: normalizeJsonSchemaObject(tool.parameters || tool.input_schema),
|
|
370
|
-
strict: tool.strict,
|
|
371
|
-
}),
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
373
|
return null;
|
|
375
374
|
}
|
|
376
375
|
|
package/src/server.js
CHANGED
|
@@ -15,6 +15,19 @@ import {
|
|
|
15
15
|
} from './protocol.js';
|
|
16
16
|
import { SessionStore } from './session-store.js';
|
|
17
17
|
import { callChatCompletions, callModels, readJsonResponse, relayChatCompletionsResponse } from './upstream.js';
|
|
18
|
+
import {
|
|
19
|
+
applyWebSearchOutputCompatibility,
|
|
20
|
+
buildWebSearchCallItem,
|
|
21
|
+
containsWebSearchTool,
|
|
22
|
+
executeWebSearchCalls,
|
|
23
|
+
hasAnyToolCalls,
|
|
24
|
+
INTERNAL_WEB_SEARCH_TOOL,
|
|
25
|
+
maxWebSearchRounds,
|
|
26
|
+
prepareWebSearchRequest,
|
|
27
|
+
shouldIncludeSearchSources,
|
|
28
|
+
shouldContinueWebSearchLoop,
|
|
29
|
+
unhandledToolMessagesFromCompletion,
|
|
30
|
+
} from './web-search-emulator.js';
|
|
18
31
|
|
|
19
32
|
function sendJson(res, statusCode, payload, headers = {}) {
|
|
20
33
|
res.writeHead(statusCode, {
|
|
@@ -53,6 +66,10 @@ function historyMessagesFromSession(session) {
|
|
|
53
66
|
if (!session?.history?.length) return [];
|
|
54
67
|
const messages = [];
|
|
55
68
|
for (const turn of session.history) {
|
|
69
|
+
if (Array.isArray(turn.historyMessages)) {
|
|
70
|
+
messages.push(...turn.historyMessages);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
56
73
|
if (Array.isArray(turn.inputMessages)) {
|
|
57
74
|
messages.push(...turn.inputMessages);
|
|
58
75
|
} else if (Array.isArray(turn.chatRequest?.messages)) {
|
|
@@ -128,6 +145,361 @@ function resolveReasoningStreamMode(config, upstreamRequest) {
|
|
|
128
145
|
return { emitReasoningSummary: false, emitReasoningText: false };
|
|
129
146
|
}
|
|
130
147
|
|
|
148
|
+
function hasTavilyWebSearch(config, normalized) {
|
|
149
|
+
return Boolean(config.tavilyWebSearchEnabled && config.tavilyApiKey && containsWebSearchTool(normalized));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function disableStreaming(request) {
|
|
153
|
+
return {
|
|
154
|
+
...request,
|
|
155
|
+
stream: false,
|
|
156
|
+
stream_options: undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function webToolTimeoutMs(config = {}) {
|
|
161
|
+
const tavilyTimeout = Number(config.tavilyTimeoutMs) || 15000;
|
|
162
|
+
if (!config.firecrawlWebFetchEnabled || !config.firecrawlApiKey) return tavilyTimeout;
|
|
163
|
+
const firecrawlTimeout = Number(config.firecrawlTimeoutMs) || 30000;
|
|
164
|
+
return Math.max(tavilyTimeout, firecrawlTimeout);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function addUsage(left, right) {
|
|
168
|
+
if (!left) return right || null;
|
|
169
|
+
if (!right) return left;
|
|
170
|
+
return {
|
|
171
|
+
prompt_tokens: (left.prompt_tokens || 0) + (right.prompt_tokens || 0),
|
|
172
|
+
completion_tokens: (left.completion_tokens || 0) + (right.completion_tokens || 0),
|
|
173
|
+
total_tokens: (left.total_tokens || 0) + (right.total_tokens || 0),
|
|
174
|
+
reasoning_tokens: (left.reasoning_tokens || 0) + (right.reasoning_tokens || 0),
|
|
175
|
+
prompt_tokens_details: {
|
|
176
|
+
cached_tokens: (left.prompt_tokens_details?.cached_tokens || 0) + (right.prompt_tokens_details?.cached_tokens || 0),
|
|
177
|
+
},
|
|
178
|
+
completion_tokens_details: {
|
|
179
|
+
reasoning_tokens:
|
|
180
|
+
(left.completion_tokens_details?.reasoning_tokens || 0) +
|
|
181
|
+
(right.completion_tokens_details?.reasoning_tokens || 0),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function combineUsage(completions) {
|
|
187
|
+
return completions.reduce((usage, completion) => addUsage(usage, completion?.usage), null);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cloneCompletionWithUsage(completion, usage) {
|
|
191
|
+
if (!usage) return completion;
|
|
192
|
+
return {
|
|
193
|
+
...completion,
|
|
194
|
+
usage,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toolCallsToFinalAnswerRequest(request) {
|
|
199
|
+
return {
|
|
200
|
+
...request,
|
|
201
|
+
tool_choice: 'none',
|
|
202
|
+
tools: undefined,
|
|
203
|
+
messages: request.messages.concat({
|
|
204
|
+
role: 'user',
|
|
205
|
+
content: [
|
|
206
|
+
'Do not call more tools.',
|
|
207
|
+
'Use the completed tool results already provided in this conversation.',
|
|
208
|
+
'Give the final answer now.',
|
|
209
|
+
'If the available tool results are incomplete, say what is missing and answer only what the provided context supports.',
|
|
210
|
+
].join(' '),
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function callUpstreamJson({ upstreamRequest, config }) {
|
|
216
|
+
const response = await callChatCompletions({
|
|
217
|
+
baseUrl: config.upstreamBaseUrl,
|
|
218
|
+
apiKey: config.upstreamApiKey,
|
|
219
|
+
request: upstreamRequest,
|
|
220
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
221
|
+
});
|
|
222
|
+
const data = await readJsonResponse(response);
|
|
223
|
+
return { response, data };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function runWebSearchChatLoop({ normalized, chatRequest, config, onSearchStart, onSearchDone }) {
|
|
227
|
+
const effectiveChatRequest = { ...chatRequest, normalized };
|
|
228
|
+
const state = prepareWebSearchRequest({ normalized, chatRequest: effectiveChatRequest, config });
|
|
229
|
+
const searchConfig = state.config || config;
|
|
230
|
+
let currentChatRequest = state.enabled ? state.chatRequest : effectiveChatRequest;
|
|
231
|
+
const completions = [];
|
|
232
|
+
const searches = [];
|
|
233
|
+
const openedPages = [];
|
|
234
|
+
let finalCompletion = null;
|
|
235
|
+
let finalResponse = null;
|
|
236
|
+
let forcedFinalAnswer = false;
|
|
237
|
+
const maxRounds = maxWebSearchRounds(searchConfig);
|
|
238
|
+
|
|
239
|
+
for (let round = 0; round <= maxRounds + 1; round += 1) {
|
|
240
|
+
const upstreamRequest = toProviderChatCompletionsRequest(currentChatRequest, config);
|
|
241
|
+
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || currentChatRequest.model;
|
|
242
|
+
const { response, data } = await callUpstreamJson({
|
|
243
|
+
upstreamRequest: disableStreaming(upstreamRequest),
|
|
244
|
+
config,
|
|
245
|
+
});
|
|
246
|
+
finalResponse = response;
|
|
247
|
+
if (!response.ok) return { ok: false, status: response.status, data };
|
|
248
|
+
completions.push(data);
|
|
249
|
+
finalCompletion = data;
|
|
250
|
+
|
|
251
|
+
if (!state.enabled || !shouldContinueWebSearchLoop(data) || round >= maxRounds) {
|
|
252
|
+
if (state.enabled && hasAnyToolCalls(data) && !forcedFinalAnswer) {
|
|
253
|
+
const unsupportedMessages = unhandledToolMessagesFromCompletion(data);
|
|
254
|
+
currentChatRequest = toolCallsToFinalAnswerRequest({
|
|
255
|
+
...currentChatRequest,
|
|
256
|
+
messages: currentChatRequest.messages.concat(unsupportedMessages),
|
|
257
|
+
});
|
|
258
|
+
forcedFinalAnswer = true;
|
|
259
|
+
if (unsupportedMessages.length) {
|
|
260
|
+
finalCompletion = null;
|
|
261
|
+
completions.pop();
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timeout = setTimeout(() => controller.abort(), webToolTimeoutMs(searchConfig));
|
|
270
|
+
let toolResult;
|
|
271
|
+
try {
|
|
272
|
+
toolResult = await executeWebSearchCalls({
|
|
273
|
+
completion: data,
|
|
274
|
+
config: searchConfig,
|
|
275
|
+
signal: controller.signal,
|
|
276
|
+
onSearchStart,
|
|
277
|
+
onSearchDone,
|
|
278
|
+
});
|
|
279
|
+
} finally {
|
|
280
|
+
clearTimeout(timeout);
|
|
281
|
+
}
|
|
282
|
+
searches.push(...toolResult.searches);
|
|
283
|
+
openedPages.push(...(toolResult.openedPages || []));
|
|
284
|
+
currentChatRequest = {
|
|
285
|
+
...currentChatRequest,
|
|
286
|
+
messages: currentChatRequest.messages.concat(toolResult.messages),
|
|
287
|
+
tool_choice:
|
|
288
|
+
currentChatRequest.tool_choice?.function?.name === INTERNAL_WEB_SEARCH_TOOL
|
|
289
|
+
? 'auto'
|
|
290
|
+
: currentChatRequest.tool_choice,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
ok: true,
|
|
296
|
+
response: finalResponse,
|
|
297
|
+
completion: cloneCompletionWithUsage(finalCompletion, combineUsage(completions)),
|
|
298
|
+
searches,
|
|
299
|
+
openedPages,
|
|
300
|
+
chatRequest: currentChatRequest,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const REPLAY_REASONING_DELTA_CHARS = 1200;
|
|
305
|
+
|
|
306
|
+
function splitReplayText(text, maxChars = REPLAY_REASONING_DELTA_CHARS) {
|
|
307
|
+
const value = String(text ?? '');
|
|
308
|
+
if (!value) return [];
|
|
309
|
+
const chunks = [];
|
|
310
|
+
for (let index = 0; index < value.length; index += maxChars) {
|
|
311
|
+
chunks.push(value.slice(index, index + maxChars));
|
|
312
|
+
}
|
|
313
|
+
return chunks;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function replayReasoningItemEvents({ item, outputIndex, mapper }) {
|
|
317
|
+
const events = [];
|
|
318
|
+
const summaries = Array.isArray(item.summary) ? item.summary : [];
|
|
319
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
320
|
+
const addedItem = {
|
|
321
|
+
...item,
|
|
322
|
+
status: 'in_progress',
|
|
323
|
+
summary: [],
|
|
324
|
+
content: content.map((part) => (part?.type === 'reasoning_text' ? { ...part, text: '' } : part)),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
events.push({
|
|
328
|
+
type: 'response.output_item.added',
|
|
329
|
+
sequence_number: mapper.nextSequence(),
|
|
330
|
+
output_index: outputIndex,
|
|
331
|
+
item: addedItem,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
for (const [summaryIndex, part] of summaries.entries()) {
|
|
335
|
+
if (part?.type !== 'summary_text') continue;
|
|
336
|
+
const text = String(part.text ?? '');
|
|
337
|
+
events.push({
|
|
338
|
+
type: 'response.reasoning_summary_part.added',
|
|
339
|
+
sequence_number: mapper.nextSequence(),
|
|
340
|
+
output_index: outputIndex,
|
|
341
|
+
item_id: item.id,
|
|
342
|
+
summary_index: summaryIndex,
|
|
343
|
+
part: { ...part, text: '' },
|
|
344
|
+
});
|
|
345
|
+
for (const delta of splitReplayText(text)) {
|
|
346
|
+
events.push({
|
|
347
|
+
type: 'response.reasoning_summary_text.delta',
|
|
348
|
+
sequence_number: mapper.nextSequence(),
|
|
349
|
+
output_index: outputIndex,
|
|
350
|
+
item_id: item.id,
|
|
351
|
+
summary_index: summaryIndex,
|
|
352
|
+
delta,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
events.push({
|
|
356
|
+
type: 'response.reasoning_summary_text.done',
|
|
357
|
+
sequence_number: mapper.nextSequence(),
|
|
358
|
+
output_index: outputIndex,
|
|
359
|
+
item_id: item.id,
|
|
360
|
+
summary_index: summaryIndex,
|
|
361
|
+
text,
|
|
362
|
+
});
|
|
363
|
+
events.push({
|
|
364
|
+
type: 'response.reasoning_summary_part.done',
|
|
365
|
+
sequence_number: mapper.nextSequence(),
|
|
366
|
+
output_index: outputIndex,
|
|
367
|
+
item_id: item.id,
|
|
368
|
+
summary_index: summaryIndex,
|
|
369
|
+
part,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
events.push({
|
|
374
|
+
type: 'response.output_item.done',
|
|
375
|
+
sequence_number: mapper.nextSequence(),
|
|
376
|
+
output_index: outputIndex,
|
|
377
|
+
item,
|
|
378
|
+
});
|
|
379
|
+
return events;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function outputEventsFromPayload(payload, mapper, { skipTypes = new Set() } = {}) {
|
|
383
|
+
const events = [];
|
|
384
|
+
const output = Array.isArray(payload.output) ? payload.output : [];
|
|
385
|
+
for (const [outputIndex, item] of output.entries()) {
|
|
386
|
+
if (skipTypes.has(item?.type)) continue;
|
|
387
|
+
mapper.output[outputIndex] = item;
|
|
388
|
+
if (item?.type === 'reasoning') {
|
|
389
|
+
events.push(...replayReasoningItemEvents({ item, outputIndex, mapper }));
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
events.push({
|
|
393
|
+
type: 'response.output_item.added',
|
|
394
|
+
sequence_number: mapper.nextSequence(),
|
|
395
|
+
output_index: outputIndex,
|
|
396
|
+
item,
|
|
397
|
+
});
|
|
398
|
+
if (item.type === 'message' && Array.isArray(item.content)) {
|
|
399
|
+
for (const [contentIndex, part] of item.content.entries()) {
|
|
400
|
+
events.push({
|
|
401
|
+
type: 'response.content_part.added',
|
|
402
|
+
sequence_number: mapper.nextSequence(),
|
|
403
|
+
output_index: outputIndex,
|
|
404
|
+
content_index: contentIndex,
|
|
405
|
+
item_id: item.id,
|
|
406
|
+
part: part.type === 'output_text' ? { ...part, text: '', annotations: [] } : part,
|
|
407
|
+
});
|
|
408
|
+
if (part.type === 'output_text' && part.text) {
|
|
409
|
+
events.push({
|
|
410
|
+
type: 'response.output_text.delta',
|
|
411
|
+
sequence_number: mapper.nextSequence(),
|
|
412
|
+
output_index: outputIndex,
|
|
413
|
+
content_index: contentIndex,
|
|
414
|
+
item_id: item.id,
|
|
415
|
+
delta: part.text,
|
|
416
|
+
logprobs: [],
|
|
417
|
+
});
|
|
418
|
+
for (const [annotationIndex, annotation] of (Array.isArray(part.annotations) ? part.annotations : []).entries()) {
|
|
419
|
+
events.push({
|
|
420
|
+
type: 'response.output_text.annotation.added',
|
|
421
|
+
sequence_number: mapper.nextSequence(),
|
|
422
|
+
output_index: outputIndex,
|
|
423
|
+
content_index: contentIndex,
|
|
424
|
+
item_id: item.id,
|
|
425
|
+
annotation_index: annotationIndex,
|
|
426
|
+
annotation,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
events.push({
|
|
430
|
+
type: 'response.output_text.done',
|
|
431
|
+
sequence_number: mapper.nextSequence(),
|
|
432
|
+
output_index: outputIndex,
|
|
433
|
+
content_index: contentIndex,
|
|
434
|
+
item_id: item.id,
|
|
435
|
+
text: part.text,
|
|
436
|
+
logprobs: [],
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
events.push({
|
|
440
|
+
type: 'response.content_part.done',
|
|
441
|
+
sequence_number: mapper.nextSequence(),
|
|
442
|
+
output_index: outputIndex,
|
|
443
|
+
content_index: contentIndex,
|
|
444
|
+
item_id: item.id,
|
|
445
|
+
part,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
events.push({
|
|
450
|
+
type: 'response.output_item.done',
|
|
451
|
+
sequence_number: mapper.nextSequence(),
|
|
452
|
+
output_index: outputIndex,
|
|
453
|
+
item,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return events;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function completedEventFromPayload(payload, mapper) {
|
|
460
|
+
return {
|
|
461
|
+
type: payload.status === 'incomplete' ? 'response.incomplete' : 'response.completed',
|
|
462
|
+
sequence_number: mapper.nextSequence(),
|
|
463
|
+
response: payload,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function writeSseEvent(res, event) {
|
|
468
|
+
res.write(serializeResponsesSseEvent(event));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function writeSseDone(res) {
|
|
472
|
+
writeSseEvent(res, { done: true });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function webSearchSseWriter({ res, mapper, outputIndexBySearch, includeSources = false }) {
|
|
476
|
+
return {
|
|
477
|
+
start(search) {
|
|
478
|
+
const outputIndex = mapper.output.length;
|
|
479
|
+
outputIndexBySearch.set(search.id, outputIndex);
|
|
480
|
+
const item = buildWebSearchCallItem(search, { status: 'in_progress', includeSources });
|
|
481
|
+
mapper.output.push(item);
|
|
482
|
+
writeSseEvent(res, {
|
|
483
|
+
type: 'response.output_item.added',
|
|
484
|
+
sequence_number: mapper.nextSequence(),
|
|
485
|
+
output_index: outputIndex,
|
|
486
|
+
item,
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
done(search) {
|
|
490
|
+
const outputIndex = outputIndexBySearch.get(search.id);
|
|
491
|
+
const item = buildWebSearchCallItem(search, { includeSources });
|
|
492
|
+
if (Number.isInteger(outputIndex)) mapper.output[outputIndex] = item;
|
|
493
|
+
writeSseEvent(res, {
|
|
494
|
+
type: 'response.output_item.done',
|
|
495
|
+
sequence_number: mapper.nextSequence(),
|
|
496
|
+
output_index: Number.isInteger(outputIndex) ? outputIndex : mapper.output.indexOf(item),
|
|
497
|
+
item,
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
131
503
|
export function createProxyServer({ config = loadConfig(), sessions = new SessionStore() } = {}) {
|
|
132
504
|
let modelCache = null;
|
|
133
505
|
|
|
@@ -177,9 +549,9 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
177
549
|
normalized.messages = prependMissingAssistantToolMessages(normalized.messages, sessions);
|
|
178
550
|
|
|
179
551
|
const chatRequest = toChatCompletionsRequest(normalized);
|
|
180
|
-
const
|
|
552
|
+
const useWebSearchEmulator = hasTavilyWebSearch(config, normalized);
|
|
553
|
+
let upstreamRequest = toProviderChatCompletionsRequest(chatRequest, config);
|
|
181
554
|
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || normalized.model;
|
|
182
|
-
logDebugPayload(config, upstreamRequest);
|
|
183
555
|
|
|
184
556
|
if (!upstreamRequest.model) {
|
|
185
557
|
sendJson(res, 400, { error: { message: 'Missing model' } });
|
|
@@ -193,6 +565,110 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
193
565
|
const responseId = generateId('resp');
|
|
194
566
|
const nextSession = { id: responseId, history: [] };
|
|
195
567
|
|
|
568
|
+
if (useWebSearchEmulator) {
|
|
569
|
+
const streamWebSearch = Boolean(request.stream);
|
|
570
|
+
let streamMapper = null;
|
|
571
|
+
let streamSearchWriter = null;
|
|
572
|
+
const streamSearchItems = new Set();
|
|
573
|
+
if (streamWebSearch) {
|
|
574
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
575
|
+
streamMapper = new ResponsesStreamMapper({
|
|
576
|
+
responseId,
|
|
577
|
+
model: upstreamRequest.model,
|
|
578
|
+
createdAt,
|
|
579
|
+
previousResponseId,
|
|
580
|
+
normalized,
|
|
581
|
+
emitReasoningSummary: true,
|
|
582
|
+
emitReasoningText: false,
|
|
583
|
+
});
|
|
584
|
+
streamSearchWriter = webSearchSseWriter({
|
|
585
|
+
res,
|
|
586
|
+
mapper: streamMapper,
|
|
587
|
+
outputIndexBySearch: new Map(),
|
|
588
|
+
includeSources: shouldIncludeSearchSources(normalized),
|
|
589
|
+
});
|
|
590
|
+
res.writeHead(200, {
|
|
591
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
592
|
+
'cache-control': 'no-cache, no-transform',
|
|
593
|
+
connection: 'keep-alive',
|
|
594
|
+
'x-accel-buffering': 'no',
|
|
595
|
+
});
|
|
596
|
+
writeSseEvent(res, streamMapper.createdEvent());
|
|
597
|
+
writeSseEvent(res, streamMapper.inProgressEvent());
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const loop = await runWebSearchChatLoop({
|
|
601
|
+
normalized,
|
|
602
|
+
chatRequest,
|
|
603
|
+
config,
|
|
604
|
+
onSearchStart: streamWebSearch
|
|
605
|
+
? (search) => {
|
|
606
|
+
if (search.action && search.auto) return;
|
|
607
|
+
streamSearchItems.add(search);
|
|
608
|
+
streamSearchWriter.start(search);
|
|
609
|
+
}
|
|
610
|
+
: undefined,
|
|
611
|
+
onSearchDone: streamWebSearch
|
|
612
|
+
? (search) => {
|
|
613
|
+
if (!streamSearchItems.has(search)) return;
|
|
614
|
+
streamSearchWriter.done(search);
|
|
615
|
+
}
|
|
616
|
+
: undefined,
|
|
617
|
+
});
|
|
618
|
+
if (!loop.ok) {
|
|
619
|
+
if (streamWebSearch) {
|
|
620
|
+
writeSseEvent(res, {
|
|
621
|
+
type: 'response.failed',
|
|
622
|
+
sequence_number: streamMapper.nextSequence(),
|
|
623
|
+
response: streamMapper.response('failed'),
|
|
624
|
+
});
|
|
625
|
+
writeSseDone(res);
|
|
626
|
+
res.end();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
sendJson(res, loop.status || 500, loop.data);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
upstreamRequest = toProviderChatCompletionsRequest(loop.chatRequest, config);
|
|
633
|
+
if (!upstreamRequest.model) upstreamRequest.model = config.upstreamModel || normalized.model;
|
|
634
|
+
const model = upstreamRequest.model;
|
|
635
|
+
const payload = applyWebSearchOutputCompatibility(convertChatCompletionToResponses({
|
|
636
|
+
completion: loop.completion,
|
|
637
|
+
model,
|
|
638
|
+
previousResponseId,
|
|
639
|
+
normalized,
|
|
640
|
+
responseId,
|
|
641
|
+
}), loop.searches, normalized, loop.openedPages);
|
|
642
|
+
const assistantMessage = assistantMessageFromResponseOutput(payload.output);
|
|
643
|
+
|
|
644
|
+
nextSession.history.push({
|
|
645
|
+
request: normalized,
|
|
646
|
+
chatRequest: loop.chatRequest,
|
|
647
|
+
upstreamRequest,
|
|
648
|
+
inputMessages: currentInputMessages,
|
|
649
|
+
historyMessages: loop.chatRequest.messages.concat(assistantMessage),
|
|
650
|
+
assistantMessage,
|
|
651
|
+
createdAt: Date.now(),
|
|
652
|
+
});
|
|
653
|
+
sessions.set(responseId, nextSession);
|
|
654
|
+
sessions.setConversation(conversationId, responseId);
|
|
655
|
+
|
|
656
|
+
if (!streamWebSearch) {
|
|
657
|
+
sendJson(res, 200, payload);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const event of outputEventsFromPayload(payload, streamMapper, { skipTypes: new Set(['web_search_call']) })) {
|
|
662
|
+
writeSseEvent(res, event);
|
|
663
|
+
}
|
|
664
|
+
writeSseEvent(res, completedEventFromPayload(payload, streamMapper));
|
|
665
|
+
writeSseDone(res);
|
|
666
|
+
res.end();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
logDebugPayload(config, upstreamRequest);
|
|
671
|
+
|
|
196
672
|
const upstreamResponse = await callChatCompletions({
|
|
197
673
|
baseUrl: config.upstreamBaseUrl,
|
|
198
674
|
apiKey: config.upstreamApiKey,
|
|
@@ -335,6 +811,20 @@ export function createProxyServer({ config = loadConfig(), sessions = new Sessio
|
|
|
335
811
|
|
|
336
812
|
sendJson(res, 404, { error: { message: 'Not found' } });
|
|
337
813
|
} catch (error) {
|
|
814
|
+
if (res.headersSent) {
|
|
815
|
+
if (!res.writableEnded) {
|
|
816
|
+
res.write(serializeResponsesSseEvent({
|
|
817
|
+
type: 'response.failed',
|
|
818
|
+
response: {
|
|
819
|
+
status: 'failed',
|
|
820
|
+
error: { message: error.message || 'Internal server error' },
|
|
821
|
+
},
|
|
822
|
+
}));
|
|
823
|
+
res.write(serializeResponsesSseEvent({ done: true }));
|
|
824
|
+
res.end();
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
338
828
|
sendJson(res, 500, { error: { message: error.message || 'Internal server error' } });
|
|
339
829
|
}
|
|
340
830
|
});
|