@galaxy-yearn/codex-deepseek-gateway 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,12 +21,16 @@ import {
21
21
  withPickerScreen,
22
22
  } from './codex-launch.js';
23
23
 
24
- const TIME_WIDTH = 10;
24
+ const TIME_WIDTH = 11;
25
25
  const PROVIDER_WIDTH = 17;
26
- const ID_WIDTH = 36;
27
- const TITLE_WIDTH = 16;
26
+ const TITLE_WIDTH = 32;
28
27
  const TABLE_INDENT = ' ';
29
28
  const COLUMN_GAP = ' ';
29
+ const MUTED_TEXT = '\x1b[90m';
30
+ const RESET_STYLE = '\x1b[0m';
31
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
32
+ const EXTENDED_PICTOGRAPHIC = /\p{Extended_Pictographic}/u;
33
+ const MARK = /\p{Mark}/u;
30
34
  export const DEFAULT_SESSION_LIMIT = 15;
31
35
 
32
36
  function print(message = '') {
@@ -136,12 +140,60 @@ function firstUserPreview(lines) {
136
140
  return '';
137
141
  }
138
142
 
143
+ function isWideCodePoint(codePoint) {
144
+ return codePoint >= 0x1100 && (
145
+ codePoint <= 0x115f
146
+ || codePoint === 0x2329
147
+ || codePoint === 0x232a
148
+ || (codePoint >= 0x2e80 && codePoint <= 0x303e)
149
+ || (codePoint >= 0x3040 && codePoint <= 0xa4cf)
150
+ || (codePoint >= 0xac00 && codePoint <= 0xd7a3)
151
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
152
+ || (codePoint >= 0xfe10 && codePoint <= 0xfe19)
153
+ || (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
154
+ || (codePoint >= 0xff00 && codePoint <= 0xff60)
155
+ || (codePoint >= 0xffe0 && codePoint <= 0xffe6)
156
+ || (codePoint >= 0x1b000 && codePoint <= 0x1b2ff)
157
+ || (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff)
158
+ || (codePoint >= 0x1f200 && codePoint <= 0x1f251)
159
+ || (codePoint >= 0x20000 && codePoint <= 0x3fffd)
160
+ );
161
+ }
162
+
163
+ function graphemeWidth(grapheme) {
164
+ const characters = [...grapheme];
165
+ const visible = characters.filter((character) => {
166
+ const codePoint = character.codePointAt(0);
167
+ return codePoint !== 0x200d && codePoint !== 0xfe0e && codePoint !== 0xfe0f && !MARK.test(character);
168
+ });
169
+ if (!visible.length) return 0;
170
+ return characters.some((character) => isWideCodePoint(character.codePointAt(0)) || EXTENDED_PICTOGRAPHIC.test(character)) ? 2 : 1;
171
+ }
172
+
173
+ function displayWidth(text) {
174
+ let width = 0;
175
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(String(text))) width += graphemeWidth(segment);
176
+ return width;
177
+ }
178
+
139
179
  function truncate(text, max) {
140
- return text.length > max ? `${text.slice(0, max - 1)}...` : text;
180
+ const value = String(text);
181
+ if (displayWidth(value) <= max) return value;
182
+ const contentWidth = max - 3;
183
+ let result = '';
184
+ let width = 0;
185
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(value)) {
186
+ const nextWidth = graphemeWidth(segment);
187
+ if (width + nextWidth > contentWidth) break;
188
+ result += segment;
189
+ width += nextWidth;
190
+ }
191
+ return `${result}...`;
141
192
  }
142
193
 
143
194
  function pad(text, width) {
144
- return truncate(String(text || ''), width).padEnd(width, ' ');
195
+ const value = truncate(String(text || ''), width);
196
+ return `${value}${' '.repeat(width - displayWidth(value))}`;
145
197
  }
146
198
 
