@galaxy-yearn/codex-deepseek-gateway 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,57 +1,272 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { isObject, safeJsonParse } from './common.js';
4
+
5
+ const SESSION_STORE_VERSION = 4;
6
+ const DEFAULT_MAX_SESSIONS = 500;
7
+ const DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
8
+
1
9
  export class SessionStore {
2
- constructor({ maxToolCallMessages = 1000 } = {}) {
10
+ constructor({
11
+ maxToolCallMessages = 1000,
12
+ maxSessions = DEFAULT_MAX_SESSIONS,
13
+ maxBytes = DEFAULT_MAX_BYTES,
14
+ persistPath = '',
15
+ } = {}) {
3
16
  this.map = new Map();
4
17
  this.toolCallMessages = new Map();
5
18
  this.conversations = new Map();
6
- this.maxToolCallMessages = maxToolCallMessages;
19
+ this.maxToolCallMessages = positiveInteger(maxToolCallMessages, 1000);
20
+ this.maxSessions = positiveInteger(maxSessions, DEFAULT_MAX_SESSIONS);
21
+ this.maxBytes = positiveInteger(maxBytes, DEFAULT_MAX_BYTES);
22
+ this.persistPath = persistPath || '';
23
+ this.lastLoadError = null;
24
+ this.lastPersistError = null;
25
+ this.loadFromDisk();
7
26
  }
8
27
 
9
28
  get(id) {
10
- return this.map.get(id) || null;
29
+ if (!id) return null;
30
+ return this.map.get(String(id)) || null;
11
31
  }
12
32
 
13
33
  set(id, value) {
14
- this.map.set(id, value);
15
- for (const turn of Array.isArray(value?.history) ? value.history : []) {
16
- this.indexAssistantMessage(turn.assistantMessage);
17
- }
34
+ if (!id) return value;
35
+ const session = normalizeSession(id, value);
36
+ this.map.set(String(id), session);
37
+ for (const message of session.messages) this.indexAssistantMessage(message);
38
+ this.pruneSessions();
39
+ this.persist();
18
40
  return value;
19
41
  }
20
42
 
21
43
  getConversation(id) {
22
- const responseId = this.conversations.get(id);
44
+ if (!id) return null;
45
+ const responseId = this.conversations.get(String(id));
23
46
  return responseId ? this.get(responseId) : null;
24
47
  }
25
48
 
26
49
  setConversation(id, responseId) {
27
- if (id && responseId) this.conversations.set(String(id), responseId);
50
+ if (!id || !responseId) return;
51
+ this.conversations.set(String(id), String(responseId));
52
+ this.dropDanglingConversations();
53
+ this.persist();
28
54
  }
29
55
 
30
56
  indexAssistantMessage(message) {
31
- if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls)) return;
57
+ if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls)) return false;
58
+ const shared = cloneAssistantMessage(message);
59
+ let indexed = false;
32
60
  for (const toolCall of message.tool_calls) {
33
61
  if (!toolCall?.id) continue;
34
- this.toolCallMessages.set(toolCall.id, cloneAssistantMessage(message));
62
+ const key = String(toolCall.id);
63
+ this.toolCallMessages.delete(key);
64
+ this.toolCallMessages.set(key, shared);
65
+ indexed = true;
35
66
  while (this.toolCallMessages.size > this.maxToolCallMessages) {
36
67
  const oldestKey = this.toolCallMessages.keys().next().value;
37
68
  this.toolCallMessages.delete(oldestKey);
38
69
  }
39
70
  }
71
+ return indexed;
72
+ }
73
+
74
+ rememberAssistantMessage(message) {
75
+ if (!this.indexAssistantMessage(message)) return false;
76
+ this.persist();
77
+ return true;
40
78
  }
41
79
 
