@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.
- package/CHANGELOG.md +99 -0
- package/README.md +142 -49
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +2 -1
- package/dist/agent/session-cache.js +6 -3
- package/dist/agent/session-classifier.d.ts +18 -0
- package/dist/agent/session-classifier.js +103 -0
- package/dist/agent/session-core.d.ts +13 -0
- package/dist/agent/session-state.js +2 -1
- package/dist/agent/session-sync.js +9 -2
- package/dist/agent/session-tools.js +24 -3
- package/dist/agent/session.d.ts +3 -0
- package/dist/agent/session.js +20 -10
- package/dist/agent/tool-runner.js +15 -2
- package/dist/agent/types.d.ts +9 -1
- package/dist/ai/apis/anthropic-messages.js +3 -2
- package/dist/ai/apis/google-generative-ai.js +3 -2
- package/dist/ai/apis/openai-completions.js +3 -2
- package/dist/ai/apis/openai-responses.js +5 -3
- package/dist/ai/cache/fingerprint.d.ts +1 -1
- package/dist/ai/cache/fingerprint.js +1 -1
- package/dist/ai/cache/reporting.js +2 -1
- package/dist/ai/http.d.ts +28 -6
- package/dist/ai/http.js +41 -8
- package/dist/ai/providers/channels.d.ts +51 -0
- package/dist/ai/providers/channels.js +96 -0
- package/dist/ai/providers/enrich.d.ts +34 -0
- package/dist/ai/providers/enrich.js +86 -0
- package/dist/ai/providers/models-dev-cache.d.ts +53 -0
- package/dist/ai/providers/models-dev-cache.js +147 -0
- package/dist/ai/providers/models-dev.d.ts +99 -0
- package/dist/ai/providers/models-dev.js +315 -0
- package/dist/ai/providers/registry.d.ts +42 -4
- package/dist/ai/providers/registry.js +195 -34
- package/dist/ai/providers/suggest.d.ts +18 -0
- package/dist/ai/providers/suggest.js +72 -0
- package/dist/ai/sse.d.ts +6 -2
- package/dist/ai/sse.js +22 -2
- package/dist/ai/types.d.ts +30 -3
- package/dist/bundle/ama.cjs +17893 -10698
- package/dist/cli/args.d.ts +23 -5
- package/dist/cli/args.js +113 -13
- package/dist/cli/bootstrap.js +28 -3
- package/dist/cli/codemode-notice.d.ts +20 -0
- package/dist/cli/codemode-notice.js +55 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-session.d.ts +8 -0
- package/dist/cli/compose-session.js +32 -1
- package/dist/cli/compose-store.d.ts +1 -1
- package/dist/cli/compose-store.js +3 -1
- package/dist/cli/compose.d.ts +17 -5
- package/dist/cli/compose.js +53 -19
- package/dist/cli/default-model.d.ts +38 -1
- package/dist/cli/default-model.js +96 -9
- package/dist/cli/deps.d.ts +38 -0
- package/dist/cli/exit-codes.d.ts +2 -0
- package/dist/cli/exit-codes.js +3 -0
- package/dist/cli/fake-visibility.d.ts +13 -0
- package/dist/cli/fake-visibility.js +26 -0
- package/dist/cli/from-prompt.d.ts +18 -0
- package/dist/cli/from-prompt.js +49 -0
- package/dist/cli/main.d.ts +10 -2
- package/dist/cli/main.js +88 -2
- package/dist/cli/proxy.d.ts +51 -0
- package/dist/cli/proxy.js +135 -0
- package/dist/cli/startup-screen.d.ts +29 -0
- package/dist/cli/startup-screen.js +47 -0
- package/dist/cli/startup-steps.d.ts +1 -1
- package/dist/cli/startup-steps.js +20 -13
- package/dist/cli/subcommands/config.d.ts +29 -4
- package/dist/cli/subcommands/config.js +188 -25
- package/dist/cli/subcommands/context.js +5 -1
- package/dist/cli/subcommands/doctor.js +12 -1
- package/dist/cli/subcommands/init.d.ts +6 -0
- package/dist/cli/subcommands/init.js +23 -0
- package/dist/cli/subcommands/model-meta.d.ts +16 -0
- package/dist/cli/subcommands/model-meta.js +54 -0
- package/dist/cli/subcommands/models-cache-probe.js +1 -1
- package/dist/cli/subcommands/models-discover.d.ts +18 -6
- package/dist/cli/subcommands/models-discover.js +87 -52
- package/dist/cli/subcommands/models.js +26 -18
- package/dist/cli/subcommands/probe-runner.d.ts +96 -0
- package/dist/cli/subcommands/probe-runner.js +264 -0
- package/dist/cli/subcommands/providers-list.d.ts +8 -0
- package/dist/cli/subcommands/providers-list.js +97 -0
- package/dist/cli/subcommands/providers-plan.d.ts +79 -0
- package/dist/cli/subcommands/providers-plan.js +215 -0
- package/dist/cli/subcommands/providers-probe.d.ts +34 -0
- package/dist/cli/subcommands/providers-probe.js +87 -0
- package/dist/cli/subcommands/providers.d.ts +28 -0
- package/dist/cli/subcommands/providers.js +436 -0
- package/dist/cli/subcommands/sessions-export.d.ts +10 -0
- package/dist/cli/subcommands/sessions-export.js +59 -0
- package/dist/cli/subcommands/sessions-search.d.ts +13 -0
- package/dist/cli/subcommands/sessions-search.js +103 -0
- package/dist/cli/subcommands/sessions.d.ts +3 -2
- package/dist/cli/subcommands/sessions.js +22 -1
- package/dist/cli/subcommands/stats.d.ts +17 -0
- package/dist/cli/subcommands/stats.js +198 -0
- package/dist/cli/system-prompt-arg.d.ts +11 -0
- package/dist/cli/system-prompt-arg.js +34 -0
- package/dist/codemode/modes.d.ts +4 -11
- package/dist/codemode/modes.js +5 -27
- package/dist/codemode/tool.d.ts +18 -14
- package/dist/codemode/tool.js +71 -28
- package/dist/config/init.d.ts +34 -0
- package/dist/config/init.js +99 -0
- package/dist/config/json-schema.d.ts +15 -0
- package/dist/config/json-schema.js +215 -0
- package/dist/config/key-docs.d.ts +22 -0
- package/dist/config/key-docs.js +105 -0
- package/dist/config/merge.d.ts +9 -8
- package/dist/config/merge.js +22 -7
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +97 -15
- package/dist/config/types.d.ts +57 -11
- package/dist/config/types.js +19 -1
- package/dist/modes/commands-core.js +6 -5
- package/dist/modes/image-input.d.ts +27 -0
- package/dist/modes/image-input.js +78 -0
- package/dist/modes/interactive/approval-dialog.d.ts +21 -5
- package/dist/modes/interactive/approval-dialog.js +106 -27
- package/dist/modes/interactive/commands.d.ts +11 -2
- package/dist/modes/interactive/commands.js +58 -19
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +70 -82
- package/dist/modes/interactive/key-dispatch.d.ts +3 -1
- package/dist/modes/interactive/key-dispatch.js +5 -6
- package/dist/modes/interactive/line/line-mode.d.ts +1 -0
- package/dist/modes/interactive/line/line-mode.js +9 -5
- package/dist/modes/interactive/line/line-render.d.ts +4 -0
- package/dist/modes/interactive/line/line-render.js +28 -6
- package/dist/modes/interactive/message-view.d.ts +48 -9
- package/dist/modes/interactive/message-view.js +238 -44
- package/dist/modes/interactive/panels.d.ts +18 -0
- package/dist/modes/interactive/panels.js +143 -0
- package/dist/modes/interactive/pickers.d.ts +23 -2
- package/dist/modes/interactive/pickers.js +48 -15
- package/dist/modes/interactive/run-indicator.d.ts +51 -0
- package/dist/modes/interactive/run-indicator.js +189 -0
- package/dist/modes/interactive/startup-header.d.ts +40 -0
- package/dist/modes/interactive/startup-header.js +169 -0
- package/dist/modes/interactive/startup-ui.d.ts +7 -2
- package/dist/modes/interactive/startup-ui.js +41 -11
- package/dist/modes/interactive/status-bar.d.ts +18 -15
- package/dist/modes/interactive/status-bar.js +98 -56
- package/dist/modes/interactive/tool-summary.d.ts +46 -0
- package/dist/modes/interactive/tool-summary.js +218 -0
- package/dist/modes/interactive/tool-view.d.ts +48 -15
- package/dist/modes/interactive/tool-view.js +203 -145
- package/dist/modes/print/print-mode.d.ts +33 -4
- package/dist/modes/print/print-mode.js +128 -8
- package/dist/modes/rpc/commands.js +12 -2
- package/dist/permissions/auto-safe.d.ts +60 -0
- package/dist/permissions/auto-safe.js +529 -0
- package/dist/permissions/classifier.d.ts +64 -0
- package/dist/permissions/classifier.js +184 -0
- package/dist/permissions/dangerous.d.ts +5 -0
- package/dist/permissions/dangerous.js +1 -1
- package/dist/permissions/modes.d.ts +30 -0
- package/dist/permissions/modes.js +78 -0
- package/dist/permissions/pipeline.d.ts +31 -4
- package/dist/permissions/pipeline.js +196 -6
- package/dist/permissions/protected.d.ts +19 -0
- package/dist/permissions/protected.js +74 -0
- package/dist/permissions/rules.js +3 -0
- package/dist/permissions/types.d.ts +50 -3
- package/dist/permissions/types.js +3 -0
- package/dist/rpc.d.ts +2 -0
- package/dist/sdk.d.ts +9 -3
- package/dist/sdk.js +10 -2
- package/dist/session/export.d.ts +32 -0
- package/dist/session/export.js +187 -0
- package/dist/session/projection.js +5 -1
- package/dist/session/redact.d.ts +15 -0
- package/dist/session/redact.js +55 -0
- package/dist/session/reuse.d.ts +33 -0
- package/dist/session/reuse.js +86 -0
- package/dist/session/scan.d.ts +34 -0
- package/dist/session/scan.js +140 -0
- package/dist/session/search.d.ts +52 -0
- package/dist/session/search.js +211 -0
- package/dist/session/stats-aggregate.d.ts +63 -0
- package/dist/session/stats-aggregate.js +163 -0
- package/dist/session/stats-index.d.ts +26 -0
- package/dist/session/stats-index.js +91 -0
- package/dist/session/stats-scan.d.ts +54 -0
- package/dist/session/stats-scan.js +236 -0
- package/dist/session/types.d.ts +2 -0
- package/dist/tools/image-file.d.ts +31 -0
- package/dist/tools/image-file.js +114 -0
- package/dist/tools/presets.d.ts +35 -8
- package/dist/tools/presets.js +56 -17
- package/dist/tools/read.d.ts +4 -8
- package/dist/tools/read.js +15 -64
- package/dist/tui/component.d.ts +8 -2
- package/dist/tui/component.js +3 -1
- package/dist/tui/components/box.d.ts +6 -1
- package/dist/tui/components/box.js +16 -6
- package/dist/tui/components/card.d.ts +23 -0
- package/dist/tui/components/card.js +37 -0
- package/dist/tui/components/editor-history.d.ts +6 -0
- package/dist/tui/components/editor-history.js +45 -0
- package/dist/tui/components/editor-paste.d.ts +1 -1
- package/dist/tui/components/editor-paste.js +4 -4
- package/dist/tui/components/editor.d.ts +11 -5
- package/dist/tui/components/editor.js +52 -58
- package/dist/tui/components/key-value.d.ts +3 -0
- package/dist/tui/components/key-value.js +16 -6
- package/dist/tui/components/loader.d.ts +37 -7
- package/dist/tui/components/loader.js +84 -21
- package/dist/tui/components/markdown.d.ts +5 -1
- package/dist/tui/components/markdown.js +45 -21
- package/dist/tui/components/meter.d.ts +3 -3
- package/dist/tui/components/meter.js +13 -11
- package/dist/tui/components/select-list.d.ts +23 -1
- package/dist/tui/components/select-list.js +76 -13
- package/dist/tui/glyphs.d.ts +68 -0
- package/dist/tui/glyphs.js +114 -0
- package/dist/tui/theme.d.ts +30 -6
- package/dist/tui/theme.js +103 -17
- package/dist/tui.d.ts +4 -2
- package/dist/tui.js +3 -1
- package/docs/codemode.md +23 -9
- package/docs/hooks.md +10 -10
- package/docs/permissions.md +148 -0
- package/docs/providers.md +241 -3
- package/docs/rpc.md +38 -38
- package/docs/session-format.md +3 -2
- package/docs/sessions.md +134 -0
- package/docs/tui.md +140 -61
- 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
|
-
|
|
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 (
|
|
32
|
-
out.
|
|
33
|
-
|
|
34
|
-
out.
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
|
251
|
-
return
|
|
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
|
-
/**
|
|
32
|
-
|
|
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
|
-
/**
|
|
106
|
-
|
|
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)
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
+
/** `channel`:渠道自己配了 key 时用渠道的,否则用供应商的。 */
|
|
453
|
+
resolveApiKey(providerId: string, channel?: string): Promise<ApiKeyResolution>;
|
|
427
454
|
getApi(api: Api): ApiImplementation | undefined;
|
|
428
455
|
}
|