@lyogvce/dsh-codex-subscription 0.0.0-stage → 0.1.0

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
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ # dsh-codex-subscription
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 为 **DeepSeek Harness 桌面版**(DSH Desktop `0.2.1+`)提供极简、零后台进程的 **Codex 订阅直连能力**。**自用**。
4
+
5
+ 直接读取本机 `~/.codex/auth.json` 与 `~/.codex/models_cache.json`,在进程内通过 SSE 直连调用 ChatGPT Codex 订阅模型,彻底替代外部网关进程(如 Magpie)。
6
+
7
+ ---
8
+
9
+ ## 核心特性
10
+
11
+ 1. **与本机 Codex CLI / Desktop 100% 共享登录态**
12
+ - 直接读取 `$CODEX_HOME/auth.json`(默认 `~/.codex/auth.json`)。
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` 与大小自动感知更新),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
16
+ 3. **支持按配置屏蔽不想看到的模型(`disabledModels`)**
17
+ - 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
18
+ 4. **原生多轮思考链回放与工具调用**
19
+ - 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、`session-id` / `thread-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
20
+
21
+ ---
22
+
23
+ ## 安装与配置
24
+
25
+ ### 1. 安装到本地 Profile
26
+
27
+ 在 DSH 中通过插件管理器安装本地目录 `/Users/lijiechengbj/projects/AI-agents/dsh-codex-subscription`,或执行:
28
+
29
+ ```bash
30
+ dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
31
+ ```
32
+
33
+ ### 2. 配置屏蔽模型(`disabledModels`)
34
+
35
+ 默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra`):
36
+
37
+ ```yaml
38
+ - id: codex-subscription
39
+ name: "@lyogvce/dsh-codex-subscription"
40
+ config:
41
+ disabledModels:
42
+ - gpt-6-sol
43
+ - gpt-6-luna
44
+ - gpt-5.6-sol
45
+ - gpt-5.6-terra
46
+ - gpt-5.6-luna
47
+ ```
48
+
49
+ 可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels`,实时生效无需重启。
50
+
51
+ ---
52
+
53
+ ## 开发与验证
54
+
55
+ ```bash
56
+ pnpm install
57
+ pnpm verify # 依次执行 tsc --noEmit、tsdown 构建、node --test 单测与产物卫生守卫、隔离脚本检查
58
+ ```
@@ -0,0 +1,10 @@
1
+ - insert:
2
+ - id: codex-subscription
3
+ name: "@lyogvce/dsh-codex-subscription"
4
+ config:
5
+ disabledModels:
6
+ - gpt-6-sol
7
+ - gpt-6-luna
8
+ - gpt-5.6-sol
9
+ - gpt-5.6-terra
10
+ - gpt-5.6-luna
package/icon.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <rect width="64" height="64" rx="16" fill="#0F172A"/>
3
+ <path d="M22 22L30 32L22 42" stroke="#38BDF8" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M34 42H44" stroke="#E2E8F0" stroke-width="4.5" stroke-linecap="round"/>
5
+ <circle cx="46" cy="20" r="4" fill="#34D399"/>
6
+ </svg>
package/lib/index.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { PiCodexModel } from "@earendil-works/pi-ai/providers/openai-codex";
2
+ //#region src/auth.d.ts
3
+ interface JwtClaims {
4
+ exp?: number;
5
+ [key: string]: unknown;
6
+ }
7
+ interface AuthManagerOptions {
8
+ getCodexHome: () => string;
9
+ fetchImpl?: typeof fetch | undefined;
10
+ now?: (() => number) | undefined;
11
+ }
12
+ declare function resolveCodexHome(envCodexHome?: string | undefined): string;
13
+ declare function decodeJwtPayload(token: unknown): JwtClaims | undefined;
14
+ declare function createAuthManager(options: AuthManagerOptions): {
15
+ resolveAccessToken: () => Promise<string>;
16
+ };
17
+ //#endregion
18
+ //#region src/catalog.d.ts
19
+ interface RawReasoningLevel {
20
+ effort?: string;
21
+ description?: string;
22
+ }
23
+ interface RawCodexModelEntry {
24
+ slug?: string;
25
+ display_name?: string;
26
+ description?: string;
27
+ visibility?: string;
28
+ priority?: number;
29
+ context_window?: number;
30
+ max_context_window?: number;
31
+ input_modalities?: unknown[];
32
+ supported_reasoning_levels?: Array<string | RawReasoningLevel>;
33
+ default_reasoning_level?: string;
34
+ support_verbosity?: boolean;
35
+ default_verbosity?: string;
36
+ }
37
+ interface RawModelsCache {
38
+ client_version?: string;
39
+ models?: RawCodexModelEntry[];
40
+ }
41
+ interface CodexModelMetadata {
42
+ readonly supportVerbosity: boolean;
43
+ readonly defaultVerbosity: 'low' | 'medium' | 'high';
44
+ readonly defaultReasoningLevel?: string | undefined;
45
+ }
46
+ interface ParsedModelsCatalog {
47
+ readonly clientVersion: string;
48
+ readonly models: PiCodexModel[];
49
+ readonly metadataById: ReadonlyMap<string, CodexModelMetadata>;
50
+ }
51
+ declare function buildThinkingLevelMap(supportedLevels: unknown): Record<string, string | null>;
52
+ declare function parseModelsCache(rawCache: RawModelsCache | null | undefined, baseModels: readonly PiCodexModel[], disabledModels?: readonly string[]): ParsedModelsCatalog;
53
+ //#endregion
54
+ //#region src/request.d.ts
55
+ declare function buildCodexUserAgent(version?: string): string;
56
+ declare function buildCodexHeaders(existingHeaders: Record<string, string> | undefined, clientVersion: string, sessionId?: string | undefined): Record<string, string>;
57
+ declare function sanitizeCodexPayload(payload: Record<string, unknown> | null | undefined, modelMeta?: CodexModelMetadata | undefined): Record<string, unknown> | null | undefined;
58
+ //#endregion
59
+ //#region src/host-types.d.ts
60
+ interface LlmServiceLike {
61
+ registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
62
+ replace?: (next: string[]) => void;
63
+ };
64
+ emitAdaptersUpdated?: () => void;
65
+ }
66
+ interface PluginContextLike {
67
+ llm: LlmServiceLike;
68
+ get?: (name: string) => unknown;
69
+ on?: (event: string, listener: () => void) => () => void;
70
+ }
71
+ //#endregion
72
+ //#region src/index.d.ts
73
+ export declare const name = "codex-subscription";
74
+ export declare const inject: string[];
75
+ export declare const Config: any;
76
+ export declare function apply(ctx: PluginContextLike, config?: Record<string, unknown>): void;
77
+ export declare const __test: Readonly<{
78
+ PROVIDER: "openai-codex";
79
+ DISPLAY_NAME: "Codex";
80
+ DEFAULT_DISABLED_MODELS: readonly string[];
81
+ resolveCodexHome: typeof resolveCodexHome;
82
+ decodeJwtPayload: typeof decodeJwtPayload;
83
+ createAuthManager: typeof createAuthManager;
84
+ buildThinkingLevelMap: typeof buildThinkingLevelMap;
85
+ parseModelsCache: typeof parseModelsCache;
86
+ sanitizeCodexPayload: typeof sanitizeCodexPayload;
87
+ buildCodexHeaders: typeof buildCodexHeaders;
88
+ buildCodexUserAgent: typeof buildCodexUserAgent;
89
+ }>;
90
+ //#endregion
91
+ export type { CodexModelMetadata, ParsedModelsCatalog, PluginContextLike, RawModelsCache };
package/lib/index.js ADDED
@@ -0,0 +1,502 @@
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { LlmError, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
5
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
6
+ import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
7
+ import { randomUUID } from "node:crypto";
8
+ import { chmod, rename, writeFile } from "node:fs/promises";
9
+ import { arch, homedir, platform, release } from "node:os";
10
+ import { execFileSync } from "node:child_process";
11
+ //#region src/auth.ts
12
+ const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
13
+ const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
14
+ const TOKEN_REFRESH_LEAD_MS = 3e5;
15
+ const TOKEN_REFRESH_TIMEOUT_MS = 3e4;
16
+ function resolveCodexHome(envCodexHome = process.env.CODEX_HOME) {
17
+ const raw = typeof envCodexHome === "string" && envCodexHome.trim() !== "" ? envCodexHome.trim() : join(homedir(), ".codex");
18
+ return raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : resolve(raw);
19
+ }
20
+ function decodeJwtPayload(token) {
21
+ if (typeof token !== "string") return void 0;
22
+ const parts = token.split(".");
23
+ const body = parts[1];
24
+ if (parts.length !== 3 || !body) return void 0;
25
+ try {
26
+ return JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
27
+ } catch {
28
+ return;
29
+ }
30
+ }
31
+ async function writeJsonAtomic(filePath, data) {
32
+ let targetPath = filePath;
33
+ try {
34
+ targetPath = realpathSync(filePath);
35
+ } catch {}
36
+ const tempPath = join(dirname(targetPath), `.auth.json.tmp-${process.pid}-${randomUUID()}`);
37
+ const content = `${JSON.stringify(data, null, 2)}\n`;
38
+ await writeFile(tempPath, content, {
39
+ encoding: "utf8",
40
+ mode: 384
41
+ });
42
+ await chmod(tempPath, 384).catch(() => {});
43
+ await rename(tempPath, targetPath);
44
+ }
45
+ function createAuthManager(options) {
46
+ const { getCodexHome } = options;
47
+ const fetchFn = options.fetchImpl ?? globalThis.fetch;
48
+ const nowFn = options.now ?? Date.now;
49
+ let inflightRefresh;
50
+ let memoryFallback;
51
+ function readAuthState() {
52
+ const authPath = join(getCodexHome(), "auth.json");
53
+ let rawText;
54
+ try {
55
+ rawText = readFileSync(authPath, "utf8");
56
+ } catch (error) {
57
+ if (memoryFallback?.authPath === authPath) return memoryFallback;
58
+ throw new LlmError(`无法读取 Codex 登录凭证 (${authPath}),请先在终端运行 codex login`, "MISSING_CREDENTIAL", { cause: error });
59
+ }
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(rawText);
63
+ } catch (error) {
64
+ if (memoryFallback?.authPath === authPath) return memoryFallback;
65
+ throw new LlmError(`Codex 凭证文件格式损坏 (${authPath})`, "INVALID_CREDENTIAL", { cause: error });
66
+ }
67
+ const accessToken = parsed?.tokens?.access_token;
68
+ if (typeof accessToken !== "string" || accessToken.trim() === "") {
69
+ if (memoryFallback?.authPath === authPath) return memoryFallback;
70
+ throw new LlmError(`Codex 未登录 (${authPath} 中无 access_token),请运行 codex login`, "MISSING_CREDENTIAL");
71
+ }
72
+ if (memoryFallback?.authPath === authPath && memoryFallback.accessToken !== accessToken && isTokenFresh(memoryFallback.accessToken)) return memoryFallback;
73
+ return {
74
+ authPath,
75
+ parsed,
76
+ accessToken
77
+ };
78
+ }
79
+ function isTokenFresh(accessToken) {
80
+ const exp = decodeJwtPayload(accessToken)?.exp;
81
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return true;
82
+ return exp * 1e3 - nowFn() > TOKEN_REFRESH_LEAD_MS;
83
+ }
84
+ async function refreshToken(authPath, rawAuth) {
85
+ const refreshTok = rawAuth?.tokens?.refresh_token;
86
+ if (typeof refreshTok !== "string" || refreshTok.trim() === "") throw new LlmError("Codex 凭证已过期且缺少 refresh_token,请重新运行 codex login", "AUTH_FAILED");
87
+ const response = await fetchFn(CODEX_TOKEN_URL, {
88
+ method: "POST",
89
+ headers: { "Content-Type": "application/json" },
90
+ body: JSON.stringify({
91
+ client_id: CODEX_CLIENT_ID,
92
+ grant_type: "refresh_token",
93
+ refresh_token: refreshTok,
94
+ scope: "openid profile email"
95
+ }),
96
+ signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS)
97
+ });
98
+ if (!response.ok) throw new LlmError(`Codex Token 刷新失败 (HTTP ${response.status}),请重新运行 codex login`, "AUTH_FAILED");
99
+ const fresh = await response.json();
100
+ if (typeof fresh?.access_token !== "string" || fresh.access_token.trim() === "") throw new LlmError("Codex Token 刷新响应缺少 access_token", "AUTH_FAILED");
101
+ const nextAuth = {
102
+ ...rawAuth,
103
+ tokens: {
104
+ ...rawAuth.tokens ?? {},
105
+ access_token: fresh.access_token,
106
+ ...typeof fresh.id_token === "string" && fresh.id_token !== "" ? { id_token: fresh.id_token } : {},
107
+ ...typeof fresh.refresh_token === "string" && fresh.refresh_token !== "" ? { refresh_token: fresh.refresh_token } : {}
108
+ },
109
+ last_refresh: new Date(nowFn()).toISOString()
110
+ };
111
+ memoryFallback = {
112
+ authPath,
113
+ parsed: nextAuth,
114
+ accessToken: fresh.access_token
115
+ };
116
+ try {
117
+ await writeJsonAtomic(authPath, nextAuth);
118
+ } catch (error) {
119
+ console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${error instanceof Error ? error.message : String(error)}`);
120
+ }
121
+ return fresh.access_token;
122
+ }
123
+ return { async resolveAccessToken() {
124
+ const current = readAuthState();
125
+ if (isTokenFresh(current.accessToken)) return current.accessToken;
126
+ if (!inflightRefresh) inflightRefresh = (async () => {
127
+ const latest = readAuthState();
128
+ if (isTokenFresh(latest.accessToken)) return latest.accessToken;
129
+ return refreshToken(latest.authPath, latest.parsed);
130
+ })().finally(() => {
131
+ inflightRefresh = void 0;
132
+ });
133
+ return inflightRefresh;
134
+ } };
135
+ }
136
+ //#endregion
137
+ //#region src/catalog.ts
138
+ const PROVIDER = "openai-codex";
139
+ const DISPLAY_NAME = "Codex";
140
+ const DEFAULT_CLIENT_VERSION = "0.163.0";
141
+ const THINKING_LEVELS = [
142
+ "off",
143
+ "minimal",
144
+ "low",
145
+ "medium",
146
+ "high",
147
+ "xhigh",
148
+ "max"
149
+ ];
150
+ const DEFAULT_DISABLED_MODELS = Object.freeze([
151
+ "gpt-6-sol",
152
+ "gpt-6-luna",
153
+ "gpt-5.6-sol",
154
+ "gpt-5.6-terra",
155
+ "gpt-5.6-luna"
156
+ ]);
157
+ function buildThinkingLevelMap(supportedLevels) {
158
+ const efforts = new Set((Array.isArray(supportedLevels) ? supportedLevels : []).map((item) => {
159
+ if (typeof item === "string") return item;
160
+ if (item !== null && typeof item === "object" && "effort" in item) {
161
+ const effort = item.effort;
162
+ return typeof effort === "string" ? effort : "";
163
+ }
164
+ return "";
165
+ }).filter((value) => value.length > 0));
166
+ const map = {};
167
+ for (const level of THINKING_LEVELS) if (level === "off") map.off = efforts.has("none") ? "none" : null;
168
+ else if (efforts.has(level) || level === "max" && efforts.has("ultra")) map[level] = level;
169
+ else map[level] = null;
170
+ return map;
171
+ }
172
+ function normalizeDefaultReasoningLevel(rawLevel, thinkingLevelMap) {
173
+ if (typeof rawLevel !== "string" || rawLevel.trim() === "") return void 0;
174
+ const trimmed = rawLevel.trim();
175
+ const mapped = trimmed === "none" ? "off" : trimmed === "ultra" ? "max" : trimmed;
176
+ return thinkingLevelMap[mapped] ? mapped : void 0;
177
+ }
178
+ function isVerbosity(value) {
179
+ return value === "low" || value === "medium" || value === "high";
180
+ }
181
+ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
182
+ const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
183
+ const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
184
+ const rawModels = Array.isArray(rawCache?.models) ? rawCache.models : [];
185
+ const templateModel = baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
186
+ 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) => {
187
+ return (typeof a.priority === "number" && Number.isFinite(a.priority) ? a.priority : 0) - (typeof b.priority === "number" && Number.isFinite(b.priority) ? b.priority : 0);
188
+ });
189
+ const models = [];
190
+ const metadataById = new Map();
191
+ const seen = new Set();
192
+ for (const entry of visibleEntries) {
193
+ const id = entry.slug.trim();
194
+ if (seen.has(id)) continue;
195
+ seen.add(id);
196
+ const base = baseModels.find((model) => model.id === id) ?? templateModel;
197
+ 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;
198
+ const maxTokens = Math.min(128e3, Math.max(32768, Math.floor(contextWindow / 4)));
199
+ const inputs = Array.isArray(entry.input_modalities) ? entry.input_modalities.filter((m) => m === "text" || m === "image") : ["text", "image"];
200
+ const thinkingLevelMap = buildThinkingLevelMap(entry.supported_reasoning_levels);
201
+ const hasReasoning = Object.entries(thinkingLevelMap).some(([k, v]) => k !== "off" && v !== null);
202
+ const supportVerbosity = entry.support_verbosity !== false;
203
+ const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
204
+ const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
205
+ metadataById.set(id, {
206
+ supportVerbosity,
207
+ defaultVerbosity,
208
+ defaultReasoningLevel
209
+ });
210
+ models.push({
211
+ ...base ?? {},
212
+ id,
213
+ name: typeof entry.display_name === "string" && entry.display_name.trim() !== "" ? entry.display_name.trim() : id,
214
+ ...typeof entry.description === "string" && entry.description.trim() !== "" ? { description: entry.description.trim() } : {},
215
+ api: "openai-codex-responses",
216
+ provider: PROVIDER,
217
+ baseUrl: "https://chatgpt.com/backend-api",
218
+ input: inputs.length > 0 ? inputs : ["text"],
219
+ contextWindow,
220
+ maxTokens,
221
+ reasoning: hasReasoning,
222
+ thinkingLevelMap,
223
+ cost: {
224
+ input: 0,
225
+ output: 0,
226
+ cacheRead: 0,
227
+ cacheWrite: 0
228
+ }
229
+ });
230
+ }
231
+ return {
232
+ clientVersion,
233
+ models,
234
+ metadataById
235
+ };
236
+ }
237
+ //#endregion
238
+ //#region src/request.ts
239
+ const FORBIDDEN_PAYLOAD_FIELDS = Object.freeze([
240
+ "max_output_tokens",
241
+ "max_completion_tokens",
242
+ "temperature",
243
+ "top_p",
244
+ "previous_response_id",
245
+ "user",
246
+ "safety_identifier",
247
+ "prompt_cache_options"
248
+ ]);
249
+ let cachedOsLabel;
250
+ function detectOsLabel() {
251
+ if (cachedOsLabel !== void 0) return cachedOsLabel;
252
+ const sys = platform();
253
+ if (sys === "darwin") try {
254
+ const parts = execFileSync("sw_vers", ["-productVersion"], {
255
+ encoding: "utf8",
256
+ timeout: 2e3
257
+ }).trim().split(".");
258
+ while (parts.length < 3) parts.push("0");
259
+ cachedOsLabel = `Mac OS ${parts.slice(0, 3).join(".")}`;
260
+ return cachedOsLabel;
261
+ } catch {
262
+ cachedOsLabel = "Mac OS";
263
+ return cachedOsLabel;
264
+ }
265
+ if (sys === "win32") {
266
+ cachedOsLabel = `Windows ${release()}`;
267
+ return cachedOsLabel;
268
+ }
269
+ cachedOsLabel = `Linux ${release()}`;
270
+ return cachedOsLabel;
271
+ }
272
+ function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
273
+ const cpu = arch() === "arm64" ? "arm64" : arch() === "x64" ? "x86_64" : arch();
274
+ const term = platform() === "darwin" ? "Apple_Terminal/455" : platform() === "win32" ? "WindowsTerminal" : "xterm-256color";
275
+ return `codex_cli_rs/${version} (${detectOsLabel()}; ${cpu}) ${term}`;
276
+ }
277
+ function buildCodexHeaders(existingHeaders, clientVersion, sessionId) {
278
+ const headers = {};
279
+ for (const [key, value] of Object.entries(existingHeaders ?? {})) {
280
+ if (key.toLowerCase() === "user-agent") continue;
281
+ headers[key] = value;
282
+ }
283
+ headers.originator = "codex_cli_rs";
284
+ headers.version = clientVersion;
285
+ headers["User-Agent"] = buildCodexUserAgent(clientVersion);
286
+ if (typeof sessionId === "string" && sessionId.length > 0) headers["thread-id"] = sessionId;
287
+ return headers;
288
+ }
289
+ function sanitizeCodexPayload(payload, modelMeta) {
290
+ if (!payload || typeof payload !== "object") return payload;
291
+ const cleaned = { ...payload };
292
+ for (const field of FORBIDDEN_PAYLOAD_FIELDS) delete cleaned[field];
293
+ if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
294
+ const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
295
+ if (modelMeta?.supportVerbosity === false) {
296
+ if (currentText !== void 0) {
297
+ delete currentText.verbosity;
298
+ if (Object.keys(currentText).length === 0) delete cleaned.text;
299
+ else cleaned.text = currentText;
300
+ }
301
+ } else {
302
+ const verbosity = modelMeta?.defaultVerbosity ?? "low";
303
+ cleaned.text = {
304
+ ...currentText ?? {},
305
+ verbosity
306
+ };
307
+ }
308
+ if (cleaned.reasoning === void 0 && modelMeta?.defaultReasoningLevel !== void 0 && modelMeta.defaultReasoningLevel !== "off") cleaned.reasoning = {
309
+ effort: modelMeta.defaultReasoningLevel,
310
+ summary: "auto"
311
+ };
312
+ if (cleaned.reasoning && typeof cleaned.reasoning === "object" && !Array.isArray(cleaned.reasoning)) {
313
+ const reasoningObj = cleaned.reasoning;
314
+ const effort = reasoningObj.effort === "ultra" ? "max" : reasoningObj.effort;
315
+ if (effort === "none") delete cleaned.include;
316
+ else {
317
+ cleaned.reasoning = {
318
+ ...reasoningObj,
319
+ effort,
320
+ summary: reasoningObj.summary ?? "auto"
321
+ };
322
+ const include = Array.isArray(cleaned.include) ? [...cleaned.include] : [];
323
+ if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
324
+ cleaned.include = include;
325
+ }
326
+ }
327
+ return cleaned;
328
+ }
329
+ //#endregion
330
+ //#region src/index.ts
331
+ const name = "codex-subscription";
332
+ const inject = ["llm"];
333
+ const STREAM_IDLE_TIMEOUT_MS = 3e5;
334
+ const MAX_REQUEST_IMAGE_BYTES = 20971520;
335
+ const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
336
+ const REQUEST_IMAGE_MAX_BYTES = 1048576;
337
+ const Config = z.object({ disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile() });
338
+ function readVolatile(field, fallback) {
339
+ const value = typeof field?.get === "function" ? field.get() : field;
340
+ return value === void 0 || value === null ? fallback : value;
341
+ }
342
+ var CodexPiAiAdapter = class extends PiAiAdapter {
343
+ #getModelMetadata;
344
+ constructor(options, getModelMetadata) {
345
+ super(options);
346
+ this.#getModelMetadata = getModelMetadata;
347
+ }
348
+ modelInfo(snapshot, provider, model) {
349
+ const info = super.modelInfo(snapshot, provider, model);
350
+ const defaultLevel = this.#getModelMetadata(model)?.defaultReasoningLevel;
351
+ if (defaultLevel !== void 0 && info.reasoning !== void 0 && info.reasoning.efforts.some((e) => e.id === defaultLevel)) return {
352
+ ...info,
353
+ reasoning: {
354
+ ...info.reasoning,
355
+ defaultEffort: defaultLevel
356
+ }
357
+ };
358
+ return info;
359
+ }
360
+ };
361
+ function apply(ctx, config = {}) {
362
+ const baseProvider = openaiCodexProvider();
363
+ const baseModels = baseProvider.getModels();
364
+ const getCodexHome = () => resolveCodexHome();
365
+ const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
366
+ const authManager = createAuthManager({ getCodexHome });
367
+ let fileSignature;
368
+ let cachedRaw;
369
+ let warnedMissingFile = false;
370
+ let snapshotSignature;
371
+ let currentCatalog = {
372
+ clientVersion: DEFAULT_CLIENT_VERSION,
373
+ models: [],
374
+ metadataById: new Map()
375
+ };
376
+ let currentProfilesMap;
377
+ const requestTokenAuth = Object.freeze({
378
+ name: "Codex CLI auth.json token",
379
+ async resolve({ credential }) {
380
+ const token = credential?.key;
381
+ if (typeof token !== "string" || token.length === 0) return void 0;
382
+ return {
383
+ auth: { apiKey: token },
384
+ source: "codex-auth-json"
385
+ };
386
+ }
387
+ });
388
+ function wrapOptions(model, options = {}) {
389
+ const { catalog } = ensureSnapshot();
390
+ const modelMeta = catalog.metadataById.get(model?.id);
391
+ const { temperature: _temperature, onPayload, ...rest } = options;
392
+ return {
393
+ ...rest,
394
+ transport: "sse",
395
+ headers: buildCodexHeaders(rest.headers, catalog.clientVersion, rest.sessionId),
396
+ async onPayload(payload, requestModel) {
397
+ return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta);
398
+ }
399
+ };
400
+ }
401
+ const piProvider = Object.freeze({
402
+ ...baseProvider,
403
+ id: PROVIDER,
404
+ name: DISPLAY_NAME,
405
+ auth: Object.freeze({
406
+ ...baseProvider.auth,
407
+ apiKey: requestTokenAuth
408
+ }),
409
+ getModels: () => ensureSnapshot().catalog.models,
410
+ stream: (model, context, options) => baseProvider.stream(model, context, wrapOptions(model, options)),
411
+ streamSimple: (model, context, options) => baseProvider.streamSimple(model, context, wrapOptions(model, options))
412
+ });
413
+ function ensureSnapshot() {
414
+ const cachePath = join(getCodexHome(), "models_cache.json");
415
+ const disabled = getDisabledModels();
416
+ try {
417
+ const stat = statSync(cachePath);
418
+ const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
419
+ if (nextFileSig !== fileSignature) {
420
+ cachedRaw = JSON.parse(readFileSync(cachePath, "utf8"));
421
+ fileSignature = nextFileSig;
422
+ warnedMissingFile = false;
423
+ }
424
+ } catch (error) {
425
+ if (!warnedMissingFile) {
426
+ warnedMissingFile = true;
427
+ console.error(`[dsh-codex-subscription] 读取 Codex models_cache.json 失败 (${cachePath}): ${error instanceof Error ? error.message : String(error)}`);
428
+ }
429
+ }
430
+ const nextSnapshotSig = JSON.stringify([fileSignature ?? "none", disabled]);
431
+ if (nextSnapshotSig !== snapshotSignature || currentProfilesMap === void 0) {
432
+ currentCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled) : {
433
+ clientVersion: DEFAULT_CLIENT_VERSION,
434
+ models: [],
435
+ metadataById: new Map()
436
+ };
437
+ currentProfilesMap = new Map([[PROVIDER, Object.freeze({
438
+ provider: PROVIDER,
439
+ displayName: DISPLAY_NAME,
440
+ piProvider,
441
+ configuredMaxTokens: new Map(),
442
+ modelErrors: new Map(),
443
+ maxRequestImageBytes: MAX_REQUEST_IMAGE_BYTES,
444
+ requestImagePixelBudget: REQUEST_IMAGE_PIXEL_BUDGET,
445
+ requestImageMaxBytes: REQUEST_IMAGE_MAX_BYTES,
446
+ streamIdleTimeoutMs: STREAM_IDLE_TIMEOUT_MS,
447
+ cacheRetention: "short",
448
+ transport: "sse"
449
+ })]]);
450
+ snapshotSignature = nextSnapshotSig;
451
+ }
452
+ return {
453
+ catalog: currentCatalog,
454
+ profiles: currentProfilesMap
455
+ };
456
+ }
457
+ const adapter = new CodexPiAiAdapter({
458
+ profiles: () => ensureSnapshot().profiles,
459
+ resolveApiKey: (_provider, _profile) => authManager.resolveAccessToken(),
460
+ auth: Object.freeze({
461
+ credentials: {
462
+ async read() {},
463
+ async list() {
464
+ return [];
465
+ },
466
+ async modify() {},
467
+ async delete() {}
468
+ },
469
+ authContext: {
470
+ async env() {},
471
+ async fileExists() {
472
+ return false;
473
+ }
474
+ }
475
+ }),
476
+ resolveAttachments: () => ctx.get?.("attachments"),
477
+ resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => (ctx.get?.("fs"))?.processPathFromHostPath(hostPath), ref),
478
+ onReplayDegrade: (provider, model, reason) => {
479
+ console.error(`[dsh-codex-subscription] replay state for "${provider}/${model}" degraded (${reason})`);
480
+ }
481
+ }, (modelId) => ensureSnapshot().catalog.metadataById.get(modelId));
482
+ ctx.llm.registerAdapter([PROVIDER], adapter);
483
+ ctx.on?.("loader/volatile-update", () => {
484
+ ensureSnapshot();
485
+ ctx.llm.emitAdaptersUpdated?.();
486
+ });
487
+ }
488
+ const __test = Object.freeze({
489
+ PROVIDER,
490
+ DISPLAY_NAME,
491
+ DEFAULT_DISABLED_MODELS,
492
+ resolveCodexHome,
493
+ decodeJwtPayload,
494
+ createAuthManager,
495
+ buildThinkingLevelMap,
496
+ parseModelsCache,
497
+ sanitizeCodexPayload,
498
+ buildCodexHeaders,
499
+ buildCodexUserAgent
500
+ });
501
+ //#endregion
502
+ export { Config, __test, apply, inject, name };
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Codex Subscription",
4
+ "description": "Reads local ~/.codex credentials and models_cache.json to call Codex subscription models directly over SSE."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Codex 订阅直连",
4
+ "description": "读取本机 ~/.codex 登录凭证与 models_cache.json,在 DSH 中通过 SSE 直连调用 Codex 订阅模型。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "icon": "./icon.svg",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ },
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json",
16
+ "./locale/*.json": "./locale/*.json",
17
+ "./icon": "./icon.svg"
18
+ },
19
+ "files": [
20
+ "lib",
21
+ "locale",
22
+ "icon.svg",
23
+ "cordis.patch.yml",
24
+ "README.md"
25
+ ],
26
+ "dsh": {
27
+ "bundle": {
28
+ "patch": "./cordis.patch.yml"
29
+ }
30
+ },
31
+ "peerDependencies": {
32
+ "@deepseek-ai/dsh-llm": "*",
33
+ "@deepseek-ai/dsh-llm-pi-ai": "*",
34
+ "@deepseek-ai/schemastery": "*",
35
+ "@earendil-works/pi-ai": "*"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.20.5",
39
+ "tsdown": "0.23.0",
40
+ "typescript": "5.9.3"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "dev": "tsdown --watch",
45
+ "typecheck": "tsc --noEmit",
46
+ "pretest": "pnpm build",
47
+ "test": "node --test tests/*.test.mjs",
48
+ "test:isolation": "./tools/test-isolation.sh",
49
+ "verify": "pnpm typecheck && pnpm test && pnpm test:isolation",
50
+ "prepack": "pnpm build",
51
+ "prepublishOnly": "pnpm verify"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "license": "MIT",
57
+ "keywords": [
58
+ "dsh",
59
+ "dsh-plugin",
60
+ "codex",
61
+ "openai-codex",
62
+ "subscription"
63
+ ]
64
+ }