micro-models-agent 0.63.0 → 1.1.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 +174 -0
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +8 -4
- package/dist/i18n/ru.json +8 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1755 -841
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/bridge-server.mjs +37 -4
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
|
@@ -12,14 +12,14 @@ const ZEN_PRICES = {
|
|
|
12
12
|
"nemotron-3-ultra-free": { input: 0, output: 0 },
|
|
13
13
|
"nemotron-3.5-lightning-free": { input: 0, output: 0 },
|
|
14
14
|
"muse-spark-1.2-contributor-free": { input: 0, output: 0 },
|
|
15
|
-
// MiniMax
|
|
16
|
-
"minimax-m3": { input: 0.3, output: 1.2 },
|
|
17
|
-
"minimax-m2.7": { input: 0.3, output: 1.2 },
|
|
18
|
-
"minimax-m2.5": { input: 0.3, output: 1.2 },
|
|
19
|
-
// GLM
|
|
20
|
-
"glm-5.2": { input: 1.4, output: 4.4 },
|
|
21
|
-
"glm-5.1": { input: 1.4, output: 4.4 },
|
|
22
|
-
"glm-5": { input: 1.0, output: 3.2 },
|
|
15
|
+
// MiniMax (Cached Read по прайсу Zen)
|
|
16
|
+
"minimax-m3": { input: 0.3, output: 1.2, cachedInput: 0.06 },
|
|
17
|
+
"minimax-m2.7": { input: 0.3, output: 1.2, cachedInput: 0.06 },
|
|
18
|
+
"minimax-m2.5": { input: 0.3, output: 1.2, cachedInput: 0.06 },
|
|
19
|
+
// GLM (Cached Read по прайсу Zen)
|
|
20
|
+
"glm-5.2": { input: 1.4, output: 4.4, cachedInput: 0.26 },
|
|
21
|
+
"glm-5.1": { input: 1.4, output: 4.4, cachedInput: 0.26 },
|
|
22
|
+
"glm-5": { input: 1.0, output: 3.2, cachedInput: 0.2 },
|
|
23
23
|
"glm-5.3": { input: 1.4, output: 4.4 },
|
|
24
24
|
// Kimi
|
|
25
25
|
"kimi-k2.7-code": { input: 0.95, output: 4.0 },
|
|
@@ -108,14 +108,32 @@ export function resolvePrice(model, config) {
|
|
|
108
108
|
return ZEN_PRICES[bare];
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
111
|
+
* Считает стоимость и экономию вызова. `promptTokens` — суммарные prompt-токены
|
|
112
|
+
* (cached + uncached); кешированная часть тарифицируется по `cachedInput`, а
|
|
113
|
+
* запись в кеш — по `cacheWrite`. Возвращает undefined без известной цены.
|
|
113
114
|
*/
|
|
114
|
-
export function
|
|
115
|
+
export function calculateCostDetailed(model, promptTokens, completionTokens, config, cache) {
|
|
115
116
|
const price = resolvePrice(model, config);
|
|
116
117
|
if (!price)
|
|
117
118
|
return undefined;
|
|
118
|
-
|
|
119
|
+
const cached = Math.max(0, cache?.cachedTokens ?? 0);
|
|
120
|
+
const write = Math.max(0, cache?.cacheWriteTokens ?? 0);
|
|
121
|
+
const uncached = Math.max(0, promptTokens - cached);
|
|
122
|
+
const cachedRate = price.cachedInput ?? price.input;
|
|
123
|
+
const writeRate = price.cacheWrite ?? 0;
|
|
124
|
+
const cost = (uncached / 1_000_000) * price.input +
|
|
125
|
+
(cached / 1_000_000) * cachedRate +
|
|
126
|
+
(write / 1_000_000) * writeRate +
|
|
127
|
+
(completionTokens / 1_000_000) * price.output;
|
|
128
|
+
const saved = (cached / 1_000_000) * (price.input - cachedRate);
|
|
129
|
+
return { cost, saved };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Стоимость вызова в USD (обёртка над `calculateCostDetailed`). Возвращает
|
|
133
|
+
* undefined, когда цена модели неизвестна.
|
|
134
|
+
*/
|
|
135
|
+
export function calculateCost(model, promptTokens, completionTokens, config, cache) {
|
|
136
|
+
return calculateCostDetailed(model, promptTokens, completionTokens, config, cache)?.cost;
|
|
119
137
|
}
|
|
120
138
|
/** Format a USD cost for display. Zero and very small amounts stay readable. */
|
|
121
139
|
export function formatCost(cost) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { platform } from "os";
|
|
3
|
+
/** Synchronous pause without blocking the event loop (via Atomics.wait). */
|
|
4
|
+
function sleepSync(ms) {
|
|
5
|
+
try {
|
|
6
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
/* ignore */
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function taskkillPid(pid) {
|
|
13
|
+
try {
|
|
14
|
+
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
stdio: "ignore",
|
|
17
|
+
timeout: 5000,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* already dead */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Kill a spawned process tree — cross-platform.
|
|
26
|
+
*
|
|
27
|
+
* Windows: double `taskkill /T /F` with a 150ms settle gap. The second pass
|
|
28
|
+
* catches late-attaching children that were still being created during the
|
|
29
|
+
* first tree walk (e.g. `cmd.exe → node → tsserver` chains where node is
|
|
30
|
+
* still starting up when cmd is killed).
|
|
31
|
+
*
|
|
32
|
+
* POSIX: process-group kill (`-pid`). Falls back to `SIGKILL` on the direct
|
|
33
|
+
* child when the group kill fails (e.g. non-detached process).
|
|
34
|
+
*/
|
|
35
|
+
export function killTree(child) {
|
|
36
|
+
const pid = child.pid;
|
|
37
|
+
if (!pid)
|
|
38
|
+
return;
|
|
39
|
+
if (platform() === "win32") {
|
|
40
|
+
taskkillPid(pid);
|
|
41
|
+
sleepSync(150);
|
|
42
|
+
taskkillPid(pid);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
process.kill(-pid, "SIGTERM");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
try {
|
|
50
|
+
child.kill("SIGKILL");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* already dead */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "child_process";
|
|
2
2
|
import { platform } from "os";
|
|
3
|
+
import { killTree } from "./kill-tree";
|
|
3
4
|
const MAX_LOG_LINES = 2000;
|
|
4
5
|
const MAX_KEPT_PROCESSES = 20;
|
|
5
6
|
let seq = 0;
|
|
@@ -64,60 +65,7 @@ function decodeLineToUtf8(buf) {
|
|
|
64
65
|
}
|
|
65
66
|
return buf.toString("latin1");
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
function sleepSync(ms) {
|
|
69
|
-
try {
|
|
70
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
/* ignore */
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function taskkillPid(pid) {
|
|
77
|
-
try {
|
|
78
|
-
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
79
|
-
windowsHide: true,
|
|
80
|
-
stdio: "ignore",
|
|
81
|
-
timeout: 5000,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
/* already dead */
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Kill a spawned tree on Windows.
|
|
90
|
-
*
|
|
91
|
-
* `taskkill /T` has a race: when cmd.exe is killed, a child (node) that is
|
|
92
|
-
* still starting up can survive as an orphan because it was never attached to
|
|
93
|
-
* the tree at kill time. A single pass misses it. Running taskkill twice with
|
|
94
|
-
* a settle gap catches those late starters on the second pass — the second
|
|
95
|
-
* tree walk sees the child that was still being created during the first one.
|
|
96
|
-
* This is much cheaper than enumerating processes (wmic/PowerShell), which
|
|
97
|
-
* slows every kill down to seconds under parallel load.
|
|
98
|
-
*/
|
|
99
|
-
export function killTree(child) {
|
|
100
|
-
const pid = child.pid;
|
|
101
|
-
if (!pid)
|
|
102
|
-
return;
|
|
103
|
-
if (platform() === "win32") {
|
|
104
|
-
taskkillPid(pid);
|
|
105
|
-
sleepSync(150);
|
|
106
|
-
taskkillPid(pid);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
process.kill(-pid, "SIGTERM");
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
try {
|
|
114
|
-
child.kill("SIGKILL");
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
/* already dead */
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
68
|
+
export { killTree } from "./kill-tree";
|
|
121
69
|
class ProcessRegistry {
|
|
122
70
|
procs = new Map();
|
|
123
71
|
children = new Map();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Фолбэк для провайдеров без кеша и без полей запроса о кеше. Используется,
|
|
3
|
+
* когда пресет не объявляет `cache`-возможность.
|
|
4
|
+
*/
|
|
5
|
+
export const NO_CACHE_CAPABILITY = {
|
|
6
|
+
mechanism: "none",
|
|
7
|
+
report: "none",
|
|
8
|
+
sessionHeader: false,
|
|
9
|
+
requestCachePrompt: false,
|
|
10
|
+
requestPromptCacheKey: false,
|
|
11
|
+
requestSessionId: false,
|
|
12
|
+
requestCacheControl: false,
|
|
13
|
+
requestStreamUsage: false,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Резолвит эффективную cache-возможность провайдера: дефолт пресета, поверх
|
|
17
|
+
* которого накладывается переопределение из записи конфига. Всегда возвращает
|
|
18
|
+
* новый объект, чтобы вызывающий не рисковал мутировать общее состояние.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveCacheCapability(spec, override) {
|
|
21
|
+
const base = spec?.capabilities.cache ?? NO_CACHE_CAPABILITY;
|
|
22
|
+
return { ...base, ...(override ?? {}) };
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ProviderManager } from "./manager";
|
|
2
|
+
import { FallbackProvider } from "./fallback";
|
|
3
|
+
/**
|
|
4
|
+
* Shared factory for building the ACTIVE provider from a config, used by both
|
|
5
|
+
* bootstrap and Agent.setProvider/reconfigure. Previously the three
|
|
6
|
+
* construction sites drifted — bootstrap wrapped the manager in
|
|
7
|
+
* FallbackProvider when `fallback && entries > 1`, but a hot-swap
|
|
8
|
+
* (setProvider) rebuilt a bare manager and silently dropped failover (B4).
|
|
9
|
+
* The wrapper reads `manager.active` live, so switching the manager after
|
|
10
|
+
* build keeps the failover active.
|
|
11
|
+
*/
|
|
12
|
+
export function buildActiveProvider(config, logger, opts) {
|
|
13
|
+
const manager = new ProviderManager(config.provider, {
|
|
14
|
+
contextWindow: config.contextWindow,
|
|
15
|
+
retry: config.retry,
|
|
16
|
+
rateLimits: config.security?.rateLimits,
|
|
17
|
+
getSessionId: opts?.getSessionId,
|
|
18
|
+
logger,
|
|
19
|
+
});
|
|
20
|
+
manager.setModel(config.model);
|
|
21
|
+
let provider = manager.active;
|
|
22
|
+
if (config.provider.fallback && manager.listNames().length > 1) {
|
|
23
|
+
provider = new FallbackProvider(manager, (from, to, error) => {
|
|
24
|
+
logger?.warn(`provider failover: ${from} -> ${to} (${error.message.slice(0, 120)})`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return { manager, provider };
|
|
28
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { LlmError } from "../../llm/llm-errors";
|
|
1
2
|
/** True when the error is worth failing over to another provider. */
|
|
2
3
|
export function isFailoverError(err) {
|
|
3
|
-
|
|
4
|
-
if (e?.name === "AbortError")
|
|
4
|
+
if (err instanceof Error && err.name === "AbortError")
|
|
5
5
|
return false;
|
|
6
|
-
// HTTP status attached by openai-compat: retryable
|
|
7
|
-
if (typeof
|
|
8
|
-
return
|
|
6
|
+
// HTTP status, attached by openai-compat as a typed LlmError: retryable only.
|
|
7
|
+
if (err instanceof LlmError && typeof err.llmStatus === "number") {
|
|
8
|
+
return err.llmStatus === 429 || err.llmStatus >= 500;
|
|
9
|
+
}
|
|
9
10
|
// Network-level failures by errno-style code (Bun: "ConnectionRefused",
|
|
10
11
|
// Node: "ECONNREFUSED", DNS: "ENOTFOUND"/"EAI_AGAIN", etc).
|
|
12
|
+
const e = err;
|
|
11
13
|
const normCode = (e?.code ?? "").replace(/[-_]/g, "").toLowerCase();
|
|
12
14
|
if (/^abort/.test(normCode))
|
|
13
15
|
return false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errMsg } from "../../utils";
|
|
1
2
|
/**
|
|
2
3
|
* Probe providers concurrently (bounded by the caller passing few entries)
|
|
3
4
|
* and report ✓/✗ per entry. Uses `listModels()` — a free, cheap call — never
|
|
@@ -15,7 +16,7 @@ async function probeOne(target, timeoutMs) {
|
|
|
15
16
|
return { name: target.name, ok: true, ms: Date.now() - start, models: models.length };
|
|
16
17
|
}
|
|
17
18
|
catch (err) {
|
|
18
|
-
const message =
|
|
19
|
+
const message = errMsg(err);
|
|
19
20
|
return {
|
|
20
21
|
name: target.name,
|
|
21
22
|
ok: false,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ProviderRegistry } from "./registry";
|
|
2
2
|
export { createProvider, defaultRegistry } from "./create";
|
|
3
3
|
export { ProviderManager, presetOf } from "./manager";
|
|
4
|
+
export { resolveCacheCapability, NO_CACHE_CAPABILITY } from "./cache";
|
|
4
5
|
export { probeProviders, targetsFromProviders, } from "./health";
|
|
5
6
|
export { BUILTIN_PROVIDERS, HOSTED_BASE_URLS } from "./presets";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createProvider, defaultRegistry } from "./create";
|
|
2
|
+
import { resolveCacheCapability } from "./cache";
|
|
2
3
|
import { BUILTIN_PROVIDERS } from "./presets";
|
|
3
4
|
/**
|
|
4
5
|
* Manages the active provider + model at runtime. Supports several configured
|
|
@@ -8,6 +9,7 @@ import { BUILTIN_PROVIDERS } from "./presets";
|
|
|
8
9
|
*/
|
|
9
10
|
export class ProviderManager {
|
|
10
11
|
entries;
|
|
12
|
+
lastKnownFallback;
|
|
11
13
|
activeProviderName;
|
|
12
14
|
model;
|
|
13
15
|
opts;
|
|
@@ -18,6 +20,7 @@ export class ProviderManager {
|
|
|
18
20
|
this.registry = opts.registry ?? defaultRegistry();
|
|
19
21
|
this.entries = this.normalizeEntries(config);
|
|
20
22
|
this.activeProviderName = this.resolveActive(config);
|
|
23
|
+
this.lastKnownFallback = config.fallback;
|
|
21
24
|
this.model = "";
|
|
22
25
|
}
|
|
23
26
|
/** The model the active provider is built with. */
|
|
@@ -92,12 +95,14 @@ export class ProviderManager {
|
|
|
92
95
|
return this.active;
|
|
93
96
|
}
|
|
94
97
|
/** Serialize back to ProviderConfig (keeps active/entries in sync). */
|
|
95
|
-
toConfig() {
|
|
98
|
+
toConfig(fallback) {
|
|
96
99
|
const active = this.activeEntry;
|
|
97
100
|
return {
|
|
98
101
|
active: this.activeProviderName,
|
|
99
102
|
entries: this.entries,
|
|
100
|
-
|
|
103
|
+
// Preserve the configured value — hardcoding false silently dropped
|
|
104
|
+
// the user's provider.fallback on every hot-swap.
|
|
105
|
+
fallback: fallback ?? this.lastKnownFallback ?? false,
|
|
101
106
|
type: active.type,
|
|
102
107
|
baseUrl: active.baseUrl,
|
|
103
108
|
apiKey: active.apiKey,
|
|
@@ -143,10 +148,20 @@ export class ProviderManager {
|
|
|
143
148
|
retry: entry.retry ?? this.opts.retry,
|
|
144
149
|
rateLimits: entry.rateLimits ?? this.opts.rateLimits,
|
|
145
150
|
maxCompletionTokens: entry.maxCompletionTokens,
|
|
151
|
+
getSessionId: this.opts.getSessionId,
|
|
152
|
+
capabilities: this.resolveCapabilities(entry),
|
|
153
|
+
logger: this.opts.logger,
|
|
146
154
|
}, this.registry);
|
|
147
155
|
this.cache.set(key, provider);
|
|
148
156
|
return provider;
|
|
149
157
|
}
|
|
158
|
+
/** Возможности пресета с наложенным переопределением кеша из записи. */
|
|
159
|
+
resolveCapabilities(entry) {
|
|
160
|
+
const spec = this.registry.get(entry.type);
|
|
161
|
+
if (!spec)
|
|
162
|
+
return { cache: resolveCacheCapability(undefined, entry.cache) };
|
|
163
|
+
return { ...spec.capabilities, cache: resolveCacheCapability(spec, entry.cache) };
|
|
164
|
+
}
|
|
150
165
|
/** Clear the build cache (on config change). */
|
|
151
166
|
resetCache() {
|
|
152
167
|
this.cache.clear();
|
|
@@ -16,6 +16,9 @@ function openaiCompat(opts) {
|
|
|
16
16
|
retry,
|
|
17
17
|
rateLimits,
|
|
18
18
|
maxCompletionTokens: opts.maxCompletionTokens,
|
|
19
|
+
getSessionId: opts.getSessionId,
|
|
20
|
+
cache: opts.capabilities?.cache,
|
|
21
|
+
logger: opts.logger,
|
|
19
22
|
});
|
|
20
23
|
}
|
|
21
24
|
const OPENAI_COMPAT = {
|
|
@@ -26,10 +29,22 @@ const OPENAI_COMPAT = {
|
|
|
26
29
|
streaming: true,
|
|
27
30
|
tools: true,
|
|
28
31
|
vision: false,
|
|
29
|
-
|
|
32
|
+
reasoningStrategy: "prompt-tag",
|
|
30
33
|
listModels: true,
|
|
31
34
|
requiresKey: false,
|
|
32
35
|
auth: "bearer",
|
|
36
|
+
// Локальные серверы (LM Studio, llama.cpp, vLLM) кешируют KV-префикс на
|
|
37
|
+
// сервере и отдают его в usage-полях OpenAI-формы.
|
|
38
|
+
cache: {
|
|
39
|
+
mechanism: "local",
|
|
40
|
+
report: "openai",
|
|
41
|
+
sessionHeader: false,
|
|
42
|
+
requestCachePrompt: false,
|
|
43
|
+
requestPromptCacheKey: false,
|
|
44
|
+
requestSessionId: false,
|
|
45
|
+
requestCacheControl: false,
|
|
46
|
+
requestStreamUsage: true,
|
|
47
|
+
},
|
|
33
48
|
},
|
|
34
49
|
create: openaiCompat,
|
|
35
50
|
};
|
|
@@ -41,10 +56,22 @@ const OPENROUTER = {
|
|
|
41
56
|
streaming: true,
|
|
42
57
|
tools: true,
|
|
43
58
|
vision: true,
|
|
44
|
-
|
|
59
|
+
reasoningStrategy: "openai-effort",
|
|
45
60
|
listModels: true,
|
|
46
61
|
requiresKey: true,
|
|
47
62
|
auth: "bearer",
|
|
63
|
+
// OpenRouter проксирует кеши провайдеров и сообщает cached/cache_write
|
|
64
|
+
// токены. `session_id` прибивает последующие запросы к провайдеру с тёплым кешем.
|
|
65
|
+
cache: {
|
|
66
|
+
mechanism: "auto",
|
|
67
|
+
report: "openai",
|
|
68
|
+
sessionHeader: false,
|
|
69
|
+
requestCachePrompt: false,
|
|
70
|
+
requestPromptCacheKey: false,
|
|
71
|
+
requestSessionId: true,
|
|
72
|
+
requestCacheControl: false,
|
|
73
|
+
requestStreamUsage: true,
|
|
74
|
+
},
|
|
48
75
|
},
|
|
49
76
|
create: openaiCompat,
|
|
50
77
|
};
|
|
@@ -56,10 +83,22 @@ const OPENAI = {
|
|
|
56
83
|
streaming: true,
|
|
57
84
|
tools: true,
|
|
58
85
|
vision: true,
|
|
59
|
-
|
|
86
|
+
reasoningStrategy: "openai-effort",
|
|
60
87
|
listModels: true,
|
|
61
88
|
requiresKey: true,
|
|
62
89
|
auth: "bearer",
|
|
90
|
+
// Автоматический prefix caching. `prompt_cache_key` держит запросы с общим
|
|
91
|
+
// префиксом на одном бэкенде ради более высокого hit-rate.
|
|
92
|
+
cache: {
|
|
93
|
+
mechanism: "auto",
|
|
94
|
+
report: "openai",
|
|
95
|
+
sessionHeader: false,
|
|
96
|
+
requestCachePrompt: false,
|
|
97
|
+
requestPromptCacheKey: true,
|
|
98
|
+
requestSessionId: false,
|
|
99
|
+
requestCacheControl: false,
|
|
100
|
+
requestStreamUsage: true,
|
|
101
|
+
},
|
|
63
102
|
},
|
|
64
103
|
create: openaiCompat,
|
|
65
104
|
};
|
|
@@ -71,10 +110,22 @@ const ANTHROPIC = {
|
|
|
71
110
|
streaming: false,
|
|
72
111
|
tools: false,
|
|
73
112
|
vision: true,
|
|
74
|
-
|
|
113
|
+
reasoningStrategy: "none",
|
|
75
114
|
listModels: false,
|
|
76
115
|
requiresKey: true,
|
|
77
116
|
auth: "header",
|
|
117
|
+
// Явные cache-брейкпоинты; отчёт идёт через собственные usage-поля Anthropic.
|
|
118
|
+
// Неактивно, пока не появится настоящий AnthropicProvider на /v1/messages.
|
|
119
|
+
cache: {
|
|
120
|
+
mechanism: "explicit",
|
|
121
|
+
report: "anthropic",
|
|
122
|
+
sessionHeader: false,
|
|
123
|
+
requestCachePrompt: false,
|
|
124
|
+
requestPromptCacheKey: false,
|
|
125
|
+
requestSessionId: false,
|
|
126
|
+
requestCacheControl: true,
|
|
127
|
+
requestStreamUsage: false,
|
|
128
|
+
},
|
|
78
129
|
},
|
|
79
130
|
// The Anthropic messages API is NOT OpenAI-compatible. This preset is a
|
|
80
131
|
// placeholder until a real AnthropicProvider lands; the factory still uses
|
|
@@ -89,10 +140,22 @@ const OPENCODE_ZEN = {
|
|
|
89
140
|
streaming: true,
|
|
90
141
|
tools: true,
|
|
91
142
|
vision: false,
|
|
92
|
-
|
|
143
|
+
reasoningStrategy: "openai-effort",
|
|
93
144
|
listModels: true,
|
|
94
145
|
requiresKey: false,
|
|
95
146
|
auth: "bearer",
|
|
147
|
+
// Серверный кеш с ценой Cached Read; для sticky-маршрутизации нужен
|
|
148
|
+
// заголовок сессии (`x-opencode-session`).
|
|
149
|
+
cache: {
|
|
150
|
+
mechanism: "auto",
|
|
151
|
+
report: "openai",
|
|
152
|
+
sessionHeader: true,
|
|
153
|
+
requestCachePrompt: false,
|
|
154
|
+
requestPromptCacheKey: false,
|
|
155
|
+
requestSessionId: false,
|
|
156
|
+
requestCacheControl: false,
|
|
157
|
+
requestStreamUsage: true,
|
|
158
|
+
},
|
|
96
159
|
},
|
|
97
160
|
create: openaiCompat,
|
|
98
161
|
};
|
|
@@ -104,10 +167,20 @@ const OPENCODE_GO = {
|
|
|
104
167
|
streaming: true,
|
|
105
168
|
tools: true,
|
|
106
169
|
vision: false,
|
|
107
|
-
|
|
170
|
+
reasoningStrategy: "openai-effort",
|
|
108
171
|
listModels: true,
|
|
109
172
|
requiresKey: false,
|
|
110
173
|
auth: "bearer",
|
|
174
|
+
cache: {
|
|
175
|
+
mechanism: "auto",
|
|
176
|
+
report: "openai",
|
|
177
|
+
sessionHeader: true,
|
|
178
|
+
requestCachePrompt: false,
|
|
179
|
+
requestPromptCacheKey: false,
|
|
180
|
+
requestSessionId: false,
|
|
181
|
+
requestCacheControl: false,
|
|
182
|
+
requestStreamUsage: true,
|
|
183
|
+
},
|
|
111
184
|
},
|
|
112
185
|
create: openaiCompat,
|
|
113
186
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function createPolicyState() {
|
|
2
|
+
return { iterationsSinceRaise: 0, boostRemaining: 0 };
|
|
3
|
+
}
|
|
4
|
+
const DECAY_THRESHOLD = 5;
|
|
5
|
+
export function evaluateReasoningPolicy(input, state) {
|
|
6
|
+
// Raise signals (highest priority)
|
|
7
|
+
if (input.hallucinationRetry || input.auditRejection || input.recoverableLlmError) {
|
|
8
|
+
state.iterationsSinceRaise = 0;
|
|
9
|
+
return { level: "high", reason: input.hallucinationRetry ? "policy:hallucination-retry" : input.auditRejection ? "policy:audit-rejection" : "policy:recoverable-llm-error" };
|
|
10
|
+
}
|
|
11
|
+
// Plan created → boost for N iterations
|
|
12
|
+
if (input.planCreated) {
|
|
13
|
+
state.boostRemaining = 3;
|
|
14
|
+
state.iterationsSinceRaise = 0;
|
|
15
|
+
return { level: "high", reason: "policy:plan-created" };
|
|
16
|
+
}
|
|
17
|
+
// Active boost from plan creation
|
|
18
|
+
if (state.boostRemaining > 0) {
|
|
19
|
+
state.boostRemaining--;
|
|
20
|
+
state.iterationsSinceRaise = 0;
|
|
21
|
+
return { level: "high", reason: "policy:plan-boost" };
|
|
22
|
+
}
|
|
23
|
+
// Lower signals
|
|
24
|
+
if (input.consecutiveSuccesses >= 2) {
|
|
25
|
+
state.iterationsSinceRaise++;
|
|
26
|
+
return { level: "low", reason: "policy:routine-write" };
|
|
27
|
+
}
|
|
28
|
+
if (input.isRepetitive) {
|
|
29
|
+
state.iterationsSinceRaise++;
|
|
30
|
+
return { level: "low", reason: "policy:repetitive-tools" };
|
|
31
|
+
}
|
|
32
|
+
// Decay: if raised for too long, return to baseline
|
|
33
|
+
if (state.iterationsSinceRaise >= DECAY_THRESHOLD) {
|
|
34
|
+
state.iterationsSinceRaise = 0;
|
|
35
|
+
return { level: input.baseline, reason: "policy:decay" };
|
|
36
|
+
}
|
|
37
|
+
// Default: baseline
|
|
38
|
+
state.iterationsSinceRaise++;
|
|
39
|
+
return { level: input.baseline, reason: "policy:default" };
|
|
40
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const PROBE_MESSAGES = [
|
|
5
|
+
{ role: "user", content: "Reply with exactly: ok" },
|
|
6
|
+
];
|
|
7
|
+
/** TTL: re-probe after 24 hours. */
|
|
8
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
/** Disk cache path: ~/.mma/reasoning-cache.json (override with MMA_REASONING_CACHE). */
|
|
10
|
+
function cachePath() {
|
|
11
|
+
return process.env.MMA_REASONING_CACHE ?? join(homedir(), ".mma", "reasoning-cache.json");
|
|
12
|
+
}
|
|
13
|
+
/** In-memory L1 cache (same-process fast path). */
|
|
14
|
+
const memCache = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Probe whether the backend actually respects the reasoning effort mechanism.
|
|
17
|
+
* Sends a trivial non-streaming request with level forced to "none" via the
|
|
18
|
+
* mapped strategy. If reasoning_content still appears, the mechanism is ignored.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Probe whether the backend actually respects the reasoning effort mechanism.
|
|
22
|
+
* @returns `true` = mechanism respected, `false` = mechanism ignored,
|
|
23
|
+
* `null` = transient error (caller must NOT cache this).
|
|
24
|
+
*/
|
|
25
|
+
export async function probeReasoningSupport(provider, strategy, signal) {
|
|
26
|
+
if (strategy === "none")
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
let hasReasoning = false;
|
|
30
|
+
for await (const chunk of provider.chat(PROBE_MESSAGES, undefined, signal, {
|
|
31
|
+
maxTokens: 32,
|
|
32
|
+
reasoningEffort: "none",
|
|
33
|
+
reasoningStrategy: strategy,
|
|
34
|
+
})) {
|
|
35
|
+
if (chunk.type === "reasoning")
|
|
36
|
+
hasReasoning = true;
|
|
37
|
+
}
|
|
38
|
+
// If reasoning content appeared despite "none", the mechanism is ignored
|
|
39
|
+
return !hasReasoning;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Transient error (network/auth/stream) — do NOT report "not supported".
|
|
43
|
+
// Returning null signals the caller to skip caching, so a one-off failure
|
|
44
|
+
// does not disable the reasoning mechanism for the 24h TTL.
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function cacheKey(baseUrl, model) {
|
|
49
|
+
return `${baseUrl}|${model}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read cached probe result. Checks in-memory first, then disk.
|
|
53
|
+
* Returns `undefined` if cache miss or stale (>24h).
|
|
54
|
+
*/
|
|
55
|
+
export function getCachedProbeResult(key) {
|
|
56
|
+
// L1: in-memory
|
|
57
|
+
const mem = memCache.get(key);
|
|
58
|
+
if (mem && Date.now() - mem.ts < CACHE_TTL_MS) {
|
|
59
|
+
return mem.result;
|
|
60
|
+
}
|
|
61
|
+
// L2: disk
|
|
62
|
+
const disk = readDiskCache();
|
|
63
|
+
const entry = disk[key];
|
|
64
|
+
if (entry) {
|
|
65
|
+
const age = Date.now() - new Date(entry.ts).getTime();
|
|
66
|
+
if (age < CACHE_TTL_MS) {
|
|
67
|
+
memCache.set(key, { result: entry.result, ts: Date.now() });
|
|
68
|
+
return entry.result;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Store probe result in both in-memory and disk cache.
|
|
75
|
+
*/
|
|
76
|
+
export function setCachedProbeResult(key, result) {
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
memCache.set(key, { result, ts: Date.now() });
|
|
79
|
+
const disk = readDiskCache();
|
|
80
|
+
disk[key] = { result, ts: now };
|
|
81
|
+
writeDiskCache(disk);
|
|
82
|
+
}
|
|
83
|
+
export function resetProbeCache() {
|
|
84
|
+
memCache.clear();
|
|
85
|
+
const path = cachePath();
|
|
86
|
+
if (existsSync(path)) {
|
|
87
|
+
writeFileSync(path, "{}", "utf-8");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function readDiskCache() {
|
|
91
|
+
try {
|
|
92
|
+
const path = cachePath();
|
|
93
|
+
if (existsSync(path)) {
|
|
94
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Corrupted cache — start fresh
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
function writeDiskCache(data) {
|
|
103
|
+
try {
|
|
104
|
+
const path = cachePath();
|
|
105
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
106
|
+
writeFileSync(path, JSON.stringify(data, null, 2), "utf-8");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Best-effort — disk write failure must not crash the agent
|
|
110
|
+
}
|
|
111
|
+
}
|