@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 +8 -4
- package/lib/client.js +4 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.js +163 -118
- package/package.json +1 -1
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.
|
|
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:
|
|
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}): ${
|
|
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
|
|
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
|
|
222
|
-
const
|
|
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({
|
|
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
|
|
477
|
-
if (
|
|
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
|
-
|
|
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}): ${
|
|
601
|
+
console.error(`[dsh-codex-subscription] 读取 Codex models_cache.json 失败 (${cachePath}): ${safeErrorMessage(error)}`);
|
|
564
602
|
}
|
|
565
603
|
}
|
|
566
|
-
const nextSnapshotSig = JSON.stringify([
|
|
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,
|