@armadra/agent 0.2.1 → 0.4.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 (232) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +142 -49
  3. package/dist/agent/retry.d.ts +1 -1
  4. package/dist/agent/retry.js +2 -1
  5. package/dist/agent/session-cache.js +6 -3
  6. package/dist/agent/session-classifier.d.ts +18 -0
  7. package/dist/agent/session-classifier.js +103 -0
  8. package/dist/agent/session-core.d.ts +13 -0
  9. package/dist/agent/session-state.js +2 -1
  10. package/dist/agent/session-sync.js +9 -2
  11. package/dist/agent/session-tools.js +24 -3
  12. package/dist/agent/session.d.ts +3 -0
  13. package/dist/agent/session.js +20 -10
  14. package/dist/agent/tool-runner.js +15 -2
  15. package/dist/agent/types.d.ts +9 -1
  16. package/dist/ai/apis/anthropic-messages.js +3 -2
  17. package/dist/ai/apis/google-generative-ai.js +3 -2
  18. package/dist/ai/apis/openai-completions.js +3 -2
  19. package/dist/ai/apis/openai-responses.js +5 -3
  20. package/dist/ai/cache/fingerprint.d.ts +1 -1
  21. package/dist/ai/cache/fingerprint.js +1 -1
  22. package/dist/ai/cache/reporting.js +2 -1
  23. package/dist/ai/http.d.ts +28 -6
  24. package/dist/ai/http.js +41 -8
  25. package/dist/ai/providers/channels.d.ts +51 -0
  26. package/dist/ai/providers/channels.js +96 -0
  27. package/dist/ai/providers/enrich.d.ts +34 -0
  28. package/dist/ai/providers/enrich.js +86 -0
  29. package/dist/ai/providers/models-dev-cache.d.ts +53 -0
  30. package/dist/ai/providers/models-dev-cache.js +147 -0
  31. package/dist/ai/providers/models-dev.d.ts +99 -0
  32. package/dist/ai/providers/models-dev.js +315 -0
  33. package/dist/ai/providers/registry.d.ts +42 -4
  34. package/dist/ai/providers/registry.js +195 -34
  35. package/dist/ai/providers/suggest.d.ts +18 -0
  36. package/dist/ai/providers/suggest.js +72 -0
  37. package/dist/ai/sse.d.ts +6 -2
  38. package/dist/ai/sse.js +22 -2
  39. package/dist/ai/types.d.ts +30 -3
  40. package/dist/bundle/ama.cjs +17893 -10698
  41. package/dist/cli/args.d.ts +23 -5
  42. package/dist/cli/args.js +113 -13
  43. package/dist/cli/bootstrap.js +28 -3
  44. package/dist/cli/codemode-notice.d.ts +20 -0
  45. package/dist/cli/codemode-notice.js +55 -0
  46. package/dist/cli/compose-providers.js +3 -0
  47. package/dist/cli/compose-session.d.ts +8 -0
  48. package/dist/cli/compose-session.js +32 -1
  49. package/dist/cli/compose-store.d.ts +1 -1
  50. package/dist/cli/compose-store.js +3 -1
  51. package/dist/cli/compose.d.ts +17 -5
  52. package/dist/cli/compose.js +53 -19
  53. package/dist/cli/default-model.d.ts +38 -1
  54. package/dist/cli/default-model.js +96 -9
  55. package/dist/cli/deps.d.ts +38 -0
  56. package/dist/cli/exit-codes.d.ts +2 -0
  57. package/dist/cli/exit-codes.js +3 -0
  58. package/dist/cli/fake-visibility.d.ts +13 -0
  59. package/dist/cli/fake-visibility.js +26 -0
  60. package/dist/cli/from-prompt.d.ts +18 -0
  61. package/dist/cli/from-prompt.js +49 -0
  62. package/dist/cli/main.d.ts +10 -2
  63. package/dist/cli/main.js +88 -2
  64. package/dist/cli/proxy.d.ts +51 -0
  65. package/dist/cli/proxy.js +135 -0
  66. package/dist/cli/startup-screen.d.ts +29 -0
  67. package/dist/cli/startup-screen.js +47 -0
  68. package/dist/cli/startup-steps.d.ts +1 -1
  69. package/dist/cli/startup-steps.js +20 -13
  70. package/dist/cli/subcommands/config.d.ts +29 -4
  71. package/dist/cli/subcommands/config.js +188 -25
  72. package/dist/cli/subcommands/context.js +5 -1
  73. package/dist/cli/subcommands/doctor.js +12 -1
  74. package/dist/cli/subcommands/init.d.ts +6 -0
  75. package/dist/cli/subcommands/init.js +23 -0
  76. package/dist/cli/subcommands/model-meta.d.ts +16 -0
  77. package/dist/cli/subcommands/model-meta.js +54 -0
  78. package/dist/cli/subcommands/models-cache-probe.js +1 -1
  79. package/dist/cli/subcommands/models-discover.d.ts +18 -6
  80. package/dist/cli/subcommands/models-discover.js +87 -52
  81. package/dist/cli/subcommands/models.js +26 -18
  82. package/dist/cli/subcommands/probe-runner.d.ts +96 -0
  83. package/dist/cli/subcommands/probe-runner.js +264 -0
  84. package/dist/cli/subcommands/providers-list.d.ts +8 -0
  85. package/dist/cli/subcommands/providers-list.js +97 -0
  86. package/dist/cli/subcommands/providers-plan.d.ts +79 -0
  87. package/dist/cli/subcommands/providers-plan.js +215 -0
  88. package/dist/cli/subcommands/providers-probe.d.ts +34 -0
  89. package/dist/cli/subcommands/providers-probe.js +87 -0
  90. package/dist/cli/subcommands/providers.d.ts +28 -0
  91. package/dist/cli/subcommands/providers.js +436 -0
  92. package/dist/cli/subcommands/sessions-export.d.ts +10 -0
  93. package/dist/cli/subcommands/sessions-export.js +59 -0
  94. package/dist/cli/subcommands/sessions-search.d.ts +13 -0
  95. package/dist/cli/subcommands/sessions-search.js +103 -0
  96. package/dist/cli/subcommands/sessions.d.ts +3 -2
  97. package/dist/cli/subcommands/sessions.js +22 -1
  98. package/dist/cli/subcommands/stats.d.ts +17 -0
  99. package/dist/cli/subcommands/stats.js +198 -0
  100. package/dist/cli/system-prompt-arg.d.ts +11 -0
  101. package/dist/cli/system-prompt-arg.js +34 -0
  102. package/dist/codemode/modes.d.ts +4 -11
  103. package/dist/codemode/modes.js +5 -27
  104. package/dist/codemode/tool.d.ts +18 -14
  105. package/dist/codemode/tool.js +71 -28
  106. package/dist/config/init.d.ts +34 -0
  107. package/dist/config/init.js +99 -0
  108. package/dist/config/json-schema.d.ts +15 -0
  109. package/dist/config/json-schema.js +215 -0
  110. package/dist/config/key-docs.d.ts +22 -0
  111. package/dist/config/key-docs.js +105 -0
  112. package/dist/config/merge.d.ts +9 -8
  113. package/dist/config/merge.js +22 -7
  114. package/dist/config/schema.d.ts +1 -1
  115. package/dist/config/schema.js +97 -15
  116. package/dist/config/types.d.ts +57 -11
  117. package/dist/config/types.js +19 -1
  118. package/dist/modes/commands-core.js +6 -5
  119. package/dist/modes/image-input.d.ts +27 -0
  120. package/dist/modes/image-input.js +78 -0
  121. package/dist/modes/interactive/approval-dialog.d.ts +21 -5
  122. package/dist/modes/interactive/approval-dialog.js +106 -27
  123. package/dist/modes/interactive/commands.d.ts +11 -2
  124. package/dist/modes/interactive/commands.js +58 -19
  125. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  126. package/dist/modes/interactive/interactive-mode.js +70 -82
  127. package/dist/modes/interactive/key-dispatch.d.ts +3 -1
  128. package/dist/modes/interactive/key-dispatch.js +5 -6
  129. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  130. package/dist/modes/interactive/line/line-mode.js +9 -5
  131. package/dist/modes/interactive/line/line-render.d.ts +4 -0
  132. package/dist/modes/interactive/line/line-render.js +28 -6
  133. package/dist/modes/interactive/message-view.d.ts +48 -9
  134. package/dist/modes/interactive/message-view.js +238 -44
  135. package/dist/modes/interactive/panels.d.ts +18 -0
  136. package/dist/modes/interactive/panels.js +143 -0
  137. package/dist/modes/interactive/pickers.d.ts +23 -2
  138. package/dist/modes/interactive/pickers.js +48 -15
  139. package/dist/modes/interactive/run-indicator.d.ts +51 -0
  140. package/dist/modes/interactive/run-indicator.js +189 -0
  141. package/dist/modes/interactive/startup-header.d.ts +40 -0
  142. package/dist/modes/interactive/startup-header.js +169 -0
  143. package/dist/modes/interactive/startup-ui.d.ts +7 -2
  144. package/dist/modes/interactive/startup-ui.js +41 -11
  145. package/dist/modes/interactive/status-bar.d.ts +18 -15
  146. package/dist/modes/interactive/status-bar.js +98 -56
  147. package/dist/modes/interactive/tool-summary.d.ts +46 -0
  148. package/dist/modes/interactive/tool-summary.js +218 -0
  149. package/dist/modes/interactive/tool-view.d.ts +48 -15
  150. package/dist/modes/interactive/tool-view.js +203 -145
  151. package/dist/modes/print/print-mode.d.ts +33 -4
  152. package/dist/modes/print/print-mode.js +128 -8
  153. package/dist/modes/rpc/commands.js +12 -2
  154. package/dist/permissions/auto-safe.d.ts +60 -0
  155. package/dist/permissions/auto-safe.js +529 -0
  156. package/dist/permissions/classifier.d.ts +64 -0
  157. package/dist/permissions/classifier.js +184 -0
  158. package/dist/permissions/dangerous.d.ts +5 -0
  159. package/dist/permissions/dangerous.js +1 -1
  160. package/dist/permissions/modes.d.ts +30 -0
  161. package/dist/permissions/modes.js +78 -0
  162. package/dist/permissions/pipeline.d.ts +31 -4
  163. package/dist/permissions/pipeline.js +196 -6
  164. package/dist/permissions/protected.d.ts +19 -0
  165. package/dist/permissions/protected.js +74 -0
  166. package/dist/permissions/rules.js +3 -0
  167. package/dist/permissions/types.d.ts +50 -3
  168. package/dist/permissions/types.js +3 -0
  169. package/dist/rpc.d.ts +2 -0
  170. package/dist/sdk.d.ts +9 -3
  171. package/dist/sdk.js +10 -2
  172. package/dist/session/export.d.ts +32 -0
  173. package/dist/session/export.js +187 -0
  174. package/dist/session/projection.js +5 -1
  175. package/dist/session/redact.d.ts +15 -0
  176. package/dist/session/redact.js +55 -0
  177. package/dist/session/reuse.d.ts +33 -0
  178. package/dist/session/reuse.js +86 -0
  179. package/dist/session/scan.d.ts +34 -0
  180. package/dist/session/scan.js +140 -0
  181. package/dist/session/search.d.ts +52 -0
  182. package/dist/session/search.js +211 -0
  183. package/dist/session/stats-aggregate.d.ts +63 -0
  184. package/dist/session/stats-aggregate.js +163 -0
  185. package/dist/session/stats-index.d.ts +26 -0
  186. package/dist/session/stats-index.js +91 -0
  187. package/dist/session/stats-scan.d.ts +54 -0
  188. package/dist/session/stats-scan.js +236 -0
  189. package/dist/session/types.d.ts +2 -0
  190. package/dist/tools/image-file.d.ts +31 -0
  191. package/dist/tools/image-file.js +114 -0
  192. package/dist/tools/presets.d.ts +35 -8
  193. package/dist/tools/presets.js +56 -17
  194. package/dist/tools/read.d.ts +4 -8
  195. package/dist/tools/read.js +15 -64
  196. package/dist/tui/component.d.ts +8 -2
  197. package/dist/tui/component.js +3 -1
  198. package/dist/tui/components/box.d.ts +6 -1
  199. package/dist/tui/components/box.js +16 -6
  200. package/dist/tui/components/card.d.ts +23 -0
  201. package/dist/tui/components/card.js +37 -0
  202. package/dist/tui/components/editor-history.d.ts +6 -0
  203. package/dist/tui/components/editor-history.js +45 -0
  204. package/dist/tui/components/editor-paste.d.ts +1 -1
  205. package/dist/tui/components/editor-paste.js +4 -4
  206. package/dist/tui/components/editor.d.ts +11 -5
  207. package/dist/tui/components/editor.js +52 -58
  208. package/dist/tui/components/key-value.d.ts +3 -0
  209. package/dist/tui/components/key-value.js +16 -6
  210. package/dist/tui/components/loader.d.ts +37 -7
  211. package/dist/tui/components/loader.js +84 -21
  212. package/dist/tui/components/markdown.d.ts +5 -1
  213. package/dist/tui/components/markdown.js +45 -21
  214. package/dist/tui/components/meter.d.ts +3 -3
  215. package/dist/tui/components/meter.js +13 -11
  216. package/dist/tui/components/select-list.d.ts +23 -1
  217. package/dist/tui/components/select-list.js +76 -13
  218. package/dist/tui/glyphs.d.ts +68 -0
  219. package/dist/tui/glyphs.js +114 -0
  220. package/dist/tui/theme.d.ts +30 -6
  221. package/dist/tui/theme.js +103 -17
  222. package/dist/tui.d.ts +4 -2
  223. package/dist/tui.js +3 -1
  224. package/docs/codemode.md +23 -9
  225. package/docs/hooks.md +10 -10
  226. package/docs/permissions.md +148 -0
  227. package/docs/providers.md +241 -3
  228. package/docs/rpc.md +38 -38
  229. package/docs/session-format.md +3 -2
  230. package/docs/sessions.md +134 -0
  231. package/docs/tui.md +140 -61
  232. package/package.json +3 -1
