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.
- package/CHANGELOG.md +39 -0
- package/dist/assets/app.js +22 -2
- package/dist/assets/index.html +39 -9
- package/dist/assets/js/dom.js +15 -0
- package/dist/assets/js/feed.js +129 -10
- package/dist/assets/js/sidebar.js +512 -0
- package/dist/assets/js/tabs.js +30 -5
- package/dist/assets/setup.js +267 -8
- package/dist/assets/styles.css +168 -3
- package/dist/cli/cache-line.js +1 -0
- package/dist/cli/command-suggest.js +5 -4
- package/dist/cli/commands.js +231 -122
- package/dist/cli/completer.js +132 -31
- package/dist/cli/index.js +1 -0
- package/dist/cli/json-payload.js +4 -4
- package/dist/cli/main.js +166 -61
- package/dist/cli/onboarding-commands.js +126 -0
- package/dist/cli/plugin-commands.js +2 -1
- package/dist/cli/relaunch.js +16 -13
- package/dist/cli/repl-commands-lsp.js +75 -0
- package/dist/cli/repl-commands-skills.js +105 -0
- package/dist/cli/repl-commands.js +589 -266
- package/dist/cli/repl.js +436 -177
- package/dist/cli/run-result.js +5 -6
- package/dist/cli/security-commands.js +37 -28
- package/dist/cli/setup-order.js +4 -55
- package/dist/cli/setup-prompt.js +3 -2
- package/dist/cli/setup.js +27 -107
- package/dist/cli/web-command.js +98 -0
- package/dist/config/budget.js +5 -5
- package/dist/config/config.js +103 -96
- package/dist/config/defaults.js +38 -8
- package/dist/config/domains.js +24 -27
- package/dist/config/env.js +46 -0
- package/dist/config/experts.js +2 -0
- package/dist/config/index.js +1 -0
- package/dist/config/paths.js +56 -0
- package/dist/config/redact.js +44 -0
- package/dist/config/registry-extra.js +334 -0
- package/dist/config/registry.js +584 -0
- package/dist/config/security.js +35 -36
- package/dist/config/timeouts.js +74 -0
- package/dist/config/utils.js +16 -15
- package/dist/core/agent/artifact-progress.js +59 -0
- package/dist/core/agent/artifact-resolve.js +79 -0
- package/dist/core/agent/audit-gate.js +7 -3
- package/dist/core/agent/compaction.js +2 -2
- package/dist/core/agent/constants.js +33 -4
- package/dist/core/agent/context-log.js +454 -0
- package/dist/core/agent/context-renderer.js +5 -0
- package/dist/core/agent/critic-gate.js +132 -0
- package/dist/core/agent/hallucination-gate.js +148 -38
- package/dist/core/agent/loop-state.js +15 -0
- package/dist/core/agent/prefix-monitor.js +1 -1
- package/dist/core/agent/reasoning-resolver.js +10 -1
- package/dist/core/agent/token-tracker.js +7 -4
- package/dist/core/agent/tool-batch.js +93 -18
- package/dist/core/agent/tool-images.js +18 -0
- package/dist/core/agent/tool-output.js +65 -11
- package/dist/core/agent/tool-pairing.js +60 -0
- package/dist/core/agent-moe.js +25 -24
- package/dist/core/agent.js +429 -108
- package/dist/core/bootstrap/agent-phase.js +66 -0
- package/dist/core/bootstrap/config-phase.js +86 -0
- package/dist/core/bootstrap/modules-phase.js +269 -0
- package/dist/core/bootstrap/provider-phase.js +183 -0
- package/dist/core/bootstrap/session-phase.js +204 -0
- package/dist/core/bootstrap/state.js +94 -0
- package/dist/core/bootstrap/workspace-phase.js +115 -0
- package/dist/core/bootstrap.js +92 -660
- package/dist/core/crash-handler.js +17 -7
- package/dist/core/environment.js +37 -12
- package/dist/core/host-plan.js +43 -0
- package/dist/core/host.js +23 -0
- package/dist/core/index.js +1 -0
- package/dist/core/prompt-builder.js +17 -1
- package/dist/core/prompt-overflow.js +42 -43
- package/dist/core/session-logger.js +122 -30
- package/dist/core/system-prompt.js +117 -0
- package/dist/core/version.js +6 -4
- package/dist/core/web-host.js +34 -0
- package/dist/core/workspace.js +19 -15
- package/dist/i18n/en.json +89 -3
- package/dist/i18n/index.js +37 -3
- package/dist/i18n/ru.json +89 -3
- package/dist/index.js +7 -3
- package/dist/llm/image-utils.js +55 -64
- package/dist/llm/llm-errors.js +9 -0
- package/dist/llm/model-loader.js +10 -32
- package/dist/llm/models-dev.js +121 -0
- package/dist/llm/openai-compat.js +146 -73
- package/dist/llm/orchestrator.js +33 -11
- package/dist/llm/provider-budget.js +2 -2
- package/dist/llm/provider-context.js +233 -0
- package/dist/llm/provider.js +1 -1
- package/dist/llm/response.js +5 -0
- package/dist/llm/token-counter.js +27 -11
- package/dist/logger/app-logger.js +48 -22
- package/dist/logger/file-log.js +28 -19
- package/dist/logger/index.js +1 -0
- package/dist/main.js +888 -139
- package/dist/migration/backup.js +14 -3
- package/dist/migration/detect.js +12 -3
- package/dist/migration/index.js +1 -0
- package/dist/modules/artifacts/store.js +9 -8
- package/dist/modules/browser/actions.js +6 -0
- package/dist/modules/browser/bridge-client.js +14 -12
- package/dist/modules/browser/bridge-path.js +4 -4
- package/dist/modules/browser/console-buffer.js +37 -0
- package/dist/modules/browser/cookie-store.js +5 -0
- package/dist/modules/browser/driver-types.js +1 -0
- package/dist/modules/browser/driver.js +19 -11
- package/dist/modules/browser/index.js +3 -1
- package/dist/modules/browser/module.js +2 -1
- package/dist/modules/browser/session.js +10 -30
- package/dist/modules/browser/snapshot.js +19 -13
- package/dist/modules/browser/types.js +1 -0
- package/dist/modules/certification/cli.js +28 -20
- package/dist/modules/certification/fact-checker.js +9 -1
- package/dist/modules/certification/loader.js +9 -2
- package/dist/modules/certification/manifest.js +83 -21
- package/dist/modules/certification/runner.js +23 -13
- package/dist/modules/certification/scenarios.js +20 -20
- package/dist/modules/certification/syntax-scenarios.js +1 -2
- package/dist/modules/context/chunk-query.js +11 -9
- package/dist/modules/context/fact-extractor.js +37 -39
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +106 -80
- package/dist/modules/critic/client.js +176 -0
- package/dist/modules/critic/evidence.js +81 -0
- package/dist/modules/critic/provider.js +45 -0
- package/dist/modules/critic/tools.js +78 -0
- package/dist/modules/critic/types.js +1 -0
- package/dist/modules/execution/audit-runners.js +28 -34
- package/dist/modules/execution/auditor.js +53 -49
- package/dist/modules/execution/execution-plugin.js +142 -102
- package/dist/modules/execution/index.js +1 -1
- package/dist/modules/execution/input-from.js +37 -13
- package/dist/modules/execution/module.js +368 -191
- package/dist/modules/execution/moe-executor.js +53 -26
- package/dist/modules/execution/plan-actions.js +320 -202
- package/dist/modules/execution/plan-checks.js +35 -0
- package/dist/modules/execution/plan-coverage.js +36 -33
- package/dist/modules/execution/plan-steps.js +11 -3
- package/dist/modules/execution/plan-store.js +83 -17
- package/dist/modules/execution/plan-tool.js +89 -35
- package/dist/modules/execution/plan-validator.js +30 -10
- package/dist/modules/execution/planner.js +22 -13
- package/dist/modules/execution/stuck-detector.js +196 -93
- package/dist/modules/execution/tracker.js +22 -8
- package/dist/modules/execution/verifier.js +69 -48
- package/dist/modules/execution/windows-commands.js +12 -12
- package/dist/modules/hallucination/artifact-claim.js +93 -0
- package/dist/modules/hallucination/confidence.js +13 -11
- package/dist/modules/hallucination/consistency.js +10 -2
- package/dist/modules/hallucination/detector.js +10 -5
- package/dist/modules/hallucination/factual.js +43 -26
- package/dist/modules/hallucination/index.js +1 -0
- package/dist/modules/hallucination/js-identifiers.js +44 -45
- package/dist/modules/hallucination/llm-judge.js +17 -12
- package/dist/modules/hallucination/success-claim.js +59 -0
- package/dist/modules/indexer/cache.js +52 -4
- package/dist/modules/indexer/index.js +1 -0
- package/dist/modules/indexer/map-command.js +2 -4
- package/dist/modules/indexer/map-select.js +16 -16
- package/dist/modules/indexer/module.js +47 -12
- package/dist/modules/indexer/project-profile.js +8 -7
- package/dist/modules/indexer/symbols.js +15 -15
- package/dist/modules/indexer/walker.js +159 -29
- package/dist/modules/lsp/check-tool.js +7 -5
- package/dist/modules/lsp/client.js +39 -39
- package/dist/modules/lsp/command.js +17 -24
- package/dist/modules/lsp/config.js +12 -11
- package/dist/modules/lsp/index.js +1 -0
- package/dist/modules/lsp/module.js +48 -46
- package/dist/modules/lsp/probe.js +13 -14
- package/dist/modules/lsp/project-root.js +14 -17
- package/dist/modules/lsp/startup-check.js +18 -16
- package/dist/modules/mcp/client.js +59 -62
- package/dist/modules/mcp/index.js +1 -0
- package/dist/modules/mcp/module.js +66 -37
- package/dist/modules/mcp/registry.js +5 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +12 -8
- package/dist/modules/memory/search.js +4 -1
- package/dist/modules/memory/store.js +40 -2
- package/dist/modules/onboarding/init.js +47 -0
- package/dist/modules/onboarding/meet.js +30 -0
- package/dist/modules/onboarding/mode.js +30 -0
- package/dist/modules/onboarding/prompts.js +43 -0
- package/dist/modules/pipelines/engine.js +10 -0
- package/dist/modules/pipelines/index.js +1 -0
- package/dist/modules/pipelines/parser.js +4 -1
- package/dist/modules/pipelines/template.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
- package/dist/modules/plugins/builtin/notify.js +2 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +18 -3
- package/dist/modules/plugins/manager.js +37 -17
- package/dist/modules/pricing/index.js +10 -11
- package/dist/modules/pricing/prices.js +12 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +27 -31
- package/dist/modules/processes/registry.js +44 -25
- package/dist/modules/processes/runner.js +4 -4
- package/dist/modules/providers/capabilities.js +28 -0
- package/dist/modules/providers/create.js +3 -4
- package/dist/modules/providers/factory.js +7 -7
- package/dist/modules/providers/fallback.js +12 -12
- package/dist/modules/providers/health.js +4 -4
- package/dist/modules/providers/manager.js +50 -19
- package/dist/modules/providers/naming.js +15 -0
- package/dist/modules/providers/preflight.js +71 -0
- package/dist/modules/providers/presets.js +39 -8
- package/dist/modules/providers/registry.js +7 -2
- package/dist/modules/reasoning/policy.js +8 -6
- package/dist/modules/reasoning/probe.js +30 -25
- package/dist/modules/security/audit-log.js +19 -36
- package/dist/modules/security/audit-notifier.js +35 -75
- package/dist/modules/security/command-validator.js +99 -51
- package/dist/modules/security/content-scanner.js +8 -8
- package/dist/modules/security/data-sanitizer.js +9 -16
- package/dist/modules/security/encryption.js +32 -52
- package/dist/modules/security/index.js +2 -1
- package/dist/modules/security/network-validator.js +120 -43
- package/dist/modules/security/path-validator.js +61 -46
- package/dist/modules/security/protected-paths.js +73 -0
- package/dist/modules/security/rate-limiter.js +17 -49
- package/dist/modules/security/security-policies.js +25 -57
- package/dist/modules/security/session-encryption.js +32 -71
- package/dist/modules/security/session-isolation.js +15 -28
- package/dist/modules/session/auto-name.js +63 -0
- package/dist/modules/session/export.js +98 -0
- package/dist/modules/session/index.js +1 -0
- package/dist/modules/session/manager.js +79 -28
- package/dist/modules/session/module.js +4 -3
- package/dist/modules/session/store.js +57 -20
- package/dist/modules/setup/answers.js +35 -0
- package/dist/modules/setup/apply.js +79 -0
- package/dist/modules/setup/discovery.js +72 -0
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/loader.js +9 -1
- package/dist/modules/skills/module.js +11 -0
- package/dist/modules/updater/changelog-reader.js +15 -16
- package/dist/modules/updater/checker.js +9 -4
- package/dist/modules/updater/dev-detect.js +7 -6
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +17 -15
- package/dist/modules/user-profile/compressor.js +2 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +10 -1
- package/dist/modules/webui/answer-api.js +29 -0
- package/dist/modules/webui/assets.js +50 -0
- package/dist/modules/webui/body.js +21 -0
- package/dist/modules/webui/commands-settings.js +209 -0
- package/dist/modules/webui/commands-shared.js +60 -0
- package/dist/modules/webui/commands.js +174 -0
- package/dist/modules/webui/config-api.js +134 -0
- package/dist/modules/webui/file-api.js +202 -0
- package/dist/modules/webui/forward.js +159 -0
- package/dist/modules/webui/history-api.js +112 -0
- package/dist/modules/webui/host-state.js +60 -0
- package/dist/modules/webui/i18n-api.js +9 -0
- package/dist/modules/webui/limits.js +6 -0
- package/dist/modules/webui/log-forward.js +20 -0
- package/dist/modules/webui/memory-commands.js +64 -0
- package/dist/modules/webui/model-cache.js +18 -0
- package/dist/modules/webui/module.js +148 -0
- package/dist/modules/webui/persist.js +24 -0
- package/dist/modules/webui/prompt-io.js +97 -0
- package/dist/modules/webui/providers-api.js +337 -0
- package/dist/modules/webui/providers-view.js +56 -0
- package/dist/modules/webui/reasoning-view.js +15 -0
- package/dist/modules/webui/run-agent.js +81 -0
- package/dist/modules/webui/server.js +284 -0
- package/dist/modules/webui/sessions-api.js +178 -0
- package/dist/modules/webui/settings-api.js +561 -0
- package/dist/modules/webui/setup-api.js +201 -0
- package/dist/modules/webui/state.js +74 -0
- package/dist/modules/webui/util.js +261 -0
- package/dist/output/bus.js +7 -2
- package/dist/output/channel.js +80 -28
- package/dist/output/format.js +2 -1
- package/dist/output/index.js +1 -0
- package/dist/output/json-sink.js +5 -3
- package/dist/output/machine.js +4 -3
- package/dist/output/session-sink.js +4 -4
- package/dist/tools/approve.js +4 -0
- package/dist/tools/attach-image.js +5 -4
- package/dist/tools/auto-fixer.js +26 -44
- package/dist/tools/auto-tags.js +83 -0
- package/dist/tools/bash.js +193 -116
- package/dist/tools/browser.js +76 -31
- package/dist/tools/chunk-query.js +14 -6
- package/dist/tools/create-dir.js +9 -5
- package/dist/tools/delete-file.js +9 -5
- package/dist/tools/download-file.js +32 -7
- package/dist/tools/edit-file.js +52 -14
- package/dist/tools/enable-tools.js +6 -6
- package/dist/tools/executor.js +132 -27
- package/dist/tools/file-info.js +3 -1
- package/dist/tools/filter-tools.js +3 -3
- package/dist/tools/glob-tool.js +4 -1
- package/dist/tools/grep-tool.js +13 -10
- package/dist/tools/hidden-tools-block.js +6 -6
- package/dist/tools/index.js +16 -8
- package/dist/tools/list-dir.js +4 -2
- package/dist/tools/load-skill.js +7 -0
- package/dist/tools/mcp-call.js +15 -4
- package/dist/tools/meet-done.js +14 -0
- package/dist/tools/move-file.js +14 -7
- package/dist/tools/path-utils.js +29 -29
- package/dist/tools/pipeline-run.js +29 -7
- package/dist/tools/preview.js +1 -1
- package/dist/tools/process-ids.js +31 -0
- package/dist/tools/process-kill.js +9 -16
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +11 -16
- package/dist/tools/process-wait.js +63 -0
- package/dist/tools/prompt-io.js +4 -3
- package/dist/tools/question.js +172 -25
- package/dist/tools/read-file.js +55 -34
- package/dist/tools/recall.js +14 -8
- package/dist/tools/registry.js +6 -5
- package/dist/tools/remember.js +8 -4
- package/dist/tools/scope-request.js +11 -6
- package/dist/tools/search-history.js +5 -2
- package/dist/tools/session-info.js +2 -0
- package/dist/tools/set-thinking.js +8 -4
- package/dist/tools/subagent.js +59 -39
- package/dist/tools/syntax-validator.js +292 -90
- package/dist/tools/user-input.js +22 -18
- package/dist/tools/web-browse.js +9 -3
- package/dist/tools/web-fetch.js +12 -23
- package/dist/tools/web-search.js +7 -5
- package/dist/tools/write-file.js +45 -15
- package/dist/ui/action-log.js +13 -0
- package/dist/ui/box.js +4 -3
- package/dist/ui/colors.js +2 -0
- package/dist/ui/diff.js +6 -5
- package/dist/ui/index.js +1 -0
- package/dist/ui/line-editor.js +230 -122
- package/dist/ui/line-math.js +10 -2
- package/dist/ui/md-formatter.js +57 -37
- package/dist/ui/output.js +8 -3
- package/dist/ui/plan-view.js +21 -10
- package/dist/ui/renderer.js +393 -67
- package/dist/ui/spinner.js +16 -5
- package/dist/ui/table.js +15 -17
- package/dist/ui/theme.js +143 -0
- package/dist/utils/child-run.js +20 -0
- package/dist/utils/error.js +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/retry.js +2 -2
- package/dist/utils/truncate.js +1 -1
- package/package.json +1 -1
package/dist/llm/model-loader.js
CHANGED
|
@@ -1,33 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
10
|
-
/**
|
|
11
|
-
const
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
//
|
|
57
|
+
// Применяем effort через стратегию провайдера (уровень нормализуется под
|
|
58
|
+
// контракт OpenAI: low|medium|high).
|
|
41
59
|
const strategy = opts.reasoningStrategy ?? "openai-effort";
|
|
42
|
-
const
|
|
43
|
-
if (strategy === "openai-effort"
|
|
44
|
-
|
|
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:
|
|
66
|
+
body.chat_template_kwargs = { enable_thinking: opts.reasoningEffort !== "none" };
|
|
48
67
|
}
|
|
49
|
-
else if (strategy === "prompt-tag"
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
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" =
|
|
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
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
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
|
-
//
|
|
141
|
+
// Фиксируем запрос
|
|
116
142
|
this.rateLimiter.recordRequest();
|
|
117
|
-
//
|
|
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
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
184
|
+
msg.content = `${msg.content}\n${tag}`;
|
|
160
185
|
}
|
|
161
186
|
else {
|
|
162
|
-
//
|
|
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
|
-
//
|
|
208
|
+
// Чистый [DONE] или частичный контент — отдаём что есть.
|
|
183
209
|
if (sawDone || emitted)
|
|
184
210
|
return;
|
|
185
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 ??
|
|
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
|
|
235
|
-
//
|
|
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(
|
|
328
|
+
reject(idleError());
|
|
278
329
|
else
|
|
279
330
|
resolve(result);
|
|
280
331
|
}, (err) => {
|
|
281
332
|
clearTimeout(idleTimer);
|
|
282
333
|
if (isTimeout())
|
|
283
|
-
reject(
|
|
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
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
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
|
|
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
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
384
|
+
// Mid-stream ошибка провайдера (Ollama шлёт {"error": "..."}
|
|
385
|
+
// голым JSON после старта генерации). Показываем — глотание
|
|
386
|
+
// даст молча пустой ответ.
|
|
387
|
+
// Брошено ВНЕ try/catch JSON-парсинга ниже.
|
|
338
388
|
st.midStreamError = String(parsed.error);
|
|
339
389
|
}
|
|
340
|
-
// NOTE:
|
|
341
|
-
//
|
|
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`
|
|
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
|
-
//
|
|
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":
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
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 ??
|
|
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
|
-
//
|
|
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
|
-
//
|
|
561
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
611
|
-
//
|
|
668
|
+
// Провал наружу: health-пробы должны отличать мёртвый провайдер
|
|
669
|
+
// от пустого списка моделей.
|
|
612
670
|
throw new Error(`HTTP ${response.status} from ${url}`);
|
|
613
671
|
}
|
|
614
|
-
const data =
|
|
615
|
-
const
|
|
616
|
-
|
|
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
|
-
//
|
|
647
|
-
//
|
|
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);
|