@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
|
@@ -5,24 +5,31 @@
|
|
|
5
5
|
* 带供应商的鉴权头,打印 id 列表并标出已配置的条目。
|
|
6
6
|
*
|
|
7
7
|
* `--probe`:同一中转下不同模型支持的协议不同,对每个 id 依次试供应商协议 → completions →
|
|
8
|
-
* responses → messages
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* responses → messages(去重),各发一次最小请求(maxTokens 16,见到首个流事件即判可用并断开),
|
|
9
|
+
* 记第一个成功的协议;模型之间并发(`--concurrency`,缺省 6),单次超时 `--probe-timeout`(缺省
|
|
10
|
+
* 15 s)。每模型最多 3 次请求,`--limit`(缺省 30)限制探测的模型数,执行前打印预估;401 / 403 立即
|
|
11
|
+
* 停止,429 降并发并重试一次、仍 429 停止(见 probe-runner.ts)。
|
|
11
12
|
*
|
|
12
13
|
* `--write`:新条目合并进用户级 config.json 的 `providers.<id>.models`——已有同 id 不覆盖;只写
|
|
13
14
|
* `id` 与探到的 `api`(与供应商协议相同时省略);带 `--probe` 时只写探测成功的模型。不猜
|
|
14
15
|
* `contextWindow`(自动压缩随之关闭,输出 warning)。写前备份为 `config.json.bak`。
|
|
16
|
+
*
|
|
17
|
+
* models.dev:列表之后按需刷新缓存(24 小时内不重拉),每个模型标出上下文、输出、图片、工具调用与
|
|
18
|
+
* 匹配结果;`--write` 跳过 models.dev 标明不支持工具调用的模型。元数据不写进配置——运行时从缓存补。
|
|
15
19
|
*/
|
|
16
20
|
import { authHeaders, mergeHeaders } from "../../ai/http.js";
|
|
17
21
|
import { discoverLocalModels, materializeModel } from "../../ai/providers/registry.js";
|
|
18
22
|
import { withCustomDefaults } from "../../ai/providers/catalog.js";
|
|
23
|
+
import { describeRefresh, refreshModelsDev } from "../../ai/providers/models-dev-cache.js";
|
|
24
|
+
import { matchLabel, modelsDevFields } from "../../ai/providers/models-dev.js";
|
|
19
25
|
import { existsSync } from "node:fs";
|
|
20
26
|
import { loadConfigFile } from "../../config/load.js";
|
|
21
27
|
import { writeConfigFile } from "../../config/write.js";
|
|
22
28
|
import { UsageError } from "../args.js";
|
|
23
29
|
import { ExitCode } from "../exit-codes.js";
|
|
30
|
+
import { compactTokens } from "./model-meta.js";
|
|
31
|
+
import { describeProbePlan, parseProbeTuning, ProbeProgress, ProbeScheduler, } from "./probe-runner.js";
|
|
24
32
|
export const DISCOVER_TIMEOUT_MS = 15_000;
|
|
25
|
-
export const PROBE_TIMEOUT_MS = 30_000;
|
|
26
33
|
export const DEFAULT_PROBE_LIMIT = 30;
|
|
27
34
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
28
35
|
/** 中转常见的三种协议,按此顺序探测(供应商协议排最前)。 */
|
|
@@ -51,57 +58,57 @@ export function probeOrder(provider) {
|
|
|
51
58
|
? [provider.api, ...PROBE_APIS.filter((api) => api !== provider.api)]
|
|
52
59
|
: [...PROBE_APIS];
|
|
53
60
|
}
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
/**
|
|
62
|
+
* 返回已探测模型 → 第一个成功的协议(都失败为 undefined);超出 `limit` 或因提前停止没探完的不在表里。
|
|
63
|
+
* 模型之间并发(有界),同一模型内按协议顺序依次试,第一个成功即停。
|
|
64
|
+
*/
|
|
56
65
|
export async function probeModelApis(registry, provider, ids, limit, options = {}) {
|
|
57
66
|
const result = new Map();
|
|
58
67
|
const key = await registry.resolveApiKey(provider.id);
|
|
59
68
|
const order = probeOrder(provider).filter((api) => registry.getApi(api) !== undefined);
|
|
60
|
-
|
|
69
|
+
const selected = ids.slice(0, Math.max(0, limit));
|
|
70
|
+
const scheduler = new ProbeScheduler(registry, key.apiKey, {
|
|
71
|
+
concurrency: options.concurrency,
|
|
72
|
+
timeoutMs: options.timeoutMs,
|
|
73
|
+
retryDelayMs: options.retryDelayMs,
|
|
74
|
+
});
|
|
75
|
+
const done = [];
|
|
76
|
+
let printed = 0;
|
|
77
|
+
await scheduler.run(selected.length, async (index) => {
|
|
78
|
+
const id = selected[index];
|
|
61
79
|
const known = provider.models.find((m) => m.id === id);
|
|
62
|
-
let found;
|
|
63
80
|
for (const api of order) {
|
|
64
81
|
const model = known !== undefined
|
|
65
82
|
? { ...known, api }
|
|
66
83
|
: materializeModel(withCustomDefaults({ id }, provider.id, api), provider);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
const outcome = await scheduler.probe(model);
|
|
85
|
+
if (outcome.aborted)
|
|
86
|
+
return { complete: false, api: undefined };
|
|
87
|
+
options.onAttempt?.(id, api, outcome.error);
|
|
88
|
+
if (outcome.error === undefined)
|
|
89
|
+
return { complete: true, api };
|
|
90
|
+
if (scheduler.stopped !== undefined)
|
|
91
|
+
return { complete: false, api: undefined };
|
|
92
|
+
}
|
|
93
|
+
return { complete: true, api: undefined };
|
|
94
|
+
}, (index, found) => {
|
|
95
|
+
done[index] = found;
|
|
96
|
+
options.onSettled?.();
|
|
97
|
+
// 按模型顺序交出结果:前面的都完成了才输出
|
|
98
|
+
while (printed < selected.length && done[printed] !== undefined) {
|
|
99
|
+
const entry = done[printed];
|
|
100
|
+
const id = selected[printed];
|
|
101
|
+
if (entry.complete) {
|
|
102
|
+
result.set(id, entry.api);
|
|
103
|
+
options.onResult?.(id, entry.api);
|
|
76
104
|
}
|
|
105
|
+
printed++;
|
|
77
106
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
});
|
|
108
|
+
if (scheduler.stopped !== undefined)
|
|
109
|
+
options.onStop?.(scheduler.stopped);
|
|
81
110
|
return result;
|
|
82
111
|
}
|
|
83
|
-
/** 一次最小请求;成功返回 undefined,失败返回错误文本。 */
|
|
84
|
-
async function attempt(registry, model, apiKey, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
85
|
-
const impl = registry.getApi(model.api);
|
|
86
|
-
if (impl === undefined)
|
|
87
|
-
return `协议 ${model.api} 尚未实现`;
|
|
88
|
-
try {
|
|
89
|
-
const message = await impl
|
|
90
|
-
.stream(model, { messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }] }, {
|
|
91
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
92
|
-
...(apiKey !== undefined ? { apiKey } : {}),
|
|
93
|
-
maxTokens: 16,
|
|
94
|
-
cacheRetention: "none",
|
|
95
|
-
})
|
|
96
|
-
.result();
|
|
97
|
-
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
98
|
-
return message.errorMessage ?? message.stopReason;
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
return error instanceof Error ? error.message : String(error);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
112
|
function parseLimit(raw) {
|
|
106
113
|
if (raw === undefined)
|
|
107
114
|
return DEFAULT_PROBE_LIMIT;
|
|
@@ -111,7 +118,7 @@ function parseLimit(raw) {
|
|
|
111
118
|
return n;
|
|
112
119
|
}
|
|
113
120
|
/** 把新条目合并进用户级 config.json;返回写入条数。 */
|
|
114
|
-
function writeEntries(ctx, provider, entries) {
|
|
121
|
+
function writeEntries(ctx, provider, entries, notes) {
|
|
115
122
|
const { io } = ctx;
|
|
116
123
|
const path = ctx.level.userConfigPath;
|
|
117
124
|
const config = structuredClone(loadConfigFile("config", path)?.value ?? { version: 1 });
|
|
@@ -134,7 +141,12 @@ function writeEntries(ctx, provider, entries) {
|
|
|
134
141
|
writeConfigFile(path, config, { backup: true });
|
|
135
142
|
io.stdout(`\n已写入 ${path}:${provider.id} 新增 ${added.length} 个模型` +
|
|
136
143
|
`${kept > 0 ? `,${kept} 个已存在未覆盖` : ""}${backup ? `(原文件备份为 ${path}.bak)` : ""}\n`);
|
|
137
|
-
|
|
144
|
+
if (notes.noTools.size > 0)
|
|
145
|
+
io.stdout(`跳过不支持工具调用的模型(models.dev):${[...notes.noTools].join(", ")}\n`);
|
|
146
|
+
const blind = added.filter((entry) => notes.unmatched.has(entry.id)).map((entry) => entry.id);
|
|
147
|
+
if (blind.length > 0)
|
|
148
|
+
io.stderr(`ama: 警告:${blind.join(", ")} 在 models.dev 未匹配,没有 contextWindow,自动压缩关闭;` +
|
|
149
|
+
`需要时在 config.json 里补上或写 modelsDev\n`);
|
|
138
150
|
return added.length;
|
|
139
151
|
}
|
|
140
152
|
async function run(ctx) {
|
|
@@ -161,33 +173,56 @@ async function run(ctx) {
|
|
|
161
173
|
}
|
|
162
174
|
const configured = new Map(provider.models.map((m) => [m.id, m]));
|
|
163
175
|
io.stdout(`${provider.id}:发现 ${found.length} 个模型(${modelsUrl(provider)})\n`);
|
|
176
|
+
const md = await refreshModelsDev({ dataDir: ctx.level.dataDir, env: io.env });
|
|
177
|
+
io.stdout(`${describeRefresh(md)}\n`);
|
|
178
|
+
if (md.warning !== undefined)
|
|
179
|
+
io.stderr(`ama: 警告:${md.warning}\n`);
|
|
180
|
+
const noTools = new Set();
|
|
181
|
+
const unmatched = new Set();
|
|
164
182
|
for (const model of found) {
|
|
165
183
|
const known = configured.get(model.id);
|
|
166
|
-
|
|
184
|
+
const match = md.index?.match(model.id);
|
|
185
|
+
const fields = match !== undefined ? modelsDevFields(match.model) : undefined;
|
|
186
|
+
if (fields?.toolCall === false)
|
|
187
|
+
noTools.add(model.id);
|
|
188
|
+
if (match === undefined)
|
|
189
|
+
unmatched.add(model.id);
|
|
190
|
+
const meta = fields === undefined
|
|
191
|
+
? " models.dev 未匹配"
|
|
192
|
+
: ` ctx ${compactTokens(fields.contextWindow)} · out ${compactTokens(fields.maxTokens)}` +
|
|
193
|
+
`${fields.input?.includes("image") ? " · 图片" : ""}${fields.reasoning ? " · 思考" : ""}` +
|
|
194
|
+
`${fields.toolCall === false ? " · 不支持工具调用" : ""} · ${matchLabel(match)}`;
|
|
195
|
+
io.stdout(` ${model.id}${known !== undefined ? ` 已配置(${known.api})` : ""}${meta}\n`);
|
|
167
196
|
}
|
|
168
197
|
const write = ctx.flags.has("write");
|
|
169
198
|
if (!ctx.flags.has("probe")) {
|
|
170
199
|
if (write)
|
|
171
|
-
writeEntries(ctx, provider, found.map((m) => ({ id: m.id })));
|
|
200
|
+
writeEntries(ctx, provider, found.filter((m) => !noTools.has(m.id)).map((m) => ({ id: m.id })), { noTools, unmatched });
|
|
172
201
|
return ExitCode.Ok;
|
|
173
202
|
}
|
|
174
203
|
const ids = found.map((m) => m.id);
|
|
175
204
|
const count = Math.min(limit, ids.length);
|
|
176
205
|
const order = probeOrder(provider);
|
|
206
|
+
const tuning = parseProbeTuning(ctx.values);
|
|
177
207
|
io.stdout(`\n探测协议:${count} 个模型(${order.join(" → ")}),最多 ${count * order.length} 次请求` +
|
|
178
|
-
`${ids.length > count ? `;另有 ${ids.length - count} 个超出 --limit ${limit},未探测` : ""}\n`
|
|
208
|
+
`${ids.length > count ? `;另有 ${ids.length - count} 个超出 --limit ${limit},未探测` : ""}\n` +
|
|
209
|
+
`${describeProbePlan(count * order.length, tuning.concurrency, tuning.timeoutMs)}\n`);
|
|
179
210
|
let stopped;
|
|
211
|
+
const progress = new ProbeProgress(io.stdout, io.stdoutIsTTY, count);
|
|
180
212
|
const probed = await probeModelApis(registry, provider, ids, limit, {
|
|
181
|
-
|
|
213
|
+
...tuning,
|
|
214
|
+
onSettled: () => progress.tick(),
|
|
215
|
+
onResult: (modelId, api) => progress.line(` ${modelId} ${api ?? "不可用(三种协议均失败)"}\n`),
|
|
182
216
|
onStop: (reason) => (stopped = reason),
|
|
183
217
|
});
|
|
218
|
+
progress.finish();
|
|
184
219
|
if (write) {
|
|
185
220
|
const entries = [];
|
|
186
221
|
for (const [modelId, api] of probed) {
|
|
187
|
-
if (api !== undefined)
|
|
222
|
+
if (api !== undefined && !noTools.has(modelId))
|
|
188
223
|
entries.push(api === provider.api ? { id: modelId } : { id: modelId, api });
|
|
189
224
|
}
|
|
190
|
-
writeEntries(ctx, provider, entries);
|
|
225
|
+
writeEntries(ctx, provider, entries, { noTools, unmatched });
|
|
191
226
|
}
|
|
192
227
|
if (stopped !== undefined) {
|
|
193
228
|
io.stderr(`ama: 探测提前停止(${stopped})\n`);
|
|
@@ -196,9 +231,9 @@ async function run(ctx) {
|
|
|
196
231
|
return ExitCode.Ok;
|
|
197
232
|
}
|
|
198
233
|
export const DISCOVER_ACTION = {
|
|
199
|
-
usage: "ama models discover <provider> [--probe] [--write] [--limit <n>]",
|
|
234
|
+
usage: "ama models discover <provider> [--probe] [--write] [--limit <n>] [--concurrency <n>] [--probe-timeout <ms>]",
|
|
200
235
|
required: "<provider>",
|
|
201
|
-
valueOptions: ["limit"],
|
|
236
|
+
valueOptions: ["limit", "concurrency", "probe-timeout"],
|
|
202
237
|
flagOptions: ["probe", "write"],
|
|
203
238
|
run,
|
|
204
239
|
};
|
|
@@ -8,22 +8,16 @@
|
|
|
8
8
|
* `cache-probe`、B12 的 `discover`)各写在自己的文件里,这里只加一行表项;用法文本、
|
|
9
9
|
* 选项表与必填位置参数都从表里来。
|
|
10
10
|
*/
|
|
11
|
+
import { formatModelRef, modelRefOf } from "../../ai/providers/channels.js";
|
|
12
|
+
import { describeRefresh, modelsDevCachePath, refreshModelsDev, } from "../../ai/providers/models-dev-cache.js";
|
|
11
13
|
import { parseSubArgs, UsageError } from "../args.js";
|
|
12
14
|
import { ExitCode } from "../exit-codes.js";
|
|
13
15
|
import { buildRegistry, loadUserLevel } from "./context.js";
|
|
14
16
|
import { CACHE_PROBE_ACTION } from "./models-cache-probe.js";
|
|
15
17
|
import { DISCOVER_ACTION } from "./models-discover.js";
|
|
18
|
+
import { metadataOf, modelFlags, sourcesLine } from "./model-meta.js";
|
|
16
19
|
const COMMON_VALUE_OPTIONS = ["profile", "auth-file"];
|
|
17
20
|
export const CHECK_TIMEOUT_MS = 30_000;
|
|
18
|
-
function compact(n) {
|
|
19
|
-
if (n === undefined)
|
|
20
|
-
return "?";
|
|
21
|
-
if (n >= 1_000_000)
|
|
22
|
-
return `${Math.round(n / 100_000) / 10}M`;
|
|
23
|
-
if (n >= 1000)
|
|
24
|
-
return `${Math.round(n / 1000)}k`;
|
|
25
|
-
return String(n);
|
|
26
|
-
}
|
|
27
21
|
async function list(io, registry, only) {
|
|
28
22
|
const providers = registry.list().filter((p) => only === undefined || p.id === only);
|
|
29
23
|
if (only !== undefined && providers.length === 0) {
|
|
@@ -38,14 +32,13 @@ async function list(io, registry, only) {
|
|
|
38
32
|
? "无 key"
|
|
39
33
|
: "无需 key";
|
|
40
34
|
io.stdout(`${provider.id} ${provider.api} ${keyText}\n`);
|
|
35
|
+
for (const channel of provider.channels ?? [])
|
|
36
|
+
io.stdout(` @${channel.name} ${channel.api} ${channel.baseUrl}\n`);
|
|
41
37
|
for (const model of provider.models) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
model.input.includes("image") ? "图片" : undefined,
|
|
47
|
-
].filter((x) => x !== undefined);
|
|
48
|
-
io.stdout(` ${provider.id}/${model.id} ${flags.join(" · ")}\n`);
|
|
38
|
+
io.stdout(` ${provider.id}/${model.id} ${modelFlags(model)}\n`);
|
|
39
|
+
const sources = sourcesLine(metadataOf(registry, provider.id, model.id));
|
|
40
|
+
if (sources !== undefined)
|
|
41
|
+
io.stdout(` ${sources}\n`);
|
|
49
42
|
}
|
|
50
43
|
}
|
|
51
44
|
return ExitCode.Ok;
|
|
@@ -58,7 +51,7 @@ async function check(io, registry, ref) {
|
|
|
58
51
|
return ExitCode.NoModel;
|
|
59
52
|
}
|
|
60
53
|
const { model, provider } = found;
|
|
61
|
-
const key = await registry.resolveApiKey(provider.id);
|
|
54
|
+
const key = await registry.resolveApiKey(provider.id, model.channel);
|
|
62
55
|
if (key.apiKey === undefined && provider.requiresApiKey) {
|
|
63
56
|
io.stderr(`ama: ${provider.id} 没有 API key(ama auth set ${provider.id})\n`);
|
|
64
57
|
return ExitCode.NoModel;
|
|
@@ -81,7 +74,8 @@ async function check(io, registry, ref) {
|
|
|
81
74
|
io.stderr(`ama: ${provider.id}/${model.id} 失败(${ms} ms):${message.errorMessage ?? message.stopReason}\n`);
|
|
82
75
|
return ExitCode.RuntimeError;
|
|
83
76
|
}
|
|
84
|
-
|
|
77
|
+
const shown = formatModelRef(modelRefOf(model));
|
|
78
|
+
io.stdout(`${shown} 可用(${model.api},${ms} ms,stopReason ${message.stopReason})\n`);
|
|
85
79
|
return ExitCode.Ok;
|
|
86
80
|
}
|
|
87
81
|
/** 动作表:键是子命令名,顺序即用法文本顺序。 */
|
|
@@ -98,6 +92,20 @@ export const MODELS_ACTIONS = Object.freeze({
|
|
|
98
92
|
},
|
|
99
93
|
discover: DISCOVER_ACTION,
|
|
100
94
|
"cache-probe": CACHE_PROBE_ACTION,
|
|
95
|
+
"refresh-catalog": {
|
|
96
|
+
usage: "ama models refresh-catalog",
|
|
97
|
+
run: async (ctx) => {
|
|
98
|
+
const result = await refreshModelsDev({
|
|
99
|
+
dataDir: ctx.level.dataDir,
|
|
100
|
+
env: ctx.io.env,
|
|
101
|
+
force: true,
|
|
102
|
+
});
|
|
103
|
+
ctx.io.stdout(`${describeRefresh(result)}\n${modelsDevCachePath(ctx.level.dataDir)}\n`);
|
|
104
|
+
if (result.warning !== undefined)
|
|
105
|
+
ctx.io.stderr(`ama: 警告:${result.warning}\n`);
|
|
106
|
+
return result.status === "unavailable" ? ExitCode.RuntimeError : ExitCode.Ok;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
101
109
|
});
|
|
102
110
|
export const MODELS_USAGE = `用法:${Object.values(MODELS_ACTIONS)
|
|
103
111
|
.map((a) => a.usage)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 探测的执行层(`ama providers add|refresh --probe`、`ama models discover --probe` 共用)。
|
|
3
|
+
*
|
|
4
|
+
* - **首事件即判定**:HTTP 成功、流里出现第一个内容事件(text / thinking / toolcall)或 done 即判可用,
|
|
5
|
+
* 立刻中止请求;只见到 `start`(推理模型可能要想很久才出第一个 delta)时再等 `settleMs`,期间没有
|
|
6
|
+
* error 事件也判可用——防止中转先回 200、紧接着在流里报错被误判。HTTP 错误、流 error 事件、超时判不可用。
|
|
7
|
+
* - **有界并发**:同时在途 ≤ `concurrency`;结果按任务下标交回,调用方自己按顺序输出。
|
|
8
|
+
* - **限流**:429 时并发减半(至少 1),等 `retryDelayMs` 后重试该请求一次;重试仍 429 → 停止;
|
|
9
|
+
* 401 / 403 → 立即停止。停止后不再发新请求,并中止在途的请求(它们的结果标记为 aborted)。
|
|
10
|
+
* - **恢复(加法增、乘法减)**:降并发之后,每连续 `recoverAfter`(缺省 4)次没被限流的请求,并发 +1,
|
|
11
|
+
* 直到回到初始并发;中途再遇 429 重新减半、计数清零。
|
|
12
|
+
*
|
|
13
|
+
* `ama models check` 不走这里:它只发一次,要看到完整回复与耗时。
|
|
14
|
+
*/
|
|
15
|
+
import type { Model, ProviderRegistryApi } from "../../ai/types.js";
|
|
16
|
+
export declare const PROBE_TIMEOUT_MS = 15000;
|
|
17
|
+
export declare const DEFAULT_PROBE_CONCURRENCY = 6;
|
|
18
|
+
export declare const MAX_PROBE_CONCURRENCY = 16;
|
|
19
|
+
export declare const PROBE_SETTLE_MS = 1000;
|
|
20
|
+
export declare const RATE_LIMIT_RETRY_MS = 2000;
|
|
21
|
+
/** 降并发后,连续多少次没被限流才 +1。 */
|
|
22
|
+
export declare const RECOVER_AFTER = 4;
|
|
23
|
+
/** 鉴权失败或限流:继续只会浪费请求。 */
|
|
24
|
+
export declare const FATAL_STATUS: RegExp;
|
|
25
|
+
export interface ProbeTuning {
|
|
26
|
+
timeoutMs?: number | undefined;
|
|
27
|
+
settleMs?: number | undefined;
|
|
28
|
+
}
|
|
29
|
+
/** 一次探测请求的结果:error 为 undefined 即可用;aborted 表示被调度方中止,结果作废。 */
|
|
30
|
+
export interface ProbeOutcome {
|
|
31
|
+
error?: string;
|
|
32
|
+
aborted?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** 发一次最小请求,见到首个有效事件即判定并中止。 */
|
|
35
|
+
export declare function probeOnce(registry: ProviderRegistryApi, model: Model, apiKey: string | undefined, tuning?: ProbeTuning, signal?: AbortSignal): Promise<ProbeOutcome>;
|
|
36
|
+
export interface SchedulerOptions extends ProbeTuning {
|
|
37
|
+
concurrency?: number | undefined;
|
|
38
|
+
retryDelayMs?: number | undefined;
|
|
39
|
+
/** 429 降并发时回调(新的并发数)。 */
|
|
40
|
+
onThrottle?: (concurrency: number) => void;
|
|
41
|
+
/** 连续多少次没被限流后并发 +1(缺省 RECOVER_AFTER)。 */
|
|
42
|
+
recoverAfter?: number | undefined;
|
|
43
|
+
/** 并发回升时回调(新的并发数)。 */
|
|
44
|
+
onRecover?: (concurrency: number) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 有界并发的探测调度器:`run` 按下标调度任务,任务里用 `probe` 发请求(带 429 重试与停止判定)。
|
|
48
|
+
*/
|
|
49
|
+
export declare class ProbeScheduler {
|
|
50
|
+
private readonly registry;
|
|
51
|
+
private readonly apiKey;
|
|
52
|
+
private readonly options;
|
|
53
|
+
private limit;
|
|
54
|
+
private readonly initial;
|
|
55
|
+
/** 上次降并发以来连续没被限流的请求数。 */
|
|
56
|
+
private streak;
|
|
57
|
+
private readonly controller;
|
|
58
|
+
private stopReason;
|
|
59
|
+
private active;
|
|
60
|
+
/** 观测用:同时在途请求数的峰值。 */
|
|
61
|
+
peak: number;
|
|
62
|
+
constructor(registry: ProviderRegistryApi, apiKey: string | undefined, options?: SchedulerOptions);
|
|
63
|
+
get stopped(): string | undefined;
|
|
64
|
+
get concurrency(): number;
|
|
65
|
+
stop(reason: string): void;
|
|
66
|
+
/** 一次探测;429 降并发、退避后重试一次,仍 429 或 401 / 403 → 停止。 */
|
|
67
|
+
probe(model: Model): Promise<ProbeOutcome>;
|
|
68
|
+
/** 一次没被限流的请求:攒够 recoverAfter 次就并发 +1(不超过初始值)。 */
|
|
69
|
+
private recover;
|
|
70
|
+
/** 并发执行 `count` 个任务;`onDone` 按完成顺序回调。停止后未开始的任务不执行(结果为 undefined)。 */
|
|
71
|
+
run<T>(count: number, task: (index: number) => Promise<T>, onDone?: (index: number, result: T) => void): Promise<(T | undefined)[]>;
|
|
72
|
+
private get signal();
|
|
73
|
+
}
|
|
74
|
+
/** `--concurrency`(1–16)与 `--probe-timeout`(毫秒,1000–300000)。 */
|
|
75
|
+
export declare function parseProbeTuning(values: ReadonlyMap<string, string>): {
|
|
76
|
+
concurrency: number;
|
|
77
|
+
timeoutMs: number;
|
|
78
|
+
};
|
|
79
|
+
/** 预估文案:`并发 6,单次最长 15 s,预计不超过 150 s`。 */
|
|
80
|
+
export declare function describeProbePlan(requests: number, concurrency: number, timeoutMs: number): string;
|
|
81
|
+
/** 进度:TTY 下单行刷新「探测 18/60」;非 TTY 只在结束时打印一行汇总。 */
|
|
82
|
+
export declare class ProbeProgress {
|
|
83
|
+
private readonly write;
|
|
84
|
+
private readonly tty;
|
|
85
|
+
private readonly total;
|
|
86
|
+
private done;
|
|
87
|
+
private shown;
|
|
88
|
+
private readonly started;
|
|
89
|
+
constructor(write: (text: string) => void, tty: boolean, total: number);
|
|
90
|
+
/** 输出一行结果(TTY 下先擦掉进度行,再重画)。 */
|
|
91
|
+
line(text: string): void;
|
|
92
|
+
tick(): void;
|
|
93
|
+
finish(): void;
|
|
94
|
+
private draw;
|
|
95
|
+
private clear;
|
|
96
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 探测的执行层(`ama providers add|refresh --probe`、`ama models discover --probe` 共用)。
|
|
3
|
+
*
|
|
4
|
+
* - **首事件即判定**:HTTP 成功、流里出现第一个内容事件(text / thinking / toolcall)或 done 即判可用,
|
|
5
|
+
* 立刻中止请求;只见到 `start`(推理模型可能要想很久才出第一个 delta)时再等 `settleMs`,期间没有
|
|
6
|
+
* error 事件也判可用——防止中转先回 200、紧接着在流里报错被误判。HTTP 错误、流 error 事件、超时判不可用。
|
|
7
|
+
* - **有界并发**:同时在途 ≤ `concurrency`;结果按任务下标交回,调用方自己按顺序输出。
|
|
8
|
+
* - **限流**:429 时并发减半(至少 1),等 `retryDelayMs` 后重试该请求一次;重试仍 429 → 停止;
|
|
9
|
+
* 401 / 403 → 立即停止。停止后不再发新请求,并中止在途的请求(它们的结果标记为 aborted)。
|
|
10
|
+
* - **恢复(加法增、乘法减)**:降并发之后,每连续 `recoverAfter`(缺省 4)次没被限流的请求,并发 +1,
|
|
11
|
+
* 直到回到初始并发;中途再遇 429 重新减半、计数清零。
|
|
12
|
+
*
|
|
13
|
+
* `ama models check` 不走这里:它只发一次,要看到完整回复与耗时。
|
|
14
|
+
*/
|
|
15
|
+
import { UsageError } from "../args.js";
|
|
16
|
+
export const PROBE_TIMEOUT_MS = 15_000;
|
|
17
|
+
export const DEFAULT_PROBE_CONCURRENCY = 6;
|
|
18
|
+
export const MAX_PROBE_CONCURRENCY = 16;
|
|
19
|
+
export const PROBE_SETTLE_MS = 1_000;
|
|
20
|
+
export const RATE_LIMIT_RETRY_MS = 2_000;
|
|
21
|
+
/** 降并发后,连续多少次没被限流才 +1。 */
|
|
22
|
+
export const RECOVER_AFTER = 4;
|
|
23
|
+
/** 鉴权失败或限流:继续只会浪费请求。 */
|
|
24
|
+
export const FATAL_STATUS = /^(?:HTTP )?(?:401|403|429)\b/;
|
|
25
|
+
const RATE_LIMITED = /^(?:HTTP )?429\b/;
|
|
26
|
+
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
27
|
+
const timer = setTimeout(done, ms);
|
|
28
|
+
function done() {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
signal?.removeEventListener("abort", done);
|
|
31
|
+
resolve();
|
|
32
|
+
}
|
|
33
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
34
|
+
});
|
|
35
|
+
/** 发一次最小请求,见到首个有效事件即判定并中止。 */
|
|
36
|
+
export async function probeOnce(registry, model, apiKey, tuning = {}, signal) {
|
|
37
|
+
const impl = registry.getApi(model.api);
|
|
38
|
+
if (impl === undefined)
|
|
39
|
+
return { error: `协议 ${model.api} 尚未实现` };
|
|
40
|
+
if (signal?.aborted)
|
|
41
|
+
return { aborted: true };
|
|
42
|
+
const timeoutMs = tuning.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
43
|
+
const settleMs = tuning.settleMs ?? PROBE_SETTLE_MS;
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
let timedOut = false;
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
timedOut = true;
|
|
48
|
+
controller.abort();
|
|
49
|
+
}, timeoutMs);
|
|
50
|
+
const onAbort = () => controller.abort();
|
|
51
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
52
|
+
const verdict = (outcome) => {
|
|
53
|
+
if (signal?.aborted)
|
|
54
|
+
return { aborted: true };
|
|
55
|
+
if (timedOut)
|
|
56
|
+
return { error: `超时(${Math.round(timeoutMs / 1000)} s 内没有响应)` };
|
|
57
|
+
return outcome;
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const stream = impl.stream(model, { messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }] }, {
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
...(apiKey !== undefined ? { apiKey } : {}),
|
|
63
|
+
maxTokens: 16,
|
|
64
|
+
cacheRetention: "none",
|
|
65
|
+
purpose: "probe",
|
|
66
|
+
});
|
|
67
|
+
const events = stream[Symbol.asyncIterator]();
|
|
68
|
+
let settled;
|
|
69
|
+
while (true) {
|
|
70
|
+
const next = events.next();
|
|
71
|
+
const step = settled !== undefined ? await Promise.race([next, settled]) : await next;
|
|
72
|
+
if (step === "settled")
|
|
73
|
+
return verdict({});
|
|
74
|
+
if (step.done)
|
|
75
|
+
break;
|
|
76
|
+
const event = step.value;
|
|
77
|
+
if (event.type === "error")
|
|
78
|
+
return verdict({ error: event.message.errorMessage ?? event.reason });
|
|
79
|
+
if (event.type === "start") {
|
|
80
|
+
settled ??= sleep(settleMs, controller.signal).then(() => "settled");
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
return verdict({});
|
|
84
|
+
}
|
|
85
|
+
// 流没有逐个产出事件(实现只给 result):按最终消息判定
|
|
86
|
+
const message = await stream.result();
|
|
87
|
+
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
88
|
+
return verdict({ error: message.errorMessage ?? message.stopReason });
|
|
89
|
+
return verdict({});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return verdict({ error: error instanceof Error ? error.message : String(error) });
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
signal?.removeEventListener("abort", onAbort);
|
|
97
|
+
// 判定后立刻断开:不等推理模型把 16 个 token 想完
|
|
98
|
+
controller.abort();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 有界并发的探测调度器:`run` 按下标调度任务,任务里用 `probe` 发请求(带 429 重试与停止判定)。
|
|
103
|
+
*/
|
|
104
|
+
export class ProbeScheduler {
|
|
105
|
+
registry;
|
|
106
|
+
apiKey;
|
|
107
|
+
options;
|
|
108
|
+
limit;
|
|
109
|
+
initial;
|
|
110
|
+
/** 上次降并发以来连续没被限流的请求数。 */
|
|
111
|
+
streak = 0;
|
|
112
|
+
controller = new AbortController();
|
|
113
|
+
stopReason;
|
|
114
|
+
active = 0;
|
|
115
|
+
/** 观测用:同时在途请求数的峰值。 */
|
|
116
|
+
peak = 0;
|
|
117
|
+
constructor(registry, apiKey, options = {}) {
|
|
118
|
+
this.registry = registry;
|
|
119
|
+
this.apiKey = apiKey;
|
|
120
|
+
this.options = options;
|
|
121
|
+
this.limit = options.concurrency ?? DEFAULT_PROBE_CONCURRENCY;
|
|
122
|
+
this.initial = this.limit;
|
|
123
|
+
}
|
|
124
|
+
get stopped() {
|
|
125
|
+
return this.stopReason;
|
|
126
|
+
}
|
|
127
|
+
get concurrency() {
|
|
128
|
+
return this.limit;
|
|
129
|
+
}
|
|
130
|
+
stop(reason) {
|
|
131
|
+
if (this.stopReason !== undefined)
|
|
132
|
+
return;
|
|
133
|
+
this.stopReason = reason;
|
|
134
|
+
this.controller.abort();
|
|
135
|
+
}
|
|
136
|
+
/** 一次探测;429 降并发、退避后重试一次,仍 429 或 401 / 403 → 停止。 */
|
|
137
|
+
async probe(model) {
|
|
138
|
+
if (this.stopReason !== undefined)
|
|
139
|
+
return { aborted: true };
|
|
140
|
+
let outcome = await probeOnce(this.registry, model, this.apiKey, this.options, this.signal);
|
|
141
|
+
if (outcome.error !== undefined && RATE_LIMITED.test(outcome.error)) {
|
|
142
|
+
this.limit = Math.max(1, Math.floor(this.limit / 2));
|
|
143
|
+
this.streak = 0;
|
|
144
|
+
this.options.onThrottle?.(this.limit);
|
|
145
|
+
await sleep(this.options.retryDelayMs ?? RATE_LIMIT_RETRY_MS, this.signal);
|
|
146
|
+
if (this.stopReason !== undefined)
|
|
147
|
+
return { aborted: true };
|
|
148
|
+
outcome = await probeOnce(this.registry, model, this.apiKey, this.options, this.signal);
|
|
149
|
+
if (outcome.error !== undefined && RATE_LIMITED.test(outcome.error)) {
|
|
150
|
+
this.stop(`连续 429 限流:${outcome.error}`);
|
|
151
|
+
return outcome;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (outcome.error !== undefined && FATAL_STATUS.test(outcome.error))
|
|
155
|
+
this.stop(outcome.error);
|
|
156
|
+
else if (outcome.aborted !== true)
|
|
157
|
+
this.recover();
|
|
158
|
+
return outcome;
|
|
159
|
+
}
|
|
160
|
+
/** 一次没被限流的请求:攒够 recoverAfter 次就并发 +1(不超过初始值)。 */
|
|
161
|
+
recover() {
|
|
162
|
+
if (this.limit >= this.initial)
|
|
163
|
+
return;
|
|
164
|
+
this.streak++;
|
|
165
|
+
if (this.streak < (this.options.recoverAfter ?? RECOVER_AFTER))
|
|
166
|
+
return;
|
|
167
|
+
this.streak = 0;
|
|
168
|
+
this.limit++;
|
|
169
|
+
this.options.onRecover?.(this.limit);
|
|
170
|
+
}
|
|
171
|
+
/** 并发执行 `count` 个任务;`onDone` 按完成顺序回调。停止后未开始的任务不执行(结果为 undefined)。 */
|
|
172
|
+
async run(count, task, onDone) {
|
|
173
|
+
const results = new Array(count).fill(undefined);
|
|
174
|
+
let next = 0;
|
|
175
|
+
await new Promise((resolve, reject) => {
|
|
176
|
+
const pump = () => {
|
|
177
|
+
while (this.stopReason === undefined && next < count && this.active < this.limit) {
|
|
178
|
+
const index = next++;
|
|
179
|
+
this.active++;
|
|
180
|
+
this.peak = Math.max(this.peak, this.active);
|
|
181
|
+
task(index).then((result) => {
|
|
182
|
+
this.active--;
|
|
183
|
+
results[index] = result;
|
|
184
|
+
onDone?.(index, result);
|
|
185
|
+
pump();
|
|
186
|
+
}, (error) => {
|
|
187
|
+
this.active--;
|
|
188
|
+
this.stop(error instanceof Error ? error.message : String(error));
|
|
189
|
+
reject(error);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (this.active === 0 && (next >= count || this.stopReason !== undefined))
|
|
193
|
+
resolve();
|
|
194
|
+
};
|
|
195
|
+
pump();
|
|
196
|
+
});
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
get signal() {
|
|
200
|
+
return this.controller.signal;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** `--concurrency`(1–16)与 `--probe-timeout`(毫秒,1000–300000)。 */
|
|
204
|
+
export function parseProbeTuning(values) {
|
|
205
|
+
const int = (name, fallback, min, max) => {
|
|
206
|
+
const raw = values.get(name);
|
|
207
|
+
if (raw === undefined)
|
|
208
|
+
return fallback;
|
|
209
|
+
const n = Number(raw);
|
|
210
|
+
if (!Number.isInteger(n) || n < min || n > max)
|
|
211
|
+
throw new UsageError(`--${name} 需要 ${min}–${max} 的整数:${raw}`);
|
|
212
|
+
return n;
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
concurrency: int("concurrency", DEFAULT_PROBE_CONCURRENCY, 1, MAX_PROBE_CONCURRENCY),
|
|
216
|
+
timeoutMs: int("probe-timeout", PROBE_TIMEOUT_MS, 1_000, 300_000),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** 预估文案:`并发 6,单次最长 15 s,预计不超过 150 s`。 */
|
|
220
|
+
export function describeProbePlan(requests, concurrency, timeoutMs) {
|
|
221
|
+
const worst = Math.ceil(requests / concurrency) * Math.ceil(timeoutMs / 1000);
|
|
222
|
+
return `并发 ${concurrency},单次最长 ${Math.round(timeoutMs / 1000)} s,预计不超过 ${worst} s`;
|
|
223
|
+
}
|
|
224
|
+
/** 进度:TTY 下单行刷新「探测 18/60」;非 TTY 只在结束时打印一行汇总。 */
|
|
225
|
+
export class ProbeProgress {
|
|
226
|
+
write;
|
|
227
|
+
tty;
|
|
228
|
+
total;
|
|
229
|
+
done = 0;
|
|
230
|
+
shown = false;
|
|
231
|
+
started = Date.now();
|
|
232
|
+
constructor(write, tty, total) {
|
|
233
|
+
this.write = write;
|
|
234
|
+
this.tty = tty;
|
|
235
|
+
this.total = total;
|
|
236
|
+
}
|
|
237
|
+
/** 输出一行结果(TTY 下先擦掉进度行,再重画)。 */
|
|
238
|
+
line(text) {
|
|
239
|
+
this.clear();
|
|
240
|
+
this.write(text);
|
|
241
|
+
this.draw();
|
|
242
|
+
}
|
|
243
|
+
tick() {
|
|
244
|
+
this.done++;
|
|
245
|
+
this.draw();
|
|
246
|
+
}
|
|
247
|
+
finish() {
|
|
248
|
+
this.clear();
|
|
249
|
+
const seconds = ((Date.now() - this.started) / 1000).toFixed(1);
|
|
250
|
+
this.write(`探测完成 ${this.done}/${this.total},用时 ${seconds} s\n`);
|
|
251
|
+
}
|
|
252
|
+
draw() {
|
|
253
|
+
if (!this.tty)
|
|
254
|
+
return;
|
|
255
|
+
this.write(`\r\x1b[2K探测 ${this.done}/${this.total}`);
|
|
256
|
+
this.shown = true;
|
|
257
|
+
}
|
|
258
|
+
clear() {
|
|
259
|
+
if (!this.tty || !this.shown)
|
|
260
|
+
return;
|
|
261
|
+
this.write("\r\x1b[2K");
|
|
262
|
+
this.shown = false;
|
|
263
|
+
}
|
|
264
|
+
}
|