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

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,13 +1,25 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  import { clearScreenDown, cursorTo } from 'node:readline';
5
6
  import { readCodexConfig } from './codex-config.js';
7
+ import { readLocalConfigFile } from './local-config.js';
8
+ import { catalogFileForPromptLanguage, normalizePromptLanguage } from './prompt-language.js';
6
9
 
7
10
  export const DEFAULT_PROVIDER = 'deepseek-gateway';
8
11
  export const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
9
- const SELECTED_ROW = '\x1b[38;5;81m';
12
+ const SELECTED_ROW = '\x1b[38;2;57;100;254m';
10
13
  const RESET_STYLE = '\x1b[0m';
14
+ const require = createRequire(import.meta.url);
15
+ const CODEX_PLATFORM_PACKAGE_BY_TARGET = {
16
+ 'x86_64-unknown-linux-musl': '@openai/codex-linux-x64',
17
+ 'aarch64-unknown-linux-musl': '@openai/codex-linux-arm64',
18
+ 'x86_64-apple-darwin': '@openai/codex-darwin-x64',
19
+ 'aarch64-apple-darwin': '@openai/codex-darwin-arm64',
20
+ 'x86_64-pc-windows-msvc': '@openai/codex-win32-x64',
21
+ 'aarch64-pc-windows-msvc': '@openai/codex-win32-arm64',
22
+ };
11
23
 
12
24
  function print(message = '') {
13
25
  process.stdout.write(message);
@@ -37,16 +49,25 @@ export function gatewayModels(installDir) {
37
49
  return Object.keys(readJsonObject(join(installDir, 'config', 'model-aliases.json'))).sort();
38
50
  }
39
51
 
52
+ function readPromptLanguage(installDir) {
53
+ const configFile = join(installDir, 'config', 'gateway.local.json');
54
+ const localConfig = readLocalConfigFile(configFile);
55
+ return normalizePromptLanguage(localConfig.CODEX_PROMPT_LANGUAGE);
56
+ }
57
+
40
58
  export function createLaunchContext(options) {
41
59
  const codexHome = defaultCodexHome();
42
60
  const models = gatewayModels(options.dir);
43
61
  const codexConfig = readCodexConfig();
62
+ const promptLanguage = readPromptLanguage(options.dir);
44
63
  const configModel = codexConfig.modelProvider === DEFAULT_PROVIDER && models.includes(codexConfig.model) ? codexConfig.model : '';
45
64
  return {
46
65
  all: options.all,
47
66
  codexHome,
48
67
  installDir: options.dir,
68
+ modelCatalogPath: join(options.dir, 'config', catalogFileForPromptLanguage(promptLanguage)),
49
69
  models,
70
+ promptLanguage,
50
71
  projectRoot: findProjectRoot(process.cwd()),
51
72
  provider: options.provider || DEFAULT_PROVIDER,
52
73
  model: options.model || configModel || models[0] || '',
@@ -59,7 +80,7 @@ export function missingModelMessage(context) {
59
80
  }
60
81
 
61
82
  function configOverrideArgs(context) {
62
- return [
83
+ const args = [
63
84
  '-c',
64
85
  `model_provider=${JSON.stringify(context.provider)}`,
65
86
  '-c',
@@ -71,21 +92,10 @@ function configOverrideArgs(context) {
71
92
  '-c',
72
93
  'model_reasoning_summary="auto"',
73
94
  ];
74
- }
75
-
76
- function configOverrideCommandParts(context) {
77
- return [
78
- '-c',
79
- `model_provider=${context.provider}`,
80
- '-c',
81
- `model=${context.model}`,
82
- '-c',
83
- `model_reasoning_effort=${context.reasoningEffort}`,
84
- '-c',
85
- 'model_supports_reasoning_summaries=true',
86
- '-c',
87
- 'model_reasoning_summary=auto',
88
- ];
95
+ if (context.provider === DEFAULT_PROVIDER && context.modelCatalogPath) {
96
+ args.push('-c', `model_catalog_json=${JSON.stringify(context.modelCatalogPath)}`);
97
+ }
98
+ return args;
89
99
  }
90
100
 
91
101
  export function codexNewArgs(context) {
@@ -96,20 +106,146 @@ export function codexResumeArgs(sessionId, context) {
96
106
  return ['resume', sessionId, ...configOverrideArgs(context)];
97
107
  }
98
108
 
99
- export function codexNewCommand(context) {
100
- return ['codex', ...configOverrideCommandParts(context)].join(' ');
109
+ function quoteShellArg(arg) {
110
+ const value = String(arg);
111
+ if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(value)) return value;
112
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
101
113
  }
102
114
 
103
115
  export function codexResumeCommand(sessionId, context) {
104
- return ['codex', 'resume', sessionId, ...configOverrideCommandParts(context)].join(' ');
116
+ return ['codex', ...codexResumeArgs(sessionId, context)].map(quoteShellArg).join(' ');
117
+ }
118
+
119
+ function codexTargetTriple(platform = process.platform, arch = process.arch) {
120
+ if ((platform === 'linux' || platform === 'android') && arch === 'x64') return 'x86_64-unknown-linux-musl';
121
+ if ((platform === 'linux' || platform === 'android') && arch === 'arm64') return 'aarch64-unknown-linux-musl';
122
+ if (platform === 'darwin' && arch === 'x64') return 'x86_64-apple-darwin';
123
+ if (platform === 'darwin' && arch === 'arm64') return 'aarch64-apple-darwin';
124
+ if (platform === 'win32' && arch === 'x64') return 'x86_64-pc-windows-msvc';
125
+ if (platform === 'win32' && arch === 'arm64') return 'aarch64-pc-windows-msvc';
126
+ return '';
127
+ }
128
+
129
+ function pathEntries(env = process.env) {
130
+ const pathValue = env.PATH || env.Path || env.path || '';
131
+ return pathValue.split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
132
+ }
133
+
134
+ function candidateExecutables(command, env = process.env) {
135
+ if (command.includes('/') || command.includes('\\')) return [command];
136
+ const extensions = process.platform === 'win32'
137
+ ? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
138
+ : [''];
139
+ const names = process.platform === 'win32'
140
+ ? extensions.map((extension) => `${command}${extension.toLowerCase()}`)
141
+ : [command];
142
+ return pathEntries(env).flatMap((entry) => names.map((name) => join(entry, name)));
143
+ }
144
+
145
+ function npmShimCodexPackageRoot(shimPath) {
146
+ const base = dirname(shimPath);
147
+ const packageRoot = join(base, 'node_modules', '@openai', 'codex');
148
+ return existsSync(join(packageRoot, 'package.json')) ? packageRoot : '';
149
+ }
150
+
151
+ function ancestorCodexPackageRoot(filePath) {
152
+ try {
153
+ let current = dirname(realpathSync(filePath));
154
+ while (true) {
155
+ const packageJson = join(current, 'package.json');
156
+ if (existsSync(packageJson)) {
157
+ try {
158
+ const parsed = JSON.parse(readFileSync(packageJson, 'utf8'));
159
+ if (parsed?.name === '@openai/codex') return current;
160
+ } catch {
161
+ // Keep walking; this is only a best-effort executable locator.
162
+ }
163
+ }
164
+ const parent = dirname(current);
165
+ if (parent === current) return '';
166
+ current = parent;
167
+ }
168
+ } catch {
169
+ return '';
170
+ }
171
+ }
172
+
173
+ function codexPackageRoot() {
174
+ try {
175
+ return dirname(require.resolve('@openai/codex/package.json'));
176
+ } catch {
177
+ for (const candidate of candidateExecutables('codex')) {
178
+ const packageRoot = npmShimCodexPackageRoot(candidate) || ancestorCodexPackageRoot(candidate);
179
+ if (packageRoot) return packageRoot;
180
+ }
181
+ return '';
182
+ }
183
+ }
184
+
185
+ function codexPlatformPackageJson(packageRoot, platformPackage) {
186
+ try {
187
+ return createRequire(join(packageRoot, 'package.json')).resolve(`${platformPackage}/package.json`);
188
+ } catch {
189
+ const nested = join(packageRoot, 'node_modules', platformPackage, 'package.json');
190
+ return existsSync(nested) ? nested : '';
191
+ }
192
+ }
193
+
194
+ export function resolveCodexExecutable() {
195
+ const packageRoot = codexPackageRoot();
196
+ const targetTriple = codexTargetTriple();
197
+ const platformPackage = CODEX_PLATFORM_PACKAGE_BY_TARGET[targetTriple];
198
+ if (packageRoot && targetTriple && platformPackage) {
199
+ const packageJson = codexPlatformPackageJson(packageRoot, platformPackage);
200
+ if (existsSync(packageJson)) {
201
+ const executable = join(
202
+ dirname(packageJson),
203
+ 'vendor',
204
+ targetTriple,
205
+ 'bin',
206
+ process.platform === 'win32' ? 'codex.exe' : 'codex',
207
+ );
208
+ if (existsSync(executable)) return executable;
209
+ }
210
+ const bundledExecutable = join(
211
+ packageRoot,
212
+ 'vendor',
213
+ targetTriple,
214
+ 'bin',
215
+ process.platform === 'win32' ? 'codex.exe' : 'codex',
216
+ );
217
+ if (existsSync(bundledExecutable)) return bundledExecutable;
218
+ }
219
+
220
+ for (const candidate of candidateExecutables('codex')) {
221
+ if (existsSync(candidate) && !candidate.endsWith('.cmd') && !candidate.endsWith('.bat') && !candidate.endsWith('.ps1')) {
222
+ return candidate;
223
+ }
224
+ }
225
+ return 'codex';
226
+ }
227
+
228
+ export function codexProcessEnv() {
229
+ const packageRoot = codexPackageRoot();
230
+ if (!packageRoot) return process.env;
231
+ return {
232
+ ...process.env,
233
+ CODEX_MANAGED_BY_NPM: '1',
234
+ CODEX_MANAGED_PACKAGE_ROOT: realpathSync(packageRoot),
235
+ };
105
236
  }
106
237
 
107
238
  export async function runCodex(args) {
108
- const child = spawn('codex', args, {
239
+ const child = spawn(resolveCodexExecutable(), args, {
109
240
  stdio: 'inherit',
110
- shell: process.platform === 'win32',
241
+ shell: false,
242
+ env: codexProcessEnv(),
111
243
  });
112
244
  process.exitCode = await new Promise((resolveChild) => {
245
+ child.on('error', (error) => {
246
+ process.stderr.write(`Failed to launch codex: ${error.message}\n`);
247
+ resolveChild(1);
248
+ });
113
249
  child.on('exit', (exitCode) => resolveChild(exitCode ?? 0));
114
250
  });
115
251
  }
@@ -118,25 +254,73 @@ function rowOffset(header) {
118
254
  return header ? 4 : 2;
119
255
  }
120
256
 
121
- function renderRow(state, index) {
122
- cursorTo(process.stdout, 0, rowOffset(state.header) + index);
123
- const row = `${index === state.selected ? '>' : ' '} ${state.rows[index]}`;
124
- const styled = index === state.selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row;
257
+ export function pickerWindow(rows, selected, windowSize, currentOffset = 0) {
258
+ const allRows = Array.isArray(rows) ? rows : [];
259
+ const size = Math.max(1, Math.trunc(Number(windowSize) || allRows.length || 1));
260
+ const clampedSelected = Math.min(Math.max(0, selected), Math.max(0, allRows.length - 1));
261
+ const maxOffset = Math.max(0, allRows.length - size);
262
+ let offset = Math.min(Math.max(0, Math.trunc(Number(currentOffset) || 0)), maxOffset);
263
+ if (clampedSelected < offset) {
264
+ offset = clampedSelected;
265
+ } else if (clampedSelected >= offset + size) {
266
+ offset = clampedSelected - size + 1;
267
+ }
268
+ return {
269
+ offset,
270
+ rows: allRows.slice(offset, offset + size),
271
+ };
272
+ }
273
+
274
+ function pickerControls(shortcuts = []) {
275
+ const labels = shortcuts.map((shortcut) => shortcut.label).filter(Boolean);
276
+ return [...labels, '↑/↓ select', '← back', 'Enter confirm', 'Esc quit'].join(' ');
277
+ }
278
+
279
+ function renderRow(state, absoluteIndex) {
280
+ if (absoluteIndex < state.offset || absoluteIndex >= state.offset + state.visibleRows.length) return;
281
+ const visibleIndex = absoluteIndex - state.offset;
282
+ cursorTo(process.stdout, 0, rowOffset(state.header) + visibleIndex);
283
+ const row = `${absoluteIndex === state.selected ? '>' : ' '} ${state.rows[absoluteIndex]}`;
284
+ const styled = absoluteIndex === state.selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row;
125
285
  print(`\x1b[2K${styled}`);
126
286
  }
127
287
 
128
- function renderPicker(state) {
129
- const { title, rows, selected, header } = state;
288
+ function pickerLines(state) {
289
+ const { title, rows, selected, header, emptyMessage, shortcuts } = state;
290
+ const window = pickerWindow(rows, selected, state.windowSize, state.offset);
291
+ state.offset = window.offset;
292
+ state.visibleRows = window.rows;
293
+ const lines = [title, ''];
294
+ if (header) lines.push(` ${header}`, ` ${'-'.repeat(header.length)}`);
295
+ for (const [visibleIndex, rowText] of state.visibleRows.entries()) {
296
+ const absoluteIndex = state.offset + visibleIndex;
297
+ const row = `${absoluteIndex === selected ? '>' : ' '} ${rowText}`;
298
+ lines.push(absoluteIndex === selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row);
299
+ }
300
+ if (!rows.length && emptyMessage) lines.push(emptyMessage);
301
+ if (rows.length > state.visibleRows.length) {
302
+ lines.push('', ` Showing ${state.offset + 1}-${state.offset + state.visibleRows.length} of ${rows.length}`);
303
+ }
304
+ lines.push('', ` ${pickerControls(shortcuts)}`);
305
+ return lines;
306
+ }
307
+
308
+ function renderPicker(state, { clear = true } = {}) {
309
+ const lines = pickerLines(state);
130
310
  cursorTo(process.stdout, 0, 0);
131
- clearScreenDown(process.stdout);
132
- let output = `${title}\n\n`;
133
- if (header) output += ` ${header}\n ${'-'.repeat(header.length)}\n`;
134
- for (const [index] of rows.entries()) {
135
- const row = `${index === selected ? '>' : ' '} ${rows[index]}`;
136
- output += `${index === selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row}\n`;
311
+ if (clear) {
312
+ clearScreenDown(process.stdout);
313
+ print(`${lines.join('\n')}\n`);
314
+ state.renderedLineCount = lines.length;
315
+ return;
316
+ }
317
+ for (const line of lines) {
318
+ print(`\x1b[2K${line}\n`);
137
319
  }
138
- output += '\nUp/Down select Enter confirm Left back Esc quit\n';
139
- print(output);
320
+ for (let index = lines.length; index < (state.renderedLineCount || 0); index += 1) {
321
+ print('\x1b[2K\n');
322
+ }
323
+ state.renderedLineCount = lines.length;
140
324
  }
141
325
 
142
326
  function openPickerScreen() {
@@ -147,11 +331,23 @@ function closePickerScreen() {
147
331
  process.stdout.write('\x1b[?25h\x1b[?1049l');
148
332
  }
149
333
 
150
- export async function pick(title, rows, header = '') {
151
- if (!rows.length) return { action: 'back' };
334
+ export async function pick(title, rows, header = '', options = {}) {
335
+ const shortcuts = Array.isArray(options.shortcuts) ? options.shortcuts : [];
336
+ if (!rows.length && !shortcuts.length) return { action: 'back' };
152
337
  let selected = 0;
153
338
  const stdin = process.stdin;
154
- const state = { title, rows, selected, header };
339
+ const state = {
340
+ title,
341
+ rows,
342
+ selected,
343
+ header,
344
+ shortcuts,
345
+ windowSize: options.windowSize,
346
+ emptyMessage: options.emptyMessage,
347
+ offset: 0,
348
+ visibleRows: [],
349
+ renderedLineCount: 0,
350
+ };
155
351
  renderPicker(state);
156
352
 
157
353
  return await new Promise((resolvePick) => {
@@ -163,13 +359,25 @@ export async function pick(title, rows, header = '') {
163
359
  const key = chunk.toString('utf8');
164
360
  if (key === '\u0003' || key === '\u001b') return done({ action: 'cancel' });
165
361
  if (key === '\u001b[D') return done({ action: 'back' });
166
- if (key === '\r' || key === '\n') return done({ action: 'select', index: selected });
362
+ for (const shortcut of shortcuts) {
363
+ if (key.toLowerCase() === String(shortcut.key || '').toLowerCase()) {
364
+ return done({ action: shortcut.action || shortcut.key });
365
+ }
366
+ }
367
+ if ((key === '\r' || key === '\n') && rows.length) return done({ action: 'select', index: selected });
368
+ if (!rows.length) return;
167
369
  const previous = selected;
370
+ const previousOffset = state.offset;
168
371
  if (key === '\u001b[A') selected = Math.max(0, selected - 1);
169
372
  else if (key === '\u001b[B') selected = Math.min(rows.length - 1, selected + 1);
170
373
  else return;
171
374
  if (selected === previous) return;
172
375
  state.selected = selected;
376
+ const nextWindow = pickerWindow(rows, selected, state.windowSize, state.offset);
377
+ if (nextWindow.offset !== previousOffset) {
378
+ renderPicker(state, { clear: false });
379
+ return;
380
+ }
173
381
  renderRow(state, previous);
174
382
  renderRow(state, selected);
175
383
  };
@@ -222,6 +430,7 @@ export async function chooseLaunchContext(context) {
222
430
  }
223
431
 
224
432
  export async function newConversation(options) {
433
+ if (options.print) throw new Error('--print is only supported with sessions');
225
434
  const context = createLaunchContext(options);
226
435
  if (!context.model) {
227
436
  print(missingModelMessage(context));
@@ -233,9 +442,5 @@ export async function newConversation(options) {
233
442
  const selected = await chooseLaunchContext(context);
234
443
  if (!selected) return;
235
444
  }
236
- if (options.print || (!hasLaunchOverrides && !canPick)) {
237
- print(`${codexNewCommand(context)}\n`);
238
- return;
239
- }
240
445
  await runCodex(codexNewArgs(context));
241
446
  }
@@ -27,7 +27,7 @@ const ID_WIDTH = 36;
27
27
  const TITLE_WIDTH = 16;
28
28
  const TABLE_INDENT = ' ';
29
29
  const COLUMN_GAP = ' ';
30
- const NEW_SESSION_ROW = '[New conversation]';
30
+ export const DEFAULT_SESSION_LIMIT = 15;
31
31
 
32
32
  function print(message = '') {
33
33
  process.stdout.write(message);
@@ -186,7 +186,7 @@ function resolveSessionSelection(selection, sessionsList) {
186
186
  }
187
187
 
188
188
  function printSessions(sessionsList, options, context) {
189
- const listed = sessionsList.slice(0, options.limit || 20);
189
+ const listed = sessionsList.slice(0, options.limit || DEFAULT_SESSION_LIMIT);
190
190
  print(`Codex sessions ${options.all ? `under ${context.codexHome}` : `for project ${context.projectRoot}`}\n`);
191
191
  print(`Target: ${context.provider} / ${context.model} / ${context.reasoningEffort}\n\n`);
192
192
  if (!listed.length) {
@@ -215,23 +215,35 @@ function sessionRows(sessionsList) {
215
215
  return sessionsList.map((session) => sessionRow(session));
216
216
  }
217
217
 
218
+ export function sessionPickerRows(allSessions) {
219
+ return sessionRows(allSessions);
220
+ }
221
+
218
222
  async function chooseSessionFlow(allSessions, context, limit) {
219
223
  if (!context.models.length) {
220
224
  print(missingModelMessage(context));
221
225
  return null;
222
226
  }
223
227
 
224
- const listed = allSessions.slice(0, limit);
225
228
  const header = sessionHeader();
229
+ const windowSize = limit || DEFAULT_SESSION_LIMIT;
226
230
  return await withPickerScreen(async () => {
227
231
  let step = 'session';
228
232
  while (true) {
229
233
  if (step === 'session') {
230
- const rows = [NEW_SESSION_ROW, ...sessionRows(listed)];
231
- const result = await pick(`Choose Codex session`, rows, header);
234
+ const result = await pick(
235
+ `Choose Codex session`,
236
+ sessionPickerRows(allSessions),
237
+ header,
238
+ {
239
+ windowSize,
240
+ emptyMessage: 'No matching sessions found.',
241
+ shortcuts: [{ key: 'n', action: 'new', label: 'N new' }],
242
+ },
243
+ );
232
244
  if (result.action === 'cancel') return null;
233
245
  if (result.action === 'back') return null;
234
- context.selectedSession = result.index === 0 ? { newConversation: true } : listed[result.index - 1];
246
+ context.selectedSession = result.action === 'new' ? { newConversation: true } : allSessions[result.index];
235
247
  step = 'model';
236
248
  } else if (step === 'model') {
237
249
  const action = await pickModel(context);
@@ -273,7 +285,7 @@ export async function sessions(options) {
273
285
  return;
274
286
  }
275
287
 
276
- const selected = await chooseSessionFlow(allSessions, context, options.limit || 20);
288
+ const selected = await chooseSessionFlow(allSessions, context, options.limit || DEFAULT_SESSION_LIMIT);
277
289
  if (selected?.newConversation) await runCodex(codexNewArgs(context));
278
290
  else if (selected) await runCodex(codexResumeArgs(selected.id, context));
279
291
  }
package/src/config.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { loadModelAliases } from './model-map.js';
2
- import { mergeLocalConfig } from './local-config.js';
2
+ import { mergeLocalConfig, readLocalConfigFile, resolveLocalConfigPath } from './local-config.js';
3
3
  import { parseBoolean, parseList } from './common.js';
4
4
  import { readCodexConfig } from './codex-config.js';
5
+ import { normalizePromptLanguage } from './prompt-language.js';
5
6
 
6
7
  export function loadConfig(env = process.env) {
7
8
  const mergedEnv = mergeLocalConfig(env);
9
+ const localConfig = readLocalConfigFile(resolveLocalConfigPath(env));
8
10
  const codexConfig = readCodexConfig(mergedEnv);
9
11
  return {
10
12
  port: Number(mergedEnv.PORT || 3000),
@@ -54,6 +56,7 @@ export function loadConfig(env = process.env) {
54
56
  codexReasoningSummary: codexConfig.modelReasoningSummary,
55
57
  codexModelSupportsReasoningSummaries: codexConfig.modelSupportsReasoningSummaries,
56
58
  codexHideAgentReasoning: codexConfig.hideAgentReasoning,
59
+ codexPromptLanguage: normalizePromptLanguage(localConfig.CODEX_PROMPT_LANGUAGE),
57
60
  modelAliases: loadModelAliases(mergedEnv),
58
61
  };
59
62
  }
@@ -36,10 +36,14 @@ export function readLocalConfigFile(path = resolve(PROJECT_ROOT, 'config', 'gate
36
36
  return flattenConfig(parsed.value);
37
37
  }
38
38
 
39
- export function mergeLocalConfig(env = process.env, cwd = PROJECT_ROOT) {
40
- const configPath = env.GATEWAY_CONFIG_FILE
39
+ export function resolveLocalConfigPath(env = process.env, cwd = PROJECT_ROOT) {
40
+ return env.GATEWAY_CONFIG_FILE
41
41
  ? resolve(cwd, env.GATEWAY_CONFIG_FILE)
42
42
  : resolve(cwd, 'config', 'gateway.local.json');
43
+ }
44
+
45
+ export function mergeLocalConfig(env = process.env, cwd = PROJECT_ROOT) {
46
+ const configPath = resolveLocalConfigPath(env, cwd);
43
47
  const fileConfig = readLocalConfigFile(configPath);
44
48
  if (fileConfig.UPSTREAM_API_KEY === 'sk-REPLACE_ME') {
45
49
  delete fileConfig.UPSTREAM_API_KEY;
@@ -0,0 +1,13 @@
1
+ export const DEFAULT_PROMPT_LANGUAGE = 'en';
2
+ export const PROMPT_LANGUAGES = ['en', 'zh'];
3
+
4
+ export function normalizePromptLanguage(value) {
5
+ const normalized = String(value || '').trim().toLowerCase();
6
+ return PROMPT_LANGUAGES.includes(normalized) ? normalized : DEFAULT_PROMPT_LANGUAGE;
7
+ }
8
+
9
+ export function catalogFileForPromptLanguage(language) {
10
+ return normalizePromptLanguage(language) === 'zh'
11
+ ? 'codex-model-catalog.zh.json'
12
+ : 'codex-model-catalog.json';
13
+ }