@lyogvce/dsh-codex-subscription 0.2.6 → 0.2.8

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 CHANGED
@@ -20,8 +20,9 @@
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 订阅限额内联展示(`conversation.input.right`)**
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 模式(互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后当前会话胶囊显示闪电标识 `⚡`,并在该会话请求体中自动注入 `"service_tier": "priority"`。
25
26
 
26
27
  ---
27
28
 
package/lib/client.js CHANGED
@@ -13,6 +13,11 @@ window.__ModuleLoader__.load({
13
13
  "gpt-5.6-terra",
14
14
  "gpt-5.6-luna"
15
15
  ]);
16
+ function normalizeCodexSessionId(sessionId) {
17
+ if (typeof sessionId !== "string") return void 0;
18
+ const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
19
+ return stripped.length > 0 ? stripped : void 0;
20
+ }
16
21
  //#endregion
17
22
  //#region src/usage.ts
18
23
  function safeErrorMessage(error) {
@@ -25,8 +30,16 @@ window.__ModuleLoader__.load({
25
30
  //#endregion
26
31
  //#region src/client/quota.ts
27
32
  const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
33
+ const DEFAULT_SESSION_KEY = "__default__";
28
34
  const DEFAULT_PRIMARY_WINDOW_SECONDS = 18e3;
29
35
  const DEFAULT_SECONDARY_WINDOW_SECONDS = 604800;
36
+ function resolveSessionFastMode(snapshot, sessionId) {
37
+ if (!snapshot) return false;
38
+ const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
39
+ const sessionVal = snapshot.sessionFastModes?.[key];
40
+ if (typeof sessionVal === "boolean") return sessionVal;
41
+ return snapshot.defaultFastMode === true;
42
+ }
30
43
  function resolveActiveProvider(directorySnapshot, projectedSelection) {
31
44
  const dir = directorySnapshot && typeof directorySnapshot === "object" ? directorySnapshot : void 0;
32
45
  const proj = projectedSelection && typeof projectedSelection === "object" ? projectedSelection : void 0;
@@ -139,21 +152,40 @@ window.__ModuleLoader__.load({
139
152
  const ttlMs = options.ttlMs ?? 6e4;
140
153
  let state = {
141
154
  quota: null,
155
+ defaultFastMode: false,
156
+ sessionFastModes: {},
142
157
  loading: false,
143
158
  error: null
144
159
  };
145
160
  let inflight = null;
146
161
  let inflightForce = false;
147
162
  let requestSeq = 0;
163
+ let fastModeSeq = 0;
148
164
  const listeners = new Set();
149
165
  function setState(next) {
150
166
  state = next;
151
167
  for (const listener of listeners) listener();
152
168
  }
169
+ function mergeFastStateFromPayload(payload, allowSessionReplace) {
170
+ const defaultFastMode = typeof payload?.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
171
+ if (allowSessionReplace && payload?.sessionFastModes && typeof payload.sessionFastModes === "object" && !Array.isArray(payload.sessionFastModes)) {
172
+ const nextMap = {};
173
+ for (const [k, v] of Object.entries(payload.sessionFastModes)) if (typeof k === "string" && k !== "" && typeof v === "boolean") nextMap[k] = v;
174
+ return {
175
+ defaultFastMode,
176
+ sessionFastModes: nextMap
177
+ };
178
+ }
179
+ return {
180
+ defaultFastMode,
181
+ sessionFastModes: state.sessionFastModes
182
+ };
183
+ }
153
184
  async function refresh(force = false) {
154
185
  if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
155
186
  if (inflight !== null && (!force || inflightForce)) return inflight;
156
187
  const seq = ++requestSeq;
188
+ const fastSeqAtStart = fastModeSeq;
157
189
  inflightForce = force;
158
190
  setState({
159
191
  ...state,
@@ -170,7 +202,7 @@ window.__ModuleLoader__.load({
170
202
  if (seq !== requestSeq) return;
171
203
  if (!response.ok) {
172
204
  setState({
173
- quota: state.quota,
205
+ ...state,
174
206
  loading: false,
175
207
  error: `HTTP ${response.status}`
176
208
  });
@@ -178,8 +210,10 @@ window.__ModuleLoader__.load({
178
210
  }
179
211
  const payload = await response.json();
180
212
  if (seq !== requestSeq) return;
213
+ const fastState = mergeFastStateFromPayload(payload, fastSeqAtStart === fastModeSeq);
181
214
  if (payload && payload.ok === true && payload.quota) setState({
182
215
  quota: payload.quota,
216
+ ...fastState,
183
217
  loading: false,
184
218
  error: null
185
219
  });
@@ -188,6 +222,7 @@ window.__ModuleLoader__.load({
188
222
  const errMsg = payload && !payload.ok && typeof payload.error === "string" ? payload.error : "Invalid response";
189
223
  setState({
190
224
  quota: fallbackQuota ?? null,
225
+ ...fastState,
191
226
  loading: false,
192
227
  error: errMsg
193
228
  });
@@ -195,7 +230,7 @@ window.__ModuleLoader__.load({
195
230
  } catch (error) {
196
231
  if (seq !== requestSeq) return;
197
232
  setState({
198
- quota: state.quota,
233
+ ...state,
199
234
  loading: false,
200
235
  error: safeErrorMessage(error)
201
236
  });
@@ -209,6 +244,37 @@ window.__ModuleLoader__.load({
209
244
  inflight = task;
210
245
  return task;
211
246
  }
247
+ async function setFastMode(sessionId, enabled) {
248
+ const nextFastMode = Boolean(enabled);
249
+ const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
250
+ const seq = ++fastModeSeq;
251
+ setState({
252
+ ...state,
253
+ sessionFastModes: {
254
+ ...state.sessionFastModes,
255
+ [key]: nextFastMode
256
+ }
257
+ });
258
+ try {
259
+ const response = await fetchFn(USAGE_ROUTE_PATH, {
260
+ method: "POST",
261
+ headers: {
262
+ "Content-Type": "application/json",
263
+ Accept: "application/json"
264
+ },
265
+ body: JSON.stringify({
266
+ sessionId: key,
267
+ fastMode: nextFastMode
268
+ })
269
+ });
270
+ if (seq !== fastModeSeq || !response.ok) return;
271
+ const payload = await response.json();
272
+ if (seq === fastModeSeq) setState({
273
+ ...state,
274
+ ...mergeFastStateFromPayload(payload, true)
275
+ });
276
+ } catch {}
277
+ }
212
278
  return {
213
279
  getSnapshot: () => state,
214
280
  subscribe(listener) {
@@ -217,7 +283,8 @@ window.__ModuleLoader__.load({
217
283
  listeners.delete(listener);
218
284
  };
219
285
  },
220
- refresh
286
+ refresh,
287
+ setFastMode
221
288
  };
222
289
  }
223
290
  //#endregion
@@ -250,6 +317,15 @@ window.__ModuleLoader__.load({
250
317
  ".dsh-codex-quota-bar-fill[data-level=\"warn\"]{background:var(--dsw-alias-state-warn-primary)}",
251
318
  ".dsh-codex-quota-bar-fill[data-level=\"error\"]{background:var(--dsw-alias-state-error-primary)}",
252
319
  ".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}",
320
+ ".dsh-codex-quota-fast-bolt{color:var(--dsw-alias-state-warn-primary,#f59e0b);flex:none}",
321
+ ".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)}",
322
+ ".dsh-codex-fast-info{display:flex;flex-direction:column;gap:1px;min-width:0}",
323
+ ".dsh-codex-fast-title{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}",
324
+ ".dsh-codex-fast-desc{font-size:11px;line-height:15px;color:var(--dsw-alias-label-tertiary)}",
325
+ ".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}",
326
+ ".dsh-codex-fast-switch[data-checked=\"true\"]{background:var(--dsw-alias-brand-primary,#3b82f6)}",
327
+ ".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}",
328
+ ".dsh-codex-fast-switch[data-checked=\"true\"] .dsh-codex-fast-switch-thumb{transform:translateX(14px)}",
253
329
  ".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
330
  ].join("");
255
331
  //#endregion
@@ -265,7 +341,9 @@ window.__ModuleLoader__.load({
265
341
  "quota.refresh": "↻ 刷新",
266
342
  "quota.refreshing": "↻ 刷新中…",
267
343
  "quota.usedPrefix": "已用 ",
268
- "quota.remainPrefix": "剩余 "
344
+ "quota.remainPrefix": "剩余 ",
345
+ "quota.fastTitle": "Fast 极速模式",
346
+ "quota.fastDesc": "2x 推理速度,消耗更多限额"
269
347
  };
270
348
  const en = {
271
349
  "quota.title": "Codex Subscription Quota",
@@ -273,7 +351,9 @@ window.__ModuleLoader__.load({
273
351
  "quota.refresh": "↻ Refresh",
274
352
  "quota.refreshing": "↻ Refreshing…",
275
353
  "quota.usedPrefix": "Used ",
276
- "quota.remainPrefix": "Remaining "
354
+ "quota.remainPrefix": "Remaining ",
355
+ "quota.fastTitle": "Fast Mode",
356
+ "quota.fastDesc": "2x speed, increased usage"
277
357
  };
278
358
  const EMPTY_MODEL_STORE = {
279
359
  getSnapshot: () => null,
@@ -344,6 +424,7 @@ window.__ModuleLoader__.load({
344
424
  })), h("div", { className: "dsh-codex-quota-meta" }, h("span", null, `${t("quota.remainPrefix")}${remainPercent}%`), countdown !== "" ? h("span", null, countdown) : null));
345
425
  };
346
426
  const quota = quotaSnap.quota;
427
+ const isFastMode = resolveSessionFastMode(quotaSnap, sessionId);
347
428
  const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
348
429
  const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
349
430
  return h("span", {
@@ -354,6 +435,7 @@ window.__ModuleLoader__.load({
354
435
  type: "button",
355
436
  className: "dsh-codex-quota-trigger",
356
437
  "data-level": level,
438
+ "data-fast": isFastMode ? "true" : void 0,
357
439
  "aria-label": `${t("quota.title")}: ${pillText}`,
358
440
  "aria-haspopup": "dialog",
359
441
  "aria-expanded": open,
@@ -380,7 +462,16 @@ window.__ModuleLoader__.load({
380
462
  r: 5.5,
381
463
  strokeDasharray: ringDash,
382
464
  transform: "rotate(-90 7 7)"
383
- })), h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
465
+ })), isFastMode ? h("svg", {
466
+ className: "dsh-codex-quota-fast-bolt",
467
+ viewBox: "0 0 12 12",
468
+ width: 11,
469
+ height: 11,
470
+ "aria-hidden": true
471
+ }, h("path", {
472
+ d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
473
+ fill: "currentColor"
474
+ })) : null, h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
384
475
  className: "dsh-codex-quota-popover",
385
476
  role: "dialog",
386
477
  "aria-label": t("quota.title"),
@@ -394,7 +485,26 @@ window.__ModuleLoader__.load({
394
485
  onClick: () => {
395
486
  quotaStore.refresh(true);
396
487
  }
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, quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
488
+ }, 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", {
489
+ className: "dsh-codex-quota-fast-bolt",
490
+ viewBox: "0 0 12 12",
491
+ width: 12,
492
+ height: 12,
493
+ "aria-hidden": true
494
+ }, h("path", {
495
+ d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
496
+ fill: "currentColor"
497
+ })), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
498
+ type: "button",
499
+ role: "switch",
500
+ className: "dsh-codex-fast-switch",
501
+ "aria-checked": isFastMode,
502
+ "aria-label": t("quota.fastTitle"),
503
+ "data-checked": isFastMode ? "true" : "false",
504
+ onClick: () => {
505
+ quotaStore.setFastMode(sessionId, !isFastMode);
506
+ }
507
+ }, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
398
508
  }
399
509
  const inject = ["slots", "locale"];
400
510
  function apply(ctx) {
@@ -445,6 +555,7 @@ window.__ModuleLoader__.load({
445
555
  PROVIDER_ID: PROVIDER,
446
556
  USAGE_API_PATH: USAGE_ROUTE_PATH,
447
557
  resolveActiveProvider,
558
+ resolveSessionFastMode,
448
559
  isCodexProvider,
449
560
  didTurnFinish,
450
561
  windowLevelOf,
package/lib/index.d.ts CHANGED
@@ -61,10 +61,16 @@ declare function createAuthManager(options: AuthManagerOptions): {
61
61
  //#region src/catalog.d.ts
62
62
  export declare const PLUGIN_CACHE_DIR_NAME = "cache/dsh-codex-subscription";
63
63
  export declare const BACKUP_CACHE_FILE_NAME = "models-cache-backup.json";
64
+ export declare const RUNTIME_STATE_FILE_NAME = "runtime-state.json";
64
65
  interface RawReasoningLevel {
65
66
  effort?: string;
66
67
  description?: string;
67
68
  }
69
+ interface RawServiceTier {
70
+ id?: string;
71
+ name?: string;
72
+ description?: string;
73
+ }
68
74
  interface RawCodexModelEntry {
69
75
  slug?: string;
70
76
  display_name?: string;
@@ -78,6 +84,8 @@ interface RawCodexModelEntry {
78
84
  default_reasoning_level?: string;
79
85
  support_verbosity?: boolean;
80
86
  default_verbosity?: string;
87
+ additional_speed_tiers?: unknown[];
88
+ service_tiers?: RawServiceTier[];
81
89
  }
82
90
  interface RawModelsCache {
83
91
  fetched_at?: string;
@@ -85,12 +93,14 @@ interface RawModelsCache {
85
93
  models?: RawCodexModelEntry[];
86
94
  }
87
95
  declare function parseRawModelsCache(rawText: string): RawModelsCache;
96
+ declare function normalizeCodexSessionId(sessionId: string | undefined): string | undefined;
88
97
  declare function compareClientVersions(a: string | undefined, b: string | undefined): number;
89
98
  interface CodexModelMetadata {
90
99
  readonly contextWindow: number;
91
100
  readonly supportVerbosity: boolean;
92
101
  readonly defaultVerbosity: 'low' | 'medium' | 'high';
93
102
  readonly defaultReasoningLevel?: string | undefined;
103
+ readonly fastServiceTier?: string | undefined;
94
104
  }
95
105
  interface ParsedModelsCatalog {
96
106
  readonly clientVersion: string;
@@ -102,9 +112,8 @@ declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, b
102
112
  //#endregion
103
113
  //#region src/request.d.ts
104
114
  declare function buildCodexUserAgent(version?: string): string;
105
- declare function normalizeCodexSessionId(sessionId: string | undefined): string | undefined;
106
115
  declare function buildCodexHeaders(existingHeaders?: Record<string, string> | undefined, clientVersion?: string): Record<string, string>;
107
- 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;
108
117
  //#endregion
109
118
  //#region src/usage.d.ts
110
119
  declare function safeErrorMessage(error: unknown): string;
@@ -123,10 +132,14 @@ interface CodexQuotaSnapshot {
123
132
  type CodexQuotaResponse = {
124
133
  readonly ok: true;
125
134
  readonly quota: CodexQuotaSnapshot;
135
+ readonly defaultFastMode?: boolean | undefined;
136
+ readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
126
137
  } | {
127
138
  readonly ok: false;
128
139
  readonly error: string;
129
140
  readonly quota?: CodexQuotaSnapshot | undefined;
141
+ readonly defaultFastMode?: boolean | undefined;
142
+ readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
130
143
  };
131
144
  interface UsageFetcherOptions {
132
145
  readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
package/lib/index.js CHANGED
@@ -15,6 +15,7 @@ const DEFAULT_CLIENT_VERSION = "0.163.0";
15
15
  const DEFAULT_CONTEXT_WINDOW = 872e3;
16
16
  const PLUGIN_CACHE_DIR_NAME = "cache/dsh-codex-subscription";
17
17
  const BACKUP_CACHE_FILE_NAME = "models-cache-backup.json";
18
+ const RUNTIME_STATE_FILE_NAME = "runtime-state.json";
18
19
  const THINKING_LEVELS = [
19
20
  "off",
20
21
  "minimal",
@@ -36,6 +37,11 @@ function parseRawModelsCache(rawText) {
36
37
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new SyntaxError("models_cache.json root must be a JSON object");
37
38
  return parsed;
38
39
  }
40
+ function normalizeCodexSessionId(sessionId) {
41
+ if (typeof sessionId !== "string") return void 0;
42
+ const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
43
+ return stripped.length > 0 ? stripped : void 0;
44
+ }
39
45
  function isSameModelsCatalogPayload(a, b) {
40
46
  if (!a || !b) return false;
41
47
  const { fetched_at: _fa, ...restA } = a;
@@ -81,6 +87,16 @@ function isVerbosity(value) {
81
87
  function isPositiveSafeInt(value) {
82
88
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
83
89
  }
90
+ function resolveFastServiceTier(entry) {
91
+ if (Array.isArray(entry.service_tiers)) {
92
+ 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"));
93
+ if (matched && typeof matched.id === "string") return matched.id.trim();
94
+ 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;
95
+ return;
96
+ }
97
+ if (Array.isArray(entry.additional_speed_tiers) && entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast")) return "priority";
98
+ return "priority";
99
+ }
84
100
  function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
85
101
  const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
86
102
  const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
@@ -108,11 +124,13 @@ function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWind
108
124
  const supportVerbosity = entry.support_verbosity !== false;
109
125
  const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
110
126
  const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
127
+ const fastServiceTier = resolveFastServiceTier(entry);
111
128
  metadataById.set(id, {
112
129
  contextWindow: targetContextWindow,
113
130
  supportVerbosity,
114
131
  defaultVerbosity,
115
- defaultReasoningLevel
132
+ defaultReasoningLevel,
133
+ fastServiceTier
116
134
  });
117
135
  models.push({
118
136
  ...base ?? {},
@@ -181,11 +199,6 @@ function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
181
199
  const term = platform() === "darwin" ? "Apple_Terminal/455" : platform() === "win32" ? "WindowsTerminal" : "xterm-256color";
182
200
  return `codex_cli_rs/${version} (${detectOsLabel()}; ${cpu}) ${term}`;
183
201
  }
184
- function normalizeCodexSessionId(sessionId) {
185
- if (typeof sessionId !== "string") return void 0;
186
- const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
187
- return stripped.length > 0 ? stripped : void 0;
188
- }
189
202
  function buildCodexHeaders(existingHeaders, clientVersion = DEFAULT_CLIENT_VERSION) {
190
203
  const headers = {};
191
204
  for (const [key, value] of Object.entries(existingHeaders ?? {})) {
@@ -197,11 +210,15 @@ function buildCodexHeaders(existingHeaders, clientVersion = DEFAULT_CLIENT_VERSI
197
210
  headers["User-Agent"] = buildCodexUserAgent(clientVersion);
198
211
  return headers;
199
212
  }
200
- function sanitizeCodexPayload(payload, modelMeta) {
213
+ function sanitizeCodexPayload(payload, modelMeta, fastMode = false) {
201
214
  if (!payload || typeof payload !== "object") return payload;
202
215
  const cleaned = { ...payload };
203
216
  for (const field of FORBIDDEN_PAYLOAD_FIELDS) delete cleaned[field];
204
- if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
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;
205
222
  const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
206
223
  if (modelMeta?.supportVerbosity === false) {
207
224
  if (currentText !== void 0) {
@@ -569,6 +586,8 @@ const MAX_REQUEST_IMAGE_BYTES = 20971520;
569
586
  const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
570
587
  const REQUEST_IMAGE_MAX_BYTES = 1048576;
571
588
  const MAX_LOG_FILE_BYTES = 524288;
589
+ const MAX_PERSISTED_SESSIONS = 200;
590
+ const DEFAULT_SESSION_KEY = "__default__";
572
591
  function resolvePluginCacheDir(codexHome) {
573
592
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
574
593
  }
@@ -613,7 +632,23 @@ function writeBackupCache(codexHome, raw) {
613
632
  writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
614
633
  } catch {}
615
634
  }
635
+ function readPersistedSessionFastModes(codexHome) {
636
+ const result = new Map();
637
+ try {
638
+ const entries = JSON.parse(readFileSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), "utf8"))?.sessionFastModes;
639
+ if (entries && typeof entries === "object" && !Array.isArray(entries)) {
640
+ for (const [k, v] of Object.entries(entries)) if (typeof k === "string" && k !== "" && typeof v === "boolean") result.set(k, v);
641
+ }
642
+ } catch {}
643
+ return result;
644
+ }
645
+ function writePersistedSessionFastModes(codexHome, sessionFastModes) {
646
+ try {
647
+ writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ sessionFastModes: Object.fromEntries(sessionFastModes) }, null, 2)}\n`);
648
+ } catch {}
649
+ }
616
650
  const Config = z.object({
651
+ fastMode: z.boolean().default(false).description("新会话默认是否开启 Fast 极速模式(未填时默认 false;单会话可在右下角限额弹窗内独立切换)").volatile(),
617
652
  disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
618
653
  contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
619
654
  });
@@ -651,8 +686,32 @@ function apply(ctx, config = {}) {
651
686
  const baseModels = baseProvider.getModels();
652
687
  const retryPolicy = resolveRetryPolicy(void 0, "codex-subscription retryPolicy");
653
688
  const getCodexHome = () => resolveCodexHome();
689
+ const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
654
690
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
655
691
  const getContextWindows = () => readVolatile(config.contextWindows, {});
692
+ const sessionFastModes = readPersistedSessionFastModes(getCodexHome());
693
+ const getEffectiveFastMode = (sessionId) => {
694
+ const key = normalizeCodexSessionId(sessionId);
695
+ if (key !== void 0 && sessionFastModes.has(key)) return sessionFastModes.get(key) === true;
696
+ if (key === void 0 && sessionFastModes.has(DEFAULT_SESSION_KEY)) return sessionFastModes.get(DEFAULT_SESSION_KEY) === true;
697
+ return getConfigFastMode();
698
+ };
699
+ const setSessionFastMode = (sessionId, nextFastMode) => {
700
+ const codexHome = getCodexHome();
701
+ const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
702
+ sessionFastModes.delete(key);
703
+ sessionFastModes.set(key, nextFastMode);
704
+ while (sessionFastModes.size > MAX_PERSISTED_SESSIONS) {
705
+ const oldest = sessionFastModes.keys().next().value;
706
+ if (oldest === void 0) break;
707
+ sessionFastModes.delete(oldest);
708
+ }
709
+ writePersistedSessionFastModes(codexHome, sessionFastModes);
710
+ appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
711
+ sessionId: key,
712
+ fastMode: nextFastMode
713
+ });
714
+ };
656
715
  const authManager = createAuthManager({
657
716
  getCodexHome,
658
717
  getClientVersion: () => ensureSnapshot().catalog.clientVersion,
@@ -690,7 +749,7 @@ function apply(ctx, config = {}) {
690
749
  transport: "sse",
691
750
  headers: buildCodexHeaders(rest.headers, catalog.clientVersion),
692
751
  async onPayload(payload, requestModel) {
693
- return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta);
752
+ return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode(normalizedSessionId));
694
753
  }
695
754
  };
696
755
  }
@@ -854,12 +913,28 @@ function apply(ctx, config = {}) {
854
913
  if (typeof registerFetch !== "function") return;
855
914
  const registerRoute = () => registerFetch.call(connection?.fetch, {
856
915
  path: USAGE_ROUTE_PATH,
857
- methods: ["GET"],
916
+ methods: ["GET", "POST"],
858
917
  requestBody: "buffered",
859
918
  fetch: async (request) => {
919
+ if (request.method === "POST") {
920
+ const body = await request.json().catch(() => null);
921
+ if (typeof body?.fastMode === "boolean") setSessionFastMode(typeof body.sessionId === "string" ? body.sessionId : void 0, body.fastMode);
922
+ return new Response(JSON.stringify({
923
+ ok: true,
924
+ defaultFastMode: getConfigFastMode(),
925
+ sessionFastModes: Object.fromEntries(sessionFastModes)
926
+ }), {
927
+ status: 200,
928
+ headers: { "content-type": "application/json" }
929
+ });
930
+ }
860
931
  const force = new URL(request.url, "http://localhost").searchParams.get("force") === "1";
861
932
  const result = await usageFetcher.getUsage(force);
862
- return new Response(JSON.stringify(result), {
933
+ return new Response(JSON.stringify({
934
+ ...result,
935
+ defaultFastMode: getConfigFastMode(),
936
+ sessionFastModes: Object.fromEntries(sessionFastModes)
937
+ }), {
863
938
  status: 200,
864
939
  headers: { "content-type": "application/json" }
865
940
  });
@@ -900,4 +975,4 @@ const __test = Object.freeze({
900
975
  createUsageFetcher
901
976
  });
902
977
  //#endregion
903
- export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, PLUGIN_CACHE_DIR_NAME, __test, apply, inject, name };
978
+ export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, PLUGIN_CACHE_DIR_NAME, RUNTIME_STATE_FILE_NAME, __test, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",