@armadra/agent 0.6.0 → 0.6.2

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CHANGELOG.zh-CN.md +32 -0
  3. package/dist/agent/session-settings.d.ts +4 -1
  4. package/dist/agent/session-settings.js +7 -2
  5. package/dist/ai/apis/chatgpt-backend.d.ts +7 -2
  6. package/dist/ai/apis/chatgpt-backend.js +12 -4
  7. package/dist/ai/providers/discovered-cache.d.ts +59 -0
  8. package/dist/ai/providers/discovered-cache.js +146 -0
  9. package/dist/ai/providers/model-visibility.d.ts +33 -0
  10. package/dist/ai/providers/model-visibility.js +70 -0
  11. package/dist/ai/providers/registry.d.ts +2 -0
  12. package/dist/ai/providers/registry.js +13 -7
  13. package/dist/ai/types.d.ts +5 -0
  14. package/dist/auth/chatgpt/backend-client.d.ts +11 -2
  15. package/dist/auth/chatgpt/backend-client.js +38 -6
  16. package/dist/auth/chatgpt/cli.d.ts +4 -1
  17. package/dist/auth/chatgpt/cli.js +40 -6
  18. package/dist/auth/chatgpt/follow.d.ts +16 -0
  19. package/dist/auth/chatgpt/follow.js +51 -0
  20. package/dist/auth/chatgpt/presets.d.ts +8 -0
  21. package/dist/auth/chatgpt/presets.js +12 -0
  22. package/dist/auth/testing/fake-oauth.d.ts +4 -1
  23. package/dist/auth/testing/fake-oauth.js +24 -1
  24. package/dist/bundle/ama.cjs +1663 -799
  25. package/dist/cli/compose-providers.d.ts +2 -0
  26. package/dist/cli/compose-providers.js +9 -0
  27. package/dist/cli/startup-steps.js +4 -1
  28. package/dist/cli/subcommands/config-set.js +19 -11
  29. package/dist/cli/subcommands/models-discover.d.ts +4 -1
  30. package/dist/cli/subcommands/models-discover.js +51 -3
  31. package/dist/cli/subcommands/models-enable.d.ts +13 -0
  32. package/dist/cli/subcommands/models-enable.js +145 -0
  33. package/dist/cli/subcommands/models.d.ts +2 -0
  34. package/dist/cli/subcommands/models.js +10 -2
  35. package/dist/config/json-schema.js +2 -1
  36. package/dist/config/key-docs.js +7 -1
  37. package/dist/config/schema-w5.js +9 -2
  38. package/dist/config/schema-w6.d.ts +1 -1
  39. package/dist/config/schema-w6.js +2 -1
  40. package/dist/config/settings-registry.d.ts +3 -0
  41. package/dist/config/settings-registry.js +1 -0
  42. package/dist/config/types-w5.d.ts +7 -0
  43. package/dist/config/types-w5.js +2 -0
  44. package/dist/config/types-w6.d.ts +5 -0
  45. package/dist/i18n/catalog.d.ts +50 -3
  46. package/dist/i18n/messages/auth.d.ts +16 -2
  47. package/dist/i18n/messages/auth.js +34 -2
  48. package/dist/i18n/messages/cli-help.js +4 -0
  49. package/dist/i18n/messages/config-keys.d.ts +6 -0
  50. package/dist/i18n/messages/config-keys.js +8 -2
  51. package/dist/i18n/messages/config.d.ts +8 -0
  52. package/dist/i18n/messages/config.js +2 -0
  53. package/dist/i18n/messages/errors.d.ts +1 -1
  54. package/dist/i18n/messages/interactive-startup.d.ts +0 -4
  55. package/dist/i18n/messages/interactive-startup.js +0 -4
  56. package/dist/i18n/messages/interactive.d.ts +0 -4
  57. package/dist/i18n/messages/panels.d.ts +43 -0
  58. package/dist/i18n/messages/panels.js +43 -0
  59. package/dist/i18n/messages/settings.d.ts +7 -0
  60. package/dist/i18n/messages/settings.js +7 -0
  61. package/dist/i18n/messages/subcommands-config.d.ts +2 -0
  62. package/dist/i18n/messages/subcommands-config.js +2 -0
  63. package/dist/i18n/messages/subcommands-models.d.ts +34 -0
  64. package/dist/i18n/messages/subcommands-models.js +34 -0
  65. package/dist/i18n/messages/subcommands.d.ts +28 -0
  66. package/dist/i18n/messages/subcommands.js +3 -0
  67. package/dist/modes/interactive/commands.d.ts +3 -0
  68. package/dist/modes/interactive/commands.js +18 -11
  69. package/dist/modes/interactive/config-ui.js +1 -1
  70. package/dist/modes/interactive/interactive-mode.js +2 -0
  71. package/dist/modes/interactive/model-items.d.ts +45 -0
  72. package/dist/modes/interactive/model-items.js +134 -0
  73. package/dist/modes/interactive/model-picker.d.ts +57 -0
  74. package/dist/modes/interactive/model-picker.js +191 -0
  75. package/dist/modes/interactive/startup-ui.d.ts +3 -9
  76. package/dist/modes/interactive/startup-ui.js +8 -60
  77. package/dist/tui/components/select-list.d.ts +2 -0
  78. package/dist/tui/components/select-list.js +7 -0
  79. package/docs/en/providers.md +9 -3
  80. package/docs/en/tui.md +8 -1
  81. package/docs/providers.md +27 -6
  82. package/docs/tui.md +8 -1
  83. package/package.json +1 -1
