@lyogvce/dsh-codex-subscription 0.2.7 → 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
@@ -22,7 +22,7 @@
22
22
  - 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化纯 UUID `session-id` / `x-client-request-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
23
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
+ - **⚡ 会话级 Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度)**:每个会话可在限额弹窗底部独立开关 Fast 模式(互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后当前会话胶囊显示闪电标识 `⚡`,并在该会话请求体中自动注入 `"service_tier": "priority"`。
26
26
 
27
27
  ---
28
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,7 +152,8 @@ window.__ModuleLoader__.load({
139
152
  const ttlMs = options.ttlMs ?? 6e4;
140
153
  let state = {
141
154
  quota: null,
142
- fastMode: false,
155
+ defaultFastMode: false,
156
+ sessionFastModes: {},
143
157
  loading: false,
144
158
  error: null
145
159
  };
@@ -152,6 +166,21 @@ window.__ModuleLoader__.load({
152
166
  state = next;
153
167
  for (const listener of listeners) listener();
154
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
+ }
155
184
  async function refresh(force = false) {
156
185
  if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
157
186
  if (inflight !== null && (!force || inflightForce)) return inflight;
@@ -181,10 +210,10 @@ window.__ModuleLoader__.load({
181
210
  }
182
211
  const payload = await response.json();
183
212
  if (seq !== requestSeq) return;
184
- const resolvedFastMode = fastSeqAtStart === fastModeSeq && typeof payload?.fastMode === "boolean" ? payload.fastMode : state.fastMode;
213
+ const fastState = mergeFastStateFromPayload(payload, fastSeqAtStart === fastModeSeq);
185
214
  if (payload && payload.ok === true && payload.quota) setState({
186
215
  quota: payload.quota,
187
- fastMode: resolvedFastMode,
216
+ ...fastState,
188
217
  loading: false,
189
218
  error: null
190
219
  });
@@ -193,7 +222,7 @@ window.__ModuleLoader__.load({
193
222
  const errMsg = payload && !payload.ok && typeof payload.error === "string" ? payload.error : "Invalid response";
194
223
  setState({
195
224
  quota: fallbackQuota ?? null,
196
- fastMode: resolvedFastMode,
225
+ ...fastState,
197
226
  loading: false,
198
227
  error: errMsg
199
228
  });
@@ -215,12 +244,16 @@ window.__ModuleLoader__.load({
215
244
  inflight = task;
216
245
  return task;
217
246
  }
218
- async function setFastMode(enabled) {
247
+ async function setFastMode(sessionId, enabled) {
219
248
  const nextFastMode = Boolean(enabled);
249
+ const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
220
250
  const seq = ++fastModeSeq;
221
251
  setState({
222
252
  ...state,
223
- fastMode: nextFastMode
253
+ sessionFastModes: {
254
+ ...state.sessionFastModes,
255
+ [key]: nextFastMode
256
+ }
224
257
  });
225
258
  try {
226
259
  const response = await fetchFn(USAGE_ROUTE_PATH, {
@@ -229,13 +262,16 @@ window.__ModuleLoader__.load({
229
262
  "Content-Type": "application/json",
230
263
  Accept: "application/json"
231
264
  },
232
- body: JSON.stringify({ fastMode: nextFastMode })
265
+ body: JSON.stringify({
266
+ sessionId: key,
267
+ fastMode: nextFastMode
268
+ })
233
269
  });
234
270
  if (seq !== fastModeSeq || !response.ok) return;
235
271
  const payload = await response.json();
236
- if (seq === fastModeSeq && typeof payload?.fastMode === "boolean") setState({
272
+ if (seq === fastModeSeq) setState({
237
273
  ...state,
238
- fastMode: payload.fastMode
274
+ ...mergeFastStateFromPayload(payload, true)
239
275
  });
240
276
  } catch {}
241
277
  }
@@ -388,6 +424,7 @@ window.__ModuleLoader__.load({
388
424
  })), h("div", { className: "dsh-codex-quota-meta" }, h("span", null, `${t("quota.remainPrefix")}${remainPercent}%`), countdown !== "" ? h("span", null, countdown) : null));
389
425
  };
390
426
  const quota = quotaSnap.quota;
427
+ const isFastMode = resolveSessionFastMode(quotaSnap, sessionId);
391
428
  const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
392
429
  const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
393
430
  return h("span", {
@@ -398,7 +435,7 @@ window.__ModuleLoader__.load({
398
435
  type: "button",
399
436
  className: "dsh-codex-quota-trigger",
400
437
  "data-level": level,
401
- "data-fast": quotaSnap.fastMode ? "true" : void 0,
438
+ "data-fast": isFastMode ? "true" : void 0,
402
439
  "aria-label": `${t("quota.title")}: ${pillText}`,
403
440
  "aria-haspopup": "dialog",
404
441
  "aria-expanded": open,
@@ -425,7 +462,7 @@ window.__ModuleLoader__.load({
425
462
  r: 5.5,
426
463
  strokeDasharray: ringDash,
427
464
  transform: "rotate(-90 7 7)"
428
- })), quotaSnap.fastMode ? h("svg", {
465
+ })), isFastMode ? h("svg", {
429
466
  className: "dsh-codex-quota-fast-bolt",
430
467
  viewBox: "0 0 12 12",
431
468
  width: 11,
@@ -461,11 +498,11 @@ window.__ModuleLoader__.load({
461
498
  type: "button",
462
499
  role: "switch",
463
500
  className: "dsh-codex-fast-switch",
464
- "aria-checked": quotaSnap.fastMode,
501
+ "aria-checked": isFastMode,
465
502
  "aria-label": t("quota.fastTitle"),
466
- "data-checked": quotaSnap.fastMode ? "true" : "false",
503
+ "data-checked": isFastMode ? "true" : "false",
467
504
  onClick: () => {
468
- quotaStore.setFastMode(!quotaSnap.fastMode);
505
+ quotaStore.setFastMode(sessionId, !isFastMode);
469
506
  }
470
507
  }, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
471
508
  }
@@ -518,6 +555,7 @@ window.__ModuleLoader__.load({
518
555
  PROVIDER_ID: PROVIDER,
519
556
  USAGE_API_PATH: USAGE_ROUTE_PATH,
520
557
  resolveActiveProvider,
558
+ resolveSessionFastMode,
521
559
  isCodexProvider,
522
560
  didTurnFinish,
523
561
  windowLevelOf,
package/lib/index.d.ts CHANGED
@@ -93,6 +93,7 @@ interface RawModelsCache {
93
93
  models?: RawCodexModelEntry[];
94
94
  }
95
95
  declare function parseRawModelsCache(rawText: string): RawModelsCache;
96
+ declare function normalizeCodexSessionId(sessionId: string | undefined): string | undefined;
96
97
  declare function compareClientVersions(a: string | undefined, b: string | undefined): number;
97
98
  interface CodexModelMetadata {
98
99
  readonly contextWindow: number;
@@ -111,7 +112,6 @@ declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, b
111
112
  //#endregion
112
113
  //#region src/request.d.ts
113
114
  declare function buildCodexUserAgent(version?: string): string;
114
- declare function normalizeCodexSessionId(sessionId: string | undefined): string | undefined;
115
115
  declare function buildCodexHeaders(existingHeaders?: Record<string, string> | undefined, clientVersion?: string): Record<string, string>;
116
116
  declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined, fastMode?: boolean): Record<string, unknown> | null | undefined;
117
117
  //#endregion
@@ -132,12 +132,14 @@ interface CodexQuotaSnapshot {
132
132
  type CodexQuotaResponse = {
133
133
  readonly ok: true;
134
134
  readonly quota: CodexQuotaSnapshot;
135
- readonly fastMode?: boolean | undefined;
135
+ readonly defaultFastMode?: boolean | undefined;
136
+ readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
136
137
  } | {
137
138
  readonly ok: false;
138
139
  readonly error: string;
139
140
  readonly quota?: CodexQuotaSnapshot | undefined;
140
- readonly fastMode?: boolean | undefined;
141
+ readonly defaultFastMode?: boolean | undefined;
142
+ readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
141
143
  };
142
144
  interface UsageFetcherOptions {
143
145
  readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
package/lib/index.js CHANGED
@@ -37,6 +37,11 @@ function parseRawModelsCache(rawText) {
37
37
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new SyntaxError("models_cache.json root must be a JSON object");
38
38
  return parsed;
39
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
+ }
40
45
  function isSameModelsCatalogPayload(a, b) {
41
46
  if (!a || !b) return false;
42
47
  const { fetched_at: _fa, ...restA } = a;
@@ -194,11 +199,6 @@ function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
194
199
  const term = platform() === "darwin" ? "Apple_Terminal/455" : platform() === "win32" ? "WindowsTerminal" : "xterm-256color";
195
200
  return `codex_cli_rs/${version} (${detectOsLabel()}; ${cpu}) ${term}`;
196
201
  }
197
- function normalizeCodexSessionId(sessionId) {
198
- if (typeof sessionId !== "string") return void 0;
199
- const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
200
- return stripped.length > 0 ? stripped : void 0;
201
- }
202
202
  function buildCodexHeaders(existingHeaders, clientVersion = DEFAULT_CLIENT_VERSION) {
203
203
  const headers = {};
204
204
  for (const [key, value] of Object.entries(existingHeaders ?? {})) {
@@ -586,6 +586,8 @@ const MAX_REQUEST_IMAGE_BYTES = 20971520;
586
586
  const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
587
587
  const REQUEST_IMAGE_MAX_BYTES = 1048576;
588
588
  const MAX_LOG_FILE_BYTES = 524288;
589
+ const MAX_PERSISTED_SESSIONS = 200;
590
+ const DEFAULT_SESSION_KEY = "__default__";
589
591
  function resolvePluginCacheDir(codexHome) {
590
592
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
591
593
  }
@@ -630,21 +632,23 @@ function writeBackupCache(codexHome, raw) {
630
632
  writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
631
633
  } catch {}
632
634
  }
633
- function readPersistedFastMode(codexHome) {
635
+ function readPersistedSessionFastModes(codexHome) {
636
+ const result = new Map();
634
637
  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
- }
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;
640
644
  }
641
- function writePersistedFastMode(codexHome, fastMode) {
645
+ function writePersistedSessionFastModes(codexHome, sessionFastModes) {
642
646
  try {
643
- writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ fastMode }, null, 2)}\n`);
647
+ writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ sessionFastModes: Object.fromEntries(sessionFastModes) }, null, 2)}\n`);
644
648
  } catch {}
645
649
  }
646
650
  const Config = z.object({
647
- fastMode: z.boolean().default(false).description("开启 Fast 极速模式(注入 service_tier: priority,最高 2x 推理速度,消耗更多订阅限额)").volatile(),
651
+ fastMode: z.boolean().default(false).description("新会话默认是否开启 Fast 极速模式(未填时默认 false;单会话可在右下角限额弹窗内独立切换)").volatile(),
648
652
  disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
649
653
  contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
650
654
  });
@@ -682,19 +686,30 @@ function apply(ctx, config = {}) {
682
686
  const baseModels = baseProvider.getModels();
683
687
  const retryPolicy = resolveRetryPolicy(void 0, "codex-subscription retryPolicy");
684
688
  const getCodexHome = () => resolveCodexHome();
685
- const getConfigFastMode = () => Boolean(readVolatile(config.fastMode, false));
689
+ const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
686
690
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
687
691
  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 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) => {
692
700
  const codexHome = getCodexHome();
693
- fastModeOverride = nextFastMode;
694
- writePersistedFastMode(codexHome, nextFastMode);
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);
695
710
  appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
696
- fastMode: nextFastMode,
697
- source
711
+ sessionId: key,
712
+ fastMode: nextFastMode
698
713
  });
699
714
  };
700
715
  const authManager = createAuthManager({
@@ -734,7 +749,7 @@ function apply(ctx, config = {}) {
734
749
  transport: "sse",
735
750
  headers: buildCodexHeaders(rest.headers, catalog.clientVersion),
736
751
  async onPayload(payload, requestModel) {
737
- return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode());
752
+ return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode(normalizedSessionId));
738
753
  }
739
754
  };
740
755
  }
@@ -903,10 +918,11 @@ function apply(ctx, config = {}) {
903
918
  fetch: async (request) => {
904
919
  if (request.method === "POST") {
905
920
  const body = await request.json().catch(() => null);
906
- if (typeof body?.fastMode === "boolean") setFastModeOverride(body.fastMode, "ui");
921
+ if (typeof body?.fastMode === "boolean") setSessionFastMode(typeof body.sessionId === "string" ? body.sessionId : void 0, body.fastMode);
907
922
  return new Response(JSON.stringify({
908
923
  ok: true,
909
- fastMode: getEffectiveFastMode()
924
+ defaultFastMode: getConfigFastMode(),
925
+ sessionFastModes: Object.fromEntries(sessionFastModes)
910
926
  }), {
911
927
  status: 200,
912
928
  headers: { "content-type": "application/json" }
@@ -916,7 +932,8 @@ function apply(ctx, config = {}) {
916
932
  const result = await usageFetcher.getUsage(force);
917
933
  return new Response(JSON.stringify({
918
934
  ...result,
919
- fastMode: getEffectiveFastMode()
935
+ defaultFastMode: getConfigFastMode(),
936
+ sessionFastModes: Object.fromEntries(sessionFastModes)
920
937
  }), {
921
938
  status: 200,
922
939
  headers: { "content-type": "application/json" }
@@ -929,11 +946,6 @@ function apply(ctx, config = {}) {
929
946
  if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
930
947
  else mountUsageRoute(ctx);
931
948
  ctx.on?.("loader/volatile-update", () => {
932
- const currentConfigFastMode = getConfigFastMode();
933
- if (currentConfigFastMode !== lastConfigFastMode) {
934
- lastConfigFastMode = currentConfigFastMode;
935
- setFastModeOverride(currentConfigFastMode, "config");
936
- }
937
949
  ensureSnapshot();
938
950
  ctx.llm.emitAdaptersUpdated?.();
939
951
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.7",
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",