147
199
  function formatTime(value) {
@@ -190,11 +242,14 @@ function printSessions(sessionsList, options, context) {
190
242
  print(`Codex sessions ${options.all ? `under ${context.codexHome}` : `for project ${context.projectRoot}`}\n`);
191
243
  print(`Target: ${context.provider} / ${context.model} / ${context.reasoningEffort}\n\n`);
192
244
  if (!listed.length) {
193
- print('No matching sessions found.\n');
245
+ print(' No matching sessions found.\n');
194
246
  return;
195
247
  }
196
- print(`${TABLE_INDENT}${sessionHeader()}\n`);
197
- print(`${TABLE_INDENT}${'-'.repeat(sessionHeader().length)}\n`);
248
+ const header = sessionHeader();
249
+ const divider = ''.repeat(displayWidth(header));
250
+ const styledDivider = process.stdout.isTTY ? `${MUTED_TEXT}${divider}${RESET_STYLE}` : divider;
251
+ print(`${TABLE_INDENT}${header}\n`);
252
+ print(`${TABLE_INDENT}${styledDivider}\n`);
198
253
  listed.forEach((session, index) => {
199
254
  print(`${String(index + 1).padStart(2, ' ')} ${sessionRow(session)}\n`);
200
255
  if (options.all) print(` cwd: ${session.cwd || '(unknown cwd)'}\n`);
@@ -204,11 +259,11 @@ function printSessions(sessionsList, options, context) {
204
259
  }
205
260
 
206
261
  function sessionHeader() {
207
- return `${pad('Date', TIME_WIDTH)}${COLUMN_GAP}${pad('Provider', PROVIDER_WIDTH)}${COLUMN_GAP}${pad('Session ID', ID_WIDTH)}${COLUMN_GAP}Title`;
262
+ return `${pad('Date', TIME_WIDTH)}${COLUMN_GAP}${pad('Provider', PROVIDER_WIDTH)}${COLUMN_GAP}${pad('Title', TITLE_WIDTH)}`;
208
263
  }
209
264
 
210
265
  function sessionRow(session) {
211
- return `${pad(formatTime(session.updatedAt), TIME_WIDTH)}${COLUMN_GAP}${pad(session.provider || '(unknown)', PROVIDER_WIDTH)}${COLUMN_GAP}${session.id}${COLUMN_GAP}${truncate(session.title, TITLE_WIDTH)}`;
266
+ return `${pad(formatTime(session.updatedAt), TIME_WIDTH)}${COLUMN_GAP}${pad(session.provider || '(unknown)', PROVIDER_WIDTH)}${COLUMN_GAP}${pad(session.title, TITLE_WIDTH)}`;
212
267
  }
213
268
 
214
269
  function sessionRows(sessionsList) {
@@ -238,7 +293,7 @@ async function chooseSessionFlow(allSessions, context, limit) {
238
293
  {
239
294
  windowSize,
240
295
  emptyMessage: 'No matching sessions found.',
241
- shortcuts: [{ key: 'n', action: 'new', label: 'N new' }],
296
+ shortcuts: [{ key: 'n', displayKey: 'N', action: 'new', label: 'new' }],
242
297
  },
243
298
  );
244
299
  if (result.action === 'cancel') return null;
@@ -247,8 +302,9 @@ async function chooseSessionFlow(allSessions, context, limit) {
247
302
  step = 'model';
248
303
  } else if (step === 'model') {
249
304
  const action = await pickModel(context);
250
- if (action !== 'select') return null;
251
- step = 'reasoning';
305
+ if (action === 'cancel') return null;
306
+ if (action === 'back') step = 'session';
307
+ else step = 'reasoning';
252
308
  } else if (step === 'reasoning') {
253
309
  const action = await pickReasoning(context);
254
310
  if (action === 'cancel') return null;
package/src/common.js CHANGED
@@ -9,7 +9,7 @@ export function generateId(prefix) {
9
9
  }
10
10
 
11
11
  export function normalizeRole(role, provider = 'generic') {
12
- if (role === 'developer') return 'system';
12
+ if (provider === 'deepseek' && role === 'developer') return 'system';
13
13
  return role || 'user';
14
14
  }
15
15
 
@@ -63,10 +63,10 @@ export function mapDeepSeekReasoningEffort(effort) {
63
63
  if (!effort) return undefined;
64
64
  const normalized = String(effort).toLowerCase().replaceAll('_', '-');
65
65
  if (normalized === 'low') return undefined;
66
- if (normalized === 'none' || normalized === 'disabled' || normalized === 'off') return undefined;
66
+ if (normalized === 'none' || normalized === 'disabled' || normalized === 'off' || normalized === 'false') return undefined;
67
67
  if (normalized === 'xhigh' || normalized === 'max') return 'max';
68
68
  if (normalized === 'medium' || normalized === 'high') return 'high';
69
- return normalized;
69
+ return undefined;
70
70
  }
71
71
 
72
72
  export function joinUrl(baseUrl, path) {
@@ -139,3 +139,18 @@ export function safeJsonParse(text) {
139
139
  return { ok: false, error };
140
140
  }
141
141
  }
142
+
143
+ export function parseJsonObject(value, { source = 'JSON value', throwOnInvalid = false } = {}) {
144
+ if (isObject(value)) return value;
145
+ if (!value || typeof value !== 'string') {
146
+ if (throwOnInvalid && value) throw new Error(`${source} must be a JSON object`);
147
+ return {};
148
+ }
149
+ const parsed = safeJsonParse(value);
150
+ if (parsed.ok && isObject(parsed.value)) return parsed.value;
151
+ if (throwOnInvalid) {
152
+ if (parsed.ok) throw new Error(`${source} must be a JSON object`);
153
+ throw parsed.error;
154
+ }
155
+ return {};
156
+ }
package/src/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { dirname, resolve } from 'node:path';
1
2
  import { loadModelAliases } from './model-map.js';
2
3
  import { mergeLocalConfig, readLocalConfigFile, resolveLocalConfigPath } from './local-config.js';
3
4
  import { parseBoolean, parseList } from './common.js';
@@ -5,8 +6,10 @@ import { readCodexConfig } from './codex-config.js';
5
6
  import { normalizePromptLanguage } from './prompt-language.js';
6
7
 
7
8
  export function loadConfig(env = process.env) {
9
+ const localConfigPath = resolveLocalConfigPath(env);
10
+ const stateDir = resolve(dirname(localConfigPath), '..', 'state');
8
11
  const mergedEnv = mergeLocalConfig(env);
9
- const localConfig = readLocalConfigFile(resolveLocalConfigPath(env));
12
+ const localConfig = readLocalConfigFile(localConfigPath);
10
13
  const codexConfig = readCodexConfig(mergedEnv);
11
14
  return {
12
15
  port: Number(mergedEnv.PORT || 3000),
@@ -16,18 +19,26 @@ export function loadConfig(env = process.env) {
16
19
  upstreamModel: mergedEnv.UPSTREAM_MODEL || '',
17
20
  upstreamProvider: mergedEnv.UPSTREAM_PROVIDER || 'deepseek',
18
21
  upstreamTimeoutMs: Number(mergedEnv.UPSTREAM_TIMEOUT_MS || 120000),
22
+ upstreamMaxTokens: Number(mergedEnv.UPSTREAM_MAX_TOKENS || mergedEnv.DEEPSEEK_MAX_TOKENS || 0),
19
23
  upstreamModels: parseList(mergedEnv.UPSTREAM_MODELS || mergedEnv.MODELS),
20
24
  fetchUpstreamModels: parseBoolean(mergedEnv.FETCH_UPSTREAM_MODELS, false),
21
25
  modelsTimeoutMs: Number(mergedEnv.MODELS_TIMEOUT_MS || 5000),
22
26
  modelsCacheMs: Number(mergedEnv.MODELS_CACHE_MS || 60000),
23
27
  proxyApiKey: mergedEnv.PROXY_API_KEY || '',
24
28
  debugPayload: parseBoolean(mergedEnv.DEBUG_PAYLOAD || mergedEnv.DEBUG_DEEPSEEK_PAYLOAD, false),
29
+ debugPayloadLogPath: mergedEnv.DEBUG_PAYLOAD_LOG_PATH || 'gateway.debug.log',
30
+ reasoningCacheEnabled: parseBoolean(mergedEnv.REASONING_CACHE_ENABLED, true),
31
+ reasoningCachePath: mergedEnv.REASONING_CACHE_PATH || resolve(stateDir, 'reasoning-cache.jsonl'),
32
+ reasoningCacheMaxMessages: Number(mergedEnv.REASONING_CACHE_MAX_MESSAGES || 1000),
33
+ reasoningCacheMaxBytes: Number(mergedEnv.REASONING_CACHE_MAX_BYTES || 0),
34
+ legacyReasoningCachePath: resolve(stateDir, 'sessions.json'),
35
+ debugPayloadLogMaxBytes: Number(mergedEnv.DEBUG_PAYLOAD_LOG_MAX_BYTES || 0),
25
36
  tavilyWebSearchEnabled: parseBoolean(mergedEnv.TAVILY_WEB_SEARCH_ENABLED ?? mergedEnv.ENABLE_TAVILY_WEB_SEARCH, false),
26
37
  tavilyApiKey: mergedEnv.TAVILY_API_KEY || '',
27
38
  tavilyBaseUrl: mergedEnv.TAVILY_BASE_URL || 'https://api.tavily.com',
28
39
  tavilySearchDepth: mergedEnv.TAVILY_SEARCH_DEPTH || 'basic',
29
40
  tavilyMaxResults: Number(mergedEnv.TAVILY_MAX_RESULTS || 5),
30
- tavilyMaxSearchRounds: Number(mergedEnv.TAVILY_MAX_SEARCH_ROUNDS || 10),
41
+ tavilyMaxSearchRounds: Number(mergedEnv.TAVILY_MAX_SEARCH_ROUNDS || 20),
31
42
  tavilyTimeoutMs: Number(mergedEnv.TAVILY_TIMEOUT_MS || 15000),
32
43
  tavilySnippetChars: Number(mergedEnv.TAVILY_SNIPPET_CHARS || 650),
33
44
  tavilyResultMaxChars: Number(mergedEnv.TAVILY_RESULT_MAX_CHARS || 6000),
package/src/firecrawl.js CHANGED
@@ -1,4 +1,5 @@
1
- import { isObject, joinUrl, safeJsonParse } from './common.js';
1
+ import { isIP } from 'node:net';
2
+ import { isObject, joinUrl, parseBoolean, safeJsonParse } from './common.js';
2
3
 
3
4
  const DEFAULT_TOTAL_CHARS = 12000;
4
5
  const DEFAULT_PAGE_CHARS = 5000;
@@ -26,15 +27,6 @@ function clampInteger(value, min, max, fallback) {
26
27
  return Math.min(max, Math.max(min, Math.trunc(number)));
27
28
  }
28
29
 
29
- function booleanValue(value, fallback) {
30
- if (typeof value === 'boolean') return value;
31
- if (value == null || value === '') return fallback;
32
- const normalized = String(value).trim().toLowerCase();
33
- if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
34
- if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
35
- return fallback;
36
- }
37
-
38
30
  function cleanText(value, maxChars = DEFAULT_PAGE_CHARS) {
39
31
  if (value == null) return '';
40
32
  const text = String(value)
@@ -79,16 +71,20 @@ function isPrivateIpv4(hostname) {
79
71
  return PRIVATE_IPV4_RANGES.some(([start, end]) => number >= start && number <= end);
80
72
  }
81
73
 
74
+ function isPrivateIpv6(hostname) {
75
+ const host = normalizedHostname(hostname);
76
+ if (isIP(host) !== 6) return false;
77
+ return host === '::1' || host === '0:0:0:0:0:0:0:1' || host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd');
78
+ }
79
+
82
80
  function isBlockedHostname(hostname, config = {}) {
83
81
  const host = normalizedHostname(hostname);
84
82
  if (!host) return true;
85
- const allowLocal = booleanValue(config.firecrawlAllowPrivateUrls, false);
83
+ const allowLocal = parseBoolean(config.firecrawlAllowPrivateUrls, false);
86
84
  if (allowLocal) return false;
87
85
  if (host === 'localhost' || host.endsWith('.localhost')) return true;
88
86
  if (host === 'metadata.google.internal') return true;
89
- if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;
90
- if (host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd')) return true;
91
- return isPrivateIpv4(host);
87
+ return isPrivateIpv4(host) || isPrivateIpv6(host);
92
88
  }
93
89
 
94
90
  export function normalizeFirecrawlUrl(value, config = {}) {
@@ -127,10 +123,10 @@ function normalizeFormats(args = {}, config = {}) {
127
123
  .map((format) => format.toLowerCase())
128
124
  .filter((format) => ['markdown', 'links', 'summary', 'changeTracking'].includes(format));
129
125
  if (!formats.includes('markdown')) formats.unshift('markdown');
130
- if (booleanValue(args.include_links ?? args.includeLinks ?? config.firecrawlIncludeLinks, true) && !formats.includes('links')) {
126
+ if (parseBoolean(args.include_links ?? args.includeLinks ?? config.firecrawlIncludeLinks, true) && !formats.includes('links')) {
131
127
  formats.push('links');
132
128
  }
133
- if (booleanValue(args.include_summary ?? args.includeSummary ?? config.firecrawlIncludeSummary, false) && !formats.includes('summary')) {
129
+ if (parseBoolean(args.include_summary ?? args.includeSummary ?? config.firecrawlIncludeSummary, false) && !formats.includes('summary')) {
134
130
  formats.push('summary');
135
131
  }
136
132
  return [...new Set(formats)];
@@ -163,14 +159,14 @@ export function buildFirecrawlScrapeRequest(args = {}, config = {}) {
163
159
  const body = {
164
160
  url: normalizedUrl.url,
165
161
  formats: normalizeFormats(source, config),
166
- onlyMainContent: booleanValue(source.only_main_content ?? source.onlyMainContent ?? config.firecrawlOnlyMainContent, true),
167
- removeBase64Images: booleanValue(source.remove_base64_images ?? source.removeBase64Images ?? config.firecrawlRemoveBase64Images, true),
162
+ onlyMainContent: parseBoolean(source.only_main_content ?? source.onlyMainContent ?? config.firecrawlOnlyMainContent, true),
163
+ removeBase64Images: parseBoolean(source.remove_base64_images ?? source.removeBase64Images ?? config.firecrawlRemoveBase64Images, true),
168
164
  waitFor: clampInteger(source.wait_for ?? source.waitFor ?? config.firecrawlWaitForMs, 0, 10000, 0),
169
165
  timeout: clampInteger(source.timeout ?? config.firecrawlTimeoutMs, 1000, 120000, 30000),
170
166
  };
171
167
  if (!body.waitFor) delete body.waitFor;
172
168
  const mobile = source.mobile ?? config.firecrawlMobile;
173
- if (mobile !== undefined && mobile !== '') body.mobile = booleanValue(mobile, false);
169
+ if (mobile !== undefined && mobile !== '') body.mobile = parseBoolean(mobile, false);
174
170
 
175
171
  return {
176
172
  ok: true,
package/src/model-map.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
- import { isObject, mapDeepSeekReasoningEffort } from './common.js';
3
+ import { isObject, mapDeepSeekReasoningEffort, parseJsonObject } from './common.js';
4
4
 
5
5
  const DEEPSEEK_V4_MODELS = ['deepseek-v4-flash', 'deepseek-v4-pro'];
6
6
  const DEPRECATED_MODEL_PATTERN = /^deepseek-(?:chat|reasoner)$/;
@@ -15,25 +15,16 @@ function deepseekV4Aliases() {
15
15
 
16
16
  export const DEFAULT_MODEL_ALIASES = deepseekV4Aliases();
17
17
 
18
- function parseJsonObject(value, source) {
19
- if (!value) return {};
20
- const parsed = JSON.parse(value);
21
- if (!isObject(parsed)) {
22
- throw new Error(`${source} must be a JSON object`);
23
- }
24
- return parsed;
25
- }
26
-
27
18
  function readJsonFile(filePath) {
28
19
  if (!filePath || !existsSync(filePath)) return {};
29
- return parseJsonObject(readFileSync(filePath, 'utf8'), filePath);
20
+ return parseJsonObject(readFileSync(filePath, 'utf8'), { source: filePath, throwOnInvalid: true });
30
21
  }
31
22
 
32
23
  export function loadModelAliases(env = process.env, cwd = process.cwd()) {
33
24
  const defaultFile = resolve(cwd, 'config', 'model-aliases.json');
34
25
  const filePath = env.MODEL_ALIASES_FILE ? resolve(cwd, env.MODEL_ALIASES_FILE) : defaultFile;
35
26
  const fileAliases = readJsonFile(filePath);
36
- const envAliases = parseJsonObject(env.MODEL_ALIASES_JSON, 'MODEL_ALIASES_JSON');
27
+ const envAliases = parseJsonObject(env.MODEL_ALIASES_JSON, { source: 'MODEL_ALIASES_JSON', throwOnInvalid: true });
37
28
  return {
38
29
  ...DEFAULT_MODEL_ALIASES,
39
30
  ...fileAliases,
@@ -94,15 +85,10 @@ export function deepseekReasoningPayload({ alias, reasoning } = {}) {
94
85
  return { thinking: { type: 'disabled' } };
95
86
  }
96
87
 
97
- const payload = {};
88
+ const payload = { thinking: { type: 'enabled' } };
98
89
  const effort = aliasEffort ?? (effortDisablesThinking(requestEffort) ? undefined : requestEffort);
99
- if (aliasThinking === 'enabled' || effort) {
100
- payload.thinking = { type: 'enabled' };
101
- }
102
- if (effort) {
103
- const reasoningEffort = mapDeepSeekReasoningEffort(effort);
104
- if (reasoningEffort) payload.reasoning_effort = reasoningEffort;
105
- }
90
+ const reasoningEffort = mapDeepSeekReasoningEffort(effort);
91
+ if (reasoningEffort) payload.reasoning_effort = reasoningEffort;
106
92
  return payload;
107
93
  }
108
94