@lyogvce/dsh-codex-subscription 0.2.11 → 0.2.13

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
@@ -11,16 +11,16 @@
11
11
  1. **与本机 Codex CLI / Desktop 100% 共享登录态**
12
12
  - 直接读取 `$CODEX_HOME/auth.json`(默认 `~/.codex/auth.json`)。
13
13
  - 每次请求前检查 `access_token` 的 JWT `exp`:距离过期不足 5 分钟时自动通过单飞锁(Single-flight)向 `https://auth.openai.com/oauth/token` 刷新,并通过 `realpathSync` + 临时文件 `rename` 原子写回 `auth.json`,保证与 Codex CLI 永不互踢下线。
14
- 2. **零网络轮询的本地模型目录发现、防降级保护与可观测性日志**
15
- - 直接读取 `~/.codex/models_cache.json`(每次按文件 `mtime` 与大小做微秒级 `stat` 校验,仅在文件变化时重读解析),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
14
+ 2. **零网络轮询的本地模型目录实时监听、防降级保护与可观测性日志**
15
+ - 直接读取并监听 `~/.codex/models_cache.json`(同时结合 `realpathSync` 目录级防抖 `fs.watch` 实时监听与微秒级 `stat` 校验,兼容软链接目录与临时文件 `rename` 原子替换),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`;当本地模型缓存新增或变更模型时,自动调用 `ctx.llm.emitAdaptersUpdated()` 通知 DSH 桌面端前端立即刷新模型下拉框,无需重启或刷新页面。
16
16
  - **防旧版客户端降级污染**:自动将高版本 `models_cache.json` 备份至 `~/.codex/cache/dsh-codex-subscription/models-cache-backup.json`;当外部旧版进程(如 `dsh-subagent-codex` 内置的 `0.153.4`)将 `models_cache.json` 降级覆写为不含新模型的旧缓存时,自动拦截降级并保留高版本模型目录。
17
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`)与原生重试策略(`retryPolicy`)**
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
  - 通过 `retryPolicy` 暴露 DSH 原生 `RetryPolicySchema`(支持 `normal` / `always` 模式、`maxRetries`、`retryableCodes` 与 `backoff`,并随 `registration.replace` 热更新生效)。未配置时保持宿主默认策略(`mode: "normal"`、`maxRetries: 5`、默认 5 类错误码 `EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT` 及 `500ms..10000ms` 退避)。**注意:`retryableCodes` 为完整覆盖集合而非追加列表**,显式配置时需同时列出默认错误码与新增错误码。
22
- 4. **原生多轮思考链回放与工具调用**
23
- - 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化纯 UUID `session-id` / `x-client-request-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
22
+ 4. **原生多轮思考链回放、工具调用与出站指纹净化**
23
+ - 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化 36 位 RFC 4122 UUID `session-id` 缓存头与 `prompt_cache_key`、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载),并通过自定义 `fetch` 与 `onPayload` 剥除 `OpenAI-Beta: responses=experimental`、静态 `x-client-request-id`、`msg_pi_*` 等非官方指纹。
24
24
  5. **Codex 订阅限额内联展示与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
25
25
  - 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看对应窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
26
26
  - **⚡ 会话树级 Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度,主会话与 Subagent / 智能体团队同开同关)**:每个主会话可在限额弹窗底部独立开关 Fast 模式(不同主会话互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),该主会话派生的所有 `subagent`、`subagent_fork`、`workflow` 子任务以及智能体团队成员(`spawn_teammate`)均自动绑定至同一主会话开关,实时联动、同开同关;状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后胶囊显示闪电标识 `⚡`,并在请求体中自动注入 `"service_tier": "priority"`。
@@ -59,7 +59,7 @@ dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
59
59
  retryPolicy:
60
60
  mode: normal
61
61
  maxRetries: 5
62
- retryableCodes: [ EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, QUOTA, ACCOUNT_QUOTA, AUTH, INVALID_REQUEST, PI_AI_ERROR, STREAM_CLOSED, MALFORMED_RESPONSE ]
62
+ retryableCodes: [ EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, STREAM_CLOSED ]
63
63
  ```
64
64
 
