@lyogvce/dsh-codex-subscription 0.0.0-stage → 0.2.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/lib/index.js ADDED
@@ -0,0 +1,666 @@
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
+ function extractAccountId(parsed, accessToken) {
32
+ const fileAccountId = parsed?.tokens?.account_id;
33
+ if (typeof fileAccountId === "string" && fileAccountId.trim() !== "") return fileAccountId.trim();
34
+ const authClaim = decodeJwtPayload(accessToken)?.["https://api.openai.com/auth"];
35
+ if (authClaim && typeof authClaim === "object" && !Array.isArray(authClaim)) {
36
+ const jwtAccountId = authClaim.chatgpt_account_id;
37
+ if (typeof jwtAccountId === "string" && jwtAccountId.trim() !== "") return jwtAccountId.trim();
38
+ }
39
+ }
40
+ async function writeJsonAtomic(filePath, data) {
41
+ let targetPath = filePath;
42
+ try {
43
+ targetPath = realpathSync(filePath);
44
+ } catch {}
45
+ const tempPath = join(dirname(targetPath), `.auth.json.tmp-${process.pid}-${randomUUID()}`);
46
+ const content = `${JSON.stringify(data, null, 2)}\n`;
47
+ await writeFile(tempPath, content, {
48
+ encoding: "utf8",
49
+ mode: 384
50
+ });
51
+ await chmod(tempPath, 384).catch(() => {});
52
+ await rename(tempPath, targetPath);
53
+ }
54
+ function createAuthManager(options) {
55
+ const { getCodexHome } = options;
56
+ const fetchFn = options.fetchImpl ?? globalThis.fetch;
57
+ const nowFn = options.now ?? Date.now;
58
+ let inflightRefresh;
59
+ let memoryFallback;
60
+ function readAuthState() {
61
+ const authPath = join(getCodexHome(), "auth.json");
62
+ let rawText;
63
+ try {
64
+ rawText = readFileSync(authPath, "utf8");
65
+ } catch (error) {
66
+ if (memoryFallback?.authPath === authPath) return memoryFallback;
67
+ throw new LlmError(`无法读取 Codex 登录凭证 (${authPath}),请先在终端运行 codex login`, "MISSING_CREDENTIAL", { cause: error });
68
+ }
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(rawText);
72
+ } catch (error) {
73
+ if (memoryFallback?.authPath === authPath) return memoryFallback;
74
+ throw new LlmError(`Codex 凭证文件格式损坏 (${authPath})`, "INVALID_CREDENTIAL", { cause: error });
75
+ }
76
+ const accessToken = parsed?.tokens?.access_token;
77
+ if (typeof accessToken !== "string" || accessToken.trim() === "") {
78
+ if (memoryFallback?.authPath === authPath) return memoryFallback;
79
+ throw new LlmError(`Codex 未登录 (${authPath} 中无 access_token),请运行 codex login`, "MISSING_CREDENTIAL");
80
+ }
81
+ if (memoryFallback?.authPath === authPath && memoryFallback.accessToken !== accessToken && isTokenFresh(memoryFallback.accessToken)) return memoryFallback;
82
+ return {
83
+ authPath,
84
+ parsed,
85
+ accessToken
86
+ };
87
+ }
88
+ function isTokenFresh(accessToken) {
89
+ const exp = decodeJwtPayload(accessToken)?.exp;
90
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return true;
91
+ return exp * 1e3 - nowFn() > TOKEN_REFRESH_LEAD_MS;
92
+ }
93
+ async function refreshToken(authPath, rawAuth) {
94
+ const refreshTok = rawAuth?.tokens?.refresh_token;
95
+ if (typeof refreshTok !== "string" || refreshTok.trim() === "") throw new LlmError("Codex 凭证已过期且缺少 refresh_token,请重新运行 codex login", "AUTH_FAILED");
96
+ const response = await fetchFn(CODEX_TOKEN_URL, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({
100
+ client_id: CODEX_CLIENT_ID,
101
+ grant_type: "refresh_token",
102
+ refresh_token: refreshTok,
103
+ scope: "openid profile email"
104
+ }),
105
+ signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS)
106
+ });
107
+ if (!response.ok) throw new LlmError(`Codex Token 刷新失败 (HTTP ${response.status}),请重新运行 codex login`, "AUTH_FAILED");
108
+ const fresh = await response.json();
109
+ if (typeof fresh?.access_token !== "string" || fresh.access_token.trim() === "") throw new LlmError("Codex Token 刷新响应缺少 access_token", "AUTH_FAILED");
110
+ const nextAuth = {
111
+ ...rawAuth,
112
+ tokens: {
113
+ ...rawAuth.tokens ?? {},
114
+ access_token: fresh.access_token,
115
+ ...typeof fresh.id_token === "string" && fresh.id_token !== "" ? { id_token: fresh.id_token } : {},
116
+ ...typeof fresh.refresh_token === "string" && fresh.refresh_token !== "" ? { refresh_token: fresh.refresh_token } : {}
117
+ },
118
+ last_refresh: new Date(nowFn()).toISOString()
119
+ };
120
+ memoryFallback = {
121
+ authPath,
122
+ parsed: nextAuth,
123
+ accessToken: fresh.access_token
124
+ };
125
+ try {
126
+ await writeJsonAtomic(authPath, nextAuth);
127
+ } catch (error) {
128
+ console.error(`[dsh-codex-subscription] 写回刷新后的 Codex auth.json 失败 (${authPath}): ${error instanceof Error ? error.message : String(error)}`);
129
+ }
130
+ return {
131
+ accessToken: fresh.access_token,
132
+ accountId: extractAccountId(nextAuth, fresh.access_token)
133
+ };
134
+ }
135
+ function toCredentials(state) {
136
+ return {
137
+ accessToken: state.accessToken,
138
+ accountId: extractAccountId(state.parsed, state.accessToken)
139
+ };
140
+ }
141
+ async function resolveCredentials() {
142
+ const current = readAuthState();
143
+ if (isTokenFresh(current.accessToken)) return toCredentials(current);
144
+ if (!inflightRefresh) inflightRefresh = (async () => {
145
+ const latest = readAuthState();
146
+ if (isTokenFresh(latest.accessToken)) return toCredentials(latest);
147
+ return refreshToken(latest.authPath, latest.parsed);
148
+ })().finally(() => {
149
+ inflightRefresh = void 0;
150
+ });
151
+ return inflightRefresh;
152
+ }
153
+ return {
154
+ resolveCredentials,
155
+ async resolveAccessToken() {
156
+ return (await resolveCredentials()).accessToken;
157
+ }
158
+ };
159
+ }
160
+ //#endregion
161
+ //#region src/catalog.ts
162
+ const PROVIDER = "openai-codex";
163
+ const DISPLAY_NAME = "Codex";
164
+ const DEFAULT_CLIENT_VERSION = "0.163.0";
165
+ const THINKING_LEVELS = [
166
+ "off",
167
+ "minimal",
168
+ "low",
169
+ "medium",
170
+ "high",
171
+ "xhigh",
172
+ "max"
173
+ ];
174
+ const DEFAULT_DISABLED_MODELS = Object.freeze([
175
+ "gpt-6-sol",
176
+ "gpt-6-luna",
177
+ "gpt-5.6-sol",
178
+ "gpt-5.6-terra",
179
+ "gpt-5.6-luna"
180
+ ]);
181
+ function buildThinkingLevelMap(supportedLevels) {
182
+ const efforts = new Set((Array.isArray(supportedLevels) ? supportedLevels : []).map((item) => {
183
+ if (typeof item === "string") return item;
184
+ if (item !== null && typeof item === "object" && "effort" in item) {
185
+ const effort = item.effort;
186
+ return typeof effort === "string" ? effort : "";
187
+ }
188
+ return "";
189
+ }).filter((value) => value.length > 0));
190
+ const map = {};
191
+ for (const level of THINKING_LEVELS) if (level === "off") map.off = efforts.has("none") ? "none" : null;
192
+ else if (efforts.has(level) || level === "max" && efforts.has("ultra")) map[level] = level;
193
+ else map[level] = null;
194
+ return map;
195
+ }
196
+ function normalizeDefaultReasoningLevel(rawLevel, thinkingLevelMap) {
197
+ if (typeof rawLevel !== "string" || rawLevel.trim() === "") return void 0;
198
+ const trimmed = rawLevel.trim();
199
+ const mapped = trimmed === "none" ? "off" : trimmed === "ultra" ? "max" : trimmed;
200
+ return thinkingLevelMap[mapped] ? mapped : void 0;
201
+ }
202
+ function isVerbosity(value) {
203
+ return value === "low" || value === "medium" || value === "high";
204
+ }
205
+ function parseModelsCache(rawCache, baseModels, disabledModels = []) {
206
+ const disabledSet = new Set((Array.isArray(disabledModels) ? disabledModels : []).filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean));
207
+ const clientVersion = typeof rawCache?.client_version === "string" && rawCache.client_version.trim() !== "" ? rawCache.client_version.trim() : DEFAULT_CLIENT_VERSION;
208
+ const rawModels = Array.isArray(rawCache?.models) ? rawCache.models : [];
209
+ const templateModel = baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
210
+ 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) => {
211
+ return (typeof a.priority === "number" && Number.isFinite(a.priority) ? a.priority : 0) - (typeof b.priority === "number" && Number.isFinite(b.priority) ? b.priority : 0);
212
+ });
213
+ const models = [];
214
+ const metadataById = new Map();
215
+ const seen = new Set();
216
+ for (const entry of visibleEntries) {
217
+ const id = entry.slug.trim();
218
+ if (seen.has(id)) continue;
219
+ seen.add(id);
220
+ const base = baseModels.find((model) => model.id === id) ?? templateModel;
221
+ const contextWindow = typeof entry.context_window === "number" && Number.isSafeInteger(entry.context_window) && entry.context_window > 0 ? entry.context_window : typeof entry.max_context_window === "number" && Number.isSafeInteger(entry.max_context_window) && entry.max_context_window > 0 ? entry.max_context_window : base?.contextWindow ?? 272e3;
222
+ const maxTokens = Math.min(128e3, Math.max(32768, Math.floor(contextWindow / 4)));
223
+ const inputs = Array.isArray(entry.input_modalities) ? entry.input_modalities.filter((m) => m === "text" || m === "image") : ["text", "image"];
224
+ const thinkingLevelMap = buildThinkingLevelMap(entry.supported_reasoning_levels);
225
+ const hasReasoning = Object.entries(thinkingLevelMap).some(([k, v]) => k !== "off" && v !== null);
226
+ const supportVerbosity = entry.support_verbosity !== false;
227
+ const defaultVerbosity = isVerbosity(entry.default_verbosity) ? entry.default_verbosity : "low";
228
+ const defaultReasoningLevel = normalizeDefaultReasoningLevel(entry.default_reasoning_level, thinkingLevelMap);
229
+ metadataById.set(id, {
230
+ supportVerbosity,
231
+ defaultVerbosity,
232
+ defaultReasoningLevel
233
+ });
234
+ models.push({
235
+ ...base ?? {},
236
+ id,
237
+ name: typeof entry.display_name === "string" && entry.display_name.trim() !== "" ? entry.display_name.trim() : id,
238
+ ...typeof entry.description === "string" && entry.description.trim() !== "" ? { description: entry.description.trim() } : {},
239
+ api: "openai-codex-responses",
240
+ provider: PROVIDER,
241
+ baseUrl: "https://chatgpt.com/backend-api",
242
+ input: inputs.length > 0 ? inputs : ["text"],
243
+ contextWindow,
244
+ maxTokens,
245
+ reasoning: hasReasoning,
246
+ thinkingLevelMap,
247
+ cost: {
248
+ input: 0,
249
+ output: 0,
250
+ cacheRead: 0,
251
+ cacheWrite: 0
252
+ }
253
+ });
254
+ }
255
+ return {
256
+ clientVersion,
257
+ models,
258
+ metadataById
259
+ };
260
+ }
261
+ //#endregion
262
+ //#region src/request.ts
263
+ const FORBIDDEN_PAYLOAD_FIELDS = Object.freeze([
264
+ "max_output_tokens",
265
+ "max_completion_tokens",
266
+ "temperature",
267
+ "top_p",
268
+ "previous_response_id",
269
+ "user",
270
+ "safety_identifier",
271
+ "prompt_cache_options"
272
+ ]);
273
+ let cachedOsLabel;
274
+ function detectOsLabel() {
275
+ if (cachedOsLabel !== void 0) return cachedOsLabel;
276
+ const sys = platform();
277
+ if (sys === "darwin") try {
278
+ const parts = execFileSync("sw_vers", ["-productVersion"], {
279
+ encoding: "utf8",
280
+ timeout: 2e3
281
+ }).trim().split(".");
282
+ while (parts.length < 3) parts.push("0");
283
+ cachedOsLabel = `Mac OS ${parts.slice(0, 3).join(".")}`;
284
+ return cachedOsLabel;
285
+ } catch {
286
+ cachedOsLabel = "Mac OS";
287
+ return cachedOsLabel;
288
+ }
289
+ if (sys === "win32") {
290
+ cachedOsLabel = `Windows ${release()}`;
291
+ return cachedOsLabel;
292
+ }
293
+ cachedOsLabel = `Linux ${release()}`;
294
+ return cachedOsLabel;
295
+ }
296
+ function buildCodexUserAgent(version = DEFAULT_CLIENT_VERSION) {
297
+ const cpu = arch() === "arm64" ? "arm64" : arch() === "x64" ? "x86_64" : arch();
298
+ const term = platform() === "darwin" ? "Apple_Terminal/455" : platform() === "win32" ? "WindowsTerminal" : "xterm-256color";
299
+ return `codex_cli_rs/${version} (${detectOsLabel()}; ${cpu}) ${term}`;
300
+ }
301
+ function buildCodexHeaders(existingHeaders, clientVersion, sessionId) {
302
+ const headers = {};
303
+ for (const [key, value] of Object.entries(existingHeaders ?? {})) {
304
+ if (key.toLowerCase() === "user-agent") continue;
305
+ headers[key] = value;
306
+ }
307
+ headers.originator = "codex_cli_rs";
308
+ headers.version = clientVersion;
309
+ headers["User-Agent"] = buildCodexUserAgent(clientVersion);
310
+ if (typeof sessionId === "string" && sessionId.length > 0) headers["thread-id"] = sessionId;
311
+ return headers;
312
+ }
313
+ function sanitizeCodexPayload(payload, modelMeta) {
314
+ if (!payload || typeof payload !== "object") return payload;
315
+ const cleaned = { ...payload };
316
+ for (const field of FORBIDDEN_PAYLOAD_FIELDS) delete cleaned[field];
317
+ if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
318
+ const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
319
+ if (modelMeta?.supportVerbosity === false) {
320
+ if (currentText !== void 0) {
321
+ delete currentText.verbosity;
322
+ if (Object.keys(currentText).length === 0) delete cleaned.text;
323
+ else cleaned.text = currentText;
324
+ }
325
+ } else {
326
+ const verbosity = modelMeta?.defaultVerbosity ?? "low";
327
+ cleaned.text = {
328
+ ...currentText ?? {},
329
+ verbosity
330
+ };
331
+ }
332
+ if (cleaned.reasoning === void 0 && modelMeta?.defaultReasoningLevel !== void 0 && modelMeta.defaultReasoningLevel !== "off") cleaned.reasoning = {
333
+ effort: modelMeta.defaultReasoningLevel,
334
+ summary: "auto"
335
+ };
336
+ if (cleaned.reasoning && typeof cleaned.reasoning === "object" && !Array.isArray(cleaned.reasoning)) {
337
+ const reasoningObj = cleaned.reasoning;
338
+ const effort = reasoningObj.effort === "ultra" ? "max" : reasoningObj.effort;
339
+ if (effort === "none") delete cleaned.include;
340
+ else {
341
+ cleaned.reasoning = {
342
+ ...reasoningObj,
343
+ effort,
344
+ summary: reasoningObj.summary ?? "auto"
345
+ };
346
+ const include = Array.isArray(cleaned.include) ? [...cleaned.include] : [];
347
+ if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
348
+ cleaned.include = include;
349
+ }
350
+ }
351
+ return cleaned;
352
+ }
353
+ //#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
+ //#region src/index.ts
457
+ const name = "codex-subscription";
458
+ const inject = ["llm"];
459
+ const STREAM_IDLE_TIMEOUT_MS = 3e5;
460
+ const MAX_REQUEST_IMAGE_BYTES = 20971520;
461
+ const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
462
+ const REQUEST_IMAGE_MAX_BYTES = 1048576;
463
+ const Config = z.object({ disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile() });
464
+ function readVolatile(field, fallback) {
465
+ const value = typeof field?.get === "function" ? field.get() : field;
466
+ return value === void 0 || value === null ? fallback : value;
467
+ }
468
+ var CodexPiAiAdapter = class extends PiAiAdapter {
469
+ #getModelMetadata;
470
+ constructor(options, getModelMetadata) {
471
+ super(options);
472
+ this.#getModelMetadata = getModelMetadata;
473
+ }
474
+ modelInfo(snapshot, provider, model) {
475
+ const info = super.modelInfo(snapshot, provider, model);
476
+ const defaultLevel = this.#getModelMetadata(model)?.defaultReasoningLevel;
477
+ if (defaultLevel !== void 0 && info.reasoning !== void 0 && info.reasoning.efforts.some((e) => e.id === defaultLevel)) return {
478
+ ...info,
479
+ reasoning: {
480
+ ...info.reasoning,
481
+ defaultEffort: defaultLevel
482
+ }
483
+ };
484
+ return info;
485
+ }
486
+ };
487
+ function apply(ctx, config = {}) {
488
+ const baseProvider = openaiCodexProvider();
489
+ const baseModels = baseProvider.getModels();
490
+ const getCodexHome = () => resolveCodexHome();
491
+ const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
492
+ const authManager = createAuthManager({ getCodexHome });
493
+ let fileSignature;
494
+ let cachedRaw;
495
+ let warnedMissingFile = false;
496
+ let snapshotSignature;
497
+ let currentCatalog = {
498
+ clientVersion: DEFAULT_CLIENT_VERSION,
499
+ models: [],
500
+ metadataById: new Map()
501
+ };
502
+ let currentProfilesMap;
503
+ const requestTokenAuth = Object.freeze({
504
+ name: "Codex CLI auth.json token",
505
+ async resolve({ credential }) {
506
+ const token = credential?.key;
507
+ if (typeof token !== "string" || token.length === 0) return void 0;
508
+ return {
509
+ auth: { apiKey: token },
510
+ source: "codex-auth-json"
511
+ };
512
+ }
513
+ });
514
+ function wrapOptions(model, options = {}) {
515
+ const { catalog } = ensureSnapshot();
516
+ const modelMeta = catalog.metadataById.get(model?.id);
517
+ const { temperature: _temperature, onPayload, ...rest } = options;
518
+ return {
519
+ ...rest,
520
+ transport: "sse",
521
+ headers: buildCodexHeaders(rest.headers, catalog.clientVersion, rest.sessionId),
522
+ async onPayload(payload, requestModel) {
523
+ return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta);
524
+ }
525
+ };
526
+ }
527
+ const usageFetcher = createUsageFetcher({
528
+ resolveCredentials: () => authManager.resolveCredentials(),
529
+ buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion)
530
+ });
531
+ const piProvider = Object.freeze({
532
+ ...baseProvider,
533
+ id: PROVIDER,
534
+ name: DISPLAY_NAME,
535
+ auth: Object.freeze({
536
+ ...baseProvider.auth,
537
+ apiKey: requestTokenAuth
538
+ }),
539
+ getModels: () => ensureSnapshot().catalog.models,
540
+ stream: (model, context, options) => {
541
+ usageFetcher.invalidate();
542
+ return baseProvider.stream(model, context, wrapOptions(model, options));
543
+ },
544
+ streamSimple: (model, context, options) => {
545
+ usageFetcher.invalidate();
546
+ return baseProvider.streamSimple(model, context, wrapOptions(model, options));
547
+ }
548
+ });
549
+ function ensureSnapshot() {
550
+ const cachePath = join(getCodexHome(), "models_cache.json");
551
+ const disabled = getDisabledModels();
552
+ try {
553
+ const stat = statSync(cachePath);
554
+ const nextFileSig = `${cachePath}:${stat.mtimeMs}:${stat.size}`;
555
+ if (nextFileSig !== fileSignature) {
556
+ cachedRaw = JSON.parse(readFileSync(cachePath, "utf8"));
557
+ fileSignature = nextFileSig;
558
+ warnedMissingFile = false;
559
+ }
560
+ } catch (error) {
561
+ if (!warnedMissingFile) {
562
+ warnedMissingFile = true;
563
+ console.error(`[dsh-codex-subscription] 读取 Codex models_cache.json 失败 (${cachePath}): ${error instanceof Error ? error.message : String(error)}`);
564
+ }
565
+ }
566
+ const nextSnapshotSig = JSON.stringify([fileSignature ?? "none", disabled]);
567
+ if (nextSnapshotSig !== snapshotSignature || currentProfilesMap === void 0) {
568
+ currentCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled) : {
569
+ clientVersion: DEFAULT_CLIENT_VERSION,
570
+ models: [],
571
+ metadataById: new Map()
572
+ };
573
+ currentProfilesMap = new Map([[PROVIDER, Object.freeze({
574
+ provider: PROVIDER,
575
+ displayName: DISPLAY_NAME,
576
+ piProvider,
577
+ configuredMaxTokens: new Map(),
578
+ modelErrors: new Map(),
579
+ maxRequestImageBytes: MAX_REQUEST_IMAGE_BYTES,
580
+ requestImagePixelBudget: REQUEST_IMAGE_PIXEL_BUDGET,
581
+ requestImageMaxBytes: REQUEST_IMAGE_MAX_BYTES,
582
+ streamIdleTimeoutMs: STREAM_IDLE_TIMEOUT_MS,
583
+ cacheRetention: "short",
584
+ transport: "sse"
585
+ })]]);
586
+ snapshotSignature = nextSnapshotSig;
587
+ }
588
+ return {
589
+ catalog: currentCatalog,
590
+ profiles: currentProfilesMap
591
+ };
592
+ }
593
+ const adapter = new CodexPiAiAdapter({
594
+ profiles: () => ensureSnapshot().profiles,
595
+ resolveApiKey: (_provider, _profile) => authManager.resolveAccessToken(),
596
+ auth: Object.freeze({
597
+ credentials: {
598
+ async read() {},
599
+ async list() {
600
+ return [];
601
+ },
602
+ async modify() {},
603
+ async delete() {}
604
+ },
605
+ authContext: {
606
+ async env() {},
607
+ async fileExists() {
608
+ return false;
609
+ }
610
+ }
611
+ }),
612
+ resolveAttachments: () => ctx.get?.("attachments"),
613
+ resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => (ctx.get?.("fs"))?.processPathFromHostPath(hostPath), ref),
614
+ onReplayDegrade: (provider, model, reason) => {
615
+ console.error(`[dsh-codex-subscription] replay state for "${provider}/${model}" degraded (${reason})`);
616
+ }
617
+ }, (modelId) => ensureSnapshot().catalog.metadataById.get(modelId));
618
+ ctx.llm.registerAdapter([PROVIDER], adapter);
619
+ const mountUsageRoute = (connCtx) => {
620
+ const connection = connCtx.get?.("connection");
621
+ const registerFetch = connection?.fetch?.register;
622
+ if (typeof registerFetch !== "function") return;
623
+ const registerRoute = () => registerFetch.call(connection?.fetch, {
624
+ path: USAGE_ROUTE_PATH,
625
+ methods: ["GET"],
626
+ requestBody: "buffered",
627
+ fetch: async (request) => {
628
+ const force = new URL(request.url, "http://localhost").searchParams.get("force") === "1";
629
+ const result = await usageFetcher.getUsage(force);
630
+ return new Response(JSON.stringify(result), {
631
+ status: 200,
632
+ headers: { "content-type": "application/json" }
633
+ });
634
+ }
635
+ });
636
+ if (typeof connCtx.effect === "function") connCtx.effect(() => registerRoute(), "codex-subscription: usage route");
637
+ else registerRoute();
638
+ };
639
+ if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
640
+ else mountUsageRoute(ctx);
641
+ ctx.on?.("loader/volatile-update", () => {
642
+ ensureSnapshot();
643
+ ctx.llm.emitAdaptersUpdated?.();
644
+ });
645
+ }
646
+ const __test = Object.freeze({
647
+ PROVIDER,
648
+ DISPLAY_NAME,
649
+ DEFAULT_DISABLED_MODELS,
650
+ CODEX_USAGE_URL,
651
+ USAGE_ROUTE_PATH,
652
+ DEFAULT_USAGE_TTL_MS,
653
+ resolveCodexHome,
654
+ decodeJwtPayload,
655
+ extractAccountId,
656
+ createAuthManager,
657
+ buildThinkingLevelMap,
658
+ parseModelsCache,
659
+ sanitizeCodexPayload,
660
+ buildCodexHeaders,
661
+ buildCodexUserAgent,
662
+ parseUsagePayload,
663
+ createUsageFetcher
664
+ });
665
+ //#endregion
666
+ 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
+ }