@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,315 @@
1
+ /**
2
+ * models.dev 模型元数据(docs/providers.md「模型元数据:models.dev」):裁剪、索引、匹配、映射。
3
+ *
4
+ * 数据源 `https://models.dev/api.json` 是「供应商 → 模型」两层表,同一个模型 id 会在几十家转售商下
5
+ * 重复出现且取值不一。`ModelsDevIndex.match()` 按文档里的顺序挑一条:显式 `provider/model` →
6
+ * `vendor/model` 形状的 id → 同名条目的 `canonical_model_id` 指向的原厂条目 → 原厂供应商 →
7
+ * 多数一致 → 归一化 id(去前缀 / `:后缀` / `-latest` / 日期)后重试 → 未匹配。
8
+ *
9
+ * 本模块只做纯计算(不读文件、不联网);缓存与拉取在 models-dev-cache.ts。
10
+ */
11
+ /**
12
+ * 原厂供应商(models.dev 的供应商 id,顺序即优先级)。通义在 `alibaba`、Llama 在 `llama`、
13
+ * 智谱国际站在 `zai`;models.dev 没有 `qwen` / `meta` 这样的供应商 id。
14
+ */
15
+ export const FIRST_PARTY_PROVIDERS = [
16
+ "anthropic",
17
+ "openai",
18
+ "google",
19
+ "deepseek",
20
+ "moonshotai",
21
+ "moonshotai-cn",
22
+ "zhipuai",
23
+ "zai",
24
+ "alibaba",
25
+ "alibaba-cn",
26
+ "xai",
27
+ "mistral",
28
+ "minimax",
29
+ "minimax-cn",
30
+ "llama",
31
+ "cohere",
32
+ "xiaomi",
33
+ "stepfun",
34
+ "perplexity",
35
+ "ai21",
36
+ "upstage",
37
+ "inception",
38
+ ];
39
+ /** maxTokens 的上限:models.dev 给原厂上限(常与上下文相同),每次请求都会作为 max_tokens 发出。 */
40
+ export const MODELS_DEV_MAX_OUTPUT = 65_536;
41
+ function isRecord(value) {
42
+ return typeof value === "object" && value !== null && !Array.isArray(value);
43
+ }
44
+ function positive(value) {
45
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
46
+ }
47
+ function nonNegative(value) {
48
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
49
+ }
50
+ function strings(value) {
51
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : undefined;
52
+ }
53
+ function trimModel(id, raw) {
54
+ const out = { id };
55
+ if (typeof raw["name"] === "string")
56
+ out.name = raw["name"];
57
+ for (const key of ["reasoning", "tool_call"]) {
58
+ if (typeof raw[key] === "boolean")
59
+ out[key] = raw[key];
60
+ }
61
+ if (typeof raw["canonical_model_id"] === "string")
62
+ out.canonical_model_id = raw["canonical_model_id"];
63
+ const modalities = raw["modalities"];
64
+ if (isRecord(modalities)) {
65
+ const input = strings(modalities["input"]);
66
+ if (input)
67
+ out.modalities = { input };
68
+ }
69
+ const limit = raw["limit"];
70
+ if (isRecord(limit)) {
71
+ const context = positive(limit["context"]);
72
+ const output = positive(limit["output"]);
73
+ out.limit = {
74
+ ...(context !== undefined ? { context } : {}),
75
+ ...(output !== undefined ? { output } : {}),
76
+ };
77
+ }
78
+ const cost = raw["cost"];
79
+ if (isRecord(cost)) {
80
+ const c = {};
81
+ for (const key of ["input", "output", "cache_read", "cache_write"]) {
82
+ const v = nonNegative(cost[key]);
83
+ if (v !== undefined)
84
+ c[key] = v;
85
+ }
86
+ out.cost = c;
87
+ }
88
+ return out;
89
+ }
90
+ /** 校验并裁剪 api.json:只留用到的字段;形状不对的供应商 / 模型跳过。整体不是对象抛错。 */
91
+ export function trimModelsDev(raw) {
92
+ if (!isRecord(raw))
93
+ throw new Error("models.dev 数据不是对象");
94
+ const out = {};
95
+ for (const [providerId, provider] of Object.entries(raw)) {
96
+ if (!isRecord(provider) || !isRecord(provider["models"]))
97
+ continue;
98
+ const models = {};
99
+ for (const [modelId, model] of Object.entries(provider["models"])) {
100
+ if (isRecord(model))
101
+ models[modelId] = trimModel(modelId, model);
102
+ }
103
+ out[providerId] = {
104
+ id: providerId,
105
+ ...(typeof provider["name"] === "string" ? { name: provider["name"] } : {}),
106
+ models,
107
+ };
108
+ }
109
+ return out;
110
+ }
111
+ /**
112
+ * 依次尝试的 id(小写、去重):原样 → 去 `vendor/` 前缀 → 去 `:free` 一类后缀 → 去 `-latest` →
113
+ * 去日期后缀(`-0902`、`-20250514`、`-2025-05-14`)。
114
+ */
115
+ export function idCandidates(id) {
116
+ const out = [];
117
+ const add = (value) => {
118
+ const v = value.trim();
119
+ if (v !== "" && !out.includes(v))
120
+ out.push(v);
121
+ };
122
+ let s = id.trim().toLowerCase();
123
+ add(s);
124
+ s = s.slice(s.lastIndexOf("/") + 1);
125
+ add(s);
126
+ s = s.replace(/:[a-z0-9._-]+$/, "");
127
+ add(s);
128
+ s = s.replace(/-latest$/, "");
129
+ add(s);
130
+ s = s.replace(/-(?:\d{4}-\d{2}-\d{2}|\d{8}|\d{6}|\d{4})$/, "");
131
+ add(s);
132
+ return out;
133
+ }
134
+ function refOf(entry) {
135
+ return `${entry.provider}/${entry.id}`;
136
+ }
137
+ function vendorRank(provider) {
138
+ const i = FIRST_PARTY_PROVIDERS.indexOf(provider);
139
+ return i < 0 ? Number.POSITIVE_INFINITY : i;
140
+ }
141
+ function hasImage(model) {
142
+ return model.modalities?.input?.includes("image") === true;
143
+ }
144
+ function signature(model) {
145
+ return `${model.limit?.context ?? "?"}/${model.limit?.output ?? "?"}/${hasImage(model) ? "img" : "text"}`;
146
+ }
147
+ export class ModelsDevIndex {
148
+ byId = new Map();
149
+ byRef = new Map();
150
+ data;
151
+ constructor(data) {
152
+ this.data = data;
153
+ for (const [providerId, provider] of Object.entries(data)) {
154
+ for (const [modelId, model] of Object.entries(provider.models)) {
155
+ const entry = { provider: providerId, id: modelId, model };
156
+ const key = modelId.toLowerCase();
157
+ const list = this.byId.get(key);
158
+ if (list)
159
+ list.push(entry);
160
+ else
161
+ this.byId.set(key, [entry]);
162
+ this.byRef.set(`${providerId}/${modelId}`.toLowerCase(), entry);
163
+ }
164
+ }
165
+ }
166
+ get providerCount() {
167
+ return Object.keys(this.data).length;
168
+ }
169
+ get modelCount() {
170
+ return this.byRef.size;
171
+ }
172
+ /** `provider/model`(不分大小写)。 */
173
+ get(ref) {
174
+ return this.byRef.get(ref.toLowerCase())?.model;
175
+ }
176
+ /**
177
+ * 匹配一个模型 id。`explicit` 是配置里的 `modelsDev: "provider/model"`:找得到就用,找不到返回
178
+ * undefined(调用方 warning),不再回落到按 id 猜。
179
+ */
180
+ match(id, explicit) {
181
+ if (explicit !== undefined) {
182
+ const entry = this.byRef.get(explicit.toLowerCase());
183
+ return entry
184
+ ? { ref: refOf(entry), model: entry.model, kind: "explicit", candidates: 1 }
185
+ : undefined;
186
+ }
187
+ if (id.includes("/")) {
188
+ const entry = this.byRef.get(id.toLowerCase());
189
+ if (entry)
190
+ return { ref: refOf(entry), model: entry.model, kind: "prefix", candidates: 1 };
191
+ }
192
+ const candidates = idCandidates(id);
193
+ for (const [i, key] of candidates.entries()) {
194
+ const entries = this.byId.get(key);
195
+ if (!entries || entries.length === 0)
196
+ continue;
197
+ const picked = this.choose(key, entries);
198
+ if (i > 0 && key !== id.toLowerCase())
199
+ picked.normalized = key;
200
+ return picked;
201
+ }
202
+ return undefined;
203
+ }
204
+ choose(key, entries) {
205
+ const candidates = entries.length;
206
+ let pool = entries;
207
+ let owner;
208
+ const votes = new Map();
209
+ for (const e of entries) {
210
+ const c = e.model.canonical_model_id?.toLowerCase();
211
+ if (c)
212
+ votes.set(c, (votes.get(c) ?? 0) + 1);
213
+ }
214
+ if (votes.size > 0) {
215
+ const tail = (ref) => ref.slice(ref.indexOf("/") + 1);
216
+ const ranked = [...votes].sort((a, b) => Number(tail(b[0]) === key) - Number(tail(a[0]) === key) || b[1] - a[1]);
217
+ const canonical = ranked[0]?.[0] ?? "";
218
+ const direct = this.byRef.get(canonical);
219
+ if (direct)
220
+ return { ref: refOf(direct), model: direct.model, kind: "canonical", candidates };
221
+ // 原厂条目不在 models.dev 里:只在指向同一 canonical 的条目里挑,原厂只认 canonical 的厂商
222
+ // (否则 alibaba 转售的 kimi 会被当成「原厂」)。
223
+ owner = canonical.slice(0, canonical.indexOf("/"));
224
+ const same = entries.filter((e) => e.model.canonical_model_id?.toLowerCase() === canonical);
225
+ if (same.length > 0)
226
+ pool = same;
227
+ }
228
+ const vendor = pool
229
+ .filter((e) => owner !== undefined
230
+ ? e.provider === owner || e.provider === `${owner}-cn`
231
+ : vendorRank(e.provider) < Number.POSITIVE_INFINITY)
232
+ .sort((a, b) => vendorRank(a.provider) - vendorRank(b.provider))[0];
233
+ if (vendor)
234
+ return { ref: refOf(vendor), model: vendor.model, kind: "vendor", candidates };
235
+ if (pool.length === 1 && pool[0]) {
236
+ return { ref: refOf(pool[0]), model: pool[0].model, kind: "single", candidates };
237
+ }
238
+ const groups = new Map();
239
+ for (const e of pool) {
240
+ const sig = signature(e.model);
241
+ const list = groups.get(sig);
242
+ if (list)
243
+ list.push(e);
244
+ else
245
+ groups.set(sig, [e]);
246
+ }
247
+ const best = [...groups.values()].sort((a, b) => b.length - a.length)[0] ?? pool;
248
+ const chosen = best[0];
249
+ const match = {
250
+ ref: refOf(chosen),
251
+ model: chosen.model,
252
+ kind: "consensus",
253
+ candidates,
254
+ };
255
+ if (groups.size > 1) {
256
+ match.warning =
257
+ `${key}:models.dev 有 ${pool.length} 个条目、${groups.size} 种取值,` +
258
+ `取多数(${best.length} 条:${signature(chosen.model)})`;
259
+ }
260
+ return match;
261
+ }
262
+ }
263
+ /** models.dev 条目 → ama Model 字段(规则见 docs/providers.md)。 */
264
+ export function modelsDevFields(model) {
265
+ const out = {};
266
+ if (model.name !== undefined)
267
+ out.name = model.name;
268
+ const context = positive(model.limit?.context);
269
+ if (context !== undefined)
270
+ out.contextWindow = Math.floor(context);
271
+ const output = positive(model.limit?.output);
272
+ if (output !== undefined) {
273
+ out.maxTokens = Math.floor(Math.min(output, MODELS_DEV_MAX_OUTPUT, context ?? output));
274
+ }
275
+ const input = model.modalities?.input;
276
+ if (input !== undefined)
277
+ out.input = input.includes("image") ? ["text", "image"] : ["text"];
278
+ if (model.reasoning !== undefined)
279
+ out.reasoning = model.reasoning;
280
+ if (model.tool_call !== undefined)
281
+ out.toolCall = model.tool_call;
282
+ const cost = model.cost;
283
+ if (cost?.input !== undefined && cost.output !== undefined) {
284
+ out.cost = {
285
+ input: cost.input,
286
+ output: cost.output,
287
+ cacheRead: cost.cache_read ?? cost.input,
288
+ cacheWrite: cost.cache_write ?? cost.input,
289
+ };
290
+ }
291
+ return out;
292
+ }
293
+ /** 可由 models.dev 补的 Model 字段(展示来源用)。 */
294
+ export const ENRICHABLE_FIELDS = [
295
+ "contextWindow",
296
+ "maxTokens",
297
+ "input",
298
+ "reasoning",
299
+ "cost",
300
+ ];
301
+ /** 匹配方式的中文说明(表格与 `models list`)。 */
302
+ export function matchLabel(match) {
303
+ if (match === undefined)
304
+ return "未匹配";
305
+ const kind = {
306
+ explicit: "显式",
307
+ prefix: "前缀",
308
+ canonical: "原厂",
309
+ vendor: "原厂",
310
+ consensus: "多数",
311
+ single: "唯一",
312
+ };
313
+ const via = match.normalized !== undefined ? `,按 ${match.normalized}` : "";
314
+ return `${kind[match.kind]} ${match.ref}${via}`;
315
+ }
@@ -14,11 +14,19 @@
14
14
  * 的;仍不唯一 → ambiguous 并列出候选。模型表为空的供应商(ollama、lmstudio、未列模型的
15
15
  * 自定义供应商)与 baseUrl 被改到非官方主机的内置供应商(中转站)接受任意 model id,按自定义
