@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/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 upstreamRequest = toProviderChatCompletionsRequest(chatRequest, config);
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
  });