micro-models-agent 2.16.1 → 2.17.2
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 +34 -0
- package/README.md +8 -3
- package/dist/assets/app.js +1 -314
- package/dist/assets/hljs.js +5 -1244
- package/dist/assets/index.html +11 -0
- package/dist/assets/js/api.js +1 -28
- package/dist/assets/js/core/menus.js +1 -0
- package/dist/assets/js/core/store/feed.js +1 -0
- package/dist/assets/js/core/store/index.js +1 -0
- package/dist/assets/js/core/store/session.js +1 -0
- package/dist/assets/js/core/store/settings.js +1 -0
- package/dist/assets/js/core/store/tabs.js +1 -0
- package/dist/assets/js/dialog.js +1 -105
- package/dist/assets/js/dom.js +1 -178
- package/dist/assets/js/feed/core.js +1 -0
- package/dist/assets/js/feed/history.js +1 -0
- package/dist/assets/js/feed/plan.js +1 -0
- package/dist/assets/js/feed/question.js +3 -0
- package/dist/assets/js/feed/sidebar.js +1 -0
- package/dist/assets/js/feed/stream.js +1 -0
- package/dist/assets/js/feed/turn.js +5 -0
- package/dist/assets/js/feed.js +1 -1250
- package/dist/assets/js/format.js +2 -122
- package/dist/assets/js/i18n.js +1 -46
- package/dist/assets/js/rail.js +1 -114
- package/dist/assets/js/session/cmdk.js +1 -0
- package/dist/assets/js/session/composer.js +1 -0
- package/dist/assets/js/session/sessions-menu.js +1 -0
- package/dist/assets/js/session/tabs.js +1 -0
- package/dist/assets/js/settings/controls.js +1 -0
- package/dist/assets/js/settings/editors.js +1 -0
- package/dist/assets/js/settings/form.js +1 -0
- package/dist/assets/js/settings/modal.js +1 -0
- package/dist/assets/js/settings/panels.js +1 -0
- package/dist/assets/js/settings/providers.js +1 -0
- package/dist/assets/js/settings/search.js +1 -0
- package/dist/assets/js/settings/subdialog.js +1 -0
- package/dist/assets/js/settings/write.js +1 -0
- package/dist/assets/js/settings.js +1 -1914
- package/dist/assets/js/sidebar/core.js +1 -0
- package/dist/assets/js/sidebar/lsp.js +2 -0
- package/dist/assets/js/sidebar/map.js +1 -0
- package/dist/assets/js/sidebar/state.js +1 -0
- package/dist/assets/js/sidebar/trace.js +5 -0
- package/dist/assets/js/sidebar.js +1 -705
- package/dist/assets/js/store.js +1 -82
- package/dist/assets/js/subdialog.js +1 -0
- package/dist/assets/md.js +1 -291
- package/dist/assets/setup.js +3 -816
- package/dist/assets/smd.js +29 -1665
- package/dist/assets/styles.css +1 -1598
- package/dist/certification/certifications.json +1 -494
- package/dist/i18n/en.json +1 -1851
- package/dist/i18n/ru.json +1 -1854
- package/dist/main.js +815 -55797
- package/dist/modules/browser/bridge-server.mjs +2 -252
- package/package.json +10 -5
- package/dist/assets/js/settings-controls.js +0 -452
- package/dist/assets/js/tabs.js +0 -867
- package/dist/cli/cache-line.js +0 -31
- package/dist/cli/command-suggest.js +0 -39
- package/dist/cli/commands.js +0 -781
- package/dist/cli/completer.js +0 -268
- package/dist/cli/index.js +0 -3
- package/dist/cli/json-payload.js +0 -32
- package/dist/cli/main.js +0 -346
- package/dist/cli/onboarding-commands.js +0 -126
- package/dist/cli/plugin-commands.js +0 -38
- package/dist/cli/relaunch.js +0 -40
- package/dist/cli/repl-commands-lsp.js +0 -75
- package/dist/cli/repl-commands-skills.js +0 -105
- package/dist/cli/repl-commands.js +0 -1218
- package/dist/cli/repl.js +0 -1238
- package/dist/cli/run-result.js +0 -38
- package/dist/cli/security-commands.js +0 -177
- package/dist/cli/setup-order.js +0 -6
- package/dist/cli/setup-prompt.js +0 -50
- package/dist/cli/setup.js +0 -161
- package/dist/cli/web-command.js +0 -98
- package/dist/config/budget.js +0 -48
- package/dist/config/config.js +0 -345
- package/dist/config/defaults.js +0 -197
- package/dist/config/domains.js +0 -135
- package/dist/config/env.js +0 -46
- package/dist/config/experts.js +0 -17
- package/dist/config/index.js +0 -5
- package/dist/config/paths.js +0 -56
- package/dist/config/redact.js +0 -44
- package/dist/config/registry-extra.js +0 -334
- package/dist/config/registry.js +0 -584
- package/dist/config/security.js +0 -212
- package/dist/config/timeouts.js +0 -74
- package/dist/config/types.js +0 -1
- package/dist/config/utils.js +0 -57
- package/dist/core/agent/artifact-progress.js +0 -59
- package/dist/core/agent/artifact-resolve.js +0 -79
- package/dist/core/agent/audit-gate.js +0 -53
- package/dist/core/agent/compaction.js +0 -89
- package/dist/core/agent/constants.js +0 -90
- package/dist/core/agent/context-log.js +0 -454
- package/dist/core/agent/context-renderer.js +0 -45
- package/dist/core/agent/critic-gate.js +0 -132
- package/dist/core/agent/hallucination-gate.js +0 -197
- package/dist/core/agent/loop-state.js +0 -68
- package/dist/core/agent/prefix-monitor.js +0 -101
- package/dist/core/agent/reasoning-resolver.js +0 -65
- package/dist/core/agent/token-tracker.js +0 -99
- package/dist/core/agent/tool-batch.js +0 -312
- package/dist/core/agent/tool-images.js +0 -18
- package/dist/core/agent/tool-output.js +0 -116
- package/dist/core/agent/tool-pairing.js +0 -60
- package/dist/core/agent-moe.js +0 -248
- package/dist/core/agent.js +0 -1299
- package/dist/core/bootstrap/agent-phase.js +0 -66
- package/dist/core/bootstrap/config-phase.js +0 -86
- package/dist/core/bootstrap/modules-phase.js +0 -269
- package/dist/core/bootstrap/provider-phase.js +0 -183
- package/dist/core/bootstrap/session-phase.js +0 -204
- package/dist/core/bootstrap/state.js +0 -94
- package/dist/core/bootstrap/workspace-phase.js +0 -115
- package/dist/core/bootstrap.js +0 -112
- package/dist/core/crash-handler.js +0 -62
- package/dist/core/environment.js +0 -224
- package/dist/core/host-plan.js +0 -43
- package/dist/core/host.js +0 -23
- package/dist/core/index.js +0 -3
- package/dist/core/prompt-builder.js +0 -95
- package/dist/core/prompt-overflow.js +0 -306
- package/dist/core/session-logger.js +0 -375
- package/dist/core/system-prompt.js +0 -117
- package/dist/core/types.js +0 -1
- package/dist/core/version.js +0 -28
- package/dist/core/web-host.js +0 -34
- package/dist/core/workspace.js +0 -80
- package/dist/i18n/index.js +0 -80
- package/dist/index.js +0 -30
- package/dist/llm/cache-usage.js +0 -76
- package/dist/llm/image-utils.js +0 -138
- package/dist/llm/index.js +0 -4
- package/dist/llm/llm-errors.js +0 -50
- package/dist/llm/model-loader.js +0 -86
- package/dist/llm/models-dev.js +0 -121
- package/dist/llm/openai-compat.js +0 -756
- package/dist/llm/orchestrator.js +0 -294
- package/dist/llm/provider-budget.js +0 -68
- package/dist/llm/provider-context.js +0 -233
- package/dist/llm/provider.js +0 -9
- package/dist/llm/response.js +0 -44
- package/dist/llm/stream-state.js +0 -26
- package/dist/llm/token-counter.js +0 -83
- package/dist/llm/types.js +0 -1
- package/dist/logger/app-logger.js +0 -212
- package/dist/logger/file-log.js +0 -160
- package/dist/logger/index.js +0 -2
- package/dist/migration/backup.js +0 -56
- package/dist/migration/detect.js +0 -61
- package/dist/migration/index.js +0 -3
- package/dist/modules/artifacts/store.js +0 -62
- package/dist/modules/browser/actions.js +0 -79
- package/dist/modules/browser/bridge-client.js +0 -203
- package/dist/modules/browser/bridge-path.js +0 -10
- package/dist/modules/browser/console-buffer.js +0 -37
- package/dist/modules/browser/cookie-store.js +0 -29
- package/dist/modules/browser/driver-types.js +0 -1
- package/dist/modules/browser/driver.js +0 -186
- package/dist/modules/browser/index.js +0 -9
- package/dist/modules/browser/module.js +0 -30
- package/dist/modules/browser/session.js +0 -322
- package/dist/modules/browser/snapshot.js +0 -154
- package/dist/modules/browser/types.js +0 -13
- package/dist/modules/certification/cli.js +0 -264
- package/dist/modules/certification/fact-checker.js +0 -90
- package/dist/modules/certification/loader.js +0 -127
- package/dist/modules/certification/manifest.js +0 -231
- package/dist/modules/certification/runner.js +0 -233
- package/dist/modules/certification/scenarios.js +0 -586
- package/dist/modules/certification/syntax-scenarios.js +0 -50
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/context/chunk-query.js +0 -122
- package/dist/modules/context/fact-extractor.js +0 -170
- package/dist/modules/context/history.js +0 -15
- package/dist/modules/context/index.js +0 -2
- package/dist/modules/context/manager.js +0 -482
- package/dist/modules/critic/client.js +0 -176
- package/dist/modules/critic/evidence.js +0 -81
- package/dist/modules/critic/provider.js +0 -45
- package/dist/modules/critic/tools.js +0 -78
- package/dist/modules/critic/types.js +0 -1
- package/dist/modules/execution/audit-runners.js +0 -206
- package/dist/modules/execution/auditor.js +0 -222
- package/dist/modules/execution/execution-plugin.js +0 -478
- package/dist/modules/execution/index.js +0 -8
- package/dist/modules/execution/input-from.js +0 -70
- package/dist/modules/execution/module.js +0 -891
- package/dist/modules/execution/moe-executor.js +0 -443
- package/dist/modules/execution/plan-actions.js +0 -642
- package/dist/modules/execution/plan-checks.js +0 -35
- package/dist/modules/execution/plan-coverage.js +0 -71
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-steps.js +0 -31
- package/dist/modules/execution/plan-store.js +0 -274
- package/dist/modules/execution/plan-tool.js +0 -249
- package/dist/modules/execution/plan-validator.js +0 -197
- package/dist/modules/execution/planner.js +0 -103
- package/dist/modules/execution/stuck-detector.js +0 -834
- package/dist/modules/execution/tracker.js +0 -92
- package/dist/modules/execution/transient-error.js +0 -30
- package/dist/modules/execution/types.js +0 -1
- package/dist/modules/execution/verifier.js +0 -343
- package/dist/modules/execution/windows-commands.js +0 -52
- package/dist/modules/hallucination/artifact-claim.js +0 -93
- package/dist/modules/hallucination/confidence.js +0 -81
- package/dist/modules/hallucination/consistency.js +0 -37
- package/dist/modules/hallucination/detector.js +0 -57
- package/dist/modules/hallucination/factual.js +0 -205
- package/dist/modules/hallucination/index.js +0 -6
- package/dist/modules/hallucination/js-identifiers.js +0 -261
- package/dist/modules/hallucination/llm-judge.js +0 -116
- package/dist/modules/hallucination/success-claim.js +0 -59
- package/dist/modules/index.js +0 -5
- package/dist/modules/indexer/cache.js +0 -88
- package/dist/modules/indexer/index.js +0 -4
- package/dist/modules/indexer/map-command.js +0 -33
- package/dist/modules/indexer/map-select.js +0 -87
- package/dist/modules/indexer/module.js +0 -293
- package/dist/modules/indexer/project-profile.js +0 -184
- package/dist/modules/indexer/symbols.js +0 -189
- package/dist/modules/indexer/walker.js +0 -285
- package/dist/modules/lsp/check-tool.js +0 -61
- package/dist/modules/lsp/client.js +0 -406
- package/dist/modules/lsp/command.js +0 -53
- package/dist/modules/lsp/config.js +0 -189
- package/dist/modules/lsp/index.js +0 -4
- package/dist/modules/lsp/module.js +0 -295
- package/dist/modules/lsp/probe.js +0 -86
- package/dist/modules/lsp/project-root.js +0 -69
- package/dist/modules/lsp/startup-check.js +0 -154
- package/dist/modules/lsp/types.js +0 -1
- package/dist/modules/mcp/client.js +0 -445
- package/dist/modules/mcp/index.js +0 -4
- package/dist/modules/mcp/module.js +0 -295
- package/dist/modules/mcp/registry.js +0 -20
- package/dist/modules/memory/index.js +0 -2
- package/dist/modules/memory/module.js +0 -101
- package/dist/modules/memory/search.js +0 -45
- package/dist/modules/memory/store.js +0 -107
- package/dist/modules/onboarding/init.js +0 -47
- package/dist/modules/onboarding/meet.js +0 -30
- package/dist/modules/onboarding/mode.js +0 -30
- package/dist/modules/onboarding/prompts.js +0 -43
- package/dist/modules/pipelines/engine.js +0 -70
- package/dist/modules/pipelines/index.js +0 -4
- package/dist/modules/pipelines/parser.js +0 -59
- package/dist/modules/pipelines/template.js +0 -18
- package/dist/modules/plugins/builtin/lint-on-write.js +0 -377
- package/dist/modules/plugins/builtin/notify.js +0 -11
- package/dist/modules/plugins/index.js +0 -2
- package/dist/modules/plugins/loader.js +0 -85
- package/dist/modules/plugins/manager.js +0 -244
- package/dist/modules/plugins/types.js +0 -1
- package/dist/modules/pricing/index.js +0 -70
- package/dist/modules/pricing/prices.js +0 -147
- package/dist/modules/processes/detect.js +0 -34
- package/dist/modules/processes/index.js +0 -4
- package/dist/modules/processes/kill-tree.js +0 -52
- package/dist/modules/processes/registry.js +0 -294
- package/dist/modules/processes/runner.js +0 -23
- package/dist/modules/providers/cache.js +0 -23
- package/dist/modules/providers/capabilities.js +0 -28
- package/dist/modules/providers/create.js +0 -21
- package/dist/modules/providers/factory.js +0 -28
- package/dist/modules/providers/fallback.js +0 -81
- package/dist/modules/providers/health.js +0 -47
- package/dist/modules/providers/index.js +0 -6
- package/dist/modules/providers/manager.js +0 -207
- package/dist/modules/providers/naming.js +0 -15
- package/dist/modules/providers/preflight.js +0 -71
- package/dist/modules/providers/presets.js +0 -232
- package/dist/modules/providers/registry.js +0 -27
- package/dist/modules/providers/types.js +0 -1
- package/dist/modules/reasoning/policy.js +0 -42
- package/dist/modules/reasoning/probe.js +0 -116
- package/dist/modules/registry.js +0 -48
- package/dist/modules/security/audit-log.js +0 -119
- package/dist/modules/security/audit-notifier.js +0 -267
- package/dist/modules/security/command-validator.js +0 -272
- package/dist/modules/security/content-scanner.js +0 -53
- package/dist/modules/security/data-sanitizer.js +0 -82
- package/dist/modules/security/encryption.js +0 -216
- package/dist/modules/security/index.js +0 -15
- package/dist/modules/security/network-validator.js +0 -236
- package/dist/modules/security/path-validator.js +0 -261
- package/dist/modules/security/protected-paths.js +0 -73
- package/dist/modules/security/rate-limiter.js +0 -98
- package/dist/modules/security/security-policies.js +0 -499
- package/dist/modules/security/session-encryption.js +0 -182
- package/dist/modules/security/session-isolation.js +0 -75
- package/dist/modules/session/auto-name.js +0 -63
- package/dist/modules/session/export.js +0 -98
- package/dist/modules/session/index.js +0 -4
- package/dist/modules/session/manager.js +0 -234
- package/dist/modules/session/module.js +0 -33
- package/dist/modules/session/store.js +0 -295
- package/dist/modules/session/types.js +0 -1
- package/dist/modules/setup/answers.js +0 -35
- package/dist/modules/setup/apply.js +0 -79
- package/dist/modules/setup/discovery.js +0 -72
- package/dist/modules/skills/index.js +0 -3
- package/dist/modules/skills/loader.js +0 -86
- package/dist/modules/skills/matcher.js +0 -27
- package/dist/modules/skills/module.js +0 -141
- package/dist/modules/types.js +0 -1
- package/dist/modules/updater/changelog-reader.js +0 -93
- package/dist/modules/updater/checker.js +0 -101
- package/dist/modules/updater/dev-detect.js +0 -18
- package/dist/modules/updater/index.js +0 -4
- package/dist/modules/updater/module.js +0 -129
- package/dist/modules/user-profile/compressor.js +0 -18
- package/dist/modules/user-profile/index.js +0 -2
- package/dist/modules/user-profile/profile.js +0 -77
- package/dist/modules/webui/answer-api.js +0 -29
- package/dist/modules/webui/assets.js +0 -50
- package/dist/modules/webui/body.js +0 -21
- package/dist/modules/webui/commands-settings.js +0 -209
- package/dist/modules/webui/commands-shared.js +0 -60
- package/dist/modules/webui/commands.js +0 -174
- package/dist/modules/webui/config-api.js +0 -134
- package/dist/modules/webui/file-api.js +0 -202
- package/dist/modules/webui/forward.js +0 -159
- package/dist/modules/webui/history-api.js +0 -112
- package/dist/modules/webui/host-state.js +0 -60
- package/dist/modules/webui/i18n-api.js +0 -9
- package/dist/modules/webui/limits.js +0 -6
- package/dist/modules/webui/log-forward.js +0 -20
- package/dist/modules/webui/memory-commands.js +0 -64
- package/dist/modules/webui/model-cache.js +0 -18
- package/dist/modules/webui/module.js +0 -148
- package/dist/modules/webui/persist.js +0 -24
- package/dist/modules/webui/prompt-io.js +0 -97
- package/dist/modules/webui/providers-api.js +0 -337
- package/dist/modules/webui/providers-view.js +0 -56
- package/dist/modules/webui/reasoning-view.js +0 -15
- package/dist/modules/webui/run-agent.js +0 -81
- package/dist/modules/webui/server.js +0 -284
- package/dist/modules/webui/sessions-api.js +0 -178
- package/dist/modules/webui/settings-api.js +0 -561
- package/dist/modules/webui/setup-api.js +0 -201
- package/dist/modules/webui/state.js +0 -74
- package/dist/modules/webui/util.js +0 -261
- package/dist/output/bus.js +0 -37
- package/dist/output/channel.js +0 -285
- package/dist/output/format.js +0 -15
- package/dist/output/index.js +0 -8
- package/dist/output/json-sink.js +0 -24
- package/dist/output/machine.js +0 -9
- package/dist/output/session-sink.js +0 -27
- package/dist/output/types.js +0 -1
- package/dist/tools/approve.js +0 -41
- package/dist/tools/attach-image.js +0 -102
- package/dist/tools/auto-fixer.js +0 -180
- package/dist/tools/auto-tags.js +0 -83
- package/dist/tools/bash.js +0 -649
- package/dist/tools/browser.js +0 -160
- package/dist/tools/chunk-query.js +0 -112
- package/dist/tools/create-dir.js +0 -60
- package/dist/tools/delete-file.js +0 -67
- package/dist/tools/download-file.js +0 -142
- package/dist/tools/edit-file.js +0 -136
- package/dist/tools/enable-tools.js +0 -59
- package/dist/tools/executor.js +0 -304
- package/dist/tools/file-info.js +0 -49
- package/dist/tools/filter-tools.js +0 -17
- package/dist/tools/glob-tool.js +0 -37
- package/dist/tools/grep-tool.js +0 -142
- package/dist/tools/hidden-tools-block.js +0 -37
- package/dist/tools/index.js +0 -88
- package/dist/tools/list-dir.js +0 -53
- package/dist/tools/load-skill.js +0 -51
- package/dist/tools/mcp-call.js +0 -80
- package/dist/tools/meet-done.js +0 -14
- package/dist/tools/move-file.js +0 -94
- package/dist/tools/path-utils.js +0 -108
- package/dist/tools/pipeline-run.js +0 -167
- package/dist/tools/preview.js +0 -2
- package/dist/tools/process-ids.js +0 -31
- package/dist/tools/process-kill.js +0 -33
- package/dist/tools/process-list.js +0 -38
- package/dist/tools/process-log.js +0 -49
- package/dist/tools/process-wait.js +0 -63
- package/dist/tools/prompt-io.js +0 -29
- package/dist/tools/question.js +0 -288
- package/dist/tools/read-file.js +0 -200
- package/dist/tools/recall.js +0 -124
- package/dist/tools/registry.js +0 -48
- package/dist/tools/remember.js +0 -72
- package/dist/tools/scope-check.js +0 -32
- package/dist/tools/scope-request.js +0 -96
- package/dist/tools/search-history.js +0 -88
- package/dist/tools/session-info.js +0 -46
- package/dist/tools/set-thinking.js +0 -75
- package/dist/tools/subagent.js +0 -257
- package/dist/tools/syntax-validator.js +0 -379
- package/dist/tools/types.js +0 -1
- package/dist/tools/user-input.js +0 -134
- package/dist/tools/web-browse.js +0 -93
- package/dist/tools/web-fetch.js +0 -108
- package/dist/tools/web-search.js +0 -107
- package/dist/tools/write-file.js +0 -128
- package/dist/ui/action-log.js +0 -13
- package/dist/ui/box.js +0 -78
- package/dist/ui/colors.js +0 -6
- package/dist/ui/diff.js +0 -189
- package/dist/ui/index.js +0 -7
- package/dist/ui/line-editor.js +0 -1083
- package/dist/ui/line-math.js +0 -98
- package/dist/ui/md-formatter.js +0 -322
- package/dist/ui/output.js +0 -19
- package/dist/ui/plan-view.js +0 -109
- package/dist/ui/renderer.js +0 -589
- package/dist/ui/spinner.js +0 -91
- package/dist/ui/table.js +0 -142
- package/dist/ui/theme.js +0 -143
- package/dist/utils/child-run.js +0 -20
- package/dist/utils/error.js +0 -4
- package/dist/utils/index.js +0 -6
- package/dist/utils/retry.js +0 -17
- package/dist/utils/sleep.js +0 -23
- package/dist/utils/truncate.js +0 -9
|
@@ -1,756 +0,0 @@
|
|
|
1
|
-
import { TokenCounter } from "./token-counter";
|
|
2
|
-
import { t } from "../i18n/index";
|
|
3
|
-
import { readMmaVersion } from "../core/version";
|
|
4
|
-
import { createRateLimiter } from "../modules/security/rate-limiter";
|
|
5
|
-
import { LlmError } from "./llm-errors";
|
|
6
|
-
import { backoffDelay } from "../utils/retry";
|
|
7
|
-
import { StreamState } from "./stream-state";
|
|
8
|
-
import { parseCacheUsage } from "./cache-usage";
|
|
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
|
-
/**
|
|
13
|
-
* Client identity-заголовок. OpenCode Go просит клиентов представляться своим
|
|
14
|
-
* user agent (не generic SDK/HTTP-библиотекой), чтобы запросы роутились в
|
|
15
|
-
* coding-agent трафик. `readMmaVersion()` мемоизирован — иначе каждая сборка
|
|
16
|
-
* провайдера перечитывала бы package.json.
|
|
17
|
-
*/
|
|
18
|
-
let cachedUserAgent = null;
|
|
19
|
-
function defaultUserAgent() {
|
|
20
|
-
if (cachedUserAgent === null)
|
|
21
|
-
cachedUserAgent = `micro-models-agent/${readMmaVersion()}`;
|
|
22
|
-
return cachedUserAgent;
|
|
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
|
-
}
|
|
41
|
-
function buildRequestBody(opts) {
|
|
42
|
-
const body = {
|
|
43
|
-
model: opts.model,
|
|
44
|
-
messages: opts.messages,
|
|
45
|
-
stream: opts.stream,
|
|
46
|
-
};
|
|
47
|
-
if (opts.maxTokens !== undefined)
|
|
48
|
-
body.max_tokens = opts.maxTokens;
|
|
49
|
-
if (opts.cachePrompt)
|
|
50
|
-
body.cache_prompt = true;
|
|
51
|
-
if (opts.promptCacheKey)
|
|
52
|
-
body.prompt_cache_key = opts.promptCacheKey;
|
|
53
|
-
if (opts.sessionId)
|
|
54
|
-
body.session_id = opts.sessionId;
|
|
55
|
-
if (opts.stream && opts.streamUsage)
|
|
56
|
-
body.stream_options = { include_usage: true };
|
|
57
|
-
// Применяем effort через стратегию провайдера (уровень нормализуется под
|
|
58
|
-
// контракт OpenAI: low|medium|high).
|
|
59
|
-
const strategy = opts.reasoningStrategy ?? "openai-effort";
|
|
60
|
-
const effort = normalizeReasoningEffort(opts.reasoningEffort);
|
|
61
|
-
if (strategy === "openai-effort") {
|
|
62
|
-
if (effort)
|
|
63
|
-
body.reasoning_effort = effort;
|
|
64
|
-
}
|
|
65
|
-
else if (strategy === "template-kwarg") {
|
|
66
|
-
body.chat_template_kwargs = { enable_thinking: opts.reasoningEffort !== "none" };
|
|
67
|
-
}
|
|
68
|
-
else if (strategy === "prompt-tag") {
|
|
69
|
-
// Шлём reasoning_effort в теле (модели с API-поддержкой) И тег /no_think
|
|
70
|
-
// в сообщение (модели только с prompt-сигналами; тег ставит `chat()`).
|
|
71
|
-
if (effort)
|
|
72
|
-
body.reasoning_effort = effort;
|
|
73
|
-
}
|
|
74
|
-
// "none" = не поддерживается, пропуск
|
|
75
|
-
if (opts.tools && opts.tools.length > 0) {
|
|
76
|
-
body.tools = opts.tools.map((t) => ({
|
|
77
|
-
type: "function",
|
|
78
|
-
function: {
|
|
79
|
-
name: t.name,
|
|
80
|
-
description: t.description,
|
|
81
|
-
parameters: t.parameters,
|
|
82
|
-
},
|
|
83
|
-
}));
|
|
84
|
-
body.tool_choice = "auto";
|
|
85
|
-
}
|
|
86
|
-
return body;
|
|
87
|
-
}
|
|
88
|
-
/** Провайдер OpenAI-совместимого API (стрим + non-stream фолбэки, ретраи). */
|
|
89
|
-
export class OpenAICompatProvider {
|
|
90
|
-
model;
|
|
91
|
-
contextWindow;
|
|
92
|
-
config;
|
|
93
|
-
tokenCounter;
|
|
94
|
-
retryConfig;
|
|
95
|
-
rateLimiter;
|
|
96
|
-
debug;
|
|
97
|
-
getSessionId;
|
|
98
|
-
userAgent;
|
|
99
|
-
cache;
|
|
100
|
-
cacheReport;
|
|
101
|
-
constructor(config) {
|
|
102
|
-
this.config = config;
|
|
103
|
-
this.model = config.model;
|
|
104
|
-
this.contextWindow = config.contextWindow ?? 32768;
|
|
105
|
-
this.cache = config.cache;
|
|
106
|
-
// Без capability всё равно читаем OpenAI-совместимые поля: это не меняет
|
|
107
|
-
// запрос и работает для локальных/кастомных серверов.
|
|
108
|
-
this.cacheReport = config.cache?.report ?? "openai";
|
|
109
|
-
this.tokenCounter = new TokenCounter();
|
|
110
|
-
this.retryConfig = config.retry ?? {
|
|
111
|
-
maxRetries: 3,
|
|
112
|
-
baseDelay: 1000,
|
|
113
|
-
maxDelay: 30000,
|
|
114
|
-
maxStreamRetries: 2,
|
|
115
|
-
noDataTimeoutMs: 180000,
|
|
116
|
-
};
|
|
117
|
-
this.rateLimiter = createRateLimiter(config.rateLimits);
|
|
118
|
-
this.getSessionId = config.getSessionId;
|
|
119
|
-
this.userAgent = config.userAgent ?? defaultUserAgent();
|
|
120
|
-
this.debug = config.logger ? config.logger.debug.bind(config.logger) : null;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Стрим чата: rate-limit → стрим → фолбэк non-stream при пустом стриме.
|
|
124
|
-
* @param messages история; @param tools определения тулов;
|
|
125
|
-
* @param signal abort; @param options лимит токенов/reasoning.
|
|
126
|
-
* @returns асинхронный итератор чанков.
|
|
127
|
-
*/
|
|
128
|
-
async *chat(messages, tools, signal, options) {
|
|
129
|
-
// Проверка лимита до запроса. Собственный лимит ждёт слота (ограниченно),
|
|
130
|
-
// а не роняет здоровый ход; recoverable-ошибка наружу — только если слот
|
|
131
|
-
// не освободился в окне (цикл скормит её модели, а не умрёт).
|
|
132
|
-
if (!this.rateLimiter.canMakeRequest()) {
|
|
133
|
-
const waitMs = Math.min(this.rateLimiter.msUntilRequest(), MAX_RATE_WAIT_MS);
|
|
134
|
-
if (waitMs > 0) {
|
|
135
|
-
await this.sleep(waitMs, signal);
|
|
136
|
-
}
|
|
137
|
-
if (!this.rateLimiter.canMakeRequest()) {
|
|
138
|
-
throw new LlmError(`Rate limit exceeded: ${this.rateLimiter.getConfig().maxRequestsPerMinute} requests per minute`, { recoverable: true });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// Фиксируем запрос
|
|
142
|
-
this.rateLimiter.recordRequest();
|
|
143
|
-
// Prompt-tag стратегия: дописать /no_think в последнее user-сообщение (только исходящая копия)
|
|
144
|
-
let effectiveMessages = messages;
|
|
145
|
-
if (options?.reasoningStrategy === "prompt-tag" && options?.reasoningEffort === "none") {
|
|
146
|
-
effectiveMessages = this.applyPromptTag(messages, "/no_think");
|
|
147
|
-
}
|
|
148
|
-
const streamResult = this.doStream(effectiveMessages, tools, signal, options);
|
|
149
|
-
let hasToolCall = false;
|
|
150
|
-
let hasText = false;
|
|
151
|
-
let hasReasoning = false;
|
|
152
|
-
for await (const chunk of streamResult) {
|
|
153
|
-
if (chunk.type === "tool_call")
|
|
154
|
-
hasToolCall = true;
|
|
155
|
-
if (chunk.type === "text")
|
|
156
|
-
hasText = true;
|
|
157
|
-
if (chunk.type === "reasoning")
|
|
158
|
-
hasReasoning = true;
|
|
159
|
-
yield chunk;
|
|
160
|
-
}
|
|
161
|
-
// Стрим кончился на reasoning без текста/tool_call: модель ПОДУМАЛА, но
|
|
162
|
-
// ответа не дала (наблюдалось: qwen3.5-9b в LM Studio шлёт reasoning_content
|
|
163
|
-
// и встаёт). Reasoning уже отдан; недостающий ответ добираем non-stream.
|
|
164
|
-
// Reasoning-чанк фолбэка дропаем, чтобы не показать дважды — текста не было,
|
|
165
|
-
// риск дубля ответа отсутствует.
|
|
166
|
-
if (!hasToolCall && !hasText) {
|
|
167
|
-
this.debug?.("LLM streaming produced no text/tool_call — falling back to non-streaming");
|
|
168
|
-
const fallback = await this.doNonStreaming(effectiveMessages, tools, signal, options);
|
|
169
|
-
this.debug?.("LLM non-streaming fallback result", { chunks: fallback.length });
|
|
170
|
-
for (const chunk of fallback) {
|
|
171
|
-
if (hasReasoning && chunk.type === "reasoning")
|
|
172
|
-
continue;
|
|
173
|
-
yield chunk;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/** Дописывает тег в последнее user-сообщение (исходящая копия, не в контекст). */
|
|
178
|
-
applyPromptTag(messages, tag) {
|
|
179
|
-
const copy = [...messages];
|
|
180
|
-
for (let i = copy.length - 1; i >= 0; i--) {
|
|
181
|
-
if (copy[i].role === "user") {
|
|
182
|
-
const msg = { ...copy[i] };
|
|
183
|
-
if (typeof msg.content === "string") {
|
|
184
|
-
msg.content = `${msg.content}\n${tag}`;
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
// Массив частей — дописываем текстовую часть
|
|
188
|
-
msg.content = [...msg.content, { type: "text", text: tag }];
|
|
189
|
-
}
|
|
190
|
-
copy[i] = msg;
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return copy;
|
|
195
|
-
}
|
|
196
|
-
async *doStream(messages, tools, signal, options) {
|
|
197
|
-
const { baseDelay, maxDelay, maxStreamRetries, noDataTimeoutMs } = this.retryConfig;
|
|
198
|
-
const streamRetries = maxStreamRetries ?? 2;
|
|
199
|
-
const idleTimeoutMs = noDataTimeoutMs ?? 180000;
|
|
200
|
-
let usedNonStreamingFallback = false;
|
|
201
|
-
for (let attempt = 0;; attempt++) {
|
|
202
|
-
let emitted = false;
|
|
203
|
-
const onEmit = () => {
|
|
204
|
-
emitted = true;
|
|
205
|
-
};
|
|
206
|
-
try {
|
|
207
|
-
const sawDone = yield* this.streamOnce(messages, tools, signal, options, onEmit, idleTimeoutMs);
|
|
208
|
-
// Чистый [DONE] или частичный контент — отдаём что есть.
|
|
209
|
-
if (sawDone || emitted)
|
|
210
|
-
return;
|
|
211
|
-
// Стрим закрылся до [DONE] с нулевым контентом — ретраябельная обрезка.
|
|
212
|
-
if (attempt >= streamRetries)
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
if (err?.name === "AbortError" || (err instanceof LlmError && err.terminal) || signal?.aborted)
|
|
217
|
-
throw err;
|
|
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
|
-
// Контент уже отдан — перестрим продублировал бы чанки.
|
|
236
|
-
if (emitted || attempt >= streamRetries)
|
|
237
|
-
throw err;
|
|
238
|
-
// Обрыв до любого контента — переспрашиваем целиком.
|
|
239
|
-
}
|
|
240
|
-
// Экспоненциальный backoff с джиттером (общий хелпер из src/utils).
|
|
241
|
-
await this.sleep(backoffDelay(attempt, baseDelay, maxDelay, 0.1), signal);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
async *streamOnce(messages, tools, signal, options, onEmit, idleTimeoutMs) {
|
|
245
|
-
const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
|
|
246
|
-
const body = buildRequestBody({
|
|
247
|
-
model: this.model,
|
|
248
|
-
messages,
|
|
249
|
-
tools,
|
|
250
|
-
stream: true,
|
|
251
|
-
maxTokens,
|
|
252
|
-
reasoningEffort: options?.reasoningEffort,
|
|
253
|
-
reasoningStrategy: options?.reasoningStrategy,
|
|
254
|
-
...this.cacheHints(),
|
|
255
|
-
});
|
|
256
|
-
this.debug?.("LLM stream request", {
|
|
257
|
-
baseUrl: this.config.baseUrl,
|
|
258
|
-
model: this.model,
|
|
259
|
-
stream: true,
|
|
260
|
-
maxTokens,
|
|
261
|
-
toolsCount: tools?.length ?? 0,
|
|
262
|
-
reasoningEffort: options?.reasoningEffort,
|
|
263
|
-
reasoningStrategy: options?.reasoningStrategy,
|
|
264
|
-
messagesCount: messages.length,
|
|
265
|
-
});
|
|
266
|
-
const { headers, abortSignal, cleanup, isTimeout, flagTimeout, controller } = this.buildRequestSetup(signal);
|
|
267
|
-
let response;
|
|
268
|
-
try {
|
|
269
|
-
response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
|
|
270
|
-
method: "POST",
|
|
271
|
-
headers,
|
|
272
|
-
body: JSON.stringify(body),
|
|
273
|
-
signal: abortSignal,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
// Fetch-сбои уже HTTP-ретраятся внутри fetchWithRetry —
|
|
278
|
-
// на уровне стрима повторно не ретраим.
|
|
279
|
-
if (err?.name === "AbortError")
|
|
280
|
-
throw err;
|
|
281
|
-
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
282
|
-
if (wrapped instanceof LlmError) {
|
|
283
|
-
wrapped.markTerminal();
|
|
284
|
-
throw wrapped;
|
|
285
|
-
}
|
|
286
|
-
throw new LlmError(wrapped.message).markTerminal();
|
|
287
|
-
}
|
|
288
|
-
if (!response.ok) {
|
|
289
|
-
cleanup();
|
|
290
|
-
const errorText = await response.text();
|
|
291
|
-
throw new LlmError(t("error.llm_api", {
|
|
292
|
-
status: response.status,
|
|
293
|
-
statusText: response.statusText,
|
|
294
|
-
errorText,
|
|
295
|
-
}), { terminal: true });
|
|
296
|
-
}
|
|
297
|
-
const contentType = response.headers?.get?.("content-type") ?? "";
|
|
298
|
-
this.debug?.("LLM stream response headers", {
|
|
299
|
-
status: response.status,
|
|
300
|
-
contentType,
|
|
301
|
-
});
|
|
302
|
-
const reader = response.body?.getReader();
|
|
303
|
-
if (!reader) {
|
|
304
|
-
cleanup();
|
|
305
|
-
throw new Error(t("error.no_response_body"));
|
|
306
|
-
}
|
|
307
|
-
const decoder = new TextDecoder();
|
|
308
|
-
let buffer = "";
|
|
309
|
-
const toolCallAccs = new Map();
|
|
310
|
-
// Единый контейнер состояния стрима (вместо 9 разрозненных флагов).
|
|
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
|
-
};
|
|
320
|
-
const readIdle = () => new Promise((resolve, reject) => {
|
|
321
|
-
const idleTimer = setTimeout(() => {
|
|
322
|
-
flagTimeout();
|
|
323
|
-
controller.abort();
|
|
324
|
-
}, idleTimeoutMs);
|
|
325
|
-
reader.read().then((result) => {
|
|
326
|
-
clearTimeout(idleTimer);
|
|
327
|
-
if (isTimeout())
|
|
328
|
-
reject(idleError());
|
|
329
|
-
else
|
|
330
|
-
resolve(result);
|
|
331
|
-
}, (err) => {
|
|
332
|
-
clearTimeout(idleTimer);
|
|
333
|
-
if (isTimeout())
|
|
334
|
-
reject(idleError());
|
|
335
|
-
else
|
|
336
|
-
reject(err);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
try {
|
|
340
|
-
while (true) {
|
|
341
|
-
const { done, value } = await readIdle();
|
|
342
|
-
if (done)
|
|
343
|
-
break;
|
|
344
|
-
buffer += decoder.decode(value, { stream: true });
|
|
345
|
-
const lines = buffer.split("\n");
|
|
346
|
-
buffer = lines.pop() || "";
|
|
347
|
-
for (const line of lines) {
|
|
348
|
-
const trimmed = line.trim();
|
|
349
|
-
if (!trimmed)
|
|
350
|
-
continue;
|
|
351
|
-
let data;
|
|
352
|
-
if (trimmed.startsWith("data: ")) {
|
|
353
|
-
data = trimmed.slice(6);
|
|
354
|
-
if (data === "[DONE]") {
|
|
355
|
-
st.sawDone = true;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
// Не SSE-фрейм — Ollama (и часть бэкендов) стримят NDJSON или шлют
|
|
361
|
-
// mid-stream ошибки голым JSON. Пробуем парсить сырую строку;
|
|
362
|
-
// непарсящийся шум (SSE-комменты, keep-alive) считаем и пропускаем.
|
|
363
|
-
st.nonDataLines++;
|
|
364
|
-
if (st.nonDataSamples.length < 5)
|
|
365
|
-
st.nonDataSamples.push(trimmed.slice(0, 200));
|
|
366
|
-
data = trimmed;
|
|
367
|
-
}
|
|
368
|
-
try {
|
|
369
|
-
const parsed = JSON.parse(data);
|
|
370
|
-
st.parsedChunks++;
|
|
371
|
-
const choice = parsed.choices?.[0];
|
|
372
|
-
if (!choice) {
|
|
373
|
-
// Usage приходит в последнем чанке с пустыми choices
|
|
374
|
-
if (parsed.usage) {
|
|
375
|
-
st.usage = {
|
|
376
|
-
promptTokens: parsed.usage.prompt_tokens ?? 0,
|
|
377
|
-
completionTokens: parsed.usage.completion_tokens ?? 0,
|
|
378
|
-
totalTokens: parsed.usage.total_tokens ?? 0,
|
|
379
|
-
cache: parseCacheUsage(parsed.usage, this.cacheReport),
|
|
380
|
-
};
|
|
381
|
-
this.debug?.("LLM stream usage", { ...st.usage });
|
|
382
|
-
}
|
|
383
|
-
else if (parsed.error) {
|
|
384
|
-
// Mid-stream ошибка провайдера (Ollama шлёт {"error": "..."}
|
|
385
|
-
// голым JSON после старта генерации). Показываем — глотание
|
|
386
|
-
// даст молча пустой ответ.
|
|
387
|
-
// Брошено ВНЕ try/catch JSON-парсинга ниже.
|
|
388
|
-
st.midStreamError = String(parsed.error);
|
|
389
|
-
}
|
|
390
|
-
// NOTE: `continue` тут нет — он пропустил бы throw midStreamError
|
|
391
|
-
// ниже (он вне этого try/catch).
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
if (st.parsedChunks <= 3 || st.parsedChunks % 50 === 0) {
|
|
395
|
-
const d = choice.delta ?? {};
|
|
396
|
-
this.debug?.("LLM stream chunk", {
|
|
397
|
-
n: st.parsedChunks,
|
|
398
|
-
finishReason: choice.finish_reason ?? null,
|
|
399
|
-
deltaKeys: Object.keys(d),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
const delta = choice.delta || {};
|
|
403
|
-
const finishReason = choice.finish_reason;
|
|
404
|
-
if (finishReason)
|
|
405
|
-
st.lastFinishReason = finishReason;
|
|
406
|
-
// `reasoning` — алиас Ollama для openai-`reasoning_content`.
|
|
407
|
-
const reasoningDelta = delta.reasoning_content ?? delta.reasoning;
|
|
408
|
-
if (reasoningDelta) {
|
|
409
|
-
onEmit();
|
|
410
|
-
yield { type: "reasoning", content: reasoningDelta };
|
|
411
|
-
}
|
|
412
|
-
if (delta.tool_calls) {
|
|
413
|
-
st.sawToolCallStart = true;
|
|
414
|
-
for (const tc of delta.tool_calls) {
|
|
415
|
-
const idx = tc.index ?? 0;
|
|
416
|
-
if (!toolCallAccs.has(idx)) {
|
|
417
|
-
toolCallAccs.set(idx, { id: "", name: "", arguments: "" });
|
|
418
|
-
}
|
|
419
|
-
const acc = toolCallAccs.get(idx);
|
|
420
|
-
if (tc.id)
|
|
421
|
-
acc.id = tc.id;
|
|
422
|
-
if (tc.function?.name)
|
|
423
|
-
acc.name = tc.function.name;
|
|
424
|
-
if (tc.function?.arguments) {
|
|
425
|
-
acc.arguments += tc.function.arguments;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (delta.content) {
|
|
430
|
-
onEmit();
|
|
431
|
-
st.sawText = true;
|
|
432
|
-
yield { type: "text", content: delta.content };
|
|
433
|
-
}
|
|
434
|
-
if (finishReason === "tool_calls" && toolCallAccs.size > 0) {
|
|
435
|
-
for (const [, acc] of toolCallAccs) {
|
|
436
|
-
if (acc.name) {
|
|
437
|
-
onEmit();
|
|
438
|
-
yield {
|
|
439
|
-
type: "tool_call",
|
|
440
|
-
toolCall: {
|
|
441
|
-
id: acc.id,
|
|
442
|
-
name: acc.name,
|
|
443
|
-
arguments: acc.arguments || "{}",
|
|
444
|
-
},
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
toolCallAccs.clear();
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
// Битые JSON-строки пропускаем
|
|
454
|
-
}
|
|
455
|
-
if (st.midStreamError) {
|
|
456
|
-
throw new Error(t("error.llm_provider_stream_error", { error: st.midStreamError.slice(0, 300) }));
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (st.usage) {
|
|
461
|
-
onEmit();
|
|
462
|
-
yield { type: "done", usage: st.usage };
|
|
463
|
-
}
|
|
464
|
-
// finish_reason "length": completion упёрся в лимит токенов. Незаконченный
|
|
465
|
-
// tool_call иначе молча глотается (tool_calls отдаются только на
|
|
466
|
-
// finish_reason "tool_calls") и всплывает пустым ответом → слепые
|
|
467
|
-
// hallucination-ретраи. Показываем настоящую причину.
|
|
468
|
-
if (st.lastFinishReason === "length") {
|
|
469
|
-
const truncatedToolCall = st.sawToolCallStart && toolCallAccs.size > 0;
|
|
470
|
-
if (truncatedToolCall || !st.sawText) {
|
|
471
|
-
// Провайдер в порядке — модель просто превысила completion-лимит.
|
|
472
|
-
// terminal: стрим не ретраим (он завершился корректно); recoverable:
|
|
473
|
-
// агентный цикл вернёт причину модели, чтобы она адаптировалась
|
|
474
|
-
// (разбила вывод) вместо смерти сессии.
|
|
475
|
-
throw new LlmError(t(truncatedToolCall ? "error.llm_truncated_toolcall" : "error.llm_truncated", {
|
|
476
|
-
tokens: maxTokens,
|
|
477
|
-
}), { terminal: true, recoverable: true });
|
|
478
|
-
}
|
|
479
|
-
// Текст уже устримлен и не завершён — не глушим ответ, но и не
|
|
480
|
-
// принимаем обрезок молча: отдаём видимое предупреждение.
|
|
481
|
-
yield {
|
|
482
|
-
type: "warning",
|
|
483
|
-
content: t("error.llm_output_truncated", { tokens: maxTokens }),
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
finally {
|
|
488
|
-
cleanup();
|
|
489
|
-
reader.releaseLock();
|
|
490
|
-
this.debug?.("LLM stream finished", st.snapshot());
|
|
491
|
-
}
|
|
492
|
-
return st.sawDone;
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Вычисляет запросные подсказки кеша из возможностей провайдера и текущего
|
|
496
|
-
* session id. Без capability возвращает «ничего», сохраняя прежнее тело.
|
|
497
|
-
*/
|
|
498
|
-
cacheHints() {
|
|
499
|
-
const sessionId = this.getSessionId?.();
|
|
500
|
-
return {
|
|
501
|
-
cachePrompt: this.cache?.requestCachePrompt === true,
|
|
502
|
-
promptCacheKey: this.cache?.requestPromptCacheKey ? sessionId : undefined,
|
|
503
|
-
sessionId: this.cache?.requestSessionId ? sessionId : undefined,
|
|
504
|
-
streamUsage: this.cache?.requestStreamUsage === true,
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
/** Общая подготовка запроса: auth-заголовки + таймаут/abort-контроллер. */
|
|
508
|
-
buildRequestSetup(signal) {
|
|
509
|
-
const headers = {
|
|
510
|
-
"Content-Type": "application/json",
|
|
511
|
-
"User-Agent": this.userAgent,
|
|
512
|
-
};
|
|
513
|
-
if (this.config.apiKey && this.config.apiKey !== "not-needed") {
|
|
514
|
-
headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
515
|
-
}
|
|
516
|
-
const sessionId = this.getSessionId?.();
|
|
517
|
-
// Когда capability задана — шлём заголовок только тем, кому он нужен
|
|
518
|
-
// (Zen/Go). Без capability сохраняем легаси-поведение.
|
|
519
|
-
const sendSessionHeader = this.cache ? this.cache.sessionHeader : true;
|
|
520
|
-
if (sessionId && sendSessionHeader)
|
|
521
|
-
headers["x-opencode-session"] = sessionId;
|
|
522
|
-
const controller = new AbortController();
|
|
523
|
-
let timedOut = false;
|
|
524
|
-
const timeoutId = setTimeout(() => {
|
|
525
|
-
timedOut = true;
|
|
526
|
-
controller.abort();
|
|
527
|
-
}, REQUEST_TIMEOUT_MS);
|
|
528
|
-
const abortSignal = (() => {
|
|
529
|
-
if (!signal)
|
|
530
|
-
return controller.signal;
|
|
531
|
-
try {
|
|
532
|
-
return AbortSignal.any([controller.signal, signal]);
|
|
533
|
-
}
|
|
534
|
-
catch {
|
|
535
|
-
signal.addEventListener("abort", () => controller.abort(), {
|
|
536
|
-
once: true,
|
|
537
|
-
});
|
|
538
|
-
return controller.signal;
|
|
539
|
-
}
|
|
540
|
-
})();
|
|
541
|
-
return {
|
|
542
|
-
headers,
|
|
543
|
-
abortSignal,
|
|
544
|
-
cleanup: () => clearTimeout(timeoutId),
|
|
545
|
-
isTimeout: () => timedOut,
|
|
546
|
-
flagTimeout: () => {
|
|
547
|
-
timedOut = true;
|
|
548
|
-
},
|
|
549
|
-
controller,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
async doNonStreaming(messages, tools, signal, options) {
|
|
553
|
-
const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
|
|
554
|
-
const body = buildRequestBody({
|
|
555
|
-
model: this.model,
|
|
556
|
-
messages,
|
|
557
|
-
tools,
|
|
558
|
-
stream: false,
|
|
559
|
-
maxTokens,
|
|
560
|
-
reasoningEffort: options?.reasoningEffort,
|
|
561
|
-
reasoningStrategy: options?.reasoningStrategy,
|
|
562
|
-
...this.cacheHints(),
|
|
563
|
-
});
|
|
564
|
-
const { headers, abortSignal, cleanup, isTimeout } = this.buildRequestSetup(signal);
|
|
565
|
-
try {
|
|
566
|
-
const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
|
|
567
|
-
method: "POST",
|
|
568
|
-
headers,
|
|
569
|
-
body: JSON.stringify(body),
|
|
570
|
-
signal: abortSignal,
|
|
571
|
-
});
|
|
572
|
-
if (!response.ok) {
|
|
573
|
-
const errorText = await response.text();
|
|
574
|
-
throw new Error(t("error.llm_api", {
|
|
575
|
-
status: response.status,
|
|
576
|
-
statusText: response.statusText,
|
|
577
|
-
errorText: errorText.slice(0, 500),
|
|
578
|
-
}));
|
|
579
|
-
}
|
|
580
|
-
const data = await response.json();
|
|
581
|
-
// Часть бэкендов (напр. Ollama) отдают HTTP 200 с {"error": "..."}.
|
|
582
|
-
if (data.error && !data.choices) {
|
|
583
|
-
throw new Error(t("error.llm_provider_stream_error", { error: String(data.error).slice(0, 300) }));
|
|
584
|
-
}
|
|
585
|
-
const choice = data.choices?.[0];
|
|
586
|
-
if (!choice) {
|
|
587
|
-
return [];
|
|
588
|
-
}
|
|
589
|
-
const msg = choice.message || {};
|
|
590
|
-
const chunks = [];
|
|
591
|
-
const reasoning = msg.reasoning_content ?? msg.reasoning;
|
|
592
|
-
if (reasoning) {
|
|
593
|
-
chunks.push({ type: "reasoning", content: reasoning });
|
|
594
|
-
}
|
|
595
|
-
if (msg.content) {
|
|
596
|
-
chunks.push({ type: "text", content: msg.content });
|
|
597
|
-
}
|
|
598
|
-
if (msg.tool_calls) {
|
|
599
|
-
for (const tc of msg.tool_calls) {
|
|
600
|
-
chunks.push({
|
|
601
|
-
type: "tool_call",
|
|
602
|
-
toolCall: {
|
|
603
|
-
id: tc.id || "",
|
|
604
|
-
name: tc.function?.name || "",
|
|
605
|
-
arguments: tc.function?.arguments || "{}",
|
|
606
|
-
},
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
// Текст готов, но упёрся в cap: видимый warning вместо молча
|
|
611
|
-
// принятого обрезанного ответа (см. стрим-путь).
|
|
612
|
-
if (choice.finish_reason === "length" && msg.content && !msg.tool_calls) {
|
|
613
|
-
chunks.push({
|
|
614
|
-
type: "warning",
|
|
615
|
-
content: t("error.llm_output_truncated", { tokens: maxTokens }),
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
// Дописываем usage из ответа API
|
|
619
|
-
if (data.usage) {
|
|
620
|
-
chunks.push({
|
|
621
|
-
type: "done",
|
|
622
|
-
usage: {
|
|
623
|
-
promptTokens: data.usage.prompt_tokens ?? 0,
|
|
624
|
-
completionTokens: data.usage.completion_tokens ?? 0,
|
|
625
|
-
totalTokens: data.usage.total_tokens ?? 0,
|
|
626
|
-
cache: parseCacheUsage(data.usage, this.cacheReport),
|
|
627
|
-
},
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
return chunks;
|
|
631
|
-
}
|
|
632
|
-
catch (err) {
|
|
633
|
-
if (isTimeout() && err?.name === "AbortError") {
|
|
634
|
-
throw new Error(t("error.llm_timeout", { timeout: REQUEST_TIMEOUT_MS }));
|
|
635
|
-
}
|
|
636
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
637
|
-
}
|
|
638
|
-
finally {
|
|
639
|
-
cleanup();
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Считает токены текста (tiktoken по дефолтной модели).
|
|
644
|
-
* @param text текст; @returns число токенов.
|
|
645
|
-
*/
|
|
646
|
-
countTokens(text) {
|
|
647
|
-
return this.tokenCounter.count(text);
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Список моделей провайдера (`/models`: id/name/model).
|
|
651
|
-
* @returns имена моделей; @throws при недоступности провайдера.
|
|
652
|
-
*/
|
|
653
|
-
async listModels() {
|
|
654
|
-
try {
|
|
655
|
-
const url = `${this.config.baseUrl.replace(/\/+$/, "")}/models`;
|
|
656
|
-
const headers = {
|
|
657
|
-
"Content-Type": "application/json",
|
|
658
|
-
"User-Agent": this.userAgent,
|
|
659
|
-
};
|
|
660
|
-
if (this.config.apiKey && this.config.apiKey !== "not-needed") {
|
|
661
|
-
headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
662
|
-
}
|
|
663
|
-
const response = await fetch(url, {
|
|
664
|
-
method: "GET",
|
|
665
|
-
headers,
|
|
666
|
-
});
|
|
667
|
-
if (!response.ok) {
|
|
668
|
-
// Провал наружу: health-пробы должны отличать мёртвый провайдер
|
|
669
|
-
// от пустого списка моделей.
|
|
670
|
-
throw new Error(`HTTP ${response.status} from ${url}`);
|
|
671
|
-
}
|
|
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
|
-
})
|
|
690
|
-
.filter(Boolean);
|
|
691
|
-
return models;
|
|
692
|
-
}
|
|
693
|
-
catch (err) {
|
|
694
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
async fetchWithRetry(url, init) {
|
|
698
|
-
const { maxRetries, baseDelay, maxDelay } = this.retryConfig;
|
|
699
|
-
let lastStatus = 0;
|
|
700
|
-
let lastRetryAfter = 0;
|
|
701
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
702
|
-
lastRetryAfter = 0;
|
|
703
|
-
try {
|
|
704
|
-
const response = await fetch(url, init);
|
|
705
|
-
if (!this.isRetryable(response.status))
|
|
706
|
-
return response;
|
|
707
|
-
lastStatus = response.status;
|
|
708
|
-
const retryAfter = Number(response.headers.get("retry-after") ?? 0);
|
|
709
|
-
lastRetryAfter = retryAfter > 0 ? retryAfter * 1000 : 0;
|
|
710
|
-
}
|
|
711
|
-
catch (err) {
|
|
712
|
-
if (err.name === "AbortError") {
|
|
713
|
-
throw err;
|
|
714
|
-
}
|
|
715
|
-
if (attempt === maxRetries)
|
|
716
|
-
throw err;
|
|
717
|
-
}
|
|
718
|
-
if (attempt < maxRetries) {
|
|
719
|
-
// Приоритет окну Retry-After провайдера (cap на maxDelay); иначе —
|
|
720
|
-
// экспоненциальный backoff. 429 с free-тарифов требуют реального ожидания.
|
|
721
|
-
const delay = lastRetryAfter > 0
|
|
722
|
-
? Math.min(lastRetryAfter, maxDelay)
|
|
723
|
-
: backoffDelay(attempt, baseDelay, maxDelay, 0.1);
|
|
724
|
-
await this.sleep(delay, init.signal ?? undefined);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
if (lastStatus === 429) {
|
|
728
|
-
throw new LlmError(t("error.llm_429", {
|
|
729
|
-
model: this.model,
|
|
730
|
-
baseUrl: this.config.baseUrl,
|
|
731
|
-
}), { llmStatus: lastStatus });
|
|
732
|
-
}
|
|
733
|
-
throw new LlmError(t("error.llm_retries"), { llmStatus: lastStatus });
|
|
734
|
-
}
|
|
735
|
-
isRetryable(status) {
|
|
736
|
-
return status === 429 || status >= 500;
|
|
737
|
-
}
|
|
738
|
-
sleep(ms, signal) {
|
|
739
|
-
return new Promise((resolve, reject) => {
|
|
740
|
-
if (signal?.aborted) {
|
|
741
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
let timer;
|
|
745
|
-
const onAbort = () => {
|
|
746
|
-
clearTimeout(timer);
|
|
747
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
748
|
-
};
|
|
749
|
-
timer = setTimeout(() => {
|
|
750
|
-
signal?.removeEventListener("abort", onAbort);
|
|
751
|
-
resolve();
|
|
752
|
-
}, ms);
|
|
753
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
}
|