16
16
  * 缺省合成;后者 compat 未显式配置 `sendPromptCacheKey` 时置 false。
17
+ *
18
+ * 渠道(channels.ts):有 `channels` 的供应商,模型按首选渠道物化;`provider/model@channel` 换渠道
19
+ * 重新物化(协议、地址、headers、compat 取渠道的,模型级 api / baseUrl 仍覆盖);渠道 key 经
20
+ * `resolveApiKey(provider, channel)`。没有 `channels` 的供应商行为不变。
21
+ *
22
+ * models.dev(enrich.ts):config 里缺元数据的自定义模型与合成模型,用传入的索引(只读缓存)补
23
+ * contextWindow / maxTokens / input / reasoning / cost,来源记在 `modelMetadata()`;内置目录不补。
17
24
  */
18
25
  import type { AmaConfig } from "../../config/types.js";
19
26
  import { ApiRegistry } from "../apis/api.js";
20
- import type { Api, ApiImplementation, ApiKeyResolution, Model, ModelLookup, ProviderData, ProviderRegistryApi } from "../types.js";
27
+ import type { Api, ApiImplementation, ApiKeyResolution, Model, ModelLookup, ProviderChannel, ProviderData, ProviderRegistryApi } from "../types.js";
21
28
  import { type KeyResolverOptions } from "./auth.js";
