@armadra/agent 0.2.1 → 0.3.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +55 -11
  3. package/dist/agent/session-cache.js +5 -2
  4. package/dist/agent/session-state.js +2 -1
  5. package/dist/agent/session-sync.js +9 -2
  6. package/dist/agent/session.js +8 -7
  7. package/dist/ai/apis/openai-responses.js +2 -1
  8. package/dist/ai/cache/fingerprint.d.ts +1 -1
  9. package/dist/ai/cache/fingerprint.js +1 -1
  10. package/dist/ai/cache/reporting.js +2 -1
  11. package/dist/ai/providers/channels.d.ts +51 -0
  12. package/dist/ai/providers/channels.js +96 -0
  13. package/dist/ai/providers/enrich.d.ts +34 -0
  14. package/dist/ai/providers/enrich.js +86 -0
  15. package/dist/ai/providers/models-dev-cache.d.ts +53 -0
  16. package/dist/ai/providers/models-dev-cache.js +147 -0
  17. package/dist/ai/providers/models-dev.d.ts +99 -0
  18. package/dist/ai/providers/models-dev.js +315 -0
  19. package/dist/ai/providers/registry.d.ts +35 -4
  20. package/dist/ai/providers/registry.js +177 -24
  21. package/dist/ai/types.d.ts +21 -1
  22. package/dist/bundle/ama.cjs +4075 -2008
  23. package/dist/cli/args.d.ts +6 -3
  24. package/dist/cli/args.js +30 -3
  25. package/dist/cli/bootstrap.js +1 -0
  26. package/dist/cli/compose-providers.js +3 -0
  27. package/dist/cli/compose-session.d.ts +3 -0
  28. package/dist/cli/compose.d.ts +3 -1
  29. package/dist/cli/compose.js +24 -9
  30. package/dist/cli/deps.d.ts +2 -0
  31. package/dist/cli/main.d.ts +2 -1
  32. package/dist/cli/main.js +13 -1
  33. package/dist/cli/startup-steps.js +6 -1
  34. package/dist/cli/subcommands/config.d.ts +11 -2
  35. package/dist/cli/subcommands/config.js +80 -4
  36. package/dist/cli/subcommands/context.js +1 -0
  37. package/dist/cli/subcommands/init.d.ts +6 -0
  38. package/dist/cli/subcommands/init.js +22 -0
  39. package/dist/cli/subcommands/model-meta.d.ts +16 -0
  40. package/dist/cli/subcommands/model-meta.js +54 -0
  41. package/dist/cli/subcommands/models-cache-probe.js +1 -1
  42. package/dist/cli/subcommands/models-discover.d.ts +6 -0
  43. package/dist/cli/subcommands/models-discover.js +37 -9
  44. package/dist/cli/subcommands/models.js +26 -18
  45. package/dist/cli/subcommands/providers-list.d.ts +8 -0
  46. package/dist/cli/subcommands/providers-list.js +97 -0
  47. package/dist/cli/subcommands/providers-plan.d.ts +79 -0
  48. package/dist/cli/subcommands/providers-plan.js +215 -0
  49. package/dist/cli/subcommands/providers.d.ts +27 -0
  50. package/dist/cli/subcommands/providers.js +433 -0
  51. package/dist/config/init.d.ts +30 -0
  52. package/dist/config/init.js +92 -0
  53. package/dist/config/json-schema.d.ts +14 -0
  54. package/dist/config/json-schema.js +182 -0
  55. package/dist/config/schema.d.ts +1 -1
  56. package/dist/config/schema.js +74 -11
  57. package/dist/config/types.d.ts +23 -5
  58. package/dist/config/types.js +2 -0
  59. package/dist/modes/image-input.d.ts +27 -0
  60. package/dist/modes/image-input.js +78 -0
  61. package/dist/modes/interactive/commands.js +2 -3
  62. package/dist/modes/interactive/interactive-mode.js +5 -1
  63. package/dist/modes/interactive/line/line-mode.js +3 -1
  64. package/dist/modes/interactive/startup-ui.d.ts +7 -2
  65. package/dist/modes/interactive/startup-ui.js +41 -11
  66. package/dist/modes/print/print-mode.d.ts +2 -0
  67. package/dist/modes/print/print-mode.js +12 -1
  68. package/dist/modes/rpc/commands.js +7 -2
  69. package/dist/rpc.d.ts +2 -0
  70. package/dist/session/projection.js +5 -1
  71. package/dist/session/types.d.ts +2 -0
  72. package/dist/tools/image-file.d.ts +31 -0
  73. package/dist/tools/image-file.js +114 -0
  74. package/dist/tools/read.d.ts +4 -8
  75. package/dist/tools/read.js +15 -64
  76. package/docs/providers.md +216 -1
  77. package/docs/rpc.md +3 -3
  78. package/docs/session-format.md +1 -1
  79. package/docs/tui.md +2 -0
  80. package/package.json +1 -1
@@ -103,7 +103,7 @@ async function run(ctx) {
103
103
  return ExitCode.NoModel;
104
104
  }
105
105
  const { model, provider } = found;
106
- const key = await registry.resolveApiKey(provider.id);
106
+ const key = await registry.resolveApiKey(provider.id, model.channel);
107
107
  if (key.apiKey === undefined && provider.requiresApiKey) {
108
108
  io.stderr(`ama: ${provider.id} 没有 API key(ama auth set ${provider.id})\n`);
109
109
  return ExitCode.NoModel;
@@ -12,6 +12,9 @@
12
12
  * `--write`:新条目合并进用户级 config.json 的 `providers.<id>.models`——已有同 id 不覆盖;只写
13
13
  * `id` 与探到的 `api`(与供应商协议相同时省略);带 `--probe` 时只写探测成功的模型。不猜
14
14
  * `contextWindow`(自动压缩随之关闭,输出 warning)。写前备份为 `config.json.bak`。
15
+ *
16
+ * models.dev:列表之后按需刷新缓存(24 小时内不重拉),每个模型标出上下文、输出、图片、工具调用与
17
+ * 匹配结果;`--write` 跳过 models.dev 标明不支持工具调用的模型。元数据不写进配置——运行时从缓存补。
15
18
  */
16
19
  import type { Api, Model, ProviderData, ProviderRegistryApi } from "../../ai/types.js";
17
20
  import type { ModelsAction } from "./models.js";
@@ -35,6 +38,9 @@ export interface ProbeOptions {
35
38
  /** 因鉴权失败或限流提前停止时回调。 */
36
39
  onStop?: (reason: string) => void;
37
40
  }
41
+ export declare const FATAL_STATUS: RegExp;
38
42
  /** 返回已探测模型 → 第一个成功的协议(都失败为 undefined);超出 `limit` 或提前停止的不在表里。 */
39
43
  export declare function probeModelApis(registry: ProviderRegistryApi, provider: ProviderData, ids: string[], limit: number, options?: ProbeOptions): Promise<Map<string, Api | undefined>>;
44
+ /** 一次最小请求;成功返回 undefined,失败返回错误文本(`ama providers add --probe` 也用)。 */
45
+ export declare function attempt(registry: ProviderRegistryApi, model: Model, apiKey: string | undefined, timeoutMs?: number): Promise<string | undefined>;
40
46
  export declare const DISCOVER_ACTION: ModelsAction;
@@ -12,15 +12,21 @@
12
12
  * `--write`:新条目合并进用户级 config.json 的 `providers.<id>.models`——已有同 id 不覆盖;只写
13
13
  * `id` 与探到的 `api`(与供应商协议相同时省略);带 `--probe` 时只写探测成功的模型。不猜
14
14
  * `contextWindow`(自动压缩随之关闭,输出 warning)。写前备份为 `config.json.bak`。
15
+ *
16
+ * models.dev:列表之后按需刷新缓存(24 小时内不重拉),每个模型标出上下文、输出、图片、工具调用与
17
+ * 匹配结果;`--write` 跳过 models.dev 标明不支持工具调用的模型。元数据不写进配置——运行时从缓存补。
15
18
  */
16
19
  import { authHeaders, mergeHeaders } from "../../ai/http.js";
17
20
  import { discoverLocalModels, materializeModel } from "../../ai/providers/registry.js";
18
21
  import { withCustomDefaults } from "../../ai/providers/catalog.js";
22
+ import { describeRefresh, refreshModelsDev } from "../../ai/providers/models-dev-cache.js";
23
+ import { matchLabel, modelsDevFields } from "../../ai/providers/models-dev.js";
19
24
  import { existsSync } from "node:fs";
20
25
  import { loadConfigFile } from "../../config/load.js";
21
26
  import { writeConfigFile } from "../../config/write.js";
22
27
  import { UsageError } from "../args.js";
23
28
  import { ExitCode } from "../exit-codes.js";
29
+ import { compactTokens } from "./model-meta.js";
24
30
  export const DISCOVER_TIMEOUT_MS = 15_000;
25
31
  export const PROBE_TIMEOUT_MS = 30_000;
26
32
  export const DEFAULT_PROBE_LIMIT = 30;
@@ -51,7 +57,7 @@ export function probeOrder(provider) {
51
57
  ? [provider.api, ...PROBE_APIS.filter((api) => api !== provider.api)]
52
58
  : [...PROBE_APIS];
53
59
  }
54
- const FATAL_STATUS = /^(?:HTTP )?(?:401|403|429)\b/;
60
+ export const FATAL_STATUS = /^(?:HTTP )?(?:401|403|429)\b/;
55
61
  /** 返回已探测模型 → 第一个成功的协议(都失败为 undefined);超出 `limit` 或提前停止的不在表里。 */
56
62
  export async function probeModelApis(registry, provider, ids, limit, options = {}) {
57
63
  const result = new Map();
@@ -80,8 +86,8 @@ export async function probeModelApis(registry, provider, ids, limit, options = {
80
86
  }
81
87
  return result;
82
88
  }
83
- /** 一次最小请求;成功返回 undefined,失败返回错误文本。 */
84
- async function attempt(registry, model, apiKey, timeoutMs = PROBE_TIMEOUT_MS) {
89
+ /** 一次最小请求;成功返回 undefined,失败返回错误文本(`ama providers add --probe` 也用)。 */
90
+ export async function attempt(registry, model, apiKey, timeoutMs = PROBE_TIMEOUT_MS) {
85
91
  const impl = registry.getApi(model.api);
86
92
  if (impl === undefined)
87
93
  return `协议 ${model.api} 尚未实现`;
@@ -111,7 +117,7 @@ function parseLimit(raw) {
111
117
  return n;
112
118
  }
113
119
  /** 把新条目合并进用户级 config.json;返回写入条数。 */
114
- function writeEntries(ctx, provider, entries) {
120
+ function writeEntries(ctx, provider, entries, notes) {
115
121
  const { io } = ctx;
116
122
  const path = ctx.level.userConfigPath;
117
123
  const config = structuredClone(loadConfigFile("config", path)?.value ?? { version: 1 });
@@ -134,7 +140,12 @@ function writeEntries(ctx, provider, entries) {
134
140
  writeConfigFile(path, config, { backup: true });
135
141
  io.stdout(`\n已写入 ${path}:${provider.id} 新增 ${added.length} 个模型` +
136
142
  `${kept > 0 ? `,${kept} 个已存在未覆盖` : ""}${backup ? `(原文件备份为 ${path}.bak)` : ""}\n`);
137
- io.stderr(`ama: 警告:新增模型未设 contextWindow,自动压缩关闭;需要时在 config.json 里补上\n`);
143
+ if (notes.noTools.size > 0)
144
+ io.stdout(`跳过不支持工具调用的模型(models.dev):${[...notes.noTools].join(", ")}\n`);
145
+ const blind = added.filter((entry) => notes.unmatched.has(entry.id)).map((entry) => entry.id);
146
+ if (blind.length > 0)
147
+ io.stderr(`ama: 警告:${blind.join(", ")} 在 models.dev 未匹配,没有 contextWindow,自动压缩关闭;` +
148
+ `需要时在 config.json 里补上或写 modelsDev\n`);
138
149
  return added.length;
139
150
  }
140
151
  async function run(ctx) {
@@ -161,14 +172,31 @@ async function run(ctx) {
161
172
  }
162
173
  const configured = new Map(provider.models.map((m) => [m.id, m]));
163
174
  io.stdout(`${provider.id}:发现 ${found.length} 个模型(${modelsUrl(provider)})\n`);
175
+ const md = await refreshModelsDev({ dataDir: ctx.level.dataDir, env: io.env });
176
+ io.stdout(`${describeRefresh(md)}\n`);
177
+ if (md.warning !== undefined)
178
+ io.stderr(`ama: 警告:${md.warning}\n`);
179
+ const noTools = new Set();
180
+ const unmatched = new Set();
164
181
  for (const model of found) {
165
182
  const known = configured.get(model.id);
166
- io.stdout(` ${model.id}${known !== undefined ? ` 已配置(${known.api})` : ""}\n`);
183
+ const match = md.index?.match(model.id);
184
+ const fields = match !== undefined ? modelsDevFields(match.model) : undefined;
185
+ if (fields?.toolCall === false)
186
+ noTools.add(model.id);
187
+ if (match === undefined)
188
+ unmatched.add(model.id);
189
+ const meta = fields === undefined
190
+ ? " models.dev 未匹配"
191
+ : ` ctx ${compactTokens(fields.contextWindow)} · out ${compactTokens(fields.maxTokens)}` +
192
+ `${fields.input?.includes("image") ? " · 图片" : ""}${fields.reasoning ? " · 思考" : ""}` +
193
+ `${fields.toolCall === false ? " · 不支持工具调用" : ""} · ${matchLabel(match)}`;
194
+ io.stdout(` ${model.id}${known !== undefined ? ` 已配置(${known.api})` : ""}${meta}\n`);
167
195
  }
168
196
  const write = ctx.flags.has("write");
169
197
  if (!ctx.flags.has("probe")) {
170
198
  if (write)
171
- writeEntries(ctx, provider, found.map((m) => ({ id: m.id })));
199
+ writeEntries(ctx, provider, found.filter((m) => !noTools.has(m.id)).map((m) => ({ id: m.id })), { noTools, unmatched });
172
200
  return ExitCode.Ok;
173
201
  }
174
202
  const ids = found.map((m) => m.id);
@@ -184,10 +212,10 @@ async function run(ctx) {
184
212
  if (write) {
185
213
  const entries = [];
186
214
  for (const [modelId, api] of probed) {
187
- if (api !== undefined)
215
+ if (api !== undefined && !noTools.has(modelId))
188
216
  entries.push(api === provider.api ? { id: modelId } : { id: modelId, api });
189
217
  }
190
- writeEntries(ctx, provider, entries);
218
+ writeEntries(ctx, provider, entries, { noTools, unmatched });
191
219
  }
192
220
  if (stopped !== undefined) {
193
221
  io.stderr(`ama: 探测提前停止(${stopped})\n`);
@@ -8,22 +8,16 @@
8
8
  * `cache-probe`、B12 的 `discover`)各写在自己的文件里,这里只加一行表项;用法文本、
9
9
  * 选项表与必填位置参数都从表里来。
10
10
  */
11
+ import { formatModelRef, modelRefOf } from "../../ai/providers/channels.js";
12
+ import { describeRefresh, modelsDevCachePath, refreshModelsDev, } from "../../ai/providers/models-dev-cache.js";
11
13
  import { parseSubArgs, UsageError } from "../args.js";
12
14
  import { ExitCode } from "../exit-codes.js";
13
15
  import { buildRegistry, loadUserLevel } from "./context.js";
14
16
  import { CACHE_PROBE_ACTION } from "./models-cache-probe.js";
15
17
  import { DISCOVER_ACTION } from "./models-discover.js";
18
+ import { metadataOf, modelFlags, sourcesLine } from "./model-meta.js";
16
19
  const COMMON_VALUE_OPTIONS = ["profile", "auth-file"];
17
20
  export const CHECK_TIMEOUT_MS = 30_000;
18
- function compact(n) {
19
- if (n === undefined)
20
- return "?";
21
- if (n >= 1_000_000)
22
- return `${Math.round(n / 100_000) / 10}M`;
23
- if (n >= 1000)
24
- return `${Math.round(n / 1000)}k`;
25
- return String(n);
26
- }
27
21
  async function list(io, registry, only) {
28
22
  const providers = registry.list().filter((p) => only === undefined || p.id === only);
29
23
  if (only !== undefined && providers.length === 0) {
@@ -38,14 +32,13 @@ async function list(io, registry, only) {
38
32
  ? "无 key"
39
33
  : "无需 key";
40
34
  io.stdout(`${provider.id} ${provider.api} ${keyText}\n`);
35
+ for (const channel of provider.channels ?? [])
36
+ io.stdout(` @${channel.name} ${channel.api} ${channel.baseUrl}\n`);
41
37
  for (const model of provider.models) {
42
- const flags = [
43
- `ctx ${compact(model.contextWindow)}`,
44
- `out ${compact(model.maxTokens)}`,
45
- model.reasoning ? "思考" : undefined,
46
- model.input.includes("image") ? "图片" : undefined,
47
- ].filter((x) => x !== undefined);
48
- io.stdout(` ${provider.id}/${model.id} ${flags.join(" · ")}\n`);
38
+ io.stdout(` ${provider.id}/${model.id} ${modelFlags(model)}\n`);
39
+ const sources = sourcesLine(metadataOf(registry, provider.id, model.id));
40
+ if (sources !== undefined)
41
+ io.stdout(` ${sources}\n`);
49
42
  }
50
43
  }
51
44
  return ExitCode.Ok;
@@ -58,7 +51,7 @@ async function check(io, registry, ref) {
58
51
  return ExitCode.NoModel;
59
52
  }
60
53
  const { model, provider } = found;
61
- const key = await registry.resolveApiKey(provider.id);
54
+ const key = await registry.resolveApiKey(provider.id, model.channel);
62
55
  if (key.apiKey === undefined && provider.requiresApiKey) {
63
56
  io.stderr(`ama: ${provider.id} 没有 API key(ama auth set ${provider.id})\n`);
64
57
  return ExitCode.NoModel;
@@ -81,7 +74,8 @@ async function check(io, registry, ref) {
81
74
  io.stderr(`ama: ${provider.id}/${model.id} 失败(${ms} ms):${message.errorMessage ?? message.stopReason}\n`);
82
75
  return ExitCode.RuntimeError;
83
76
  }
84
- io.stdout(`${provider.id}/${model.id} 可用(${ms} ms,stopReason ${message.stopReason})\n`);
77
+ const shown = formatModelRef(modelRefOf(model));
78
+ io.stdout(`${shown} 可用(${model.api},${ms} ms,stopReason ${message.stopReason})\n`);
85
79
  return ExitCode.Ok;
86
80
  }
87
81
  /** 动作表:键是子命令名,顺序即用法文本顺序。 */
@@ -98,6 +92,20 @@ export const MODELS_ACTIONS = Object.freeze({
98
92
  },
99
93
  discover: DISCOVER_ACTION,
100
94
  "cache-probe": CACHE_PROBE_ACTION,
95
+ "refresh-catalog": {
96
+ usage: "ama models refresh-catalog",
97
+ run: async (ctx) => {
98
+ const result = await refreshModelsDev({
99
+ dataDir: ctx.level.dataDir,
100
+ env: ctx.io.env,
101
+ force: true,
102
+ });
103
+ ctx.io.stdout(`${describeRefresh(result)}\n${modelsDevCachePath(ctx.level.dataDir)}\n`);
104
+ if (result.warning !== undefined)
105
+ ctx.io.stderr(`ama: 警告:${result.warning}\n`);
106
+ return result.status === "unavailable" ? ExitCode.RuntimeError : ExitCode.Ok;
107
+ },
108
+ },
101
109
  });
102
110
  export const MODELS_USAGE = `用法:${Object.values(MODELS_ACTIONS)
103
111
  .map((a) => a.usage)
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `ama providers list|channels|remove`(docs/providers.md「一键接入」):供应商 → 渠道 → 模型数与 key 来源
3
+ * (从不显示 key);remove 删配置条目(先备份)与 auth.json 里该供应商及其渠道的 key。
4
+ */
5
+ import { type Ctx } from "./providers.js";
6
+ export declare function listProviders(ctx: Ctx): Promise<number>;
7
+ export declare function listChannels(ctx: Ctx, id: string): Promise<number>;
8
+ export declare function removeProvider(ctx: Ctx, id: string): number;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `ama providers list|channels|remove`(docs/providers.md「一键接入」):供应商 → 渠道 → 模型数与 key 来源
3
+ * (从不显示 key);remove 删配置条目(先备份)与 auth.json 里该供应商及其渠道的 key。
4
+ */
5
+ import { apiShortName } from "../../ai/providers/channels.js";
6
+ import { classifyKeyValue, readAuthFile, removeAuthKey } from "../../config/auth-file.js";
7
+ import { writeConfigFile } from "../../config/write.js";
8
+ import { ExitCode } from "../exit-codes.js";
9
+ import { buildRegistry } from "./context.js";
10
+ import { userConfig } from "./providers.js";
11
+ async function keySource(registry, config, id, channel) {
12
+ const raw = channel !== undefined ? config?.channels?.[channel]?.apiKey : config?.apiKey;
13
+ if (raw !== undefined) {
14
+ const kind = classifyKeyValue(raw);
15
+ return kind === "env-ref"
16
+ ? raw
17
+ : kind === "command"
18
+ ? "!命令(config.json)"
19
+ : "字面量(config.json)";
20
+ }
21
+ const resolved = await registry.resolveApiKey(id, channel);
22
+ if (resolved.apiKey === undefined)
23
+ return channel !== undefined ? "同供应商" : "无";
24
+ if (channel !== undefined && resolved.source !== "auth-file")
25
+ return "同供应商";
26
+ return resolved.source === "env"
27
+ ? `环境变量 ${resolved.origin ?? ""}`
28
+ : resolved.source === "auth-file"
29
+ ? "auth.json"
30
+ : resolved.source;
31
+ }
32
+ export async function listProviders(ctx) {
33
+ const registry = await buildRegistry(ctx.level, ctx.io, ctx.deps);
34
+ const config = userConfig(ctx.level);
35
+ let shown = 0;
36
+ for (const provider of registry.list()) {
37
+ const own = config.providers?.[provider.id];
38
+ if (own === undefined &&
39
+ !(provider.builtin &&
40
+ provider.requiresApiKey &&
41
+ (await registry.resolveApiKey(provider.id)).apiKey !== undefined))
42
+ continue;
43
+ shown++;
44
+ const kind = provider.builtin ? "内置" : "自定义";
45
+ const channels = provider.channels ?? [];
46
+ ctx.io.stdout(`${provider.id} ${kind} · ${channels.length > 0 ? `${channels.length} 渠道` : `${provider.api} ${provider.baseUrl}`} · ` +
47
+ `${provider.models.length} 模型 · key ${await keySource(registry, own, provider.id)}\n`);
48
+ for (const c of channels) {
49
+ const count = provider.models.filter((m) => m.channels?.includes(c.name)).length;
50
+ ctx.io.stdout(` @${c.name} ${c.api} ${c.baseUrl} ${count} 模型 · key ${await keySource(registry, own, provider.id, c.name)}\n`);
51
+ }
52
+ }
53
+ if (shown === 0)
54
+ ctx.io.stdout(`没有配置供应商(ama providers add <id> --base-url <url>)\n`);
55
+ return ExitCode.Ok;
56
+ }
57
+ export async function listChannels(ctx, id) {
58
+ const registry = await buildRegistry(ctx.level, ctx.io, ctx.deps);
59
+ const provider = registry.get(id);
60
+ if (provider === undefined) {
61
+ ctx.io.stderr(`ama: 供应商不存在:${id}\n`);
62
+ return ExitCode.NoModel;
63
+ }
64
+ const own = userConfig(ctx.level).providers?.[id];
65
+ if (provider.channels === undefined) {
66
+ ctx.io.stdout(`${id}:单渠道(${provider.api} ${provider.baseUrl}),${provider.models.length} 模型\n`);
67
+ return ExitCode.Ok;
68
+ }
69
+ for (const c of provider.channels) {
70
+ const models = provider.models.filter((m) => m.channels?.includes(c.name));
71
+ const mark = c.name === provider.defaultChannel ? "(缺省)" : "";
72
+ ctx.io.stdout(`@${c.name}${mark} ${apiShortName(c.api)} ${c.api} ${c.baseUrl} key ${await keySource(registry, own, id, c.name)}\n` +
73
+ ` ${models.length} 模型${models.length > 0 ? `:${models.map((m) => m.id).join(", ")}` : ""}\n`);
74
+ }
75
+ return ExitCode.Ok;
76
+ }
77
+ export function removeProvider(ctx, id) {
78
+ const config = userConfig(ctx.level);
79
+ const had = config.providers?.[id] !== undefined;
80
+ if (had) {
81
+ delete config.providers?.[id];
82
+ writeConfigFile(ctx.level.userConfigPath, config, { backup: true });
83
+ ctx.io.stdout(`已从 ${ctx.level.userConfigPath} 删除 ${id}(原文件备份为 config.json.bak)\n`);
84
+ }
85
+ const auth = readAuthFile(ctx.level.authFile).file;
86
+ let keys = 0;
87
+ for (const name of Object.keys(auth.providers))
88
+ if (name === id || name.startsWith(`${id}@`))
89
+ keys += removeAuthKey(ctx.level.authFile, name) ? 1 : 0;
90
+ if (keys > 0)
91
+ ctx.io.stdout(`已删除 ${ctx.level.authFile} 里 ${id} 的 ${keys} 个 key\n`);
92
+ if (!had && keys === 0) {
93
+ ctx.io.stderr(`ama: 没有供应商 ${id}\n`);
94
+ return ExitCode.RuntimeError;
95
+ }
96
+ return ExitCode.Ok;
97
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `ama providers add|refresh` 的纯计算部分(docs/providers.md「一键接入」):候选渠道、模型列表的
3
+ * 协议提示、探测结果 → 模型渠道、渠道收敛、追加式合并进配置、表格。
4
+ *
5
+ * 不读写文件、不联网(`fetchModelList` 除外,它只是一次 GET);命令流程在 providers.ts。
6
+ */
7
+ import { type ModelsDevFields, type ModelsDevMatch } from "../../ai/providers/models-dev.js";
8
+ import type { Api } from "../../ai/types.js";
9
+ import type { ProviderConfig } from "../../config/types.js";
10
+ export declare const PROBE_APIS: readonly Api[];
11
+ export declare const DEFAULT_PREFER: readonly string[];
12
+ export interface CandidateChannel {
13
+ name: string;
14
+ api: Api;
15
+ baseUrl: string;
16
+ }
17
+ /** `--channel name=api@baseUrl`。 */
18
+ export declare function parseChannelSpec(spec: string): CandidateChannel;
19
+ /** 从 `--base-url` 推出候选渠道;`api` 给定(非 auto)时只留对应的一个。 */
20
+ export declare function defaultChannels(baseUrl: string, api?: string): CandidateChannel[];
21
+ export interface ListedModel {
22
+ id: string;
23
+ /** new-api 一类中转的 `supported_endpoint_types`。 */
24
+ endpoints?: string[];
25
+ }
26
+ /**
27
+ * 模型列表的提示 → 候选渠道名。没有提示(或提示里没有认识的协议)返回 undefined;有提示但候选渠道
28
+ * 一个都不支持返回 [](该模型在这些渠道上不可用)。
29
+ */
30
+ export declare function hintedChannels(listed: ListedModel, candidates: readonly CandidateChannel[]): string[] | undefined;
31
+ /** 按 `prefer` 排序(不在 prefer 里的保持原顺序排在后面)。 */
32
+ export declare function orderChannels(names: readonly string[], prefer: readonly string[]): string[];
33
+ /** 解析 `GET /models` 的响应体。 */
34
+ export declare function parseModelList(body: unknown): ListedModel[];
35
+ /** 列模型用的渠道与 URL:第一个 OpenAI 系渠道 `{baseUrl}/models`;只有 Messages 时 `/v1/models`。 */
36
+ export declare function listingTarget(candidates: readonly CandidateChannel[]): {
37
+ channel: CandidateChannel;
38
+ url: string;
39
+ } | undefined;
40
+ export type ModelStatus = "ok" | "unprobed" | "failed" | "no-tools" | "no-channel" | "existing";
41
+ export interface ModelPlan {
42
+ id: string;
43
+ channels: string[];
44
+ status: ModelStatus;
45
+ fields: ModelsDevFields;
46
+ match: ModelsDevMatch | undefined;
47
+ /** 探测失败的原因(最后一次)。 */
48
+ error?: string;
49
+ }
50
+ /** 要探测的模型:`--probe-models` 给出的(保持顺序),否则按 id 字母序(不分大小写)取前 limit 个。 */
51
+ export declare function probeSelection(ids: readonly string[], limit: number, only: readonly string[] | undefined): string[];
52
+ /**
53
+ * 按请求上限截断:每个模型要试的渠道数相加超过 `max` 时丢掉后面的模型。返回保留的模型与请求数。
54
+ */
55
+ export declare function capRequests(selected: readonly string[], channelsFor: (id: string) => readonly string[], max: number): {
56
+ ids: string[];
57
+ requests: number;
58
+ dropped: number;
59
+ };
60
+ /**
61
+ * 合并进已有的供应商配置(追加式):已有渠道定义与模型条目一字不改,只加新渠道与新模型;
62
+ * 新模型只写 `id` 与 `channels`。返回新的配置与新增数。
63
+ */
64
+ export declare function mergeProvider(existing: ProviderConfig | undefined, input: {
65
+ name?: string;
66
+ apiKey?: string;
67
+ channels: readonly CandidateChannel[];
68
+ models: readonly {
69
+ id: string;
70
+ channels: readonly string[];
71
+ }[];
72
+ prefer: readonly string[];
73
+ }): {
74
+ config: ProviderConfig;
75
+ addedChannels: string[];
76
+ addedModels: string[];
77
+ };
78
+ /** 表格:id、渠道、上下文、输出、图像、推理、工具、价格($/M 入/出)、状态、models.dev 匹配。 */
79
+ export declare function renderTable(plans: readonly ModelPlan[]): string;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * `ama providers add|refresh` 的纯计算部分(docs/providers.md「一键接入」):候选渠道、模型列表的
3
+ * 协议提示、探测结果 → 模型渠道、渠道收敛、追加式合并进配置、表格。
4
+ *
5
+ * 不读写文件、不联网(`fetchModelList` 除外,它只是一次 GET);命令流程在 providers.ts。
6
+ */
7
+ import { apiShortName } from "../../ai/providers/channels.js";
8
+ import { matchLabel, } from "../../ai/providers/models-dev.js";
9
+ import { CHANNEL_NAME_PATTERN } from "../../config/types.js";
10
+ import { padToWidth, visibleWidth } from "../../tui/ansi.js";
11
+ import { UsageError } from "../args.js";
12
+ import { compactTokens } from "./model-meta.js";
13
+ export const PROBE_APIS = [
14
+ "openai-completions",
15
+ "openai-responses",
16
+ "anthropic-messages",
17
+ ];
18
+ export const DEFAULT_PREFER = ["chat", "responses", "messages"];
19
+ const API_CHOICES = [...PROBE_APIS, "google-generative-ai"];
20
+ /** `--channel name=api@baseUrl`。 */
21
+ export function parseChannelSpec(spec) {
22
+ const m = /^([^=]+)=([^@]+)@(.+)$/.exec(spec.trim());
23
+ if (!m)
24
+ throw new UsageError(`--channel 应为 <名字>=<协议>@<地址>:${spec}`);
25
+ const [, name = "", api = "", baseUrl = ""] = m;
26
+ if (!CHANNEL_NAME_PATTERN.test(name))
27
+ throw new UsageError(`渠道名不合法:${name}`);
28
+ if (!API_CHOICES.includes(api))
29
+ throw new UsageError(`--channel 的协议应为 ${API_CHOICES.join(" | ")}(收到 ${api})`);
30
+ if (!/^https?:\/\//.test(baseUrl))
31
+ throw new UsageError(`--channel 的地址应为 http(s) URL:${baseUrl}`);
32
+ return { name, api: api, baseUrl: baseUrl.replace(/\/+$/, "") };
33
+ }
34
+ /** 从 `--base-url` 推出候选渠道;`api` 给定(非 auto)时只留对应的一个。 */
35
+ export function defaultChannels(baseUrl, api) {
36
+ const base = baseUrl.replace(/\/+$/, "");
37
+ const root = base.replace(/\/v1$/, "");
38
+ const all = [
39
+ { name: "chat", api: "openai-completions", baseUrl: base },
40
+ { name: "responses", api: "openai-responses", baseUrl: base },
41
+ { name: "messages", api: "anthropic-messages", baseUrl: root },
42
+ ];
43
+ if (api === undefined || api === "auto")
44
+ return all;
45
+ if (!API_CHOICES.includes(api))
46
+ throw new UsageError(`--api 的取值应为 ${[...PROBE_APIS, "auto"].join(" | ")}(收到 ${api})`);
47
+ const one = all.find((c) => c.api === api);
48
+ return [one ?? { name: apiShortName(api), api: api, baseUrl: base }];
49
+ }
50
+ const ENDPOINT_APIS = {
51
+ openai: "openai-completions",
52
+ "openai-response": "openai-responses",
53
+ "openai-responses": "openai-responses",
54
+ responses: "openai-responses",
55
+ anthropic: "anthropic-messages",
56
+ gemini: "google-generative-ai",
57
+ };
58
+ /**
59
+ * 模型列表的提示 → 候选渠道名。没有提示(或提示里没有认识的协议)返回 undefined;有提示但候选渠道
60
+ * 一个都不支持返回 [](该模型在这些渠道上不可用)。
61
+ */
62
+ export function hintedChannels(listed, candidates) {
63
+ if (listed.endpoints === undefined || listed.endpoints.length === 0)
64
+ return undefined;
65
+ const apis = new Set(listed.endpoints.map((e) => ENDPOINT_APIS[e.toLowerCase()]).filter((a) => a !== undefined));
66
+ if (apis.size === 0)
67
+ return undefined;
68
+ return candidates.filter((c) => apis.has(c.api)).map((c) => c.name);
69
+ }
70
+ /** 按 `prefer` 排序(不在 prefer 里的保持原顺序排在后面)。 */
71
+ export function orderChannels(names, prefer) {
72
+ const rank = (n) => {
73
+ const i = prefer.indexOf(n);
74
+ return i < 0 ? prefer.length : i;
75
+ };
76
+ return [...new Set(names)].sort((a, b) => rank(a) - rank(b));
77
+ }
78
+ /** 解析 `GET /models` 的响应体。 */
79
+ export function parseModelList(body) {
80
+ const data = body?.data;
81
+ if (!Array.isArray(data))
82
+ return [];
83
+ const seen = new Set();
84
+ const out = [];
85
+ for (const item of data) {
86
+ const id = item?.id;
87
+ if (typeof id !== "string" || id === "" || seen.has(id))
88
+ continue;
89
+ seen.add(id);
90
+ const endpoints = item.supported_endpoint_types;
91
+ const listed = { id };
92
+ if (Array.isArray(endpoints))
93
+ listed.endpoints = endpoints.filter((e) => typeof e === "string");
94
+ out.push(listed);
95
+ }
96
+ return out;
97
+ }
98
+ /** 列模型用的渠道与 URL:第一个 OpenAI 系渠道 `{baseUrl}/models`;只有 Messages 时 `/v1/models`。 */
99
+ export function listingTarget(candidates) {
100
+ const openai = candidates.find((c) => c.api === "openai-completions" || c.api === "openai-responses");
101
+ const channel = openai ?? candidates[0];
102
+ if (channel === undefined)
103
+ return undefined;
104
+ const base = channel.baseUrl.replace(/\/+$/, "");
105
+ const url = channel.api === "anthropic-messages" && !/\/v1$/.test(base)
106
+ ? `${base}/v1/models`
107
+ : `${base}/models`;
108
+ return { channel, url };
109
+ }
110
+ /** 要探测的模型:`--probe-models` 给出的(保持顺序),否则按 id 字母序(不分大小写)取前 limit 个。 */
111
+ export function probeSelection(ids, limit, only) {
112
+ if (only !== undefined)
113
+ return only.filter((id) => ids.includes(id));
114
+ return [...ids].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).slice(0, limit);
115
+ }
116
+ /**
117
+ * 按请求上限截断:每个模型要试的渠道数相加超过 `max` 时丢掉后面的模型。返回保留的模型与请求数。
118
+ */
119
+ export function capRequests(selected, channelsFor, max) {
120
+ const ids = [];
121
+ let requests = 0;
122
+ for (const id of selected) {
123
+ const n = channelsFor(id).length;
124
+ if (requests + n > max)
125
+ break;
126
+ ids.push(id);
127
+ requests += n;
128
+ }
129
+ return { ids, requests, dropped: selected.length - ids.length };
130
+ }
131
+ /**
132
+ * 合并进已有的供应商配置(追加式):已有渠道定义与模型条目一字不改,只加新渠道与新模型;
133
+ * 新模型只写 `id` 与 `channels`。返回新的配置与新增数。
134
+ */
135
+ export function mergeProvider(existing, input) {
136
+ const config = structuredClone(existing ?? {});
137
+ if (input.name !== undefined && config.name === undefined)
138
+ config.name = input.name;
139
+ if (input.apiKey !== undefined && config.apiKey === undefined)
140
+ config.apiKey = input.apiKey;
141
+ const channels = config.channels ?? {};
142
+ const used = new Set(input.models.flatMap((m) => m.channels));
143
+ const addedChannels = [];
144
+ for (const c of input.channels) {
145
+ if (channels[c.name] !== undefined || !used.has(c.name))
146
+ continue;
147
+ channels[c.name] = { api: c.api, baseUrl: c.baseUrl };
148
+ addedChannels.push(c.name);
149
+ }
150
+ config.channels = channels;
151
+ if (config.defaultChannel === undefined || channels[config.defaultChannel] === undefined) {
152
+ const first = orderChannels(Object.keys(channels), input.prefer)[0];
153
+ if (first !== undefined)
154
+ config.defaultChannel = first;
155
+ }
156
+ const models = config.models ?? [];
157
+ const known = new Set(models.map((m) => m.id));
158
+ const addedModels = [];
159
+ for (const m of input.models) {
160
+ if (known.has(m.id) || m.channels.length === 0)
161
+ continue;
162
+ models.push({ id: m.id, channels: [...m.channels] });
163
+ known.add(m.id);
164
+ addedModels.push(m.id);
165
+ }
166
+ config.models = models;
167
+ return { config, addedChannels, addedModels };
168
+ }
169
+ const STATUS_TEXT = {
170
+ ok: "探测通过",
171
+ unprobed: "未探测",
172
+ failed: "探测失败,不写入",
173
+ "no-tools": "不支持工具调用,不写入",
174
+ "no-channel": "候选渠道都不支持,不写入",
175
+ existing: "已存在,未改动",
176
+ };
177
+ function cost(fields) {
178
+ return fields.cost !== undefined ? `$${fields.cost.input}/${fields.cost.output}` : "—";
179
+ }
180
+ function yesNo(value) {
181
+ return value === undefined ? "?" : value ? "是" : "否";
182
+ }
183
+ /** 表格:id、渠道、上下文、输出、图像、推理、工具、价格($/M 入/出)、状态、models.dev 匹配。 */
184
+ export function renderTable(plans) {
185
+ const head = [
186
+ "模型",
187
+ "渠道",
188
+ "上下文",
189
+ "输出",
190
+ "图像",
191
+ "推理",
192
+ "工具",
193
+ "价格$/M",
194
+ "状态",
195
+ "models.dev",
196
+ ];
197
+ const rows = plans.map((p) => [
198
+ p.id,
199
+ p.channels.length > 0 ? p.channels.join(",") : "—",
200
+ compactTokens(p.fields.contextWindow),
201
+ compactTokens(p.fields.maxTokens),
202
+ p.fields.input === undefined ? "?" : p.fields.input.includes("image") ? "是" : "否",
203
+ yesNo(p.fields.reasoning),
204
+ yesNo(p.fields.toolCall),
205
+ cost(p.fields),
206
+ STATUS_TEXT[p.status],
207
+ matchLabel(p.match),
208
+ ]);
209
+ const widths = head.map((h, i) => Math.max(visibleWidth(h), ...rows.map((r) => visibleWidth(r[i] ?? ""))));
210
+ const line = (cells) => cells
211
+ .map((cell, i) => (i === cells.length - 1 ? cell : padToWidth(cell, widths[i] ?? 0)))
212
+ .join(" ")
213
+ .trimEnd();
214
+ return [line(head), ...rows.map(line)].join("\n") + "\n";
215
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `ama providers add|list|channels|remove|refresh`(docs/providers.md「一键接入」「渠道」)。
3
+ *
4
+ * - add:只给 baseUrl 与 key,列出中转的模型(`GET /models`)、用 models.dev 补元数据、可选逐渠道
5
+ * 探测,把渠道与模型追加进用户级 config.json(先备份);key 从 stdin 读(不回显)存 auth.json,
6
+ * 或 `--key-env VAR` 写 `"apiKey": "$VAR"`。对已存在的供应商只追加,不改已有条目。
7
+ * - refresh:重拉 `/models` 与 models.dev,只追加新模型;已下架的只提示。
8
+ * - list / channels:供应商 → 渠道 → 模型数与 key 来源(从不显示 key)。remove:删配置与 auth.json 条目。
9
+ *
10
+ * 计费:`--probe` 每模型每渠道一次最小请求;执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
11
+ */
12
+ import type { AmaConfig } from "../../config/types.js";
13
+ import type { CliIo, RuntimeDeps } from "../deps.js";
14
+ import { type UserLevel } from "./context.js";
15
+ export declare const DEFAULT_MAX_REQUESTS = 60;
16
+ export declare const DEFAULT_LIMIT = 30;
17
+ export declare const PROVIDERS_USAGE = "\u7528\u6CD5\uFF1Aama providers add <id> --base-url <url> [--channel <\u540D\u5B57>=<\u534F\u8BAE>@<\u5730\u5740> \u2026]\n [--api openai-completions|openai-responses|anthropic-messages|auto]\n [--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,\u2026]\n [--max-requests N] [--prefer chat,responses,messages]\n [--include-no-tools] [--yes]\n ama providers list\n ama providers channels <id>\n ama providers remove <id>\n ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,\u2026] [--max-requests N] [--yes]\n";
18
+ export interface Ctx {
19
+ io: CliIo;
20
+ deps: Pick<RuntimeDeps, "providers">;
21
+ level: UserLevel;
22
+ values: ReadonlyMap<string, string>;
23
+ flags: ReadonlySet<string>;
24
+ channelSpecs: string[];
25
+ }
26
+ export declare function userConfig(level: UserLevel): AmaConfig;
27
+ export declare function runProviders(argv: readonly string[], io: CliIo, deps: Pick<RuntimeDeps, "providers"> | undefined): Promise<number>;