@armadra/agent 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +55 -11
  3. package/dist/agent/session-cache.js +5 -2
  4. package/dist/agent/session-state.js +2 -1
  5. package/dist/agent/session-sync.js +9 -2
  6. package/dist/agent/session.js +8 -7
  7. package/dist/ai/apis/openai-responses.js +2 -1
  8. package/dist/ai/cache/fingerprint.d.ts +1 -1
  9. package/dist/ai/cache/fingerprint.js +1 -1
  10. package/dist/ai/cache/reporting.js +2 -1
  11. package/dist/ai/providers/channels.d.ts +51 -0
  12. package/dist/ai/providers/channels.js +96 -0
  13. package/dist/ai/providers/enrich.d.ts +34 -0
  14. package/dist/ai/providers/enrich.js +86 -0
  15. package/dist/ai/providers/models-dev-cache.d.ts +53 -0
  16. package/dist/ai/providers/models-dev-cache.js +147 -0
  17. package/dist/ai/providers/models-dev.d.ts +99 -0
  18. package/dist/ai/providers/models-dev.js +315 -0
  19. package/dist/ai/providers/registry.d.ts +35 -4
  20. package/dist/ai/providers/registry.js +177 -24
  21. package/dist/ai/types.d.ts +21 -1
  22. package/dist/bundle/ama.cjs +4075 -2008
  23. package/dist/cli/args.d.ts +6 -3
  24. package/dist/cli/args.js +30 -3
  25. package/dist/cli/bootstrap.js +1 -0
  26. package/dist/cli/compose-providers.js +3 -0
  27. package/dist/cli/compose-session.d.ts +3 -0
  28. package/dist/cli/compose.d.ts +3 -1
  29. package/dist/cli/compose.js +24 -9
  30. package/dist/cli/deps.d.ts +2 -0
  31. package/dist/cli/main.d.ts +2 -1
  32. package/dist/cli/main.js +13 -1
  33. package/dist/cli/startup-steps.js +6 -1
  34. package/dist/cli/subcommands/config.d.ts +11 -2
  35. package/dist/cli/subcommands/config.js +80 -4
  36. package/dist/cli/subcommands/context.js +1 -0
  37. package/dist/cli/subcommands/init.d.ts +6 -0
  38. package/dist/cli/subcommands/init.js +22 -0
  39. package/dist/cli/subcommands/model-meta.d.ts +16 -0
  40. package/dist/cli/subcommands/model-meta.js +54 -0
  41. package/dist/cli/subcommands/models-cache-probe.js +1 -1
  42. package/dist/cli/subcommands/models-discover.d.ts +6 -0
  43. package/dist/cli/subcommands/models-discover.js +37 -9
  44. package/dist/cli/subcommands/models.js +26 -18
  45. package/dist/cli/subcommands/providers-list.d.ts +8 -0
  46. package/dist/cli/subcommands/providers-list.js +97 -0
  47. package/dist/cli/subcommands/providers-plan.d.ts +79 -0
  48. package/dist/cli/subcommands/providers-plan.js +215 -0
  49. package/dist/cli/subcommands/providers.d.ts +27 -0
  50. package/dist/cli/subcommands/providers.js +433 -0
  51. package/dist/config/init.d.ts +30 -0
  52. package/dist/config/init.js +92 -0
  53. package/dist/config/json-schema.d.ts +14 -0
  54. package/dist/config/json-schema.js +182 -0
  55. package/dist/config/schema.d.ts +1 -1
  56. package/dist/config/schema.js +74 -11
  57. package/dist/config/types.d.ts +23 -5
  58. package/dist/config/types.js +2 -0
  59. package/dist/modes/image-input.d.ts +27 -0
  60. package/dist/modes/image-input.js +78 -0
  61. package/dist/modes/interactive/commands.js +2 -3
  62. package/dist/modes/interactive/interactive-mode.js +5 -1
  63. package/dist/modes/interactive/line/line-mode.js +3 -1
  64. package/dist/modes/interactive/startup-ui.d.ts +7 -2
  65. package/dist/modes/interactive/startup-ui.js +41 -11
  66. package/dist/modes/print/print-mode.d.ts +2 -0
  67. package/dist/modes/print/print-mode.js +12 -1
  68. package/dist/modes/rpc/commands.js +7 -2
  69. package/dist/rpc.d.ts +2 -0
  70. package/dist/session/projection.js +5 -1
  71. package/dist/session/types.d.ts +2 -0
  72. package/dist/tools/image-file.d.ts +31 -0
  73. package/dist/tools/image-file.js +114 -0
  74. package/dist/tools/read.d.ts +4 -8
  75. package/dist/tools/read.js +15 -64
  76. package/docs/providers.md +216 -1
  77. package/docs/rpc.md +3 -3
  78. package/docs/session-format.md +1 -1
  79. package/docs/tui.md +2 -0
  80. package/package.json +1 -1
@@ -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,42 @@ 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
+ /**
34
+ * 物化:模型补齐供应商级字段(不可变:返回新对象)。给了 `channel` 时协议与地址取渠道的(模型级
35
+ * `explicit` 覆盖优先),headers / compat 按 供应商 ← 渠道 ← 模型 合并,并记下渠道名。
36
+ */
37
+ export function materializeModel(model, provider, channel, explicit) {
26
38
  const out = {
27
39
  ...model,
28
40
  provider: provider.id,
29
41
  baseUrl: model.baseUrl ?? provider.baseUrl,
30
42
  };
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;
43
+ if (channel !== undefined) {
44
+ out.api = explicit?.api ?? channel.api;
45
+ out.baseUrl = explicit?.baseUrl ?? channel.baseUrl;
46
+ out.channel = channel.name;
47
+ }
48
+ if (provider.headers || channel?.headers || model.headers)
49
+ out.headers = mergeHeaders(provider.headers, channel?.headers, model.headers);
50
+ if (provider.compat || channel?.compat || model.compat)
51
+ out.compat = { ...provider.compat, ...channel?.compat, ...model.compat };
52
+ const authHeader = channel?.authHeader ?? provider.authHeader;
53
+ if (authHeader !== undefined)
54
+ out.authHeader = model.authHeader ?? authHeader;
37
55
  out.requiresApiKey = provider.requiresApiKey;
38
56
  return out;
39
57
  }
40
58
  function configKeysOf(config) {
41
59
  const keys = {};
42
- for (const [id, provider] of Object.entries(config?.providers ?? {}))
60
+ for (const [id, provider] of Object.entries(config?.providers ?? {})) {
43
61
  keys[id] = provider.apiKey;
62
+ for (const [name, channel] of Object.entries(provider.channels ?? {})) {
63
+ if (channel?.apiKey !== undefined)
64
+ keys[channelKeyId(id, name)] = channel.apiKey;
65
+ }
66
+ }
44
67
  return keys;
45
68
  }
