@galaxy-yearn/codex-deepseek-gateway 0.1.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.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # Codex DeepSeek Gateway
2
+
3
+ A small local gateway for using DeepSeek V4 models from Codex.
4
+
5
+ Codex sends OpenAI `Responses API` requests to the local gateway. The gateway converts them to DeepSeek-compatible `Chat Completions`, calls DeepSeek, then converts the answer back to Responses objects or streaming `response.*` events.
6
+
7
+ The runtime uses only the Node.js standard library. It installs under `~/.codex/deepseek-gateway`, runs as a detached background process, and does not edit your existing Codex config.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 20 or newer
12
+ - A DeepSeek API key
13
+ - Codex configured from `~/.codex/config.toml`
14
+
15
+ ## Install
16
+
17
+ Run:
18
+
19
+ ```sh
20
+ npx @galaxy-yearn/codex-deepseek-gateway install
21
+ ```
22
+
23
+ This copies the gateway to:
24
+
25
+ ```text
26
+ ~/.codex/deepseek-gateway
27
+ ```
28
+
29
+ It also creates:
30
+
31
+ ```text
32
+ ~/.codex/deepseek-gateway/config/gateway.local.json
33
+ ```
34
+
35
+ Open that file and replace `sk-REPLACE_ME` with your DeepSeek API key.
36
+
37
+ The install also creates:
38
+
39
+ ```text
40
+ ~/.codex/deepseek-gateway/config/model-aliases.json
41
+ ```
42
+
43
+ That file controls which model IDs the gateway exposes on `GET /v1/models`. You only need to edit it if you want to add or rename gateway-facing model aliases.
44
+
45
+ By default, the gateway serves the local alias list directly. If you also want to merge DeepSeek's upstream `/models` list, set `"fetchUpstreamModels": true` in `gateway.local.json`. Leaving it `false` keeps `/v1/models` lighter and more predictable.
46
+
47
+ If the key is already configured, `install` also starts the gateway. If this is your first install, run `start` after adding the key:
48
+
49
+ ```sh
50
+ npx @galaxy-yearn/codex-deepseek-gateway start
51
+ ```
52
+
53
+ Check that it is running:
54
+
55
+ ```sh
56
+ npx @galaxy-yearn/codex-deepseek-gateway status
57
+ ```
58
+
59
+ You should see `"reachable": true`.
60
+
61
+ ## Configure Codex
62
+
63
+ Edit `~/.codex/config.toml` and set the active provider to the gateway:
64
+
65
+ ```toml
66
+ model_provider = "deepseek-gateway"
67
+ model = "deepseek-v4-flash"
68
+ model_reasoning_effort = "low"
69
+ model_supports_reasoning_summaries = true
70
+ model_reasoning_summary = "auto"
71
+
72
+ [model_providers.deepseek-gateway]
73
+ name = "DeepSeekGateway"
74
+ base_url = "http://127.0.0.1:3000/v1"
75
+ wire_api = "responses"
76
+ ```
77
+
78
+ Use `deepseek-v4-pro` instead of `deepseek-v4-flash` if you want the pro model.
79
+
80
+ `config.toml` can change:
81
+
82
+ - the active provider via `model_provider`
83
+ - the active model ID via `model`
84
+ - the provider label via `[model_providers.<id>].name`
85
+
86
+ It does not define a separate display name for each model entry inside Codex `/model`. If your Codex build reads custom provider models from `/v1/models`, the visible model names come from the gateway's model IDs in `~/.codex/deepseek-gateway/config/model-aliases.json`.
87
+
88
+ These reasoning settings do different jobs:
89
+
90
+ - `model_supports_reasoning_summaries = true`
91
+ - `model_reasoning_summary = "auto"`
92
+
93
+ These tell Codex to use its normal thinking UI path. When DeepSeek returns `reasoning_content`, the gateway maps it into that path while thinking is enabled. It keeps the raw text as `reasoning_text` and sends a display-safe `summary_text` version with a small heading and common Markdown markers removed, because Codex TUI renders reasoning summaries as plain summary blocks.
94
+
95
+ When DeepSeek thinking is enabled, the gateway buffers visible assistant text and tool-call output until the upstream response completes. It then flushes collected reasoning before the answer/tool calls through one summary part streamed as small deltas. The tradeoff is higher first-token latency while thinking is on.
96
+
97
+ Restart Codex after changing `config.toml`.
98
+
99
+ ## Verify
100
+
101
+ Run:
102
+
103
+ ```sh
104
+ npx @galaxy-yearn/codex-deepseek-gateway doctor
105
+ ```
106
+
107
+ Important fields:
108
+
109
+ - `codexConfigUsingGateway` should be `true`
110
+ - `codexModel` should be your DeepSeek model, for example `deepseek-v4-pro`
111
+ - `codexReasoningEffort` should match `model_reasoning_effort`
112
+ - `deepseekThinking` shows the DeepSeek `thinking` payload
113
+ - `deepseekReasoningEffort` shows the DeepSeek effort sent upstream
114
+ - `reasoningDisplayMode` shows whether the gateway will emit `summary`, `disabled`, or `hidden`
115
+ - `gatewayEmitsReasoningSummary` should be `true` when DeepSeek thinking is enabled
116
+ - `codexSummaryConfigured` should be `true` so Codex TUI is configured to show summaries
117
+
118
+ For example, with:
119
+
120
+ ```toml
121
+ model_reasoning_effort = "xhigh"
122
+ ```
123
+
124
+ `doctor` should show:
125
+
126
+ ```json
127
+ "deepseekThinking": { "type": "enabled" },
128
+ "deepseekReasoningEffort": "max"
129
+ ```
130
+
131
+ ## Reasoning Mapping
132
+
133
+ | Codex `model_reasoning_effort` | DeepSeek request |
134
+ | --- | --- |
135
+ | `low` | `thinking.type = disabled` |
136
+ | `medium` | `thinking.type = enabled`, `reasoning_effort = high` |
137
+ | `high` | `thinking.type = enabled`, `reasoning_effort = high` |
138
+ | `xhigh` | `thinking.type = enabled`, `reasoning_effort = max` |
139
+
140
+ When thinking is enabled, every non-empty DeepSeek streaming `reasoning_content` chunk is collected and converted into Responses reasoning events:
141
+
142
+ - `reasoning_summary_text.delta` for Codex's native summary-style thinking UI
143
+
144
+ The completed response keeps the original DeepSeek text as `reasoning_text`. The visible summary path uses a plain-text rendering of the same text with common Markdown markers removed and a `Reasoning` heading for Codex TUI compatibility. When thinking is enabled, the gateway holds back visible answer/tool output until completion so the reasoning block can land first and stay contiguous.
145
+
146
+ ## Commands
147
+
148
+ ```sh
149
+ npx @galaxy-yearn/codex-deepseek-gateway install
150
+ npx @galaxy-yearn/codex-deepseek-gateway start
151
+ npx @galaxy-yearn/codex-deepseek-gateway stop
152
+ npx @galaxy-yearn/codex-deepseek-gateway status
153
+ npx @galaxy-yearn/codex-deepseek-gateway doctor
154
+ npx @galaxy-yearn/codex-deepseek-gateway uninstall
155
+ ```
156
+
157
+ `start` launches a headless background Node.js process. `stop` stops it. `uninstall` stops the gateway and removes `~/.codex/deepseek-gateway`, but it does not edit `~/.codex/config.toml`.
158
+
159
+ If `start` returns without visible output on your terminal, run `status`; `"reachable": true` is the source of truth.
160
+
161
+ ## What Works
162
+
163
+ - Codex `POST /v1/responses`
164
+ - streaming and non-streaming responses
165
+ - text input and output
166
+ - image, file, and audio content parts when DeepSeek accepts the corresponding Chat Completions shape
167
+ - function tools and tool-call history
168
+ - conservative schema-based repair for streamed and non-streamed tool arguments where DeepSeek returns stringified JSON values for fields that Codex declared as arrays, objects, booleans, or numbers
169
+ - DeepSeek thinking mode and `reasoning_content`
170
+ - lightweight local `previous_response_id` / `conversation` history while the gateway process is running
171
+ - `GET /v1/models` with local DeepSeek V4 aliases and optional upstream discovery
172
+
173
+ ## Limits
174
+
175
+ Chat Completions is not a full Responses API replacement. Some Responses features have no equivalent upstream field.
176
+
177
+ - Hosted tools such as web search, file search, computer use, image generation, and code interpreter are represented as function-tool shims unless Codex executes matching tools locally.
178
+ - OpenAI `file_id` values are passed through; the gateway cannot fetch private OpenAI-hosted files.
179
+ - In-memory conversation history is lost when the gateway restarts.
180
+ - The gateway exposes model aliases on `/v1/models`, including aliases from `config/model-aliases.json`. Whether Codex TUI `/model` actually shows custom provider models depends on the Codex build. `config.toml` remains the reliable fallback.
181
+
182
+ ## Local Testing Before Publish
183
+
184
+ From this repository:
185
+
186
+ ```sh
187
+ npm test
188
+ npm pack
189
+ npx --yes --package ./<generated-tarball>.tgz codex-deepseek-gateway install --no-edit
190
+ npx --yes --package ./<generated-tarball>.tgz codex-deepseek-gateway status
191
+ npx --yes --package ./<generated-tarball>.tgz codex-deepseek-gateway doctor
192
+ ```
193
+
194
+ Replace `<generated-tarball>.tgz` with the filename printed by `npm pack`.
195
+
196
+ For published usage, use the shorter package command shown in the install section.
197
+
198
+ ## Publish
199
+
200
+ When ready:
201
+
202
+ ```sh
203
+ npm login
204
+ npm test
205
+ npm pack --dry-run
206
+ npm publish
207
+ ```
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync, spawn } from 'node:child_process';
3
+ import {
4
+ copyFileSync,
5
+ cpSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { createRequire } from 'node:module';
13
+ import { dirname, join, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { loadConfig } from '../src/config.js';
16
+ import { toProviderChatCompletionsRequest } from '../src/protocol.js';
17
+
18
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
19
+ const require = createRequire(import.meta.url);
20
+ const packageJson = require('../package.json');
21
+
22
+ function print(message = '') {
23
+ process.stdout.write(message);
24
+ }
25
+
26
+ function usage() {
27
+ print(`Codex DeepSeek Gateway
28
+
29
+ Usage:
30
+ codex-deepseek-gateway install
31
+ codex-deepseek-gateway start
32
+ codex-deepseek-gateway stop
33
+ codex-deepseek-gateway status
34
+ codex-deepseek-gateway doctor
35
+ codex-deepseek-gateway uninstall
36
+
37
+ Options:
38
+ --dir <path> Install directory, defaults to ~/.codex/deepseek-gateway
39
+ --no-edit Do not open the local config file after install
40
+ `);
41
+ }
42
+
43
+ function parseArgs(argv) {
44
+ const options = { dir: defaultInstallDir(), noEdit: false };
45
+ const rest = [];
46
+ for (let index = 0; index < argv.length; index += 1) {
47
+ const arg = argv[index];
48
+ if (arg === '--dir' || arg === '-d' || arg === '-InstallDir') {
49
+ const value = argv[index + 1];
50
+ if (!value) throw new Error(`${arg} requires a path`);
51
+ options.dir = resolve(value);
52
+ index += 1;
53
+ } else if (arg === '--no-edit' || arg === '-NoEdit') {
54
+ options.noEdit = true;
55
+ } else {
56
+ rest.push(arg);
57
+ }
58
+ }
59
+ return { command: rest[0], options };
60
+ }
61
+
62
+ function defaultInstallDir() {
63
+ const home = process.env.CODEX_HOME || join(process.env.USERPROFILE || process.env.HOME || process.cwd(), '.codex');
64
+ return join(home, 'deepseek-gateway');
65
+ }
66
+
67
+ function configPath(installDir) {
68
+ return join(installDir, 'config', 'gateway.local.json');
69
+ }
70
+
71
+ function pidPath(installDir) {
72
+ return join(installDir, 'gateway.pid');
73
+ }
74
+
75
+ function serverPath(installDir) {
76
+ return join(installDir, 'src', 'server.js');
77
+ }
78
+
79
+ function loadInstalledConfig(installDir) {
80
+ return loadConfig({ ...process.env, GATEWAY_CONFIG_FILE: configPath(installDir) });
81
+ }
82
+
83
+ function endpoint(config, path = '') {
84
+ return `http://${config.host}:${config.port}${path}`;
85
+ }
86
+
87
+ function isConfigured(installDir) {
88
+ if (!existsSync(configPath(installDir))) return false;
89
+ const config = loadInstalledConfig(installDir);
90
+ return Boolean(config.upstreamApiKey);
91
+ }
92
+
93
+ async function health(config) {
94
+ const controller = new AbortController();
95
+ const timeout = setTimeout(() => controller.abort(), 1500);
96
+ try {
97
+ const response = await fetch(endpoint(config, '/health'), { signal: controller.signal });
98
+ if (!response.ok) return null;
99
+ return await response.json();
100
+ } catch {
101
+ return null;
102
+ } finally {
103
+ clearTimeout(timeout);
104
+ }
105
+ }
106
+
107
+ function processExists(pid) {
108
+ if (!pid) return false;
109
+ try {
110
+ process.kill(pid, 0);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ function killProcess(pid) {
118
+ if (!pid || pid === process.pid) return;
119
+ try {
120
+ if (process.platform === 'win32') {
121
+ execFileSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
122
+ } else {
123
+ process.kill(pid);
124
+ }
125
+ } catch {
126
+ }
127
+ }
128
+
129
+ function findWindowsPidOnPort(port) {
130
+ if (process.platform !== 'win32') return 0;
131
+ try {
132
+ const output = execFileSync('netstat.exe', ['-ano', '-p', 'tcp'], { encoding: 'utf8', windowsHide: true });
133
+ const escapedPort = String(port).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
134
+ const pattern = new RegExp(`(?:127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]|\\[::\\]):${escapedPort}\\s+[^\\s]+\\s+LISTENING\\s+(\\d+)`, 'i');
135
+ for (const line of output.split(/\r?\n/)) {
136
+ const match = line.match(pattern);
137
+ if (match) return Number(match[1]) || 0;
138
+ }
139
+ } catch {
140
+ }
141
+ return 0;
142
+ }
143
+
144
+ function processCommandLine(pid) {
145
+ if (process.platform !== 'win32' || !pid) return '';
146
+ try {
147
+ const output = execFileSync(
148
+ 'wmic.exe',
149
+ ['process', 'where', `ProcessId=${pid}`, 'get', 'CommandLine', '/value'],
150
+ { encoding: 'utf8', windowsHide: true },
151
+ );
152
+ const match = output.match(/CommandLine=(.*)/s);
153
+ return match ? match[1].trim() : '';
154
+ } catch {
155
+ return '';
156
+ }
157
+ }
158
+
159
+ function readPid(installDir) {
160
+ try {
161
+ return Number(readFileSync(pidPath(installDir), 'utf8').trim()) || 0;
162
+ } catch {
163
+ return 0;
164
+ }
165
+ }
166
+
167
+ function copyRuntime(installDir) {
168
+ mkdirSync(join(installDir, 'bin'), { recursive: true });
169
+ mkdirSync(join(installDir, 'src'), { recursive: true });
170
+ mkdirSync(join(installDir, 'config'), { recursive: true });
171
+ copyFileSync(join(ROOT, 'package.json'), join(installDir, 'package.json'));
172
+ rmSync(join(installDir, 'bin'), { recursive: true, force: true });
173
+ rmSync(join(installDir, 'src'), { recursive: true, force: true });
174
+ cpSync(join(ROOT, 'bin'), join(installDir, 'bin'), { recursive: true });
175
+ cpSync(join(ROOT, 'src'), join(installDir, 'src'), { recursive: true });
176
+ const modelAliasesConfig = join(installDir, 'config', 'model-aliases.json');
177
+ if (!existsSync(modelAliasesConfig)) {
178
+ copyFileSync(join(ROOT, 'config', 'model-aliases.example.json'), modelAliasesConfig);
179
+ }
180
+ const localConfig = configPath(installDir);
181
+ if (!existsSync(localConfig)) {
182
+ copyFileSync(join(ROOT, 'config', 'gateway.example.json'), localConfig);
183
+ }
184
+ }
185
+
186
+ function openConfig(file) {
187
+ const opener =
188
+ process.platform === 'win32'
189
+ ? ['notepad.exe', [file]]
190
+ : process.platform === 'darwin'
191
+ ? ['open', [file]]
192
+ : ['xdg-open', [file]];
193
+ try {
194
+ const child = spawn(opener[0], opener[1], { detached: true, stdio: 'ignore' });
195
+ child.on('error', () => print(`Edit config: ${file}\n`));
196
+ child.unref();
197
+ } catch {
198
+ print(`Edit config: ${file}\n`);
199
+ }
200
+ }
201
+
202
+ async function install(options) {
203
+ copyRuntime(options.dir);
204
+ print(`Installed to ${options.dir}\n`);
205
+ if (!isConfigured(options.dir)) {
206
+ print(`Edit this file and put your DeepSeek API key:\n ${configPath(options.dir)}\n`);
207
+ if (!options.noEdit) openConfig(configPath(options.dir));
208
+ return;
209
+ }
210
+ await start(options);
211
+ }
212
+
213
+ async function start(options) {
214
+ if (!existsSync(serverPath(options.dir))) {
215
+ throw new Error(`Missing runtime at ${options.dir}. Run install first.`);
216
+ }
217
+ if (!isConfigured(options.dir)) {
218
+ throw new Error(`Missing DeepSeek API key in ${configPath(options.dir)} or DEEPSEEK_API_KEY`);
219
+ }
220
+ const config = loadInstalledConfig(options.dir);
221
+ const currentHealth = await health(config);
222
+ if (currentHealth?.ok) {
223
+ const pid = readPid(options.dir);
224
+ const message = processExists(pid)
225
+ ? 'Gateway is already running.\n'
226
+ : `Gateway is already reachable at ${endpoint(config)}. Another install may already be running.\n`;
227
+ print(message);
228
+ return;
229
+ }
230
+
231
+ mkdirSync(options.dir, { recursive: true });
232
+ const out = 'ignore';
233
+ print(`Starting gateway on ${endpoint(config)} ...\n`);
234
+ const child = spawn(process.execPath, [serverPath(options.dir)], {
235
+ cwd: options.dir,
236
+ detached: true,
237
+ stdio: ['ignore', out, out],
238
+ windowsHide: true,
239
+ });
240
+ writeFileSync(pidPath(options.dir), String(child.pid));
241
+ child.unref();
242
+
243
+ for (let index = 0; index < 20; index += 1) {
244
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 250));
245
+ const ready = await health(config);
246
+ if (ready?.ok) {
247
+ print(`Gateway started. PID ${child.pid}\n`);
248
+ return;
249
+ }
250
+ }
251
+ throw new Error(`Gateway did not become reachable on ${endpoint(config)}`);
252
+ }
253
+
254
+ async function stop(options) {
255
+ const config = loadInstalledConfig(options.dir);
256
+ const pid = readPid(options.dir);
257
+ const hadManagedPid = processExists(pid);
258
+ let killedPortProcess = false;
259
+ if (processExists(pid)) {
260
+ killProcess(pid);
261
+ for (let index = 0; index < 20 && processExists(pid); index += 1) {
262
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
263
+ }
264
+ }
265
+ const portPid = findWindowsPidOnPort(config.port);
266
+ if (portPid && portPid !== pid) {
267
+ const commandLine = processCommandLine(portPid);
268
+ if (commandLine.includes(serverPath(options.dir))) {
269
+ killProcess(portPid);
270
+ killedPortProcess = true;
271
+ for (let index = 0; index < 20 && processExists(portPid); index += 1) {
272
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
273
+ }
274
+ }
275
+ }
276
+ rmSync(pidPath(options.dir), { force: true });
277
+ const running = await health(config);
278
+ if (!running?.ok) {
279
+ print('Gateway stopped.\n');
280
+ } else if (!hadManagedPid && !killedPortProcess) {
281
+ print(`No gateway process is recorded for this install. ${endpoint(config)} is still reachable, likely from another install.\n`);
282
+ } else {
283
+ print(`Gateway may still be running on ${endpoint(config)}.\n`);
284
+ }
285
+ }
286
+
287
+ async function status(options) {
288
+ const config = loadInstalledConfig(options.dir);
289
+ const pid = readPid(options.dir);
290
+ const running = await health(config);
291
+ const versionPath = join(options.dir, 'package.json');
292
+ const installedVersion = existsSync(versionPath) ? JSON.parse(readFileSync(versionPath, 'utf8')).version : '';
293
+ print(JSON.stringify({
294
+ installed: existsSync(options.dir),
295
+ installDir: options.dir,
296
+ configPath: configPath(options.dir),
297
+ pid,
298
+ pidRunning: processExists(pid),
299
+ reachable: Boolean(running?.ok),
300
+ url: endpoint(config),
301
+ version: installedVersion || packageJson.version,
302
+ }, null, 2));
303
+ print('\n');
304
+ }
305
+
306
+ async function doctor(options) {
307
+ const config = loadInstalledConfig(options.dir);
308
+ const codexConfigUsingGateway = config.codexModelProvider === 'deepseek-gateway';
309
+ const model = codexConfigUsingGateway && config.codexModel ? config.codexModel : 'deepseek-v4-pro';
310
+ const upstreamRequest = toProviderChatCompletionsRequest(
311
+ {
312
+ model,
313
+ messages: [{ role: 'user', content: 'ping' }],
314
+ stream: true,
315
+ },
316
+ config,
317
+ );
318
+ const supportsReasoningSummaries = String(config.codexModelSupportsReasoningSummaries).toLowerCase() === 'true';
319
+ const summaryMode = String(config.codexReasoningSummary || '').toLowerCase();
320
+ const hideAgentReasoning = String(config.codexHideAgentReasoning).toLowerCase() === 'true';
321
+ const thinkingEnabled = upstreamRequest.thinking?.type === 'enabled';
322
+ const reasoningDisplayMode = hideAgentReasoning
323
+ ? 'hidden'
324
+ : thinkingEnabled
325
+ ? 'summary'
326
+ : 'disabled';
327
+ const codexSummaryConfigured = supportsReasoningSummaries && summaryMode && summaryMode !== 'none';
328
+ print(JSON.stringify({
329
+ packageVersion: packageJson.version,
330
+ installDir: options.dir,
331
+ localConfig: configPath(options.dir),
332
+ codexConfigUsingGateway,
333
+ codexModelProvider: config.codexModelProvider || null,
334
+ codexModel: config.codexModel || null,
335
+ codexReasoningEffort: config.codexReasoningEffort || null,
336
+ codexReasoningSummary: config.codexReasoningSummary || null,
337
+ codexModelSupportsReasoningSummaries: config.codexModelSupportsReasoningSummaries || null,
338
+ codexHideAgentReasoning: config.codexHideAgentReasoning || null,
339
+ sampleModel: model,
340
+ upstreamModel: upstreamRequest.model,
341
+ deepseekThinking: upstreamRequest.thinking || null,
342
+ deepseekReasoningEffort: upstreamRequest.reasoning_effort || null,
343
+ reasoningDisplayMode,
344
+ gatewayEmitsReasoningSummary: reasoningDisplayMode === 'summary',
345
+ codexSummaryConfigured,
346
+ reasoningSummaryHint: 'When DeepSeek thinking is enabled, the gateway maps every non-empty reasoning_content chunk into the Responses reasoning summary path. Keep model_supports_reasoning_summaries = true and model_reasoning_summary = "auto" so Codex TUI is configured to show summaries.',
347
+ modelDiscoveryHint: 'The gateway exposes model aliases on /v1/models. Whether Codex TUI /model shows them depends on the Codex build.',
348
+ }, null, 2));
349
+ print('\n');
350
+ }
351
+
352
+ async function uninstall(options) {
353
+ await stop(options);
354
+ rmSync(options.dir, { recursive: true, force: true });
355
+ print(`Removed ${options.dir}\n`);
356
+ }
357
+
358
+ async function main() {
359
+ const { command, options } = parseArgs(process.argv.slice(2));
360
+ if (!command || command === '-h' || command === '--help' || command === 'help') {
361
+ usage();
362
+ return;
363
+ }
364
+ const commands = { install, start, stop, status, doctor, uninstall };
365
+ const handler = commands[command];
366
+ if (!handler) {
367
+ throw new Error(`Unknown command: ${command}`);
368
+ }
369
+ await handler(options);
370
+ }
371
+
372
+ main().catch((error) => {
373
+ process.stderr.write(`${error.message || error}\n`);
374
+ process.exit(1);
375
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "port": 3000,
3
+ "host": "127.0.0.1",
4
+ "upstreamProvider": "deepseek",
5
+ "upstreamBaseUrl": "https://api.deepseek.com",
6
+ "upstreamApiKey": "sk-REPLACE_ME",
7
+ "upstreamTimeoutMs": 120000,
8
+ "fetchUpstreamModels": false,
9
+ "modelsTimeoutMs": 5000,
10
+ "modelsCacheMs": 60000,
11
+ "debugPayload": false
12
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "deepseek-v4-flash": {
3
+ "model": "deepseek-v4-flash",
4
+ "thinking": "auto"
5
+ },
6
+ "deepseek-v4-pro": {
7
+ "model": "deepseek-v4-pro",
8
+ "thinking": "auto"
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@galaxy-yearn/codex-deepseek-gateway",
3
+ "version": "0.1.0",
4
+ "description": "Local Codex Responses API to DeepSeek Chat Completions gateway.",
5
+ "type": "module",
6
+ "main": "src/server.js",
7
+ "bin": {
8
+ "codex-deepseek-gateway": "bin/codex-deepseek-gateway.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "config",
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "node --test"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "keywords": [
23
+ "codex",
24
+ "deepseek",
25
+ "openai",
26
+ "responses-api",
27
+ "chat-completions",
28
+ "gateway"
29
+ ],
30
+ "license": "MIT",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }