@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
@@ -12,6 +12,8 @@
12
12
  * - [W6-O] ChatGPT 登录(docs/wave6-plan.md D14):`chatgpt` 的缺省渠道按 auth.json 条目的 flavor 定(用户写了
13
13
  * `defaultChannel` 时不动);`AMA_CHATGPT_BASE_URL` 改该渠道地址;`auth.chatgpt.originator` 改 codex 渠道的
14
14
  * `originator` 头;OAuth 刷新拿到 `auth.chatgpt` 配置与同一份 env。
15
+ * - 发现缓存(`<dataDir>/models/discovered/<provider>.json`)并入模型表为空的供应商(discovered-cache.ts);
16
+ * chatgpt 的缓存 flavor 与当前登录不符时视为过期、不并入。
15
17
  */
16
18
  import type { ApiRegistry } from "../ai/apis/api.js";
17
19
  import { ProviderRegistry } from "../ai/providers/registry.js";
@@ -12,9 +12,13 @@
12
12
  * - [W6-O] ChatGPT 登录(docs/wave6-plan.md D14):`chatgpt` 的缺省渠道按 auth.json 条目的 flavor 定(用户写了
13
13
  * `defaultChannel` 时不动);`AMA_CHATGPT_BASE_URL` 改该渠道地址;`auth.chatgpt.originator` 改 codex 渠道的
14
14
  * `originator` 头;OAuth 刷新拿到 `auth.chatgpt` 配置与同一份 env。
15
+ * - 发现缓存(`<dataDir>/models/discovered/<provider>.json`)并入模型表为空的供应商(discovered-cache.ts);
16
+ * chatgpt 的缓存 flavor 与当前登录不符时视为过期、不并入。
15
17
  */
16
18
  import { loadModelsDevIndex } from "../ai/providers/models-dev-cache.js";
17
19
  import { ProviderRegistry, discoverLocalModels } from "../ai/providers/registry.js";
20
+ import { mergeDiscoveredModels } from "../ai/providers/discovered-cache.js";
21
+ import { lazyIndex } from "../ai/providers/enrich.js";
18
22
  import { readOAuthEntry } from "../auth/oauth/token-store.js";
19
23
  import { CHATGPT_BASE_URLS, CHATGPT_PROVIDER_ID } from "../auth/chatgpt/presets.js";
20
24
  export const LOCAL_PROBE_TIMEOUT_MS = 300;
@@ -104,6 +108,11 @@ export async function buildProviderRegistry(input, options = {}) {
104
108
  if (provider !== undefined)
105
109
  registry = make({ provider, apiKey: cli.apiKey });
106
110
  }
111
+ // 发现缓存(`ama auth login chatgpt` / `ama models discover chatgpt`)并入模型表为空的供应商
112
+ if (dataDir !== undefined) {
113
+ const flavor = readOAuthEntry(input.authFile, CHATGPT_PROVIDER_ID)?.flavor;
114
+ mergeDiscoveredModels(registry, dataDir, lazyIndex(() => loadModelsDevIndex(dataDir)), (id) => (id === CHATGPT_PROVIDER_ID ? flavor : undefined));
115
+ }
107
116
  const probe = options.probeLocal ?? env[NO_LOCAL_PROBE_ENV] !== "1";
108
117
  if (probe && config.defaultModel === undefined && !anyKeyConfigured(registry)) {
109
118
  await probeLocalProviders(registry, options.probeTimeoutMs ?? LOCAL_PROBE_TIMEOUT_MS);
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { describeLookupFailure } from "../ai/providers/suggest.js";
6
6
  import { formatModelRef } from "../ai/providers/channels.js";
7
+ import { CHATGPT_PROVIDER_ID } from "../auth/chatgpt/presets.js";
7
8
  import { statSync } from "node:fs";
8
9
  import { isAbsolute, resolve } from "node:path";
9
10
  import { AmaError, StartupError, isAmaError } from "../errors.js";
@@ -126,10 +127,12 @@ export async function resolveModel(args, registry, manager, defaultModel, deps,
126
127
  else {
127
128
  const change = lastEntry(manager, (e) => e.type === "model_change");
128
129
  if (change?.type === "model_change") {
130
+ // ChatGPT 的渠道跟随当前登录方式:会话里记的渠道不当显式渠道(可能是换登录前的)
131
+ const channel = change.provider === CHATGPT_PROVIDER_ID ? undefined : change.channel;
129
132
  const found = registry.findModel(formatModelRef({
130
133
  provider: change.provider,
131
134
  id: change.modelId,
132
- ...(change.channel !== undefined ? { channel: change.channel } : {}),
135
+ ...(channel !== undefined ? { channel } : {}),
133
136
  }));
134
137
  if (found.ok)
135
138
  choice = { model: found.model, provider: found.provider };
@@ -62,16 +62,24 @@ function textRow(value) {
62
62
  function scopeLabel(scope) {
63
63
  return msg().settings.scope[scope];
64
64
  }
65
- function report(io, result, unset) {
65
+ /**
66
+ * Receipt of a write. When a higher layer (env, project, cli, profile) still hides the key, the first line shows the
67
+ * value just written and its layer, and the second line names the overriding source and the effective value.
68
+ */
69
+ function report(io, result, written) {
66
70
  const s = msg().settings;
67
- const value = formatSettingValue(result.after.value);
68
- io.stdout(`${unset
69
- ? s.unsetDone(result.key, value, scopeLabel(result.scope))
70
- : s.setDone(result.key, value, scopeLabel(result.scope))}\n`);
71
- if (result.overriddenBy !== undefined) {
72
- const by = result.after.envName ?? result.overriddenBy;
73
- io.stdout(`${s.stillOverridden(result.key, by)}\n`);
74
- }
71
+ const effective = formatSettingValue(result.after.value);
72
+ const scope = scopeLabel(result.scope);
73
+ const by = result.after.envName ?? result.overriddenBy;
74
+ if (written === undefined)
75
+ io.stdout(`${s.unsetDone(result.key, effective, scope)}\n`);
76
+ else if (by === undefined)
77
+ io.stdout(`${s.setDone(result.key, effective, scope)}\n`);
78
+ else
79
+ io.stdout(`${s.writtenTo(result.key, formatSettingValue(written), scope)}\n`);
80
+ if (by === undefined)
81
+ return;
82
+ io.stdout(`${written === undefined ? s.stillOverridden(result.key, by) : s.overriddenNow(by, effective)}\n`);
75
83
  }
76
84
  /** Confirm persisting `permission.mode full-auto`: TTY asks, otherwise `--yes` is required. */
77
85
  async function confirmBypass(io, yes, deps) {
@@ -118,7 +126,7 @@ export async function runConfigEdit(action, argv, io, deps = {}) {
118
126
  return ExitCode.Ok;
119
127
  }
120
128
  if (action === "unset") {
121
- report(io, setConfigValue({ ...input, scope, key, value: undefined }), true);
129
+ report(io, setConfigValue({ ...input, scope, key, value: undefined }), undefined);
122
130
  return ExitCode.Ok;
123
131
  }
124
132
  const raw = positionals.slice(1).join(" ");
@@ -132,6 +140,6 @@ export async function runConfigEdit(action, argv, io, deps = {}) {
132
140
  // first write to a fresh config dir: lay out config.json + config.schema.json like `ama init`
133
141
  if (scope === "user" && !existsSync(join(input.configDir, CONFIG_FILE)))
134
142
  initConfigDir(input.configDir);
135
- report(io, setConfigValue({ ...input, scope, key, value }), value === undefined);
143
+ report(io, setConfigValue({ ...input, scope, key, value }), value);
136
144
  return ExitCode.Ok;
137
145
  }
@@ -18,7 +18,9 @@
18
18
  * 匹配结果;`--write` 跳过 models.dev 标明不支持工具调用的模型。元数据不写进配置——运行时从缓存补。
19
19
  *
20
20
  * [W6-O] `chatgpt`:按登录的 flavor 调对应后端的模型列表(SIWC `GET /v1/models` 筛 `visibility: list`;codex
21
- * `GET /models?client_version=…`),只取 slug 与显示名;`--probe` 不适用(订阅后端只有一种协议)。
21
+ * `GET /models?client_version=<codexClientVersion>`,另带上下文窗口 / 输入模态 / 推理强度),取 slug 与显示名;
22
+ * `--probe` 不适用(订阅后端只有一种协议)。结果另写进发现缓存 `<dataDir>/models/discovered/chatgpt.json`
23
+ * (discovered-cache.ts,0 个也写),`/model` 选择器由此列出;codex 返回 0 个时提示 codexClientVersion 可能过旧。
22
24
  */
23
25
  import type { Api, Model, ProviderData, ProviderRegistryApi } from "../../ai/types.js";
24
26
  import type { ModelsAction } from "./models.js";
@@ -30,6 +32,7 @@ export declare const PROBE_APIS: readonly Api[];
30
32
  export declare function modelsUrl(provider: ProviderData): string;
31
33
  export declare function discoverModels(provider: ProviderData, apiKey: string | undefined, options?: {
32
34
  timeoutMs?: number;
35
+ codexClientVersion?: string;
33
36
  }): Promise<Model[]>;
34
37
  export declare function probeOrder(provider: ProviderData): Api[];
35
38
  export interface ProbeOptions {
@@ -18,13 +18,16 @@
18
18
  * 匹配结果;`--write` 跳过 models.dev 标明不支持工具调用的模型。元数据不写进配置——运行时从缓存补。
19
19
  *
20
20
  * [W6-O] `chatgpt`:按登录的 flavor 调对应后端的模型列表(SIWC `GET /v1/models` 筛 `visibility: list`;codex
21
- * `GET /models?client_version=…`),只取 slug 与显示名;`--probe` 不适用(订阅后端只有一种协议)。
21
+ * `GET /models?client_version=<codexClientVersion>`,另带上下文窗口 / 输入模态 / 推理强度),取 slug 与显示名;
22
+ * `--probe` 不适用(订阅后端只有一种协议)。结果另写进发现缓存 `<dataDir>/models/discovered/chatgpt.json`
23
+ * (discovered-cache.ts,0 个也写),`/model` 选择器由此列出;codex 返回 0 个时提示 codexClientVersion 可能过旧。
22
24
  */
23
25
  import { authHeaders, mergeHeaders } from "../../ai/http.js";
24
26
  import { listChatGptModels } from "../../auth/chatgpt/backend-client.js";
25
- import { CHATGPT_PROVIDER_ID } from "../../auth/chatgpt/presets.js";
27
+ import { CHATGPT_PROVIDER_ID, codexClientVersion } from "../../auth/chatgpt/presets.js";
26
28
  import { liveToken } from "../../auth/oauth/live.js";
27
29
  import { discoverLocalModels, materializeModel } from "../../ai/providers/registry.js";
30
+ import { thinkingMapOf, writeDiscoveredCache, } from "../../ai/providers/discovered-cache.js";
28
31
  import { withCustomDefaults } from "../../ai/providers/catalog.js";
29
32
  import { describeModelsDev, loadModelsDevIndex } from "../../ai/providers/models-dev-cache.js";
30
33
  import { matchLabel, modelsDevFields } from "../../ai/providers/models-dev.js";
@@ -63,11 +66,20 @@ export async function discoverModels(provider, apiKey, options = {}) {
63
66
  accessToken: apiKey,
64
67
  accountId: live?.accountId,
65
68
  originator: channel?.headers?.["originator"],
69
+ clientVersion: options.codexClientVersion,
66
70
  });
67
71
  return models.map((m) => {
68
72
  const model = withCustomDefaults({ id: m.id }, provider.id, provider.api);
69
73
  if (m.name !== undefined)
70
74
  model.name = m.name;
75
+ if (m.contextWindow !== undefined)
76
+ model.contextWindow = m.contextWindow;
77
+ if (m.input !== undefined)
78
+ model.input = m.input;
79
+ if (m.reasoningLevels?.some((x) => x !== "none") === true) {
80
+ model.reasoning = true;
81
+ model.thinkingLevelMap = thinkingMapOf(m.reasoningLevels);
82
+ }
71
83
  return model;
72
84
  });
73
85
  }
@@ -189,7 +201,9 @@ async function run(ctx) {
189
201
  }
190
202
  let found;
191
203
  try {
192
- found = await discoverModels(provider, key.apiKey);
204
+ found = await discoverModels(provider, key.apiKey, {
205
+ codexClientVersion: codexClientVersion(ctx.level.merged.config.auth?.chatgpt, io.env),
206
+ });
193
207
  }
194
208
  catch (error) {
195
209
  io.stderr(m.listFailed(provider.id, error.message));
@@ -197,6 +211,8 @@ async function run(ctx) {
197
211
  }
198
212
  const configured = new Map(provider.models.map((m) => [m.id, m]));
199
213
  io.stdout(m.found(provider.id, found.length, modelsUrl(provider)));
214
+ if (provider.id === CHATGPT_PROVIDER_ID)
215
+ cacheChatGpt(ctx, found, key.apiKey);
200
216
  // models.dev 只读本地(快照 ⊕ `ama models refresh` 的覆盖),不联网。
201
217
  const mdIndex = loadModelsDevIndex(ctx.level.dataDir);
202
218
  io.stdout(`${describeModelsDev(ctx.level.dataDir)}\n`);
@@ -252,6 +268,38 @@ async function run(ctx) {
252
268
  }
253
269
  return ExitCode.Ok;
254
270
  }
271
+ /** 发现得到的模型 → 缓存条目(后端给的元数据一并写入;推理强度由映射表还原)。 */
272
+ function cacheEntry(m) {
273
+ const entry = { id: m.id };
274
+ if (m.name !== m.id)
275
+ entry.name = m.name;
276
+ if (m.contextWindow !== undefined)
277
+ entry.contextWindow = m.contextWindow;
278
+ if (m.input.length !== 1 || m.input[0] !== "text")
279
+ entry.input = [...m.input];
280
+ if (m.reasoning && m.thinkingLevelMap !== undefined)
281
+ entry.reasoningLevels = Object.entries(m.thinkingLevelMap).flatMap(([level, value]) => typeof value !== "string" ? [] : level === "off" ? ["none"] : [level]);
282
+ return entry;
283
+ }
284
+ /**
285
+ * chatgpt 的发现结果写进发现缓存(选择器与注册表读它;0 个也写,免得残留另一 flavor 的缓存);写失败不影响
286
+ * 列表输出。codex 返回 0 个时提示 codexClientVersion 可能过旧。
287
+ */
288
+ function cacheChatGpt(ctx, found, apiKey) {
289
+ const flavor = liveToken(apiKey)?.flavor;
290
+ try {
291
+ const models = found.map(cacheEntry);
292
+ const path = writeDiscoveredCache(ctx.level.dataDir, CHATGPT_PROVIDER_ID, { models, flavor });
293
+ ctx.io.stdout(msg().subcommands.discover.cached(path));
294
+ }
295
+ catch (error) {
296
+ ctx.io.stderr(`ama: ${error.message}\n`);
297
+ }
298
+ if (found.length === 0 && flavor === "codex") {
299
+ const version = codexClientVersion(ctx.level.merged.config.auth?.chatgpt, ctx.io.env);
300
+ ctx.io.stderr(`${msg().auth.login.codexNoModels(version)}\n`);
301
+ }
302
+ }
255
303
  export const DISCOVER_ACTION = {
256
304
  usage: "ama models discover <provider> [--probe] [--write] [--limit <n>] [--concurrency <n>] [--probe-timeout <ms>]",
257
305
  required: "<provider>",
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `ama models enable <ref…>` / `ama models disable <ref…>` / `ama models list --enabled`:编辑与查看用户级
3
+ * `models.enabled`(模型选择器的显式清单,见 ai/providers/model-visibility.ts)。
4
+ *
5
+ * 写回走 config/edit.ts(重读目标文件、校验、`.bak` 备份、原子写);`models` 只认用户级,没有 `--project`。
6
+ * 引用要是 `provider/model[@channel]` 或 `provider/*`,供应商必须存在;模型不在模型表里只警告(中转站与
7
+ * 订阅后端接受任意 id)。清单移空时删掉这个键。
8
+ */
9
+ import type { ModelsAction, ModelsActionContext } from "./models.js";
10
+ /** `ama models list --enabled`:清单(及各项供应商状态);没设清单时列出选择器缺省显示的模型。 */
11
+ export declare function listEnabled(ctx: ModelsActionContext): Promise<number>;
12
+ export declare const ENABLE_ACTION: ModelsAction;
13
+ export declare const DISABLE_ACTION: ModelsAction;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `ama models enable <ref…>` / `ama models disable <ref…>` / `ama models list --enabled`:编辑与查看用户级
3
+ * `models.enabled`(模型选择器的显式清单,见 ai/providers/model-visibility.ts)。
4
+ *
5
+ * 写回走 config/edit.ts(重读目标文件、校验、`.bak` 备份、原子写);`models` 只认用户级,没有 `--project`。
6
+ * 引用要是 `provider/model[@channel]` 或 `provider/*`,供应商必须存在;模型不在模型表里只警告(中转站与
7
+ * 订阅后端接受任意 id)。清单移空时删掉这个键。
8
+ */
9
+ import { accessReady, addEnabled, enabledList, providerAccess, removeEnabled, splitEnabledRef, } from "../../ai/providers/model-visibility.js";
10
+ import { readSnapshot, setConfigValue } from "../../config/edit.js";
11
+ import { MODELS_ENABLED_REF } from "../../config/types-w5.js";
12
+ import { msg } from "../../i18n/index.js";
13
+ import { UsageError } from "../args.js";
14
+ import { ExitCode } from "../exit-codes.js";
15
+ function layerInput(ctx) {
16
+ const input = {
17
+ configDir: ctx.level.configDir,
18
+ cwd: ctx.io.cwd,
19
+ env: ctx.io.env,
20
+ };
21
+ if (ctx.level.profile !== undefined) {
22
+ input.profile = { config: ctx.level.profile.config };
23
+ input.hasProfile = true;
24
+ }
25
+ return input;
26
+ }
27
+ /** 校验引用;供应商不存在返回退出码,模型不在表里只警告。 */
28
+ function checkRefs(ctx, refs) {
29
+ const m = msg().subcommands.modelsEnabled;
30
+ for (const ref of refs) {
31
+ const parts = splitEnabledRef(ref);
32
+ if (!MODELS_ENABLED_REF.test(ref) || parts === undefined)
33
+ throw new UsageError(m.badRef(ref));
34
+ const provider = ctx.registry.get(parts.provider);
35
+ if (provider === undefined) {
36
+ ctx.io.stderr(msg().subcommands.common.providerNotFound(parts.provider));
37
+ return ExitCode.NoModel;
38
+ }
39
+ if (parts.model !== "*" &&
40
+ provider.models.length > 0 &&
41
+ !provider.models.some((model) => model.id === parts.model))
42
+ ctx.io.stderr(m.unknownModel(ref));
43
+ }
44
+ return undefined;
45
+ }
46
+ function userList(input) {
47
+ return enabledList(readSnapshot(input).user?.models?.enabled);
48
+ }
49
+ async function enable(ctx) {
50
+ const m = msg().subcommands.modelsEnabled;
51
+ const refs = [...new Set(ctx.args)];
52
+ const bad = checkRefs(ctx, refs);
53
+ if (bad !== undefined)
54
+ return bad;
55
+ const input = layerInput(ctx);
56
+ const current = userList(input);
57
+ const fresh = refs.filter((ref) => !(current ?? []).includes(ref));
58
+ if (fresh.length === 0) {
59
+ ctx.io.stdout(m.alreadyListed(refs.join(", ")));
60
+ return ExitCode.Ok;
61
+ }
62
+ const result = setConfigValue({
63
+ ...input,
64
+ scope: "user",
65
+ key: "models.enabled",
66
+ value: addEnabled(current, fresh),
67
+ });
68
+ ctx.io.stdout(m.added(fresh.join(", "), result.path));
69
+ return ExitCode.Ok;
70
+ }
71
+ async function disable(ctx) {
72
+ const m = msg().subcommands.modelsEnabled;
73
+ const refs = [...new Set(ctx.args)];
74
+ for (const ref of refs)
75
+ if (!MODELS_ENABLED_REF.test(ref))
76
+ throw new UsageError(m.badRef(ref));
77
+ const input = layerInput(ctx);
78
+ const current = userList(input) ?? [];
79
+ const listed = refs.filter((ref) => current.includes(ref));
80
+ const missing = refs.filter((ref) => !current.includes(ref));
81
+ if (missing.length > 0)
82
+ ctx.io.stderr(m.notListed(missing.join(", ")));
83
+ if (listed.length === 0)
84
+ return missing.length > 0 ? ExitCode.RuntimeError : ExitCode.Ok;
85
+ const next = removeEnabled(current, listed);
86
+ const result = setConfigValue({ ...input, scope: "user", key: "models.enabled", value: next });
87
+ ctx.io.stdout(next === undefined ? m.cleared(result.path) : m.removed(listed.join(", "), result.path));
88
+ return ExitCode.Ok;
89
+ }
90
+ /** `ama models list --enabled`:清单(及各项供应商状态);没设清单时列出选择器缺省显示的模型。 */
91
+ export async function listEnabled(ctx) {
92
+ const m = msg().subcommands.modelsEnabled;
93
+ const labels = msg().panels.model;
94
+ const list = enabledList(ctx.level.merged.config.models?.enabled);
95
+ const status = async (providerId) => {
96
+ const provider = ctx.registry.get(providerId);
97
+ if (provider === undefined)
98
+ return "?";
99
+ const access = await providerAccess(ctx.registry, provider);
100
+ return access === "none" ? labels.noKey : labels[access];
101
+ };
102
+ if (list !== undefined) {
103
+ ctx.io.stdout(m.listHeader(ctx.level.userConfigPath));
104
+ for (const ref of list) {
105
+ const parts = splitEnabledRef(ref);
106
+ const provider = parts === undefined ? undefined : ctx.registry.get(parts.provider);
107
+ const known = parts === undefined ||
108
+ parts.model === "*" ||
109
+ provider === undefined ||
110
+ provider.models.length === 0 ||
111
+ provider.models.some((model) => model.id === parts.model);
112
+ const notes = [
113
+ parts === undefined ? "?" : await status(parts.provider),
114
+ ...(known ? [] : [m.notInTable]),
115
+ ];
116
+ ctx.io.stdout(` ${ref} ${notes.join(" · ")}\n`);
117
+ }
118
+ return ExitCode.Ok;
119
+ }
120
+ ctx.io.stdout(m.listUnset);
121
+ let shown = 0;
122
+ for (const provider of ctx.registry.list()) {
123
+ const access = await providerAccess(ctx.registry, provider);
124
+ if (!accessReady(access))
125
+ continue;
126
+ const label = access === "none" ? labels.noKey : labels[access];
127
+ for (const model of provider.models) {
128
+ ctx.io.stdout(` ${provider.id}/${model.id} ${label}\n`);
129
+ shown++;
130
+ }
131
+ }
132
+ if (shown === 0)
133
+ ctx.io.stdout(m.nothing);
134
+ return ExitCode.Ok;
135
+ }
136
+ export const ENABLE_ACTION = {
137
+ usage: "ama models enable <provider/model[@channel]|provider/*>…",
138
+ required: "<provider/model>",
139
+ run: enable,
140
+ };
141
+ export const DISABLE_ACTION = {
142
+ usage: "ama models disable <provider/model[@channel]|provider/*>…",
143
+ required: "<provider/model>",
144
+ run: disable,
145
+ };
@@ -9,6 +9,8 @@
9
9
  * 选项表与必填位置参数都从表里来。
10
10
  *
11
11
  * [W5-M1] `refresh`:models.dev 显式刷新(`refresh-catalog` 保留为别名)。
12
+ *
13
+ * `enable` / `disable` / `list --enabled`:模型选择器清单 `models.enabled`(models-enable.ts)。
12
14
  */
13
15
  import type { ProviderRegistryApi } from "../../ai/types.js";
14
16
  import type { CliIo, RuntimeDeps } from "../deps.js";
@@ -9,6 +9,8 @@
9
9
  * 选项表与必填位置参数都从表里来。
10
10
  *
11
11
  * [W5-M1] `refresh`:models.dev 显式刷新(`refresh-catalog` 保留为别名)。
12
+ *
13
+ * `enable` / `disable` / `list --enabled`:模型选择器清单 `models.enabled`(models-enable.ts)。
12
14
  */
13
15
  import { formatModelRef, modelRefOf } from "../../ai/providers/channels.js";
14
16
  import { describeRefresh, modelsDevCachePath, refreshModelsDev, } from "../../ai/providers/models-dev-cache.js";
@@ -18,6 +20,7 @@ import { ExitCode } from "../exit-codes.js";
18
20
  import { buildRegistry, loadUserLevel } from "./context.js";
19
21
  import { CACHE_PROBE_ACTION } from "./models-cache-probe.js";
20
22
  import { DISCOVER_ACTION } from "./models-discover.js";
23
+ import { DISABLE_ACTION, ENABLE_ACTION, listEnabled } from "./models-enable.js";
21
24
  import { metadataOf, modelFlags, sourcesLine } from "./model-meta.js";
22
25
  import { msg } from "../../i18n/index.js";
23
26
  const COMMON_VALUE_OPTIONS = ["profile", "auth-file"];
@@ -113,9 +116,12 @@ const REFRESH_ACTION = {
113
116
  /** 动作表:键是子命令名,顺序即用法文本顺序。 */
114
117
  export const MODELS_ACTIONS = Object.freeze({
115
118
  list: {
116
- usage: "ama models list [--provider <id>]",
119
+ usage: "ama models list [--provider <id>] [--enabled]",
117
120
  valueOptions: ["provider"],
118
- run: (ctx) => list(ctx.io, ctx.registry, ctx.values.get("provider")),
121
+ flagOptions: ["enabled"],
122
+ run: (ctx) => ctx.flags.has("enabled")
123
+ ? listEnabled(ctx)
124
+ : list(ctx.io, ctx.registry, ctx.values.get("provider")),
119
125
  },
120
126
  check: {
121
127
  usage: "ama models check <provider/id>",
@@ -132,6 +138,8 @@ export const MODELS_ACTIONS = Object.freeze({
132
138
  return msg().subcommands.models.refreshCatalogUsage;
133
139
  },
134
140
  },
141
+ enable: ENABLE_ACTION,
142
+ disable: DISABLE_ACTION,
135
143
  });
136
144
  export function modelsUsage() {
137
145
  return msg().subcommands.models.usage(Object.values(MODELS_ACTIONS).map((a) => a.usage));
@@ -151,7 +151,7 @@ function w5Sections() {
151
151
  },
152
152
  }),
153
153
  subagents: object({ maxConcurrent: num(1, 64), maxPending: num(0, 1024), defaultModel: str() }),
154
- models: object({ aliases: object({ fast: str(), strong: str() }) }),
154
+ models: object({ aliases: object({ fast: str(), strong: str() }), enabled: strings }),
155
155
  fallbackModel: str(),
156
156
  limits: object({ maxTurns: num(1), maxCostUsd: num(0) }),
157
157
  reminders: object({
@@ -180,6 +180,7 @@ function w6Sections() {
180
180
  clientId: str(),
181
181
  issuer: str(),
182
182
  originator: str(),
183
+ codexClientVersion: str(),
183
184
  redirectPorts: {
184
185
  type: "array",
185
186
  items: { type: "integer", minimum: 0, maximum: 65535 },
@@ -10,6 +10,7 @@
10
10
  * DISPLAY_DEFAULTS 相同,DISPLAY_DEFAULTS 本身通过 `validateConfig`。
11
11
  */
12
12
  import { DEFAULT_IDLE_TIMEOUT_MS } from "../ai/http.js";
13
+ import { DEFAULT_CODEX_CLIENT_VERSION } from "../auth/chatgpt/presets.js";
13
14
  import { messagesFor, msg } from "../i18n/index.js";
14
15
  import { DEFAULT_INLINE_BUDGET } from "../codemode/declarations.js";
15
16
  import { DEFAULT_CONFIG, mergeConfig } from "./merge.js";
@@ -33,7 +34,12 @@ const IMPLICIT_DEFAULTS = {
33
34
  subagents: "read",
34
35
  },
35
36
  auth: {
36
- chatgpt: { flavor: "siwc", issuer: "https://auth.openai.com", originator: "codex_cli_rs" },
37
+ chatgpt: {
38
+ flavor: "siwc",
39
+ issuer: "https://auth.openai.com",
40
+ originator: "codex_cli_rs",
41
+ codexClientVersion: DEFAULT_CODEX_CLIENT_VERSION,
42
+ },
37
43
  },
38
44
  checkpoints: { ...DEFAULT_CHECKPOINTS_CONFIG },
39
45
  sandbox: { enabled: "auto", bash: "off", network: "deny", writable: [] },
@@ -7,7 +7,7 @@
7
7
  import { PERMISSION_MODES_STRICT_FIRST } from "../permissions/types.js";
8
8
  import { msg } from "../i18n/index.js";
9
9
  import { Checker, THINKING_LEVELS, checkSection, join } from "./checker.js";
10
- import { AGENTS_RESERVED_KEYS, IMAGE_RESIZE_MODES, PLAN_BASH_MODES_STRICT_FIRST, PLAN_UNATTENDED_MODES, STATUS_LINE_MODES, } from "./types-w5.js";
10
+ import { AGENTS_RESERVED_KEYS, IMAGE_RESIZE_MODES, MODELS_ENABLED_REF, PLAN_BASH_MODES_STRICT_FIRST, PLAN_UNATTENDED_MODES, STATUS_LINE_MODES, } from "./types-w5.js";
11
11
  /** 第五波新增的顶层键(schema.ts 的 CONFIG_KEYS 并入)。 */
12
12
  export const W5_CONFIG_KEYS = [
13
13
  "images",
@@ -88,7 +88,14 @@ export function validateConfigW5(c, config) {
88
88
  c.number(s, "maxPending", p, 0, 1024);
89
89
  c.string(s, "defaultModel", p);
90
90
  });
91
- checkSection(c, config, "models", ["aliases"], (s, p) => {
91
+ checkSection(c, config, "models", ["aliases", "enabled"], (s, p) => {
92
+ c.stringArray(s, "enabled", p);
93
+ const enabled = s["enabled"];
94
+ if (Array.isArray(enabled))
95
+ enabled.forEach((ref, i) => {
96
+ if (typeof ref === "string" && !MODELS_ENABLED_REF.test(ref))
97
+ c.error(join(join(p, "enabled"), i), msg().config.schema.modelsEnabled);
98
+ });
92
99
  const aliases = s["aliases"];
93
100
  const ap = join(p, "aliases");
94
101
  if (aliases === undefined || !c.object(aliases, ap))
@@ -9,7 +9,7 @@ export declare const W6_CONFIG_KEYS: readonly ["memory", "auth"];
9
9
  /** 已有 `ui` 段新增的键。 */
10
10
  export declare const W6_UI_KEYS: readonly ["language", "replyLanguage", "agentBar"];
11
11
  export declare const MEMORY_KEYS: readonly ["enabled", "scopes", "indexMaxBytes", "fileMaxBytes", "maxFiles", "subagents"];
12
- export declare const CHATGPT_AUTH_KEYS: readonly ["flavor", "clientId", "issuer", "originator", "redirectPorts"];
12
+ export declare const CHATGPT_AUTH_KEYS: readonly ["flavor", "clientId", "issuer", "originator", "codexClientVersion", "redirectPorts"];
13
13
  export declare function checkUiW6(c: Checker, ui: Obj, path: string): void;
14
14
  /** 第六波顶层段的校验(validateConfig 末尾调用)。 */
15
15
  export declare function validateConfigW6(c: Checker, config: Obj): void;
@@ -24,6 +24,7 @@ export const CHATGPT_AUTH_KEYS = [
24
24
  "clientId",
25
25
  "issuer",
26
26
  "originator",
27
+ "codexClientVersion",
27
28
  "redirectPorts",
28
29
  ];
29
30
  export function checkUiW6(c, ui, path) {
@@ -63,7 +64,7 @@ export function validateConfigW6(c, config) {
63
64
  return;
64
65
  c.keys(chatgpt, cp, CHATGPT_AUTH_KEYS);
65
66
  c.oneOf(chatgpt, "flavor", cp, CHATGPT_FLAVORS);
66
- for (const key of ["clientId", "issuer", "originator"])
67
+ for (const key of ["clientId", "issuer", "originator", "codexClientVersion"])
67
68
  c.string(chatgpt, key, cp);
68
69
  checkPorts(c, chatgpt, "redirectPorts", cp);
69
70
  });
@@ -59,6 +59,9 @@ export declare const SETTING_HINTS: readonly [{
59
59
  }, {
60
60
  readonly key: "auth.chatgpt";
61
61
  readonly hint: "auth";
62
+ }, {
63
+ readonly key: "models.enabled";
64
+ readonly hint: "modelsEnabled";
62
65
  }];
63
66
  export type SettingHintId = (typeof SETTING_HINTS)[number]["hint"];
64
67
  type Schema = Record<string, unknown>;
@@ -106,6 +106,7 @@ export const SETTING_HINTS = [
106
106
  { key: "agents.<id>", hint: "agents" },
107
107
  { key: "hooks", hint: "hooks" },
108
108
  { key: "auth.chatgpt", hint: "auth" },
109
+ { key: "models.enabled", hint: "modelsEnabled" },
109
110
  ];
110
111
  /** Schema node of a dotted key (undefined when the key is not in the schema). */
111
112
  export function schemaAt(key, root = buildConfigJsonSchema()) {
@@ -68,12 +68,19 @@ export interface SubagentsConfig {
68
68
  /** 子会话缺省模型;不设继承父会话。 */
69
69
  defaultModel?: string;
70
70
  }
71
+ /** `models.enabled` 的一项:`provider/model[@channel]` 或 `provider/*`。 */
72
+ export declare const MODELS_ENABLED_REF: RegExp;
71
73
  /** [W5-G] 模型别名(子 Agent 定义的 `model: fast | strong`)。 */
72
74
  export interface ModelsConfig {
73
75
  aliases?: {
74
76
  fast?: string;
75
77
  strong?: string;
76
78
  };
79
+ /**
80
+ * 模型选择器的显式清单(`provider/model[@channel]`,`provider/*` 整个供应商):设置后 `/model` 只显示清单内
81
+ * 的模型(加当前模型);不设时显示已配置 key / 登录 / 本地可达的供应商的全部模型。只认用户级。
82
+ */
83
+ enabled?: string[];
77
84
  }
78
85
  /** [W5-H1] 档一裁剪参数(其余随窗口缩放的常量不暴露)。 */
79
86
  export interface PruneConfig {
@@ -18,3 +18,5 @@ export function agentEntry(agents, agentId) {
18
18
  const entry = agents[agentId];
19
19
  return typeof entry === "object" && !Array.isArray(entry) ? entry : undefined;
20
20
  }
21
+ /** `models.enabled` 的一项:`provider/model[@channel]` 或 `provider/*`。 */
22
+ export const MODELS_ENABLED_REF = /^[^/\s]+\/\S+$/;
@@ -53,6 +53,11 @@ export interface ChatGptAuthConfig {
53
53
  issuer?: string;
54
54
  /** codex flavor 的 `originator` 请求头,缺省 codex_cli_rs。 */
55
55
  originator?: string;
56
+ /**
57
+ * codex flavor 模型列表的 `client_version`(Codex CLI 版本号,后端按它过滤模型),缺省见
58
+ * `DEFAULT_CODEX_CLIENT_VERSION`;`AMA_CHATGPT_CODEX_CLIENT_VERSION` 优先。
59
+ */
60
+ codexClientVersion?: string;
56
61
  /** 本地回调端口(依次尝试;0 = 任意空闲端口)。 */
57
62
  redirectPorts?: number[];
58
63
  }