@lyogvce/dsh-codex-subscription 0.2.5 → 0.2.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.
- package/README.md +4 -3
- package/lib/client.js +80 -7
- package/lib/index.d.ts +15 -3
- package/lib/index.js +105 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
- 每次请求前检查 `access_token` 的 JWT `exp`:距离过期不足 5 分钟时自动通过单飞锁(Single-flight)向 `https://auth.openai.com/oauth/token` 刷新,并通过 `realpathSync` + 临时文件 `rename` 原子写回 `auth.json`,保证与 Codex CLI 永不互踢下线。
|
|
14
14
|
2. **零网络轮询的本地模型目录发现、防降级保护与可观测性日志**
|
|
15
15
|
- 直接读取 `~/.codex/models_cache.json`(每次按文件 `mtime` 与大小做微秒级 `stat` 校验,仅在文件变化时重读解析),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
|
|
16
|
-
- **防旧版客户端降级污染**:自动将高版本 `models_cache.json` 备份至 `~/.codex
|
|
17
|
-
- **可观测性日志**:自动在 `~/.codex/dsh-codex-subscription.log`(带 512KB
|
|
16
|
+
- **防旧版客户端降级污染**:自动将高版本 `models_cache.json` 备份至 `~/.codex/cache/dsh-codex-subscription/models-cache-backup.json`;当外部旧版进程(如 `dsh-subagent-codex` 内置的 `0.153.4`)将 `models_cache.json` 降级覆写为不含新模型的旧缓存时,自动拦截降级并保留高版本模型目录。
|
|
17
|
+
- **可观测性日志**:自动在 `~/.codex/cache/dsh-codex-subscription/plugin.log`(带 512KB 自动滚动截断,忽略纯 `fetched_at` 心跳刷新)记录目录加载(`catalog.load` / `catalog.snapshot`)、降级拦截(`catalog.downgrade_rejected`)、备份恢复(`catalog.backup_restored`)与凭证续期(`auth.refreshed` / `auth.refresh_failed`)事件。
|
|
18
18
|
3. **支持按配置屏蔽模型(`disabledModels`)与自定义上下文窗口(`contextWindows`)**
|
|
19
19
|
- 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
|
|
20
20
|
- 通过 `contextWindows` 按模型 `slug` 自定义向 DSH 会话与自动压缩器暴露的预算窗口(例如设为 `400000`);未配置时默认使用模型物理上限 `max_context_window`(`872000`)。同时底层 `PiCodexModel.contextWindow` 始终保持物理上限 `872000`,彻底防止 `dsh-llm-pi-ai` 在预算窗口与物理上限之间误判 `CONTEXT_WINDOW_EXCEEDED`。
|
|
21
21
|
4. **原生多轮思考链回放与工具调用**
|
|
22
22
|
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化纯 UUID `session-id` / `x-client-request-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
|
|
23
|
-
5. **Codex
|
|
23
|
+
5. **Codex 订阅限额内联展示与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
|
|
24
24
|
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看对应窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
25
|
+
- **⚡ Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度)**:可在限额弹窗底部直接一键开关 Fast 模式(自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`,也可在插件设置页通过 `fastMode` 配置),开启后胶囊上显示闪电标识 `⚡`,并在请求体中自动为支持 Fast 档位的模型注入 `"service_tier": "priority"`。
|
|
25
26
|
|
|
26
27
|
---
|
|
27
28
|
|
package/lib/client.js
CHANGED
|
@@ -139,12 +139,14 @@ window.__ModuleLoader__.load({
|
|
|
139
139
|
const ttlMs = options.ttlMs ?? 6e4;
|
|
140
140
|
let state = {
|
|
141
141
|
quota: null,
|
|
142
|
+
fastMode: false,
|
|
142
143
|
loading: false,
|
|
143
144
|
error: null
|
|
144
145
|
};
|
|
145
146
|
let inflight = null;
|
|
146
147
|
let inflightForce = false;
|
|
147
148
|
let requestSeq = 0;
|
|
149
|
+
let fastModeSeq = 0;
|
|
148
150
|
const listeners = new Set();
|
|
149
151
|
function setState(next) {
|
|
150
152
|
state = next;
|
|
@@ -154,6 +156,7 @@ window.__ModuleLoader__.load({
|
|
|
154
156
|
if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
|
|
155
157
|
if (inflight !== null && (!force || inflightForce)) return inflight;
|
|
156
158
|
const seq = ++requestSeq;
|
|
159
|
+
const fastSeqAtStart = fastModeSeq;
|
|
157
160
|
inflightForce = force;
|
|
158
161
|
setState({
|
|
159
162
|
...state,
|
|
@@ -170,7 +173,7 @@ window.__ModuleLoader__.load({
|
|
|
170
173
|
if (seq !== requestSeq) return;
|
|
171
174
|
if (!response.ok) {
|
|
172
175
|
setState({
|
|
173
|
-
|
|
176
|
+
...state,
|
|
174
177
|
loading: false,
|
|
175
178
|
error: `HTTP ${response.status}`
|
|
176
179
|
});
|
|
@@ -178,8 +181,10 @@ window.__ModuleLoader__.load({
|
|
|
178
181
|
}
|
|
179
182
|
const payload = await response.json();
|
|
180
183
|
if (seq !== requestSeq) return;
|
|
184
|
+
const resolvedFastMode = fastSeqAtStart === fastModeSeq && typeof payload?.fastMode === "boolean" ? payload.fastMode : state.fastMode;
|
|
181
185
|
if (payload && payload.ok === true && payload.quota) setState({
|
|
182
186
|
quota: payload.quota,
|
|
187
|
+
fastMode: resolvedFastMode,
|
|
183
188
|
loading: false,
|
|
184
189
|
error: null
|
|
185
190
|
});
|
|
@@ -188,6 +193,7 @@ window.__ModuleLoader__.load({
|
|
|
188
193
|
const errMsg = payload && !payload.ok && typeof payload.error === "string" ? payload.error : "Invalid response";
|
|
189
194
|
setState({
|
|
190
195
|
quota: fallbackQuota ?? null,
|
|
196
|
+
fastMode: resolvedFastMode,
|
|
191
197
|
loading: false,
|
|
192
198
|
error: errMsg
|
|
193
199
|
});
|
|
@@ -195,7 +201,7 @@ window.__ModuleLoader__.load({
|
|
|
195
201
|
} catch (error) {
|
|
196
202
|
if (seq !== requestSeq) return;
|
|
197
203
|
setState({
|
|
198
|
-
|
|
204
|
+
...state,
|
|
199
205
|
loading: false,
|
|
200
206
|
error: safeErrorMessage(error)
|
|
201
207
|
});
|
|
@@ -209,6 +215,30 @@ window.__ModuleLoader__.load({
|
|
|
209
215
|
inflight = task;
|
|
210
216
|
return task;
|
|
211
217
|
}
|
|
218
|
+
async function setFastMode(enabled) {
|
|
219
|
+
const nextFastMode = Boolean(enabled);
|
|
220
|
+
const seq = ++fastModeSeq;
|
|
221
|
+
setState({
|
|
222
|
+
...state,
|
|
223
|
+
fastMode: nextFastMode
|
|
224
|
+
});
|
|
225
|
+
try {
|
|
226
|
+
const response = await fetchFn(USAGE_ROUTE_PATH, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: {
|
|
229
|
+
"Content-Type": "application/json",
|
|
230
|
+
Accept: "application/json"
|
|
231
|
+
},
|
|
232
|
+
body: JSON.stringify({ fastMode: nextFastMode })
|
|
233
|
+
});
|
|
234
|
+
if (seq !== fastModeSeq || !response.ok) return;
|
|
235
|
+
const payload = await response.json();
|
|
236
|
+
if (seq === fastModeSeq && typeof payload?.fastMode === "boolean") setState({
|
|
237
|
+
...state,
|
|
238
|
+
fastMode: payload.fastMode
|
|
239
|
+
});
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
212
242
|
return {
|
|
213
243
|
getSnapshot: () => state,
|
|
214
244
|
subscribe(listener) {
|
|
@@ -217,7 +247,8 @@ window.__ModuleLoader__.load({
|
|
|
217
247
|
listeners.delete(listener);
|
|
218
248
|
};
|
|
219
249
|
},
|
|
220
|
-
refresh
|
|
250
|
+
refresh,
|
|
251
|
+
setFastMode
|
|
221
252
|
};
|
|
222
253
|
}
|
|
223
254
|
//#endregion
|
|
@@ -250,6 +281,15 @@ window.__ModuleLoader__.load({
|
|
|
250
281
|
".dsh-codex-quota-bar-fill[data-level=\"warn\"]{background:var(--dsw-alias-state-warn-primary)}",
|
|
251
282
|
".dsh-codex-quota-bar-fill[data-level=\"error\"]{background:var(--dsw-alias-state-error-primary)}",
|
|
252
283
|
".dsh-codex-quota-meta{display:flex;justify-content:space-between;gap:8px;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
284
|
+
".dsh-codex-quota-fast-bolt{color:var(--dsw-alias-state-warn-primary,#f59e0b);flex:none}",
|
|
285
|
+
".dsh-codex-fast-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
286
|
+
".dsh-codex-fast-info{display:flex;flex-direction:column;gap:1px;min-width:0}",
|
|
287
|
+
".dsh-codex-fast-title{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}",
|
|
288
|
+
".dsh-codex-fast-desc{font-size:11px;line-height:15px;color:var(--dsw-alias-label-tertiary)}",
|
|
289
|
+
".dsh-codex-fast-switch{position:relative;width:32px;height:18px;border-radius:999px;border:none;padding:0;background:var(--dsw-alias-border-l2,rgba(128,128,128,0.35));cursor:pointer;flex:none;transition:background-color .15s ease}",
|
|
290
|
+
".dsh-codex-fast-switch[data-checked=\"true\"]{background:var(--dsw-alias-brand-primary,#3b82f6)}",
|
|
291
|
+
".dsh-codex-fast-switch-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.2);transition:transform .15s ease}",
|
|
292
|
+
".dsh-codex-fast-switch[data-checked=\"true\"] .dsh-codex-fast-switch-thumb{transform:translateX(14px)}",
|
|
253
293
|
".dsh-codex-quota-error{margin-top:6px;padding:4px 6px;border-radius:4px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-error-primary);font-size:11px;word-break:break-word}"
|
|
254
294
|
].join("");
|
|
255
295
|
//#endregion
|
|
@@ -265,7 +305,9 @@ window.__ModuleLoader__.load({
|
|
|
265
305
|
"quota.refresh": "↻ 刷新",
|
|
266
306
|
"quota.refreshing": "↻ 刷新中…",
|
|
267
307
|
"quota.usedPrefix": "已用 ",
|
|
268
|
-
"quota.remainPrefix": "剩余 "
|
|
308
|
+
"quota.remainPrefix": "剩余 ",
|
|
309
|
+
"quota.fastTitle": "Fast 极速模式",
|
|
310
|
+
"quota.fastDesc": "2x 推理速度,消耗更多限额"
|
|
269
311
|
};
|
|
270
312
|
const en = {
|
|
271
313
|
"quota.title": "Codex Subscription Quota",
|
|
@@ -273,7 +315,9 @@ window.__ModuleLoader__.load({
|
|
|
273
315
|
"quota.refresh": "↻ Refresh",
|
|
274
316
|
"quota.refreshing": "↻ Refreshing…",
|
|
275
317
|
"quota.usedPrefix": "Used ",
|
|
276
|
-
"quota.remainPrefix": "Remaining "
|
|
318
|
+
"quota.remainPrefix": "Remaining ",
|
|
319
|
+
"quota.fastTitle": "Fast Mode",
|
|
320
|
+
"quota.fastDesc": "2x speed, increased usage"
|
|
277
321
|
};
|
|
278
322
|
const EMPTY_MODEL_STORE = {
|
|
279
323
|
getSnapshot: () => null,
|
|
@@ -354,6 +398,7 @@ window.__ModuleLoader__.load({
|
|
|
354
398
|
type: "button",
|
|
355
399
|
className: "dsh-codex-quota-trigger",
|
|
356
400
|
"data-level": level,
|
|
401
|
+
"data-fast": quotaSnap.fastMode ? "true" : void 0,
|
|
357
402
|
"aria-label": `${t("quota.title")}: ${pillText}`,
|
|
358
403
|
"aria-haspopup": "dialog",
|
|
359
404
|
"aria-expanded": open,
|
|
@@ -380,7 +425,16 @@ window.__ModuleLoader__.load({
|
|
|
380
425
|
r: 5.5,
|
|
381
426
|
strokeDasharray: ringDash,
|
|
382
427
|
transform: "rotate(-90 7 7)"
|
|
383
|
-
})),
|
|
428
|
+
})), quotaSnap.fastMode ? h("svg", {
|
|
429
|
+
className: "dsh-codex-quota-fast-bolt",
|
|
430
|
+
viewBox: "0 0 12 12",
|
|
431
|
+
width: 11,
|
|
432
|
+
height: 11,
|
|
433
|
+
"aria-hidden": true
|
|
434
|
+
}, h("path", {
|
|
435
|
+
d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
|
|
436
|
+
fill: "currentColor"
|
|
437
|
+
})) : null, h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
|
|
384
438
|
className: "dsh-codex-quota-popover",
|
|
385
439
|
role: "dialog",
|
|
386
440
|
"aria-label": t("quota.title"),
|
|
@@ -394,7 +448,26 @@ window.__ModuleLoader__.load({
|
|
|
394
448
|
onClick: () => {
|
|
395
449
|
quotaStore.refresh(true);
|
|
396
450
|
}
|
|
397
|
-
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null,
|
|
451
|
+
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null, h("div", { className: "dsh-codex-fast-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, h("svg", {
|
|
452
|
+
className: "dsh-codex-quota-fast-bolt",
|
|
453
|
+
viewBox: "0 0 12 12",
|
|
454
|
+
width: 12,
|
|
455
|
+
height: 12,
|
|
456
|
+
"aria-hidden": true
|
|
457
|
+
}, h("path", {
|
|
458
|
+
d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
|
|
459
|
+
fill: "currentColor"
|
|
460
|
+
})), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
|
|
461
|
+
type: "button",
|
|
462
|
+
role: "switch",
|
|
463
|
+
className: "dsh-codex-fast-switch",
|
|
464
|
+
"aria-checked": quotaSnap.fastMode,
|
|
465
|
+
"aria-label": t("quota.fastTitle"),
|
|
466
|
+
"data-checked": quotaSnap.fastMode ? "true" : "false",
|
|
467
|
+
onClick: () => {
|
|
468
|
+
quotaStore.setFastMode(!quotaSnap.fastMode);
|
|
469
|
+
}
|
|
470
|
+
}, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
|
|
398
471
|
}
|
|
399
472
|
const inject = ["slots", "locale"];
|
|
400
473
|
function apply(ctx) {
|
package/lib/index.d.ts
CHANGED
|
@@ -59,11 +59,18 @@ declare function createAuthManager(options: AuthManagerOptions): {
|
|
|
59
59
|
};
|
|
60
60
|
//#endregion
|
|
61
61
|
//#region src/catalog.d.ts
|
|
62
|
-
export declare const
|
|
62
|
+
export declare const PLUGIN_CACHE_DIR_NAME = "cache/dsh-codex-subscription";
|
|
63
|
+
export declare const BACKUP_CACHE_FILE_NAME = "models-cache-backup.json";
|
|
64
|
+
export declare const RUNTIME_STATE_FILE_NAME = "runtime-state.json";
|
|
63
65
|
interface RawReasoningLevel {
|
|
64
66
|
effort?: string;
|
|
65
67
|
description?: string;
|
|
66
68
|
}
|
|
69
|
+
interface RawServiceTier {
|
|
70
|
+
id?: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
}
|
|
67
74
|
interface RawCodexModelEntry {
|
|
68
75
|
slug?: string;
|
|
69
76
|
display_name?: string;
|
|
@@ -77,6 +84,8 @@ interface RawCodexModelEntry {
|
|
|
77
84
|
default_reasoning_level?: string;
|
|
78
85
|
support_verbosity?: boolean;
|
|
79
86
|
default_verbosity?: string;
|
|
87
|
+
additional_speed_tiers?: unknown[];
|
|
88
|
+
service_tiers?: RawServiceTier[];
|
|
80
89
|
}
|
|
81
90
|
interface RawModelsCache {
|
|
82
91
|
fetched_at?: string;
|
|
@@ -90,6 +99,7 @@ interface CodexModelMetadata {
|
|
|
90
99
|
readonly supportVerbosity: boolean;
|
|
91
100
|
readonly defaultVerbosity: 'low' | 'medium' | 'high';
|
|
92
101
|
readonly defaultReasoningLevel?: string | undefined;
|
|
102
|
+
readonly fastServiceTier?: string | undefined;
|
|
93
103
|
}
|
|
94
104
|
interface ParsedModelsCatalog {
|
|
95
105
|
readonly clientVersion: string;
|
|
@@ -103,7 +113,7 @@ declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, b
|
|
|
103
113
|
declare function buildCodexUserAgent(version?: string): string;
|
|
104
114
|
declare function normalizeCodexSessionId(sessionId: string | undefined): string | undefined;
|
|
105
115
|
declare function buildCodexHeaders(existingHeaders?: Record<string, string> | undefined, clientVersion?: string): Record<string, string>;
|
|
106
|
-
declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined): Record<string, unknown> | null | undefined;
|
|
116
|
+
declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined, fastMode?: boolean): Record<string, unknown> | null | undefined;
|
|
107
117
|
//#endregion
|
|
108
118
|
//#region src/usage.d.ts
|
|
109
119
|
declare function safeErrorMessage(error: unknown): string;
|
|
@@ -122,10 +132,12 @@ interface CodexQuotaSnapshot {
|
|
|
122
132
|
type CodexQuotaResponse = {
|
|
123
133
|
readonly ok: true;
|
|
124
134
|
readonly quota: CodexQuotaSnapshot;
|
|
135
|
+
readonly fastMode?: boolean | undefined;
|
|
125
136
|
} | {
|
|
126
137
|
readonly ok: false;
|
|
127
138
|
readonly error: string;
|
|
128
139
|
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
140
|
+
readonly fastMode?: boolean | undefined;
|
|
129
141
|
};
|
|
130
142
|
interface UsageFetcherOptions {
|
|
131
143
|
readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
|
|
@@ -143,7 +155,7 @@ declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
|
143
155
|
//#region src/index.d.ts
|
|
144
156
|
export declare const name = "codex-subscription";
|
|
145
157
|
export declare const inject: string[];
|
|
146
|
-
export declare const LOG_FILE_NAME = "
|
|
158
|
+
export declare const LOG_FILE_NAME = "plugin.log";
|
|
147
159
|
export declare const Config: any;
|
|
148
160
|
export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
|
|
149
161
|
export declare const __test: Readonly<{
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, chmodSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { LlmError, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
@@ -13,7 +13,9 @@ const PROVIDER = "openai-codex";
|
|
|
13
13
|
const DISPLAY_NAME = "Codex";
|
|
14
14
|
const DEFAULT_CLIENT_VERSION = "0.163.0";
|
|
15
15
|
const DEFAULT_CONTEXT_WINDOW = 872e3;
|
|
16
|
-
const
|
|
16
|
+
const PLUGIN_CACHE_DIR_NAME = "cache/dsh-codex-subscription";
|
|
17
|
+
const BACKUP_CACHE_FILE_NAME = "models-cache-backup.json";
|
|
18
|
+
const RUNTIME_STATE_FILE_NAME = "runtime-state.json";
|
|
17
19
|
const THINKING_LEVELS = [
|
|
18
20
|
"off",
|
|
19
21
|
"minimal",
|
|
@@ -35,6 +37,12 @@ function parseRawModelsCache(rawText) {
|
|
|
35
37
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new SyntaxError("models_cache.json root must be a JSON object");
|
|
36
38
|
return parsed;
|
|
37
39
|
}
|
|
40
|
+
function isSameModelsCatalogPayload(a, b) {
|
|
41
|
+
if (!a || !b) return false;
|
|
42
|
+
const { fetched_at: _fa, ...restA } = a;
|
|
43
|
+
const { fetched_at: _fb, ...restB } = b;
|
|
44
|
+
return JSON.stringify(restA) === JSON.stringify(restB);
|
|
45
|
+
}
|
|
38
46
|
function compareClientVersions(a, b) {
|
|
39
47
|
const parseParts = (ver) => typeof ver === "string" && ver.trim() !== "" ? ver.trim().split(/[^0-9]+/u).filter(Boolean).map((n) => Number.parseInt(n, 10)) : [];
|
|
40
48
|
const pa = parseParts(a);
|
|
@@ -74,6 +82,16 @@ function isVerbosity(value) {
|
|
|
74
82
|
function isPositiveSafeInt(value) {
|
|
75
83
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
76
84
|
}
|
|
85
|
+
function resolveFastServiceTier(entry) {
|
|
86
|
+
if (Array.isArray(entry.service_tiers)) {
|
|
87
|
+
const matched = entry.service_tiers.find((tier) => tier !== null && typeof tier === "object" && typeof tier.id === "string" && tier.id.trim() !== "" && (tier.id.trim() === "priority" || typeof tier.name === "string" && tier.name.trim().toLowerCase() === "fast"));
|
|
88
|
+
if (matched && typeof matched.id === "string") return matched.id.trim();
|
|
89
|
+
if (entry.service_tiers.length === 0 && Array.isArray(entry.additional_speed_tiers)) return entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast") ? "priority" : void 0;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(entry.additional_speed_tiers) && entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast")) return "priority";
|
|
93
|
+
return "priority";
|
|
94
|
+
}
|
|
77
95
|
function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
|
|
78
96
|
const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
|
|
79
97
|
const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
|
|
@@ -101,11 +119,13 @@ function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWind
|
|
|
101
119
|
const supportVerbosity = entry.support_verbosity !== false;
|
|
102
120
|
const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
|
|
103
121
|
const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
|
|
122
|
+
const fastServiceTier = resolveFastServiceTier(entry);
|
|
104
123
|
metadataById.set(id, {
|
|
105
124
|
contextWindow: targetContextWindow,
|
|
106
125
|
supportVerbosity,
|
|
107
126
|
defaultVerbosity,
|
|
108
|
-
defaultReasoningLevel
|
|
127
|
+
defaultReasoningLevel,
|
|
128
|
+
fastServiceTier
|
|
109
129
|
});
|
|
110
130
|
models.push({
|
|
111
131
|
...base ?? {},
|
|
@@ -190,11 +210,15 @@ function buildCodexHeaders(existingHeaders, clientVersion = DEFAULT_CLIENT_VERSI
|
|
|
190
210
|
headers["User-Agent"] = buildCodexUserAgent(clientVersion);
|
|
191
211
|
return headers;
|
|
192
212
|
}
|
|
193
|
-
function sanitizeCodexPayload(payload, modelMeta) {
|
|
213
|
+
function sanitizeCodexPayload(payload, modelMeta, fastMode = false) {
|
|
194
214
|
if (!payload || typeof payload !== "object") return payload;
|
|
195
215
|
const cleaned = { ...payload };
|
|
196
216
|
for (const field of FORBIDDEN_PAYLOAD_FIELDS) delete cleaned[field];
|
|
197
|
-
if (
|
|
217
|
+
if (fastMode) {
|
|
218
|
+
const tier = modelMeta === void 0 ? "priority" : modelMeta.fastServiceTier;
|
|
219
|
+
if (typeof tier === "string" && tier !== "") cleaned.service_tier = tier;
|
|
220
|
+
else delete cleaned.service_tier;
|
|
221
|
+
} else if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
|
|
198
222
|
const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
|
|
199
223
|
if (modelMeta?.supportVerbosity === false) {
|
|
200
224
|
if (currentText !== void 0) {
|
|
@@ -380,6 +404,10 @@ function resolveRealTargetPath(filePath) {
|
|
|
380
404
|
}
|
|
381
405
|
}
|
|
382
406
|
function writeFileAtomicSync(filePath, content) {
|
|
407
|
+
mkdirSync(dirname(filePath), {
|
|
408
|
+
recursive: true,
|
|
409
|
+
mode: 448
|
|
410
|
+
});
|
|
383
411
|
const targetPath = resolveRealTargetPath(filePath);
|
|
384
412
|
const tempPath = join(dirname(targetPath), `.${basename(targetPath)}.tmp-${process.pid}-${randomUUID()}`);
|
|
385
413
|
try {
|
|
@@ -552,12 +580,15 @@ function createAuthManager(options) {
|
|
|
552
580
|
//#region src/index.ts
|
|
553
581
|
const name = "codex-subscription";
|
|
554
582
|
const inject = ["llm"];
|
|
555
|
-
const LOG_FILE_NAME = "
|
|
583
|
+
const LOG_FILE_NAME = "plugin.log";
|
|
556
584
|
const STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
557
585
|
const MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
558
586
|
const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
559
587
|
const REQUEST_IMAGE_MAX_BYTES = 1048576;
|
|
560
588
|
const MAX_LOG_FILE_BYTES = 524288;
|
|
589
|
+
function resolvePluginCacheDir(codexHome) {
|
|
590
|
+
return join(codexHome, PLUGIN_CACHE_DIR_NAME);
|
|
591
|
+
}
|
|
561
592
|
function appendPluginLog(codexHome, ctx, level, event, detail) {
|
|
562
593
|
const line = `[${new Date().toISOString()}] [${level}] ${event} ${JSON.stringify(detail)}\n`;
|
|
563
594
|
if (level === "ERROR") {
|
|
@@ -567,7 +598,12 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
|
|
|
567
598
|
if (typeof ctx.logger?.warn === "function") ctx.logger.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
|
|
568
599
|
}
|
|
569
600
|
try {
|
|
570
|
-
const
|
|
601
|
+
const cacheDir = resolvePluginCacheDir(codexHome);
|
|
602
|
+
mkdirSync(cacheDir, {
|
|
603
|
+
recursive: true,
|
|
604
|
+
mode: 448
|
|
605
|
+
});
|
|
606
|
+
const logPath = join(cacheDir, LOG_FILE_NAME);
|
|
571
607
|
try {
|
|
572
608
|
if (statSync(logPath).size > MAX_LOG_FILE_BYTES) {
|
|
573
609
|
const buf = readFileSync(logPath);
|
|
@@ -584,17 +620,31 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
|
|
|
584
620
|
}
|
|
585
621
|
function readBackupCache(codexHome) {
|
|
586
622
|
try {
|
|
587
|
-
return parseRawModelsCache(readFileSync(join(codexHome, BACKUP_CACHE_FILE_NAME), "utf8"));
|
|
623
|
+
return parseRawModelsCache(readFileSync(join(resolvePluginCacheDir(codexHome), BACKUP_CACHE_FILE_NAME), "utf8"));
|
|
588
624
|
} catch {
|
|
589
625
|
return;
|
|
590
626
|
}
|
|
591
627
|
}
|
|
592
628
|
function writeBackupCache(codexHome, raw) {
|
|
593
629
|
try {
|
|
594
|
-
writeFileAtomicSync(join(codexHome, BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
|
|
630
|
+
writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
|
|
631
|
+
} catch {}
|
|
632
|
+
}
|
|
633
|
+
function readPersistedFastMode(codexHome) {
|
|
634
|
+
try {
|
|
635
|
+
const raw = JSON.parse(readFileSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), "utf8"));
|
|
636
|
+
return typeof raw?.fastMode === "boolean" ? raw.fastMode : void 0;
|
|
637
|
+
} catch {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function writePersistedFastMode(codexHome, fastMode) {
|
|
642
|
+
try {
|
|
643
|
+
writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ fastMode }, null, 2)}\n`);
|
|
595
644
|
} catch {}
|
|
596
645
|
}
|
|
597
646
|
const Config = z.object({
|
|
647
|
+
fastMode: z.boolean().default(false).description("开启 Fast 极速模式(注入 service_tier: priority,最高 2x 推理速度,消耗更多订阅限额)").volatile(),
|
|
598
648
|
disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
|
|
599
649
|
contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
|
|
600
650
|
});
|
|
@@ -632,8 +682,21 @@ function apply(ctx, config = {}) {
|
|
|
632
682
|
const baseModels = baseProvider.getModels();
|
|
633
683
|
const retryPolicy = resolveRetryPolicy(void 0, "codex-subscription retryPolicy");
|
|
634
684
|
const getCodexHome = () => resolveCodexHome();
|
|
685
|
+
const getConfigFastMode = () => Boolean(readVolatile(config.fastMode, false));
|
|
635
686
|
const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
|
|
636
687
|
const getContextWindows = () => readVolatile(config.contextWindows, {});
|
|
688
|
+
let lastConfigFastMode = getConfigFastMode();
|
|
689
|
+
let fastModeOverride = readPersistedFastMode(getCodexHome());
|
|
690
|
+
const getEffectiveFastMode = () => fastModeOverride !== void 0 ? fastModeOverride : getConfigFastMode();
|
|
691
|
+
const setFastModeOverride = (nextFastMode, source) => {
|
|
692
|
+
const codexHome = getCodexHome();
|
|
693
|
+
fastModeOverride = nextFastMode;
|
|
694
|
+
writePersistedFastMode(codexHome, nextFastMode);
|
|
695
|
+
appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
|
|
696
|
+
fastMode: nextFastMode,
|
|
697
|
+
source
|
|
698
|
+
});
|
|
699
|
+
};
|
|
637
700
|
const authManager = createAuthManager({
|
|
638
701
|
getCodexHome,
|
|
639
702
|
getClientVersion: () => ensureSnapshot().catalog.clientVersion,
|
|
@@ -671,7 +734,7 @@ function apply(ctx, config = {}) {
|
|
|
671
734
|
transport: "sse",
|
|
672
735
|
headers: buildCodexHeaders(rest.headers, catalog.clientVersion),
|
|
673
736
|
async onPayload(payload, requestModel) {
|
|
674
|
-
return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta);
|
|
737
|
+
return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode());
|
|
675
738
|
}
|
|
676
739
|
};
|
|
677
740
|
}
|
|
@@ -749,14 +812,16 @@ function apply(ctx, config = {}) {
|
|
|
749
812
|
incomingFetchedAt: incomingRaw.fetched_at ?? null
|
|
750
813
|
});
|
|
751
814
|
else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
815
|
+
if (!isSameModelsCatalogPayload(cachedRaw, incomingRaw)) {
|
|
816
|
+
setActiveRaw(incomingRaw);
|
|
817
|
+
writeBackupCache(codexHome, incomingRaw);
|
|
818
|
+
appendPluginLog(codexHome, ctx, "INFO", "catalog.load", {
|
|
819
|
+
cachePath,
|
|
820
|
+
clientVersion: incomingRaw.client_version ?? "0.163.0",
|
|
821
|
+
fetchedAt: incomingRaw.fetched_at ?? null,
|
|
822
|
+
rawModelCount: Array.isArray(incomingRaw.models) ? incomingRaw.models.length : 0
|
|
823
|
+
});
|
|
824
|
+
}
|
|
760
825
|
}
|
|
761
826
|
} catch (error) {
|
|
762
827
|
handleReadError(error);
|
|
@@ -833,12 +898,26 @@ function apply(ctx, config = {}) {
|
|
|
833
898
|
if (typeof registerFetch !== "function") return;
|
|
834
899
|
const registerRoute = () => registerFetch.call(connection?.fetch, {
|
|
835
900
|
path: USAGE_ROUTE_PATH,
|
|
836
|
-
methods: ["GET"],
|
|
901
|
+
methods: ["GET", "POST"],
|
|
837
902
|
requestBody: "buffered",
|
|
838
903
|
fetch: async (request) => {
|
|
904
|
+
if (request.method === "POST") {
|
|
905
|
+
const body = await request.json().catch(() => null);
|
|
906
|
+
if (typeof body?.fastMode === "boolean") setFastModeOverride(body.fastMode, "ui");
|
|
907
|
+
return new Response(JSON.stringify({
|
|
908
|
+
ok: true,
|
|
909
|
+
fastMode: getEffectiveFastMode()
|
|
910
|
+
}), {
|
|
911
|
+
status: 200,
|
|
912
|
+
headers: { "content-type": "application/json" }
|
|
913
|
+
});
|
|
914
|
+
}
|
|
839
915
|
const force = new URL(request.url, "http://localhost").searchParams.get("force") === "1";
|
|
840
916
|
const result = await usageFetcher.getUsage(force);
|
|
841
|
-
return new Response(JSON.stringify(
|
|
917
|
+
return new Response(JSON.stringify({
|
|
918
|
+
...result,
|
|
919
|
+
fastMode: getEffectiveFastMode()
|
|
920
|
+
}), {
|
|
842
921
|
status: 200,
|
|
843
922
|
headers: { "content-type": "application/json" }
|
|
844
923
|
});
|
|
@@ -850,6 +929,11 @@ function apply(ctx, config = {}) {
|
|
|
850
929
|
if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
|
|
851
930
|
else mountUsageRoute(ctx);
|
|
852
931
|
ctx.on?.("loader/volatile-update", () => {
|
|
932
|
+
const currentConfigFastMode = getConfigFastMode();
|
|
933
|
+
if (currentConfigFastMode !== lastConfigFastMode) {
|
|
934
|
+
lastConfigFastMode = currentConfigFastMode;
|
|
935
|
+
setFastModeOverride(currentConfigFastMode, "config");
|
|
936
|
+
}
|
|
853
937
|
ensureSnapshot();
|
|
854
938
|
ctx.llm.emitAdaptersUpdated?.();
|
|
855
939
|
});
|
|
@@ -879,4 +963,4 @@ const __test = Object.freeze({
|
|
|
879
963
|
createUsageFetcher
|
|
880
964
|
});
|
|
881
965
|
//#endregion
|
|
882
|
-
export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, __test, apply, inject, name };
|
|
966
|
+
export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, PLUGIN_CACHE_DIR_NAME, RUNTIME_STATE_FILE_NAME, __test, apply, inject, name };
|