micro-models-agent 0.63.3 → 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.
Files changed (185) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +7 -4
  41. package/dist/i18n/ru.json +7 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1606 -800
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/driver.js +46 -4
  59. package/dist/modules/certification/cli.js +85 -42
  60. package/dist/modules/certification/loader.js +15 -1
  61. package/dist/modules/certification/manifest.js +126 -15
  62. package/dist/modules/certification/runner.js +4 -26
  63. package/dist/modules/certification/scenarios.js +184 -5
  64. package/dist/modules/certification/syntax-scenarios.js +51 -0
  65. package/dist/modules/context/chunk-query.js +25 -5
  66. package/dist/modules/context/fact-extractor.js +6 -2
  67. package/dist/modules/context/manager.js +23 -7
  68. package/dist/modules/execution/audit-runners.js +7 -1
  69. package/dist/modules/execution/auditor.js +3 -3
  70. package/dist/modules/execution/execution-plugin.js +22 -15
  71. package/dist/modules/execution/input-from.js +46 -0
  72. package/dist/modules/execution/module.js +107 -18
  73. package/dist/modules/execution/moe-executor.js +166 -54
  74. package/dist/modules/execution/plan-actions.js +524 -0
  75. package/dist/modules/execution/plan-steps.js +23 -0
  76. package/dist/modules/execution/plan-store.js +15 -3
  77. package/dist/modules/execution/plan-tool.js +6 -488
  78. package/dist/modules/execution/plan-validator.js +24 -0
  79. package/dist/modules/execution/stuck-detector.js +3 -18
  80. package/dist/modules/execution/tracker.js +14 -5
  81. package/dist/modules/execution/transient-error.js +30 -0
  82. package/dist/modules/execution/verifier.js +94 -7
  83. package/dist/modules/execution/windows-commands.js +11 -0
  84. package/dist/modules/hallucination/confidence.js +36 -23
  85. package/dist/modules/hallucination/consistency.js +3 -0
  86. package/dist/modules/hallucination/detector.js +8 -3
  87. package/dist/modules/hallucination/factual.js +26 -7
  88. package/dist/modules/hallucination/llm-judge.js +12 -2
  89. package/dist/modules/indexer/map-command.js +35 -0
  90. package/dist/modules/indexer/map-select.js +87 -0
  91. package/dist/modules/indexer/module.js +34 -22
  92. package/dist/modules/indexer/symbols.js +189 -0
  93. package/dist/modules/indexer/walker.js +96 -42
  94. package/dist/modules/lsp/check-tool.js +2 -1
  95. package/dist/modules/lsp/client.js +49 -32
  96. package/dist/modules/lsp/config.js +55 -2
  97. package/dist/modules/lsp/module.js +38 -5
  98. package/dist/modules/lsp/probe.js +4 -3
  99. package/dist/modules/lsp/project-root.js +41 -1
  100. package/dist/modules/lsp/startup-check.js +12 -4
  101. package/dist/modules/mcp/client.js +153 -104
  102. package/dist/modules/mcp/module.js +165 -41
  103. package/dist/modules/memory/module.js +4 -3
  104. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  105. package/dist/modules/plugins/manager.js +47 -84
  106. package/dist/modules/pricing/index.js +17 -7
  107. package/dist/modules/pricing/prices.js +30 -12
  108. package/dist/modules/processes/index.js +1 -0
  109. package/dist/modules/processes/kill-tree.js +56 -0
  110. package/dist/modules/processes/registry.js +2 -54
  111. package/dist/modules/providers/cache.js +23 -0
  112. package/dist/modules/providers/factory.js +28 -0
  113. package/dist/modules/providers/fallback.js +7 -5
  114. package/dist/modules/providers/health.js +2 -1
  115. package/dist/modules/providers/index.js +1 -0
  116. package/dist/modules/providers/manager.js +17 -2
  117. package/dist/modules/providers/presets.js +79 -6
  118. package/dist/modules/reasoning/policy.js +40 -0
  119. package/dist/modules/reasoning/probe.js +111 -0
  120. package/dist/modules/security/audit-notifier.js +42 -27
  121. package/dist/modules/security/command-validator.js +25 -20
  122. package/dist/modules/security/encryption.js +6 -12
  123. package/dist/modules/security/network-validator.js +76 -5
  124. package/dist/modules/security/path-validator.js +77 -34
  125. package/dist/modules/security/rate-limiter.js +11 -0
  126. package/dist/modules/security/security-policies.js +1 -1
  127. package/dist/modules/security/session-encryption.js +13 -2
  128. package/dist/modules/security/session-isolation.js +2 -9
  129. package/dist/modules/session/manager.js +11 -0
  130. package/dist/modules/session/module.js +11 -3
  131. package/dist/modules/session/store.js +41 -5
  132. package/dist/modules/skills/loader.js +7 -1
  133. package/dist/modules/skills/module.js +2 -1
  134. package/dist/modules/updater/changelog-reader.js +94 -0
  135. package/dist/modules/updater/dev-detect.js +17 -0
  136. package/dist/modules/updater/index.js +1 -0
  137. package/dist/modules/updater/module.js +14 -3
  138. package/dist/output/bus.js +32 -0
  139. package/dist/output/channel.js +233 -0
  140. package/dist/output/format.js +14 -0
  141. package/dist/output/index.js +7 -0
  142. package/dist/output/json-sink.js +22 -0
  143. package/dist/output/machine.js +8 -0
  144. package/dist/output/session-sink.js +27 -0
  145. package/dist/output/types.js +1 -0
  146. package/dist/tools/approve.js +6 -2
  147. package/dist/tools/attach-image.js +11 -11
  148. package/dist/tools/auto-fixer.js +198 -0
  149. package/dist/tools/bash.js +142 -89
  150. package/dist/tools/chunk-query.js +10 -6
  151. package/dist/tools/download-file.js +1 -1
  152. package/dist/tools/edit-file.js +20 -2
  153. package/dist/tools/executor.js +54 -9
  154. package/dist/tools/glob-tool.js +7 -0
  155. package/dist/tools/grep-tool.js +15 -1
  156. package/dist/tools/index.js +3 -1
  157. package/dist/tools/list-dir.js +3 -1
  158. package/dist/tools/load-skill.js +2 -1
  159. package/dist/tools/mcp-call.js +1 -1
  160. package/dist/tools/move-file.js +5 -4
  161. package/dist/tools/path-utils.js +7 -0
  162. package/dist/tools/pipeline-run.js +1 -1
  163. package/dist/tools/prompt-io.js +28 -0
  164. package/dist/tools/question.js +12 -12
  165. package/dist/tools/scope-request.js +91 -0
  166. package/dist/tools/session-info.js +44 -0
  167. package/dist/tools/set-thinking.js +71 -0
  168. package/dist/tools/subagent.js +50 -9
  169. package/dist/tools/syntax-validator.js +177 -0
  170. package/dist/tools/user-input.js +16 -9
  171. package/dist/tools/write-file.js +17 -1
  172. package/dist/ui/diff.js +10 -0
  173. package/dist/ui/line-editor.js +179 -26
  174. package/dist/ui/line-math.js +20 -3
  175. package/dist/ui/md-formatter.js +100 -10
  176. package/dist/ui/output.js +5 -4
  177. package/dist/ui/plan-view.js +2 -7
  178. package/dist/ui/renderer.js +89 -85
  179. package/dist/ui/spinner.js +14 -4
  180. package/dist/utils/error.js +4 -0
  181. package/dist/utils/index.js +4 -0
  182. package/dist/utils/retry.js +17 -0
  183. package/dist/utils/sleep.js +23 -0
  184. package/dist/utils/truncate.js +9 -0
  185. 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
