@lyogvce/dsh-codex-subscription 0.2.1 → 0.2.3
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 +2 -2
- package/lib/client.js +46 -7
- package/lib/index.d.ts +5 -1
- package/lib/index.js +221 -205
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
- 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
|
|
18
18
|
- 通过 `contextWindows` 按模型 `slug` 自定义向 DSH 会话与自动压缩器暴露的预算窗口(例如设为 `400000`);未配置时默认使用模型物理上限 `max_context_window`(`872000`)。同时底层 `PiCodexModel.contextWindow` 始终保持物理上限 `872000`,彻底防止 `dsh-llm-pi-ai` 在预算窗口与物理上限之间误判 `CONTEXT_WINDOW_EXCEEDED`。
|
|
19
19
|
4. **原生多轮思考链回放与工具调用**
|
|
20
|
-
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd`
|
|
20
|
+
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化纯 UUID `session-id` / `x-client-request-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
|
|
21
21
|
5. **Codex 订阅限额内联展示(`conversation.input.right`)**
|
|
22
|
-
- 当会话选中 `openai-codex`
|
|
22
|
+
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看对应窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
package/lib/client.js
CHANGED
|
@@ -25,6 +25,8 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/client/quota.ts
|
|
27
27
|
const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
|
|
28
|
+
const DEFAULT_PRIMARY_WINDOW_SECONDS = 18e3;
|
|
29
|
+
const DEFAULT_SECONDARY_WINDOW_SECONDS = 604800;
|
|
28
30
|
function resolveActiveProvider(directorySnapshot, projectedSelection) {
|
|
29
31
|
const dir = directorySnapshot && typeof directorySnapshot === "object" ? directorySnapshot : void 0;
|
|
30
32
|
const proj = projectedSelection && typeof projectedSelection === "object" ? projectedSelection : void 0;
|
|
@@ -46,14 +48,53 @@ window.__ModuleLoader__.load({
|
|
|
46
48
|
if (usedPercent >= 80) return "warn";
|
|
47
49
|
return "ok";
|
|
48
50
|
}
|
|
51
|
+
function resolveWindowSpan(windowSeconds, fallbackSeconds) {
|
|
52
|
+
const sec = typeof windowSeconds === "number" && Number.isFinite(windowSeconds) && windowSeconds > 0 ? windowSeconds : fallbackSeconds;
|
|
53
|
+
if (sec >= 86400) return {
|
|
54
|
+
value: Math.round(sec / 86400),
|
|
55
|
+
unit: "d"
|
|
56
|
+
};
|
|
57
|
+
if (sec >= 3600) return {
|
|
58
|
+
value: Math.round(sec / 3600),
|
|
59
|
+
unit: "h"
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
value: Math.max(1, Math.round(sec / 60)),
|
|
63
|
+
unit: "m"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const WINDOW_UNIT_LABELS = {
|
|
67
|
+
zh: {
|
|
68
|
+
d: "天窗口",
|
|
69
|
+
h: "小时窗口",
|
|
70
|
+
m: "分钟窗口"
|
|
71
|
+
},
|
|
72
|
+
en: {
|
|
73
|
+
d: "-day window",
|
|
74
|
+
h: "-hour window",
|
|
75
|
+
m: "-minute window"
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
function formatWindowShortLabel(windowSeconds, fallbackSeconds = DEFAULT_PRIMARY_WINDOW_SECONDS) {
|
|
79
|
+
const { value, unit } = resolveWindowSpan(windowSeconds, fallbackSeconds);
|
|
80
|
+
return `${value}${unit}`;
|
|
81
|
+
}
|
|
82
|
+
function formatWindowTitle(windowSeconds, locale, fallbackSeconds = DEFAULT_PRIMARY_WINDOW_SECONDS) {
|
|
83
|
+
const { value, unit } = resolveWindowSpan(windowSeconds, fallbackSeconds);
|
|
84
|
+
const suffix = WINDOW_UNIT_LABELS[locale][unit];
|
|
85
|
+
return locale === "en" ? `${value}${suffix}` : `${value} ${suffix}`;
|
|
86
|
+
}
|
|
49
87
|
function formatQuotaSummary(snapshot) {
|
|
50
88
|
const primaryPercent = clampPercent(snapshot.primary.usedPercent);
|
|
51
89
|
const secondaryPercent = snapshot.secondary !== null ? clampPercent(snapshot.secondary.usedPercent) : null;
|
|
90
|
+
const level = windowLevelOf(secondaryPercent !== null ? Math.max(primaryPercent, secondaryPercent) : primaryPercent, snapshot.limitReached);
|
|
91
|
+
const primaryLabel = formatWindowShortLabel(snapshot.primary.windowSeconds, DEFAULT_PRIMARY_WINDOW_SECONDS);
|
|
92
|
+
const secondaryLabel = snapshot.secondary !== null ? formatWindowShortLabel(snapshot.secondary.windowSeconds, DEFAULT_SECONDARY_WINDOW_SECONDS) : null;
|
|
52
93
|
return {
|
|
53
94
|
primaryPercent,
|
|
54
95
|
secondaryPercent,
|
|
55
|
-
level
|
|
56
|
-
pillText: secondaryPercent !== null ?
|
|
96
|
+
level,
|
|
97
|
+
pillText: secondaryPercent !== null && secondaryLabel !== null ? `${primaryLabel} ${primaryPercent}% · ${secondaryLabel} ${secondaryPercent}%` : `${primaryLabel} ${primaryPercent}%`,
|
|
57
98
|
ringDash: `${(RING_CIRCUMFERENCE * primaryPercent / 100).toFixed(2)} ${RING_CIRCUMFERENCE.toFixed(2)}`
|
|
58
99
|
};
|
|
59
100
|
}
|
|
@@ -223,8 +264,6 @@ window.__ModuleLoader__.load({
|
|
|
223
264
|
"quota.loading": "正在获取 Codex 限额…",
|
|
224
265
|
"quota.refresh": "↻ 刷新",
|
|
225
266
|
"quota.refreshing": "↻ 刷新中…",
|
|
226
|
-
"quota.window5h": "5 小时窗口",
|
|
227
|
-
"quota.window7d": "7 天窗口",
|
|
228
267
|
"quota.usedPrefix": "已用 ",
|
|
229
268
|
"quota.remainPrefix": "剩余 "
|
|
230
269
|
};
|
|
@@ -233,8 +272,6 @@ window.__ModuleLoader__.load({
|
|
|
233
272
|
"quota.loading": "Loading Codex quota…",
|
|
234
273
|
"quota.refresh": "↻ Refresh",
|
|
235
274
|
"quota.refreshing": "↻ Refreshing…",
|
|
236
|
-
"quota.window5h": "5-hour window",
|
|
237
|
-
"quota.window7d": "7-day window",
|
|
238
275
|
"quota.usedPrefix": "Used ",
|
|
239
276
|
"quota.remainPrefix": "Remaining "
|
|
240
277
|
};
|
|
@@ -357,7 +394,7 @@ window.__ModuleLoader__.load({
|
|
|
357
394
|
onClick: () => {
|
|
358
395
|
quotaStore.refresh(true);
|
|
359
396
|
}
|
|
360
|
-
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(
|
|
397
|
+
}, 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, quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
|
|
361
398
|
}
|
|
362
399
|
const inject = ["slots", "locale"];
|
|
363
400
|
function apply(ctx) {
|
|
@@ -411,6 +448,8 @@ window.__ModuleLoader__.load({
|
|
|
411
448
|
isCodexProvider,
|
|
412
449
|
didTurnFinish,
|
|
413
450
|
windowLevelOf,
|
|
451
|
+
formatWindowShortLabel,
|
|
452
|
+
formatWindowTitle,
|
|
414
453
|
formatQuotaSummary,
|
|
415
454
|
formatResetCountdown,
|
|
416
455
|
createQuotaStore,
|
package/lib/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface JwtClaims {
|
|
|
18
18
|
}
|
|
19
19
|
interface AuthManagerOptions {
|
|
20
20
|
getCodexHome: () => string;
|
|
21
|
+
getClientVersion?: (() => string) | undefined;
|
|
21
22
|
fetchImpl?: typeof fetch | undefined;
|
|
22
23
|
now?: (() => number) | undefined;
|
|
23
24
|
}
|
|
@@ -72,13 +73,15 @@ declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, b
|
|
|
72
73
|
//#endregion
|
|
73
74
|
//#region src/request.d.ts
|
|
74
75
|
declare function buildCodexUserAgent(version?: string): string;
|
|
75
|
-
declare function
|
|
76
|
+
declare function normalizeCodexSessionId(sessionId: string | undefined): string | undefined;
|
|
77
|
+
declare function buildCodexHeaders(existingHeaders?: Record<string, string> | undefined, clientVersion?: string): Record<string, string>;
|
|
76
78
|
declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined): Record<string, unknown> | null | undefined;
|
|
77
79
|
//#endregion
|
|
78
80
|
//#region src/usage.d.ts
|
|
79
81
|
declare function safeErrorMessage(error: unknown): string;
|
|
80
82
|
interface CodexQuotaWindow {
|
|
81
83
|
readonly usedPercent: number;
|
|
84
|
+
readonly windowSeconds?: number | null | undefined;
|
|
82
85
|
readonly resetAt: number | null;
|
|
83
86
|
}
|
|
84
87
|
interface CodexQuotaSnapshot {
|
|
@@ -147,6 +150,7 @@ export declare const __test: Readonly<{
|
|
|
147
150
|
sanitizeCodexPayload: typeof sanitizeCodexPayload;
|
|
148
151
|
buildCodexHeaders: typeof buildCodexHeaders;
|
|
149
152
|
buildCodexUserAgent: typeof buildCodexUserAgent;
|
|
153
|
+
normalizeCodexSessionId: typeof normalizeCodexSessionId;
|
|
150
154
|
parseUsagePayload: typeof parseUsagePayload;
|
|
151
155
|
createUsageFetcher: typeof createUsageFetcher;
|
|
152
156
|
}>;
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,211 @@ 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/catalog.ts
|
|
12
|
+
const PROVIDER = "openai-codex";
|
|
13
|
+
const DISPLAY_NAME = "Codex";
|
|
14
|
+
const DEFAULT_CLIENT_VERSION = "0.163.0";
|
|
15
|
+
const DEFAULT_CONTEXT_WINDOW = 872e3;
|
|
16
|
+
const THINKING_LEVELS = [
|
|
17
|
+
"off",
|
|
18
|
+
"minimal",
|
|
19
|
+
"low",
|
|
20
|
+
"medium",
|
|
21
|
+
"high",
|
|
22
|
+
"xhigh",
|
|
23
|
+
"max"
|
|
24
|
+
];
|
|
25
|
+
const DEFAULT_DISABLED_MODELS = Object.freeze([
|
|
26
|
+
"gpt-6-sol",
|
|
27
|
+
"gpt-6-luna",
|
|
28
|
+
"gpt-5.6-sol",
|
|
29
|
+
"gpt-5.6-terra",
|
|
30
|
+
"gpt-5.6-luna"
|
|
31
|
+
]);
|
|
32
|
+
function buildThinkingLevelMap(supportedLevels) {
|
|
33
|
+
const efforts = new Set((Array.isArray(supportedLevels) ? supportedLevels : []).map((item) => {
|
|
34
|
+
if (typeof item === "string") return item;
|
|
35
|
+
if (item !== null && typeof item === "object" && "effort" in item) {
|
|
36
|
+
const effort = item.effort;
|
|
37
|
+
return typeof effort === "string" ? effort : "";
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
}).filter((value) => value.length > 0));
|
|
41
|
+
const map = {};
|
|
42
|
+
for (const level of THINKING_LEVELS) if (level === "off") map.off = efforts.has("none") ? "none" : null;
|
|
43
|
+
else if (efforts.has(level) || level === "max" && efforts.has("ultra")) map[level] = level;
|
|
44
|
+
else map[level] = null;
|
|
45
|
+
return map;
|
|
46
|
+
}
|
|
47
|
+
function normalizeDefaultReasoningLevel(rawLevel, thinkingLevelMap) {
|
|
48
|
+
if (typeof rawLevel !== "string" || rawLevel.trim() === "") return void 0;
|
|
49
|
+
const trimmed = rawLevel.trim();
|
|
50
|
+
const mapped = trimmed === "none" ? "off" : trimmed === "ultra" ? "max" : trimmed;
|
|
51
|
+
return thinkingLevelMap[mapped] ? mapped : void 0;
|
|
52
|
+
}
|
|
53
|
+
function isVerbosity(value) {
|
|
54
|
+
return value === "low" || value === "medium" || value === "high";
|
|
55
|
+
}
|
|
56
|
+
function isPositiveSafeInt(value) {
|
|
57
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
58
|
+
}
|
|
59
|
+
function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
|
|
60
|
+
const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
|
|
61
|
+
const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
|
|
62
|
+
const rawModels = Array.isArray(rawCache?.models) ? rawCache.models : [];
|
|
63
|
+
const templateModel = baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
|
|
64
|
+
const visibleEntries = rawModels.filter((entry) => Boolean(entry && typeof entry.slug === "string" && entry.slug.trim() !== "")).filter((entry) => entry.visibility === "list" && !disabledSet.has(entry.slug.trim())).sort((a, b) => {
|
|
65
|
+
return (typeof a.priority === "number" && Number.isFinite(a.priority) ? a.priority : 0) - (typeof b.priority === "number" && Number.isFinite(b.priority) ? b.priority : 0);
|
|
66
|
+
});
|
|
67
|
+
const models = [];
|
|
68
|
+
const metadataById = new Map();
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const entry of visibleEntries) {
|
|
71
|
+
const id = entry.slug.trim();
|
|
72
|
+
if (seen.has(id)) continue;
|
|
73
|
+
seen.add(id);
|
|
74
|
+
const base = baseModels.find((model) => model.id === id) ?? templateModel;
|
|
75
|
+
const maxContextWindow = isPositiveSafeInt(entry.max_context_window) ? entry.max_context_window : DEFAULT_CONTEXT_WINDOW;
|
|
76
|
+
const configuredOverride = contextWindows && typeof contextWindows === "object" ? contextWindows[id] : void 0;
|
|
77
|
+
const targetContextWindow = isPositiveSafeInt(configuredOverride) ? configuredOverride : maxContextWindow;
|
|
78
|
+
const wireContextWindow = Math.max(maxContextWindow, targetContextWindow);
|
|
79
|
+
const maxTokens = Math.min(128e3, Math.max(32768, Math.floor(targetContextWindow / 4)));
|
|
80
|
+
const inputs = Array.isArray(entry.input_modalities) ? entry.input_modalities.filter((m) => m === "text" || m === "image") : ["text", "image"];
|
|
81
|
+
const thinkingLevelMap = buildThinkingLevelMap(entry.supported_reasoning_levels);
|
|
82
|
+
const hasReasoning = Object.entries(thinkingLevelMap).some(([k, v]) => k !== "off" && v !== null);
|
|
83
|
+
const supportVerbosity = entry.support_verbosity !== false;
|
|
84
|
+
const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
|
|
85
|
+
const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
|
|
86
|
+
metadataById.set(id, {
|
|
87
|
+
contextWindow: targetContextWindow,
|
|
88
|
+
supportVerbosity,
|
|
89
|
+
defaultVerbosity,
|
|
90
|
+
defaultReasoningLevel
|
|
91
|
+
});
|
|
92
|
+
models.push({
|
|
93
|
+
...base ?? {},
|
|
94
|
+
id,
|
|
95
|
+
name: typeof entry.display_name === "string" && entry.display_name.trim() !== "" ? entry.display_name.trim() : id,
|
|
96
|
+
...typeof entry.description === "string" && entry.description.trim() !== "" ? { description: entry.description.trim() } : {},
|
|
97
|
+
api: "openai-codex-responses",
|
|
98
|
+
provider: PROVIDER,
|
|
99
|
+
baseUrl: "https://chatgpt.com/backend-api",
|
|
100
|
+
input: inputs.length > 0 ? inputs : ["text"],
|
|
101
|
+
contextWindow: wireContextWindow,
|
|
102
|
+
maxTokens,
|
|
103
|
+
reasoning: hasReasoning,
|
|
104
|
+
thinkingLevelMap,
|
|
105
|
+
cost: {
|
|
106
|
+
input: 0,
|
|
107
|
+
output: 0,
|
|
108
|
+
cacheRead: 0,
|
|
109
|
+
cacheWrite: 0
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
clientVersion,
|
|
115
|
+
models,
|
|
116
|
+
metadataById
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/request.ts
|
|
121
|
+
const FORBIDDEN_PAYLOAD_FIELDS = Object.freeze([
|
|
122
|
+
"max_output_tokens",
|
|
123
|
+
"max_completion_tokens",
|
|
124
|
+
"temperature",
|
|
125
|
+
"top_p",
|
|
126
|
+
"previous_response_id",
|
|
127
|
+
"user",
|
|
128
|
+
"safety_identifier",
|
|
129
|
+
"prompt_cache_options"
|
|
130
|
+
]);
|
|
131
|
+
let cachedOsLabel;
|
|
132
|
+
function detectOsLabel() {
|
|
133
|
+
if (cachedOsLabel !== void 0) return cachedOsLabel;
|
|
134
|
+
const sys = platform();
|
|
135
|
+
if (sys === "darwin") try {
|
|
136
|
+
const parts = execFileSync("sw_vers", ["-productVersion"], {
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
timeout: 2e3
|
|
139
|
+
}).trim().split(".");
|
|
140
|
+
while (parts.length < 3) parts.push("0");
|
|
141
|
+
cachedOsLabel = `Mac OS ${parts.slice(0, 3).join(".")}`;
|
|
142
|
+
return cachedOsLabel;
|
|
143
|
+
} catch {
|
|
144
|
+
cachedOsLabel = "Mac OS";
|
|
145
|
+
return cachedOsLabel;
|
|
146
|
+
}
|
|
147
|
+
if (sys === "win32") {
|
|
148
|
+
cachedOsLabel = `Windows ${release()}`;
|
|
149
|
+
return cachedOsLabel;
|
|
150
|
+
}
|
|
151
|
+
cachedOsLabel = `Linux ${release()}`;
|
|
152
|
+
return cachedOsLabel;
|
|
153
|
+
}
|
|
154
|
+
function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
|
|
155
|
+
const cpu = arch() === "arm64" ? "arm64" : arch() === "x64" ? "x86_64" : arch();
|
|
156
|
+
const term = platform() === "darwin" ? "Apple_Terminal/455" : platform() === "win32" ? "WindowsTerminal" : "xterm-256color";
|
|
157
|
+
return `codex_cli_rs/${version} (${detectOsLabel()}; ${cpu}) ${term}`;
|
|
158
|
+
}
|
|
159
|
+
function normalizeCodexSessionId(sessionId) {
|
|
160
|
+
if (typeof sessionId !== "string") return void 0;
|
|
161
|
+
const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
|
|
162
|
+
return stripped.length > 0 ? stripped : void 0;
|
|
163
|
+
}
|
|
164
|
+
function buildCodexHeaders(existingHeaders, clientVersion = DEFAULT_CLIENT_VERSION) {
|
|
165
|
+
const headers = {};
|
|
166
|
+
for (const [key, value] of Object.entries(existingHeaders ?? {})) {
|
|
167
|
+
if (key.toLowerCase() === "user-agent") continue;
|
|
168
|
+
headers[key] = value;
|
|
169
|
+
}
|
|
170
|
+
headers.originator = "codex_cli_rs";
|
|
171
|
+
headers.version = clientVersion;
|
|
172
|
+
headers["User-Agent"] = buildCodexUserAgent(clientVersion);
|
|
173
|
+
return headers;
|
|
174
|
+
}
|
|
175
|
+
function sanitizeCodexPayload(payload, modelMeta) {
|
|
176
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
177
|
+
const cleaned = { ...payload };
|
|
178
|
+
for (const field of FORBIDDEN_PAYLOAD_FIELDS) delete cleaned[field];
|
|
179
|
+
if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
|
|
180
|
+
const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
|
|
181
|
+
if (modelMeta?.supportVerbosity === false) {
|
|
182
|
+
if (currentText !== void 0) {
|
|
183
|
+
delete currentText.verbosity;
|
|
184
|
+
if (Object.keys(currentText).length === 0) delete cleaned.text;
|
|
185
|
+
else cleaned.text = currentText;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const verbosity = modelMeta?.defaultVerbosity ?? "low";
|
|
189
|
+
cleaned.text = {
|
|
190
|
+
...currentText ?? {},
|
|
191
|
+
verbosity
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (cleaned.reasoning === void 0 && modelMeta?.defaultReasoningLevel !== void 0 && modelMeta.defaultReasoningLevel !== "off") cleaned.reasoning = {
|
|
195
|
+
effort: modelMeta.defaultReasoningLevel,
|
|
196
|
+
summary: "auto"
|
|
197
|
+
};
|
|
198
|
+
if (cleaned.reasoning && typeof cleaned.reasoning === "object" && !Array.isArray(cleaned.reasoning)) {
|
|
199
|
+
const reasoningObj = cleaned.reasoning;
|
|
200
|
+
const effort = reasoningObj.effort === "ultra" ? "max" : reasoningObj.effort;
|
|
201
|
+
if (effort === "none") delete cleaned.include;
|
|
202
|
+
else {
|
|
203
|
+
cleaned.reasoning = {
|
|
204
|
+
...reasoningObj,
|
|
205
|
+
effort,
|
|
206
|
+
summary: reasoningObj.summary ?? "auto"
|
|
207
|
+
};
|
|
208
|
+
const include = Array.isArray(cleaned.include) ? [...cleaned.include] : [];
|
|
209
|
+
if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
|
|
210
|
+
cleaned.include = include;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return cleaned;
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
11
216
|
//#region src/usage.ts
|
|
12
217
|
function safeErrorMessage(error) {
|
|
13
218
|
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);
|
|
@@ -24,6 +229,8 @@ function parseWindow(raw, fetchedAtMs) {
|
|
|
24
229
|
const record = raw;
|
|
25
230
|
const usedRaw = record.used_percent;
|
|
26
231
|
if (typeof usedRaw !== "number" || !Number.isFinite(usedRaw)) return null;
|
|
232
|
+
const windowSecRaw = record.limit_window_seconds;
|
|
233
|
+
const windowSeconds = typeof windowSecRaw === "number" && Number.isFinite(windowSecRaw) && windowSecRaw > 0 ? Math.round(windowSecRaw) : null;
|
|
27
234
|
const resetAtRaw = record.reset_at;
|
|
28
235
|
const resetAfterRaw = record.reset_after_seconds;
|
|
29
236
|
let resetAt = null;
|
|
@@ -31,6 +238,7 @@ function parseWindow(raw, fetchedAtMs) {
|
|
|
31
238
|
else if (typeof resetAfterRaw === "number" && Number.isFinite(resetAfterRaw) && resetAfterRaw >= 0) resetAt = Math.floor(fetchedAtMs / 1e3) + Math.round(resetAfterRaw);
|
|
32
239
|
return {
|
|
33
240
|
usedPercent: clampPercent(usedRaw),
|
|
241
|
+
windowSeconds,
|
|
34
242
|
resetAt
|
|
35
243
|
};
|
|
36
244
|
}
|
|
@@ -215,7 +423,10 @@ function createAuthManager(options) {
|
|
|
215
423
|
if (typeof refreshTok !== "string" || refreshTok.trim() === "") throw new LlmError("Codex 凭证已过期且缺少 refresh_token,请重新运行 codex login", "AUTH_FAILED");
|
|
216
424
|
const response = await fetchFn(CODEX_TOKEN_URL, {
|
|
217
425
|
method: "POST",
|
|
218
|
-
headers: {
|
|
426
|
+
headers: buildCodexHeaders({
|
|
427
|
+
"Content-Type": "application/json",
|
|
428
|
+
Accept: "application/json"
|
|
429
|
+
}, options.getClientVersion?.()),
|
|
219
430
|
body: JSON.stringify({
|
|
220
431
|
client_id: CODEX_CLIENT_ID,
|
|
221
432
|
grant_type: "refresh_token",
|
|
@@ -278,207 +489,6 @@ function createAuthManager(options) {
|
|
|
278
489
|
};
|
|
279
490
|
}
|
|
280
491
|
//#endregion
|
|
281
|
-
//#region src/catalog.ts
|
|
282
|
-
const PROVIDER = "openai-codex";
|
|
283
|
-
const DISPLAY_NAME = "Codex";
|
|
284
|
-
const DEFAULT_CLIENT_VERSION = "0.163.0";
|
|
285
|
-
const DEFAULT_CONTEXT_WINDOW = 872e3;
|
|
286
|
-
const THINKING_LEVELS = [
|
|
287
|
-
"off",
|
|
288
|
-
"minimal",
|
|
289
|
-
"low",
|
|
290
|
-
"medium",
|
|
291
|
-
"high",
|
|
292
|
-
"xhigh",
|
|
293
|
-
"max"
|
|
294
|
-
];
|
|
295
|
-
const DEFAULT_DISABLED_MODELS = Object.freeze([
|
|
296
|
-
"gpt-6-sol",
|
|
297
|
-
"gpt-6-luna",
|
|
298
|
-
"gpt-5.6-sol",
|
|
299
|
-
"gpt-5.6-terra",
|
|
300
|
-
"gpt-5.6-luna"
|
|
301
|
-
]);
|
|
302
|
-
function buildThinkingLevelMap(supportedLevels) {
|
|
303
|
-
const efforts = new Set((Array.isArray(supportedLevels) ? supportedLevels : []).map((item) => {
|
|
304
|
-
if (typeof item === "string") return item;
|
|
305
|
-
if (item !== null && typeof item === "object" && "effort" in item) {
|
|
306
|
-
const effort = item.effort;
|
|
307
|
-
return typeof effort === "string" ? effort : "";
|
|
308
|
-
}
|
|
309
|
-
return "";
|
|
310
|
-
}).filter((value) => value.length > 0));
|
|
311
|
-
const map = {};
|
|
312
|
-
for (const level of THINKING_LEVELS) if (level === "off") map.off = efforts.has("none") ? "none" : null;
|
|
313
|
-
else if (efforts.has(level) || level === "max" && efforts.has("ultra")) map[level] = level;
|
|
314
|
-
else map[level] = null;
|
|
315
|
-
return map;
|
|
316
|
-
}
|
|
317
|
-
function normalizeDefaultReasoningLevel(rawLevel, thinkingLevelMap) {
|
|
318
|
-
if (typeof rawLevel !== "string" || rawLevel.trim() === "") return void 0;
|
|
319
|
-
const trimmed = rawLevel.trim();
|
|
320
|
-
const mapped = trimmed === "none" ? "off" : trimmed === "ultra" ? "max" : trimmed;
|
|
321
|
-
return thinkingLevelMap[mapped] ? mapped : void 0;
|
|
322
|
-
}
|
|
323
|
-
function isVerbosity(value) {
|
|
324
|
-
return value === "low" || value === "medium" || value === "high";
|
|
325
|
-
}
|
|
326
|
-
function isPositiveSafeInt(value) {
|
|
327
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
328
|
-
}
|
|
329
|
-
function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
|
|
330
|
-
const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
|
|
331
|
-
const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
|
|
332
|
-
const rawModels = Array.isArray(rawCache?.models) ? rawCache.models : [];
|
|
333
|
-
const templateModel = baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
|
|
334
|
-
const visibleEntries = rawModels.filter((entry) => Boolean(entry && typeof entry.slug === "string" && entry.slug.trim() !== "")).filter((entry) => entry.visibility === "list" && !disabledSet.has(entry.slug.trim())).sort((a, b) => {
|
|
335
|
-
return (typeof a.priority === "number" && Number.isFinite(a.priority) ? a.priority : 0) - (typeof b.priority === "number" && Number.isFinite(b.priority) ? b.priority : 0);
|
|
336
|
-
});
|
|
337
|
-
const models = [];
|
|
338
|
-
const metadataById = new Map();
|
|
339
|
-
const seen = new Set();
|
|
340
|
-
for (const entry of visibleEntries) {
|
|
341
|
-
const id = entry.slug.trim();
|
|
342
|
-
if (seen.has(id)) continue;
|
|
343
|
-
seen.add(id);
|
|
344
|
-
const base = baseModels.find((model) => model.id === id) ?? templateModel;
|
|
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)));
|
|
350
|
-
const inputs = Array.isArray(entry.input_modalities) ? entry.input_modalities.filter((m) => m === "text" || m === "image") : ["text", "image"];
|
|
351
|
-
const thinkingLevelMap = buildThinkingLevelMap(entry.supported_reasoning_levels);
|
|
352
|
-
const hasReasoning = Object.entries(thinkingLevelMap).some(([k, v]) => k !== "off" && v !== null);
|
|
353
|
-
const supportVerbosity = entry.support_verbosity !== false;
|
|
354
|
-
const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
|
|
355
|
-
const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
|
|
356
|
-
metadataById.set(id, {
|
|
357
|
-
contextWindow: targetContextWindow,
|
|
358
|
-
supportVerbosity,
|
|
359
|
-
defaultVerbosity,
|
|
360
|
-
defaultReasoningLevel
|
|
361
|
-
});
|
|
362
|
-
models.push({
|
|
363
|
-
...base ?? {},
|
|
364
|
-
id,
|
|
365
|
-
name: typeof entry.display_name === "string" && entry.display_name.trim() !== "" ? entry.display_name.trim() : id,
|
|
366
|
-
...typeof entry.description === "string" && entry.description.trim() !== "" ? { description: entry.description.trim() } : {},
|
|
367
|
-
api: "openai-codex-responses",
|
|
368
|
-
provider: PROVIDER,
|
|
369
|
-
baseUrl: "https://chatgpt.com/backend-api",
|
|
370
|
-
input: inputs.length > 0 ? inputs : ["text"],
|
|
371
|
-
contextWindow: wireContextWindow,
|
|
372
|
-
maxTokens,
|
|
373
|
-
reasoning: hasReasoning,
|
|
374
|
-
thinkingLevelMap,
|
|
375
|
-
cost: {
|
|
376
|
-
input: 0,
|
|
377
|
-
output: 0,
|
|
378
|
-
cacheRead: 0,
|
|
379
|
-
cacheWrite: 0
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
return {
|
|
384
|
-
clientVersion,
|
|
385
|
-
models,
|
|
386
|
-
metadataById
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
//#endregion
|
|
390
|
-
//#region src/request.ts
|
|
391
|
-
const FORBIDDEN_PAYLOAD_FIELDS = Object.freeze([
|
|
392
|
-
"max_output_tokens",
|
|
393
|
-
"max_completion_tokens",
|
|
394
|
-
"temperature",
|
|
395
|
-
"top_p",
|
|
396
|
-
"previous_response_id",
|
|
397
|
-
"user",
|
|
398
|
-
"safety_identifier",
|
|
399
|
-
"prompt_cache_options"
|
|
400
|
-
]);
|
|
401
|
-
let cachedOsLabel;
|
|
402
|
-
function detectOsLabel() {
|
|
403
|
-
if (cachedOsLabel !== void 0) return cachedOsLabel;
|
|
404
|
-
const sys = platform();
|
|
405
|
-
if (sys === "darwin") try {
|
|
406
|
-
const parts = execFileSync("sw_vers", ["-productVersion"], {
|
|
407
|
-
encoding: "utf8",
|
|
408
|
-
timeout: 2e3
|
|
409
|
-
}).trim().split(".");
|
|
410
|
-
while (parts.length < 3) parts.push("0");
|
|
411
|
-
cachedOsLabel = `Mac OS ${parts.slice(0, 3).join(".")}`;
|
|
412
|
-
return cachedOsLabel;
|
|
413
|
-
} catch {
|
|
414
|
-
cachedOsLabel = "Mac OS";
|
|
415
|
-
return cachedOsLabel;
|
|
416
|
-
}
|
|
417
|
-
if (sys === "win32") {
|
|
418
|
-
cachedOsLabel = `Windows ${release()}`;
|
|
419
|
-
return cachedOsLabel;
|
|
420
|
-
}
|
|
421
|
-
cachedOsLabel = `Linux ${release()}`;
|
|
422
|
-
return cachedOsLabel;
|
|
423
|
-
}
|
|
424
|
-
function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
|
|
425
|
-
const cpu = arch() === "arm64" ? "arm64" : arch() === "x64" ? "x86_64" : arch();
|
|
426
|
-
const term = platform() === "darwin" ? "Apple_Terminal/455" : platform() === "win32" ? "WindowsTerminal" : "xterm-256color";
|
|
427
|
-
return `codex_cli_rs/${version} (${detectOsLabel()}; ${cpu}) ${term}`;
|
|
428
|
-
}
|
|
429
|
-
function buildCodexHeaders(existingHeaders, clientVersion, sessionId) {
|
|
430
|
-
const headers = {};
|
|
431
|
-
for (const [key, value] of Object.entries(existingHeaders ?? {})) {
|
|
432
|
-
if (key.toLowerCase() === "user-agent") continue;
|
|
433
|
-
headers[key] = value;
|
|
434
|
-
}
|
|
435
|
-
headers.originator = "codex_cli_rs";
|
|
436
|
-
headers.version = clientVersion;
|
|
437
|
-
headers["User-Agent"] = buildCodexUserAgent(clientVersion);
|
|
438
|
-
if (typeof sessionId === "string" && sessionId.length > 0) headers["thread-id"] = sessionId;
|
|
439
|
-
return headers;
|
|
440
|
-
}
|
|
441
|
-
function sanitizeCodexPayload(payload, modelMeta) {
|
|
442
|
-
if (!payload || typeof payload !== "object") return payload;
|
|
443
|
-
const cleaned = { ...payload };
|
|
444
|
-
for (const field of FORBIDDEN_PAYLOAD_FIELDS) delete cleaned[field];
|
|
445
|
-
if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
|
|
446
|
-
const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
|
|
447
|
-
if (modelMeta?.supportVerbosity === false) {
|
|
448
|
-
if (currentText !== void 0) {
|
|
449
|
-
delete currentText.verbosity;
|
|
450
|
-
if (Object.keys(currentText).length === 0) delete cleaned.text;
|
|
451
|
-
else cleaned.text = currentText;
|
|
452
|
-
}
|
|
453
|
-
} else {
|
|
454
|
-
const verbosity = modelMeta?.defaultVerbosity ?? "low";
|
|
455
|
-
cleaned.text = {
|
|
456
|
-
...currentText ?? {},
|
|
457
|
-
verbosity
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
if (cleaned.reasoning === void 0 && modelMeta?.defaultReasoningLevel !== void 0 && modelMeta.defaultReasoningLevel !== "off") cleaned.reasoning = {
|
|
461
|
-
effort: modelMeta.defaultReasoningLevel,
|
|
462
|
-
summary: "auto"
|
|
463
|
-
};
|
|
464
|
-
if (cleaned.reasoning && typeof cleaned.reasoning === "object" && !Array.isArray(cleaned.reasoning)) {
|
|
465
|
-
const reasoningObj = cleaned.reasoning;
|
|
466
|
-
const effort = reasoningObj.effort === "ultra" ? "max" : reasoningObj.effort;
|
|
467
|
-
if (effort === "none") delete cleaned.include;
|
|
468
|
-
else {
|
|
469
|
-
cleaned.reasoning = {
|
|
470
|
-
...reasoningObj,
|
|
471
|
-
effort,
|
|
472
|
-
summary: reasoningObj.summary ?? "auto"
|
|
473
|
-
};
|
|
474
|
-
const include = Array.isArray(cleaned.include) ? [...cleaned.include] : [];
|
|
475
|
-
if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
|
|
476
|
-
cleaned.include = include;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return cleaned;
|
|
480
|
-
}
|
|
481
|
-
//#endregion
|
|
482
492
|
//#region src/index.ts
|
|
483
493
|
const name = "codex-subscription";
|
|
484
494
|
const inject = ["llm"];
|
|
@@ -526,7 +536,10 @@ function apply(ctx, config = {}) {
|
|
|
526
536
|
const getCodexHome = () => resolveCodexHome();
|
|
527
537
|
const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
|
|
528
538
|
const getContextWindows = () => readVolatile(config.contextWindows, {});
|
|
529
|
-
const authManager = createAuthManager({
|
|
539
|
+
const authManager = createAuthManager({
|
|
540
|
+
getCodexHome,
|
|
541
|
+
getClientVersion: () => ensureSnapshot().catalog.clientVersion
|
|
542
|
+
});
|
|
530
543
|
let fileSignature;
|
|
531
544
|
let cachedRaw;
|
|
532
545
|
let warnedMissingFile = false;
|
|
@@ -551,11 +564,13 @@ function apply(ctx, config = {}) {
|
|
|
551
564
|
function wrapOptions(model, options = {}) {
|
|
552
565
|
const { catalog } = ensureSnapshot();
|
|
553
566
|
const modelMeta = catalog.metadataById.get(model?.id);
|
|
554
|
-
const { temperature: _temperature, onPayload, ...rest } = options;
|
|
567
|
+
const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
|
|
568
|
+
const normalizedSessionId = normalizeCodexSessionId(sessionId);
|
|
555
569
|
return {
|
|
556
570
|
...rest,
|
|
571
|
+
...normalizedSessionId !== void 0 ? { sessionId: normalizedSessionId } : {},
|
|
557
572
|
transport: "sse",
|
|
558
|
-
headers: buildCodexHeaders(rest.headers, catalog.clientVersion
|
|
573
|
+
headers: buildCodexHeaders(rest.headers, catalog.clientVersion),
|
|
559
574
|
async onPayload(payload, requestModel) {
|
|
560
575
|
return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta);
|
|
561
576
|
}
|
|
@@ -704,6 +719,7 @@ const __test = Object.freeze({
|
|
|
704
719
|
sanitizeCodexPayload,
|
|
705
720
|
buildCodexHeaders,
|
|
706
721
|
buildCodexUserAgent,
|
|
722
|
+
normalizeCodexSessionId,
|
|
707
723
|
parseUsagePayload,
|
|
708
724
|
createUsageFetcher
|
|
709
725
|
});
|