codex-slot 0.1.27 → 0.1.29
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 +32 -0
- package/dist/app/status-service.js +30 -0
- package/dist/cli.js +63 -0
- package/dist/codex-config.js +36 -148
- package/dist/config.js +12 -2
- package/dist/model-proxy-dispatcher.js +67 -0
- package/dist/relay-commands.js +166 -0
- package/dist/relay-proxy-service.js +160 -0
- package/dist/relay-store.js +138 -0
- package/dist/server.js +2 -2
- package/dist/state.js +50 -0
- package/dist/status-command.js +141 -19
- package/dist/status.js +72 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- Manage multiple accounts or workspaces as separate slots
|
|
11
11
|
- Refresh and cache the latest usage from the official usage endpoint
|
|
12
12
|
- Expose a local provider endpoint for Codex
|
|
13
|
+
- Optionally pin model requests to an OpenAI-compatible relay slot
|
|
13
14
|
- Proxy ChatGPT backend plugin requests through the selected cslot account
|
|
14
15
|
- Apply local block rules for temporary, 5-hour, and weekly limits
|
|
15
16
|
- Automatically switch `~/.codex/config.toml` to the `cslot` provider while the local proxy is running (and restore it on stop)
|
|
@@ -89,12 +90,21 @@ codex-slot import <name> [HOME]
|
|
|
89
90
|
codex-slot status
|
|
90
91
|
codex-slot start [--port <port>]
|
|
91
92
|
codex-slot stop
|
|
93
|
+
codex-slot relay add <name> --base-url <url> --api-key <key>
|
|
94
|
+
codex-slot relay list
|
|
95
|
+
codex-slot use relay <name>
|
|
96
|
+
codex-slot use auth
|
|
97
|
+
codex-slot current
|
|
92
98
|
```
|
|
93
99
|
|
|
94
100
|
Common patterns:
|
|
95
101
|
|
|
96
102
|
- `cslot import work ~/workspace-home`
|
|
97
103
|
- `cslot rename work work-main`
|
|
104
|
+
- `cslot relay add third --base-url https://relay.example.com/v1 --api-key sk-...`
|
|
105
|
+
- `cslot status`
|
|
106
|
+
- `cslot use relay third`
|
|
107
|
+
- `cslot use auth`
|
|
98
108
|
- `cslot start`
|
|
99
109
|
|
|
100
110
|
## Architecture
|
|
@@ -108,6 +118,7 @@ The project is intentionally split by responsibility:
|
|
|
108
118
|
- `src/status-command.ts`: usage refresh output and interactive toggle UI
|
|
109
119
|
- `src/codex-config.ts`: managed `~/.codex/config.toml` apply/restore logic
|
|
110
120
|
- `src/backend-proxy-service.ts`: ChatGPT backend proxy for Codex plugin/runtime requests
|
|
121
|
+
- `src/relay-proxy-service.ts`, `src/model-proxy-dispatcher.ts`, `src/relay-store.ts`: optional OpenAI-compatible relay slots and `/v1/*` model route dispatch
|
|
111
122
|
- `src/account-store.ts`, `src/usage-sync.ts`, `src/scheduler.ts`, `src/status.ts`: core domain and runtime logic
|
|
112
123
|
- `src/text.ts`: shared bilingual text and locale-independent formatting helpers
|
|
113
124
|
|
|
@@ -146,6 +157,27 @@ Behavior:
|
|
|
146
157
|
- `requires_openai_auth = true` keeps Codex App treating the local cslot provider as a ChatGPT-authenticated provider, so plugin navigation and trusted plugin runtimes are not disabled as API-key/custom-provider mode
|
|
147
158
|
- `/backend-api/*` requests are forwarded to ChatGPT backend with the current selected account's upstream token; client `Authorization` headers are not forwarded upstream
|
|
148
159
|
|
|
160
|
+
## OpenAI-compatible Relay Slots
|
|
161
|
+
|
|
162
|
+
Relay slots are optional model-only exits. They do not replace the official Codex / ChatGPT login state.
|
|
163
|
+
Use `codex-slot relay add` to register a relay slot, then use `codex-slot status` for daily enable/disable and model-route selection.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
codex-slot relay add third --base-url https://relay.example.com/v1 --api-key sk-...
|
|
167
|
+
codex-slot use relay third
|
|
168
|
+
codex-slot current
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Behavior:
|
|
172
|
+
|
|
173
|
+
- `/v1/*` model requests are fixed to the selected relay slot
|
|
174
|
+
- Relay requests use the relay slot API key; client `Authorization` headers are not forwarded upstream
|
|
175
|
+
- Relay failures are returned directly and do not fall back to official cslot accounts
|
|
176
|
+
- `/backend-api/*` plugin/runtime requests still use the selected official Codex / ChatGPT login state
|
|
177
|
+
- Usage refresh only applies to official accounts, not relay slots
|
|
178
|
+
- In interactive `status`, press `Space` to enable/disable the selected account or relay slot, and press `m` to select the model route
|
|
179
|
+
- Run `codex-slot use auth` to restore `/v1/*` model requests to the official account scheduler
|
|
180
|
+
|
|
149
181
|
## Codex App Plugins
|
|
150
182
|
|
|
151
183
|
`codex-slot start` also switches the main `~/.codex` login state to the selected managed account while cslot is running.
|
|
@@ -3,11 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.refreshStatusSnapshot = refreshStatusSnapshot;
|
|
4
4
|
exports.getStatusSnapshot = getStatusSnapshot;
|
|
5
5
|
exports.persistAccountEnabledState = persistAccountEnabledState;
|
|
6
|
+
exports.persistRelayEnabledState = persistRelayEnabledState;
|
|
6
7
|
const config_1 = require("../config");
|
|
7
8
|
const scheduler_1 = require("../scheduler");
|
|
8
9
|
const status_1 = require("../status");
|
|
9
10
|
const state_1 = require("../state");
|
|
11
|
+
const relay_store_1 = require("../relay-store");
|
|
12
|
+
const state_2 = require("../state");
|
|
10
13
|
const usage_sync_1 = require("../usage-sync");
|
|
14
|
+
function formatModelRouteLabel(route) {
|
|
15
|
+
if (route.mode === "relay_slot") {
|
|
16
|
+
return `relay:${route.relay_slot_id}`;
|
|
17
|
+
}
|
|
18
|
+
return "auth_pool";
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* 刷新全部账号额度并返回最新状态快照。
|
|
13
22
|
*
|
|
@@ -27,8 +36,12 @@ async function refreshStatusSnapshot() {
|
|
|
27
36
|
function getStatusSnapshot() {
|
|
28
37
|
const statuses = (0, status_1.collectAccountStatuses)();
|
|
29
38
|
const selected = (0, scheduler_1.pickBestAccount)();
|
|
39
|
+
const modelRoute = (0, state_2.getSelectedModelRoute)();
|
|
30
40
|
return {
|
|
31
41
|
statuses,
|
|
42
|
+
relaySlots: (0, relay_store_1.listRelaySlots)(),
|
|
43
|
+
modelRoute,
|
|
44
|
+
modelRouteLabel: formatModelRouteLabel(modelRoute),
|
|
32
45
|
selectedName: selected?.account.name ?? null,
|
|
33
46
|
codexAuthAccountId: (0, state_1.getSelectedCodexAuthAccountId)(),
|
|
34
47
|
summary: (0, status_1.summarizeAccountStatuses)(statuses)
|
|
@@ -51,3 +64,20 @@ function persistAccountEnabledState(accounts) {
|
|
|
51
64
|
}
|
|
52
65
|
(0, config_1.saveConfig)(latest);
|
|
53
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* 将交互式界面中的 relay 启用状态修改写回配置文件。
|
|
69
|
+
*
|
|
70
|
+
* @param slots 用户在交互界面中调整后的 relay slot 数组。
|
|
71
|
+
* @returns 无返回值。
|
|
72
|
+
* @throws 当配置写入失败时抛出异常。
|
|
73
|
+
*/
|
|
74
|
+
function persistRelayEnabledState(slots) {
|
|
75
|
+
const latest = (0, config_1.loadConfig)();
|
|
76
|
+
for (const slot of slots) {
|
|
77
|
+
const index = latest.relay_slots.findIndex((item) => item.id === slot.id);
|
|
78
|
+
if (index >= 0) {
|
|
79
|
+
latest.relay_slots[index].enabled = slot.enabled;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
(0, config_1.saveConfig)(latest);
|
|
83
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const account_commands_1 = require("./account-commands");
|
|
6
6
|
const cli_helpers_1 = require("./cli-helpers");
|
|
7
7
|
const config_1 = require("./config");
|
|
8
|
+
const relay_commands_1 = require("./relay-commands");
|
|
8
9
|
const service_control_1 = require("./service-control");
|
|
9
10
|
const status_command_1 = require("./status-command");
|
|
10
11
|
const text_1 = require("./text");
|
|
@@ -71,6 +72,66 @@ function registerAccountCommands(program) {
|
|
|
71
72
|
.argument("<newName>", (0, text_1.bi)("新账号或工作空间标识", "New managed slot name"))
|
|
72
73
|
.action(account_commands_1.handleAccountRename);
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 注册 relay 与模型出口选择相关子命令。
|
|
77
|
+
*
|
|
78
|
+
* @param program Commander 程序实例。
|
|
79
|
+
* @returns 无返回值。
|
|
80
|
+
* @throws 无显式抛出。
|
|
81
|
+
*/
|
|
82
|
+
function registerRelayCommands(program) {
|
|
83
|
+
const relay = program
|
|
84
|
+
.command("relay")
|
|
85
|
+
.description((0, text_1.bi)("管理 OpenAI-compatible 中转槽位", "Manage OpenAI-compatible relay slots"));
|
|
86
|
+
relay
|
|
87
|
+
.command("add")
|
|
88
|
+
.description((0, text_1.bi)("新增一个中转槽位", "Add a relay slot"))
|
|
89
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
90
|
+
.requiredOption("--base-url <url>", (0, text_1.bi)("OpenAI-compatible base_url,通常以 /v1 结尾", "OpenAI-compatible base_url, usually ending with /v1"))
|
|
91
|
+
.requiredOption("--api-key <key>", (0, text_1.bi)("中转 API key", "Relay API key"))
|
|
92
|
+
.action(relay_commands_1.handleRelayAdd);
|
|
93
|
+
relay
|
|
94
|
+
.command("list")
|
|
95
|
+
.description((0, text_1.bi)("列出中转槽位", "List relay slots"))
|
|
96
|
+
.action(relay_commands_1.handleRelayList);
|
|
97
|
+
relay
|
|
98
|
+
.command("del")
|
|
99
|
+
.description((0, text_1.bi)("删除一个中转槽位", "Remove a relay slot"))
|
|
100
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
101
|
+
.action(relay_commands_1.handleRelayRemove);
|
|
102
|
+
relay
|
|
103
|
+
.command("rename")
|
|
104
|
+
.description((0, text_1.bi)("重命名一个中转槽位", "Rename a relay slot"))
|
|
105
|
+
.argument("<oldName>", (0, text_1.bi)("原中转槽位名", "Old relay slot name"))
|
|
106
|
+
.argument("<newName>", (0, text_1.bi)("新中转槽位名", "New relay slot name"))
|
|
107
|
+
.action(relay_commands_1.handleRelayRename);
|
|
108
|
+
relay
|
|
109
|
+
.command("enable")
|
|
110
|
+
.description((0, text_1.bi)("启用一个中转槽位", "Enable a relay slot"))
|
|
111
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
112
|
+
.action(relay_commands_1.handleRelayEnable);
|
|
113
|
+
relay
|
|
114
|
+
.command("disable")
|
|
115
|
+
.description((0, text_1.bi)("禁用一个中转槽位", "Disable a relay slot"))
|
|
116
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
117
|
+
.action(relay_commands_1.handleRelayDisable);
|
|
118
|
+
const use = program
|
|
119
|
+
.command("use")
|
|
120
|
+
.description((0, text_1.bi)("切换模型请求出口", "Switch model route"));
|
|
121
|
+
use
|
|
122
|
+
.command("relay")
|
|
123
|
+
.description((0, text_1.bi)("固定模型请求到指定中转槽位", "Fix model route to a relay slot"))
|
|
124
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
125
|
+
.action(relay_commands_1.handleUseRelay);
|
|
126
|
+
use
|
|
127
|
+
.command("auth")
|
|
128
|
+
.description((0, text_1.bi)("恢复模型请求到官方账号池", "Restore model route to the official auth pool"))
|
|
129
|
+
.action(relay_commands_1.handleUseAuthPool);
|
|
130
|
+
program
|
|
131
|
+
.command("current")
|
|
132
|
+
.description((0, text_1.bi)("查看当前模型出口与 Codex App 登录态选择", "Show current model route and Codex App auth selection"))
|
|
133
|
+
.action(relay_commands_1.handleCurrent);
|
|
134
|
+
}
|
|
74
135
|
/**
|
|
75
136
|
* 注册配置与状态相关子命令。
|
|
76
137
|
*
|
|
@@ -81,6 +142,7 @@ function registerAccountCommands(program) {
|
|
|
81
142
|
function registerRuntimeCommands(program) {
|
|
82
143
|
program
|
|
83
144
|
.command("status")
|
|
145
|
+
.alias("usage")
|
|
84
146
|
.description((0, text_1.bi)("刷新并查看所有已录入账号或工作空间的最新额度", "Refresh usage for all managed slots"))
|
|
85
147
|
.option("--no-interactive", (0, text_1.bi)("仅输出状态表,不进入交互式切换", "Print only"))
|
|
86
148
|
.action(async (options) => {
|
|
@@ -116,6 +178,7 @@ async function main() {
|
|
|
116
178
|
(0, config_1.loadConfig)();
|
|
117
179
|
configureRootProgram(program);
|
|
118
180
|
registerAccountCommands(program);
|
|
181
|
+
registerRelayCommands(program);
|
|
119
182
|
registerRuntimeCommands(program);
|
|
120
183
|
await program.parseAsync(process.argv);
|
|
121
184
|
}
|
package/dist/codex-config.js
CHANGED
|
@@ -325,53 +325,6 @@ function findTableHeaderOffset(content, header) {
|
|
|
325
325
|
}
|
|
326
326
|
return null;
|
|
327
327
|
}
|
|
328
|
-
/**
|
|
329
|
-
* 查找指定偏移之前最近的表头,供恢复原有表块位置时作为后备锚点。
|
|
330
|
-
*
|
|
331
|
-
* @param content 当前 `config.toml` 内容。
|
|
332
|
-
* @param offset 截止偏移。
|
|
333
|
-
* @returns 最近的表头文本;未命中返回 `null`。
|
|
334
|
-
*/
|
|
335
|
-
function findPreviousTableHeaderBeforeOffset(content, offset) {
|
|
336
|
-
const lines = content.split(/\r?\n/);
|
|
337
|
-
let currentOffset = 0;
|
|
338
|
-
let previousHeader = null;
|
|
339
|
-
for (const line of lines) {
|
|
340
|
-
const lineEnd = currentOffset + line.length;
|
|
341
|
-
const trimmed = line.trim();
|
|
342
|
-
if (currentOffset >= offset) {
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
if (trimmed.startsWith("[") && !trimmed.startsWith("[[") && !trimmed.startsWith("#")) {
|
|
346
|
-
previousHeader = trimmed;
|
|
347
|
-
}
|
|
348
|
-
currentOffset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
349
|
-
}
|
|
350
|
-
return previousHeader;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* 查找指定偏移之后的首个表头,供恢复原有表块位置时作为优先锚点。
|
|
354
|
-
*
|
|
355
|
-
* @param content 当前 `config.toml` 内容。
|
|
356
|
-
* @param offset 起始偏移。
|
|
357
|
-
* @returns 首个后续表头文本;未命中返回 `null`。
|
|
358
|
-
*/
|
|
359
|
-
function findNextTableHeaderAfterOffset(content, offset) {
|
|
360
|
-
const lines = content.split(/\r?\n/);
|
|
361
|
-
let currentOffset = 0;
|
|
362
|
-
for (const line of lines) {
|
|
363
|
-
const lineEnd = currentOffset + line.length;
|
|
364
|
-
const trimmed = line.trim();
|
|
365
|
-
if (currentOffset >= offset &&
|
|
366
|
-
trimmed.startsWith("[") &&
|
|
367
|
-
!trimmed.startsWith("[[") &&
|
|
368
|
-
!trimmed.startsWith("#")) {
|
|
369
|
-
return trimmed;
|
|
370
|
-
}
|
|
371
|
-
currentOffset = lineEnd + (content.slice(lineEnd, lineEnd + 2) === "\r\n" ? 2 : 1);
|
|
372
|
-
}
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
328
|
/**
|
|
376
329
|
* 清理文本中的所有 `model_provider` 配置块,确保每次接管都以单一稳定块重新写入。
|
|
377
330
|
*
|
|
@@ -414,6 +367,35 @@ function stripAllManagedBlocks(content) {
|
|
|
414
367
|
const withoutProviderBlock = stripMarkedBlocks(content, PROVIDER_BLOCK_START_MARKER, PROVIDER_BLOCK_END_MARKER);
|
|
415
368
|
return stripMarkedBlocks(withoutProviderBlock, MODEL_PROVIDER_START_MARKER, MODEL_PROVIDER_END_MARKER);
|
|
416
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* 清理文本中的所有 `model_provider = "cslot"` 根级配置,避免 stop 后残留 cslot 入口。
|
|
372
|
+
*
|
|
373
|
+
* @param content 当前 `config.toml` 内容。
|
|
374
|
+
* @returns 移除后的文本内容。
|
|
375
|
+
*/
|
|
376
|
+
function removeAllCslotModelProviderLines(content) {
|
|
377
|
+
let nextContent = content;
|
|
378
|
+
while (true) {
|
|
379
|
+
const range = findModelProviderLine(nextContent);
|
|
380
|
+
if (!range) {
|
|
381
|
+
return nextContent;
|
|
382
|
+
}
|
|
383
|
+
if (range.value.includes('model_provider = "cslot"')) {
|
|
384
|
+
nextContent = nextContent.slice(0, range.start) + nextContent.slice(range.end);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
return nextContent;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 在 stop 阶段只做去 cslot 化,移除所有受管块与残留的 cslot provider 配置。
|
|
392
|
+
*
|
|
393
|
+
* @param content 当前 `config.toml` 内容。
|
|
394
|
+
* @returns 已去除 cslot 接管痕迹的文本。
|
|
395
|
+
*/
|
|
396
|
+
function removeAllCslotManagedConfig(content) {
|
|
397
|
+
return removeAllProviderSections(removeAllCslotModelProviderLines(stripAllManagedBlocks(content)));
|
|
398
|
+
}
|
|
417
399
|
/**
|
|
418
400
|
* 查找根级配置区的尾部插入点。
|
|
419
401
|
*
|
|
@@ -487,86 +469,6 @@ function appendBlockToEnd(content, block, eol) {
|
|
|
487
469
|
}
|
|
488
470
|
return `${trimmed}${eol}${eol}${block}${eol}`;
|
|
489
471
|
}
|
|
490
|
-
/**
|
|
491
|
-
* 将表块尽量插回原有相邻表头附近;若锚点已不存在,则退回文件尾部追加。
|
|
492
|
-
*
|
|
493
|
-
* @param content 当前 `config.toml` 内容。
|
|
494
|
-
* @param block 待插入的表块。
|
|
495
|
-
* @param eol 目标换行符。
|
|
496
|
-
* @param preferredNextTableHeader 原始后续表头锚点,命中时优先插到该表之前。
|
|
497
|
-
* @param preferredPreviousTableHeader 原始前驱表头锚点,当前者失效时插到该表之后。
|
|
498
|
-
* @returns 插入后的完整文本。
|
|
499
|
-
*/
|
|
500
|
-
function insertTableBlock(content, block, eol, preferredNextTableHeader, preferredPreviousTableHeader) {
|
|
501
|
-
if (preferredNextTableHeader) {
|
|
502
|
-
const nextOffset = findTableHeaderOffset(content, preferredNextTableHeader);
|
|
503
|
-
if (nextOffset !== null) {
|
|
504
|
-
return insertBlockBetween(content.slice(0, nextOffset), block, content.slice(nextOffset), eol);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (preferredPreviousTableHeader) {
|
|
508
|
-
const previousRange = findTableSectionRange(content, preferredPreviousTableHeader);
|
|
509
|
-
if (previousRange) {
|
|
510
|
-
return insertBlockBetween(content.slice(0, previousRange.end), block, content.slice(previousRange.end), eol);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return appendBlockToEnd(content, block, eol);
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* 解析当前目标文件对应的上一轮接管快照。
|
|
517
|
-
*
|
|
518
|
-
* @param targetFile 当前准备接管或恢复的 `config.toml` 路径。
|
|
519
|
-
* @returns 命中同一目标文件时返回上一轮快照;否则返回 `null`。
|
|
520
|
-
*/
|
|
521
|
-
function resolveManagedStateForTarget(targetFile) {
|
|
522
|
-
const managedState = (0, state_1.getManagedCodexConfigState)();
|
|
523
|
-
if (!managedState || managedState.target_file !== targetFile) {
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
return managedState;
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* 基于当前未受管的配置文本与上一轮快照,生成本轮接管所需的最小恢复快照。
|
|
530
|
-
*
|
|
531
|
-
* 业务规则:
|
|
532
|
-
* 1. 优先记录当前文件里实际存在的原始 `model_provider` 与 `[model_providers.cslot]`。
|
|
533
|
-
* 2. 若当前文件只剩残留受管块,允许继承上一轮快照中的原始片段。
|
|
534
|
-
* 3. 仅保存 cslot 自己声明所有权的两块配置及其锚点,不保存整文件内容。
|
|
535
|
-
*
|
|
536
|
-
* @param targetFile 当前准备接管的 `config.toml` 路径。
|
|
537
|
-
* @param strippedCurrent 已移除受管标记块后的配置文本。
|
|
538
|
-
* @param previousManagedState 同一目标文件的上一轮快照;不存在时传 `null`。
|
|
539
|
-
* @returns 本轮接管后用于 stop 恢复的快照。
|
|
540
|
-
*/
|
|
541
|
-
function buildManagedSnapshot(targetFile, strippedCurrent, previousManagedState) {
|
|
542
|
-
const originalModelProviderLine = findModelProviderLine(strippedCurrent);
|
|
543
|
-
const originalProviderSection = findProviderSectionRange(strippedCurrent);
|
|
544
|
-
return {
|
|
545
|
-
target_file: targetFile,
|
|
546
|
-
original_model_provider_block: originalModelProviderLine?.value ??
|
|
547
|
-
previousManagedState?.original_model_provider_block ??
|
|
548
|
-
null,
|
|
549
|
-
original_model_provider_next_table_header: (originalModelProviderLine
|
|
550
|
-
? findNextTableHeaderAfterOffset(strippedCurrent, originalModelProviderLine.end)
|
|
551
|
-
: null) ??
|
|
552
|
-
previousManagedState?.original_model_provider_next_table_header ??
|
|
553
|
-
null,
|
|
554
|
-
original_cslot_provider_block: (originalProviderSection ? sanitizeLegacyCslotProviderBlock(originalProviderSection.value) : null) ??
|
|
555
|
-
(previousManagedState?.original_cslot_provider_block
|
|
556
|
-
? sanitizeLegacyCslotProviderBlock(previousManagedState.original_cslot_provider_block)
|
|
557
|
-
: null),
|
|
558
|
-
original_cslot_provider_previous_table_header: (originalProviderSection
|
|
559
|
-
? findPreviousTableHeaderBeforeOffset(strippedCurrent, originalProviderSection.start)
|
|
560
|
-
: null) ??
|
|
561
|
-
previousManagedState?.original_cslot_provider_previous_table_header ??
|
|
562
|
-
null,
|
|
563
|
-
original_cslot_provider_next_table_header: (originalProviderSection
|
|
564
|
-
? findNextTableHeaderAfterOffset(strippedCurrent, originalProviderSection.end)
|
|
565
|
-
: null) ??
|
|
566
|
-
previousManagedState?.original_cslot_provider_next_table_header ??
|
|
567
|
-
null
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
472
|
/**
|
|
571
473
|
* 将 cslot 需要的 provider 配置写入指定 `config.toml`,并保存恢复快照。
|
|
572
474
|
*
|
|
@@ -579,21 +481,19 @@ function applyManagedCodexConfig(targetPathOrDir, options) {
|
|
|
579
481
|
const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
|
|
580
482
|
const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
|
|
581
483
|
const current = node_fs_1.default.existsSync(targetFile) ? node_fs_1.default.readFileSync(targetFile, "utf8") : "";
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
const eol = detectEol(strippedCurrent);
|
|
585
|
-
const snapshot = buildManagedSnapshot(targetFile, strippedCurrent, previousManagedState);
|
|
484
|
+
const cleanedCurrent = removeAllCslotManagedConfig(current);
|
|
485
|
+
const eol = detectEol(cleanedCurrent);
|
|
586
486
|
const config = options?.config ?? (0, config_1.loadConfig)();
|
|
587
487
|
const managedModelProviderBlock = buildManagedModelProviderBlock(eol);
|
|
588
488
|
const managedProviderBlock = buildManagedProviderBlock(eol, config);
|
|
589
|
-
const cleanedBaseContent = removeAllProviderSections(removeAllModelProviderLines(
|
|
590
|
-
let nextContent = insertRootBlock(cleanedBaseContent, managedModelProviderBlock, eol
|
|
489
|
+
const cleanedBaseContent = removeAllProviderSections(removeAllModelProviderLines(cleanedCurrent));
|
|
490
|
+
let nextContent = insertRootBlock(cleanedBaseContent, managedModelProviderBlock, eol);
|
|
591
491
|
nextContent = appendBlockToEnd(nextContent, managedProviderBlock, eol);
|
|
592
492
|
if (!nextContent.endsWith(eol)) {
|
|
593
493
|
nextContent = `${nextContent}${eol}`;
|
|
594
494
|
}
|
|
595
495
|
writeFileAtomic(targetFile, nextContent);
|
|
596
|
-
(0, state_1.setManagedCodexConfigState)(
|
|
496
|
+
(0, state_1.setManagedCodexConfigState)({ target_file: targetFile });
|
|
597
497
|
if (!options?.silent) {
|
|
598
498
|
console.log((0, text_1.bi)(`已写入: ${targetFile}`, `Written to: ${targetFile}`));
|
|
599
499
|
console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
|
|
@@ -609,25 +509,13 @@ function applyManagedCodexConfig(targetPathOrDir, options) {
|
|
|
609
509
|
*/
|
|
610
510
|
function deactivateManagedCodexConfig() {
|
|
611
511
|
const managedState = (0, state_1.getManagedCodexConfigState)();
|
|
612
|
-
|
|
613
|
-
return null;
|
|
614
|
-
}
|
|
615
|
-
const targetFile = managedState.target_file;
|
|
512
|
+
const targetFile = managedState?.target_file ?? getDefaultCodexConfigPath();
|
|
616
513
|
if (!node_fs_1.default.existsSync(targetFile)) {
|
|
617
514
|
(0, state_1.clearManagedCodexConfigState)();
|
|
618
515
|
return null;
|
|
619
516
|
}
|
|
620
517
|
const current = node_fs_1.default.readFileSync(targetFile, "utf8");
|
|
621
|
-
const
|
|
622
|
-
let restored = sanitizeExistingCslotProviderSection(stripAllManagedBlocks(current));
|
|
623
|
-
const existingModelProviderLine = findModelProviderLine(restored);
|
|
624
|
-
if (!existingModelProviderLine && managedState.original_model_provider_block) {
|
|
625
|
-
restored = insertRootBlock(restored, managedState.original_model_provider_block, eol, managedState.original_model_provider_next_table_header);
|
|
626
|
-
}
|
|
627
|
-
const existingProviderSection = findProviderSectionRange(restored);
|
|
628
|
-
if (!existingProviderSection && managedState.original_cslot_provider_block) {
|
|
629
|
-
restored = insertTableBlock(restored, managedState.original_cslot_provider_block, eol, managedState.original_cslot_provider_next_table_header, managedState.original_cslot_provider_previous_table_header);
|
|
630
|
-
}
|
|
518
|
+
const restored = removeAllCslotManagedConfig(sanitizeExistingCslotProviderSection(current));
|
|
631
519
|
writeFileAtomic(targetFile, restored);
|
|
632
520
|
(0, state_1.clearManagedCodexConfigState)();
|
|
633
521
|
return targetFile;
|
package/dist/config.js
CHANGED
|
@@ -26,6 +26,14 @@ const managedAccountSchema = zod_1.z.object({
|
|
|
26
26
|
enabled: zod_1.z.boolean().default(true),
|
|
27
27
|
imported_at: zod_1.z.string().optional()
|
|
28
28
|
});
|
|
29
|
+
const relaySlotSchema = zod_1.z.object({
|
|
30
|
+
id: zod_1.z.string().min(1),
|
|
31
|
+
name: zod_1.z.string().min(1),
|
|
32
|
+
base_url: zod_1.z.string().url(),
|
|
33
|
+
api_key: zod_1.z.string().min(1),
|
|
34
|
+
enabled: zod_1.z.boolean().default(true),
|
|
35
|
+
imported_at: zod_1.z.string().optional()
|
|
36
|
+
});
|
|
29
37
|
const configSchema = zod_1.z.object({
|
|
30
38
|
version: zod_1.z.number().int().default(1),
|
|
31
39
|
server: zod_1.z
|
|
@@ -52,7 +60,8 @@ const configSchema = zod_1.z.object({
|
|
|
52
60
|
auth_base_url: "https://auth.openai.com",
|
|
53
61
|
oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
54
62
|
}),
|
|
55
|
-
accounts: zod_1.z.array(managedAccountSchema).default([])
|
|
63
|
+
accounts: zod_1.z.array(managedAccountSchema).default([]),
|
|
64
|
+
relay_slots: zod_1.z.array(relaySlotSchema).default([])
|
|
56
65
|
});
|
|
57
66
|
/**
|
|
58
67
|
* 解析当前进程应使用的用户 HOME 目录,兼容 Windows 缺少 `HOME` 的场景。
|
|
@@ -138,7 +147,8 @@ function loadConfig() {
|
|
|
138
147
|
auth_base_url: "https://auth.openai.com",
|
|
139
148
|
oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
140
149
|
},
|
|
141
|
-
accounts: []
|
|
150
|
+
accounts: [],
|
|
151
|
+
relay_slots: []
|
|
142
152
|
};
|
|
143
153
|
saveConfig(defaultConfig);
|
|
144
154
|
return defaultConfig;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.proxyModelWithRoute = void 0;
|
|
4
|
+
exports.createModelProxyDispatcher = createModelProxyDispatcher;
|
|
5
|
+
const proxy_retry_service_1 = require("./proxy-retry-service");
|
|
6
|
+
const relay_proxy_service_1 = require("./relay-proxy-service");
|
|
7
|
+
const relay_store_1 = require("./relay-store");
|
|
8
|
+
const state_1 = require("./state");
|
|
9
|
+
function buildSendResult(statusCode, payload, headers) {
|
|
10
|
+
return {
|
|
11
|
+
type: "send",
|
|
12
|
+
statusCode,
|
|
13
|
+
payload,
|
|
14
|
+
headers
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 创建 `/v1/*` 模型请求分发器。
|
|
19
|
+
*
|
|
20
|
+
* 业务含义:
|
|
21
|
+
* 1. 默认 `auth_pool` 模式保持原有官方账号自动调度行为。
|
|
22
|
+
* 2. 手动选择 `relay_slot` 后,模型请求固定走该 relay slot。
|
|
23
|
+
* 3. relay 模式不影响 `/backend-api/*` 插件链路,也不会失败回退官方账号。
|
|
24
|
+
*
|
|
25
|
+
* @param overrides 可选依赖覆盖项。
|
|
26
|
+
* @returns 模型请求分发器实例。
|
|
27
|
+
* @throws 无显式抛出。
|
|
28
|
+
*/
|
|
29
|
+
function createModelProxyDispatcher(overrides) {
|
|
30
|
+
const dependencies = {
|
|
31
|
+
getSelectedModelRoute: state_1.getSelectedModelRoute,
|
|
32
|
+
findRelaySlot: relay_store_1.findRelaySlot,
|
|
33
|
+
proxyCodexWithRetry: proxy_retry_service_1.proxyCodexWithRetry,
|
|
34
|
+
proxyRelaySlot: relay_proxy_service_1.proxyRelaySlot,
|
|
35
|
+
...overrides
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
async proxyModelWithRoute(request) {
|
|
39
|
+
const route = dependencies.getSelectedModelRoute();
|
|
40
|
+
if (route.mode !== "relay_slot") {
|
|
41
|
+
return await dependencies.proxyCodexWithRetry(request);
|
|
42
|
+
}
|
|
43
|
+
const slot = dependencies.findRelaySlot(route.relay_slot_id);
|
|
44
|
+
if (!slot) {
|
|
45
|
+
return buildSendResult(503, {
|
|
46
|
+
error: {
|
|
47
|
+
message: `Relay slot not found: ${route.relay_slot_id}`,
|
|
48
|
+
type: "relay_slot_not_found"
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!slot.enabled) {
|
|
53
|
+
return buildSendResult(503, {
|
|
54
|
+
error: {
|
|
55
|
+
message: `Relay slot is disabled: ${route.relay_slot_id}`,
|
|
56
|
+
type: "relay_slot_disabled"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return await dependencies.proxyRelaySlot({
|
|
61
|
+
slot,
|
|
62
|
+
request
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
exports.proxyModelWithRoute = createModelProxyDispatcher().proxyModelWithRoute;
|