42
80
  getAssistantMessageForToolCall(callId) {
43
- return this.toolCallMessages.get(callId) || null;
81
+ if (!callId) return null;
82
+ const message = this.toolCallMessages.get(String(callId));
83
+ return message ? cloneAssistantMessage(message) : null;
44
84
  }
45
85
 
46
86
  delete(id) {
47
- return this.map.delete(id);
87
+ if (!id) return false;
88
+ const deleted = this.map.delete(String(id));
89
+ if (deleted) {
90
+ this.dropDanglingConversations();
91
+ this.persist();
92
+ }
93
+ return deleted;
48
94
  }
49
95
 
50
96
  clear() {
51
97
  this.map.clear();
52
98
  this.toolCallMessages.clear();
53
99
  this.conversations.clear();
100
+ this.persist();
101
+ }
102
+
103
+ loadFromDisk() {
104
+ if (!this.persistPath || !existsSync(this.persistPath)) return;
105
+ try {
106
+ const parsed = safeJsonParse(readFileSync(this.persistPath, 'utf8').replace(/^/, ''));
107
+ if (!parsed.ok || !isObject(parsed.value)) return;
108
+ this.map.clear();
109
+ this.conversations.clear();
110
+ this.toolCallMessages.clear();
111
+ for (const [id, session] of entriesFrom(parsed.value.sessions)) {
112
+ if (!id || !isObject(session)) continue;
113
+ this.map.set(String(id), normalizeSession(id, session));
114
+ }
115
+ for (const [id, responseId] of entriesFrom(parsed.value.conversations)) {
116
+ if (!id || !responseId) continue;
117
+ const normalizedResponseId = String(responseId);
118
+ if (this.map.has(normalizedResponseId)) this.conversations.set(String(id), normalizedResponseId);
119
+ }
120
+ const messagePool = Array.isArray(parsed.value.assistantMessages) ? parsed.value.assistantMessages : [];
121
+ const storedToolCallMessages = entriesFrom(parsed.value.toolCallMessages);
122
+ const sharedMessages = new Map();
123
+ for (const [callId, stored] of storedToolCallMessages) {
124
+ if (!callId) continue;
125
+ const message = resolveStoredToolCallMessage(stored, messagePool);
126
+ if (!message) continue;
127
+ const fingerprint = JSON.stringify(message);
128
+ if (!sharedMessages.has(fingerprint)) sharedMessages.set(fingerprint, message);
129
+ this.toolCallMessages.set(String(callId), sharedMessages.get(fingerprint));
130
+ while (this.toolCallMessages.size > this.maxToolCallMessages) {
131
+ this.toolCallMessages.delete(this.toolCallMessages.keys().next().value);
132
+ }
133
+ }
134
+ this.pruneSessions();
135
+ if (!storedToolCallMessages.length) this.rebuildToolCallIndex();
136
+ } catch (error) {
137
+ this.lastLoadError = error;
138
+ this.map.clear();
139
+ this.conversations.clear();
140
+ this.toolCallMessages.clear();
141
+ }
142
+ }
143
+
144
+ persist() {
145
+ if (!this.persistPath) return;
146
+ try {
147
+ mkdirSync(dirname(this.persistPath), { recursive: true });
148
+ let serialized = this.serializePayload();
149
+ while (serialized.length > this.maxBytes && this.map.size > 1) {
150
+ this.map.delete(this.map.keys().next().value);
151
+ this.dropDanglingConversations();
152
+ serialized = this.serializePayload();
153
+ }
154
+ while (serialized.length > this.maxBytes && this.toolCallMessages.size > 0) {
155
+ this.toolCallMessages.delete(this.toolCallMessages.keys().next().value);
156
+ serialized = this.serializePayload();
157
+ }
158
+ const tmpPath = `${this.persistPath}.tmp-${process.pid}-${Date.now()}`;
159
+ writeFileSync(tmpPath, serialized, 'utf8');
160
+ renameSync(tmpPath, this.persistPath);
161
+ this.lastPersistError = null;
162
+ } catch (error) {
163
+ this.lastPersistError = error;
164
+ }
165
+ }
166
+
167
+ serializePayload() {
168
+ return `${JSON.stringify(this.persistPayload(), null, 2)}\n`;
169
+ }
170
+
171
+ persistPayload() {
172
+ const assistantMessages = [];
173
+ const poolIndexes = new Map();
174
+ const toolCallMessages = [];
175
+ for (const [callId, message] of this.toolCallMessages.entries()) {
176
+ const fingerprint = JSON.stringify(message);
177
+ let index = poolIndexes.get(fingerprint);
178
+ if (index === undefined) {
179
+ index = assistantMessages.length;
180
+ poolIndexes.set(fingerprint, index);
181
+ assistantMessages.push(message);
182
+ }
183
+ toolCallMessages.push([callId, index]);
184
+ }
185
+ return {
186
+ version: SESSION_STORE_VERSION,
187
+ sessions: Array.from(this.map.entries()),
188
+ conversations: Array.from(this.conversations.entries()),
189
+ assistantMessages,
190
+ toolCallMessages,
191
+ };
192
+ }
193
+
194
+ pruneSessions() {
195
+ while (this.map.size > this.maxSessions) {
196
+ const oldestKey = this.map.keys().next().value;
197
+ this.map.delete(oldestKey);
198
+ }
199
+ this.dropDanglingConversations();
54
200
  }