46
69
  export class ProviderRegistry {
@@ -51,11 +74,17 @@ export class ProviderRegistry {
51
74
  envBaseUrls = new Map();
52
75
  /** baseUrl 指向非官方主机的内置供应商:接受目录外的 model id。 */
53
76
  relayed = new Set();
77
+ /** `provider/model` → 物化前的模型(多渠道供应商切换渠道时重新物化)。 */
78
+ raw = new Map();
79
+ /** `provider/model` → 元数据来源与 models.dev 匹配。 */
80
+ metadata = new Map();
81
+ modelsDev;
54
82
  apis;
55
83
  keys;
56
84
  onWarning;
57
85
  constructor(options = {}) {
58
86
  this.onWarning = options.onWarning;
87
+ this.modelsDev = lazyIndex(options.modelsDev);
59
88
  this.apis = options.apis ?? createDefaultApiRegistry();
60
89
  this.keys = new ApiKeyResolver({
61
90
  ...options.keys,
@@ -68,7 +97,11 @@ export class ProviderRegistry {
68
97
  const models = (catalog.get(base.id) ?? []).map((entry) => {
69
98
  const api = entry.api ?? base.api;
70
99
  this.sources.set(`${base.id}/${entry.id}`, "builtin");
71
- return toModel(entry, base.id, api);
100
+ const model = toModel(entry, base.id, api);
101
+ this.metadata.set(`${base.id}/${entry.id}`, catalogMetadata(model));
102
+ if (entry.api !== undefined)
103
+ this.raw.set(`${base.id}/${entry.id}`, { model, explicit: { api } });
104
+ return model;
72
105
  });
73
106
  const fromEnv = baseUrlEnv !== undefined ? env[baseUrlEnv]?.trim() : undefined;
74
107
  if (baseUrlEnv !== undefined && fromEnv)
@@ -102,24 +135,43 @@ export class ProviderRegistry {
102
135
  this.providers.set(fake.id, fake);
103
136
  }
104
137
  for (const provider of this.providers.values()) {
105
- provider.models = provider.models.map((model) => materializeModel(model, provider));
138
+ provider.models = provider.models.map((model) => this.materialize(provider, model));
106
139
  }
107
140
  }
108
141
  warn(message) {
109
142
  this.warnings.push(message);
110
143
  this.onWarning?.(message);
111
144
  }
145
+ /** 物化并记下原始模型;多渠道供应商用模型的首选渠道。 */
146
+ materialize(provider, model, channelName) {
147
+ const key = `${provider.id}/${model.id}`;
148
+ const raw = this.raw.get(key) ?? { model, explicit: {} };
149
+ if (!this.raw.has(key)) {
150
+ if (model.baseUrl !== undefined)
151
+ raw.explicit.baseUrl = model.baseUrl;
152
+ this.raw.set(key, raw);
153
+ }
154
+ if (provider.channels === undefined || provider.channels.length === 0)
155
+ return materializeModel(raw.model, provider);
156
+ const name = channelName ?? raw.model.channels?.[0] ?? provider.defaultChannel;
157
+ const channel = provider.channels.find((c) => c.name === name) ?? provider.channels[0];
158
+ return materializeModel(raw.model, provider, channel, raw.explicit);
159
+ }
112
160
  applyConfig(id, config) {
113
161
  const existing = this.providers.get(id);
114
- if (!existing && !config.baseUrl) {
162
+ const parsed = config.channels !== undefined ? parseChannels(id, config) : undefined;
163
+ for (const warning of parsed?.warnings ?? [])
164
+ this.warn(warning);
165
+ const first = parsed?.channels.find((c) => c.name === parsed.defaultChannel);
166
+ if (!existing && !config.baseUrl && first === undefined) {
115
167
  this.warn(`provider "${id}" in config.json has no baseUrl; ignored`);
116
168
  return;
117
169
  }
118
170
  const provider = existing ?? {
119
171
  id,
120
172
  name: config.name ?? id,
121
- api: config.api ?? "openai-completions",
122
- baseUrl: config.baseUrl ?? "",
173
+ api: config.api ?? first?.api ?? "openai-completions",
174
+ baseUrl: config.baseUrl ?? first?.baseUrl ?? "",
123
175
  envKeys: [fallbackEnvKey(id)],
124
176
  models: [],
125
177
  requiresApiKey: true,
@@ -133,6 +185,14 @@ export class ProviderRegistry {
133
185
  provider.baseUrl = config.baseUrl;
134
186
  this.envBaseUrls.delete(id);
135
187
  }
188
+ if (parsed !== undefined && first !== undefined) {
189
+ // 有渠道时供应商级 api / baseUrl 取首选渠道的(discover 等单地址的用法看它)。
190
+ provider.channels = parsed.channels;
191
+ provider.defaultChannel = first.name;
192
+ provider.api = first.api;
193
+ provider.baseUrl = first.baseUrl;
194
+ this.envBaseUrls.delete(id);
195
+ }
136
196
  if (config.envKeys !== undefined) {
137
197
  provider.envKeys = [
138
198
  ...config.envKeys,
@@ -147,15 +207,30 @@ export class ProviderRegistry {
147
207
  provider.compat = { ...provider.compat, ...config.compat };
148
208
  if (config.requiresApiKey !== undefined)
149
209
  provider.requiresApiKey = config.requiresApiKey;
210
+ const channelsOf = (wanted, modelId) => modelChannels(wanted, provider.channels ?? [], provider.defaultChannel ?? "", (name) => this.warn(`model "${id}/${modelId}" channel "${name}" not found; ignored`));
150
211
  for (const entry of config.models ?? []) {
151
- // 模型级 api(第三波 §2.3):同一中转下不同模型走不同协议;缺省沿用供应商的。
152
- const model = withCustomDefaults(entry, id, entry.api ?? provider.api);
212
+ // 模型级 api(第三波 §2.3):同一中转下不同模型走不同协议;缺省沿用供应商(或渠道)的。
213
+ const { entry: filled, metadata } = enrichEntry(entry, this.modelsDev);
214
+ if (typeof entry.modelsDev === "string" && metadata.looked && metadata.match === undefined)
215
+ this.warn(`modelsDev "${entry.modelsDev}" for "${id}/${entry.id}" not found`);
216
+ const model = withCustomDefaults(filled, id, entry.api ?? provider.api);
217
+ if (provider.channels !== undefined)
218
+ model.channels = channelsOf(entry.channels, entry.id);
153
219
  const index = provider.models.findIndex((m) => m.id === entry.id);
154
220
  if (index >= 0)
155
221
  provider.models[index] = model;
156
222
  else
157
223
  provider.models.push(model);
158
- this.sources.set(`${id}/${entry.id}`, "config");
224
+ const key = `${id}/${entry.id}`;
225
+ this.raw.set(key, {
226
+ model,
227
+ explicit: {
228
+ ...(entry.api !== undefined ? { api: entry.api } : {}),
229
+ ...(entry.baseUrl !== undefined ? { baseUrl: entry.baseUrl } : {}),
230
+ },
231
+ });
232
+ this.sources.set(key, "config");
233
+ this.metadata.set(key, metadata);
159
234
  }
160
235
  for (const override of config.modelOverrides ?? []) {
161
236
  const index = provider.models.findIndex((m) => m.id === override.id);
@@ -164,8 +239,33 @@ export class ProviderRegistry {
164
239
  this.warn(`modelOverrides: "${id}/${override.id}" not found; ignored`);
165
240
  continue;
166
241
  }
167
- const next = applyModelOverride(current, override);
168
- provider.models[index] = override.api !== undefined ? { ...next, api: override.api } : next;
242
+ const { channels, modelsDev: _md, ...fields } = override;
243
+ let next = applyModelOverride(current, fields);
244
+ if (override.api !== undefined)
245
+ next = { ...next, api: override.api };
246
+ if (channels !== undefined && provider.channels !== undefined)
247
+ next.channels = channelsOf(channels, override.id);
248
+ provider.models[index] = next;
249
+ const key = `${id}/${override.id}`;
250
+ const raw = this.raw.get(key);
251
+ this.raw.set(key, {
252
+ model: next,
253
+ explicit: {
254
+ ...raw?.explicit,
255
+ ...(override.api !== undefined ? { api: override.api } : {}),
256
+ ...(override.baseUrl !== undefined ? { baseUrl: override.baseUrl } : {}),
257
+ },
258
+ });
259
+ const meta = this.metadata.get(key);
260
+ if (meta !== undefined)
261
+ for (const field of Object.keys(meta.sources))
262
+ if (fields[field] !== undefined)
263
+ meta.sources[field] = "config";
264
+ }
265
+ if (provider.channels !== undefined) {
266
+ // 目录模型(给内置供应商加了渠道时)挂到首选渠道。
267
+ for (const model of provider.models)
268
+ model.channels ??= [provider.defaultChannel ?? ""];
169
269
  }
170
270
  this.providers.set(id, provider);
171
271
  }
@@ -177,6 +277,10 @@ export class ProviderRegistry {
177
277
  isRelayed(providerId) {
178
278
  return this.relayed.has(providerId);
179
279
  }
280
+ /** 模型元数据的来源与 models.dev 匹配(`ama models list` / `config show` / providers 表格)。 */
281
+ modelMetadata(providerId, modelId) {
282
+ return this.metadata.get(`${providerId}/${modelId}`);
283
+ }
180
284
  list() {
181
285
  return [...this.providers.values()];
182
286
  }
@@ -201,7 +305,8 @@ export class ProviderRegistry {
201
305
  if (!provider)
202
306
  return;
203
307
  for (const raw of models) {
204
- const model = materializeModel({ ...raw, provider: providerId }, provider);
308
+ this.raw.delete(`${providerId}/${raw.id}`);
309
+ const model = this.materialize(provider, { ...raw, provider: providerId });
205
310
  const index = provider.models.findIndex((m) => m.id === model.id);
206
311
  if (index >= 0)
207
312
  provider.models[index] = model;
@@ -210,11 +315,47 @@ export class ProviderRegistry {
210
315
  this.sources.set(`${providerId}/${model.id}`, source);
211
316
  }
212
317
  }
213
- synthesize(provider, modelId) {
214
- return materializeModel(withCustomDefaults({ id: modelId }, provider.id, provider.api), provider);
318
+ synthesize(provider, modelId, channel) {
319
+ const key = `${provider.id}/${modelId}`;
320
+ const { entry, metadata } = enrichEntry({ id: modelId }, this.modelsDev);
321
+ if (!this.metadata.has(key))
322
+ this.metadata.set(key, metadata);
323
+ const model = withCustomDefaults(entry, provider.id, provider.api);
324
+ if (provider.channels !== undefined)
325
+ model.channels = [provider.defaultChannel ?? ""];
326
+ const channelData = provider.channels?.find((c) => c.name === (channel ?? model.channels?.[0]));
327
+ return materializeModel(model, provider, channelData);
328
+ }
329
+ /** 按渠道取模型:渠道不在模型的 channels 里 → 列出可用渠道。 */
330
+ withChannel(provider, model, channel) {
331
+ const available = model.channels ?? [];
332
+ if (!available.includes(channel)) {
333
+ return {
334
+ ok: false,
335
+ reason: "not_found",
336
+ candidates: available.map((c) => `${provider.id}/${model.id}@${c}`),
337
+ };
338
+ }
339
+ if (channel === model.channel)
340
+ return { ok: true, model, provider };
341
+ return { ok: true, model: this.materialize(provider, model, channel), provider };
215
342
  }
216
343
  findModel(ref) {
217
344
  const trimmed = ref.trim();
345
+ const direct = this.findPlain(trimmed);
346
+ if (direct.ok)
347
+ return direct;
348
+ const split = splitChannelRef(trimmed);
349
+ if (split === undefined)
350
+ return direct;
351
+ const base = this.findPlain(split.base, split.channel);
352
+ if (!base.ok)
353
+ return base.reason === "ambiguous" ? base : direct;
354
+ if (!base.provider.channels?.some((c) => c.name === split.channel))
355
+ return direct;
356
+ return this.withChannel(base.provider, base.model, split.channel);
357
+ }
358
+ findPlain(trimmed, channel) {
218
359
  const slash = trimmed.indexOf("/");
219
360
  if (slash > 0) {
220
361
  const provider = this.providers.get(trimmed.slice(0, slash));
@@ -224,7 +365,10 @@ export class ProviderRegistry {
224
365
  if (model)
225
366
  return { ok: true, model, provider };
226
367
  if ((provider.models.length === 0 || this.relayed.has(provider.id)) && id.length > 0) {
227
- return { ok: true, model: this.synthesize(provider, id), provider };
368
+ const synthesized = this.synthesize(provider, id);
369
+ if (channel !== undefined && provider.channels?.some((c) => c.name === channel))
370
+ synthesized.channels = [...new Set([...(synthesized.channels ?? []), channel])];
371
+ return { ok: true, model: synthesized, provider };
228
372
  }
229
373
  return { ok: false, reason: "not_found", candidates: this.similar(id, provider.id) };
230
374
  }
@@ -261,10 +405,19 @@ export class ProviderRegistry {
261
405
  const provider = this.providers.get(providerId);
262
406
  return provider ? this.keys.hasConfiguredKey(provider) : false;
263
407
  }
264
- async resolveApiKey(providerId) {
408
+ async resolveApiKey(providerId, channel) {
265
409
  const provider = this.providers.get(providerId);
266
410
  if (!provider)
267
411
  return { apiKey: undefined, source: "none" };
412
+ if (channel !== undefined && provider.channels?.some((c) => c.name === channel)) {
413
+ const own = await this.keys.resolve({
414
+ id: channelKeyId(provider.id, channel),
415
+ envKeys: [],
416
+ requiresApiKey: provider.requiresApiKey,
417
+ });
418
+ if (own.apiKey !== undefined)
419
+ return own;
420
+ }
268
421
  return this.keys.resolve(provider);
269
422
  }
270
423
  getApi(api) {
@@ -32,6 +32,8 @@ export type RequestPurpose = "turn" | "summary" | "warm" | "probe";
32
32
  export interface ModelRef {
33
33
  provider: string;
34
34
  id: string;
35
+ /** 所选渠道(多渠道供应商;单渠道 / 隐式渠道不填)。 */
36
+ channel?: string;
35
37
  }
36
38
  export type JsonSchemaType = "object" | "string" | "number" | "integer" | "boolean" | "array";
37
39
  export type JsonPrimitive = string | number | boolean | null;
@@ -295,6 +297,19 @@ export interface Model {
295
297
  * 填入。缺省视为 true:流函数缺 key 时同步抛 `AmaError{code:"no_api_key"}`。
296
298
  */
297
299
  requiresApiKey?: boolean;
300
+ /** 所选渠道名(registry 物化时填入;隐式 `default` 渠道不填)。 */
301
+ channel?: string;
302
+ /** 该模型挂载的全部渠道,首个为首选(多渠道供应商才有)。 */
303
+ channels?: string[];
304
+ }
305
+ /** 物化后的渠道(docs/providers.md「渠道」);key 不在这里,经 `resolveApiKey(provider, channel)` 取。 */
306
+ export interface ProviderChannel {
307
+ name: string;
308
+ api: Api;
309
+ baseUrl: string;
310
+ authHeader?: AuthHeader;
311
+ headers?: Record<string, string>;
312
+ compat?: ProviderCompat;
298
313
  }
299
314
  export interface ProviderData {
300
315
  id: string;
@@ -310,6 +325,10 @@ export interface ProviderData {
310
325
  /** 本地服务 false:无 key 也能用。 */
311
326
  requiresApiKey: boolean;
312
327
  builtin: boolean;
328
+ /** 显式配置的渠道(没有 `channels` 的供应商不填,按单渠道处理)。 */
329
+ channels?: ProviderChannel[];
330
+ /** 首选渠道名(有 `channels` 时)。 */
331
+ defaultChannel?: string;
313
332
  }
314
333
  /** `catalog/*.json` 文件形状(§3.4)。 */
315
334
  export interface ModelCatalogFile {
@@ -423,6 +442,7 @@ export interface ProviderRegistryApi {
423
442
  get(providerId: string): ProviderData | undefined;
424
443
  /** 解析 `provider/model-id` 或无斜杠的模型 id(§3.4)。 */
425
444
  findModel(ref: string): ModelLookup;
426
- resolveApiKey(providerId: string): Promise<ApiKeyResolution>;
445
+ /** `channel`:渠道自己配了 key 时用渠道的,否则用供应商的。 */
446
+ resolveApiKey(providerId: string, channel?: string): Promise<ApiKeyResolution>;
427
447
  getApi(api: Api): ApiImplementation | undefined;
428
448
  }