65
65
  可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels`、`contextWindows` 与 `retryPolicy`,实时热更新生效无需重启。
package/lib/index.d.ts CHANGED
@@ -59,6 +59,7 @@ interface CodexResolvedCredentials {
59
59
  declare function createAuthManager(options: AuthManagerOptions): {
60
60
  resolveCredentials: () => Promise<CodexResolvedCredentials>;
61
61
  resolveAccessToken: () => Promise<string>;
62
+ getLastKnownAccountId: () => string | undefined;
62
63
  };
63
64
  //#endregion
64
65
  //#region src/catalog.d.ts
@@ -114,8 +115,14 @@ declare function buildThinkingLevelMap(supportedLevels: unknown): Record<string,
114
115
  declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, baseModels: readonly PiCodexModel[], disabledModels?: readonly string[], contextWindows?: Readonly<Record<string, number>>): ParsedModelsCatalog;
115
116
  //#endregion
116
117
  //#region src/request.d.ts
118
+ declare function toCodexWireSessionId(sessionId: string | undefined): string | undefined;
117
119
  declare function buildCodexUserAgent(version?: string): string;
118
120
  declare function buildCodexHeaders(existingHeaders?: Record<string, string> | undefined, clientVersion?: string): Record<string, string>;
121
+ interface CodexResponsesFetchOptions {
122
+ readonly baseFetch?: typeof fetch | undefined;
123
+ readonly getAccountId?: (() => string | undefined) | undefined;
124
+ }
125
+ declare function createCodexResponsesFetch(options?: CodexResponsesFetchOptions): typeof fetch;
119
126
  declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined, fastMode?: boolean): Record<string, unknown> | null | undefined;
120
127
  //#endregion
121
128
  //#region src/usage.d.ts
@@ -152,6 +159,7 @@ interface UsageFetcherOptions {
152
159
  readonly fetchImpl?: typeof fetch | undefined;
153
160
  readonly now?: (() => number) | undefined;
154
161
  readonly ttlMs?: number | undefined;
162
+ readonly minForceIntervalMs?: number | undefined;
155
163
  }
156
164
  declare function parseUsagePayload(raw: unknown, fetchedAt: number): CodexQuotaSnapshot | null;
157
165
  declare function createUsageFetcher(options: UsageFetcherOptions): {
@@ -163,6 +171,8 @@ declare function createUsageFetcher(options: UsageFetcherOptions): {
163
171
  export declare const name = "codex-subscription";
164
172
  export declare const inject: string[];
165
173
  export declare const LOG_FILE_NAME = "plugin.log";
174
+ export declare const MODELS_CACHE_FILE_NAME = "models_cache.json";
175
+ export declare const MODELS_CACHE_WATCH_DEBOUNCE_MS = 100;
166
176
  export declare const Config: any;
167
177
  export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
168
178
  export declare const __test: Readonly<{
@@ -185,7 +195,9 @@ export declare const __test: Readonly<{
185
195
  sanitizeCodexPayload: typeof sanitizeCodexPayload;
186
196
  buildCodexHeaders: typeof buildCodexHeaders;
187
197
  buildCodexUserAgent: typeof buildCodexUserAgent;
198
+ createCodexResponsesFetch: typeof createCodexResponsesFetch;
188
199
  normalizeCodexSessionId: typeof normalizeCodexSessionId;
200
+ toCodexWireSessionId: typeof toCodexWireSessionId;
189
201
  parseUsagePayload: typeof parseUsagePayload;
190
202
  createUsageFetcher: typeof createUsageFetcher;
191
203
  }>;
package/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { appendFileSync, chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, watch, 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, RetryPolicySchema, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
5
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
6
6
  import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import { chmod, rename, writeFile } from "node:fs/promises";
9
9
  import { arch, homedir, platform, release } from "node:os";
10
10
  import { execFileSync } from "node:child_process";
@@ -155,8 +155,15 @@ function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWind
155
155
  defaultReasoningLevel,
156
156
  fastServiceTier
157
157
  });
158
+ const rawCompat = base?.compat;
159
+ let sanitizedCompat;
160
+ if (rawCompat && typeof rawCompat === "object" && !Array.isArray(rawCompat)) {
161
+ const { supportsAdditionalTools: _supportsAdditionalTools, supportsToolSearch: _supportsToolSearch, ...restCompat } = rawCompat;
162
+ sanitizedCompat = restCompat;
163
+ }
158
164
  models.push({
159
165
  ...base ?? {},
166
+ ...sanitizedCompat !== void 0 ? { compat: sanitizedCompat } : {},
160
167
  id,
161
168
  name: typeof entry.display_name === "string" && entry.display_name.trim() !== "" ? entry.display_name.trim() : id,
162
169
  ...typeof entry.description === "string" && entry.description.trim() !== "" ? { description: entry.description.trim() } : {},
@@ -184,6 +191,33 @@ function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWind
184
191
  }
185
192
  //#endregion
186
193
  //#region src/request.ts
194
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
195
+ const FORBIDDEN_HEADER_NAMES = new Set([
196
+ "user-agent",
197
+ "openai-beta",
198
+ "version",
199
+ "x-client-request-id",
200
+ "http-referer",
201
+ "x-title"
202
+ ]);
203
+ function isForbiddenHeaderName(name) {
204
+ const lower = name.toLowerCase();
205
+ return FORBIDDEN_HEADER_NAMES.has(lower) || lower.startsWith("x-stainless-");
206
+ }
207
+ function formatDeterministicUuid(seed) {
208
+ const digest = createHash("sha256").update(seed).digest();
209
+ const bytes = Uint8Array.from(digest.subarray(0, 16));
210
+ bytes[6] = bytes[6] & 15 | 80;
211
+ bytes[8] = bytes[8] & 63 | 128;
212
+ const hex = Buffer.from(bytes).toString("hex");
213
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
214
+ }
215
+ function toCodexWireSessionId(sessionId) {
216
+ const normalized = normalizeCodexSessionId(sessionId);
217
+ if (normalized === void 0) return void 0;
218
+ if (UUID_REGEX.test(normalized)) return normalized.toLowerCase();
219
+ return formatDeterministicUuid(`codex-session:${normalized}`);
220
+ }
187
221
  const FORBIDDEN_PAYLOAD_FIELDS = Object.freeze([
188
222
  "max_output_tokens",
189
223
  "max_completion_tokens",
@@ -225,18 +259,62 @@ function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
225
259
  function buildCodexHeaders(existingHeaders, clientVersion = DEFAULT_CLIENT_VERSION) {
226
260
  const headers = {};
227
261
  for (const [key, value] of Object.entries(existingHeaders ?? {})) {
228
- if (key.toLowerCase() === "user-agent") continue;
262
+ if (isForbiddenHeaderName(key)) continue;
229
263
  headers[key] = value;
230
264
  }
231
265
  headers.originator = "codex_cli_rs";
232
- headers.version = clientVersion;
233
266
  headers["User-Agent"] = buildCodexUserAgent(clientVersion);
234
267
  return headers;
235
268
  }
269
+ function createCodexResponsesFetch(options = {}) {
270
+ const fetchFn = options.baseFetch ?? globalThis.fetch;
271
+ return async (input, init) => {
272
+ const headers = new Headers(init?.headers);
273
+ for (const key of [...headers.keys()]) {
274
+ if (key.toLowerCase() === "user-agent") continue;
275
+ if (isForbiddenHeaderName(key)) headers.delete(key);
276
+ }
277
+ const accountId = options.getAccountId?.();
278
+ if (typeof accountId === "string" && accountId.trim() !== "") headers.set("chatgpt-account-id", accountId.trim());
279
+ return fetchFn(input, {
280
+ ...init,
281
+ headers
282
+ });
283
+ };
284
+ }
285
+ function sanitizeInputItem(item) {
286
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
287
+ const record = item;
288
+ let patched;
289
+ if (typeof record.id === "string" && /^msg_pi_/iu.test(record.id)) patched = {
290
+ ...patched ?? record,
291
+ id: `msg_${createHash("sha256").update(record.id).digest("hex").slice(0, 48)}`
292
+ };
293
+ if (typeof record.call_id === "string" && /^pi_tool_load_/iu.test(record.call_id)) patched = {
294
+ ...patched ?? record,
295
+ call_id: `call_${createHash("sha256").update(record.call_id).digest("hex").slice(0, 24)}`
296
+ };
297
+ return patched ?? item;
298
+ }
236
299
  function sanitizeCodexPayload(payload, modelMeta, fastMode = false) {
237
300
  if (!payload || typeof payload !== "object") return payload;
238
301
  const cleaned = { ...payload };
239
302
  for (const field of FORBIDDEN_PAYLOAD_FIELDS) delete cleaned[field];
303
+ if (typeof cleaned.prompt_cache_key === "string") {
304
+ const wireKey = toCodexWireSessionId(cleaned.prompt_cache_key);
305
+ if (wireKey !== void 0) cleaned.prompt_cache_key = wireKey;
306
+ else delete cleaned.prompt_cache_key;
307
+ }
308
+ if (!Array.isArray(cleaned.tools) || cleaned.tools.length === 0) {
309
+ delete cleaned.tools;
310
+ delete cleaned.parallel_tool_calls;
311
+ delete cleaned.tool_choice;
312
+ }
313
+ if (Array.isArray(cleaned.input)) cleaned.input = cleaned.input.filter((item) => {
314
+ if (!item || typeof item !== "object" || Array.isArray(item)) return true;
315
+ const t = item.type;
316
+ return t !== "additional_tools" && t !== "tool_search_call" && t !== "tool_search_output";
317
+ }).map(sanitizeInputItem);
240
318
  if (fastMode) {
241
319
  const tier = modelMeta === void 0 ? "priority" : modelMeta.fastServiceTier;
242
320
  if (typeof tier === "string" && tier !== "") cleaned.service_tier = tier;
@@ -328,6 +406,7 @@ function createUsageFetcher(options) {
328
406
  const fetchFn = options.fetchImpl ?? globalThis.fetch;
329
407
  const nowFn = options.now ?? Date.now;
330
408
  const ttlMs = options.ttlMs ?? 6e4;
409
+ const minForceIntervalMs = options.minForceIntervalMs ?? 5e3;
331
410
  let cached;
332
411
  let stale = false;
333
412
  let inflight;
@@ -374,10 +453,17 @@ function createUsageFetcher(options) {
374
453
  stale = true;
375
454
  },
376
455
  async getUsage(force = false) {
377
- if (!force && !stale && cached !== void 0 && nowFn() - cached.fetchedAt < ttlMs) return {
378
- ok: true,
379
- quota: cached
380
- };
456
+ if (!stale && cached !== void 0) {
457
+ const ageMs = nowFn() - cached.fetchedAt;
458
+ if (!force && ageMs < ttlMs) return {
459
+ ok: true,
460
+ quota: cached
461
+ };
462
+ if (force && ageMs >= 0 && ageMs < minForceIntervalMs) return {
463
+ ok: true,
464
+ quota: cached
465
+ };
466
+ }
381
467
  if (!inflight) inflight = fetchFresh().finally(() => {
382
468
  inflight = void 0;
383
469
  });
@@ -462,6 +548,7 @@ function createAuthManager(options) {
462
548
  const fetchFn = options.fetchImpl ?? globalThis.fetch;
463
549
  const nowFn = options.now ?? Date.now;
464
550
  let inflightRefresh;
551
+ let lastKnownAccountId;
465
552
  let memoryFallback;
466
553
  function readAuthState() {
467
554
  const authPath = join(getCodexHome(), "auth.json");
@@ -525,8 +612,7 @@ function createAuthManager(options) {
525
612
  body: JSON.stringify({
526
613
  client_id: CODEX_CLIENT_ID,
527
614
  grant_type: "refresh_token",
528
- refresh_token: refreshTok,
529
- scope: "openid profile email"
615
+ refresh_token: refreshTok
530
616
  }),
531
617
  signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS)
532
618
  });
@@ -555,11 +641,13 @@ function createAuthManager(options) {
555
641
  parsed: nextAuth,
556
642
  accessToken: fresh.access_token
557
643
  };
644
+ const refreshedAccountId = extractAccountId(nextAuth, fresh.access_token);
645
+ lastKnownAccountId = refreshedAccountId;
558
646
  try {
559
647
  await writeJsonAtomic(authPath, nextAuth);
560
648
  options.onLog?.("INFO", "auth.refreshed", {
561
649
  authPath,
562
- accountId: extractAccountId(nextAuth, fresh.access_token) ?? null
650
+ accountId: refreshedAccountId ?? null
563
651
  });
564
652
  } catch (error) {
565
653
  const errMsg = safeErrorMessage(error);
@@ -571,13 +659,15 @@ function createAuthManager(options) {
571
659
  }
572
660
  return {
573
661
  accessToken: fresh.access_token,
574
- accountId: extractAccountId(nextAuth, fresh.access_token)
662
+ accountId: refreshedAccountId
575
663
  };
576
664
  }
577
665
  function toCredentials(state) {
666
+ const accountId = extractAccountId(state.parsed, state.accessToken);
667
+ lastKnownAccountId = accountId;
578
668
  return {
579
669
  accessToken: state.accessToken,
580
- accountId: extractAccountId(state.parsed, state.accessToken)
670
+ accountId
581
671
  };
582
672
  }
583
673
  async function resolveCredentials() {
@@ -596,6 +686,16 @@ function createAuthManager(options) {
596
686
  resolveCredentials,
597
687
  async resolveAccessToken() {
598
688
  return (await resolveCredentials()).accessToken;
689
+ },
690
+ getLastKnownAccountId() {
691
+ if (lastKnownAccountId !== void 0) return lastKnownAccountId;
692
+ try {
693
+ const current = readAuthState();
694
+ lastKnownAccountId = extractAccountId(current.parsed, current.accessToken);
695
+ return lastKnownAccountId;
696
+ } catch {
697
+ return;
698
+ }
599
699
  }
600
700
  };
601
701
  }
@@ -604,6 +704,8 @@ function createAuthManager(options) {
604
704
  const name = "codex-subscription";
605
705
  const inject = ["llm"];
606
706
  const LOG_FILE_NAME = "plugin.log";
707
+ const MODELS_CACHE_FILE_NAME = "models_cache.json";
708
+ const MODELS_CACHE_WATCH_DEBOUNCE_MS = 100;
607
709
  const STREAM_IDLE_TIMEOUT_MS = 3e5;
608
710
  const MAX_REQUEST_IMAGE_BYTES = 20971520;
609
711
  const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
@@ -611,6 +713,27 @@ const REQUEST_IMAGE_MAX_BYTES = 1048576;
611
713
  const MAX_LOG_FILE_BYTES = 524288;
612
714
  const MAX_PERSISTED_SESSIONS = 200;
613
715
  const MAX_PERSISTED_PARENTS = 1e3;
716
+ function resolveModelsCacheWatchTargets(codexHome) {
717
+ const dirs = new Set();
718
+ const fileNames = new Set([MODELS_CACHE_FILE_NAME]);
719
+ try {
720
+ dirs.add(realpathSync(codexHome));
721
+ } catch {
722
+ try {
723
+ if (statSync(codexHome).isDirectory()) dirs.add(codexHome);
724
+ } catch {}
725
+ }
726
+ const cachePath = join(codexHome, MODELS_CACHE_FILE_NAME);
727
+ try {
728
+ const realCachePath = realpathSync(cachePath);
729
+ dirs.add(dirname(realCachePath));
730
+ fileNames.add(basename(realCachePath));
731
+ } catch {}
732
+ return {
733
+ dirs: [...dirs],
734
+ fileNames
735
+ };
736
+ }
614
737
  function resolvePluginCacheDir(codexHome) {
615
738
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
616
739
  }
@@ -740,7 +863,8 @@ var CodexPiAiAdapter = class extends PiAiAdapter {
740
863
  function apply(ctx, config = {}) {
741
864
  const baseProvider = openaiCodexProvider();
742
865
  const baseModels = baseProvider.getModels();
743
- const getCodexHome = () => resolveCodexHome();
866
+ const boundCodexHome = resolveCodexHome();
867
+ const getCodexHome = () => boundCodexHome;
744
868
  const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
745
869
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
746
870
  const getContextWindows = () => readVolatile(config.contextWindows, {});
@@ -893,7 +1017,77 @@ function apply(ctx, config = {}) {
893
1017
  let rawRevision = 0;
894
1018
  let activeRetryPolicy = resolveConfiguredRetryPolicy();
895
1019
  let currentState;
1020
+ let adapterRegistered = false;
896
1021
  const catalogByProfiles = new WeakMap();
1022
+ const activeWatchers = new Map();
1023
+ let watchedFileNames = new Set([MODELS_CACHE_FILE_NAME]);
1024
+ let watchDebounceTimer;
1025
+ let watcherDisposed = false;
1026
+ const closeModelsCacheWatchers = () => {
1027
+ if (watchDebounceTimer !== void 0) {
1028
+ clearTimeout(watchDebounceTimer);
1029
+ watchDebounceTimer = void 0;
1030
+ }
1031
+ for (const watcher of activeWatchers.values()) try {
1032
+ watcher.close();
1033
+ } catch {}
1034
+ activeWatchers.clear();
1035
+ };
1036
+ const scheduleModelsCacheCheck = () => {
1037
+ if (watcherDisposed) return;
1038
+ if (watchDebounceTimer !== void 0) clearTimeout(watchDebounceTimer);
1039
+ watchDebounceTimer = setTimeout(() => {
1040
+ watchDebounceTimer = void 0;
1041
+ if (watcherDisposed) return;
1042
+ try {
1043
+ if (!statSync(getCodexHome()).isDirectory()) {
1044
+ watcherDisposed = true;
1045
+ closeModelsCacheWatchers();
1046
+ return;
1047
+ }
1048
+ } catch {
1049
+ watcherDisposed = true;
1050
+ closeModelsCacheWatchers();
1051
+ return;
1052
+ }
1053
+ syncModelsCacheWatchers();
1054
+ ensureSnapshot();
1055
+ }, 100);
1056
+ watchDebounceTimer.unref?.();
1057
+ };
1058
+ const syncModelsCacheWatchers = () => {
1059
+ if (watcherDisposed) return;
1060
+ const { dirs, fileNames } = resolveModelsCacheWatchTargets(getCodexHome());
1061
+ watchedFileNames = fileNames;
1062
+ const nextDirSet = new Set(dirs);
1063
+ for (const [dir, watcher] of activeWatchers.entries()) if (!nextDirSet.has(dir)) {
1064
+ try {
1065
+ watcher.close();
1066
+ } catch {}
1067
+ activeWatchers.delete(dir);
1068
+ }
1069
+ for (const dir of nextDirSet) {
1070
+ if (activeWatchers.has(dir)) continue;
1071
+ try {
1072
+ const watcher = watch(dir, { persistent: false }, (_eventType, rawFilename) => {
1073
+ if (watcherDisposed) return;
1074
+ if (rawFilename != null) {
1075
+ const name = typeof rawFilename === "string" ? rawFilename : String(rawFilename);
1076
+ if (name !== "" && !watchedFileNames.has(name)) return;
1077
+ }
1078
+ scheduleModelsCacheCheck();
1079
+ });
1080
+ watcher.on("error", () => {
1081
+ try {
1082
+ watcher.close();
1083
+ } catch {}
1084
+ activeWatchers.delete(dir);
1085
+ });
1086
+ watcher.unref?.();
1087
+ activeWatchers.set(dir, watcher);
1088
+ } catch {}
1089
+ }
1090
+ };
897
1091
  const requestTokenAuth = Object.freeze({
898
1092
  name: "Codex CLI auth.json token",
899
1093
  async resolve({ credential }) {
@@ -910,12 +1104,17 @@ function apply(ctx, config = {}) {
910
1104
  const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
911
1105
  const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
912
1106
  const normalizedSessionId = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
1107
+ const wireSessionId = toCodexWireSessionId(normalizedSessionId);
913
1108
  resolveRootSessionKey(normalizedSessionId);
914
1109
  return {
915
1110
  ...rest,
916
- ...normalizedSessionId !== void 0 ? { sessionId: normalizedSessionId } : {},
1111
+ ...wireSessionId !== void 0 ? { sessionId: wireSessionId } : {},
917
1112
  transport: "sse",
918
1113
  headers: buildCodexHeaders(rest.headers, catalog.clientVersion),
1114
+ fetch: createCodexResponsesFetch({
1115
+ baseFetch: rest.fetch,
1116
+ getAccountId: () => authManager.getLastKnownAccountId()
1117
+ }),
919
1118
  async onPayload(payload, requestModel) {
920
1119
  return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode(normalizedSessionId));
921
1120
  }
@@ -945,10 +1144,11 @@ function apply(ctx, config = {}) {
945
1144
  }
946
1145
  });
947
1146
  }
948
- function ensureSnapshot() {
1147
+ function ensureSnapshot(options) {
1148
+ const prevCatalogSig = currentState?.catalogSig;
949
1149
  const retryPolicy = activeRetryPolicy;
950
1150
  const codexHome = getCodexHome();
951
- const cachePath = join(codexHome, "models_cache.json");
1151
+ const cachePath = join(codexHome, MODELS_CACHE_FILE_NAME);
952
1152
  const disabled = getDisabledModels();
953
1153
  const contextWindows = getContextWindows();
954
1154
  const setActiveRaw = (next) => {
@@ -1067,6 +1267,11 @@ function apply(ctx, config = {}) {
1067
1267
  retryPolicySig
1068
1268
  });
1069
1269
  }
1270
+ if (adapterRegistered && !watcherDisposed) syncModelsCacheWatchers();
1271
+ if (adapterRegistered && options?.notifyAdaptersUpdated !== false && prevCatalogSig !== void 0 && nextCatalogSig !== prevCatalogSig) {
1272
+ committedState = currentState;
1273
+ ctx.llm.emitAdaptersUpdated?.();
1274
+ }
1070
1275
  return currentState;
1071
1276
  }
1072
1277
  const adapter = new CodexPiAiAdapter({
@@ -1097,8 +1302,24 @@ function apply(ctx, config = {}) {
1097
1302
  const snapProfiles = snapshot?.profiles;
1098
1303
  return ((snapProfiles !== void 0 ? catalogByProfiles.get(snapProfiles) : void 0) ?? ensureSnapshot().catalog).metadataById.get(modelId);
1099
1304
  });
1100
- let committedState = ensureSnapshot();
1305
+ let committedState = ensureSnapshot({ notifyAdaptersUpdated: false });
1101
1306
  const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
1307
+ adapterRegistered = true;
1308
+ if (typeof ctx.effect === "function") ctx.effect(() => {
1309
+ watcherDisposed = false;
1310
+ syncModelsCacheWatchers();
1311
+ return () => {
1312
+ watcherDisposed = true;
1313
+ closeModelsCacheWatchers();
1314
+ };
1315
+ }, "codex-subscription: models_cache watcher");
1316
+ else {
1317
+ syncModelsCacheWatchers();
1318
+ ctx.on?.("dispose", () => {
1319
+ watcherDisposed = true;
1320
+ closeModelsCacheWatchers();
1321
+ });
1322
+ }
1102
1323
  const mountUsageRoute = (connCtx) => {
1103
1324
  const connection = connCtx.get?.("connection");
1104
1325
  const registerFetch = connection?.fetch?.register;
@@ -1179,7 +1400,7 @@ function apply(ctx, config = {}) {
1179
1400
  activeRetryPolicy = candidateRetryPolicy;
1180
1401
  let nextState;
1181
1402
  try {
1182
- nextState = ensureSnapshot();
1403
+ nextState = ensureSnapshot({ notifyAdaptersUpdated: false });
1183
1404
  if (nextState.retryPolicySig !== prevCommitted.retryPolicySig) {
1184
1405
  registration.replace([PROVIDER]);
1185
1406
  committedState = nextState;
@@ -1214,9 +1435,11 @@ const __test = Object.freeze({
1214
1435
  sanitizeCodexPayload,
1215
1436
  buildCodexHeaders,
1216
1437
  buildCodexUserAgent,
1438
+ createCodexResponsesFetch,
1217
1439
  normalizeCodexSessionId,
1440
+ toCodexWireSessionId,
1218
1441
  parseUsagePayload,
1219
1442
  createUsageFetcher
1220
1443
  });
1221
1444
  //#endregion
1222
- export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, PLUGIN_CACHE_DIR_NAME, RUNTIME_STATE_FILE_NAME, __test, apply, inject, name };
1445
+ export { BACKUP_CACHE_FILE_NAME, Config, LOG_FILE_NAME, MODELS_CACHE_FILE_NAME, MODELS_CACHE_WATCH_DEBOUNCE_MS, 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.11",
3
+ "version": "0.2.13",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",