201
+
202
+ dropDanglingConversations() {
203
+ for (const [conversationId, responseId] of this.conversations.entries()) {
204
+ if (!this.map.has(responseId)) this.conversations.delete(conversationId);
205
+ }
206
+ }
207
+
208
+ rebuildToolCallIndex() {
209
+ this.toolCallMessages.clear();
210
+ for (const session of this.map.values()) {
211
+ for (const message of Array.isArray(session?.messages) ? session.messages : []) {
212
+ this.indexAssistantMessage(message);
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ function normalizeSession(id, value) {
219
+ const messages = Array.isArray(value?.messages) ? value.messages : legacyHistoryMessages(value?.history);
220
+ return { id: String(value?.id || id), messages: stripLeadingSystemMessages(messages) };
221
+ }
222
+
223
+ function legacyHistoryMessages(history) {
224
+ const turns = Array.isArray(history) ? history : [];
225
+ if (!turns.length) return [];
226
+ if (turns.every((turn) => Array.isArray(turn?.historyMessages))) {
227
+ return turns[turns.length - 1].historyMessages.slice();
228
+ }
229
+ const messages = [];
230
+ for (const turn of turns) {
231
+ if (Array.isArray(turn?.historyMessages)) {
232
+ messages.push(...turn.historyMessages);
233
+ continue;
234
+ }
235
+ if (Array.isArray(turn?.inputMessages)) {
236
+ messages.push(...turn.inputMessages);
237
+ } else if (Array.isArray(turn?.chatRequest?.messages)) {
238
+ messages.push(...turn.chatRequest.messages);
239
+ }
240
+ if (turn?.assistantMessage) messages.push(turn.assistantMessage);
241
+ }
242
+ return messages;
243
+ }
244
+
245
+ function stripLeadingSystemMessages(messages) {
246
+ let start = 0;
247
+ while (start < messages.length && (messages[start]?.role === 'system' || messages[start]?.role === 'developer')) {
248
+ start += 1;
249
+ }
250
+ return start ? messages.slice(start) : messages;
251
+ }
252
+
253
+ function entriesFrom(value) {
254
+ if (Array.isArray(value)) return value;
255
+ if (isObject(value)) return Object.entries(value);
256
+ return [];
257
+ }
258
+
259
+ function resolveStoredToolCallMessage(stored, messagePool) {
260
+ if (isObject(stored)) return stored;
261
+ if (Number.isInteger(stored) && stored >= 0 && stored < messagePool.length && isObject(messagePool[stored])) {
262
+ return messagePool[stored];
263
+ }
264
+ return null;
265
+ }
266
+
267
+ function positiveInteger(value, fallback) {
268
+ const number = Number(value);
269
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
55
270
  }
56
271
 
57
272
  function cloneAssistantMessage(message) {
package/src/tavily.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isObject, joinUrl, safeJsonParse } from './common.js';
1
+ import { isObject, joinUrl, parseBoolean, safeJsonParse } from './common.js';
2
2
 
3
3
  const DEFAULT_MAX_RESULTS = 5;
4
4
  const HARD_MAX_RESULTS = 10;
@@ -14,15 +14,6 @@ function clampInteger(value, min, max, fallback) {
14
14
  return Math.min(max, Math.max(min, Math.trunc(number)));
15
15
  }
16
16
 
17
- function booleanValue(value, fallback) {
18
- if (typeof value === 'boolean') return value;
19
- if (value == null || value === '') return fallback;
20
- const normalized = String(value).trim().toLowerCase();
21
- if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
22
- if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
23
- return fallback;
24
- }
25
-
26
17
  function cleanText(value, maxChars = DEFAULT_SNIPPET_CHARS) {
27
18
  if (value == null) return '';
28
19
  const text = String(value)
@@ -111,7 +102,7 @@ export function buildTavilySearchRequest(args = {}, config = {}) {
111
102
  query: normalized.query,
112
103
  search_depth: normalized.searchDepth,
113
104
  max_results: normalized.maxResults,
114
- include_answer: booleanValue(config.tavilyIncludeAnswer, true),
105
+ include_answer: parseBoolean(config.tavilyIncludeAnswer, true),
115
106
  include_raw_content: false,
116
107
  };
117
108
  if (normalized.topic) body.topic = normalized.topic;
package/src/upstream.js CHANGED
@@ -82,9 +82,11 @@ export async function relayChatCompletionsResponse({
82
82
  onStreamChunk,
83
83
  passThrough = true,
84
84
  writeHeaders = true,
85
+ endResponse = true,
85
86
  }) {
86
87
  const contentType = upstreamResponse.headers.get('content-type') || '';
87
88
  const isStream = contentType.includes('text/event-stream');
89
+ const writable = () => !res.destroyed && !res.writableEnded;
88
90
 
89
91
  if (!isStream) {
90
92
  const data = await readJsonResponse(upstreamResponse);
@@ -92,18 +94,18 @@ export async function relayChatCompletionsResponse({
92
94
  onStreamChunk({ data });
93
95
  onStreamChunk({ done: true });
94
96
  }
95
- if (passThrough) {
97
+ if (passThrough && writable()) {
96
98
  res.writeHead(upstreamResponse.status, {
97
99
  'content-type': 'application/json; charset=utf-8',
98
100
  });
99
101
  res.end(JSON.stringify(data));
100
- } else {
102
+ } else if (endResponse && writable()) {
101
103
  res.end();
102
104
  }
103
105
  return;
104
106
  }
105
107
 
106
- if (writeHeaders) {
108
+ if (writeHeaders && writable()) {
107
109
  res.writeHead(upstreamResponse.status, {
108
110
  'content-type': 'text/event-stream; charset=utf-8',
109
111
  'cache-control': 'no-cache, no-transform',
@@ -114,17 +116,17 @@ export async function relayChatCompletionsResponse({
114
116
 
115
117
  const reader = upstreamResponse.body?.getReader?.();
116
118
  if (!reader) {
117
- res.end();
119
+ if (endResponse && writable()) res.end();
118
120
  return;
119
121
  }
120
122
 
121
123
  const parser = new SseParser();
122
124
  let sawDone = false;
123
- const emitDone = () => {
125
+ const emitDone = ({ eof = false } = {}) => {
124
126
  if (sawDone) return;
125
127
  sawDone = true;
126
- if (typeof onStreamChunk === 'function') onStreamChunk({ done: true });
127
- if (passThrough) res.write(`data: [DONE]\n\n`);
128
+ if (typeof onStreamChunk === 'function') onStreamChunk(eof ? { done: true, eof: true } : { done: true });
129
+ if (passThrough && writable()) res.write(`data: [DONE]\n\n`);
128
130
  };
129
131
 
130
132
  while (true) {
@@ -134,11 +136,11 @@ export async function relayChatCompletionsResponse({
134
136
  for (const event of events) {
135
137
  if (event.done) {
136
138
  emitDone();
137
- res.end();
139
+ if (endResponse && writable()) res.end();
138
140
  return;
139
141
  }
140
142
  if (typeof onStreamChunk === 'function') onStreamChunk(event);
141
- if (passThrough) res.write(`data: ${event.data}\n\n`);
143
+ if (passThrough && writable()) res.write(`data: ${event.data}\n\n`);
142
144
  }
143
145
  }
144
146
 
@@ -148,8 +150,8 @@ export async function relayChatCompletionsResponse({
148
150
  break;
149
151
  }
150
152
  if (typeof onStreamChunk === 'function') onStreamChunk(event);
151
- if (passThrough) res.write(`data: ${event.data}\n\n`);
153
+ if (passThrough && writable()) res.write(`data: ${event.data}\n\n`);
152
154
  }
153
- emitDone();
154
- res.end();
155
+ emitDone({ eof: true });
156
+ if (endResponse && writable()) res.end();
155
157
  }
@@ -1,4 +1,4 @@
1
- import { generateId, isObject, safeJsonParse, toText } from './common.js';
1
+ import { generateId, isObject, parseJsonObject, toText } from './common.js';
2
2
  import { callFirecrawlScrape } from './firecrawl.js';
3
3
  import { callTavilySearch, formatTavilySearchResult } from './tavily.js';
4
4
 
@@ -14,7 +14,7 @@ const INTERNAL_WEB_TOOL_NAMES = new Set([
14
14
  INTERNAL_WEB_OPEN_PAGE_TOOL,
15
15
  INTERNAL_WEB_FIND_IN_PAGE_TOOL,
16
16
  ]);
17
- const MAX_SEARCH_ROUNDS = 20;
17
+ const MAX_SEARCH_ROUNDS = 40;
18
18
 
19
19
  const WEB_SEARCH_INSTRUCTIONS = [
20
20
  'For live web information, use tavily_search.',
@@ -135,13 +135,6 @@ function clone(value) {
135
135
  return value == null ? value : JSON.parse(JSON.stringify(value));
136
136
  }
137
137
 
138
- function parseJsonObject(text) {
139
- if (isObject(text)) return text;
140
- if (typeof text !== 'string' || !text.trim()) return {};
141
- const parsed = safeJsonParse(text);
142
- return parsed.ok && isObject(parsed.value) ? parsed.value : {};
143
- }
144
-
145
138
  function webSearchToolOptions(tools) {
146
139
  const options = {};
147
140
  for (const tool of Array.isArray(tools) ? tools : []) {
@@ -395,7 +388,7 @@ export function knownExternalToolCallsCompletion(completion, tools) {
395
388
 
396
389
  export function maxWebSearchRounds(config = {}) {
397
390
  const value = Number(config.tavilyMaxSearchRounds);
398
- if (!Number.isFinite(value)) return 10;
391
+ if (!Number.isFinite(value)) return 20;
399
392
  return Math.min(MAX_SEARCH_ROUNDS, Math.max(0, Math.trunc(value)));
400
393
  }
401
394
 
@@ -683,6 +676,14 @@ function buildAnnotations(text, searches = [], openedPages = []) {
683
676
  return annotations.sort((a, b) => a.start_index - b.start_index);
684
677
  }
685
678
 
679
+ export function annotateMessagePartWithWebCitations(part, searches = [], openedPages = []) {
680
+ if (!part || part.type !== 'output_text' || typeof part.text !== 'string') return;
681
+ if (!(searches || []).length && !(openedPages || []).length) return;
682
+ const annotations = buildAnnotations(part.text, searches, openedPages);
683
+ if (!annotations.length) return;
684
+ part.annotations = [...(Array.isArray(part.annotations) ? part.annotations : []), ...annotations];
685
+ }
686
+
686
687
  export function applyWebSearchOutputCompatibility(payload, searches = [], normalized = payload?.normalized, openedPages = []) {
687
688
  if (!payload || !Array.isArray(payload.output)) return payload;
688
689
  const explicitOpenedPages = (openedPages || []).filter((page) => !page.auto);
@@ -732,7 +733,7 @@ export function applyWebSearchOutputCompatibility(payload, searches = [], normal
732
733
  }
733
734
  payload.output = output;
734
735
  payload.output_text = output
735
- .filter((item) => item?.type === 'message')
736
+ .filter((item) => item?.type === 'message' && item.phase !== 'commentary')
736
737
  .flatMap((item) => Array.isArray(item.content) ? item.content : [])
737
738
  .filter((part) => part?.type === 'output_text' && typeof part.text === 'string')
738
739
  .map((part) => part.text)
@@ -0,0 +1,72 @@
1
+ {
2
+ "version": 4,
3
+ "sessions": [
4
+ [
5
+ "resp_0example1234567890abcdef",
6
+ {
7
+ "id": "resp_0example1234567890abcdef",
8
+ "messages": [
9
+ { "role": "user", "content": "Find the config loader and summarize it." },
10
+ {
11
+ "role": "assistant",
12
+ "content": "",
13
+ "reasoning_content": "Raw DeepSeek thinking text, kept verbatim so thinking-mode tool turns can be replayed upstream.",
14
+ "tool_calls": [
15
+ {
16
+ "id": "call_example0001",
17
+ "type": "function",
18
+ "function": {
19
+ "name": "shell_command",
20
+ "arguments": "{\"command\":\"rg --files -g 'config*'\"}"
21
+ }
22
+ },
23
+ {
24
+ "id": "call_example0002",
25
+ "type": "function",
26
+ "function": {
27
+ "name": "shell_command",
28
+ "arguments": "{\"command\":\"rg -n 'loadConfig' src\"}"
29
+ }
30
+ }
31
+ ]
32
+ },
33
+ { "role": "tool", "tool_call_id": "call_example0001", "content": "src/config.js" },
34
+ { "role": "tool", "tool_call_id": "call_example0002", "content": "src/config.js:12" },
35
+ { "role": "assistant", "content": "src/config.js merges env vars and gateway.local.json into one config object." }
36
+ ]
37
+ }
38
+ ]
39
+ ],
40
+ "conversations": [
41
+ ["conv_example", "resp_0example1234567890abcdef"]
42
+ ],
43
+ "assistantMessages": [
44
+ {
45
+ "role": "assistant",
46
+ "content": "",
47
+ "reasoning_content": "Raw DeepSeek thinking text, kept verbatim so thinking-mode tool turns can be replayed upstream.",
48
+ "tool_calls": [
49
+ {
50
+ "id": "call_example0001",
51
+ "type": "function",
52
+ "function": {
53
+ "name": "shell_command",
54
+ "arguments": "{\"command\":\"rg --files -g 'config*'\"}"
55
+ }
56
+ },
57
+ {
58
+ "id": "call_example0002",
59
+ "type": "function",
60
+ "function": {
61
+ "name": "shell_command",
62
+ "arguments": "{\"command\":\"rg -n 'loadConfig' src\"}"
63
+ }
64
+ }
65
+ ]
66
+ }
67
+ ],
68
+ "toolCallMessages": [
69
+ ["call_example0001", 0],
70
+ ["call_example0002", 0]
71
+ ]
72
+ }