@@ -2,13 +2,14 @@
2
2
  * ChatGPT 后端的只读查询([W6-O]):模型列表(`ama models discover chatgpt`)与 codex 的 `wham/usage`。
3
3
  *
4
4
  * - SIWC:`GET {base}/models`,只留 `visibility === "list"`(没有该字段的也留);
5
- * - codex:`GET {base}/models?client_version=<ama 版本>`,`models[].slug`(同样按 visibility 筛);
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 补不到时用);
6
8
  * `GET https://chatgpt.com/backend-api/wham/usage`(由 codex base 推出),私有格式,解析失败返回 undefined。
7
- * 只取 slug 与显示名,上下文窗口不猜。请求带 Bearer;codex 另带 `ChatGPT-Account-ID` 与 `originator`。
9
+ * SIWC 只取 slug 与显示名。请求带 Bearer;codex 另带 `ChatGPT-Account-ID` 与 `originator`。
8
10
  */
9
11
  import { parseUsagePayload } from "../../ai/apis/chatgpt-rate-limits.js";
10
- import { AMA_VERSION } from "../../version.js";
11
- import { DEFAULT_ORIGINATOR } from "./presets.js";
12
+ import { DEFAULT_CODEX_CLIENT_VERSION, DEFAULT_ORIGINATOR } from "./presets.js";
12
13
  export const BACKEND_TIMEOUT_MS = 15_000;
