@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 +57 -2
- package/cordis.patch.yml +10 -0
- package/icon.svg +6 -0
- package/lib/index.d.ts +91 -0
- package/lib/index.js +502 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +62 -4
package/README.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-codex-subscription
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
```
|
package/cordis.patch.yml
ADDED
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
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyogvce/dsh-codex-subscription",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|