@lyogvce/dsh-codex-subscription 0.2.0 → 0.2.1

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
@@ -13,8 +13,9 @@
13
13
  - 每次请求前检查 `access_token` 的 JWT `exp`:距离过期不足 5 分钟时自动通过单飞锁(Single-flight)向 `https://auth.openai.com/oauth/token` 刷新,并通过 `realpathSync` + 临时文件 `rename` 原子写回 `auth.json`,保证与 Codex CLI 永不互踢下线。
14
14
  2. **零网络轮询的本地模型目录发现**
15
15
  - 直接读取 `~/.codex/models_cache.json`(按文件 `mtime` 与大小自动感知更新),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
16
- 3. **支持按配置屏蔽不想看到的模型(`disabledModels`)**
16
+ 3. **支持按配置屏蔽模型(`disabledModels`)与自定义上下文窗口(`contextWindows`)**
17
17
  - 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
18
+ - 通过 `contextWindows` 按模型 `slug` 自定义向 DSH 会话与自动压缩器暴露的预算窗口(例如设为 `400000`);未配置时默认使用模型物理上限 `max_context_window`(`872000`)。同时底层 `PiCodexModel.contextWindow` 始终保持物理上限 `872000`,彻底防止 `dsh-llm-pi-ai` 在预算窗口与物理上限之间误判 `CONTEXT_WINDOW_EXCEEDED`。
18
19
  4. **原生多轮思考链回放与工具调用**
19
20
  - 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、`session-id` / `thread-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
20
21
  5. **Codex 订阅限额内联展示(`conversation.input.right`)**
@@ -32,9 +33,9 @@
32
33
  dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
33
34
  ```
34
35
 
35
- ### 2. 配置屏蔽模型(`disabledModels`)
36
+ ### 2. 配置屏蔽模型(`disabledModels`)与上下文窗口(`contextWindows`)
36
37
 
37
- 默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra`):
38
+ 默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra`)。未配置 `contextWindows` 时,模型默认使用 `872000` 上下文窗口;若希望主动限制长上下文预算(例如统一设为 400K),可在 Profile 的 `cordis.patch.yml` 中配置:
38
39
 
39
40
  ```yaml
40
41
  - id: codex-subscription
@@ -46,9 +47,12 @@ dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
46
47
  - gpt-5.6-sol
47
48
  - gpt-5.6-terra
48
49
  - gpt-5.6-luna
50
+ contextWindows:
51
+ gpt-6.1-sol: 400000
52
+ gpt-6-astra: 400000
49
53
  ```
50
54
 
51
- 可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels`,实时生效无需重启。
55
+ 可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels` 与 `contextWindows`,实时生效无需重启。
52
56
 
53
57
  ---
54
58
 
package/lib/client.js CHANGED
@@ -15,6 +15,9 @@ window.__ModuleLoader__.load({
15
15
  ]);
16
16
  //#endregion
17
17
  //#region src/usage.ts
18
+ function safeErrorMessage(error) {
19
+ return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted]").replace(/\b(Bearer\s+)[^\s,;'"&]+/giu, "$1[redacted]").replace(/(\b(?:code|token|access_token|refresh_token|id_token)=)[^&\s"']+/giu, "$1[redacted]").replace(/("(?:access_token|refresh_token|id_token)"\s*:\s*")[^"]+"/giu, "$1[redacted]\"").slice(0, 1e3);
20
+ }
18
21
  const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