13
14
  function headers(auth) {
14
15
  const out = {
@@ -26,10 +27,37 @@ function listed(item) {
26
27
  const visibility = item["visibility"];
27
28
  return visibility === undefined || visibility === "list";
28
29
  }
30
+ /** codex 模型条目里的元数据;字段缺失或形状不对就不填。 */
31
+ function codexMetadata(item) {
32
+ const out = {};
33
+ const window = item["context_window"];
34
+ if (typeof window === "number" && Number.isInteger(window) && window > 0)
35
+ out.contextWindow = window;
36
+ const modalities = item["input_modalities"];
37
+ if (Array.isArray(modalities)) {
38
+ const input = ["text", "image"].filter((x) => modalities.includes(x));
39
+ if (input.includes("text"))
40
+ out.input = input;
41
+ }
42
+ const levels = item["supported_reasoning_levels"];
43
+ if (Array.isArray(levels)) {
44
+ const efforts = levels
45
+ .map((level) => typeof level === "string"
46
+ ? level
47
+ : typeof level === "object" && level !== null
48
+ ? level["effort"]
49
+ : undefined)
50
+ .filter((x) => typeof x === "string" && x !== "");
51
+ if (efforts.length > 0)
52
+ out.reasoningLevels = [...new Set(efforts)];
53
+ }
54
+ return out;
55
+ }
29
56
  export async function listChatGptModels(fetchFn, baseUrl, auth) {
30
57
  const base = baseUrl.replace(/\/+$/, "");
58
+ const version = auth.clientVersion ?? DEFAULT_CODEX_CLIENT_VERSION;
31
59
  const url = auth.flavor === "codex"
32
- ? `${base}/models?client_version=${encodeURIComponent(AMA_VERSION)}`
60
+ ? `${base}/models?client_version=${encodeURIComponent(version)}`
33
61
  : `${base}/models`;
34
62
  const response = await fetchFn(url, {
35
63
  headers: headers(auth),
@@ -49,7 +77,11 @@ export async function listChatGptModels(fetchFn, baseUrl, auth) {
49
77
  continue;
50
78
  seen.add(id);
51
79
  const name = item["display_name"];
52
- out.push(typeof name === "string" && name !== "" ? { id, name } : { id });
80
+ out.push({
81
+ id,
82
+ ...(typeof name === "string" && name !== "" ? { name } : {}),
83
+ ...(auth.flavor === "codex" ? codexMetadata(item) : {}),
84
+ });
53
85
  }
54
86
  return out;
55
87
  }
@@ -4,7 +4,10 @@
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
- * - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地。
7
+ * 成功后先删旧的发现缓存,再查一次模型列表写缓存(`<dataDir>/models/discovered/chatgpt.json`;0 个也写空表并带
8
+ * flavor,免得残留另一种登录方式的缓存;查询失败静默)。codex 返回 0 个时提示 codexClientVersion 可能过旧。
9
+ * - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
10
+ * 同时删发现缓存。
8
11
  * - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
9
12
  *
10
13
  * 输出里从不出现 token、code、账户 id;错误按码渲染(i18n),不回显响应原文。
@@ -4,7 +4,10 @@
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
- * - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地。
7
+ * 成功后先删旧的发现缓存,再查一次模型列表写缓存(`<dataDir>/models/discovered/chatgpt.json`;0 个也写空表并带
8
+ * flavor,免得残留另一种登录方式的缓存;查询失败静默)。codex 返回 0 个时提示 codexClientVersion 可能过旧。
9
+ * - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
10
+ * 同时删发现缓存。
8
11
  * - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
9
12
  *
10
13
  * 输出里从不出现 token、code、账户 id;错误按码渲染(i18n),不回显响应原文。
@@ -19,10 +22,11 @@ import { UsageError } from "../../cli/args.js";
19
22
  import { ExitCode } from "../../cli/exit-codes.js";
20
23
  import { openBrowser } from "../oauth/browser.js";
21
24
  import { readOAuthEntry, withRefreshLock, writeOAuthEntry } from "../oauth/token-store.js";
22
- import { fetchCodexUsage } from "./backend-client.js";
25
+ import { clearDiscoveredCache, writeDiscoveredCache } from "../../ai/providers/discovered-cache.js";
26
+ import { fetchCodexUsage, listChatGptModels } from "./backend-client.js";
23
27
  import { maskEmail } from "./claims.js";
24
28
  import { loginChatGpt, revokeChatGpt } from "./login.js";
25
- import { CHATGPT_PROVIDER_ID, chatgptBaseUrl } from "./presets.js";
29
+ import { CHATGPT_PROVIDER_ID, chatgptBaseUrl, codexClientVersion } from "./presets.js";
26
30
  import { quotaParts } from "./quota-text.js";
27
31
  function userAuthConfig(io) {
28
32
  try {
@@ -55,7 +59,7 @@ export function loginErrorText(error, flavor) {
55
59
  case "oauth_timeout":
56
60
  return m.timeout;
57
61
  case "oauth_denied":
58
- return m.denied(detail.error ?? "error");
62
+ return m.denied(detail.error ?? "error", flavor === "siwc");
59
63
  case "oauth_state_mismatch":
60
64
  return m.stateMismatch;
61
65
  case "oauth_no_code":
@@ -183,10 +187,39 @@ export async function runLogin(args, io, deps = {}) {
183
187
  if (entry.planType === "free")
184
188
  io.stdout(`${m.freePlanHint(entry.planType)}\n`);
185
189
  if (flavor === "siwc")
186
- io.stdout(`${m.siwcLimitHint}\n`);
187
- io.stdout(`${m.modelHint}\n`);
190
+ io.stdout(`${m.siwcLimitHint}\n${m.siwcEligibilityHint}\n`);
191
+ const count = await cacheModels(io, entry, config, deps);
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`);
188
198
  return ExitCode.Ok;
189
199
  }
200
+ /**
201
+ * 登录后查一次账户可用的模型(只读的模型列表接口,不消耗额度)并写发现缓存,`/model` 由此列出;
202
+ * 任何失败静默(返回 undefined,调用方改提示 `ama models discover chatgpt`)。
203
+ */
204
+ async function cacheModels(io, entry, config, deps) {
205
+ const dataDir = resolveDataDir({ env: io.env });
206
+ try {
207
+ // 先删旧缓存:换了登录方式时另一 flavor 的缓存不能留下
208
+ clearDiscoveredCache(dataDir, CHATGPT_PROVIDER_ID);
209
+ const models = await listChatGptModels(deps.fetch ?? fetch, chatgptBaseUrl(entry.flavor, io.env), {
210
+ flavor: entry.flavor,
211
+ accessToken: entry.accessToken,
212
+ accountId: entry.accountId,
213
+ originator: config?.originator,
214
+ clientVersion: codexClientVersion(config, io.env),
215
+ });
216
+ writeDiscoveredCache(dataDir, CHATGPT_PROVIDER_ID, { models, flavor: entry.flavor });
217
+ return models.length;
218
+ }
219
+ catch {
220
+ return undefined;
221
+ }
222
+ }
190
223
  export async function runLogout(args, io, deps = {}) {
191
224
  const m = msg().auth.logout;
192
225
  const provider = providerOf(args, "logout");
@@ -205,6 +238,7 @@ export async function runLogout(args, io, deps = {}) {
205
238
  io.stderr(`ama: ${loginErrorText(error, entry.flavor)}\n`);
206
239
  return ExitCode.RuntimeError;
207
240
  }
241
+ clearDiscoveredCache(resolveDataDir({ env: io.env }), provider);
208
242
  const text = revoked === undefined
209
243
  ? m.local(provider, args.path)
210
244
  : revoked
@@ -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();