- * Compute the cost in USD of a single call given its token usage and the
112
- * model price. Returns undefined when the model has no known price.
111
+ * Считает стоимость и экономию вызова. `promptTokens` суммарные prompt-токены
112
+ * (cached + uncached); кешированная часть тарифицируется по `cachedInput`, а
113
+ * запись в кеш — по `cacheWrite`. Возвращает undefined без известной цены.
113
114
  */
114
- export function calculateCost(model, promptTokens, completionTokens, config) {
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
- return (promptTokens / 1_000_000) * price.input + (completionTokens / 1_000_000) * price.output;
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) {
@@ -1,2 +1,3 @@
1
1
  export { registerKillable, unregisterKillable, killByCallId } from "./runner";
2
2
  export { processRegistry } from "./registry";
3
+ export { killTree } from "./kill-tree";
@@ -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
- /** Synchronous pause without blocking the event loop (via Atomics.wait). */
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
- const e = err;
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 statuses only.
7
- if (typeof e?.llmStatus === "number")
8
- return e.llmStatus === 429 || e.llmStatus >= 500;
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 = err instanceof Error ? err.message : String(err);
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
- fallback: false,
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
- reasoningEffort: false,
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
- reasoningEffort: false,
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
- reasoningEffort: true,
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
- reasoningEffort: false,
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
- reasoningEffort: false,
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
- reasoningEffort: false,
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
+ }