19
22
  function clampPercent(value) {
20
23
  return Math.max(0, Math.min(100, Math.round(value)));
@@ -153,7 +156,7 @@ window.__ModuleLoader__.load({
153
156
  setState({
154
157
  quota: state.quota,
155
158
  loading: false,
156
- error: error instanceof Error ? error.message : String(error)
159
+ error: safeErrorMessage(error)
157
160
  });
158
161
  } finally {
159
162
  if (seq === requestSeq) {
package/lib/index.d.ts CHANGED
@@ -57,6 +57,7 @@ interface RawModelsCache {
57
57
  models?: RawCodexModelEntry[];
58
58
  }
59
59
  interface CodexModelMetadata {
60
+ readonly contextWindow: number;
60
61
  readonly supportVerbosity: boolean;
61
62
  readonly defaultVerbosity: 'low' | 'medium' | 'high';
62
63
  readonly defaultReasoningLevel?: string | undefined;
@@ -67,7 +68,7 @@ interface ParsedModelsCatalog {
67
68
  readonly metadataById: ReadonlyMap<string, CodexModelMetadata>;
68
69
  }
69
70
  declare function buildThinkingLevelMap(supportedLevels: unknown): Record<string, string | null>;
70
- declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, baseModels: readonly PiCodexModel[], disabledModels?: readonly string[]): ParsedModelsCatalog;
71
+ declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, baseModels: readonly PiCodexModel[], disabledModels?: readonly string[], contextWindows?: Readonly<Record<string, number>>): ParsedModelsCatalog;
71
72
  //#endregion
72
73
  //#region src/request.d.ts
73
74
  declare function buildCodexUserAgent(version?: string): string;
@@ -75,6 +76,7 @@ declare function buildCodexHeaders(existingHeaders: Record<string, string> | und
75
76
  declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined): Record<string, unknown> | null | undefined;
76
77
  //#endregion
77
78
  //#region src/usage.d.ts
79
+ declare function safeErrorMessage(error: unknown): string;
78
80
  interface CodexQuotaWindow {
79
81
  readonly usedPercent: number;
80
82
  readonly resetAt: number | null;
@@ -131,12 +133,14 @@ export declare const __test: Readonly<{
131
133
  PROVIDER: "openai-codex";
132
134
  DISPLAY_NAME: "Codex";
133
135
  DEFAULT_DISABLED_MODELS: readonly string[];
136
+ DEFAULT_CONTEXT_WINDOW: 872000;
134
137
  CODEX_USAGE_URL: "https://chatgpt.com/backend-api/wham/usage";
135
138
  USAGE_ROUTE_PATH: "/api/codex-subscription/usage";
136
139
  DEFAULT_USAGE_TTL_MS: 60000;
137
140
  resolveCodexHome: typeof resolveCodexHome;
138
141
  decodeJwtPayload: typeof decodeJwtPayload;
139
142
  extractAccountId: typeof extractAccountId;
143
+ safeErrorMessage: typeof safeErrorMessage;
140
144
  createAuthManager: typeof createAuthManager;
141
145
  buildThinkingLevelMap: typeof buildThinkingLevelMap;
142
146
  parseModelsCache: typeof parseModelsCache;
package/lib/index.js CHANGED
@@ -1,13 +1,118 @@
1
- import { readFileSync, realpathSync, statSync } from "node:fs";
1
+ import { chmodSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
- import { LlmError, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
4
+ import { LlmError, 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
7
  import { 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";
11
+ //#region src/usage.ts
12
+ function safeErrorMessage(error) {
13
+ return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted]").replace(/\b(Bearer\s+)[^\s,;'"&]+/giu, "$1[redacted]").replace(/(\b(?:code|token|access_token|refresh_token|id_token)=)[^&\s"']+/giu, "$1[redacted]").replace(/("(?:access_token|refresh_token|id_token)"\s*:\s*")[^"]+"/giu, "$1[redacted]\"").slice(0, 1e3);
14
+ }
15
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
16
+ const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
17
+ const DEFAULT_USAGE_TTL_MS = 6e4;
18
+ const USAGE_FETCH_TIMEOUT_MS = 15e3;
19
+ function clampPercent(value) {
20
+ return Math.max(0, Math.min(100, Math.round(value)));
21
+ }
22
+ function parseWindow(raw, fetchedAtMs) {
23
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
24
+ const record = raw;
25
+ const usedRaw = record.used_percent;
26
+ if (typeof usedRaw !== "number" || !Number.isFinite(usedRaw)) return null;
27
+ const resetAtRaw = record.reset_at;
28
+ const resetAfterRaw = record.reset_after_seconds;
29
+ let resetAt = null;
30
+ if (typeof resetAtRaw === "number" && Number.isFinite(resetAtRaw) && resetAtRaw > 0) resetAt = Math.round(resetAtRaw);
31
+ else if (typeof resetAfterRaw === "number" && Number.isFinite(resetAfterRaw) && resetAfterRaw >= 0) resetAt = Math.floor(fetchedAtMs / 1e3) + Math.round(resetAfterRaw);
32
+ return {
33
+ usedPercent: clampPercent(usedRaw),
34
+ resetAt
35
+ };
36
+ }
37
+ function parseUsagePayload(raw, fetchedAt) {
38
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
39
+ const record = raw;
40
+ const rateLimit = record.rate_limit;
41
+ if (!rateLimit || typeof rateLimit !== "object" || Array.isArray(rateLimit)) return null;
42
+ const rateRecord = rateLimit;
43
+ const primary = parseWindow(rateRecord.primary_window, fetchedAt);
44
+ if (primary === null) return null;
45
+ const secondary = parseWindow(rateRecord.secondary_window, fetchedAt);
46
+ return {
47
+ planType: typeof record.plan_type === "string" && record.plan_type.trim() !== "" ? record.plan_type.trim() : "plus",
48
+ limitReached: rateRecord.limit_reached === true || rateRecord.allowed === false || primary.usedPercent >= 100 || secondary !== null && secondary.usedPercent >= 100,
49
+ primary,
50
+ secondary,
51
+ fetchedAt
52
+ };
53
+ }
54
+ function createUsageFetcher(options) {
55
+ const fetchFn = options.fetchImpl ?? globalThis.fetch;
56
+ const nowFn = options.now ?? Date.now;
57
+ const ttlMs = options.ttlMs ?? 6e4;
58
+ let cached;
59
+ let stale = false;
60
+ let inflight;
61
+ async function fetchFresh() {
62
+ try {
63
+ const creds = await options.resolveCredentials();
64
+ const headers = options.buildHeaders({
65
+ Authorization: `Bearer ${creds.accessToken}`,
66
+ Accept: "application/json",
67
+ ...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {}
68
+ });
69
+ const response = await fetchFn(CODEX_USAGE_URL, {
70
+ method: "GET",
71
+ headers,
72
+ signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
73
+ });
74
+ if (!response.ok) return {
75
+ ok: false,
76
+ error: `Codex 限额查询失败 (HTTP ${response.status})`,
77
+ ...cached !== void 0 ? { quota: cached } : {}
78
+ };
79
+ const parsed = parseUsagePayload(await response.json(), nowFn());
80
+ if (parsed === null) return {
81
+ ok: false,
82
+ error: "Codex 限额响应格式无法解析",
83
+ ...cached !== void 0 ? { quota: cached } : {}
84
+ };
85
+ cached = parsed;
86
+ stale = false;
87
+ return {
88
+ ok: true,
89
+ quota: parsed
90
+ };
91
+ } catch (error) {
92
+ return {
93
+ ok: false,
94
+ error: safeErrorMessage(error),
95
+ ...cached !== void 0 ? { quota: cached } : {}
96
+ };
97
+ }
98
+ }
99
+ return {
100
+ invalidate() {
101
+ stale = true;
102
+ },
103
+ async getUsage(force = false) {
104
+ if (!force && !stale && cached !== void 0 && nowFn() - cached.fetchedAt < ttlMs) return {
105
+ ok: true,
106
+ quota: cached
107
+ };
108
+ if (!inflight) inflight = fetchFresh().finally(() => {
109
+ inflight = void 0;
110
+ });
111
+ return inflight;
112
+ }
113
+ };
114
+ }
115
+ //#endregion
11
116
  //#region src/auth.ts
12
117
  const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
13
118
  const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -59,11 +164,26 @@ function createAuthManager(options) {
59
164
  let memoryFallback;
60
165
  function readAuthState() {
61
166
  const authPath = join(getCodexHome(), "auth.json");
167
+ if (process.platform !== "win32") try {
168
+ const stat = statSync(authPath);
169
+ if ((stat.mode & 256) === 0) {
170
+ if (memoryFallback?.authPath === authPath) return memoryFallback;
171
+ const octal = (stat.mode & 511).toString(8).padStart(4, "0");
172
+ throw new LlmError(`Codex 凭证文件属主不可读 (${authPath}, mode ${octal}),请执行 chmod 600 "${authPath}"`, "INVALID_CREDENTIAL");
173
+ }
174
+ if ((stat.mode & 63) !== 0) try {
175
+ chmodSync(realpathSync(authPath), 384);
176
+ } catch {}
177
+ } catch (error) {
178
+ if (error instanceof LlmError) throw error;
179
+ }
62
180
  let rawText;
63
181
  try {
64
182
  rawText = readFileSync(authPath, "utf8");
65
183
  } catch (error) {
66
184
  if (memoryFallback?.authPath === authPath) return memoryFallback;
185
+ const code = error?.code;
186
+ if (code === "EACCES" || code === "EPERM") throw new LlmError(`Codex 凭证文件无读取权限 (${authPath}),请执行 chmod 600 "${authPath}"`, "INVALID_CREDENTIAL", { cause: error });
67
187
  throw new LlmError(`无法读取 Codex 登录凭证 (${authPath}),请先在终端运行 codex login`, "MISSING_CREDENTIAL", { cause: error });
68
188
  }
69
189
  let parsed;
@@ -125,7 +245,7 @@ function createAuthManager(options) {
125
245
  try {
126
246
  await writeJsonAtomic(authPath, nextAuth);
127
247
  } catch (error) {
128
- console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${error instanceof Error ? error.message : String(error)}`);
248
+ console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${safeErrorMessage(error)}`);
129
249
  }
130
250
  return {
131
251
  accessToken: fresh.access_token,
@@ -162,6 +282,7 @@ function createAuthManager(options) {
162
282
  const PROVIDER = "openai-codex";
163
283
  const DISPLAY_NAME = "Codex";
164
284
  const DEFAULT_CLIENT_VERSION = "0.163.0";
285
+ const DEFAULT_CONTEXT_WINDOW = 872e3;
165
286
  const THINKING_LEVELS = [
166
287
  "off",
167
288
  "minimal",
@@ -202,7 +323,10 @@ function normalizeDefaultReasoningLevel(rawLevel, thinkingLevelMap) {
202
323
  function isVerbosity(value) {
203
324
  return value === "low" || value === "medium" || value === "high";
204
325
  }
205
- function parseModelsCache(rawCache, baseModels, disabledModels = []) {
326
+ function isPositiveSafeInt(value) {
327
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
328
+ }
329
+ function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
206
330
  const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
207
331
  const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
208
332
  const rawModels = Array.isArray(rawCache?.models) ? rawCache.models : [];
@@ -218,8 +342,11 @@ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
218
342
  if (seen.has(id)) continue;
219
343
  seen.add(id);
220
344
  const base = baseModels.find((model) => model.id === id) ?? templateModel;
221
- const contextWindow = typeof entry.context_window === "number" && Number.isSafeInteger(entry.context_window) && entry.context_window > 0 ? entry.context_window : typeof entry.max_context_window === "number" && Number.isSafeInteger(entry.max_context_window) && entry.max_context_window > 0 ? entry.max_context_window : base?.contextWindow ?? 272e3;
222
- const maxTokens = Math.min(128e3, Math.max(32768, Math.floor(contextWindow / 4)));
345
+ const maxContextWindow = isPositiveSafeInt(entry.max_context_window) ? entry.max_context_window : DEFAULT_CONTEXT_WINDOW;
346
+ const configuredOverride = contextWindows && typeof contextWindows === "object" ? contextWindows[id] : void 0;
347
+ const targetContextWindow = isPositiveSafeInt(configuredOverride) ? configuredOverride : maxContextWindow;
348
+ const wireContextWindow = Math.max(maxContextWindow, targetContextWindow);
349
+ const maxTokens = Math.min(128e3, Math.max(32768, Math.floor(targetContextWindow / 4)));
223
350
  const inputs = Array.isArray(entry.input_modalities) ? entry.input_modalities.filter((m) => m === "text" || m === "image") : ["text", "image"];
224
351
  const thinkingLevelMap = buildThinkingLevelMap(entry.supported_reasoning_levels);
225
352
  const hasReasoning = Object.entries(thinkingLevelMap).some(([k, v]) => k !== "off" && v !== null);
@@ -227,6 +354,7 @@ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
227
354
  const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
228
355
  const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
229
356
  metadataById.set(id, {
357
+ contextWindow: targetContextWindow,
230
358
  supportVerbosity,
231
359
  defaultVerbosity,
232
360
  defaultReasoningLevel
@@ -240,7 +368,7 @@ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
240
368
  provider: PROVIDER,
241
369
  baseUrl: "https://chatgpt.com/backend-api",
242
370
  input: inputs.length > 0 ? inputs : ["text"],
243
- contextWindow,
371
+ contextWindow: wireContextWindow,
244
372
  maxTokens,
245
373
  reasoning: hasReasoning,
246
374
  thinkingLevelMap,
@@ -351,108 +479,6 @@ function sanitizeCodexPayload(payload, modelMeta) {
351
479
  return cleaned;
352
480
  }
353
481
  //#endregion
354
- //#region src/usage.ts
355
- const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
356
- const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
357
- const DEFAULT_USAGE_TTL_MS = 6e4;
358
- const USAGE_FETCH_TIMEOUT_MS = 15e3;
359
- function clampPercent(value) {
360
- return Math.max(0, Math.min(100, Math.round(value)));
361
- }
362
- function parseWindow(raw, fetchedAtMs) {
363
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
364
- const record = raw;
365
- const usedRaw = record.used_percent;
366
- if (typeof usedRaw !== "number" || !Number.isFinite(usedRaw)) return null;
367
- const resetAtRaw = record.reset_at;
368
- const resetAfterRaw = record.reset_after_seconds;
369
- let resetAt = null;
370
- if (typeof resetAtRaw === "number" && Number.isFinite(resetAtRaw) && resetAtRaw > 0) resetAt = Math.round(resetAtRaw);
371
- else if (typeof resetAfterRaw === "number" && Number.isFinite(resetAfterRaw) && resetAfterRaw >= 0) resetAt = Math.floor(fetchedAtMs / 1e3) + Math.round(resetAfterRaw);
372
- return {
373
- usedPercent: clampPercent(usedRaw),
374
- resetAt
375
- };
376
- }
377
- function parseUsagePayload(raw, fetchedAt) {
378
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
379
- const record = raw;
380
- const rateLimit = record.rate_limit;
381
- if (!rateLimit || typeof rateLimit !== "object" || Array.isArray(rateLimit)) return null;
382
- const rateRecord = rateLimit;
383
- const primary = parseWindow(rateRecord.primary_window, fetchedAt);
384
- if (primary === null) return null;
385
- const secondary = parseWindow(rateRecord.secondary_window, fetchedAt);
386
- return {
387
- planType: typeof record.plan_type === "string" && record.plan_type.trim() !== "" ? record.plan_type.trim() : "plus",
388
- limitReached: rateRecord.limit_reached === true || rateRecord.allowed === false || primary.usedPercent >= 100 || secondary !== null && secondary.usedPercent >= 100,
389
- primary,
390
- secondary,
391
- fetchedAt
392
- };
393
- }
394
- function createUsageFetcher(options) {
395
- const fetchFn = options.fetchImpl ?? globalThis.fetch;
396
- const nowFn = options.now ?? Date.now;
397
- const ttlMs = options.ttlMs ?? 6e4;
398
- let cached;
399
- let stale = false;
400
- let inflight;
401
- async function fetchFresh() {
402
- try {
403
- const creds = await options.resolveCredentials();
404
- const headers = options.buildHeaders({
405
- Authorization: `Bearer ${creds.accessToken}`,
406
- Accept: "application/json",
407
- ...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {}
408
- });
409
- const response = await fetchFn(CODEX_USAGE_URL, {
410
- method: "GET",
411
- headers,
412
- signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
413
- });
414
- if (!response.ok) return {
415
- ok: false,
416
- error: `Codex 限额查询失败 (HTTP ${response.status})`,
417
- ...cached !== void 0 ? { quota: cached } : {}
418
- };
419
- const parsed = parseUsagePayload(await response.json(), nowFn());
420
- if (parsed === null) return {
421
- ok: false,
422
- error: "Codex 限额响应格式无法解析",
423
- ...cached !== void 0 ? { quota: cached } : {}
424
- };
425
- cached = parsed;
426
- stale = false;
427
- return {
428
- ok: true,
429
- quota: parsed
430
- };
431
- } catch (error) {
432
- return {
433
- ok: false,
434
- error: error instanceof Error ? error.message : String(error),
435
- ...cached !== void 0 ? { quota: cached } : {}
436
- };
437
- }
438
- }
439
- return {
440
- invalidate() {
441
- stale = true;
442
- },
443
- async getUsage(force = false) {
444
- if (!force && !stale && cached !== void 0 && nowFn() - cached.fetchedAt < ttlMs) return {
445
- ok: true,
446
- quota: cached
447
- };
448
- if (!inflight) inflight = fetchFresh().finally(() => {
449
- inflight = void 0;
450
- });
451
- return inflight;
452
- }
453
- };
454
- }
455
- //#endregion
456
482
  //#region src/index.ts
457
483
  const name = "codex-subscription";
458
484
  const inject = ["llm"];
@@ -460,7 +486,10 @@ const STREAM_IDLE_TIMEOUT_MS = 3e5;
460
486
  const MAX_REQUEST_IMAGE_BYTES = 20971520;
461
487
  const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
462
488
  const REQUEST_IMAGE_MAX_BYTES = 1048576;
463
- const Config = z.object({ disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile() });
489
+ const Config = z.object({
490
+ disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
491
+ contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
492
+ });
464
493
  function readVolatile(field, fallback) {
465
494
  const value = typeof field?.get === "function" ? field.get() : field;
466
495
  return value === void 0 || value === null ? fallback : value;
@@ -473,22 +502,30 @@ var CodexPiAiAdapter = class extends PiAiAdapter {
473
502
  }
474
503
  modelInfo(snapshot, provider, model) {
475
504
  const info = super.modelInfo(snapshot, provider, model);
476
- const defaultLevel = this.#getModelMetadata(model)?.defaultReasoningLevel;
477
- if (defaultLevel !== void 0 && info.reasoning !== void 0 && info.reasoning.efforts.some((e) => e.id === defaultLevel)) return {
505
+ const meta = this.#getModelMetadata(model);
506
+ if (meta === void 0) return info;
507
+ const defaultLevel = meta.defaultReasoningLevel;
508
+ const hasDefaultLevel = defaultLevel !== void 0 && info.reasoning !== void 0 && info.reasoning.efforts.some((e) => e.id === defaultLevel);
509
+ return {
478
510
  ...info,
479
- reasoning: {
511
+ context: {
512
+ ...info.context ?? {},
513
+ contextWindow: meta.contextWindow
514
+ },
515
+ ...hasDefaultLevel && info.reasoning !== void 0 ? { reasoning: {
480
516
  ...info.reasoning,
481
517
  defaultEffort: defaultLevel
482
- }
518
+ } } : {}
483
519
  };
484
- return info;
485
520
  }
486
521
  };
487
522
  function apply(ctx, config = {}) {
488
523
  const baseProvider = openaiCodexProvider();
489
524
  const baseModels = baseProvider.getModels();
525
+ const retryPolicy = resolveRetryPolicy(void 0, "codex-subscription retryPolicy");
490
526
  const getCodexHome = () => resolveCodexHome();
491
527
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
528
+ const getContextWindows = () => readVolatile(config.contextWindows, {});
492
529
  const authManager = createAuthManager({ getCodexHome });
493
530
  let fileSignature;
494
531
  let cachedRaw;
@@ -549,6 +586,7 @@ function apply(ctx, config = {}) {
549
586
  function ensureSnapshot() {
550
587
  const cachePath = join(getCodexHome(), "models_cache.json");
551
588
  const disabled = getDisabledModels();
589
+ const contextWindows = getContextWindows();
552
590
  try {
553
591
  const stat = statSync(cachePath);
554
592
  const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
@@ -560,12 +598,16 @@ function apply(ctx, config = {}) {
560
598
  } catch (error) {
561
599
  if (!warnedMissingFile) {
562
600
  warnedMissingFile = true;
563
- console.error(`[dsh-codex-subscription] 读取 Codex models_cache.json 失败 (${cachePath}): ${error instanceof Error ? error.message : String(error)}`);
601
+ console.error(`[dsh-codex-subscription] 读取 Codex models_cache.json 失败 (${cachePath}): ${safeErrorMessage(error)}`);
564
602
  }
565
603
  }
566
- const nextSnapshotSig = JSON.stringify([fileSignature ?? "none", disabled]);
604
+ const nextSnapshotSig = JSON.stringify([
605
+ fileSignature ?? "none",
606
+ disabled,
607
+ contextWindows
608
+ ]);
567
609
  if (nextSnapshotSig !== snapshotSignature || currentProfilesMap === void 0) {
568
- currentCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled) : {
610
+ currentCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled, contextWindows) : {
569
611
  clientVersion: DEFAULT_CLIENT_VERSION,
570
612
  models: [],
571
613
  metadataById: new Map()
@@ -574,6 +616,7 @@ function apply(ctx, config = {}) {
574
616
  provider: PROVIDER,
575
617
  displayName: DISPLAY_NAME,
576
618
  piProvider,
619
+ retryPolicy,
577
620
  configuredMaxTokens: new Map(),
578
621
  modelErrors: new Map(),
579
622
  maxRequestImageBytes: MAX_REQUEST_IMAGE_BYTES,
@@ -647,12 +690,14 @@ const __test = Object.freeze({
647
690
  PROVIDER,
648
691
  DISPLAY_NAME,
649
692
  DEFAULT_DISABLED_MODELS,
693
+ DEFAULT_CONTEXT_WINDOW,
650
694
  CODEX_USAGE_URL,
651
695
  USAGE_ROUTE_PATH,
652
696
  DEFAULT_USAGE_TTL_MS,
653
697
  resolveCodexHome,
654
698
  decodeJwtPayload,
655
699
  extractAccountId,
700
+ safeErrorMessage,
656
701
  createAuthManager,
657
702
  buildThinkingLevelMap,
658
703
  parseModelsCache,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",