micro-models-agent 2.11.0 → 2.13.1

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 (356) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/app.js +22 -2
  3. package/dist/assets/index.html +39 -9
  4. package/dist/assets/js/dom.js +15 -0
  5. package/dist/assets/js/feed.js +129 -10
  6. package/dist/assets/js/sidebar.js +512 -0
  7. package/dist/assets/js/tabs.js +30 -5
  8. package/dist/assets/setup.js +267 -8
  9. package/dist/assets/styles.css +168 -3
  10. package/dist/cli/cache-line.js +1 -0
  11. package/dist/cli/command-suggest.js +5 -4
  12. package/dist/cli/commands.js +231 -122
  13. package/dist/cli/completer.js +132 -31
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/json-payload.js +4 -4
  16. package/dist/cli/main.js +166 -61
  17. package/dist/cli/onboarding-commands.js +126 -0
  18. package/dist/cli/plugin-commands.js +2 -1
  19. package/dist/cli/relaunch.js +16 -13
  20. package/dist/cli/repl-commands-lsp.js +75 -0
  21. package/dist/cli/repl-commands-skills.js +105 -0
  22. package/dist/cli/repl-commands.js +589 -266
  23. package/dist/cli/repl.js +436 -177
  24. package/dist/cli/run-result.js +5 -6
  25. package/dist/cli/security-commands.js +37 -28
  26. package/dist/cli/setup-order.js +4 -55
  27. package/dist/cli/setup-prompt.js +3 -2
  28. package/dist/cli/setup.js +27 -107
  29. package/dist/cli/web-command.js +98 -0
  30. package/dist/config/budget.js +5 -5
  31. package/dist/config/config.js +103 -96
  32. package/dist/config/defaults.js +38 -8
  33. package/dist/config/domains.js +24 -27
  34. package/dist/config/env.js +46 -0
  35. package/dist/config/experts.js +2 -0
  36. package/dist/config/index.js +1 -0
  37. package/dist/config/paths.js +56 -0
  38. package/dist/config/redact.js +44 -0
  39. package/dist/config/registry-extra.js +334 -0
  40. package/dist/config/registry.js +584 -0
  41. package/dist/config/security.js +35 -36
  42. package/dist/config/timeouts.js +74 -0
  43. package/dist/config/utils.js +16 -15
  44. package/dist/core/agent/artifact-progress.js +59 -0
  45. package/dist/core/agent/artifact-resolve.js +79 -0
  46. package/dist/core/agent/audit-gate.js +7 -3
  47. package/dist/core/agent/compaction.js +2 -2
  48. package/dist/core/agent/constants.js +33 -4
  49. package/dist/core/agent/context-log.js +454 -0
  50. package/dist/core/agent/context-renderer.js +5 -0
  51. package/dist/core/agent/critic-gate.js +132 -0
  52. package/dist/core/agent/hallucination-gate.js +148 -38
  53. package/dist/core/agent/loop-state.js +15 -0
  54. package/dist/core/agent/prefix-monitor.js +1 -1
  55. package/dist/core/agent/reasoning-resolver.js +10 -1
  56. package/dist/core/agent/token-tracker.js +7 -4
  57. package/dist/core/agent/tool-batch.js +93 -18
  58. package/dist/core/agent/tool-images.js +18 -0
  59. package/dist/core/agent/tool-output.js +65 -11
  60. package/dist/core/agent/tool-pairing.js +60 -0
  61. package/dist/core/agent-moe.js +25 -24
  62. package/dist/core/agent.js +429 -108
  63. package/dist/core/bootstrap/agent-phase.js +66 -0
  64. package/dist/core/bootstrap/config-phase.js +86 -0
  65. package/dist/core/bootstrap/modules-phase.js +269 -0
  66. package/dist/core/bootstrap/provider-phase.js +183 -0
  67. package/dist/core/bootstrap/session-phase.js +204 -0
  68. package/dist/core/bootstrap/state.js +94 -0
  69. package/dist/core/bootstrap/workspace-phase.js +115 -0
  70. package/dist/core/bootstrap.js +92 -660
  71. package/dist/core/crash-handler.js +17 -7
  72. package/dist/core/environment.js +37 -12
  73. package/dist/core/host-plan.js +43 -0
  74. package/dist/core/host.js +23 -0
  75. package/dist/core/index.js +1 -0
  76. package/dist/core/prompt-builder.js +17 -1
  77. package/dist/core/prompt-overflow.js +42 -43
  78. package/dist/core/session-logger.js +122 -30
  79. package/dist/core/system-prompt.js +117 -0
  80. package/dist/core/version.js +6 -4
  81. package/dist/core/web-host.js +34 -0
  82. package/dist/core/workspace.js +19 -15
  83. package/dist/i18n/en.json +89 -3
  84. package/dist/i18n/index.js +37 -3
  85. package/dist/i18n/ru.json +89 -3
  86. package/dist/index.js +7 -3
  87. package/dist/llm/image-utils.js +55 -64
  88. package/dist/llm/llm-errors.js +9 -0
  89. package/dist/llm/model-loader.js +10 -32
  90. package/dist/llm/models-dev.js +121 -0
  91. package/dist/llm/openai-compat.js +146 -73
  92. package/dist/llm/orchestrator.js +33 -11
  93. package/dist/llm/provider-budget.js +2 -2
  94. package/dist/llm/provider-context.js +233 -0
  95. package/dist/llm/provider.js +1 -1
  96. package/dist/llm/response.js +5 -0
  97. package/dist/llm/token-counter.js +27 -11
  98. package/dist/logger/app-logger.js +48 -22
  99. package/dist/logger/file-log.js +28 -19
  100. package/dist/logger/index.js +1 -0
  101. package/dist/main.js +888 -139
  102. package/dist/migration/backup.js +14 -3
  103. package/dist/migration/detect.js +12 -3
  104. package/dist/migration/index.js +1 -0
  105. package/dist/modules/artifacts/store.js +9 -8
  106. package/dist/modules/browser/actions.js +6 -0
  107. package/dist/modules/browser/bridge-client.js +14 -12
  108. package/dist/modules/browser/bridge-path.js +4 -4
  109. package/dist/modules/browser/console-buffer.js +37 -0
  110. package/dist/modules/browser/cookie-store.js +5 -0
  111. package/dist/modules/browser/driver-types.js +1 -0
  112. package/dist/modules/browser/driver.js +19 -11
  113. package/dist/modules/browser/index.js +3 -1
  114. package/dist/modules/browser/module.js +2 -1
  115. package/dist/modules/browser/session.js +10 -30
  116. package/dist/modules/browser/snapshot.js +19 -13
  117. package/dist/modules/browser/types.js +1 -0
  118. package/dist/modules/certification/cli.js +28 -20
  119. package/dist/modules/certification/fact-checker.js +9 -1
  120. package/dist/modules/certification/loader.js +9 -2
  121. package/dist/modules/certification/manifest.js +83 -21
  122. package/dist/modules/certification/runner.js +23 -13
  123. package/dist/modules/certification/scenarios.js +20 -20
  124. package/dist/modules/certification/syntax-scenarios.js +1 -2
  125. package/dist/modules/context/chunk-query.js +11 -9
  126. package/dist/modules/context/fact-extractor.js +37 -39
  127. package/dist/modules/context/index.js +1 -0
  128. package/dist/modules/context/manager.js +106 -80
  129. package/dist/modules/critic/client.js +176 -0
  130. package/dist/modules/critic/evidence.js +81 -0
  131. package/dist/modules/critic/provider.js +45 -0
  132. package/dist/modules/critic/tools.js +78 -0
  133. package/dist/modules/critic/types.js +1 -0
  134. package/dist/modules/execution/audit-runners.js +28 -34
  135. package/dist/modules/execution/auditor.js +53 -49
  136. package/dist/modules/execution/execution-plugin.js +142 -102
  137. package/dist/modules/execution/index.js +1 -1
  138. package/dist/modules/execution/input-from.js +37 -13
  139. package/dist/modules/execution/module.js +368 -191
  140. package/dist/modules/execution/moe-executor.js +53 -26
  141. package/dist/modules/execution/plan-actions.js +320 -202
  142. package/dist/modules/execution/plan-checks.js +35 -0
  143. package/dist/modules/execution/plan-coverage.js +36 -33
  144. package/dist/modules/execution/plan-steps.js +11 -3
  145. package/dist/modules/execution/plan-store.js +83 -17
  146. package/dist/modules/execution/plan-tool.js +89 -35
  147. package/dist/modules/execution/plan-validator.js +30 -10
  148. package/dist/modules/execution/planner.js +22 -13
  149. package/dist/modules/execution/stuck-detector.js +196 -93
  150. package/dist/modules/execution/tracker.js +22 -8
  151. package/dist/modules/execution/verifier.js +69 -48
  152. package/dist/modules/execution/windows-commands.js +12 -12
  153. package/dist/modules/hallucination/artifact-claim.js +93 -0
  154. package/dist/modules/hallucination/confidence.js +13 -11
  155. package/dist/modules/hallucination/consistency.js +10 -2
  156. package/dist/modules/hallucination/detector.js +10 -5
  157. package/dist/modules/hallucination/factual.js +43 -26
  158. package/dist/modules/hallucination/index.js +1 -0
  159. package/dist/modules/hallucination/js-identifiers.js +44 -45
  160. package/dist/modules/hallucination/llm-judge.js +17 -12
  161. package/dist/modules/hallucination/success-claim.js +59 -0
  162. package/dist/modules/indexer/cache.js +52 -4
  163. package/dist/modules/indexer/index.js +1 -0
  164. package/dist/modules/indexer/map-command.js +2 -4
  165. package/dist/modules/indexer/map-select.js +16 -16
  166. package/dist/modules/indexer/module.js +47 -12
  167. package/dist/modules/indexer/project-profile.js +8 -7
  168. package/dist/modules/indexer/symbols.js +15 -15
  169. package/dist/modules/indexer/walker.js +159 -29
  170. package/dist/modules/lsp/check-tool.js +7 -5
  171. package/dist/modules/lsp/client.js +39 -39
  172. package/dist/modules/lsp/command.js +17 -24
  173. package/dist/modules/lsp/config.js +12 -11
  174. package/dist/modules/lsp/index.js +1 -0
  175. package/dist/modules/lsp/module.js +48 -46
  176. package/dist/modules/lsp/probe.js +13 -14
  177. package/dist/modules/lsp/project-root.js +14 -17
  178. package/dist/modules/lsp/startup-check.js +18 -16
  179. package/dist/modules/mcp/client.js +59 -62
  180. package/dist/modules/mcp/index.js +1 -0
  181. package/dist/modules/mcp/module.js +66 -37
  182. package/dist/modules/mcp/registry.js +5 -0
  183. package/dist/modules/memory/index.js +1 -0
  184. package/dist/modules/memory/module.js +12 -8
  185. package/dist/modules/memory/search.js +4 -1
  186. package/dist/modules/memory/store.js +40 -2
  187. package/dist/modules/onboarding/init.js +47 -0
  188. package/dist/modules/onboarding/meet.js +30 -0
  189. package/dist/modules/onboarding/mode.js +30 -0
  190. package/dist/modules/onboarding/prompts.js +43 -0
  191. package/dist/modules/pipelines/engine.js +10 -0
  192. package/dist/modules/pipelines/index.js +1 -0
  193. package/dist/modules/pipelines/parser.js +4 -1
  194. package/dist/modules/pipelines/template.js +4 -0
  195. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  196. package/dist/modules/plugins/builtin/notify.js +2 -0
  197. package/dist/modules/plugins/index.js +1 -0
  198. package/dist/modules/plugins/loader.js +18 -3
  199. package/dist/modules/plugins/manager.js +37 -17
  200. package/dist/modules/pricing/index.js +10 -11
  201. package/dist/modules/pricing/prices.js +12 -12
  202. package/dist/modules/processes/index.js +1 -0
  203. package/dist/modules/processes/kill-tree.js +27 -31
  204. package/dist/modules/processes/registry.js +44 -25
  205. package/dist/modules/processes/runner.js +4 -4
  206. package/dist/modules/providers/capabilities.js +28 -0
  207. package/dist/modules/providers/create.js +3 -4
  208. package/dist/modules/providers/factory.js +7 -7
  209. package/dist/modules/providers/fallback.js +12 -12
  210. package/dist/modules/providers/health.js +4 -4
  211. package/dist/modules/providers/manager.js +50 -19
  212. package/dist/modules/providers/naming.js +15 -0
  213. package/dist/modules/providers/preflight.js +71 -0
  214. package/dist/modules/providers/presets.js +39 -8
  215. package/dist/modules/providers/registry.js +7 -2
  216. package/dist/modules/reasoning/policy.js +8 -6
  217. package/dist/modules/reasoning/probe.js +30 -25
  218. package/dist/modules/security/audit-log.js +19 -36
  219. package/dist/modules/security/audit-notifier.js +35 -75
  220. package/dist/modules/security/command-validator.js +99 -51
  221. package/dist/modules/security/content-scanner.js +8 -8
  222. package/dist/modules/security/data-sanitizer.js +9 -16
  223. package/dist/modules/security/encryption.js +32 -52
  224. package/dist/modules/security/index.js +2 -1
  225. package/dist/modules/security/network-validator.js +120 -43
  226. package/dist/modules/security/path-validator.js +61 -46
  227. package/dist/modules/security/protected-paths.js +73 -0
  228. package/dist/modules/security/rate-limiter.js +17 -49
  229. package/dist/modules/security/security-policies.js +25 -57
  230. package/dist/modules/security/session-encryption.js +32 -71
  231. package/dist/modules/security/session-isolation.js +15 -28
  232. package/dist/modules/session/auto-name.js +63 -0
  233. package/dist/modules/session/export.js +98 -0
  234. package/dist/modules/session/index.js +1 -0
  235. package/dist/modules/session/manager.js +79 -28
  236. package/dist/modules/session/module.js +4 -3
  237. package/dist/modules/session/store.js +57 -20
  238. package/dist/modules/setup/answers.js +35 -0
  239. package/dist/modules/setup/apply.js +79 -0
  240. package/dist/modules/setup/discovery.js +72 -0
  241. package/dist/modules/skills/index.js +1 -0
  242. package/dist/modules/skills/loader.js +9 -1
  243. package/dist/modules/skills/module.js +11 -0
  244. package/dist/modules/updater/changelog-reader.js +15 -16
  245. package/dist/modules/updater/checker.js +9 -4
  246. package/dist/modules/updater/dev-detect.js +7 -6
  247. package/dist/modules/updater/index.js +1 -0
  248. package/dist/modules/updater/module.js +17 -15
  249. package/dist/modules/user-profile/compressor.js +2 -0
  250. package/dist/modules/user-profile/index.js +1 -0
  251. package/dist/modules/user-profile/profile.js +10 -1
  252. package/dist/modules/webui/answer-api.js +29 -0
  253. package/dist/modules/webui/assets.js +50 -0
  254. package/dist/modules/webui/body.js +21 -0
  255. package/dist/modules/webui/commands-settings.js +209 -0
  256. package/dist/modules/webui/commands-shared.js +60 -0
  257. package/dist/modules/webui/commands.js +174 -0
  258. package/dist/modules/webui/config-api.js +134 -0
  259. package/dist/modules/webui/file-api.js +202 -0
  260. package/dist/modules/webui/forward.js +159 -0
  261. package/dist/modules/webui/history-api.js +112 -0
  262. package/dist/modules/webui/host-state.js +60 -0
  263. package/dist/modules/webui/i18n-api.js +9 -0
  264. package/dist/modules/webui/limits.js +6 -0
  265. package/dist/modules/webui/log-forward.js +20 -0
  266. package/dist/modules/webui/memory-commands.js +64 -0
  267. package/dist/modules/webui/model-cache.js +18 -0
  268. package/dist/modules/webui/module.js +148 -0
  269. package/dist/modules/webui/persist.js +24 -0
  270. package/dist/modules/webui/prompt-io.js +97 -0
  271. package/dist/modules/webui/providers-api.js +337 -0
  272. package/dist/modules/webui/providers-view.js +56 -0
  273. package/dist/modules/webui/reasoning-view.js +15 -0
  274. package/dist/modules/webui/run-agent.js +81 -0
  275. package/dist/modules/webui/server.js +284 -0
  276. package/dist/modules/webui/sessions-api.js +178 -0
  277. package/dist/modules/webui/settings-api.js +561 -0
  278. package/dist/modules/webui/setup-api.js +201 -0
  279. package/dist/modules/webui/state.js +74 -0
  280. package/dist/modules/webui/util.js +261 -0
  281. package/dist/output/bus.js +7 -2
  282. package/dist/output/channel.js +80 -28
  283. package/dist/output/format.js +2 -1
  284. package/dist/output/index.js +1 -0
  285. package/dist/output/json-sink.js +5 -3
  286. package/dist/output/machine.js +4 -3
  287. package/dist/output/session-sink.js +4 -4
  288. package/dist/tools/approve.js +4 -0
  289. package/dist/tools/attach-image.js +5 -4
  290. package/dist/tools/auto-fixer.js +26 -44
  291. package/dist/tools/auto-tags.js +83 -0
  292. package/dist/tools/bash.js +193 -116
  293. package/dist/tools/browser.js +76 -31
  294. package/dist/tools/chunk-query.js +14 -6
  295. package/dist/tools/create-dir.js +9 -5
  296. package/dist/tools/delete-file.js +9 -5
  297. package/dist/tools/download-file.js +32 -7
  298. package/dist/tools/edit-file.js +52 -14
  299. package/dist/tools/enable-tools.js +6 -6
  300. package/dist/tools/executor.js +132 -27
  301. package/dist/tools/file-info.js +3 -1
  302. package/dist/tools/filter-tools.js +3 -3
  303. package/dist/tools/glob-tool.js +4 -1
  304. package/dist/tools/grep-tool.js +13 -10
  305. package/dist/tools/hidden-tools-block.js +6 -6
  306. package/dist/tools/index.js +16 -8
  307. package/dist/tools/list-dir.js +4 -2
  308. package/dist/tools/load-skill.js +7 -0
  309. package/dist/tools/mcp-call.js +15 -4
  310. package/dist/tools/meet-done.js +14 -0
  311. package/dist/tools/move-file.js +14 -7
  312. package/dist/tools/path-utils.js +29 -29
  313. package/dist/tools/pipeline-run.js +29 -7
  314. package/dist/tools/preview.js +1 -1
  315. package/dist/tools/process-ids.js +31 -0
  316. package/dist/tools/process-kill.js +9 -16
  317. package/dist/tools/process-list.js +1 -0
  318. package/dist/tools/process-log.js +11 -16
  319. package/dist/tools/process-wait.js +63 -0
  320. package/dist/tools/prompt-io.js +4 -3
  321. package/dist/tools/question.js +172 -25
  322. package/dist/tools/read-file.js +55 -34
  323. package/dist/tools/recall.js +14 -8
  324. package/dist/tools/registry.js +6 -5
  325. package/dist/tools/remember.js +8 -4
  326. package/dist/tools/scope-request.js +11 -6
  327. package/dist/tools/search-history.js +5 -2
  328. package/dist/tools/session-info.js +2 -0
  329. package/dist/tools/set-thinking.js +8 -4
  330. package/dist/tools/subagent.js +59 -39
  331. package/dist/tools/syntax-validator.js +292 -90
  332. package/dist/tools/user-input.js +22 -18
  333. package/dist/tools/web-browse.js +9 -3
  334. package/dist/tools/web-fetch.js +12 -23
  335. package/dist/tools/web-search.js +7 -5
  336. package/dist/tools/write-file.js +45 -15
  337. package/dist/ui/action-log.js +13 -0
  338. package/dist/ui/box.js +4 -3
  339. package/dist/ui/colors.js +2 -0
  340. package/dist/ui/diff.js +6 -5
  341. package/dist/ui/index.js +1 -0
  342. package/dist/ui/line-editor.js +230 -122
  343. package/dist/ui/line-math.js +10 -2
  344. package/dist/ui/md-formatter.js +57 -37
  345. package/dist/ui/output.js +8 -3
  346. package/dist/ui/plan-view.js +21 -10
  347. package/dist/ui/renderer.js +393 -67
  348. package/dist/ui/spinner.js +16 -5
  349. package/dist/ui/table.js +15 -17
  350. package/dist/ui/theme.js +143 -0
  351. package/dist/utils/child-run.js +20 -0
  352. package/dist/utils/error.js +1 -1
  353. package/dist/utils/index.js +2 -0
  354. package/dist/utils/retry.js +2 -2
  355. package/dist/utils/truncate.js +1 -1
  356. package/package.json +1 -1