@@ -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,34 @@
1
+ /**
2
+ * `ama providers add|refresh --probe` 的逐渠道探测:任务是「模型 × 渠道」,有界并发执行(见
3
+ * probe-runner.ts);每个模型的所有渠道探完后按模型顺序打印一行,不按完成顺序。
4
+ */
5
+ import type { ProviderRegistryApi } from "../../ai/types.js";
6
+ import type { CliIo } from "../deps.js";
7
+ import type { CandidateChannel } from "./providers-plan.js";
8
+ export interface ChannelProbeResult {
9
+ /** 探测成功的渠道(按渠道尝试顺序)。 */
10
+ ok: string[];
11
+ /** 最后一个失败渠道的原因(首行,≤ 120 字符)。 */
12
+ error?: string;
13
+ }
14
+ export interface ChannelProbeInput {
15
+ io: Pick<CliIo, "stdout" | "stderr" | "stdoutIsTTY">;
16
+ providerId: string;
17
+ ids: readonly string[];
18
+ /** 每个模型要试的渠道名(顺序即输出顺序)。 */
19
+ channelsFor: (id: string) => readonly string[];
20
+ candidates: readonly CandidateChannel[];
21
+ registry: ProviderRegistryApi;
22
+ apiKey: string | undefined;
23
+ concurrency: number;
24
+ timeoutMs: number;
25
+ /** 测试用:429 重试前的退避。 */
26
+ retryDelayMs?: number;
27
+ }
28
+ /**
29
+ * 探测全部「模型 × 渠道」。返回有结论的模型(全部渠道探完,或因停止没探完但已有成功渠道)与停止原因。
30
+ */
31
+ export declare function probeChannels(input: ChannelProbeInput): Promise<{
32
+ results: Map<string, ChannelProbeResult>;
33
+ stopped?: string;
34
+ }>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `ama providers add|refresh --probe` 的逐渠道探测:任务是「模型 × 渠道」,有界并发执行(见
3
+ * probe-runner.ts);每个模型的所有渠道探完后按模型顺序打印一行,不按完成顺序。
4
+ */
5
+ import { withCustomDefaults } from "../../ai/providers/catalog.js";
6
+ import { materializeModel } from "../../ai/providers/registry.js";
7
+ import { ProbeProgress, ProbeScheduler } from "./probe-runner.js";
8
+ /** 渠道的探测用供应商数据(key 由调用方传入)。 */
9
+ function probeProvider(id, channel) {
10
+ return {
11
+ id,
12
+ name: id,
13
+ api: channel.api,
14
+ baseUrl: channel.baseUrl,
15
+ envKeys: [],
16
+ models: [],
17
+ requiresApiKey: true,
18
+ builtin: false,
19
+ };
20
+ }
21
+ const firstLine = (error) => error.split("\n")[0]?.slice(0, 120) ?? error;
22
+ /**
23
+ * 探测全部「模型 × 渠道」。返回有结论的模型(全部渠道探完,或因停止没探完但已有成功渠道)与停止原因。
24
+ */
25
+ export async function probeChannels(input) {
26
+ const { io } = input;
27
+ const tasks = input.ids.flatMap((id) => input.channelsFor(id).map((name) => ({ id, name })));
28
+ const pending = new Map(input.ids.map((id) => [id, input.channelsFor(id).length]));
29
+ const outcomes = new Map();
30
+ const results = new Map();
31
+ const progress = new ProbeProgress(io.stdout, io.stdoutIsTTY, tasks.length);
32
+ const scheduler = new ProbeScheduler(input.registry, input.apiKey, {
33
+ concurrency: input.concurrency,
34
+ timeoutMs: input.timeoutMs,
35
+ retryDelayMs: input.retryDelayMs,
36
+ onThrottle: (n) => progress.line(` 遇到 429 限流,并发降到 ${n},稍后重试一次\n`),
37
+ onRecover: (n) => progress.line(` 一段时间没再限流,并发回升到 ${n}\n`),
38
+ });
39
+ let printed = 0;
40
+ /** 按模型顺序输出:前面的模型都探完才输出后面的。 */
41
+ const flush = (final) => {
42
+ while (printed < input.ids.length) {
43
+ const id = input.ids[printed];
44
+ if (!final && (pending.get(id) ?? 0) > 0)
45
+ return;
46
+ printed++;
47
+ const result = { ok: [] };
48
+ const failed = [];
49
+ let complete = true;
50
+ for (const name of input.channelsFor(id)) {
51
+ const outcome = outcomes.get(`${id}\n${name}`);
52
+ if (outcome === undefined || outcome.aborted)
53
+ complete = false;
54
+ else if (outcome.error === undefined)
55
+ result.ok.push(name);
56
+ else {
57
+ result.error = firstLine(outcome.error);
58
+ failed.push(`${name}:${result.error}`);
59
+ }
60
+ }
61
+ if (!complete && result.ok.length === 0)
62
+ continue;
63
+ results.set(id, result);
64
+ progress.line(` ${id} ${result.ok.length > 0 ? result.ok.join(", ") : "全部失败"}` +
65
+ `${failed.length > 0 ? `(失败 ${failed.join(";")})` : ""}` +
66
+ `${complete ? "" : "(探测提前停止,部分渠道未探)"}\n`);
67
+ }
68
+ };
69
+ await scheduler.run(tasks.length, async (index) => {
70
+ const { id, name } = tasks[index];
71
+ const channel = input.candidates.find((c) => c.name === name);
72
+ const provider = probeProvider(input.providerId, channel);
73
+ const model = materializeModel(withCustomDefaults({ id }, input.providerId, channel.api), provider);
74
+ return scheduler.probe(model);
75
+ }, (index, outcome) => {
76
+ const { id, name } = tasks[index];
77
+ outcomes.set(`${id}\n${name}`, outcome);
78
+ pending.set(id, (pending.get(id) ?? 1) - 1);
79
+ if (!outcome.aborted)
80
+ progress.tick();
81
+ flush(false);
82
+ });
83
+ flush(true);
84
+ progress.finish();
85
+ const stopped = scheduler.stopped;
86
+ return { results, ...(stopped !== undefined ? { stopped } : {}) };
87
+ }
@@ -0,0 +1,28 @@
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` 每模型每渠道一次最小请求(并发执行,见到首个流事件即判可用并断开,见
11
+ * probe-runner.ts);执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
12
+ */
13
+ import type { AmaConfig } from "../../config/types.js";
14
+ import type { CliIo, RuntimeDeps } from "../deps.js";
15
+ import { type UserLevel } from "./context.js";
16
+ export declare const DEFAULT_MAX_REQUESTS = 60;
17
+ export declare const DEFAULT_LIMIT = 30;
18
+ 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] [--concurrency N] [--probe-timeout ms]\n [--prefer chat,responses,messages] [--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]\n [--concurrency N] [--probe-timeout ms] [--yes]\n";
19
+ export interface Ctx {
20
+ io: CliIo;
21
+ deps: Pick<RuntimeDeps, "providers">;
22
+ level: UserLevel;
23
+ values: ReadonlyMap<string, string>;
24
+ flags: ReadonlySet<string>;
25
+ channelSpecs: string[];
26
+ }
27
+ export declare function userConfig(level: UserLevel): AmaConfig;
28
+ export declare function runProviders(argv: readonly string[], io: CliIo, deps: Pick<RuntimeDeps, "providers"> | undefined): Promise<number>;