@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
@@ -14,6 +14,13 @@
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 { ApiRegistry, createDefaultApiRegistry } from "../apis/api.js";
19
26
  import { fakeProviderData } from "../fake/fake-provider.js";
@@ -21,26 +28,43 @@ import { mergeHeaders } from "../http.js";
21
28
  import { ApiKeyResolver } from "./auth.js";
22
29
  import { BUILTIN_PROVIDERS, fallbackEnvKey, isRelayedBaseUrl } from "./builtin.js";
23
30
  import { applyModelOverride, loadBuiltinCatalog, toModel, withCustomDefaults } from "./catalog.js";
24
- /** 物化:模型补齐供应商级字段(不可变:返回新对象)。 */
25
- export function materializeModel(model, provider) {
31
+ import { channelKeyId, modelChannels, parseChannels, splitChannelRef } from "./channels.js";
32
+ import { catalogMetadata, enrichEntry, lazyIndex, } from "./enrich.js";
33
+ import { closest } from "./suggest.js";
34
+ /**
35
+ * 物化:模型补齐供应商级字段(不可变:返回新对象)。给了 `channel` 时协议与地址取渠道的(模型级
36
+ * `explicit` 覆盖优先),headers / compat 按 供应商 ← 渠道 ← 模型 合并,并记下渠道名。
37
+ */
38
+ export function materializeModel(model, provider, channel, explicit) {
26
39
  const out = {
27
40
  ...model,
28
41
  provider: provider.id,
29
42
  baseUrl: model.baseUrl ?? provider.baseUrl,
30
43
  };
31
- if (provider.headers || model.headers)
32
- out.headers = mergeHeaders(provider.headers, model.headers);
33
- if (provider.compat || model.compat)
34
- out.compat = { ...provider.compat, ...model.compat };
35
- if (provider.authHeader !== undefined)
36
- out.authHeader = model.authHeader ?? provider.authHeader;
44
+ if (channel !== undefined) {
45
+ out.api = explicit?.api ?? channel.api;
46
+ out.baseUrl = explicit?.baseUrl ?? channel.baseUrl;
47
+ out.channel = channel.name;
48
+ }
49
+ if (provider.headers || channel?.headers || model.headers)
50
+ out.headers = mergeHeaders(provider.headers, channel?.headers, model.headers);
51
+ if (provider.compat || channel?.compat || model.compat)
52
+ out.compat = { ...provider.compat, ...channel?.compat, ...model.compat };
53
+ const authHeader = channel?.authHeader ?? provider.authHeader;
54
+ if (authHeader !== undefined)
55
+ out.authHeader = model.authHeader ?? authHeader;
37
56
  out.requiresApiKey = provider.requiresApiKey;
38
57
  return out;
39
58
  }
40
59
  function configKeysOf(config) {
41
60
  const keys = {};
42
- for (const [id, provider] of Object.entries(config?.providers ?? {}))
61
+ for (const [id, provider] of Object.entries(config?.providers ?? {})) {
43
62
  keys[id] = provider.apiKey;
63
+ for (const [name, channel] of Object.entries(provider.channels ?? {})) {
64
+ if (channel?.apiKey !== undefined)
65
+ keys[channelKeyId(id, name)] = channel.apiKey;
66
+ }
67
+ }
44
68
  return keys;
45
69
  }
46
70
  export class ProviderRegistry {
@@ -51,11 +75,17 @@ export class ProviderRegistry {
51
75
  envBaseUrls = new Map();
52
76
  /** baseUrl 指向非官方主机的内置供应商:接受目录外的 model id。 */
53
77
  relayed = new Set();
78
+ /** `provider/model` → 物化前的模型(多渠道供应商切换渠道时重新物化)。 */
79
+ raw = new Map();
80
+ /** `provider/model` → 元数据来源与 models.dev 匹配。 */
81
+ metadata = new Map();
82
+ modelsDev;
54
83
  apis;
55
84
  keys;
56
85
  onWarning;
57
86
  constructor(options = {}) {
58
87
  this.onWarning = options.onWarning;
88
+ this.modelsDev = lazyIndex(options.modelsDev);
59
89
  this.apis = options.apis ?? createDefaultApiRegistry();
60
90
  this.keys = new ApiKeyResolver({
61
91
  ...options.keys,
@@ -68,7 +98,11 @@ export class ProviderRegistry {
68
98
  const models = (catalog.get(base.id) ?? []).map((entry) => {
69
99
  const api = entry.api ?? base.api;
70
100
  this.sources.set(`${base.id}/${entry.id}`, "builtin");
71
- return toModel(entry, base.id, api);
101
+ const model = toModel(entry, base.id, api);
102
+ this.metadata.set(`${base.id}/${entry.id}`, catalogMetadata(model));
103
+ if (entry.api !== undefined)
104
+ this.raw.set(`${base.id}/${entry.id}`, { model, explicit: { api } });
105
+ return model;
72
106
  });
73
107
  const fromEnv = baseUrlEnv !== undefined ? env[baseUrlEnv]?.trim() : undefined;
74
108
  if (baseUrlEnv !== undefined && fromEnv)
@@ -102,24 +136,43 @@ export class ProviderRegistry {
102
136
  this.providers.set(fake.id, fake);
103
137
  }
104
138
  for (const provider of this.providers.values()) {
105
- provider.models = provider.models.map((model) => materializeModel(model, provider));
139
+ provider.models = provider.models.map((model) => this.materialize(provider, model));
106
140
  }
107
141
  }
108
142
  warn(message) {
109
143
  this.warnings.push(message);
110
144
  this.onWarning?.(message);
111
145
  }
146
+ /** 物化并记下原始模型;多渠道供应商用模型的首选渠道。 */
147
+ materialize(provider, model, channelName) {
148
+ const key = `${provider.id}/${model.id}`;
149
+ const raw = this.raw.get(key) ?? { model, explicit: {} };
150
+ if (!this.raw.has(key)) {
151
+ if (model.baseUrl !== undefined)
152
+ raw.explicit.baseUrl = model.baseUrl;
153
+ this.raw.set(key, raw);
154
+ }
155
+ if (provider.channels === undefined || provider.channels.length === 0)
156
+ return materializeModel(raw.model, provider);
157
+ const name = channelName ?? raw.model.channels?.[0] ?? provider.defaultChannel;
158
+ const channel = provider.channels.find((c) => c.name === name) ?? provider.channels[0];
159
+ return materializeModel(raw.model, provider, channel, raw.explicit);
160
+ }
112
161
  applyConfig(id, config) {
113
162
  const existing = this.providers.get(id);
114
- if (!existing && !config.baseUrl) {
163
+ const parsed = config.channels !== undefined ? parseChannels(id, config) : undefined;
164
+ for (const warning of parsed?.warnings ?? [])
165
+ this.warn(warning);
166
+ const first = parsed?.channels.find((c) => c.name === parsed.defaultChannel);
167
+ if (!existing && !config.baseUrl && first === undefined) {
115
168
  this.warn(`provider "${id}" in config.json has no baseUrl; ignored`);
116
169
  return;
117
170
  }
118
171
  const provider = existing ?? {
119
172
  id,
120
173
  name: config.name ?? id,
121
- api: config.api ?? "openai-completions",
122
- baseUrl: config.baseUrl ?? "",
174
+ api: config.api ?? first?.api ?? "openai-completions",
175
+ baseUrl: config.baseUrl ?? first?.baseUrl ?? "",
123
176
  envKeys: [fallbackEnvKey(id)],
124
177
  models: [],
125
178
  requiresApiKey: true,
@@ -133,6 +186,14 @@ export class ProviderRegistry {
133
186
  provider.baseUrl = config.baseUrl;
134
187
  this.envBaseUrls.delete(id);
135
188
  }
189
+ if (parsed !== undefined && first !== undefined) {
190
+ // 有渠道时供应商级 api / baseUrl 取首选渠道的(discover 等单地址的用法看它)。
191
+ provider.channels = parsed.channels;
192
+ provider.defaultChannel = first.name;
193
+ provider.api = first.api;
194
+ provider.baseUrl = first.baseUrl;
195
+ this.envBaseUrls.delete(id);
196
+ }
136
197
  if (config.envKeys !== undefined) {
137
198
  provider.envKeys = [
138
199
  ...config.envKeys,
@@ -147,15 +208,30 @@ export class ProviderRegistry {
147
208
  provider.compat = { ...provider.compat, ...config.compat };
148
209
  if (config.requiresApiKey !== undefined)
149
210
  provider.requiresApiKey = config.requiresApiKey;
211
+ const channelsOf = (wanted, modelId) => modelChannels(wanted, provider.channels ?? [], provider.defaultChannel ?? "", (name) => this.warn(`model "${id}/${modelId}" channel "${name}" not found; ignored`));
150
212
  for (const entry of config.models ?? []) {
151
- // 模型级 api(第三波 §2.3):同一中转下不同模型走不同协议;缺省沿用供应商的。
152
- const model = withCustomDefaults(entry, id, entry.api ?? provider.api);
213
+ // 模型级 api(第三波 §2.3):同一中转下不同模型走不同协议;缺省沿用供应商(或渠道)的。
214
+ const { entry: filled, metadata } = enrichEntry(entry, this.modelsDev);
215
+ if (typeof entry.modelsDev === "string" && metadata.looked && metadata.match === undefined)
216
+ this.warn(`modelsDev "${entry.modelsDev}" for "${id}/${entry.id}" not found`);
217
+ const model = withCustomDefaults(filled, id, entry.api ?? provider.api);
218
+ if (provider.channels !== undefined)
219
+ model.channels = channelsOf(entry.channels, entry.id);
153
220
  const index = provider.models.findIndex((m) => m.id === entry.id);
154
221
  if (index >= 0)
155
222
  provider.models[index] = model;
156
223
  else
157
224
  provider.models.push(model);
158
- this.sources.set(`${id}/${entry.id}`, "config");
225
+ const key = `${id}/${entry.id}`;
226
+ this.raw.set(key, {
227
+ model,
228
+ explicit: {
229
+ ...(entry.api !== undefined ? { api: entry.api } : {}),
230
+ ...(entry.baseUrl !== undefined ? { baseUrl: entry.baseUrl } : {}),
231
+ },
232
+ });
233
+ this.sources.set(key, "config");
234
+ this.metadata.set(key, metadata);
159
235
  }
160
236
  for (const override of config.modelOverrides ?? []) {
161
237
  const index = provider.models.findIndex((m) => m.id === override.id);
@@ -164,8 +240,33 @@ export class ProviderRegistry {
164
240
  this.warn(`modelOverrides: "${id}/${override.id}" not found; ignored`);
165
241
  continue;
166
242
  }
167
- const next = applyModelOverride(current, override);
168
- provider.models[index] = override.api !== undefined ? { ...next, api: override.api } : next;
243
+ const { channels, modelsDev: _md, ...fields } = override;
244
+ let next = applyModelOverride(current, fields);
245
+ if (override.api !== undefined)
246
+ next = { ...next, api: override.api };
247
+ if (channels !== undefined && provider.channels !== undefined)
248
+ next.channels = channelsOf(channels, override.id);
249
+ provider.models[index] = next;
250
+ const key = `${id}/${override.id}`;
251
+ const raw = this.raw.get(key);
252
+ this.raw.set(key, {
253
+ model: next,
254
+ explicit: {
255
+ ...raw?.explicit,
256
+ ...(override.api !== undefined ? { api: override.api } : {}),
257
+ ...(override.baseUrl !== undefined ? { baseUrl: override.baseUrl } : {}),
258
+ },
259
+ });
260
+ const meta = this.metadata.get(key);
261
+ if (meta !== undefined)
262
+ for (const field of Object.keys(meta.sources))
263
+ if (fields[field] !== undefined)
264
+ meta.sources[field] = "config";
265
+ }
266
+ if (provider.channels !== undefined) {
267
+ // 目录模型(给内置供应商加了渠道时)挂到首选渠道。
268
+ for (const model of provider.models)
269
+ model.channels ??= [provider.defaultChannel ?? ""];
169
270
  }
170
271
  this.providers.set(id, provider);
171
272
  }
@@ -177,6 +278,10 @@ export class ProviderRegistry {
177
278
  isRelayed(providerId) {
178
279
  return this.relayed.has(providerId);
179
280
  }
281
+ /** 模型元数据的来源与 models.dev 匹配(`ama models list` / `config show` / providers 表格)。 */
282
+ modelMetadata(providerId, modelId) {
283
+ return this.metadata.get(`${providerId}/${modelId}`);
284
+ }
180
285
  list() {
181
286
  return [...this.providers.values()];
182
287
  }
@@ -201,7 +306,8 @@ export class ProviderRegistry {
201
306
  if (!provider)
202
307
  return;
203
308
  for (const raw of models) {
204
- const model = materializeModel({ ...raw, provider: providerId }, provider);
309
+ this.raw.delete(`${providerId}/${raw.id}`);
310
+ const model = this.materialize(provider, { ...raw, provider: providerId });
205
311
  const index = provider.models.findIndex((m) => m.id === model.id);
206
312
  if (index >= 0)
207
313
  provider.models[index] = model;
@@ -210,11 +316,51 @@ export class ProviderRegistry {
210
316
  this.sources.set(`${providerId}/${model.id}`, source);
211
317
  }
212
318
  }
213
- synthesize(provider, modelId) {
214
- return materializeModel(withCustomDefaults({ id: modelId }, provider.id, provider.api), provider);
319
+ synthesize(provider, modelId, channel) {
320
+ const key = `${provider.id}/${modelId}`;
321
+ const { entry, metadata } = enrichEntry({ id: modelId }, this.modelsDev);
322
+ if (!this.metadata.has(key))
323
+ this.metadata.set(key, metadata);
324
+ const model = withCustomDefaults(entry, provider.id, provider.api);
325
+ if (provider.channels !== undefined)
326
+ model.channels = [provider.defaultChannel ?? ""];
327
+ const channelData = provider.channels?.find((c) => c.name === (channel ?? model.channels?.[0]));
328
+ return materializeModel(model, provider, channelData);
329
+ }
330
+ /** 按渠道取模型:渠道不在模型的 channels 里 → channel_not_found,列出可用渠道。 */
331
+ withChannel(provider, model, channel) {
332
+ const available = model.channels ?? [];
333
+ if (!available.includes(channel) || !provider.channels?.some((c) => c.name === channel)) {
334
+ return {
335
+ ok: false,
336
+ reason: "channel_not_found",
337
+ candidates: available.filter(Boolean).map((c) => `${provider.id}/${model.id}@${c}`),
338
+ };
339
+ }
340
+ if (channel === model.channel)
341
+ return { ok: true, model, provider };
342
+ return { ok: true, model: this.materialize(provider, model, channel), provider };
215
343
  }
344
+ /**
345
+ * `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
346
+ * 模型除外),中转供应商的「未登记即合成」不会把 `@渠道` 吞进模型 id。失败的 reason:供应商不存在 →
347
+ * provider_not_found(候选是编辑距离最近的供应商);渠道不存在 → channel_not_found;模型不存在 →
348
+ * not_found(候选是最接近的模型)。
349
+ */
216
350
  findModel(ref) {
217
351
  const trimmed = ref.trim();
352
+ const split = splitChannelRef(trimmed);
353
+ if (split === undefined)
354
+ return this.findPlain(trimmed);
355
+ const exact = this.findPlain(trimmed, undefined, false);
356
+ if (exact.ok)
357
+ return exact;
358
+ const base = this.findPlain(split.base, split.channel);
359
+ if (!base.ok)
360
+ return base;
361
+ return this.withChannel(base.provider, base.model, split.channel);
362
+ }
363
+ findPlain(trimmed, channel, synthesize = true) {
218
364
  const slash = trimmed.indexOf("/");
219
365
  if (slash > 0) {
220
366
  const provider = this.providers.get(trimmed.slice(0, slash));
@@ -223,13 +369,25 @@ export class ProviderRegistry {
223
369
  const model = provider.models.find((m) => m.id === id);
224
370
  if (model)
225
371
  return { ok: true, model, provider };
226
- if ((provider.models.length === 0 || this.relayed.has(provider.id)) && id.length > 0) {
227
- return { ok: true, model: this.synthesize(provider, id), provider };
372
+ const relayed = provider.models.length === 0 || this.relayed.has(provider.id);
373
+ if (synthesize && relayed && id.length > 0) {
374
+ const synthesized = this.synthesize(provider, id);
375
+ if (channel !== undefined && provider.channels?.some((c) => c.name === channel))
376
+ synthesized.channels = [...new Set([...(synthesized.channels ?? []), channel])];
377
+ return { ok: true, model: synthesized, provider };
228
378
  }
229
379
  return { ok: false, reason: "not_found", candidates: this.similar(id, provider.id) };
230
380
  }
231
381
  }
232
382
  const matches = this.listModels().filter((entry) => entry.model.id === trimmed);
383
+ if (matches.length === 0 && slash > 0) {
384
+ const ids = [...this.providers.keys()];
385
+ return {
386
+ ok: false,
387
+ reason: "provider_not_found",
388
+ candidates: closest(trimmed.slice(0, slash), ids),
389
+ };
390
+ }
233
391
  if (matches.length === 1 && matches[0]) {
234
392
  return { ok: true, model: matches[0].model, provider: matches[0].provider };
235
393
  }
@@ -246,25 +404,28 @@ export class ProviderRegistry {
246
404
  candidates: (configured.length > 0 ? configured : matches).map((entry) => `${entry.provider.id}/${entry.model.id}`),
247
405
  };
248
406
  }
407
+ /** 最接近的模型(包含关系优先,其次编辑距离),`provider/id` 形式。 */
249
408
  similar(id, providerId) {
250
- const needle = id.toLowerCase();
251
- return this.listModels()
252
- .filter((entry) => providerId === undefined || entry.provider.id === providerId)
253
- .filter((entry) => {
254
- const candidate = entry.model.id.toLowerCase();
255
- return needle.length > 0 && (candidate.includes(needle) || needle.includes(candidate));
256
- })
257
- .slice(0, 10)
258
- .map((entry) => `${entry.provider.id}/${entry.model.id}`);
409
+ const entries = this.listModels().filter((entry) => providerId === undefined || entry.provider.id === providerId);
410
+ return closest(id, entries, 5, (entry) => entry.model.id).map((entry) => `${entry.provider.id}/${entry.model.id}`);
259
411
  }
260
412
  hasConfiguredKey(providerId) {
261
413
  const provider = this.providers.get(providerId);
262
414
  return provider ? this.keys.hasConfiguredKey(provider) : false;
263
415
  }
264
- async resolveApiKey(providerId) {
416
+ async resolveApiKey(providerId, channel) {
265
417
  const provider = this.providers.get(providerId);
266
418
  if (!provider)
267
419
  return { apiKey: undefined, source: "none" };
420
+ if (channel !== undefined && provider.channels?.some((c) => c.name === channel)) {
421
+ const own = await this.keys.resolve({
422
+ id: channelKeyId(provider.id, channel),
423
+ envKeys: [],
424
+ requiresApiKey: provider.requiresApiKey,
425
+ });
426
+ if (own.apiKey !== undefined)
427
+ return own;
428
+ }
268
429
  return this.keys.resolve(provider);
269
430
  }
270
431
  getApi(api) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 模型 / 供应商 / 渠道写错时的候选与报错文案(W4-C)。
3
+ *
4
+ * 候选排序:包含关系(`sonnet` ⊂ `claude-sonnet-5`)优先,其次按编辑距离;距离超过
5
+ * `max(2, ⌈长度 / 3⌉)` 的不算接近。只比较 id 本身(不含 `provider/` 前缀),大小写不敏感。
6
+ */
7
+ import type { ModelLookup } from "../types.js";
8
+ /** Levenshtein 距离(插入 / 删除 / 替换各计 1)。 */
9
+ export declare function editDistance(a: string, b: string): number;
10
+ /**
11
+ * 从 `candidates` 里挑与 `needle` 最接近的至多 `limit` 个。`key` 取参与比较的部分
12
+ * (缺省整串),返回原值。
13
+ */
14
+ export declare function closest<T>(needle: string, candidates: readonly T[], limit?: number, key?: (item: T) => string): T[];
15
+ /** 解析失败的一行说明(不含前缀标签):`渠道不存在:x;该模型可用渠道:…` 等。 */
16
+ export declare function describeLookupFailure(ref: string, failure: Extract<ModelLookup, {
17
+ ok: false;
18
+ }>): string;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 模型 / 供应商 / 渠道写错时的候选与报错文案(W4-C)。
3
+ *
4
+ * 候选排序:包含关系(`sonnet` ⊂ `claude-sonnet-5`)优先,其次按编辑距离;距离超过
5
+ * `max(2, ⌈长度 / 3⌉)` 的不算接近。只比较 id 本身(不含 `provider/` 前缀),大小写不敏感。
6
+ */
7
+ /** Levenshtein 距离(插入 / 删除 / 替换各计 1)。 */
8
+ export function editDistance(a, b) {
9
+ if (a === b)
10
+ return 0;
11
+ if (a.length === 0)
12
+ return b.length;
13
+ if (b.length === 0)
14
+ return a.length;
15
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
16
+ for (let i = 1; i <= a.length; i++) {
17
+ const row = [i];
18
+ for (let j = 1; j <= b.length; j++) {
19
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
20
+ row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
21
+ }
22
+ prev = row;
23
+ }
24
+ return prev[b.length] ?? 0;
25
+ }
26
+ /**
27
+ * 从 `candidates` 里挑与 `needle` 最接近的至多 `limit` 个。`key` 取参与比较的部分
28
+ * (缺省整串),返回原值。
29
+ */
30
+ export function closest(needle, candidates, limit = 5, key = String) {
31
+ const target = needle.toLowerCase();
32
+ if (target === "")
33
+ return [];
34
+ const budget = Math.max(2, Math.ceil(target.length / 3));
35
+ const scored = [];
36
+ candidates.forEach((item, index) => {
37
+ const value = key(item).toLowerCase();
38
+ if (value === "")
39
+ return;
40
+ if (value.includes(target) || target.includes(value)) {
41
+ scored.push({ item, score: Math.abs(value.length - target.length) / 1000, index });
42
+ return;
43
+ }
44
+ const distance = editDistance(target, value);
45
+ if (distance <= budget)
46
+ scored.push({ item, score: distance, index });
47
+ });
48
+ return scored
49
+ .sort((a, b) => a.score - b.score || a.index - b.index)
50
+ .slice(0, limit)
51
+ .map((entry) => entry.item);
52
+ }
53
+ /** 解析失败的一行说明(不含前缀标签):`渠道不存在:x;该模型可用渠道:…` 等。 */
54
+ export function describeLookupFailure(ref, failure) {
55
+ const list = failure.candidates.slice(0, 20).join(", ");
56
+ switch (failure.reason) {
57
+ case "channel_not_found":
58
+ return failure.candidates.length > 0
59
+ ? `渠道不存在:${ref};该模型可用渠道:${list}`
60
+ : `渠道不存在:${ref};该模型没有可选渠道(去掉 @ 后缀)`;
61
+ case "provider_not_found":
62
+ return failure.candidates.length > 0
63
+ ? `供应商不存在:${ref};最接近的供应商:${list}`
64
+ : `供应商不存在:${ref}(ama providers list 查看已配置的供应商)`;
65
+ case "ambiguous":
66
+ return `模型名有歧义:${ref};候选:${list}`;
67
+ default:
68
+ return failure.candidates.length > 0
69
+ ? `模型不存在:${ref};最接近的模型:${list}`
70
+ : `模型不存在:${ref}(ama models list 查看可用模型)`;
71
+ }
72
+ }
package/dist/ai/sse.d.ts CHANGED
@@ -28,5 +28,9 @@ export declare class SseParser {
28
28
  private processLine;
29
29
  private dispatch;
30
30
  }
31
- /** 从字节流读 SSE 事件;`signal` 中止时停止读取(调用方据此产出 aborted)。 */
32
- export declare function readSseEvents(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<SseEvent>;
31
+ /**
32
+ * 从字节流读 SSE 事件;`signal` 中止时停止读取(调用方据此产出 aborted)。
33
+ * `idleTimeoutMs`:等待下一块字节的上限,每收到一块即重新计时(消费者处理事件的时间不计);
34
+ * 超时取消底层流并抛 `IdleTimeoutError`(phase `stream`)。
35
+ */
36
+ export declare function readSseEvents(body: ReadableStream<Uint8Array>, signal?: AbortSignal, idleTimeoutMs?: number): AsyncGenerator<SseEvent>;
package/dist/ai/sse.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * 第一个空格去掉;多条 data 以 LF 连接。为兼容不规范服务端,流结束时未以空行收尾的最后一个
7
7
  * 事件也会分派。
8
8
  */
9
+ import { IdleTimeoutError } from "./http.js";
9
10
  export class SseParser {
10
11
  buffer = "";
11
12
  event;
@@ -102,8 +103,12 @@ export class SseParser {
102
103
  return hadContent ? event : undefined;
103
104
  }
104
105
  }
105
- /** 从字节流读 SSE 事件;`signal` 中止时停止读取(调用方据此产出 aborted)。 */
106
- export async function* readSseEvents(body, signal) {
106
+ /**
107
+ * 从字节流读 SSE 事件;`signal` 中止时停止读取(调用方据此产出 aborted)。
108
+ * `idleTimeoutMs`:等待下一块字节的上限,每收到一块即重新计时(消费者处理事件的时间不计);
109
+ * 超时取消底层流并抛 `IdleTimeoutError`(phase `stream`)。
110
+ */
111
+ export async function* readSseEvents(body, signal, idleTimeoutMs) {
107
112
  const reader = body.getReader();
108
113
  const decoder = new TextDecoder("utf-8");
109
114
  const parser = new SseParser();
@@ -112,11 +117,25 @@ export async function* readSseEvents(body, signal) {
112
117
  };
113
118
  signal?.addEventListener("abort", onAbort, { once: true });
114
119
  let drained = false;
120
+ let idle;
121
+ let stalled = false;
122
+ const arm = () => {
123
+ if (idleTimeoutMs === undefined || idleTimeoutMs <= 0)
124
+ return;
125
+ idle = setTimeout(() => {
126
+ stalled = true;
127
+ reader.cancel().catch(() => undefined);
128
+ }, idleTimeoutMs);
129
+ };
115
130
  try {
116
131
  while (true) {
117
132
  if (signal?.aborted)
118
133
  return;
134
+ arm();
119
135
  const { value, done } = await reader.read();
136
+ clearTimeout(idle);
137
+ if (stalled && !signal?.aborted)
138
+ throw new IdleTimeoutError(idleTimeoutMs ?? 0, "stream");
120
139
  if (done)
121
140
  break;
122
141
  if (signal?.aborted)
@@ -131,6 +150,7 @@ export async function* readSseEvents(body, signal) {
131
150
  yield event;
132
151
  }
133
152
  finally {
153
+ clearTimeout(idle);
134
154
  signal?.removeEventListener("abort", onAbort);
135
155
  // 消费者提前退出(读到终止事件即 break)或中止:取消底层流,释放连接
136
156
  if (!drained)
@@ -27,11 +27,14 @@ export type CacheRetention = "none" | "short" | "long";
27
27
  * 真实回合,`summary` 是压缩 / 分支摘要,`warm` 是缓存保温重放,`probe` 是 `models check` /
28
28
  * `cache-probe` 一类的探测。缺省视为 `turn`。协议层只透传,不改变请求体。
29
29
  */
30
- export type RequestPurpose = "turn" | "summary" | "warm" | "probe";
30
+ /** `classify`:auto 权限模式的分类请求(独立请求,不经会话层缓存观测、不触发保温)。 */
31
+ export type RequestPurpose = "turn" | "summary" | "warm" | "probe" | "classify";
31
32
  /** `provider/model-id` 拆开后的引用。 */
32
33
  export interface ModelRef {
33
34
  provider: string;
34
35
  id: string;
36
+ /** 所选渠道(多渠道供应商;单渠道 / 隐式渠道不填)。 */
37
+ channel?: string;
35
38
  }
36
39
  export type JsonSchemaType = "object" | "string" | "number" | "integer" | "boolean" | "array";
37
40
  export type JsonPrimitive = string | number | boolean | null;
@@ -295,6 +298,19 @@ export interface Model {
295
298
  * 填入。缺省视为 true:流函数缺 key 时同步抛 `AmaError{code:"no_api_key"}`。
296
299
  */
297
300
  requiresApiKey?: boolean;
301
+ /** 所选渠道名(registry 物化时填入;隐式 `default` 渠道不填)。 */
302
+ channel?: string;
303
+ /** 该模型挂载的全部渠道,首个为首选(多渠道供应商才有)。 */
304
+ channels?: string[];
305
+ }
306
+ /** 物化后的渠道(docs/providers.md「渠道」);key 不在这里,经 `resolveApiKey(provider, channel)` 取。 */
307
+ export interface ProviderChannel {
308
+ name: string;
309
+ api: Api;
310
+ baseUrl: string;
311
+ authHeader?: AuthHeader;
312
+ headers?: Record<string, string>;
313
+ compat?: ProviderCompat;
298
314
  }
299
315
  export interface ProviderData {
300
316
  id: string;
@@ -310,6 +326,10 @@ export interface ProviderData {
310
326
  /** 本地服务 false:无 key 也能用。 */
311
327
  requiresApiKey: boolean;
312
328
  builtin: boolean;
329
+ /** 显式配置的渠道(没有 `channels` 的供应商不填,按单渠道处理)。 */
330
+ channels?: ProviderChannel[];
331
+ /** 首选渠道名(有 `channels` 时)。 */
332
+ defaultChannel?: string;
313
333
  }
314
334
  /** `catalog/*.json` 文件形状(§3.4)。 */
315
335
  export interface ModelCatalogFile {
@@ -323,6 +343,11 @@ export interface StreamOptions {
323
343
  /** 值为 null 表示删除该头。 */
324
344
  headers?: Record<string, string | null>;
325
345
  timeoutMs?: number;
346
+ /**
347
+ * 空闲超时:等响应头、以及流中两块数据之间的最长间隔(每收到字节即重新计时)。缺省 300 000,
348
+ * 0 关闭。超时报 `idle timeout` 错误,会话层按可重试处理。
349
+ */
350
+ idleTimeoutMs?: number;
326
351
  maxTokens?: number;
327
352
  temperature?: number;
328
353
  thinkingLevel?: ModelThinkingLevel;
@@ -415,7 +440,8 @@ export type ModelLookup = {
415
440
  provider: ProviderData;
416
441
  } | {
417
442
  ok: false;
418
- reason: "not_found" | "ambiguous" | "provider_not_found";
443
+ /** `channel_not_found`:模型在,`@渠道` 不在(候选是该模型可用的 `provider/model@渠道`)。 */
444
+ reason: "not_found" | "ambiguous" | "provider_not_found" | "channel_not_found";
419
445
  candidates: string[];
420
446
  };
421
447
  export interface ProviderRegistryApi {
@@ -423,6 +449,7 @@ export interface ProviderRegistryApi {
423
449
  get(providerId: string): ProviderData | undefined;
424
450
  /** 解析 `provider/model-id` 或无斜杠的模型 id(§3.4)。 */
425
451
  findModel(ref: string): ModelLookup;
426
- resolveApiKey(providerId: string): Promise<ApiKeyResolution>;
452
+ /** `channel`:渠道自己配了 key 时用渠道的,否则用供应商的。 */
453
+ resolveApiKey(providerId: string, channel?: string): Promise<ApiKeyResolution>;
427
454
  getApi(api: Api): ApiImplementation | undefined;
428
455
  }