@armadra/agent 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/CHANGELOG.zh-CN.md +56 -0
- package/README.md +168 -575
- package/README.zh-CN.md +165 -596
- package/dist/agent/session-settings.d.ts +4 -1
- package/dist/agent/session-settings.js +7 -2
- package/dist/agent/session.d.ts +6 -0
- package/dist/agent/session.js +8 -0
- package/dist/agent/subagent-background.d.ts +75 -0
- package/dist/agent/subagent-background.js +209 -0
- package/dist/agent/subagent-registry.d.ts +24 -5
- package/dist/agent/subagent-registry.js +64 -92
- package/dist/agent/types-w5.d.ts +11 -1
- package/dist/agent/types.d.ts +5 -0
- package/dist/agents/builtin.js +0 -1
- package/dist/agents/external.js +0 -1
- package/dist/agents/parse.js +4 -3
- package/dist/agents/result.d.ts +7 -1
- package/dist/agents/result.js +20 -1
- package/dist/agents/task-control.d.ts +13 -2
- package/dist/agents/task-record.d.ts +13 -1
- package/dist/agents/task-record.js +32 -0
- package/dist/agents/types.d.ts +5 -1
- package/dist/ai/apis/chatgpt-backend.d.ts +7 -2
- package/dist/ai/apis/chatgpt-backend.js +12 -4
- package/dist/ai/providers/discovered-cache.d.ts +29 -12
- package/dist/ai/providers/discovered-cache.js +72 -12
- package/dist/ai/providers/registry.js +7 -6
- package/dist/ai/types.d.ts +5 -0
- package/dist/auth/chatgpt/backend-client.d.ts +14 -4
- package/dist/auth/chatgpt/backend-client.js +41 -8
- package/dist/auth/chatgpt/cli.d.ts +2 -1
- package/dist/auth/chatgpt/cli.js +14 -7
- package/dist/auth/chatgpt/follow.d.ts +16 -0
- package/dist/auth/chatgpt/follow.js +51 -0
- package/dist/auth/chatgpt/presets.d.ts +8 -0
- package/dist/auth/chatgpt/presets.js +12 -0
- package/dist/auth/testing/fake-oauth.d.ts +4 -1
- package/dist/auth/testing/fake-oauth.js +24 -1
- package/dist/bundle/ama.cjs +2016 -664
- package/dist/cli/compose-agents.d.ts +2 -1
- package/dist/cli/compose-agents.js +11 -2
- package/dist/cli/compose-providers.d.ts +2 -1
- package/dist/cli/compose-providers.js +6 -3
- package/dist/cli/startup-steps.js +4 -1
- package/dist/cli/subcommands/models-discover.d.ts +5 -2
- package/dist/cli/subcommands/models-discover.js +52 -10
- package/dist/config/json-schema.js +10 -2
- package/dist/config/key-docs.js +9 -3
- package/dist/config/merge.d.ts +1 -1
- package/dist/config/merge.js +20 -3
- package/dist/config/schema-w5.js +4 -2
- package/dist/config/schema-w6.d.ts +1 -1
- package/dist/config/schema-w6.js +2 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/settings-registry.js +4 -0
- package/dist/config/types-w5.d.ts +10 -0
- package/dist/config/types-w5.js +2 -0
- package/dist/config/types-w6.d.ts +5 -0
- package/dist/config/types.d.ts +3 -1
- package/dist/git/info.d.ts +19 -0
- package/dist/git/info.js +64 -8
- package/dist/i18n/catalog.d.ts +52 -8
- package/dist/i18n/messages/agents.d.ts +56 -0
- package/dist/i18n/messages/agents.js +58 -2
- package/dist/i18n/messages/auth.d.ts +12 -0
- package/dist/i18n/messages/auth.js +20 -0
- package/dist/i18n/messages/config-keys.d.ts +8 -0
- package/dist/i18n/messages/config-keys.js +18 -10
- package/dist/i18n/messages/config.d.ts +8 -0
- package/dist/i18n/messages/interactive-startup.d.ts +0 -16
- package/dist/i18n/messages/interactive-startup.js +0 -16
- package/dist/i18n/messages/interactive.d.ts +23 -16
- package/dist/i18n/messages/interactive.js +25 -0
- package/dist/i18n/messages/print.d.ts +4 -0
- package/dist/i18n/messages/print.js +4 -0
- package/dist/i18n/messages/report.js +4 -4
- package/dist/i18n/messages/settings.d.ts +6 -0
- package/dist/i18n/messages/settings.js +6 -0
- package/dist/i18n/messages/subcommands-config.d.ts +4 -0
- package/dist/i18n/messages/subcommands-config.js +4 -0
- package/dist/i18n/messages/subcommands.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/modes/commands-core.js +8 -4
- package/dist/modes/interactive/agent-bar.d.ts +3 -1
- package/dist/modes/interactive/agent-bar.js +12 -5
- package/dist/modes/interactive/agent-ui.d.ts +21 -3
- package/dist/modes/interactive/agent-ui.js +81 -12
- package/dist/modes/interactive/agent-view.d.ts +4 -0
- package/dist/modes/interactive/agent-view.js +14 -1
- package/dist/modes/interactive/approval-dock.d.ts +51 -0
- package/dist/modes/interactive/approval-dock.js +112 -0
- package/dist/modes/interactive/approval-ui.d.ts +43 -0
- package/dist/modes/interactive/approval-ui.js +64 -0
- package/dist/modes/interactive/commands.js +4 -1
- package/dist/modes/interactive/event-notices.d.ts +6 -1
- package/dist/modes/interactive/event-notices.js +7 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -2
- package/dist/modes/interactive/interactive-mode.js +33 -38
- package/dist/modes/interactive/key-dispatch.d.ts +21 -4
- package/dist/modes/interactive/key-dispatch.js +60 -8
- package/dist/modes/interactive/line/line-mode.js +5 -0
- package/dist/modes/interactive/run-indicator.d.ts +15 -0
- package/dist/modes/interactive/run-indicator.js +37 -5
- package/dist/modes/interactive/session-events.js +5 -1
- package/dist/modes/interactive/startup-header.d.ts +29 -14
- package/dist/modes/interactive/startup-header.js +93 -59
- package/dist/modes/interactive/startup-logo.d.ts +83 -0
- package/dist/modes/interactive/startup-logo.js +183 -0
- package/dist/modes/interactive/status-area.d.ts +18 -0
- package/dist/modes/interactive/status-area.js +67 -1
- package/dist/modes/interactive/status-bar.d.ts +9 -1
- package/dist/modes/interactive/status-bar.js +43 -11
- package/dist/modes/interactive/status-line.d.ts +2 -0
- package/dist/modes/interactive/status-line.js +11 -6
- package/dist/modes/interactive/status-quota.d.ts +44 -0
- package/dist/modes/interactive/status-quota.js +135 -0
- package/dist/modes/interactive/subagent-view.d.ts +1 -0
- package/dist/modes/interactive/subagent-view.js +8 -0
- package/dist/modes/interactive/task-background.d.ts +31 -0
- package/dist/modes/interactive/task-background.js +68 -0
- package/dist/modes/interactive/tool-view.d.ts +7 -1
- package/dist/modes/interactive/tool-view.js +24 -1
- package/dist/modes/print/print-mode.d.ts +11 -0
- package/dist/modes/print/print-mode.js +36 -1
- package/dist/modes/rpc/commands.d.ts +2 -1
- package/dist/modes/rpc/commands.js +9 -1
- package/dist/rpc.d.ts +13 -0
- package/dist/rpc.js +3 -0
- package/dist/tools/task-ctl.d.ts +2 -0
- package/dist/tools/task-ctl.js +7 -2
- package/dist/tools/task.d.ts +11 -0
- package/dist/tools/task.js +20 -2
- package/dist/tui/components/editor.d.ts +2 -0
- package/dist/tui/components/editor.js +4 -0
- package/dist/tui/components/loader.d.ts +5 -1
- package/dist/tui/components/loader.js +18 -5
- package/dist/tui/keybindings.d.ts +8 -3
- package/dist/tui/keybindings.js +8 -3
- package/docs/agents.md +52 -28
- package/docs/en/host-api.md +5 -1
- package/docs/en/providers.md +8 -4
- package/docs/en/rpc.md +19 -7
- package/docs/en/sessions.md +3 -1
- package/docs/en/tui.md +84 -66
- package/docs/host-api.md +5 -1
- package/docs/providers.md +29 -12
- package/docs/rpc.md +19 -7
- package/docs/sessions.md +2 -1
- package/docs/tui-design.md +42 -29
- package/docs/tui.md +67 -49
- package/package.json +1 -1
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
|
|
5
5
|
* `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
|
|
6
|
-
* (`mergeDiscoveredModels
|
|
7
|
-
*
|
|
6
|
+
* (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商。`ama auth logout chatgpt` 删掉它。只存 slug、显示名、
|
|
7
|
+
* 后端给的元数据(上下文窗口、输入模态、推理强度)、flavor 与时间戳——没有 token、账户 id。
|
|
8
|
+
*
|
|
9
|
+
* 元数据优先级:后端明确给的 > models.dev(快照 ⊕ 用户刷新)> 自定义缺省。上下文窗口尤其如此:订阅后端的生效
|
|
10
|
+
* 窗口(如 codex 的 272k)可能远小于 models.dev 记的 API 版窗口(1.1M),按后者算压缩阈值会让请求超窗被拒。
|
|
11
|
+
* 旧版本写的缓存(siwc 不含上下文窗口)照旧用 models.dev;`chatgpt` 两边都没有时用保守缺省
|
|
12
|
+
* `SUBSCRIPTION_FALLBACK_CONTEXT_WINDOW`,不让自动压缩关闭。
|
|
13
|
+
*
|
|
14
|
+
* ChatGPT 的缓存带登录 flavor;组装时与当前登录的 flavor 不符视为过期、不并入(选择器提示重新发现)。
|
|
8
15
|
*/
|
|
9
16
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
17
|
import { dirname, join } from "node:path";
|
|
@@ -15,11 +22,24 @@ export const DISCOVERED_CACHE_VERSION = 1;
|
|
|
15
22
|
export function discoveredCachePath(dataDir, provider) {
|
|
16
23
|
return join(dataDir, "models", "discovered", `${provider}.json`);
|
|
17
24
|
}
|
|
18
|
-
|
|
25
|
+
/** 未知值 → 缓存模型;id 不对返回 undefined,元数据字段形状不对的丢掉。 */
|
|
26
|
+
function toEntry(value) {
|
|
19
27
|
if (typeof value !== "object" || value === null)
|
|
20
|
-
return
|
|
21
|
-
const
|
|
22
|
-
|
|
28
|
+
return undefined;
|
|
29
|
+
const raw = value;
|
|
30
|
+
const { id, name, contextWindow, input, reasoningLevels } = raw;
|
|
31
|
+
if (typeof id !== "string" || id === "")
|
|
32
|
+
return undefined;
|
|
33
|
+
const out = { id };
|
|
34
|
+
if (typeof name === "string" && name !== "")
|
|
35
|
+
out.name = name;
|
|
36
|
+
if (typeof contextWindow === "number" && Number.isInteger(contextWindow) && contextWindow > 0)
|
|
37
|
+
out.contextWindow = contextWindow;
|
|
38
|
+
if (Array.isArray(input) && input.includes("text"))
|
|
39
|
+
out.input = ["text", "image"].filter((x) => input.includes(x));
|
|
40
|
+
if (Array.isArray(reasoningLevels) && reasoningLevels.every((x) => typeof x === "string"))
|
|
41
|
+
out.reasoningLevels = reasoningLevels;
|
|
42
|
+
return out;
|
|
23
43
|
}
|
|
24
44
|
/** 读缓存;不存在、损坏或形状不对返回 undefined(不报错,选择器给提示行)。 */
|
|
25
45
|
export function readDiscoveredCache(dataDir, provider) {
|
|
@@ -40,9 +60,7 @@ export function readDiscoveredCache(dataDir, provider) {
|
|
|
40
60
|
provider,
|
|
41
61
|
fetchedAt: typeof file.fetchedAt === "string" ? file.fetchedAt : "",
|
|
42
62
|
...(typeof file.flavor === "string" ? { flavor: file.flavor } : {}),
|
|
43
|
-
models: file.models
|
|
44
|
-
.filter(isEntry)
|
|
45
|
-
.map((m) => (m.name ? { id: m.id, name: m.name } : { id: m.id })),
|
|
63
|
+
models: file.models.map(toEntry).filter((m) => m !== undefined),
|
|
46
64
|
};
|
|
47
65
|
}
|
|
48
66
|
/** 写缓存(先写临时文件再改名);返回路径。 */
|
|
@@ -53,7 +71,7 @@ export function writeDiscoveredCache(dataDir, provider, input, now = Date.now())
|
|
|
53
71
|
provider,
|
|
54
72
|
fetchedAt: new Date(now).toISOString(),
|
|
55
73
|
...(input.flavor !== undefined ? { flavor: input.flavor } : {}),
|
|
56
|
-
models: input.models.map((m) =>
|
|
74
|
+
models: input.models.map(toEntry).filter((m) => m !== undefined),
|
|
57
75
|
};
|
|
58
76
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
59
77
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
@@ -69,11 +87,48 @@ export function clearDiscoveredCache(dataDir, provider) {
|
|
|
69
87
|
rmSync(path, { force: true });
|
|
70
88
|
return true;
|
|
71
89
|
}
|
|
72
|
-
|
|
90
|
+
const EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
91
|
+
/** 后端给的推理强度 → 思考级别映射(`none` 对应 off;没列出的级别不支持)。 */
|
|
92
|
+
export function thinkingMapOf(levels) {
|
|
93
|
+
const map = {
|
|
94
|
+
off: levels.includes("none") ? "none" : null,
|
|
95
|
+
};
|
|
96
|
+
for (const level of EFFORT_LEVELS)
|
|
97
|
+
map[level] = levels.includes(level) ? level : null;
|
|
98
|
+
return map;
|
|
99
|
+
}
|
|
100
|
+
/** 订阅后端:models.dev 与后端都没给上下文窗口时的保守缺省。 */
|
|
101
|
+
export const SUBSCRIPTION_FALLBACK_CONTEXT_WINDOW = 128_000;
|
|
102
|
+
const SUBSCRIPTION_PROVIDERS = new Set(["chatgpt"]);
|
|
103
|
+
/** 后端明确给的元数据覆盖 models.dev 补的;没给的字段保留 models.dev 的(maxTokens 只来自 models.dev)。 */
|
|
104
|
+
function withBackendMetadata(model, cached) {
|
|
105
|
+
if (cached.contextWindow !== undefined) {
|
|
106
|
+
model.contextWindow = cached.contextWindow;
|
|
107
|
+
// models.dev 的输入上限是 API 版的,超过后端窗口就没有意义
|
|
108
|
+
if (model.inputLimit !== undefined && model.inputLimit >= cached.contextWindow)
|
|
109
|
+
delete model.inputLimit;
|
|
110
|
+
}
|
|
111
|
+
if (cached.input !== undefined)
|
|
112
|
+
model.input = [...cached.input];
|
|
113
|
+
if (cached.reasoningLevels === undefined)
|
|
114
|
+
return;
|
|
115
|
+
if (cached.reasoningLevels.some((x) => x !== "none")) {
|
|
116
|
+
model.reasoning = true;
|
|
117
|
+
model.thinkingLevelMap = thinkingMapOf(cached.reasoningLevels);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
model.reasoning = false;
|
|
121
|
+
delete model.thinkingLevelMap;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** 缓存里的模型 → 注册表模型(后端给的元数据优先,其次 models.dev,再次自定义缺省)。 */
|
|
73
125
|
export function discoveredModels(file, provider, modelsDev) {
|
|
74
126
|
return file.models.map((m) => {
|
|
75
127
|
const { entry } = enrichEntry(m.name ? { id: m.id, name: m.name } : { id: m.id }, modelsDev);
|
|
76
128
|
const model = withCustomDefaults(entry, provider.id, provider.api);
|
|
129
|
+
withBackendMetadata(model, m);
|
|
130
|
+
if (model.contextWindow === undefined && SUBSCRIPTION_PROVIDERS.has(provider.id))
|
|
131
|
+
model.contextWindow = SUBSCRIPTION_FALLBACK_CONTEXT_WINDOW;
|
|
77
132
|
// 只挂发现时登录的 flavor 对应的渠道(订阅后端的另一条渠道用不了,不在说明与 `@` 行里出现);
|
|
78
133
|
// 缓存没写 flavor 或渠道不存在时用供应商的缺省渠道
|
|
79
134
|
const names = (provider.channels ?? []).map((c) => c.name);
|
|
@@ -87,7 +142,9 @@ export function discoveredModels(file, provider, modelsDev) {
|
|
|
87
142
|
/**
|
|
88
143
|
* 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
|
|
89
144
|
*/
|
|
90
|
-
export function mergeDiscoveredModels(registry, dataDir, modelsDev
|
|
145
|
+
export function mergeDiscoveredModels(registry, dataDir, modelsDev,
|
|
146
|
+
/** 当前登录的 flavor(只有 OAuth 供应商有):缓存的 flavor 与之不符视为过期,不并入。 */
|
|
147
|
+
loginFlavor = () => undefined) {
|
|
91
148
|
const merged = [];
|
|
92
149
|
for (const provider of registry.list()) {
|
|
93
150
|
if (provider.models.length > 0 || !provider.requiresApiKey)
|
|
@@ -95,6 +152,9 @@ export function mergeDiscoveredModels(registry, dataDir, modelsDev) {
|
|
|
95
152
|
const file = readDiscoveredCache(dataDir, provider.id);
|
|
96
153
|
if (file === undefined || file.models.length === 0)
|
|
97
154
|
continue;
|
|
155
|
+
const flavor = loginFlavor(provider.id);
|
|
156
|
+
if (flavor !== undefined && file.flavor !== undefined && file.flavor !== flavor)
|
|
157
|
+
continue;
|
|
98
158
|
registry.addModels(provider.id, discoveredModels(file, provider, modelsDev), "discovered");
|
|
99
159
|
merged.push(provider.id);
|
|
100
160
|
}
|
|
@@ -380,9 +380,8 @@ export class ProviderRegistry {
|
|
|
380
380
|
candidates: available.filter(Boolean).map((c) => `${provider.id}/${model.id}@${c}`),
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return { ok: true, model: this.materialize(provider, model, channel), provider };
|
|
383
|
+
const picked = channel === model.channel ? model : this.materialize(provider, model, channel);
|
|
384
|
+
return { ok: true, model: { ...picked, channelPinned: true }, provider };
|
|
386
385
|
}
|
|
387
386
|
/**
|
|
388
387
|
* `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
|
|
@@ -415,9 +414,11 @@ export class ProviderRegistry {
|
|
|
415
414
|
const open = this.relayed.has(provider.id) || this.openTables.has(provider.id);
|
|
416
415
|
const relayed = provider.models.length === 0 || open;
|
|
417
416
|
if (synthesize && relayed && id.length > 0) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
417
|
+
// 按所要的渠道直接物化(先按缺省渠道物化再换渠道会留下缺省渠道的地址与 compat)
|
|
418
|
+
const known = provider.channels?.some((c) => c.name === channel) ? channel : undefined;
|
|
419
|
+
const synthesized = this.synthesize(provider, id, known);
|
|
420
|
+
if (known)
|
|
421
|
+
synthesized.channels = [...new Set([...(synthesized.channels ?? []), known])];
|
|
421
422
|
return { ok: true, model: synthesized, provider };
|
|
422
423
|
}
|
|
423
424
|
return { ok: false, reason: "not_found", candidates: this.similar(id, provider.id) };
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -321,6 +321,11 @@ export interface Model {
|
|
|
321
321
|
requiresApiKey?: boolean;
|
|
322
322
|
/** 所选渠道名(registry 物化时填入;隐式 `default` 渠道不填)。 */
|
|
323
323
|
channel?: string;
|
|
324
|
+
/**
|
|
325
|
+
* 渠道是引用里显式写的 `@渠道`(`findModel("p/m@c")` 填 true)。ChatGPT 订阅供应商据此区分:没写的渠道在
|
|
326
|
+
* 请求时跟随当前登录方式,写了的不跟随(不符时报 `chatgpt_flavor_mismatch`)。
|
|
327
|
+
*/
|
|
328
|
+
channelPinned?: boolean;
|
|
324
329
|
/** 该模型挂载的全部渠道,首个为首选(多渠道供应商才有)。 */
|
|
325
330
|
channels?: string[];
|
|
326
331
|
/** 模型家族(如 `claude-sonnet`)。 */
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChatGPT 后端的只读查询([W6-O]):模型列表(`ama models discover chatgpt`)与 codex 的 `wham/usage`。
|
|
3
3
|
*
|
|
4
|
-
* - SIWC:`GET {base}/models`,只留 `visibility === "list"
|
|
5
|
-
* - codex:`GET {base}/models?client_version=<
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
4
|
+
* - SIWC:`GET {base}/models`,只留 `visibility === "list"`(没有该字段的也留);条目若带下述元数据字段同样取;
|
|
5
|
+
* - codex:`GET {base}/models?client_version=<Codex CLI 版本>`(`codexClientVersion()`;后端按每个模型的
|
|
6
|
+
* `minimal_client_version` 过滤,发 ama 自己的版本号会一个都拿不到),`models[].slug`(同样按 visibility 筛),
|
|
7
|
+
* 另取 `context_window`、`input_modalities`、`supported_reasoning_levels` 作元数据(后端给了就优先于 models.dev,
|
|
8
|
+
* 见 discovered-cache.ts;`max_context_window` 是可调上限而非生效值、`auto_compact_token_limit` 没有对应的模型字段,
|
|
9
|
+
* 都不取);`GET https://chatgpt.com/backend-api/wham/usage`(由 codex base 推出),私有格式,解析失败返回 undefined。
|
|
10
|
+
* 请求带 Bearer;codex 另带 `ChatGPT-Account-ID` 与 `originator`。
|
|
8
11
|
*/
|
|
9
12
|
import { type QuotaSnapshot } from "../../ai/apis/chatgpt-rate-limits.js";
|
|
10
13
|
import type { ChatGptFlavor } from "../../config/types-w6.js";
|
|
@@ -15,10 +18,17 @@ export interface BackendAuth {
|
|
|
15
18
|
accessToken: string;
|
|
16
19
|
accountId?: string | undefined;
|
|
17
20
|
originator?: string | undefined;
|
|
21
|
+
/** codex 模型列表的 `client_version`,缺省 `DEFAULT_CODEX_CLIENT_VERSION`。 */
|
|
22
|
+
clientVersion?: string | undefined;
|
|
18
23
|
}
|
|
19
24
|
export interface DiscoveredModel {
|
|
20
25
|
id: string;
|
|
21
26
|
name?: string;
|
|
27
|
+
/** 以下是后端给的元数据(codex 后端给;SIWC 条目带了也取)。 */
|
|
28
|
+
contextWindow?: number;
|
|
29
|
+
input?: ("text" | "image")[];
|
|
30
|
+
/** 支持的推理强度(`none` / `minimal` / `low` / `medium` / `high` / `xhigh` …)。 */
|
|
31
|
+
reasoningLevels?: string[];
|
|
22
32
|
}
|
|
23
33
|
export declare function listChatGptModels(fetchFn: FetchLike, baseUrl: string, auth: BackendAuth): Promise<DiscoveredModel[]>;
|
|
24
34
|
export declare function usageUrl(codexBaseUrl: string): string;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChatGPT 后端的只读查询([W6-O]):模型列表(`ama models discover chatgpt`)与 codex 的 `wham/usage`。
|
|
3
3
|
*
|
|
4
|
-
* - SIWC:`GET {base}/models`,只留 `visibility === "list"
|
|
5
|
-
* - codex:`GET {base}/models?client_version=<
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
4
|
+
* - SIWC:`GET {base}/models`,只留 `visibility === "list"`(没有该字段的也留);条目若带下述元数据字段同样取;
|
|
5
|
+
* - codex:`GET {base}/models?client_version=<Codex CLI 版本>`(`codexClientVersion()`;后端按每个模型的
|
|
6
|
+
* `minimal_client_version` 过滤,发 ama 自己的版本号会一个都拿不到),`models[].slug`(同样按 visibility 筛),
|
|
7
|
+
* 另取 `context_window`、`input_modalities`、`supported_reasoning_levels` 作元数据(后端给了就优先于 models.dev,
|
|
8
|
+
* 见 discovered-cache.ts;`max_context_window` 是可调上限而非生效值、`auto_compact_token_limit` 没有对应的模型字段,
|
|
9
|
+
* 都不取);`GET https://chatgpt.com/backend-api/wham/usage`(由 codex base 推出),私有格式,解析失败返回 undefined。
|
|
10
|
+
* 请求带 Bearer;codex 另带 `ChatGPT-Account-ID` 与 `originator`。
|
|
8
11
|
*/
|
|
9
12
|
import { parseUsagePayload } from "../../ai/apis/chatgpt-rate-limits.js";
|
|
10
|
-
import {
|
|
11
|
-
import { DEFAULT_ORIGINATOR } from "./presets.js";
|
|
13
|
+
import { DEFAULT_CODEX_CLIENT_VERSION, DEFAULT_ORIGINATOR } from "./presets.js";
|
|
12
14
|
export const BACKEND_TIMEOUT_MS = 15_000;
|
|
13
15
|
function headers(auth) {
|
|
14
16
|
const out = {
|
|
@@ -26,10 +28,37 @@ function listed(item) {
|
|
|
26
28
|
const visibility = item["visibility"];
|
|
27
29
|
return visibility === undefined || visibility === "list";
|
|
28
30
|
}
|
|
31
|
+
/** 模型条目里的元数据;字段缺失或形状不对就不填。 */
|
|
32
|
+
function backendMetadata(item) {
|
|
33
|
+
const out = {};
|
|
34
|
+
const window = item["context_window"];
|
|
35
|
+
if (typeof window === "number" && Number.isInteger(window) && window > 0)
|
|
36
|
+
out.contextWindow = window;
|
|
37
|
+
const modalities = item["input_modalities"];
|
|
38
|
+
if (Array.isArray(modalities)) {
|
|
39
|
+
const input = ["text", "image"].filter((x) => modalities.includes(x));
|
|
40
|
+
if (input.includes("text"))
|
|
41
|
+
out.input = input;
|
|
42
|
+
}
|
|
43
|
+
const levels = item["supported_reasoning_levels"];
|
|
44
|
+
if (Array.isArray(levels)) {
|
|
45
|
+
const efforts = levels
|
|
46
|
+
.map((level) => typeof level === "string"
|
|
47
|
+
? level
|
|
48
|
+
: typeof level === "object" && level !== null
|
|
49
|
+
? level["effort"]
|
|
50
|
+
: undefined)
|
|
51
|
+
.filter((x) => typeof x === "string" && x !== "");
|
|
52
|
+
if (efforts.length > 0)
|
|
53
|
+
out.reasoningLevels = [...new Set(efforts)];
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
29
57
|
export async function listChatGptModels(fetchFn, baseUrl, auth) {
|
|
30
58
|
const base = baseUrl.replace(/\/+$/, "");
|
|
59
|
+
const version = auth.clientVersion ?? DEFAULT_CODEX_CLIENT_VERSION;
|
|
31
60
|
const url = auth.flavor === "codex"
|
|
32
|
-
? `${base}/models?client_version=${encodeURIComponent(
|
|
61
|
+
? `${base}/models?client_version=${encodeURIComponent(version)}`
|
|
33
62
|
: `${base}/models`;
|
|
34
63
|
const response = await fetchFn(url, {
|
|
35
64
|
headers: headers(auth),
|
|
@@ -49,7 +78,11 @@ export async function listChatGptModels(fetchFn, baseUrl, auth) {
|
|
|
49
78
|
continue;
|
|
50
79
|
seen.add(id);
|
|
51
80
|
const name = item["display_name"];
|
|
52
|
-
out.push(
|
|
81
|
+
out.push({
|
|
82
|
+
id,
|
|
83
|
+
...(typeof name === "string" && name !== "" ? { name } : {}),
|
|
84
|
+
...backendMetadata(item),
|
|
85
|
+
});
|
|
53
86
|
}
|
|
54
87
|
return out;
|
|
55
88
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* - login:flavor 取 `--flavor` > 用户级 `auth.chatgpt.flavor` > siwc;`--paste` / `--device`(只 codex)/ 缺省
|
|
5
5
|
* 浏览器;codex 首次在 TTY 下一次性确认「非官方、仅个人使用」(非 TTY 需 `--yes`),`acknowledgedAt` 入条目;
|
|
6
6
|
* 成功后在刷新锁下写 auth.json(0600)。
|
|
7
|
-
*
|
|
7
|
+
* 成功后先删旧的发现缓存,再查一次模型列表写缓存(`<dataDir>/models/discovered/chatgpt.json`;0 个也写空表并带
|
|
8
|
+
* flavor,免得残留另一种登录方式的缓存;查询失败静默)。codex 返回 0 个时提示 codexClientVersion 可能过旧。
|
|
8
9
|
* - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
|
|
9
10
|
* 同时删发现缓存。
|
|
10
11
|
* - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
|
package/dist/auth/chatgpt/cli.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* - login:flavor 取 `--flavor` > 用户级 `auth.chatgpt.flavor` > siwc;`--paste` / `--device`(只 codex)/ 缺省
|
|
5
5
|
* 浏览器;codex 首次在 TTY 下一次性确认「非官方、仅个人使用」(非 TTY 需 `--yes`),`acknowledgedAt` 入条目;
|
|
6
6
|
* 成功后在刷新锁下写 auth.json(0600)。
|
|
7
|
-
*
|
|
7
|
+
* 成功后先删旧的发现缓存,再查一次模型列表写缓存(`<dataDir>/models/discovered/chatgpt.json`;0 个也写空表并带
|
|
8
|
+
* flavor,免得残留另一种登录方式的缓存;查询失败静默)。codex 返回 0 个时提示 codexClientVersion 可能过旧。
|
|
8
9
|
* - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
|
|
9
10
|
* 同时删发现缓存。
|
|
10
11
|
* - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
|
|
@@ -25,7 +26,7 @@ import { clearDiscoveredCache, writeDiscoveredCache } from "../../ai/providers/d
|
|
|
25
26
|
import { fetchCodexUsage, listChatGptModels } from "./backend-client.js";
|
|
26
27
|
import { maskEmail } from "./claims.js";
|
|
27
28
|
import { loginChatGpt, revokeChatGpt } from "./login.js";
|
|
28
|
-
import { CHATGPT_PROVIDER_ID, chatgptBaseUrl } from "./presets.js";
|
|
29
|
+
import { CHATGPT_PROVIDER_ID, chatgptBaseUrl, codexClientVersion } from "./presets.js";
|
|
29
30
|
import { quotaParts } from "./quota-text.js";
|
|
30
31
|
function userAuthConfig(io) {
|
|
31
32
|
try {
|
|
@@ -186,9 +187,14 @@ export async function runLogin(args, io, deps = {}) {
|
|
|
186
187
|
if (entry.planType === "free")
|
|
187
188
|
io.stdout(`${m.freePlanHint(entry.planType)}\n`);
|
|
188
189
|
if (flavor === "siwc")
|
|
189
|
-
io.stdout(`${m.siwcLimitHint}\n`);
|
|
190
|
+
io.stdout(`${m.siwcLimitHint}\n${m.siwcEligibilityHint}\n`);
|
|
190
191
|
const count = await cacheModels(io, entry, config, deps);
|
|
191
|
-
|
|
192
|
+
const done = count === 0 && flavor === "codex"
|
|
193
|
+
? m.codexNoModels(codexClientVersion(config, io.env))
|
|
194
|
+
: count === undefined || count === 0
|
|
195
|
+
? m.modelHint
|
|
196
|
+
: m.modelsCached(count);
|
|
197
|
+
io.stdout(`${done}\n`);
|
|
192
198
|
return ExitCode.Ok;
|
|
193
199
|
}
|
|
194
200
|
/**
|
|
@@ -196,16 +202,17 @@ export async function runLogin(args, io, deps = {}) {
|
|
|
196
202
|
* 任何失败静默(返回 undefined,调用方改提示 `ama models discover chatgpt`)。
|
|
197
203
|
*/
|
|
198
204
|
async function cacheModels(io, entry, config, deps) {
|
|
205
|
+
const dataDir = resolveDataDir({ env: io.env });
|
|
199
206
|
try {
|
|
207
|
+
// 先删旧缓存:换了登录方式时另一 flavor 的缓存不能留下
|
|
208
|
+
clearDiscoveredCache(dataDir, CHATGPT_PROVIDER_ID);
|
|
200
209
|
const models = await listChatGptModels(deps.fetch ?? fetch, chatgptBaseUrl(entry.flavor, io.env), {
|
|
201
210
|
flavor: entry.flavor,
|
|
202
211
|
accessToken: entry.accessToken,
|
|
203
212
|
accountId: entry.accountId,
|
|
204
213
|
originator: config?.originator,
|
|
214
|
+
clientVersion: codexClientVersion(config, io.env),
|
|
205
215
|
});
|
|
206
|
-
if (models.length === 0)
|
|
207
|
-
return undefined;
|
|
208
|
-
const dataDir = resolveDataDir({ env: io.env });
|
|
209
216
|
writeDiscoveredCache(dataDir, CHATGPT_PROVIDER_ID, { models, flavor: entry.flavor });
|
|
210
217
|
return models.length;
|
|
211
218
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT 订阅:请求渠道跟随当前登录方式。
|
|
3
|
+
*
|
|
4
|
+
* `siwc` 与 `codex` 两条渠道协议相同(openai-responses),只差端点、请求头与请求体白名单。会话里的模型是启动
|
|
5
|
+
* (或选模型)时按当时的登录物化的;用户中途 `ama auth logout` 再换另一种方式登录后,同一会话的下一次请求
|
|
6
|
+
* 拿到的 token 属于新的 flavor。这里在**请求时**按 token 的 flavor(活 token 登记)把模型改到对应渠道:
|
|
7
|
+
*
|
|
8
|
+
* - 模型引用没有显式 `@渠道`(`channelPinned` 不为 true)→ 改到登录 flavor 的渠道(端点、渠道头、compat);
|
|
9
|
+
* - 显式写了另一渠道 → 不动,协议层报 `chatgpt_flavor_mismatch`(chatgpt-backend.ts);
|
|
10
|
+
* - 不是 ChatGPT 后端、token 不是 OAuth、flavor 已一致 → 原样返回。
|
|
11
|
+
*
|
|
12
|
+
* 会话里记录的模型(`model_change`)不算显式:恢复时由 startup-steps 去掉渠道再解析。
|
|
13
|
+
*/
|
|
14
|
+
import type { Model, ProviderData } from "../../ai/types.js";
|
|
15
|
+
/** 按 `apiKey` 对应登录的 flavor 改写模型的渠道;无需改动时返回原对象。 */
|
|
16
|
+
export declare function followChatGptLogin(model: Model, provider: ProviderData | undefined, apiKey: string | undefined): Model;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT 订阅:请求渠道跟随当前登录方式。
|
|
3
|
+
*
|
|
4
|
+
* `siwc` 与 `codex` 两条渠道协议相同(openai-responses),只差端点、请求头与请求体白名单。会话里的模型是启动
|
|
5
|
+
* (或选模型)时按当时的登录物化的;用户中途 `ama auth logout` 再换另一种方式登录后,同一会话的下一次请求
|
|
6
|
+
* 拿到的 token 属于新的 flavor。这里在**请求时**按 token 的 flavor(活 token 登记)把模型改到对应渠道:
|
|
7
|
+
*
|
|
8
|
+
* - 模型引用没有显式 `@渠道`(`channelPinned` 不为 true)→ 改到登录 flavor 的渠道(端点、渠道头、compat);
|
|
9
|
+
* - 显式写了另一渠道 → 不动,协议层报 `chatgpt_flavor_mismatch`(chatgpt-backend.ts);
|
|
10
|
+
* - 不是 ChatGPT 后端、token 不是 OAuth、flavor 已一致 → 原样返回。
|
|
11
|
+
*
|
|
12
|
+
* 会话里记录的模型(`model_change`)不算显式:恢复时由 startup-steps 去掉渠道再解析。
|
|
13
|
+
*/
|
|
14
|
+
import { liveToken } from "../oauth/live.js";
|
|
15
|
+
function backendOf(compat) {
|
|
16
|
+
if (typeof compat !== "object" || compat === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const value = compat.chatgptBackend;
|
|
19
|
+
return value === "siwc" || value === "codex" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** 按 `apiKey` 对应登录的 flavor 改写模型的渠道;无需改动时返回原对象。 */
|
|
22
|
+
export function followChatGptLogin(model, provider, apiKey) {
|
|
23
|
+
const current = backendOf(model.compat);
|
|
24
|
+
if (current === undefined || model.channelPinned === true)
|
|
25
|
+
return model;
|
|
26
|
+
const flavor = liveToken(apiKey)?.flavor;
|
|
27
|
+
if (flavor === undefined || flavor === current)
|
|
28
|
+
return model;
|
|
29
|
+
const channels = provider?.channels ?? [];
|
|
30
|
+
const target = channels.find((c) => backendOf(c.compat) === flavor) ?? channels.find((c) => c.name === flavor);
|
|
31
|
+
if (target === undefined)
|
|
32
|
+
return model;
|
|
33
|
+
const previous = channels.find((c) => c.name === model.channel);
|
|
34
|
+
// 去掉旧渠道带来的头(codex 的 originator 等),换上新渠道的
|
|
35
|
+
const headers = { ...model.headers };
|
|
36
|
+
for (const name of Object.keys(previous?.headers ?? {}))
|
|
37
|
+
delete headers[name];
|
|
38
|
+
Object.assign(headers, target.headers);
|
|
39
|
+
const next = {
|
|
40
|
+
...model,
|
|
41
|
+
api: target.api,
|
|
42
|
+
baseUrl: target.baseUrl,
|
|
43
|
+
channel: target.name,
|
|
44
|
+
compat: { ...model.compat, ...target.compat, chatgptBackend: flavor },
|
|
45
|
+
};
|
|
46
|
+
if (Object.keys(headers).length > 0)
|
|
47
|
+
next.headers = headers;
|
|
48
|
+
else
|
|
49
|
+
delete next.headers;
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 覆盖:`AMA_CHATGPT_CLIENT_ID` / `AMA_CHATGPT_ISSUER`(测试用)/ `AMA_CHATGPT_BASE_URL` > 配置
|
|
11
11
|
* `auth.chatgpt.*` > 条目里签发的 client id > 缺省。
|
|
12
|
+
*
|
|
13
|
+
* codex 后端的模型列表按 `client_version` 过滤(每个模型带 `minimal_client_version`,版本过低一个都不给):
|
|
14
|
+
* 发的是 Codex CLI 的版本号而不是 ama 的,`AMA_CHATGPT_CODEX_CLIENT_VERSION` > `auth.chatgpt.codexClientVersion`
|
|
15
|
+
* > `DEFAULT_CODEX_CLIENT_VERSION`。推理请求不带版本(Codex 自己也只在 User-Agent 里带)。
|
|
12
16
|
*/
|
|
13
17
|
import type { ChatGptAuthConfig, ChatGptFlavor, OAuthAuthEntry } from "../../config/types-w6.js";
|
|
14
18
|
export declare const CHATGPT_PROVIDER_ID = "chatgpt";
|
|
@@ -19,6 +23,8 @@ export declare const SIWC_RESOURCE = "https://api.openai.com/v1";
|
|
|
19
23
|
export declare const SIWC_REQUIRED_SCOPE = "chatgpt.tokens.use.direct";
|
|
20
24
|
export declare const AGENT_NAME_HINT = "ama";
|
|
21
25
|
export declare const DEFAULT_ORIGINATOR = "codex_cli_rs";
|
|
26
|
+
/** codex 后端 `GET /models?client_version=` 用的 Codex CLI 版本号(模型列表按它过滤)。 */
|
|
27
|
+
export declare const DEFAULT_CODEX_CLIENT_VERSION = "0.160.0";
|
|
22
28
|
export declare const CHATGPT_BASE_URLS: Readonly<Record<ChatGptFlavor, string>>;
|
|
23
29
|
export interface AuthorizeInput {
|
|
24
30
|
redirectUri: string;
|
|
@@ -61,6 +67,8 @@ export interface PresetOverrides {
|
|
|
61
67
|
port?: number | undefined;
|
|
62
68
|
}
|
|
63
69
|
export declare function chatgptBaseUrl(flavor: ChatGptFlavor, env?: Readonly<Record<string, string | undefined>>): string;
|
|
70
|
+
/** codex 模型列表的 `client_version`:环境变量 > 用户级配置 > 缺省。 */
|
|
71
|
+
export declare function codexClientVersion(config?: ChatGptAuthConfig, env?: Readonly<Record<string, string | undefined>>): string;
|
|
64
72
|
export declare function resolveChatGptPreset(flavor: ChatGptFlavor, overrides?: PresetOverrides): ChatGptPreset;
|
|
65
73
|
/** 授权 URL(参数顺序固定,便于测试)。 */
|
|
66
74
|
export declare function authorizeUrl(preset: ChatGptPreset, input: AuthorizeInput): string;
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 覆盖:`AMA_CHATGPT_CLIENT_ID` / `AMA_CHATGPT_ISSUER`(测试用)/ `AMA_CHATGPT_BASE_URL` > 配置
|
|
11
11
|
* `auth.chatgpt.*` > 条目里签发的 client id > 缺省。
|
|
12
|
+
*
|
|
13
|
+
* codex 后端的模型列表按 `client_version` 过滤(每个模型带 `minimal_client_version`,版本过低一个都不给):
|
|
14
|
+
* 发的是 Codex CLI 的版本号而不是 ama 的,`AMA_CHATGPT_CODEX_CLIENT_VERSION` > `auth.chatgpt.codexClientVersion`
|
|
15
|
+
* > `DEFAULT_CODEX_CLIENT_VERSION`。推理请求不带版本(Codex 自己也只在 User-Agent 里带)。
|
|
12
16
|
*/
|
|
13
17
|
export const CHATGPT_PROVIDER_ID = "chatgpt";
|
|
14
18
|
export const DEFAULT_ISSUER = "https://auth.openai.com";
|
|
@@ -18,6 +22,8 @@ export const SIWC_RESOURCE = "https://api.openai.com/v1";
|
|
|
18
22
|
export const SIWC_REQUIRED_SCOPE = "chatgpt.tokens.use.direct";
|
|
19
23
|
export const AGENT_NAME_HINT = "ama";
|
|
20
24
|
export const DEFAULT_ORIGINATOR = "codex_cli_rs";
|
|
25
|
+
/** codex 后端 `GET /models?client_version=` 用的 Codex CLI 版本号(模型列表按它过滤)。 */
|
|
26
|
+
export const DEFAULT_CODEX_CLIENT_VERSION = "0.160.0";
|
|
21
27
|
export const CHATGPT_BASE_URLS = {
|
|
22
28
|
siwc: "https://api.openai.com/v1",
|
|
23
29
|
codex: "https://chatgpt.com/backend-api/codex",
|
|
@@ -29,6 +35,12 @@ function nonEmpty(value) {
|
|
|
29
35
|
export function chatgptBaseUrl(flavor, env = process.env) {
|
|
30
36
|
return nonEmpty(env["AMA_CHATGPT_BASE_URL"]) ?? CHATGPT_BASE_URLS[flavor];
|
|
31
37
|
}
|
|
38
|
+
/** codex 模型列表的 `client_version`:环境变量 > 用户级配置 > 缺省。 */
|
|
39
|
+
export function codexClientVersion(config, env = process.env) {
|
|
40
|
+
return (nonEmpty(env["AMA_CHATGPT_CODEX_CLIENT_VERSION"]) ??
|
|
41
|
+
nonEmpty(config?.codexClientVersion) ??
|
|
42
|
+
DEFAULT_CODEX_CLIENT_VERSION);
|
|
43
|
+
}
|
|
32
44
|
export function resolveChatGptPreset(flavor, overrides = {}) {
|
|
33
45
|
const env = overrides.env ?? process.env;
|
|
34
46
|
const config = overrides.config;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* OAuth:authorize(记参数、302 回 redirect_uri;SIWC 注册时带签发的 client_id 与 scope)、token(校验 PKCE;
|
|
5
5
|
* 授权码 / 刷新;刷新轮换,旧 refresh token 再用 → `refresh_token_reused`)、deviceauth、discovery + JWKS
|
|
6
6
|
* (现场生成 RSA 密钥对)、revoke。后端:`/v1/responses`、`/codex/responses`、`/v1/models`、`/codex/models`、
|
|
7
|
-
* `/wham/usage
|
|
7
|
+
* `/wham/usage`,按脚本回放;给了 `codexModels` 时 `/codex/models` 改按 `client_version` 过滤(不带 → 400,
|
|
8
|
+
* 低于条目的 `minimal_client_version` 不给),与真实 codex 后端一致。
|
|
8
9
|
*/
|
|
9
10
|
export interface RecordedRequest {
|
|
10
11
|
method: string;
|
|
@@ -37,6 +38,8 @@ export interface FakeOAuthOptions {
|
|
|
37
38
|
/** 设备码轮询先返回几次 403。 */
|
|
38
39
|
devicePending?: number;
|
|
39
40
|
marker?: string;
|
|
41
|
+
/** codex 模型目录(`/codex/models` 按 `client_version` 过滤后返回 `{ models }`)。 */
|
|
42
|
+
codexModels?: Record<string, unknown>[];
|
|
40
43
|
}
|
|
41
44
|
export declare class FakeOAuthServer {
|
|
42
45
|
readonly options: FakeOAuthOptions;
|
|
@@ -4,10 +4,22 @@
|
|
|
4
4
|
* OAuth:authorize(记参数、302 回 redirect_uri;SIWC 注册时带签发的 client_id 与 scope)、token(校验 PKCE;
|
|
5
5
|
* 授权码 / 刷新;刷新轮换,旧 refresh token 再用 → `refresh_token_reused`)、deviceauth、discovery + JWKS
|
|
6
6
|
* (现场生成 RSA 密钥对)、revoke。后端:`/v1/responses`、`/codex/responses`、`/v1/models`、`/codex/models`、
|
|
7
|
-
* `/wham/usage
|
|
7
|
+
* `/wham/usage`,按脚本回放;给了 `codexModels` 时 `/codex/models` 改按 `client_version` 过滤(不带 → 400,
|
|
8
|
+
* 低于条目的 `minimal_client_version` 不给),与真实 codex 后端一致。
|
|
8
9
|
*/
|
|
9
10
|
import { createHash, generateKeyPairSync, randomBytes, sign } from "node:crypto";
|
|
10
11
|
import { createServer } from "node:http";
|
|
12
|
+
/** `a.b.c` 比较;非数字段按 0。 */
|
|
13
|
+
function compareVersion(a, b) {
|
|
14
|
+
const pa = a.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
15
|
+
const pb = b.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
16
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
17
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
18
|
+
if (d !== 0)
|
|
19
|
+
return d;
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
11
23
|
function b64url(value) {
|
|
12
24
|
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
13
25
|
}
|
|
@@ -212,6 +224,17 @@ export class FakeOAuthServer {
|
|
|
212
224
|
code_challenge: "x",
|
|
213
225
|
});
|
|
214
226
|
}
|
|
227
|
+
case "/codex/models": {
|
|
228
|
+
const catalog = this.options.codexModels;
|
|
229
|
+
if (catalog === undefined)
|
|
230
|
+
return this.backend(res);
|
|
231
|
+
const version = url.searchParams.get("client_version");
|
|
232
|
+
if (version === null)
|
|
233
|
+
return json(400, { error: { message: "client_version required" } });
|
|
234
|
+
return json(200, {
|
|
235
|
+
models: catalog.filter((m) => compareVersion(version, String(m["minimal_client_version"] ?? "0")) >= 0),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
215
238
|
case "/revoke":
|
|
216
239
|
this.revoked.push(params()["token"] ?? "");
|
|
217
240
|
res.writeHead(200).end();
|