@@ -1,33 +1,7 @@
1
- /**
2
- * Ask LM Studio's native API which context length the model is currently
3
- * loaded with. Prefers the running instance (`loaded_instances[].config.
4
- * context_length`), falls back to the model's `max_context_length`. Returns
5
- * null when the endpoint is unavailable (not LM Studio), the model is not
6
- * found/loaded, or the request fails — the caller must degrade silently.
7
- */
8
- export async function getLoadedContextLength(baseUrl, model, timeoutMs = 3000) {
9
- const base = baseUrl.replace(/\/$/, "").replace(/\/v1\/?$/, "");
10
- try {
11
- const response = await fetch(`${base}/api/v1/models`, {
12
- signal: AbortSignal.timeout(timeoutMs),
13
- });
14
- if (!response.ok)
15
- return null;
16
- const data = (await response.json());
17
- const models = data.models || [];
18
- const found = models.find((m) => m.key === model || m.name === model || model.includes(m.key));
19
- if (!found)
20
- return null;
21
- const loadedInstance = (found.loaded_instances ?? [])[0];
22
- const actual = loadedInstance?.config?.context_length ?? found.max_context_length;
23
- if (typeof actual !== "number" || actual <= 0)
24
- return null;
25
- return { model: found.key ?? found.name ?? model, actual };
26
- }
27
- catch {
28
- return null;
29
- }
30
- }
1
+ // Хелперы живой context-probe — в ./provider-context.ts и ./models-dev.ts;
2
+ // реэкспорт отсюда, чтобы существующие импорты не ломать.
3
+ export { probeModel, getLoadedContextLength, probeProviderContext, } from "./provider-context";
4
+ /** Загрузчик модели LM Studio: ensure + load через native API. */
31
5
  export class ModelLoader {
32
6
  baseUrl;
33
7
  logger;
@@ -36,6 +10,10 @@ export class ModelLoader {
36
10
  this.baseUrl = url.replace(/\/v1\/?$/, "");
37
11
  this.logger = logger;
38
12
  }
13
+ /**
14
+ * Гарантирует загруженную модель с нужным контекстом (иначе грузит).
15
+ * @param config конфиг загрузки; @returns итог (уже была/загружена/ошибка).
16
+ */
39
17
  async ensureModelLoaded(config) {
40
18
  if (config.autoLoad === false) {
41
19
  return { success: true, alreadyLoaded: true };
@@ -61,11 +39,11 @@ export class ModelLoader {
61
39
  if (!response.ok)
62
40
  return false;
63
41
  const data = (await response.json());
64
- const models = data.models || [];
42
+ const models = data.models ?? [];
65
43
  const found = models.find((m) => m.key === model || m.name === model);
66
44
  if (!found)
67
45
  return false;
68
- const loadedInstances = found.loaded_instances || [];
46
+ const loadedInstances = found.loaded_instances ?? [];
69
47
  return loadedInstances.some((inst) => inst.config?.context_length === contextLength);
70
48
  }
71
49
  catch {
@@ -0,0 +1,121 @@
1
+ /**
2
+ * models.dev-фолбэк для хостед-шлюзов (OpenCode Zen/Go), чей собственный
3
+ * `/models` отдаёт голые id без метаданных контекста (проверено вживую:
4
+ * только `{id, object, created, owned_by}`).
5
+ *
6
+ * models.dev — открытая база моделей (MIT, sst/models.dev), её же использует
7
+ * сам opencode. Каталог `api.json`: `Record<providerId, { models:
8
+ * Record<modelId, { limit: { context } }> }>`, провайдер `opencode` держит
9
+ * модели под теми же bare id, что отдаёт Zen (`deepseek-v4.1-flash` →
10
+ * `limit.context`).
11
+ *
12
+ * На диске только slim-проекция (`opencode` → id → контекст):
13
+ * `~/.mma/models-dev-cache.json` (override `MMA_MODELS_DEV_CACHE`), TTL 24h
14
+ * как у reasoning-пробника. Любой сбой — null, вызывающий хранит настроенный
15
+ * `contextWindow` (fail-open).
16
+ */
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
18
+ import { join, dirname } from "path";
19
+ import { globalMmaDir } from "../config/paths";
20
+ import { readEnv } from "../config/env";
21
+ import { MODELS_DEV_CATALOG_TTL_MS, MODELS_DEV_FETCH_TIMEOUT_MS, } from "../config/timeouts";
22
+ const MODELS_DEV_URL = "https://models.dev/api.json";
23
+ function cachePath() {
24
+ return readEnv("MODELS_DEV_CACHE") ?? join(globalMmaDir(), "models-dev-cache.json");
25
+ }
26
+ /** In-memory L1: свежая проекция на время жизни процесса. */
27
+ let memCache = null;
28
+ function readDiskCache() {
29
+ try {
30
+ const path = cachePath();
31
+ if (!existsSync(path))
32
+ return null;
33
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
34
+ if (typeof parsed.ts !== "string" || !parsed.models || typeof parsed.models !== "object") {
35
+ return null;
36
+ }
37
+ return { ts: parsed.ts, models: parsed.models };
38
+ }
39
+ catch {
40
+ // Битый кэш — перечитываем ниже.
41
+ return null;
42
+ }
43
+ }
44
+ function writeDiskCache(models) {
45
+ try {
46
+ const path = cachePath();
47
+ mkdirSync(dirname(path), { recursive: true });
48
+ const entry = { ts: new Date().toISOString(), models };
49
+ writeFileSync(path, JSON.stringify(entry), "utf-8");
50
+ }
51
+ catch {
52
+ // Best-effort — падение записи не должно ронять агента.
53
+ }
54
+ }
55
+ /** `limit.context` одной записи модели, если это годное число. */
56
+ function modelContextOf(meta) {
57
+ const context = meta?.limit?.context;
58
+ if (typeof context !== "number" || !Number.isFinite(context) || context <= 0)
59
+ return null;
60
+ return Math.floor(context);
61
+ }
62
+ function extractOpencodeModels(catalog) {
63
+ if (!catalog || typeof catalog !== "object")
64
+ return null;
65
+ const provider = catalog.opencode;
66
+ if (!provider || typeof provider !== "object")
67
+ return null;
68
+ const models = provider.models;
69
+ if (!models || typeof models !== "object")
70
+ return null;
71
+ const out = {};
72
+ for (const [id, meta] of Object.entries(models)) {
73
+ const context = modelContextOf(meta);
74
+ if (context !== null)
75
+ out[id] = context;
76
+ }
77
+ return out;
78
+ }
79
+ async function fetchCatalog(timeoutMs) {
80
+ try {
81
+ const response = await fetch(MODELS_DEV_URL, { signal: AbortSignal.timeout(timeoutMs) });
82
+ if (!response.ok)
83
+ return null;
84
+ return extractOpencodeModels((await response.json()));
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ /**
91
+ * Ищет `model` в секции провайдера `opencode` каталога models.dev.
92
+ * Только точное совпадение id (Zen отдаёт bare id, каталог ключует так же).
93
+ * Возвращает каталожный `limit.context`, на любом промахе — null.
94
+ */
95
+ export async function lookupModelsDevContext(model, opts = {}) {
96
+ const timeoutMs = opts.timeoutMs ?? MODELS_DEV_FETCH_TIMEOUT_MS;
97
+ if (memCache && Date.now() - memCache.ts < MODELS_DEV_CATALOG_TTL_MS) {
98
+ return memCache.models[model] ?? null;
99
+ }
100
+ const disk = readDiskCache();
101
+ if (disk && Date.now() - new Date(disk.ts).getTime() < MODELS_DEV_CATALOG_TTL_MS) {
102
+ memCache = { models: disk.models, ts: Date.now() };
103
+ return disk.models[model] ?? null;
104
+ }
105
+ const fresh = await fetchCatalog(timeoutMs);
106
+ if (!fresh) {
107
+ // Офлайн с протухшим кэшем: протухшее окно лучше никакого.
108
+ if (disk) {
109
+ memCache = { models: disk.models, ts: Date.now() };
110
+ return disk.models[model] ?? null;
111
+ }
112
+ return null;
113
+ }
114
+ memCache = { models: fresh, ts: Date.now() };
115
+ writeDiskCache(fresh);
116
+ return fresh[model] ?? null;
117
+ }
118
+ /** Тестовый шов: сбросить in-memory проекцию (диск изолирован через env). */
119
+ export function resetModelsDevCache() {
120
+ memCache = null;
121
+ }
@@ -6,14 +6,14 @@ import { LlmError } from "./llm-errors";
6
6
  import { backoffDelay } from "../utils/retry";
7
7
  import { StreamState } from "./stream-state";
8
8
  import { parseCacheUsage } from "./cache-usage";
9
- const REQUEST_TIMEOUT_MS = 120000;
10
- /** Max wait (ms) for a self-inflicted rate-limit slot before giving up. */
11
- const MAX_RATE_WAIT_MS = 30_000;
9
+ import { LLM_REQUEST_TIMEOUT_MS as REQUEST_TIMEOUT_MS, LLM_MAX_RATE_WAIT_MS as MAX_RATE_WAIT_MS, } from "../config/timeouts";
10
+ /** Cap completion-токенов в max_tokens, если конфиг не задал. */
11
+ const DEFAULT_MAX_COMPLETION_TOKENS = 8192;
12
12
  /**
13
- * Client identity header. OpenCode Go asks clients to identify themselves with
14
- * their own user agent (not a generic SDK/HTTP-library name) so requests route
15
- * to coding-agent traffic. `readMmaVersion()` is memoized — every provider
16
- * construction would otherwise re-read package.json.
13
+ * Client identity-заголовок. OpenCode Go просит клиентов представляться своим
14
+ * user agent (не generic SDK/HTTP-библиотекой), чтобы запросы роутились в
15
+ * coding-agent трафик. `readMmaVersion()` мемоизирован — иначе каждая сборка
16
+ * провайдера перечитывала бы package.json.
17
17
  */
18
18
  let cachedUserAgent = null;
19
19
  function defaultUserAgent() {
@@ -21,6 +21,23 @@ function defaultUserAgent() {
21
21
  cachedUserAgent = `micro-models-agent/${readMmaVersion()}`;
22
22
  return cachedUserAgent;
23
23
  }
24
+ /**
25
+ * Нормализует харнессный уровень reasoning под контракт провайдерского поля
26
+ * `reasoning_effort` (strict OpenAI/o-series принимает только
27
+ * low|medium|high). Иначе:
28
+ * - "default"/undefined → не шлём (решает провайдерский дефолт);
29
+ * - "none" → не шлём (strict OpenAI отвечает 400; отсутствие поля = не просим
30
+ * повышать, а prompt-tag ещё и получает тег /no_think в `chat()`);
31
+ * - "max" → клампим к "high" (наибольшее валидное значение).
32
+ * Без нормализации неизвестное значение роняет ВЕСЬ запрос HTTP 400.
33
+ */
34
+ function normalizeReasoningEffort(level) {
35
+ if (!level || level === "default" || level === "none")
36
+ return undefined;
37
+ if (level === "max")
38
+ return "high";
39
+ return level;
40
+ }
24
41
  function buildRequestBody(opts) {
25
42
  const body = {
26
43
  model: opts.model,
@@ -37,21 +54,24 @@ function buildRequestBody(opts) {
37
54
  body.session_id = opts.sessionId;
38
55
  if (opts.stream && opts.streamUsage)
39
56
  body.stream_options = { include_usage: true };
40
- // Apply reasoning effort via the provider's strategy
57
+ // Применяем effort через стратегию провайдера (уровень нормализуется под
58
+ // контракт OpenAI: low|medium|high).
41
59
  const strategy = opts.reasoningStrategy ?? "openai-effort";
42
- const level = opts.reasoningEffort;
43
- if (strategy === "openai-effort" && level && level !== "default") {
44
- body.reasoning_effort = level;
60
+ const effort = normalizeReasoningEffort(opts.reasoningEffort);
61
+ if (strategy === "openai-effort") {
62
+ if (effort)
63
+ body.reasoning_effort = effort;
45
64
  }
46
65
  else if (strategy === "template-kwarg") {
47
- body.chat_template_kwargs = { enable_thinking: level !== "none" };
66
+ body.chat_template_kwargs = { enable_thinking: opts.reasoningEffort !== "none" };
48
67
  }
49
- else if (strategy === "prompt-tag" && level && level !== "default") {
50
- // Send reasoning_effort in body (models that respect it via API) AND
51
- // add /no_think tag to message (models that only respect prompt signals).
52
- body.reasoning_effort = level;
68
+ else if (strategy === "prompt-tag") {
69
+ // Шлём reasoning_effort в теле (модели с API-поддержкой) И тег /no_think
70
+ // в сообщение (модели только с prompt-сигналами; тег ставит `chat()`).
71
+ if (effort)
72
+ body.reasoning_effort = effort;
53
73
  }
54
- // "none" = unsupported, skip
74
+ // "none" = не поддерживается, пропуск
55
75
  if (opts.tools && opts.tools.length > 0) {
56
76
  body.tools = opts.tools.map((t) => ({
57
77
  type: "function",
@@ -65,6 +85,7 @@ function buildRequestBody(opts) {
65
85
  }
66
86
  return body;
67
87
  }
88
+ /** Провайдер OpenAI-совместимого API (стрим + non-stream фолбэки, ретраи). */
68
89
  export class OpenAICompatProvider {
69
90
  model;
70
91
  contextWindow;
@@ -98,11 +119,16 @@ export class OpenAICompatProvider {
98
119
  this.userAgent = config.userAgent ?? defaultUserAgent();
99
120
  this.debug = config.logger ? config.logger.debug.bind(config.logger) : null;
100
121
  }
122
+ /**
123
+ * Стрим чата: rate-limit → стрим → фолбэк non-stream при пустом стриме.
124
+ * @param messages история; @param tools определения тулов;
125
+ * @param signal abort; @param options лимит токенов/reasoning.
126
+ * @returns асинхронный итератор чанков.
127
+ */
101
128
  async *chat(messages, tools, signal, options) {
102
- // Check rate limit before making request. A self-inflicted limit should
103
- // wait for a slot (bounded) instead of aborting a healthy turn; only if
104
- // no slot frees within the window do we surface a recoverable error the
105
- // agent loop can feed back (rather than dying).
129
+ // Проверка лимита до запроса. Собственный лимит ждёт слота (ограниченно),
130
+ // а не роняет здоровый ход; recoverable-ошибка наружу — только если слот
131
+ // не освободился в окне (цикл скормит её модели, а не умрёт).
106
132
  if (!this.rateLimiter.canMakeRequest()) {
107
133
  const waitMs = Math.min(this.rateLimiter.msUntilRequest(), MAX_RATE_WAIT_MS);
108
134
  if (waitMs > 0) {
@@ -112,9 +138,9 @@ export class OpenAICompatProvider {
112
138
  throw new LlmError(`Rate limit exceeded: ${this.rateLimiter.getConfig().maxRequestsPerMinute} requests per minute`, { recoverable: true });
113
139
  }
114
140
  }
115
- // Record this request
141
+ // Фиксируем запрос
116
142
  this.rateLimiter.recordRequest();
117
- // Apply prompt-tag strategy: append /no_think to last user message (outgoing copy only)
143
+ // Prompt-tag стратегия: дописать /no_think в последнее user-сообщение (только исходящая копия)
118
144
  let effectiveMessages = messages;
119
145
  if (options?.reasoningStrategy === "prompt-tag" && options?.reasoningEffort === "none") {
120
146
  effectiveMessages = this.applyPromptTag(messages, "/no_think");
@@ -132,12 +158,11 @@ export class OpenAICompatProvider {
132
158
  hasReasoning = true;
133
159
  yield chunk;
134
160
  }
135
- // A stream that ended with reasoning but no text/tool_call means the model
136
- // THOUGHT but never produced an answer (observed: qwen3.5-9b in LM Studio
137
- // emits reasoning_content then stops). The streamed reasoning was already
138
- // delivered; fetch the missing answer via non-streaming. The fallback's
139
- // own reasoning chunk is dropped so thinking is not shown twice — no text
140
- // was emitted, so there is no duplicate-answer risk.
161
+ // Стрим кончился на reasoning без текста/tool_call: модель ПОДУМАЛА, но
162
+ // ответа не дала (наблюдалось: qwen3.5-9b в LM Studio шлёт reasoning_content
163
+ // и встаёт). Reasoning уже отдан; недостающий ответ добираем non-stream.
164
+ // Reasoning-чанк фолбэка дропаем, чтобы не показать дважды — текста не было,
165
+ // риск дубля ответа отсутствует.
141
166
  if (!hasToolCall && !hasText) {
142
167
  this.debug?.("LLM streaming produced no text/tool_call — falling back to non-streaming");
143
168
  const fallback = await this.doNonStreaming(effectiveMessages, tools, signal, options);
@@ -149,17 +174,17 @@ export class OpenAICompatProvider {
149
174
  }
150
175
  }
151
176
  }
152
- /** Append a tag to the last user message (outgoing copy, never stored in context). */
177
+ /** Дописывает тег в последнее user-сообщение (исходящая копия, не в контекст). */
153
178
  applyPromptTag(messages, tag) {
154
179
  const copy = [...messages];
155
180
  for (let i = copy.length - 1; i >= 0; i--) {
156
181
  if (copy[i].role === "user") {
157
182
  const msg = { ...copy[i] };
158
183
  if (typeof msg.content === "string") {
159
- msg.content = msg.content + "\n" + tag;
184
+ msg.content = `${msg.content}\n${tag}`;
160
185
  }
161
186
  else {
162
- // Array of parts — append a text part
187
+ // Массив частей — дописываем текстовую часть
163
188
  msg.content = [...msg.content, { type: "text", text: tag }];
164
189
  }
165
190
  copy[i] = msg;
@@ -172,6 +197,7 @@ export class OpenAICompatProvider {
172
197
  const { baseDelay, maxDelay, maxStreamRetries, noDataTimeoutMs } = this.retryConfig;
173
198
  const streamRetries = maxStreamRetries ?? 2;
174
199
  const idleTimeoutMs = noDataTimeoutMs ?? 180000;
200
+ let usedNonStreamingFallback = false;
175
201
  for (let attempt = 0;; attempt++) {
176
202
  let emitted = false;
177
203
  const onEmit = () => {
@@ -179,27 +205,44 @@ export class OpenAICompatProvider {
179
205
  };
180
206
  try {
181
207
  const sawDone = yield* this.streamOnce(messages, tools, signal, options, onEmit, idleTimeoutMs);
182
- // Clean [DONE] or any partial content → deliver what we have.
208
+ // Чистый [DONE] или частичный контент — отдаём что есть.
183
209
  if (sawDone || emitted)
184
210
  return;
185
- // Stream closed before [DONE] with zero content → retryable truncation.
211
+ // Стрим закрылся до [DONE] с нулевым контентом — ретраябельная обрезка.
186
212
  if (attempt >= streamRetries)
187
213
  return;
188
214
  }
189
215
  catch (err) {
190
216
  if (err?.name === "AbortError" || (err instanceof LlmError && err.terminal) || signal?.aborted)
191
217
  throw err;
192
- // Content was already delivered — a re-stream would duplicate chunks.
218
+ // Stall буферизованного SSE после tool_call-дельт: один раз переспрашиваем
219
+ // non-stream. Стрим тут — битый путь, повтор тем же способом снова
220
+ // встанет; а tool_call агент получить обязан.
221
+ if (err?.stalledAfterToolCall === true && !usedNonStreamingFallback) {
222
+ usedNonStreamingFallback = true;
223
+ this.debug?.("LLM stream stalled after tool_call — falling back to non-streaming request");
224
+ const fallback = await this.doNonStreaming(messages, tools, signal, options);
225
+ this.debug?.("LLM non-streaming fallback result", { chunks: fallback.length });
226
+ for (const chunk of fallback) {
227
+ // Reasoning до stall уже, вероятно, устримлен; копию фолбэка дропаем,
228
+ // чтобы не дублировать мышление.
229
+ if (chunk.type === "reasoning")
230
+ continue;
231
+ yield chunk;
232
+ }
233
+ return;
234
+ }
235
+ // Контент уже отдан — перестрим продублировал бы чанки.
193
236
  if (emitted || attempt >= streamRetries)
194
237
  throw err;
195
- // Connection dropped before any content → retry the whole request.
238
+ // Обрыв до любого контента — переспрашиваем целиком.
196
239
  }
197
240
  // Экспоненциальный backoff с джиттером (общий хелпер из src/utils).
198
241
  await this.sleep(backoffDelay(attempt, baseDelay, maxDelay, 0.1), signal);
199
242
  }
200
243
  }
201
244
  async *streamOnce(messages, tools, signal, options, onEmit, idleTimeoutMs) {
202
- const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096;
245
+ const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
203
246
  const body = buildRequestBody({
204
247
  model: this.model,
205
248
  messages,
@@ -231,8 +274,8 @@ export class OpenAICompatProvider {
231
274
  });
232
275
  }
233
276
  catch (err) {
234
- // Fetch-level failures are already HTTP-retried inside fetchWithRetry —
235
- // don't double-retry them at the stream level.
277
+ // Fetch-сбои уже HTTP-ретраятся внутри fetchWithRetry —
278
+ // на уровне стрима повторно не ретраим.
236
279
  if (err?.name === "AbortError")
237
280
  throw err;
238
281
  const wrapped = err instanceof Error ? err : new Error(String(err));
@@ -266,6 +309,14 @@ export class OpenAICompatProvider {
266
309
  const toolCallAccs = new Map();
267
310
  // Единый контейнер состояния стрима (вместо 9 разрозненных флагов).
268
311
  const st = new StreamState();
312
+ // Stall после старта tool_call-дельт обычно значит: провайдер буферизует
313
+ // SSE вместо стрима дельт аргументов (видели на LM Studio). Помечаем ошибку,
314
+ // чтобы `doStream` переспросил non-stream вместо провала всего прогона.
315
+ const idleError = () => {
316
+ const e = new Error(t(st.sawToolCallStart ? "error.llm_stream_idle_toolcall" : "error.llm_stream_idle", { timeout: idleTimeoutMs }));
317
+ e.stalledAfterToolCall = st.sawToolCallStart;
318
+ return e;
319
+ };
269
320
  const readIdle = () => new Promise((resolve, reject) => {
270
321
  const idleTimer = setTimeout(() => {
271
322
  flagTimeout();
@@ -274,13 +325,13 @@ export class OpenAICompatProvider {
274
325
  reader.read().then((result) => {
275
326
  clearTimeout(idleTimer);
276
327
  if (isTimeout())
277
- reject(new Error(t(st.sawToolCallStart ? "error.llm_stream_idle_toolcall" : "error.llm_stream_idle", { timeout: idleTimeoutMs })));
328
+ reject(idleError());
278
329
  else
279
330
  resolve(result);
280
331
  }, (err) => {
281
332
  clearTimeout(idleTimer);
282
333
  if (isTimeout())
283
- reject(new Error(t(st.sawToolCallStart ? "error.llm_stream_idle_toolcall" : "error.llm_stream_idle", { timeout: idleTimeoutMs })));
334
+ reject(idleError());
284
335
  else
285
336
  reject(err);
286
337
  });
@@ -306,10 +357,9 @@ export class OpenAICompatProvider {
306
357
  }
307
358
  }
308
359
  else {
309
- // Not SSE-framed — Ollama (and some other backends) may stream
310
- // NDJSON or send mid-stream errors as bare JSON. Try to parse the
311
- // raw line; unparseable noise (SSE comments, keep-alives) is
312
- // counted and skipped.
360
+ // Не SSE-фрейм — Ollama (и часть бэкендов) стримят NDJSON или шлют
361
+ // mid-stream ошибки голым JSON. Пробуем парсить сырую строку;
362
+ // непарсящийся шум (SSE-комменты, keep-alive) считаем и пропускаем.
313
363
  st.nonDataLines++;
314
364
  if (st.nonDataSamples.length < 5)
315
365
  st.nonDataSamples.push(trimmed.slice(0, 200));
@@ -320,7 +370,7 @@ export class OpenAICompatProvider {
320
370
  st.parsedChunks++;
321
371
  const choice = parsed.choices?.[0];
322
372
  if (!choice) {
323
- // Usage comes in the last chunk with empty choices
373
+ // Usage приходит в последнем чанке с пустыми choices
324
374
  if (parsed.usage) {
325
375
  st.usage = {
326
376
  promptTokens: parsed.usage.prompt_tokens ?? 0,
@@ -331,14 +381,14 @@ export class OpenAICompatProvider {
331
381
  this.debug?.("LLM stream usage", { ...st.usage });
332
382
  }
333
383
  else if (parsed.error) {
334
- // Mid-stream provider error (Ollama sends {"error": "..."} as
335
- // bare JSON once generation already started). Surface it —
336
- // swallowing it makes the response look silently empty.
337
- // Thrown OUTSIDE the JSON-parse try/catch below.
384
+ // Mid-stream ошибка провайдера (Ollama шлёт {"error": "..."}
385
+ // голым JSON после старта генерации). Показываем — глотание
386
+ // даст молча пустой ответ.
387
+ // Брошено ВНЕ try/catch JSON-парсинга ниже.
338
388
  st.midStreamError = String(parsed.error);
339
389
  }
340
- // NOTE: no `continue` here — it would skip the midStreamError
341
- // throw below (which lives outside this try/catch).
390
+ // NOTE: `continue` тут нет — он пропустил бы throw midStreamError
391
+ // ниже (он вне этого try/catch).
342
392
  }
343
393
  else {
344
394
  if (st.parsedChunks <= 3 || st.parsedChunks % 50 === 0) {
@@ -353,7 +403,7 @@ export class OpenAICompatProvider {
353
403
  const finishReason = choice.finish_reason;
354
404
  if (finishReason)
355
405
  st.lastFinishReason = finishReason;
356
- // `reasoning` is the Ollama alias for OpenAI's `reasoning_content`.
406
+ // `reasoning` — алиас Ollama для openai-`reasoning_content`.
357
407
  const reasoningDelta = delta.reasoning_content ?? delta.reasoning;
358
408
  if (reasoningDelta) {
359
409
  onEmit();
@@ -400,7 +450,7 @@ export class OpenAICompatProvider {
400
450
  }
401
451
  }
402
452
  catch {
403
- // Skip malformed JSON lines
453
+ // Битые JSON-строки пропускаем
404
454
  }
405
455
  if (st.midStreamError) {
406
456
  throw new Error(t("error.llm_provider_stream_error", { error: st.midStreamError.slice(0, 300) }));
@@ -411,10 +461,10 @@ export class OpenAICompatProvider {
411
461
  onEmit();
412
462
  yield { type: "done", usage: st.usage };
413
463
  }
414
- // finish_reason "length": the completion hit the token limit. An
415
- // unfinished tool_call would otherwise be silently swallowed (tool_calls
416
- // are only yielded on finish_reason "tool_calls") and surface as an
417
- // empty response → blind hallucination retries. Surface the real cause.
464
+ // finish_reason "length": completion упёрся в лимит токенов. Незаконченный
465
+ // tool_call иначе молча глотается (tool_calls отдаются только на
466
+ // finish_reason "tool_calls") и всплывает пустым ответом → слепые
467
+ // hallucination-ретраи. Показываем настоящую причину.
418
468
  if (st.lastFinishReason === "length") {
419
469
  const truncatedToolCall = st.sawToolCallStart && toolCallAccs.size > 0;
420
470
  if (truncatedToolCall || !st.sawText) {
@@ -454,14 +504,14 @@ export class OpenAICompatProvider {
454
504
  streamUsage: this.cache?.requestStreamUsage === true,
455
505
  };
456
506
  }
457
- /** Shared request setup: auth headers + timeout/abort controller wiring. */
507
+ /** Общая подготовка запроса: auth-заголовки + таймаут/abort-контроллер. */
458
508
  buildRequestSetup(signal) {
459
509
  const headers = {
460
510
  "Content-Type": "application/json",
461
511
  "User-Agent": this.userAgent,
462
512
  };
463
513
  if (this.config.apiKey && this.config.apiKey !== "not-needed") {
464
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
514
+ headers.Authorization = `Bearer ${this.config.apiKey}`;
465
515
  }
466
516
  const sessionId = this.getSessionId?.();
467
517
  // Когда capability задана — шлём заголовок только тем, кому он нужен
@@ -500,7 +550,7 @@ export class OpenAICompatProvider {
500
550
  };
501
551
  }
502
552
  async doNonStreaming(messages, tools, signal, options) {
503
- const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096;
553
+ const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
504
554
  const body = buildRequestBody({
505
555
  model: this.model,
506
556
  messages,
@@ -528,7 +578,7 @@ export class OpenAICompatProvider {
528
578
  }));
529
579
  }
530
580
  const data = await response.json();
531
- // Some backends (e.g. Ollama) return HTTP 200 with {"error": "..."}.
581
+ // Часть бэкендов (напр. Ollama) отдают HTTP 200 с {"error": "..."}.
532
582
  if (data.error && !data.choices) {
533
583
  throw new Error(t("error.llm_provider_stream_error", { error: String(data.error).slice(0, 300) }));
534
584
  }
@@ -557,15 +607,15 @@ export class OpenAICompatProvider {
557
607
  });
558
608
  }
559
609
  }
560
- // Completed text but hit the cap: surface a visible warning instead of
561
- // silently accepting a cut-off answer (see the streaming path).
610
+ // Текст готов, но упёрся в cap: видимый warning вместо молча
611
+ // принятого обрезанного ответа (см. стрим-путь).
562
612
  if (choice.finish_reason === "length" && msg.content && !msg.tool_calls) {
563
613
  chunks.push({
564
614
  type: "warning",
565
615
  content: t("error.llm_output_truncated", { tokens: maxTokens }),
566
616
  });
567
617
  }
568
- // Append usage from API response
618
+ // Дописываем usage из ответа API
569
619
  if (data.usage) {
570
620
  chunks.push({
571
621
  type: "done",
@@ -589,9 +639,17 @@ export class OpenAICompatProvider {
589
639
  cleanup();
590
640
  }
591
641
  }
642
+ /**
643
+ * Считает токены текста (tiktoken по дефолтной модели).
644
+ * @param text текст; @returns число токенов.
645
+ */
592
646
  countTokens(text) {
593
647
  return this.tokenCounter.count(text);
594
648
  }
649
+ /**
650
+ * Список моделей провайдера (`/models`: id/name/model).
651
+ * @returns имена моделей; @throws при недоступности провайдера.
652
+ */
595
653
  async listModels() {
596
654
  try {
597
655
  const url = `${this.config.baseUrl.replace(/\/+$/, "")}/models`;
@@ -600,20 +658,35 @@ export class OpenAICompatProvider {
600
658
  "User-Agent": this.userAgent,
601
659
  };
602
660
  if (this.config.apiKey && this.config.apiKey !== "not-needed") {
603
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
661
+ headers.Authorization = `Bearer ${this.config.apiKey}`;
604
662
  }
605
663
  const response = await fetch(url, {
606
664
  method: "GET",
607
665
  headers,
608
666
  });
609
667
  if (!response.ok) {
610
- // Surface the failure: health probes must distinguish a dead
611
- // provider from one that serves an empty model list.
668
+ // Провал наружу: health-пробы должны отличать мёртвый провайдер
669
+ // от пустого списка моделей.
612
670
  throw new Error(`HTTP ${response.status} from ${url}`);
613
671
  }
614
- const data = (await response.json());
615
- const models = (data.data || data || [])
616
- .map((m) => m.id || m.name || m.model || "")
672
+ const data = await response.json();
673
+ const rawList = Array.isArray(data)
674
+ ? data
675
+ : data && typeof data === "object" && Array.isArray(data.data)
676
+ ? data.data
677
+ : [];
678
+ const models = rawList
679
+ .map((m) => {
680
+ if (m && typeof m === "object") {
681
+ const obj = m;
682
+ for (const key of ["id", "name", "model"]) {
683
+ const v = obj[key];
684
+ if (typeof v === "string" && v)
685
+ return v;
686
+ }
687
+ }
688
+ return "";
689
+ })
617
690
  .filter(Boolean);
618
691
  return models;
619
692
  }
@@ -643,8 +716,8 @@ export class OpenAICompatProvider {
643
716
  throw err;
644
717
  }
645
718
  if (attempt < maxRetries) {
646
- // Prefer the provider's Retry-After window (saturating at maxDelay);
647
- // otherwise exponential backoff. 429s from free tiers need a real wait.
719
+ // Приоритет окну Retry-After провайдера (cap на maxDelay); иначе —
720
+ // экспоненциальный backoff. 429 с free-тарифов требуют реального ожидания.
648
721
  const delay = lastRetryAfter > 0
649
722
  ? Math.min(lastRetryAfter, maxDelay)
650
723
  : backoffDelay(attempt, baseDelay, maxDelay, 0.1);