29
+ import { type ModelMetadata, type ModelsDevSource } from "./enrich.js";
22
30
  export type ModelSource = "builtin" | "config" | "discovered";
23
31
  export interface ProviderRegistryOptions {
24
32
  config?: AmaConfig | undefined;
@@ -29,14 +37,25 @@ export interface ProviderRegistryOptions {
29
37
  /** 注册 `fake` 供应商(缺省 true)。 */
30
38
  includeFake?: boolean | undefined;
31
39
  onWarning?: ((message: string) => void) | undefined;
40
+ /**
41
+ * models.dev 索引(或惰性加载函数,只读缓存不联网):给 config 里缺元数据的自定义模型补
42
+ * contextWindow / maxTokens / input / reasoning / cost。
43
+ */
44
+ modelsDev?: ModelsDevSource;
32
45
  }
33
46
  export interface ModelEntry {
34
47
  model: Model;
35
48
  provider: ProviderData;
36
49
  source: ModelSource;
37
50
  }
38
- /** 物化:模型补齐供应商级字段(不可变:返回新对象)。 */
39
- export declare function materializeModel(model: Model, provider: ProviderData): Model;
51
+ /**
52
+ * 物化:模型补齐供应商级字段(不可变:返回新对象)。给了 `channel` 时协议与地址取渠道的(模型级
53
+ * `explicit` 覆盖优先),headers / compat 按 供应商 ← 渠道 ← 模型 合并,并记下渠道名。
54
+ */
55
+ export declare function materializeModel(model: Model, provider: ProviderData, channel?: ProviderChannel, explicit?: {
56
+ api?: Api | undefined;
57
+ baseUrl?: string | undefined;
58
+ }): Model;
40
59
  export declare class ProviderRegistry implements ProviderRegistryApi {
41
60
  readonly warnings: string[];
42
61
  private readonly providers;
@@ -45,16 +64,25 @@ export declare class ProviderRegistry implements ProviderRegistryApi {
45
64
  private readonly envBaseUrls;
46
65
  /** baseUrl 指向非官方主机的内置供应商:接受目录外的 model id。 */
47
66
  private readonly relayed;
67
+ /** `provider/model` → 物化前的模型(多渠道供应商切换渠道时重新物化)。 */
68
+ private readonly raw;
69
+ /** `provider/model` → 元数据来源与 models.dev 匹配。 */
70
+ private readonly metadata;
71
+ private readonly modelsDev;
48
72
  private readonly apis;
49
73
  private readonly keys;
50
74
  private readonly onWarning;
51
75
  constructor(options?: ProviderRegistryOptions);
52
76
  private warn;
77
+ /** 物化并记下原始模型;多渠道供应商用模型的首选渠道。 */
78
+ private materialize;
53
79
  private applyConfig;
54
80
  /** baseUrl 来自哪个环境变量(未生效返回 undefined)。 */
55
81
  baseUrlEnv(providerId: string): string | undefined;
56
82
  /** 内置供应商的 baseUrl 指向非官方主机(中转站)。 */
57
83
  isRelayed(providerId: string): boolean;
84
+ /** 模型元数据的来源与 models.dev 匹配(`ama models list` / `config show` / providers 表格)。 */
85
+ modelMetadata(providerId: string, modelId: string): ModelMetadata | undefined;
58
86
  list(): readonly ProviderData[];
59
87
  get(providerId: string): ProviderData | undefined;
60
88
  listModels(): ModelEntry[];
@@ -62,10 +90,20 @@ export declare class ProviderRegistry implements ProviderRegistryApi {
62
90
  /** 追加 / 替换模型(例如本地枚举得到的 ollama 模型)。 */
63
91
  addModels(providerId: string, models: readonly Model[], source?: ModelSource): void;
64
92
  private synthesize;
93
+ /** 按渠道取模型:渠道不在模型的 channels 里 → channel_not_found,列出可用渠道。 */
94
+ private withChannel;
95
+ /**
96
+ * `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
97
+ * 模型除外),中转供应商的「未登记即合成」不会把 `@渠道` 吞进模型 id。失败的 reason:供应商不存在 →
98
+ * provider_not_found(候选是编辑距离最近的供应商);渠道不存在 → channel_not_found;模型不存在 →
99
+ * not_found(候选是最接近的模型)。
100
+ */
65
101
  findModel(ref: string): ModelLookup;
102
+ private findPlain;
103
+ /** 最接近的模型(包含关系优先,其次编辑距离),`provider/id` 形式。 */
66
104
  private similar;
67
105
  hasConfiguredKey(providerId: string): boolean;
68
- resolveApiKey(providerId: string): Promise<ApiKeyResolution>;
106
+ resolveApiKey(providerId: string, channel?: string): Promise<ApiKeyResolution>;
69
107
  getApi(api: Api): ApiImplementation | undefined;
70
108
  }
71
109
  /** 注册表若是 ProviderRegistry(或同形状),报告 baseUrl 来自的环境变量;否则 undefined。 */