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.
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleRelayAdd = handleRelayAdd;
4
+ exports.handleRelayList = handleRelayList;
5
+ exports.handleRelayRemove = handleRelayRemove;
6
+ exports.handleRelayRename = handleRelayRename;
7
+ exports.handleRelayEnable = handleRelayEnable;
8
+ exports.handleRelayDisable = handleRelayDisable;
9
+ exports.handleUseRelay = handleUseRelay;
10
+ exports.handleUseAuthPool = handleUseAuthPool;
11
+ exports.handleCurrent = handleCurrent;
12
+ const relay_store_1 = require("./relay-store");
13
+ const state_1 = require("./state");
14
+ const text_1 = require("./text");
15
+ function maskApiKey(apiKey) {
16
+ if (apiKey.length <= 8) {
17
+ return "****";
18
+ }
19
+ return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
20
+ }
21
+ function formatModelRoute() {
22
+ const route = (0, state_1.getSelectedModelRoute)();
23
+ if (route.mode === "relay_slot") {
24
+ return `relay:${route.relay_slot_id}`;
25
+ }
26
+ return "auth_pool";
27
+ }
28
+ /**
29
+ * 新增 OpenAI-compatible relay slot。
30
+ *
31
+ * @param name relay slot 名称。
32
+ * @param options relay 参数,必须包含 baseUrl 与 apiKey。
33
+ * @returns 无返回值。
34
+ * @throws 当 slot 已存在、URL 非法或配置写入失败时抛出异常。
35
+ */
36
+ function handleRelayAdd(name, options) {
37
+ const slot = (0, relay_store_1.addRelaySlot)({
38
+ name,
39
+ baseUrl: options.baseUrl,
40
+ apiKey: options.apiKey
41
+ });
42
+ console.log((0, text_1.bi)(`已添加中转槽位: ${slot.name}`, `Relay slot added: ${slot.name}`));
43
+ }
44
+ /**
45
+ * 列出 OpenAI-compatible relay slot。
46
+ *
47
+ * @returns 无返回值。
48
+ * @throws 当配置读取失败时抛出异常。
49
+ */
50
+ function handleRelayList() {
51
+ const slots = (0, relay_store_1.listRelaySlots)();
52
+ const route = (0, state_1.getSelectedModelRoute)();
53
+ if (slots.length === 0) {
54
+ console.log((0, text_1.bi)("当前没有中转槽位。", "No relay slots found."));
55
+ return;
56
+ }
57
+ for (const slot of slots) {
58
+ const selected = route.mode === "relay_slot" && route.relay_slot_id === slot.id ? "*" : " ";
59
+ const enabled = slot.enabled ? "enabled" : "disabled";
60
+ console.log(`${selected} ${slot.name} ${enabled} ${slot.base_url} key=${maskApiKey(slot.api_key)}`);
61
+ }
62
+ }
63
+ /**
64
+ * 删除 OpenAI-compatible relay slot。
65
+ *
66
+ * @param name relay slot 名称。
67
+ * @returns 无返回值。
68
+ * @throws 当配置写入失败时抛出异常。
69
+ */
70
+ function handleRelayRemove(name) {
71
+ const removed = (0, relay_store_1.removeRelaySlot)(name);
72
+ if (!removed) {
73
+ console.log((0, text_1.bi)(`未找到中转槽位: ${name}`, `Relay slot not found: ${name}`));
74
+ return;
75
+ }
76
+ console.log((0, text_1.bi)(`已删除中转槽位: ${name}`, `Relay slot removed: ${name}`));
77
+ }
78
+ /**
79
+ * 重命名 OpenAI-compatible relay slot。
80
+ *
81
+ * @param oldName 原 slot 名称。
82
+ * @param newName 新 slot 名称。
83
+ * @returns 无返回值。
84
+ * @throws 当旧 slot 不存在、新 slot 已存在或写入失败时抛出异常。
85
+ */
86
+ function handleRelayRename(oldName, newName) {
87
+ const slot = (0, relay_store_1.renameRelaySlot)(oldName, newName);
88
+ console.log((0, text_1.bi)(`已重命名中转槽位: ${oldName} -> ${slot.name}`, `Relay slot renamed: ${oldName} -> ${slot.name}`));
89
+ }
90
+ /**
91
+ * 启用或禁用 OpenAI-compatible relay slot。
92
+ *
93
+ * @param name relay slot 名称。
94
+ * @param enabled 是否启用。
95
+ * @returns 无返回值。
96
+ * @throws 当 slot 不存在或写入失败时抛出异常。
97
+ */
98
+ function handleRelayEnabled(name, enabled) {
99
+ const slot = (0, relay_store_1.setRelaySlotEnabled)(name, enabled);
100
+ console.log(enabled
101
+ ? (0, text_1.bi)(`已启用中转槽位: ${slot.name}`, `Relay slot enabled: ${slot.name}`)
102
+ : (0, text_1.bi)(`已禁用中转槽位: ${slot.name}`, `Relay slot disabled: ${slot.name}`));
103
+ }
104
+ /**
105
+ * 启用 OpenAI-compatible relay slot。
106
+ *
107
+ * @param name relay slot 名称。
108
+ * @returns 无返回值。
109
+ * @throws 当 slot 不存在或写入失败时抛出异常。
110
+ */
111
+ function handleRelayEnable(name) {
112
+ handleRelayEnabled(name, true);
113
+ }
114
+ /**
115
+ * 禁用 OpenAI-compatible relay slot。
116
+ *
117
+ * @param name relay slot 名称。
118
+ * @returns 无返回值。
119
+ * @throws 当 slot 不存在或写入失败时抛出异常。
120
+ */
121
+ function handleRelayDisable(name) {
122
+ handleRelayEnabled(name, false);
123
+ }
124
+ /**
125
+ * 将模型请求固定到指定 relay slot。
126
+ *
127
+ * @param name relay slot 名称。
128
+ * @returns 无返回值。
129
+ * @throws 当 relay slot 不存在或状态写入失败时抛出异常。
130
+ */
131
+ function handleUseRelay(name) {
132
+ const slot = (0, relay_store_1.findRelaySlot)(name);
133
+ if (!slot) {
134
+ throw new Error((0, text_1.bi)(`未找到中转槽位 ${name}`, `Relay slot not found: ${name}`));
135
+ }
136
+ if (!slot.enabled) {
137
+ throw new Error((0, text_1.bi)(`中转槽位 ${name} 已禁用`, `Relay slot is disabled: ${name}`));
138
+ }
139
+ (0, state_1.setSelectedModelRoute)({
140
+ mode: "relay_slot",
141
+ relay_slot_id: slot.id
142
+ });
143
+ console.log((0, text_1.bi)(`模型请求已固定到中转槽位: ${slot.name}`, `Model route fixed to relay slot: ${slot.name}`));
144
+ }
145
+ /**
146
+ * 恢复模型请求到官方账号自动调度池。
147
+ *
148
+ * @returns 无返回值。
149
+ * @throws 当状态写入失败时抛出异常。
150
+ */
151
+ function handleUseAuthPool() {
152
+ (0, state_1.setSelectedModelRoute)({
153
+ mode: "auth_pool"
154
+ });
155
+ console.log((0, text_1.bi)("模型请求已恢复官方账号池。", "Model route restored to auth pool."));
156
+ }
157
+ /**
158
+ * 输出当前模型出口与 Codex App 登录态选择。
159
+ *
160
+ * @returns 无返回值。
161
+ * @throws 当状态读取失败时抛出异常。
162
+ */
163
+ function handleCurrent() {
164
+ console.log(`model_route=${formatModelRoute()}`);
165
+ console.log(`codex_auth=${(0, state_1.getSelectedCodexAuthAccountId)() ?? "none"}`);
166
+ }
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.proxyRelaySlot = void 0;
4
+ exports.buildRelayHeaders = buildRelayHeaders;
5
+ exports.sendRelayRequest = sendRelayRequest;
6
+ exports.createRelayProxyService = createRelayProxyService;
7
+ const undici_1 = require("undici");
8
+ /**
9
+ * 构造 OpenAI-compatible relay 上游请求头。
10
+ *
11
+ * 业务含义:
12
+ * 1. 客户端发给本地 cslot 的 Authorization 不代表 relay 凭据,不能透传。
13
+ * 2. relay 模式只使用 relay slot 自己的 api_key 鉴权,并保留其余兼容请求头。
14
+ *
15
+ * @param requestHeaders 客户端发到本地服务的原始请求头。
16
+ * @param apiKey relay slot 的 API key。
17
+ * @param bodyLength 请求体字节长度;无 body 时不写 content-length。
18
+ * @returns 可发往 relay 上游的请求头。
19
+ * @throws 无显式抛出。
20
+ */
21
+ function buildRelayHeaders(requestHeaders, apiKey, bodyLength) {
22
+ const headers = {};
23
+ for (const [headerName, headerValue] of Object.entries(requestHeaders)) {
24
+ const normalizedName = headerName.toLowerCase();
25
+ if (headerValue == null ||
26
+ normalizedName === "authorization" ||
27
+ normalizedName === "host" ||
28
+ normalizedName === "connection" ||
29
+ normalizedName === "content-length") {
30
+ continue;
31
+ }
32
+ headers[normalizedName] = Array.isArray(headerValue)
33
+ ? headerValue.join(", ")
34
+ : headerValue;
35
+ }
36
+ headers.authorization = `Bearer ${apiKey}`;
37
+ if (typeof bodyLength === "number") {
38
+ headers["content-length"] = String(bodyLength);
39
+ }
40
+ headers["user-agent"] = "codex-slot/0.1.1";
41
+ return headers;
42
+ }
43
+ /**
44
+ * 将本地 `/v1/*` 请求解析成 relay base_url 后的相对路径。
45
+ *
46
+ * @param url 本地代理收到的 URL。
47
+ * @returns relay 上游相对 path 与 query;不属于 `/v1/*` 时返回 `null`。
48
+ * @throws URL 解析失败时透传异常。
49
+ */
50
+ function resolveRelayPath(url) {
51
+ const parsedUrl = new URL(url, "http://127.0.0.1");
52
+ const openAiPrefix = "/v1";
53
+ if (!parsedUrl.pathname.startsWith(`${openAiPrefix}/`)) {
54
+ return null;
55
+ }
56
+ return `${parsedUrl.pathname.slice(openAiPrefix.length)}${parsedUrl.search}`;
57
+ }
58
+ /**
59
+ * 提取上游响应中允许透传给客户端的响应头。
60
+ *
61
+ * @param headers 上游响应头对象。
62
+ * @returns 可透传响应头。
63
+ * @throws 无显式抛出。
64
+ */
65
+ function pickResponseHeaders(headers) {
66
+ const picked = {};
67
+ const contentType = headers["content-type"];
68
+ const cacheControl = headers["cache-control"];
69
+ if (typeof contentType === "string") {
70
+ picked["content-type"] = contentType;
71
+ }
72
+ if (typeof cacheControl === "string") {
73
+ picked["cache-control"] = cacheControl;
74
+ }
75
+ return picked;
76
+ }
77
+ function buildSendResult(statusCode, payload, headers) {
78
+ return {
79
+ type: "send",
80
+ statusCode,
81
+ payload,
82
+ headers
83
+ };
84
+ }
85
+ /**
86
+ * 向 OpenAI-compatible relay 上游发送一次请求。
87
+ *
88
+ * @param options relay 请求参数。
89
+ * @returns undici 上游响应对象。
90
+ * @throws 当网络层或 undici 请求失败时透传底层异常。
91
+ */
92
+ async function sendRelayRequest(options) {
93
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
94
+ const pathWithQuery = options.pathWithQuery.startsWith("/")
95
+ ? options.pathWithQuery
96
+ : `/${options.pathWithQuery}`;
97
+ return await (0, undici_1.request)(`${baseUrl}${pathWithQuery}`, {
98
+ method: options.method,
99
+ headers: options.headers,
100
+ body: options.body && options.body.length > 0 ? options.body : undefined
101
+ });
102
+ }
103
+ /**
104
+ * 创建 relay 模型代理服务。
105
+ *
106
+ * 业务含义:
107
+ * 1. relay slot 是手动固定的模型出口,不参与官方账号自动调度。
108
+ * 2. relay 上游返回 401/429/5xx 时原样返回给客户端,不回退到官方账号。
109
+ *
110
+ * @param overrides 可选依赖覆盖项。
111
+ * @returns relay 代理服务实例。
112
+ * @throws 无显式抛出。
113
+ */
114
+ function createRelayProxyService(overrides) {
115
+ const dependencies = {
116
+ sendRelayRequest,
117
+ ...overrides
118
+ };
119
+ return {
120
+ async proxyRelaySlot(options) {
121
+ const pathWithQuery = resolveRelayPath(options.request.url);
122
+ if (!pathWithQuery) {
123
+ return buildSendResult(404, {
124
+ error: {
125
+ message: "Unsupported relay proxy path",
126
+ type: "unsupported_relay_proxy_path"
127
+ }
128
+ });
129
+ }
130
+ const bodyLength = options.request.body && options.request.body.length > 0
131
+ ? options.request.body.length
132
+ : undefined;
133
+ let upstream;
134
+ try {
135
+ upstream = await dependencies.sendRelayRequest({
136
+ baseUrl: options.slot.base_url,
137
+ method: options.request.method.toUpperCase(),
138
+ pathWithQuery,
139
+ headers: buildRelayHeaders(options.request.headers, options.slot.api_key, bodyLength),
140
+ body: options.request.body
141
+ });
142
+ }
143
+ catch (error) {
144
+ return buildSendResult(502, {
145
+ error: {
146
+ message: error instanceof Error ? error.message : String(error),
147
+ type: "relay_request_failed"
148
+ }
149
+ });
150
+ }
151
+ return {
152
+ type: "proxy",
153
+ statusCode: upstream.statusCode,
154
+ headers: pickResponseHeaders(upstream.headers),
155
+ body: upstream.body
156
+ };
157
+ }
158
+ };
159
+ }
160
+ exports.proxyRelaySlot = createRelayProxyService().proxyRelaySlot;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listRelaySlots = listRelaySlots;
4
+ exports.findRelaySlot = findRelaySlot;
5
+ exports.addRelaySlot = addRelaySlot;
6
+ exports.removeRelaySlot = removeRelaySlot;
7
+ exports.renameRelaySlot = renameRelaySlot;
8
+ exports.setRelaySlotEnabled = setRelaySlotEnabled;
9
+ const config_1 = require("./config");
10
+ const state_1 = require("./state");
11
+ const text_1 = require("./text");
12
+ /**
13
+ * 列出所有 OpenAI-compatible relay 槽位。
14
+ *
15
+ * @returns 当前配置中的 relay slot 列表。
16
+ * @throws 当配置读取失败时抛出异常。
17
+ */
18
+ function listRelaySlots() {
19
+ return (0, config_1.loadConfig)().relay_slots;
20
+ }
21
+ /**
22
+ * 查找指定 relay slot。
23
+ *
24
+ * @param slotId relay slot 标识。
25
+ * @returns 命中时返回 slot;不存在时返回 `null`。
26
+ * @throws 当配置读取失败时抛出异常。
27
+ */
28
+ function findRelaySlot(slotId) {
29
+ return listRelaySlots().find((item) => item.id === slotId) ?? null;
30
+ }
31
+ /**
32
+ * 新增一个 OpenAI-compatible relay slot。
33
+ *
34
+ * @param input relay slot 配置。
35
+ * @returns 新增后的 relay slot。
36
+ * @throws 当同名 slot 已存在或配置写入失败时抛出异常。
37
+ */
38
+ function addRelaySlot(input) {
39
+ const config = (0, config_1.loadConfig)();
40
+ if (config.relay_slots.some((item) => item.id === input.name)) {
41
+ throw new Error((0, text_1.bi)(`中转槽位 ${input.name} 已存在`, `Relay slot already exists: ${input.name}`));
42
+ }
43
+ try {
44
+ new URL(input.baseUrl);
45
+ }
46
+ catch {
47
+ throw new Error((0, text_1.bi)(`中转 base_url 非法: ${input.baseUrl}`, `Invalid relay base_url: ${input.baseUrl}`));
48
+ }
49
+ const slot = {
50
+ id: input.name,
51
+ name: input.name,
52
+ base_url: input.baseUrl,
53
+ api_key: input.apiKey,
54
+ enabled: true,
55
+ imported_at: new Date().toISOString()
56
+ };
57
+ config.relay_slots.push(slot);
58
+ (0, config_1.saveConfig)(config);
59
+ return slot;
60
+ }
61
+ /**
62
+ * 删除指定 relay slot,并在当前模型出口指向它时恢复官方账号池。
63
+ *
64
+ * @param slotId relay slot 标识。
65
+ * @returns 被删除的 slot;不存在时返回 `null`。
66
+ * @throws 当配置或状态写入失败时抛出异常。
67
+ */
68
+ function removeRelaySlot(slotId) {
69
+ const config = (0, config_1.loadConfig)();
70
+ const index = config.relay_slots.findIndex((item) => item.id === slotId);
71
+ if (index < 0) {
72
+ return null;
73
+ }
74
+ const [removed] = config.relay_slots.splice(index, 1);
75
+ (0, config_1.saveConfig)(config);
76
+ const route = (0, state_1.getSelectedModelRoute)();
77
+ if (route.mode === "relay_slot" && route.relay_slot_id === slotId) {
78
+ (0, state_1.setSelectedModelRoute)({ mode: "auth_pool" });
79
+ }
80
+ return removed;
81
+ }
82
+ /**
83
+ * 重命名 relay slot,并迁移当前模型出口选择。
84
+ *
85
+ * @param oldName 原 slot 标识。
86
+ * @param newName 新 slot 标识。
87
+ * @returns 重命名后的 relay slot。
88
+ * @throws 当旧 slot 不存在、新 slot 已存在或写入失败时抛出异常。
89
+ */
90
+ function renameRelaySlot(oldName, newName) {
91
+ const config = (0, config_1.loadConfig)();
92
+ const index = config.relay_slots.findIndex((item) => item.id === oldName);
93
+ if (index < 0) {
94
+ throw new Error((0, text_1.bi)(`未找到中转槽位 ${oldName}`, `Relay slot not found: ${oldName}`));
95
+ }
96
+ if (config.relay_slots.some((item) => item.id === newName)) {
97
+ throw new Error((0, text_1.bi)(`中转槽位 ${newName} 已存在`, `Relay slot already exists: ${newName}`));
98
+ }
99
+ const renamed = {
100
+ ...config.relay_slots[index],
101
+ id: newName,
102
+ name: newName
103
+ };
104
+ config.relay_slots[index] = renamed;
105
+ (0, config_1.saveConfig)(config);
106
+ (0, state_1.updateState)((state) => {
107
+ if (state.selected_model_route?.mode === "relay_slot" &&
108
+ state.selected_model_route.relay_slot_id === oldName) {
109
+ state.selected_model_route = {
110
+ mode: "relay_slot",
111
+ relay_slot_id: newName
112
+ };
113
+ }
114
+ });
115
+ return renamed;
116
+ }
117
+ /**
118
+ * 更新 relay slot 启用状态。
119
+ *
120
+ * @param slotId relay slot 标识。
121
+ * @param enabled 是否启用。
122
+ * @returns 更新后的 relay slot。
123
+ * @throws 当 slot 不存在或配置写入失败时抛出异常。
124
+ */
125
+ function setRelaySlotEnabled(slotId, enabled) {
126
+ const config = (0, config_1.loadConfig)();
127
+ const index = config.relay_slots.findIndex((item) => item.id === slotId);
128
+ if (index < 0) {
129
+ throw new Error((0, text_1.bi)(`未找到中转槽位 ${slotId}`, `Relay slot not found: ${slotId}`));
130
+ }
131
+ const updated = {
132
+ ...config.relay_slots[index],
133
+ enabled
134
+ };
135
+ config.relay_slots[index] = updated;
136
+ (0, config_1.saveConfig)(config);
137
+ return updated;
138
+ }
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ exports.startServer = startServer;
7
7
  const fastify_1 = __importDefault(require("fastify"));
8
8
  const config_1 = require("./config");
9
9
  const backend_proxy_service_1 = require("./backend-proxy-service");
10
- const proxy_retry_service_1 = require("./proxy-retry-service");
10
+ const model_proxy_dispatcher_1 = require("./model-proxy-dispatcher");
11
11
  const status_1 = require("./status");
12
12
  const scheduler_1 = require("./scheduler");
13
13
  const usage_sync_1 = require("./usage-sync");
@@ -91,7 +91,7 @@ async function startServer(port) {
91
91
  });
92
92
  const codexProxyHandler = async (request, reply) => {
93
93
  const requestBody = await readRawRequestBody(request.body);
94
- const result = await (0, proxy_retry_service_1.proxyCodexWithRetry)({
94
+ const result = await (0, model_proxy_dispatcher_1.proxyModelWithRoute)({
95
95
  method: request.method,
96
96
  url: request.url,
97
97
  headers: request.headers,
package/dist/state.js CHANGED
@@ -17,6 +17,8 @@ exports.clearUsageRefreshError = clearUsageRefreshError;
17
17
  exports.getUsageRefreshError = getUsageRefreshError;
18
18
  exports.getSelectedCodexAuthAccountId = getSelectedCodexAuthAccountId;
19
19
  exports.setSelectedCodexAuthAccountId = setSelectedCodexAuthAccountId;
20
+ exports.getSelectedModelRoute = getSelectedModelRoute;
21
+ exports.setSelectedModelRoute = setSelectedModelRoute;
20
22
  exports.getManagedCodexConfigState = getManagedCodexConfigState;
21
23
  exports.getManagedCodexAuthState = getManagedCodexAuthState;
22
24
  exports.setManagedCodexConfigState = setManagedCodexConfigState;
@@ -44,6 +46,9 @@ function createDefaultState() {
44
46
  return {
45
47
  state_version: STATE_SCHEMA_VERSION,
46
48
  selected_codex_auth_account_id: null,
49
+ selected_model_route: {
50
+ mode: "auth_pool"
51
+ },
47
52
  account_blocks: {},
48
53
  usage_cache: {},
49
54
  usage_refresh_errors: {},
@@ -64,6 +69,7 @@ function normalizeState(parsed) {
64
69
  return {
65
70
  state_version: STATE_SCHEMA_VERSION,
66
71
  selected_codex_auth_account_id: parsed?.selected_codex_auth_account_id ?? defaults.selected_codex_auth_account_id,
72
+ selected_model_route: normalizeModelRouteSelection(parsed?.selected_model_route ?? defaults.selected_model_route),
67
73
  account_blocks: parsed?.account_blocks ?? defaults.account_blocks,
68
74
  usage_cache: parsed?.usage_cache ?? defaults.usage_cache,
69
75
  usage_refresh_errors: parsed?.usage_refresh_errors ?? defaults.usage_refresh_errors,
@@ -72,6 +78,24 @@ function normalizeState(parsed) {
72
78
  managed_codex_config: parsed?.managed_codex_config ?? defaults.managed_codex_config
73
79
  };
74
80
  }
81
+ /**
82
+ * 归一化当前模型出口选择,避免历史 state 或手工编辑内容让代理入口进入未知模式。
83
+ *
84
+ * @param value state 文件中记录的模型出口选择。
85
+ * @returns 可安全使用的模型出口选择;非法内容回退到官方账号池。
86
+ * @throws 无显式抛出。
87
+ */
88
+ function normalizeModelRouteSelection(value) {
89
+ if (value?.mode === "relay_slot" && value.relay_slot_id) {
90
+ return {
91
+ mode: "relay_slot",
92
+ relay_slot_id: value.relay_slot_id
93
+ };
94
+ }
95
+ return {
96
+ mode: "auth_pool"
97
+ };
98
+ }
75
99
  /**
76
100
  * 读取 cslot 的本地运行状态;文件不存在时返回默认空状态。
77
101
  *
@@ -261,6 +285,32 @@ function setSelectedCodexAuthAccountId(accountId) {
261
285
  state.selected_codex_auth_account_id = accountId;
262
286
  });
263
287
  }
288
+ /**
289
+ * 读取当前模型请求出口选择。
290
+ *
291
+ * 业务含义:
292
+ * 1. `auth_pool` 表示 `/v1/*` 继续走官方账号自动调度。
293
+ * 2. `relay_slot` 表示 `/v1/*` 固定走指定 OpenAI-compatible 中转槽位。
294
+ * 3. 该选择不影响 Codex App 主登录态与 `/backend-api/*` 插件链路。
295
+ *
296
+ * @returns 当前模型出口选择。
297
+ * @throws 当 state 文件读取或解析失败时透传底层异常。
298
+ */
299
+ function getSelectedModelRoute() {
300
+ return normalizeModelRouteSelection(loadState().selected_model_route);
301
+ }
302
+ /**
303
+ * 保存当前模型请求出口选择。
304
+ *
305
+ * @param selection 模型出口选择;`null` 表示恢复官方账号池。
306
+ * @returns 无返回值。
307
+ * @throws 当 state 文件写入失败时透传底层异常。
308
+ */
309
+ function setSelectedModelRoute(selection) {
310
+ updateState((state) => {
311
+ state.selected_model_route = normalizeModelRouteSelection(selection);
312
+ });
313
+ }
264
314
  /**
265
315
  * 读取当前记录的 Codex `config.toml` 接管快照。
266
316
  *