@lyogvce/dsh-codex-subscription 0.2.7 → 0.2.9

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,24 +152,54 @@ 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
  };
160
+ let confirmedSessionFastModes = {};
161
+ let lastConfirmedFastSeq = 0;
162
+ const latestSeqBySession = new Map();
163
+ const pendingBySession = new Map();
146
164
  let inflight = null;
147
165
  let inflightForce = false;
148
166
  let requestSeq = 0;
149
167
  let fastModeSeq = 0;
168
+ let fastMutationEpoch = 0;
150
169
  const listeners = new Set();
151
170
  function setState(next) {
152
171
  state = next;
153
172
  for (const listener of listeners) listener();
154
173
  }
174
+ function deriveSessionFastModes() {
175
+ if (pendingBySession.size === 0) return confirmedSessionFastModes;
176
+ const merged = { ...confirmedSessionFastModes };
177
+ for (const [k, pending] of pendingBySession) merged[k] = pending.enabled;
178
+ return merged;
179
+ }
180
+ function parseServerSessionFastModes(raw) {
181
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
182
+ const nextMap = {};
183
+ for (const [k, v] of Object.entries(raw)) if (typeof k === "string" && k !== "" && typeof v === "boolean") nextMap[k] = v;
184
+ return nextMap;
185
+ }
186
+ function mergeFastStateFromPayload(payload, allowSessionReplace) {
187
+ const defaultFastMode = typeof payload?.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
188
+ if (allowSessionReplace) {
189
+ const parsedMap = parseServerSessionFastModes(payload?.sessionFastModes);
190
+ if (parsedMap !== void 0) confirmedSessionFastModes = parsedMap;
191
+ }
192
+ return {
193
+ defaultFastMode,
194
+ sessionFastModes: deriveSessionFastModes()
195
+ };
196
+ }
155
197
  async function refresh(force = false) {
156
198
  if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
157
199
  if (inflight !== null && (!force || inflightForce)) return inflight;
158
200
  const seq = ++requestSeq;
159
- const fastSeqAtStart = fastModeSeq;
201
+ const canReplaceFastAtStart = pendingBySession.size === 0;
202
+ const fastEpochAtStart = fastMutationEpoch;
160
203
  inflightForce = force;
161
204
  setState({
162
205
  ...state,
@@ -181,10 +224,10 @@ window.__ModuleLoader__.load({
181
224
  }
182
225
  const payload = await response.json();
183
226
  if (seq !== requestSeq) return;
184
- const resolvedFastMode = fastSeqAtStart === fastModeSeq && typeof payload?.fastMode === "boolean" ? payload.fastMode : state.fastMode;
227
+ const fastState = mergeFastStateFromPayload(payload, canReplaceFastAtStart && pendingBySession.size === 0 && fastEpochAtStart === fastMutationEpoch);
185
228
  if (payload && payload.ok === true && payload.quota) setState({
186
229
  quota: payload.quota,
187
- fastMode: resolvedFastMode,
230
+ ...fastState,
188
231
  loading: false,
189
232
  error: null
190
233
  });
@@ -193,7 +236,7 @@ window.__ModuleLoader__.load({
193
236
  const errMsg = payload && !payload.ok && typeof payload.error === "string" ? payload.error : "Invalid response";
194
237
  setState({
195
238
  quota: fallbackQuota ?? null,
196
- fastMode: resolvedFastMode,
239
+ ...fastState,
197
240
  loading: false,
198
241
  error: errMsg
199
242
  });
@@ -215,12 +258,30 @@ window.__ModuleLoader__.load({
215
258
  inflight = task;
216
259
  return task;
217
260
  }
218
- async function setFastMode(enabled) {
261
+ async function setFastMode(sessionId, enabled) {
219
262
  const nextFastMode = Boolean(enabled);
263
+ const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
220
264
  const seq = ++fastModeSeq;
265
+ fastMutationEpoch += 1;
266
+ latestSeqBySession.set(key, seq);
267
+ pendingBySession.set(key, {
268
+ seq,
269
+ enabled: nextFastMode
270
+ });
271
+ const rollback = (errorMsg) => {
272
+ if (latestSeqBySession.get(key) !== seq) return;
273
+ fastMutationEpoch += 1;
274
+ pendingBySession.delete(key);
275
+ setState({
276
+ ...state,
277
+ sessionFastModes: deriveSessionFastModes(),
278
+ error: errorMsg
279
+ });
280
+ };
221
281
  setState({
222
282
  ...state,
223
- fastMode: nextFastMode
283
+ sessionFastModes: deriveSessionFastModes(),
284
+ error: null
224
285
  });
225
286
  try {
226
287
  const response = await fetchFn(USAGE_ROUTE_PATH, {
@@ -229,15 +290,43 @@ window.__ModuleLoader__.load({
229
290
  "Content-Type": "application/json",
230
291
  Accept: "application/json"
231
292
  },
232
- body: JSON.stringify({ fastMode: nextFastMode })
293
+ body: JSON.stringify({
294
+ sessionId: key,
295
+ fastMode: nextFastMode
296
+ })
233
297
  });
234
- if (seq !== fastModeSeq || !response.ok) return;
298
+ if (latestSeqBySession.get(key) !== seq) return;
299
+ if (!response.ok) {
300
+ const errPayload = await response.json().catch(() => null);
301
+ rollback(typeof errPayload?.error === "string" && errPayload.error !== "" ? errPayload.error : `HTTP ${response.status}`);
302
+ return;
303
+ }
235
304
  const payload = await response.json();
236
- if (seq === fastModeSeq && typeof payload?.fastMode === "boolean") setState({
305
+ if (latestSeqBySession.get(key) !== seq) return;
306
+ if (!payload || payload.ok === false) {
307
+ rollback(typeof payload?.error === "string" && payload.error !== "" ? payload.error : "Invalid response");
308
+ return;
309
+ }
310
+ fastMutationEpoch += 1;
311
+ pendingBySession.delete(key);
312
+ const parsedMap = parseServerSessionFastModes(payload.sessionFastModes);
313
+ if (seq > lastConfirmedFastSeq && parsedMap !== void 0) {
314
+ lastConfirmedFastSeq = seq;
315
+ confirmedSessionFastModes = parsedMap;
316
+ } else confirmedSessionFastModes = {
317
+ ...confirmedSessionFastModes,
318
+ [key]: parsedMap?.[key] ?? nextFastMode
319
+ };
320
+ const defaultFastMode = typeof payload.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
321
+ setState({
237
322
  ...state,
238
- fastMode: payload.fastMode
323
+ defaultFastMode,
324
+ sessionFastModes: deriveSessionFastModes(),
325
+ error: null
239
326
  });
240
- } catch {}
327
+ } catch (error) {
328
+ rollback(safeErrorMessage(error));
329
+ }
241
330
  }
242
331
  return {
243
332
  getSnapshot: () => state,
@@ -323,6 +412,18 @@ window.__ModuleLoader__.load({
323
412
  getSnapshot: () => null,
324
413
  subscribe: () => () => {}
325
414
  };
415
+ function renderFastBolt(size) {
416
+ return h("svg", {
417
+ className: "dsh-codex-quota-fast-bolt",
418
+ viewBox: "0 0 12 12",
419
+ width: size,
420
+ height: size,
421
+ "aria-hidden": true
422
+ }, h("path", {
423
+ d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
424
+ fill: "currentColor"
425
+ }));
426
+ }
326
427
  function CodexQuotaPill(props) {
327
428
  const { sessionId, quotaStore, getModelStore, useProjection, useSession, t, getLocale } = props;
328
429
  const modelStore = getModelStore(sessionId);
@@ -388,6 +489,7 @@ window.__ModuleLoader__.load({
388
489
  })), h("div", { className: "dsh-codex-quota-meta" }, h("span", null, `${t("quota.remainPrefix")}${remainPercent}%`), countdown !== "" ? h("span", null, countdown) : null));
389
490
  };
390
491
  const quota = quotaSnap.quota;
492
+ const isFastMode = resolveSessionFastMode(quotaSnap, sessionId);
391
493
  const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
392
494
  const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
393
495
  return h("span", {
@@ -398,7 +500,7 @@ window.__ModuleLoader__.load({
398
500
  type: "button",
399
501
  className: "dsh-codex-quota-trigger",
400
502
  "data-level": level,
401
- "data-fast": quotaSnap.fastMode ? "true" : void 0,
503
+ "data-fast": isFastMode ? "true" : void 0,
402
504
  "aria-label": `${t("quota.title")}: ${pillText}`,
403
505
  "aria-haspopup": "dialog",
404
506
  "aria-expanded": open,
@@ -425,16 +527,7 @@ window.__ModuleLoader__.load({
425
527
  r: 5.5,
426
528
  strokeDasharray: ringDash,
427
529
  transform: "rotate(-90 7 7)"
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", {
530
+ })), isFastMode ? renderFastBolt(11) : null, h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
438
531
  className: "dsh-codex-quota-popover",
439
532
  role: "dialog",
440
533
  "aria-label": t("quota.title"),
@@ -448,24 +541,15 @@ window.__ModuleLoader__.load({
448
541
  onClick: () => {
449
542
  quotaStore.refresh(true);
450
543
  }
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", {
544
+ }, 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" }, renderFastBolt(12), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
461
545
  type: "button",
462
546
  role: "switch",
463
547
  className: "dsh-codex-fast-switch",
464
- "aria-checked": quotaSnap.fastMode,
548
+ "aria-checked": isFastMode,
465
549
  "aria-label": t("quota.fastTitle"),
466
- "data-checked": quotaSnap.fastMode ? "true" : "false",
550
+ "data-checked": isFastMode ? "true" : "false",
467
551
  onClick: () => {
468
- quotaStore.setFastMode(!quotaSnap.fastMode);
552
+ quotaStore.setFastMode(sessionId, !isFastMode);
469
553
  }
470
554
  }, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
471
555
  }
@@ -518,6 +602,7 @@ window.__ModuleLoader__.load({
518
602
  PROVIDER_ID: PROVIDER,
519
603
  USAGE_API_PATH: USAGE_ROUTE_PATH,
520
604
  resolveActiveProvider,
605
+ resolveSessionFastMode,
521
606
  isCodexProvider,
522
607
  didTurnFinish,
523
608
  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
@@ -35,8 +35,15 @@ const DEFAULT_DISABLED_MODELS = Object.freeze([
35
35
  function parseRawModelsCache(rawText) {
36
36
  const parsed = JSON.parse(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
+ const models = parsed.models;
39
+ if (!Array.isArray(models) || !models.some((entry) => entry !== null && typeof entry === "object" && typeof entry.slug === "string" && entry.slug.trim() !== "" && entry.visibility === "list")) throw new SyntaxError("models_cache.json must contain at least one visible model entry");
38
40
  return parsed;
39
41
  }
42
+ function normalizeCodexSessionId(sessionId) {
43
+ if (typeof sessionId !== "string") return void 0;
44
+ const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
45
+ return stripped.length > 0 ? stripped : void 0;
46
+ }
40
47
  function isSameModelsCatalogPayload(a, b) {
41
48
  if (!a || !b) return false;
42
49
  const { fetched_at: _fa, ...restA } = a;
@@ -89,7 +96,7 @@ function resolveFastServiceTier(entry) {
89
96
  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
97
  return;
91
98
  }
92
- if (Array.isArray(entry.additional_speed_tiers) && entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast")) return "priority";
99
+ if (Array.isArray(entry.additional_speed_tiers)) return entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast") ? "priority" : void 0;
93
100
  return "priority";
94
101
  }
95
102
  function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
@@ -194,11 +201,6 @@ function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
194
201
  const term = platform() === "darwin" ? "Apple_Terminal/455" : platform() === "win32" ? "WindowsTerminal" : "xterm-256color";
195
202
  return `codex_cli_rs/${version} (${detectOsLabel()}; ${cpu}) ${term}`;
196
203
  }
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
204
  function buildCodexHeaders(existingHeaders, clientVersion = DEFAULT_CLIENT_VERSION) {
203
205
  const headers = {};
204
206
  for (const [key, value] of Object.entries(existingHeaders ?? {})) {
@@ -218,7 +220,7 @@ function sanitizeCodexPayload(payload, modelMeta, fastMode = false) {
218
220
  const tier = modelMeta === void 0 ? "priority" : modelMeta.fastServiceTier;
219
221
  if (typeof tier === "string" && tier !== "") cleaned.service_tier = tier;
220
222
  else delete cleaned.service_tier;
221
- } else if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
223
+ } else delete cleaned.service_tier;
222
224
  const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
223
225
  if (modelMeta?.supportVerbosity === false) {
224
226
  if (currentText !== void 0) {
@@ -586,9 +588,17 @@ const MAX_REQUEST_IMAGE_BYTES = 20971520;
586
588
  const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
587
589
  const REQUEST_IMAGE_MAX_BYTES = 1048576;
588
590
  const MAX_LOG_FILE_BYTES = 524288;
591
+ const MAX_PERSISTED_SESSIONS = 200;
592
+ const DEFAULT_SESSION_KEY = "__default__";
589
593
  function resolvePluginCacheDir(codexHome) {
590
594
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
591
595
  }
596
+ function sliceUtf8Prefix(buf, maxBytes) {
597
+ if (buf.length <= maxBytes) return buf;
598
+ let end = maxBytes;
599
+ while (end > 0 && (buf[end] & 192) === 128) end -= 1;
600
+ return buf.subarray(0, end);
601
+ }
592
602
  function appendPluginLog(codexHome, ctx, level, event, detail) {
593
603
  const line = `[${new Date().toISOString()}] [${level}] ${event} ${JSON.stringify(detail)}\n`;
594
604
  if (level === "ERROR") {
@@ -596,6 +606,7 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
596
606
  else console.error(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
597
607
  } else if (level === "WARN") {
598
608
  if (typeof ctx.logger?.warn === "function") ctx.logger.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
609
+ else console.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
599
610
  }
600
611
  try {
601
612
  const cacheDir = resolvePluginCacheDir(codexHome);
@@ -604,18 +615,24 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
604
615
  mode: 448
605
616
  });
606
617
  const logPath = join(cacheDir, LOG_FILE_NAME);
618
+ const rawLineBuf = Buffer.from(line, "utf8");
619
+ const lineBuf = rawLineBuf.length <= MAX_LOG_FILE_BYTES ? rawLineBuf : Buffer.concat([sliceUtf8Prefix(rawLineBuf, 524287), Buffer.from("\n", "utf8")]);
620
+ let st;
607
621
  try {
608
- if (statSync(logPath).size > MAX_LOG_FILE_BYTES) {
609
- const buf = readFileSync(logPath);
610
- const halfOffset = Math.max(0, buf.length - Math.floor(MAX_LOG_FILE_BYTES / 2));
611
- const newlineIdx = buf.indexOf(10, halfOffset);
612
- writeFileAtomicSync(logPath, buf.subarray(newlineIdx >= 0 && newlineIdx + 1 < buf.length ? newlineIdx + 1 : halfOffset));
613
- }
614
- } catch {}
615
- appendFileSync(logPath, line, {
616
- encoding: "utf8",
617
- mode: 384
618
- });
622
+ st = statSync(logPath);
623
+ } catch {
624
+ st = void 0;
625
+ }
626
+ if (st !== void 0 && st.size + lineBuf.length > MAX_LOG_FILE_BYTES) {
627
+ const buf = readFileSync(logPath);
628
+ const keepBudget = Math.max(0, Math.floor(MAX_LOG_FILE_BYTES / 2) - lineBuf.length);
629
+ const startOffset = Math.max(0, buf.length - keepBudget);
630
+ const newlineIdx = keepBudget > 0 ? buf.indexOf(10, startOffset) : -1;
631
+ const tail = newlineIdx >= 0 && newlineIdx + 1 < buf.length ? buf.subarray(newlineIdx + 1) : Buffer.alloc(0);
632
+ writeFileAtomicSync(logPath, Buffer.concat([tail, lineBuf]));
633
+ return;
634
+ }
635
+ appendFileSync(logPath, lineBuf, { mode: 384 });
619
636
  } catch {}
620
637
  }
621
638
  function readBackupCache(codexHome) {
@@ -625,26 +642,32 @@ function readBackupCache(codexHome) {
625
642
  return;
626
643
  }
627
644
  }
628
- function writeBackupCache(codexHome, raw) {
645
+ function writeBackupCache(codexHome, ctx, raw) {
629
646
  try {
630
647
  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;
648
+ } catch (error) {
649
+ appendPluginLog(codexHome, ctx, "WARN", "catalog.backup_write_error", { error: safeErrorMessage(error) });
639
650
  }
640
651
  }
641
- function writePersistedFastMode(codexHome, fastMode) {
652
+ function readPersistedSessionFastModes(codexHome) {
653
+ const result = new Map();
642
654
  try {
643
- writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ fastMode }, null, 2)}\n`);
655
+ const entries = JSON.parse(readFileSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), "utf8"))?.sessionFastModes;
656
+ if (entries && typeof entries === "object" && !Array.isArray(entries)) {
657
+ for (const [k, v] of Object.entries(entries)) if (typeof k === "string" && k !== "" && typeof v === "boolean") result.set(k, v);
658
+ }
644
659
  } catch {}
660
+ return result;
661
+ }
662
+ function writePersistedSessionFastModes(codexHome, ctx, sessionFastModes) {
663
+ try {
664
+ writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ sessionFastModes: Object.fromEntries(sessionFastModes) }, null, 2)}\n`);
665
+ } catch (error) {
666
+ appendPluginLog(codexHome, ctx, "WARN", "fast_mode.persist_error", { error: safeErrorMessage(error) });
667
+ }
645
668
  }
646
669
  const Config = z.object({
647
- fastMode: z.boolean().default(false).description("开启 Fast 极速模式(注入 service_tier: priority,最高 2x 推理速度,消耗更多订阅限额)").volatile(),
670
+ fastMode: z.boolean().default(false).description("新会话默认是否开启 Fast 极速模式(未填时默认 false;单会话可在右下角限额弹窗内独立切换)").volatile(),
648
671
  disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
649
672
  contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
650
673
  });
@@ -682,19 +705,30 @@ function apply(ctx, config = {}) {
682
705
  const baseModels = baseProvider.getModels();
683
706
  const retryPolicy = resolveRetryPolicy(void 0, "codex-subscription retryPolicy");
684
707
  const getCodexHome = () => resolveCodexHome();
685
- const getConfigFastMode = () => Boolean(readVolatile(config.fastMode, false));
708
+ const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
686
709
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
687
710
  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) => {
711
+ const sessionFastModes = readPersistedSessionFastModes(getCodexHome());
712
+ const getEffectiveFastMode = (sessionId) => {
713
+ const key = normalizeCodexSessionId(sessionId);
714
+ if (key !== void 0 && sessionFastModes.has(key)) return sessionFastModes.get(key) === true;
715
+ if (key === void 0 && sessionFastModes.has(DEFAULT_SESSION_KEY)) return sessionFastModes.get(DEFAULT_SESSION_KEY) === true;
716
+ return getConfigFastMode();
717
+ };
718
+ const setSessionFastMode = (sessionId, nextFastMode) => {
692
719
  const codexHome = getCodexHome();
693
- fastModeOverride = nextFastMode;
694
- writePersistedFastMode(codexHome, nextFastMode);
720
+ const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
721
+ sessionFastModes.delete(key);
722
+ sessionFastModes.set(key, nextFastMode);
723
+ while (sessionFastModes.size > MAX_PERSISTED_SESSIONS) {
724
+ const oldest = sessionFastModes.keys().next().value;
725
+ if (oldest === void 0) break;
726
+ sessionFastModes.delete(oldest);
727
+ }
728
+ writePersistedSessionFastModes(codexHome, ctx, sessionFastModes);
695
729
  appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
696
- fastMode: nextFastMode,
697
- source
730
+ sessionId: key,
731
+ fastMode: nextFastMode
698
732
  });
699
733
  };
700
734
  const authManager = createAuthManager({
@@ -704,6 +738,7 @@ function apply(ctx, config = {}) {
704
738
  });
705
739
  let fileSignature;
706
740
  let cachedRaw;
741
+ let lastRejectedRaw;
707
742
  let rawRevision = 0;
708
743
  let snapshotSignature;
709
744
  let currentCatalog = {
@@ -734,7 +769,7 @@ function apply(ctx, config = {}) {
734
769
  transport: "sse",
735
770
  headers: buildCodexHeaders(rest.headers, catalog.clientVersion),
736
771
  async onPayload(payload, requestModel) {
737
- return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode());
772
+ return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode(normalizedSessionId));
738
773
  }
739
774
  };
740
775
  }
@@ -767,6 +802,7 @@ function apply(ctx, config = {}) {
767
802
  const contextWindows = getContextWindows();
768
803
  const setActiveRaw = (next) => {
769
804
  cachedRaw = next;
805
+ lastRejectedRaw = void 0;
770
806
  rawRevision += 1;
771
807
  };
772
808
  const tryRestoreFromBackup = (extra, minVersion) => {
@@ -805,16 +841,20 @@ function apply(ctx, config = {}) {
805
841
  fileSignature = nextFileSig;
806
842
  try {
807
843
  const incomingRaw = parseRawModelsCache(readFileSync(cachePath, "utf8"));
808
- if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
809
- cachePath,
810
- keptVersion: cachedRaw.client_version ?? "0.163.0",
811
- rejectedVersion: incomingRaw.client_version ?? "unknown",
812
- incomingFetchedAt: incomingRaw.fetched_at ?? null
813
- });
814
- else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
844
+ if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) {
845
+ if (!isSameModelsCatalogPayload(lastRejectedRaw, incomingRaw)) {
846
+ lastRejectedRaw = incomingRaw;
847
+ appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
848
+ cachePath,
849
+ keptVersion: cachedRaw.client_version ?? "0.163.0",
850
+ rejectedVersion: incomingRaw.client_version ?? "unknown",
851
+ incomingFetchedAt: incomingRaw.fetched_at ?? null
852
+ });
853
+ }
854
+ } else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
815
855
  if (!isSameModelsCatalogPayload(cachedRaw, incomingRaw)) {
816
856
  setActiveRaw(incomingRaw);
817
- writeBackupCache(codexHome, incomingRaw);
857
+ writeBackupCache(codexHome, ctx, incomingRaw);
818
858
  appendPluginLog(codexHome, ctx, "INFO", "catalog.load", {
819
859
  cachePath,
820
860
  clientVersion: incomingRaw.client_version ?? "0.163.0",
@@ -902,11 +942,25 @@ function apply(ctx, config = {}) {
902
942
  requestBody: "buffered",
903
943
  fetch: async (request) => {
904
944
  if (request.method === "POST") {
905
- const body = await request.json().catch(() => null);
906
- if (typeof body?.fastMode === "boolean") setFastModeOverride(body.fastMode, "ui");
945
+ let body;
946
+ try {
947
+ body = await request.json();
948
+ } catch {
949
+ body = null;
950
+ }
951
+ const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
952
+ if (record === null || typeof record.fastMode !== "boolean" || record.sessionId !== void 0 && typeof record.sessionId !== "string") return new Response(JSON.stringify({
953
+ ok: false,
954
+ error: "Invalid request body: boolean fastMode is required"
955
+ }), {
956
+ status: 400,
957
+ headers: { "content-type": "application/json" }
958
+ });
959
+ setSessionFastMode(record.sessionId, record.fastMode);
907
960
  return new Response(JSON.stringify({
908
961
  ok: true,
909
- fastMode: getEffectiveFastMode()
962
+ defaultFastMode: getConfigFastMode(),
963
+ sessionFastModes: Object.fromEntries(sessionFastModes)
910
964
  }), {
911
965
  status: 200,
912
966
  headers: { "content-type": "application/json" }
@@ -916,7 +970,8 @@ function apply(ctx, config = {}) {
916
970
  const result = await usageFetcher.getUsage(force);
917
971
  return new Response(JSON.stringify({
918
972
  ...result,
919
- fastMode: getEffectiveFastMode()
973
+ defaultFastMode: getConfigFastMode(),
974
+ sessionFastModes: Object.fromEntries(sessionFastModes)
920
975
  }), {
921
976
  status: 200,
922
977
  headers: { "content-type": "application/json" }
@@ -929,11 +984,6 @@ function apply(ctx, config = {}) {
929
984
  if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
930
985
  else mountUsageRoute(ctx);
931
986
  ctx.on?.("loader/volatile-update", () => {
932
- const currentConfigFastMode = getConfigFastMode();
933
- if (currentConfigFastMode !== lastConfigFastMode) {
934
- lastConfigFastMode = currentConfigFastMode;
935
- setFastModeOverride(currentConfigFastMode, "config");
936
- }
937
987
  ensureSnapshot();
938
988
  ctx.llm.emitAdaptersUpdated?.();
939
989
  });
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.9",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",