@f5-sales-demo/xcsh 19.51.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 +7073 -0
- package/README.md +14 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +109 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/todo/index.ts +206 -0
- package/examples/extensions/README.md +143 -0
- package/examples/extensions/api-demo.ts +89 -0
- package/examples/extensions/chalk-logger.ts +25 -0
- package/examples/extensions/hello.ts +32 -0
- package/examples/extensions/pirate.ts +43 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/reload-runtime.ts +37 -0
- package/examples/extensions/todo.ts +296 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +48 -0
- package/examples/hooks/confirm-destructive.ts +58 -0
- package/examples/hooks/custom-compaction.ts +115 -0
- package/examples/hooks/dirty-repo-guard.ts +51 -0
- package/examples/hooks/file-trigger.ts +40 -0
- package/examples/hooks/git-checkpoint.ts +52 -0
- package/examples/hooks/handoff.ts +149 -0
- package/examples/hooks/permission-gate.ts +33 -0
- package/examples/hooks/protected-paths.ts +29 -0
- package/examples/hooks/qna.ts +118 -0
- package/examples/hooks/status-line.ts +39 -0
- package/examples/sdk/01-minimal.ts +21 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +43 -0
- package/examples/sdk/04-skills.ts +43 -0
- package/examples/sdk/06-extensions.ts +80 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +35 -0
- package/examples/sdk/08-prompt-templates.ts +41 -0
- package/examples/sdk/08-slash-commands.ts +46 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
- package/examples/sdk/11-sessions.ts +47 -0
- package/examples/sdk/README.md +172 -0
- package/package.json +539 -0
- package/scripts/build-info/resolvers.ts +51 -0
- package/scripts/capture-ax-fixture.ts +84 -0
- package/scripts/capture-login-fixture.ts +30 -0
- package/scripts/check-workflow-field-coverage.ts +101 -0
- package/scripts/extension-uat-harness.ts +340 -0
- package/scripts/format-prompts.ts +68 -0
- package/scripts/generate-api-spec-index.ts +580 -0
- package/scripts/generate-branding-index.ts +81 -0
- package/scripts/generate-build-info.ts +106 -0
- package/scripts/generate-console-catalog.ts +88 -0
- package/scripts/generate-console-field-metadata.ts +74 -0
- package/scripts/generate-docs-index.ts +40 -0
- package/scripts/generate-template.ts +32 -0
- package/scripts/generate-terraform-index.ts +81 -0
- package/scripts/mermaid-gallery.ts +132 -0
- package/scripts/uat-matrix-modalities.ts +451 -0
- package/scripts/uat-matrix-report.ts +175 -0
- package/scripts/uat-matrix.ts +353 -0
- package/src/async/index.ts +2 -0
- package/src/async/job-manager.ts +416 -0
- package/src/async/support.ts +5 -0
- package/src/autoresearch/apply-contract-to-state.ts +1 -0
- package/src/autoresearch/command-resume.md +17 -0
- package/src/autoresearch/contract.ts +205 -0
- package/src/autoresearch/dashboard.ts +374 -0
- package/src/autoresearch/git.ts +191 -0
- package/src/autoresearch/helpers.ts +328 -0
- package/src/autoresearch/index.ts +402 -0
- package/src/autoresearch/prompt.md +236 -0
- package/src/autoresearch/resume-message.md +16 -0
- package/src/autoresearch/state.ts +303 -0
- package/src/autoresearch/tools/init-experiment.ts +400 -0
- package/src/autoresearch/tools/log-experiment.ts +793 -0
- package/src/autoresearch/tools/run-experiment.ts +679 -0
- package/src/autoresearch/types.ts +114 -0
- package/src/browser/acquire.ts +249 -0
- package/src/browser/actions.ts +126 -0
- package/src/browser/auth.ts +176 -0
- package/src/browser/ax.ts +134 -0
- package/src/browser/cdp-core.ts +45 -0
- package/src/browser/cdp-page-actions.ts +51 -0
- package/src/browser/chrome-locate.ts +66 -0
- package/src/browser/dom-context.ts +89 -0
- package/src/browser/dt-context.ts +100 -0
- package/src/browser/extension-bridge.ts +182 -0
- package/src/browser/extension-page-actions.ts +397 -0
- package/src/browser/extension-provider.ts +373 -0
- package/src/browser/index.ts +17 -0
- package/src/browser/input-commit.ts +74 -0
- package/src/browser/native-messaging.ts +20 -0
- package/src/browser/page-actions.ts +18 -0
- package/src/browser/provider.ts +155 -0
- package/src/browser/resolver.ts +126 -0
- package/src/browser/selector.ts +40 -0
- package/src/bun-imports.d.ts +22 -0
- package/src/capability/context-file.ts +43 -0
- package/src/capability/extension-module.ts +34 -0
- package/src/capability/extension.ts +47 -0
- package/src/capability/fs.ts +107 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +438 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +74 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +224 -0
- package/src/capability/settings.ts +34 -0
- package/src/capability/skill.ts +50 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/ssh.ts +41 -0
- package/src/capability/system-prompt.ts +34 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +168 -0
- package/src/cli/agents-cli.ts +138 -0
- package/src/cli/args.ts +286 -0
- package/src/cli/chrome-cli.ts +88 -0
- package/src/cli/classify-install-target.ts +50 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/config-cli.ts +418 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/grep-cli.ts +160 -0
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/initial-message.ts +58 -0
- package/src/cli/jupyter-cli.ts +106 -0
- package/src/cli/list-models.ts +128 -0
- package/src/cli/plugin-cli.ts +964 -0
- package/src/cli/read-cli.ts +68 -0
- package/src/cli/session-picker.ts +52 -0
- package/src/cli/setup-cli.ts +479 -0
- package/src/cli/shell-cli.ts +174 -0
- package/src/cli/ssh-cli.ts +179 -0
- package/src/cli/stats-cli.ts +182 -0
- package/src/cli/update-cli.ts +446 -0
- package/src/cli/web-search-cli.ts +203 -0
- package/src/cli.ts +123 -0
- package/src/commands/agents.ts +57 -0
- package/src/commands/chrome-host.ts +71 -0
- package/src/commands/chrome.ts +28 -0
- package/src/commands/commit.ts +36 -0
- package/src/commands/config.ts +51 -0
- package/src/commands/grep.ts +48 -0
- package/src/commands/grievances.ts +20 -0
- package/src/commands/jupyter.ts +32 -0
- package/src/commands/launch.ts +144 -0
- package/src/commands/plugin.ts +78 -0
- package/src/commands/read.ts +33 -0
- package/src/commands/setup.ts +42 -0
- package/src/commands/shell.ts +29 -0
- package/src/commands/ssh.ts +60 -0
- package/src/commands/stats.ts +29 -0
- package/src/commands/update.ts +21 -0
- package/src/commands/web-search.ts +46 -0
- package/src/commit/agentic/agent.ts +314 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +354 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +25 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +38 -0
- package/src/commit/agentic/state.ts +60 -0
- package/src/commit/agentic/tools/analyze-file.ts +127 -0
- package/src/commit/agentic/tools/git-file-diff.ts +191 -0
- package/src/commit/agentic/tools/git-hunk.ts +50 -0
- package/src/commit/agentic/tools/git-overview.ts +81 -0
- package/src/commit/agentic/tools/index.ts +54 -0
- package/src/commit/agentic/tools/propose-changelog.ts +139 -0
- package/src/commit/agentic/tools/propose-commit.ts +122 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/schemas.ts +31 -0
- package/src/commit/agentic/tools/split-commit.ts +251 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +183 -0
- package/src/commit/analysis/conventional.ts +122 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +105 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +40 -0
- package/src/commit/changelog/generate.ts +101 -0
- package/src/commit/changelog/index.ts +234 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +85 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/index.ts +69 -0
- package/src/commit/map-reduce/map-phase.ts +193 -0
- package/src/commit/map-reduce/reduce-phase.ts +108 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +66 -0
- package/src/commit/pipeline.ts +242 -0
- package/src/commit/prompts/analysis-system.md +148 -0
- package/src/commit/prompts/analysis-user.md +38 -0
- package/src/commit/prompts/changelog-system.md +50 -0
- package/src/commit/prompts/changelog-user.md +18 -0
- package/src/commit/prompts/file-observer-system.md +24 -0
- package/src/commit/prompts/file-observer-user.md +8 -0
- package/src/commit/prompts/reduce-system.md +50 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +3 -0
- package/src/commit/prompts/summary-system.md +38 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/types.ts +118 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/commit/utils.ts +44 -0
- package/src/config/auto-config.ts +698 -0
- package/src/config/context-schema.json +115 -0
- package/src/config/file-lock.ts +121 -0
- package/src/config/keybindings.ts +493 -0
- package/src/config/mcp-schema.json +230 -0
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +2265 -0
- package/src/config/model-resolver.ts +1289 -0
- package/src/config/prompt-templates.ts +271 -0
- package/src/config/resolve-config-value.ts +110 -0
- package/src/config/settings-schema.ts +2155 -0
- package/src/config/settings.ts +730 -0
- package/src/config.ts +418 -0
- package/src/cursor.ts +323 -0
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/index.ts +440 -0
- package/src/debug/log-formatting.ts +58 -0
- package/src/debug/log-viewer.ts +908 -0
- package/src/debug/profiler.ts +158 -0
- package/src/debug/report-bundle.ts +365 -0
- package/src/debug/system-info.ts +107 -0
- package/src/discovery/agents-md.ts +67 -0
- package/src/discovery/agents.ts +219 -0
- package/src/discovery/builtin.ts +851 -0
- package/src/discovery/claude-plugins.ts +297 -0
- package/src/discovery/claude.ts +543 -0
- package/src/discovery/cline.ts +83 -0
- package/src/discovery/codex.ts +535 -0
- package/src/discovery/cursor.ts +220 -0
- package/src/discovery/gemini.ts +396 -0
- package/src/discovery/github.ts +118 -0
- package/src/discovery/helpers.ts +934 -0
- package/src/discovery/index.ts +79 -0
- package/src/discovery/language.ts +46 -0
- package/src/discovery/mcp-json.ts +171 -0
- package/src/discovery/opencode.ts +393 -0
- package/src/discovery/plugin-dir-roots.ts +28 -0
- package/src/discovery/ssh.ts +153 -0
- package/src/discovery/substitute-plugin-root.ts +29 -0
- package/src/discovery/vscode.ts +105 -0
- package/src/discovery/windsurf.ts +147 -0
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +481 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +762 -0
- package/src/edit/modes/hashline.ts +1366 -0
- package/src/edit/modes/patch.ts +1791 -0
- package/src/edit/modes/replace.ts +1115 -0
- package/src/edit/normalize.ts +375 -0
- package/src/edit/renderer.ts +627 -0
- package/src/exa/factory.ts +61 -0
- package/src/exa/index.ts +27 -0
- package/src/exa/mcp-client.ts +293 -0
- package/src/exa/render.ts +232 -0
- package/src/exa/researcher.ts +47 -0
- package/src/exa/search.ts +86 -0
- package/src/exa/types.ts +166 -0
- package/src/exa/websets.ts +247 -0
- package/src/exec/bash-executor.ts +332 -0
- package/src/exec/exec.ts +53 -0
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +48 -0
- package/src/export/custom-share.ts +65 -0
- package/src/export/html/index.ts +161 -0
- package/src/export/html/template.css +1001 -0
- package/src/export/html/template.generated.ts +2 -0
- package/src/export/html/template.html +46 -0
- package/src/export/html/template.js +1950 -0
- package/src/export/html/template.macro.ts +24 -0
- package/src/export/html/vendor/highlight.min.js +1213 -0
- package/src/export/html/vendor/marked.min.js +6 -0
- package/src/export/ttsr.ts +434 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +25 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +456 -0
- package/src/extensibility/custom-commands/index.ts +2 -0
- package/src/extensibility/custom-commands/loader.ts +236 -0
- package/src/extensibility/custom-commands/types.ts +111 -0
- package/src/extensibility/custom-tools/index.ts +7 -0
- package/src/extensibility/custom-tools/loader.ts +238 -0
- package/src/extensibility/custom-tools/types.ts +251 -0
- package/src/extensibility/custom-tools/wrapper.ts +47 -0
- package/src/extensibility/extensions/index.ts +15 -0
- package/src/extensibility/extensions/loader.ts +572 -0
- package/src/extensibility/extensions/runner.ts +848 -0
- package/src/extensibility/extensions/types.ts +1394 -0
- package/src/extensibility/extensions/wrapper.ts +191 -0
- package/src/extensibility/hooks/index.ts +5 -0
- package/src/extensibility/hooks/loader.ts +255 -0
- package/src/extensibility/hooks/runner.ts +425 -0
- package/src/extensibility/hooks/tool-wrapper.ts +108 -0
- package/src/extensibility/hooks/types.ts +826 -0
- package/src/extensibility/plugins/doctor.ts +66 -0
- package/src/extensibility/plugins/git-url.ts +281 -0
- package/src/extensibility/plugins/index.ts +9 -0
- package/src/extensibility/plugins/installer.ts +192 -0
- package/src/extensibility/plugins/loader.ts +267 -0
- package/src/extensibility/plugins/manager.ts +731 -0
- package/src/extensibility/plugins/marketplace/cache.ts +136 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +318 -0
- package/src/extensibility/plugins/marketplace/index.ts +6 -0
- package/src/extensibility/plugins/marketplace/manager.ts +781 -0
- package/src/extensibility/plugins/marketplace/prerequisites.ts +255 -0
- package/src/extensibility/plugins/marketplace/registry.ts +196 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
- package/src/extensibility/plugins/marketplace/types.ts +201 -0
- package/src/extensibility/plugins/parser.ts +105 -0
- package/src/extensibility/plugins/types.ts +194 -0
- package/src/extensibility/skills.ts +269 -0
- package/src/extensibility/slash-commands.ts +246 -0
- package/src/extensibility/tool-proxy.ts +25 -0
- package/src/extensibility/utils.ts +38 -0
- package/src/index.ts +61 -0
- package/src/internal-urls/agent-protocol.ts +136 -0
- package/src/internal-urls/api-catalog-resolve.ts +495 -0
- package/src/internal-urls/api-catalog-types.ts +94 -0
- package/src/internal-urls/api-spec-resolve.ts +1036 -0
- package/src/internal-urls/api-spec-types.ts +239 -0
- package/src/internal-urls/artifact-protocol.ts +97 -0
- package/src/internal-urls/branding-index.generated.ts +88 -0
- package/src/internal-urls/build-info-runtime.ts +302 -0
- package/src/internal-urls/build-info.generated.ts +33 -0
- package/src/internal-urls/computer-profile.ts +692 -0
- package/src/internal-urls/console-catalog-types.ts +21 -0
- package/src/internal-urls/console-catalog.generated.ts +532 -0
- package/src/internal-urls/console-field-metadata-types.ts +59 -0
- package/src/internal-urls/console-field-metadata.generated.ts +4841 -0
- package/src/internal-urls/console-resolve.ts +178 -0
- package/src/internal-urls/docs-index.generated.ts +698 -0
- package/src/internal-urls/extension-api.md +147 -0
- package/src/internal-urls/index.ts +46 -0
- package/src/internal-urls/jobs-protocol.ts +119 -0
- package/src/internal-urls/json-query.ts +126 -0
- package/src/internal-urls/local-protocol.ts +206 -0
- package/src/internal-urls/mcp-protocol.ts +156 -0
- package/src/internal-urls/memory-protocol.ts +133 -0
- package/src/internal-urls/parse.ts +72 -0
- package/src/internal-urls/profile-collectors.ts +106 -0
- package/src/internal-urls/router.ts +55 -0
- package/src/internal-urls/rule-protocol.ts +55 -0
- package/src/internal-urls/skill-protocol.ts +111 -0
- package/src/internal-urls/terraform-index.generated.ts +1852 -0
- package/src/internal-urls/terraform-resolve.ts +184 -0
- package/src/internal-urls/terraform-types.ts +45 -0
- package/src/internal-urls/types.ts +52 -0
- package/src/internal-urls/user-profile.ts +363 -0
- package/src/internal-urls/xcsh-protocol.ts +644 -0
- package/src/ipy/cancellation.ts +28 -0
- package/src/ipy/executor.ts +1155 -0
- package/src/ipy/gateway-coordinator.ts +424 -0
- package/src/ipy/kernel.ts +1091 -0
- package/src/ipy/modules.ts +144 -0
- package/src/ipy/prelude.py +849 -0
- package/src/ipy/prelude.ts +3 -0
- package/src/ipy/runtime.ts +221 -0
- package/src/locales/ar.json +512 -0
- package/src/locales/de.json +512 -0
- package/src/locales/en.json +549 -0
- package/src/locales/es.json +512 -0
- package/src/locales/fr.json +512 -0
- package/src/locales/hi.json +512 -0
- package/src/locales/index.ts +29 -0
- package/src/locales/it.json +512 -0
- package/src/locales/ja.json +512 -0
- package/src/locales/ko.json +512 -0
- package/src/locales/pt-br.json +512 -0
- package/src/locales/th.json +512 -0
- package/src/locales/zh-cn.json +512 -0
- package/src/locales/zh-tw.json +512 -0
- package/src/lsp/client.ts +941 -0
- package/src/lsp/clients/biome-client.ts +202 -0
- package/src/lsp/clients/index.ts +50 -0
- package/src/lsp/clients/lsp-linter-client.ts +93 -0
- package/src/lsp/clients/swiftlint-client.ts +120 -0
- package/src/lsp/config.ts +418 -0
- package/src/lsp/defaults.json +999 -0
- package/src/lsp/edits.ts +109 -0
- package/src/lsp/index.ts +1746 -0
- package/src/lsp/lspmux.ts +233 -0
- package/src/lsp/render.ts +692 -0
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +444 -0
- package/src/lsp/utils.ts +691 -0
- package/src/main.ts +944 -0
- package/src/mcp/client.ts +482 -0
- package/src/mcp/config-writer.ts +225 -0
- package/src/mcp/config.ts +365 -0
- package/src/mcp/discoverable-tool-metadata.ts +202 -0
- package/src/mcp/index.ts +29 -0
- package/src/mcp/json-rpc.ts +84 -0
- package/src/mcp/loader.ts +124 -0
- package/src/mcp/manager.ts +1152 -0
- package/src/mcp/oauth-discovery.ts +349 -0
- package/src/mcp/oauth-flow.ts +387 -0
- package/src/mcp/render.ts +157 -0
- package/src/mcp/smithery-auth.ts +104 -0
- package/src/mcp/smithery-connect.ts +145 -0
- package/src/mcp/smithery-registry.ts +477 -0
- package/src/mcp/tool-bridge.ts +417 -0
- package/src/mcp/tool-cache.ts +117 -0
- package/src/mcp/transports/http.ts +475 -0
- package/src/mcp/transports/index.ts +6 -0
- package/src/mcp/transports/stdio.ts +325 -0
- package/src/mcp/types.ts +423 -0
- package/src/memories/index.ts +1115 -0
- package/src/memories/storage.ts +577 -0
- package/src/modes/acp/acp-agent.ts +1361 -0
- package/src/modes/acp/acp-event-mapper.ts +538 -0
- package/src/modes/acp/acp-mode.ts +15 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/components/agent-dashboard.ts +1124 -0
- package/src/modes/components/assistant-message.ts +213 -0
- package/src/modes/components/bash-execution.ts +345 -0
- package/src/modes/components/bordered-loader.ts +41 -0
- package/src/modes/components/branch-summary-message.ts +45 -0
- package/src/modes/components/btw-panel.ts +103 -0
- package/src/modes/components/compaction-summary-message.ts +51 -0
- package/src/modes/components/context-add-wizard.ts +534 -0
- package/src/modes/components/countdown-timer.ts +75 -0
- package/src/modes/components/custom-editor.ts +238 -0
- package/src/modes/components/custom-message.ts +91 -0
- package/src/modes/components/diff.ts +210 -0
- package/src/modes/components/dynamic-border.ts +25 -0
- package/src/modes/components/extensions/extension-dashboard.ts +326 -0
- package/src/modes/components/extensions/extension-list.ts +492 -0
- package/src/modes/components/extensions/index.ts +9 -0
- package/src/modes/components/extensions/inspector-panel.ts +317 -0
- package/src/modes/components/extensions/state-manager.ts +587 -0
- package/src/modes/components/extensions/types.ts +191 -0
- package/src/modes/components/footer.ts +262 -0
- package/src/modes/components/gutter-block.ts +315 -0
- package/src/modes/components/history-search.ts +158 -0
- package/src/modes/components/hook-editor.ts +151 -0
- package/src/modes/components/hook-input.ts +81 -0
- package/src/modes/components/hook-message.ts +100 -0
- package/src/modes/components/hook-selector.ts +192 -0
- package/src/modes/components/index.ts +36 -0
- package/src/modes/components/keybinding-hints.ts +65 -0
- package/src/modes/components/login-dialog.ts +164 -0
- package/src/modes/components/mcp-add-wizard.ts +1314 -0
- package/src/modes/components/model-selector.ts +946 -0
- package/src/modes/components/oauth-selector.ts +211 -0
- package/src/modes/components/plugin-selector.ts +95 -0
- package/src/modes/components/plugin-settings.ts +477 -0
- package/src/modes/components/plugins/index.ts +5 -0
- package/src/modes/components/plugins/plugin-dashboard.ts +523 -0
- package/src/modes/components/plugins/plugin-inspector-pane.ts +84 -0
- package/src/modes/components/plugins/plugin-list-pane.ts +109 -0
- package/src/modes/components/plugins/state-manager.ts +229 -0
- package/src/modes/components/plugins/types.ts +51 -0
- package/src/modes/components/plugins/utils.ts +6 -0
- package/src/modes/components/python-execution.ts +244 -0
- package/src/modes/components/queue-mode-selector.ts +56 -0
- package/src/modes/components/read-tool-group.ts +137 -0
- package/src/modes/components/session-observer-overlay.ts +483 -0
- package/src/modes/components/session-selector.ts +343 -0
- package/src/modes/components/settings-defs.ts +568 -0
- package/src/modes/components/settings-selector.ts +641 -0
- package/src/modes/components/show-images-selector.ts +45 -0
- package/src/modes/components/skill-message.ts +90 -0
- package/src/modes/components/status-line/context-gradient.ts +64 -0
- package/src/modes/components/status-line/git-utils.ts +42 -0
- package/src/modes/components/status-line/hex-ansi.ts +29 -0
- package/src/modes/components/status-line/index.ts +4 -0
- package/src/modes/components/status-line/presets.ts +174 -0
- package/src/modes/components/status-line/segments.ts +494 -0
- package/src/modes/components/status-line/separators.ts +55 -0
- package/src/modes/components/status-line/token-rate.ts +66 -0
- package/src/modes/components/status-line/types.ts +96 -0
- package/src/modes/components/status-line-segment-editor.ts +361 -0
- package/src/modes/components/status-line.ts +729 -0
- package/src/modes/components/theme-selector.ts +63 -0
- package/src/modes/components/thinking-selector.ts +52 -0
- package/src/modes/components/todo-reminder.ts +54 -0
- package/src/modes/components/tool-execution.ts +781 -0
- package/src/modes/components/tree-selector.ts +908 -0
- package/src/modes/components/ttsr-notification.ts +80 -0
- package/src/modes/components/user-message-selector.ts +141 -0
- package/src/modes/components/user-message.ts +100 -0
- package/src/modes/components/visual-truncate.ts +63 -0
- package/src/modes/components/welcome-checks.ts +313 -0
- package/src/modes/components/welcome.ts +270 -0
- package/src/modes/controllers/btw-controller.ts +193 -0
- package/src/modes/controllers/chord-routing.ts +37 -0
- package/src/modes/controllers/command-controller.ts +1293 -0
- package/src/modes/controllers/context-command-controller.ts +61 -0
- package/src/modes/controllers/event-controller.ts +743 -0
- package/src/modes/controllers/extension-ui-controller.ts +983 -0
- package/src/modes/controllers/input-controller.ts +877 -0
- package/src/modes/controllers/mcp-command-controller.ts +1940 -0
- package/src/modes/controllers/selector-controller.ts +1292 -0
- package/src/modes/controllers/ssh-command-controller.ts +421 -0
- package/src/modes/index.ts +34 -0
- package/src/modes/interactive-mode.ts +1781 -0
- package/src/modes/oauth-manual-input.ts +42 -0
- package/src/modes/print-mode.ts +196 -0
- package/src/modes/prompt-action-autocomplete.ts +231 -0
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +762 -0
- package/src/modes/rpc/rpc-mode.ts +877 -0
- package/src/modes/rpc/rpc-types.ts +322 -0
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +30 -0
- package/src/modes/theme/defaults/index.ts +7 -0
- package/src/modes/theme/defaults/xcsh-dark.json +113 -0
- package/src/modes/theme/defaults/xcsh-light.json +115 -0
- package/src/modes/theme/mermaid-cache.ts +151 -0
- package/src/modes/theme/mermaid-palette.ts +39 -0
- package/src/modes/theme/theme-schema.json +553 -0
- package/src/modes/theme/theme.ts +2563 -0
- package/src/modes/types.ts +273 -0
- package/src/modes/utils/hotkeys-markdown.ts +60 -0
- package/src/modes/utils/keybinding-matchers.ts +30 -0
- package/src/modes/utils/read-group-outcome-aggregator.ts +55 -0
- package/src/modes/utils/sanitize-error-message.ts +60 -0
- package/src/modes/utils/tools-markdown.ts +28 -0
- package/src/modes/utils/ui-helpers.ts +734 -0
- package/src/plan-mode/approved-plan.ts +55 -0
- package/src/plan-mode/state.ts +6 -0
- package/src/priority.json +37 -0
- package/src/prompts/agents/designer.md +67 -0
- package/src/prompts/agents/explore.md +61 -0
- package/src/prompts/agents/frontmatter.md +10 -0
- package/src/prompts/agents/init.md +36 -0
- package/src/prompts/agents/librarian.md +121 -0
- package/src/prompts/agents/plan.md +49 -0
- package/src/prompts/agents/reviewer.md +127 -0
- package/src/prompts/agents/task.md +16 -0
- package/src/prompts/ci-green-request.md +35 -0
- package/src/prompts/compaction/branch-summary-context.md +5 -0
- package/src/prompts/compaction/branch-summary-preamble.md +2 -0
- package/src/prompts/compaction/branch-summary.md +30 -0
- package/src/prompts/compaction/compaction-short-summary.md +9 -0
- package/src/prompts/compaction/compaction-summary-context.md +5 -0
- package/src/prompts/compaction/compaction-summary.md +38 -0
- package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
- package/src/prompts/compaction/compaction-update-summary.md +45 -0
- package/src/prompts/memories/consolidation.md +30 -0
- package/src/prompts/memories/read-path.md +11 -0
- package/src/prompts/memories/stage_one_input.md +6 -0
- package/src/prompts/memories/stage_one_system.md +21 -0
- package/src/prompts/review-request.md +70 -0
- package/src/prompts/system/agent-creation-architect.md +65 -0
- package/src/prompts/system/agent-creation-user.md +6 -0
- package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/commit-message-system.md +2 -0
- package/src/prompts/system/custom-system-prompt.md +82 -0
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/system/file-operations.md +10 -0
- package/src/prompts/system/handoff-document.md +48 -0
- package/src/prompts/system/plan-mode-active.md +114 -0
- package/src/prompts/system/plan-mode-approved.md +23 -0
- package/src/prompts/system/plan-mode-reference.md +14 -0
- package/src/prompts/system/plan-mode-subagent.md +34 -0
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
- package/src/prompts/system/subagent-submit-reminder.md +7 -0
- package/src/prompts/system/subagent-system-prompt.md +36 -0
- package/src/prompts/system/subagent-user-prompt.md +12 -0
- package/src/prompts/system/summarization-system.md +3 -0
- package/src/prompts/system/system-prompt.md +698 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/prompts/system/web-search.md +28 -0
- package/src/prompts/tools/ask.md +28 -0
- package/src/prompts/tools/ast-edit.md +48 -0
- package/src/prompts/tools/ast-grep.md +54 -0
- package/src/prompts/tools/async-result.md +5 -0
- package/src/prompts/tools/bash.md +67 -0
- package/src/prompts/tools/browser.md +26 -0
- package/src/prompts/tools/calculator.md +10 -0
- package/src/prompts/tools/cancel-job.md +5 -0
- package/src/prompts/tools/catalog-workflow-runner.md +13 -0
- package/src/prompts/tools/checkpoint.md +16 -0
- package/src/prompts/tools/chunk-edit.md +279 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/display-image.md +23 -0
- package/src/prompts/tools/exit-plan-mode.md +41 -0
- package/src/prompts/tools/find.md +26 -0
- package/src/prompts/tools/gemini-image.md +7 -0
- package/src/prompts/tools/grep.md +29 -0
- package/src/prompts/tools/hashline.md +131 -0
- package/src/prompts/tools/inspect-image-system.md +20 -0
- package/src/prompts/tools/inspect-image.md +32 -0
- package/src/prompts/tools/lsp.md +33 -0
- package/src/prompts/tools/patch.md +77 -0
- package/src/prompts/tools/poll.md +5 -0
- package/src/prompts/tools/python.md +60 -0
- package/src/prompts/tools/read-chunk.md +35 -0
- package/src/prompts/tools/read.md +63 -0
- package/src/prompts/tools/render-mermaid.md +9 -0
- package/src/prompts/tools/replace.md +36 -0
- package/src/prompts/tools/resolve.md +8 -0
- package/src/prompts/tools/rewind.md +13 -0
- package/src/prompts/tools/search-tool-bm25.md +34 -0
- package/src/prompts/tools/ssh.md +44 -0
- package/src/prompts/tools/task-summary.md +28 -0
- package/src/prompts/tools/task.md +131 -0
- package/src/prompts/tools/todo-write.md +89 -0
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +14 -0
- package/src/prompts/tools/xcsh-api.md +283 -0
- package/src/resource-management/index.ts +42 -0
- package/src/sdk.ts +2123 -0
- package/src/secrets/index.ts +146 -0
- package/src/secrets/obfuscator.ts +290 -0
- package/src/secrets/regex.ts +21 -0
- package/src/services/context-env.ts +102 -0
- package/src/services/xcsh-api-client.ts +230 -0
- package/src/services/xcsh-context-command.ts +803 -0
- package/src/services/xcsh-context-display.ts +49 -0
- package/src/services/xcsh-context-indicators.ts +17 -0
- package/src/services/xcsh-context-segment.ts +39 -0
- package/src/services/xcsh-context.ts +1410 -0
- package/src/services/xcsh-env.ts +107 -0
- package/src/services/xcsh-knowledge.ts +177 -0
- package/src/services/xcsh-table.ts +164 -0
- package/src/session/agent-session.ts +6690 -0
- package/src/session/agent-storage.ts +451 -0
- package/src/session/artifacts.ts +132 -0
- package/src/session/auth-storage.ts +17 -0
- package/src/session/blob-store.ts +135 -0
- package/src/session/compaction/branch-summarization.ts +322 -0
- package/src/session/compaction/compaction.ts +1388 -0
- package/src/session/compaction/index.ts +7 -0
- package/src/session/compaction/pruning.ts +91 -0
- package/src/session/compaction/utils.ts +184 -0
- package/src/session/history-storage.ts +253 -0
- package/src/session/messages.ts +597 -0
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +2816 -0
- package/src/session/session-storage.ts +370 -0
- package/src/session/streaming-output.ts +770 -0
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +1614 -0
- package/src/slash-commands/export-command.ts +178 -0
- package/src/slash-commands/marketplace-install-parser.ts +99 -0
- package/src/slash-commands/resource-commands.ts +250 -0
- package/src/ssh/config-writer.ts +183 -0
- package/src/ssh/connection-manager.ts +445 -0
- package/src/ssh/ssh-executor.ts +131 -0
- package/src/ssh/sshfs-mount.ts +137 -0
- package/src/ssh/utils.ts +8 -0
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/system-prompt.ts +688 -0
- package/src/task/agents.ts +166 -0
- package/src/task/commands.ts +131 -0
- package/src/task/discovery.ts +126 -0
- package/src/task/executor.ts +1324 -0
- package/src/task/index.ts +1197 -0
- package/src/task/isolation-backend.ts +72 -0
- package/src/task/name-generator.ts +1577 -0
- package/src/task/output-manager.ts +107 -0
- package/src/task/parallel.ts +116 -0
- package/src/task/render.ts +1035 -0
- package/src/task/subprocess-tool-registry.ts +88 -0
- package/src/task/template.ts +33 -0
- package/src/task/types.ts +230 -0
- package/src/task/worktree.ts +580 -0
- package/src/task/xcsh-command.ts +26 -0
- package/src/thinking.ts +87 -0
- package/src/tools/action-renderer.ts +198 -0
- package/src/tools/archive-reader.ts +315 -0
- package/src/tools/ask.ts +791 -0
- package/src/tools/ast-edit.ts +492 -0
- package/src/tools/ast-grep.ts +453 -0
- package/src/tools/auto-generated-guard.ts +305 -0
- package/src/tools/bash-interactive.ts +383 -0
- package/src/tools/bash-interceptor.ts +67 -0
- package/src/tools/bash-normalize.ts +107 -0
- package/src/tools/bash-skill-urls.ts +246 -0
- package/src/tools/bash.ts +878 -0
- package/src/tools/browser-renderer.ts +129 -0
- package/src/tools/browser.ts +1366 -0
- package/src/tools/calculator.ts +543 -0
- package/src/tools/cancel-job.ts +100 -0
- package/src/tools/catalog-workflow-runner.ts +718 -0
- package/src/tools/checkpoint.ts +133 -0
- package/src/tools/context.ts +39 -0
- package/src/tools/debug.ts +1004 -0
- package/src/tools/display-image-renderer.ts +96 -0
- package/src/tools/display-image.ts +109 -0
- package/src/tools/exit-plan-mode.ts +101 -0
- package/src/tools/fetch.ts +1462 -0
- package/src/tools/find.ts +437 -0
- package/src/tools/fs-cache-invalidation.ts +33 -0
- package/src/tools/gemini-image.ts +1004 -0
- package/src/tools/grep.ts +705 -0
- package/src/tools/index.ts +442 -0
- package/src/tools/inspect-image-renderer.ts +101 -0
- package/src/tools/inspect-image.ts +176 -0
- package/src/tools/json-tree.ts +266 -0
- package/src/tools/jtd-to-json-schema.ts +199 -0
- package/src/tools/jtd-to-typescript.ts +136 -0
- package/src/tools/jtd-utils.ts +102 -0
- package/src/tools/list-limit.ts +40 -0
- package/src/tools/mermaid-renderer.ts +127 -0
- package/src/tools/notebook.ts +288 -0
- package/src/tools/output-meta.ts +588 -0
- package/src/tools/path-utils.ts +560 -0
- package/src/tools/plan-mode-guard.ts +41 -0
- package/src/tools/poll-tool.ts +177 -0
- package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
- package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
- package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
- package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
- package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
- package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
- package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
- package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
- package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
- package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
- package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
- package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
- package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
- package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
- package/src/tools/python.ts +1184 -0
- package/src/tools/read.ts +1591 -0
- package/src/tools/render-mermaid.ts +93 -0
- package/src/tools/render-utils.ts +677 -0
- package/src/tools/renderers.ts +82 -0
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +196 -0
- package/src/tools/review.ts +238 -0
- package/src/tools/search-tool-bm25.ts +288 -0
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/ssh.ts +322 -0
- package/src/tools/submit-result.ts +229 -0
- package/src/tools/todo-render.ts +33 -0
- package/src/tools/todo-write.ts +440 -0
- package/src/tools/tool-errors.ts +62 -0
- package/src/tools/tool-result.ts +93 -0
- package/src/tools/tool-timeouts.ts +30 -0
- package/src/tools/vim.ts +981 -0
- package/src/tools/write.ts +671 -0
- package/src/tools/xcsh-api-renderer.ts +402 -0
- package/src/tools/xcsh-api.ts +887 -0
- package/src/tui/code-cell.ts +101 -0
- package/src/tui/file-list.ts +47 -0
- package/src/tui/index.ts +11 -0
- package/src/tui/output-block.ts +175 -0
- package/src/tui/status-line.ts +39 -0
- package/src/tui/tree-list.ts +84 -0
- package/src/tui/types.ts +15 -0
- package/src/tui/utils.ts +103 -0
- package/src/utils/changelog.ts +98 -0
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/command-args.ts +76 -0
- package/src/utils/commit-message-generator.ts +137 -0
- package/src/utils/edit-mode.ts +50 -0
- package/src/utils/event-bus.ts +33 -0
- package/src/utils/external-editor.ts +65 -0
- package/src/utils/file-display-mode.ts +37 -0
- package/src/utils/file-mentions.ts +376 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/git.ts +1495 -0
- package/src/utils/gitstatus.ts +140 -0
- package/src/utils/image-convert.ts +27 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/image-passthrough.ts +118 -0
- package/src/utils/image-resize.ts +272 -0
- package/src/utils/image-viewer.ts +17 -0
- package/src/utils/lang-from-path.ts +239 -0
- package/src/utils/markit.ts +81 -0
- package/src/utils/open.ts +20 -0
- package/src/utils/session-color.ts +55 -0
- package/src/utils/shell-snapshot.ts +188 -0
- package/src/utils/title-generator.ts +193 -0
- package/src/utils/tool-choice.ts +28 -0
- package/src/utils/tools-manager.ts +356 -0
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
- package/src/web/kagi.ts +174 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/artifacthub.ts +207 -0
- package/src/web/scrapers/arxiv.ts +83 -0
- package/src/web/scrapers/aur.ts +162 -0
- package/src/web/scrapers/biorxiv.ts +133 -0
- package/src/web/scrapers/bluesky.ts +262 -0
- package/src/web/scrapers/brew.ts +172 -0
- package/src/web/scrapers/cheatsh.ts +68 -0
- package/src/web/scrapers/chocolatey.ts +196 -0
- package/src/web/scrapers/choosealicense.ts +95 -0
- package/src/web/scrapers/cisa-kev.ts +87 -0
- package/src/web/scrapers/clojars.ts +154 -0
- package/src/web/scrapers/coingecko.ts +177 -0
- package/src/web/scrapers/crates-io.ts +110 -0
- package/src/web/scrapers/crossref.ts +136 -0
- package/src/web/scrapers/devto.ts +147 -0
- package/src/web/scrapers/discogs.ts +306 -0
- package/src/web/scrapers/discourse.ts +197 -0
- package/src/web/scrapers/dockerhub.ts +138 -0
- package/src/web/scrapers/docs-rs.ts +653 -0
- package/src/web/scrapers/fdroid.ts +134 -0
- package/src/web/scrapers/firefox-addons.ts +191 -0
- package/src/web/scrapers/flathub.ts +223 -0
- package/src/web/scrapers/github-gist.ts +58 -0
- package/src/web/scrapers/github.ts +452 -0
- package/src/web/scrapers/gitlab.ts +401 -0
- package/src/web/scrapers/go-pkg.ts +266 -0
- package/src/web/scrapers/hackage.ts +140 -0
- package/src/web/scrapers/hackernews.ts +189 -0
- package/src/web/scrapers/hex.ts +105 -0
- package/src/web/scrapers/huggingface.ts +321 -0
- package/src/web/scrapers/iacr.ts +89 -0
- package/src/web/scrapers/index.ts +252 -0
- package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
- package/src/web/scrapers/lemmy.ts +203 -0
- package/src/web/scrapers/lobsters.ts +175 -0
- package/src/web/scrapers/mastodon.ts +292 -0
- package/src/web/scrapers/maven.ts +138 -0
- package/src/web/scrapers/mdn.ts +169 -0
- package/src/web/scrapers/metacpan.ts +222 -0
- package/src/web/scrapers/musicbrainz.ts +250 -0
- package/src/web/scrapers/npm.ts +98 -0
- package/src/web/scrapers/nuget.ts +183 -0
- package/src/web/scrapers/nvd.ts +222 -0
- package/src/web/scrapers/ollama.ts +239 -0
- package/src/web/scrapers/open-vsx.ts +106 -0
- package/src/web/scrapers/opencorporates.ts +292 -0
- package/src/web/scrapers/openlibrary.ts +336 -0
- package/src/web/scrapers/orcid.ts +286 -0
- package/src/web/scrapers/osv.ts +176 -0
- package/src/web/scrapers/packagist.ts +160 -0
- package/src/web/scrapers/pub-dev.ts +143 -0
- package/src/web/scrapers/pubmed.ts +211 -0
- package/src/web/scrapers/pypi.ts +112 -0
- package/src/web/scrapers/rawg.ts +110 -0
- package/src/web/scrapers/readthedocs.ts +120 -0
- package/src/web/scrapers/reddit.ts +95 -0
- package/src/web/scrapers/repology.ts +251 -0
- package/src/web/scrapers/rfc.ts +201 -0
- package/src/web/scrapers/rubygems.ts +103 -0
- package/src/web/scrapers/searchcode.ts +190 -0
- package/src/web/scrapers/sec-edgar.ts +261 -0
- package/src/web/scrapers/semantic-scholar.ts +171 -0
- package/src/web/scrapers/snapcraft.ts +187 -0
- package/src/web/scrapers/sourcegraph.ts +336 -0
- package/src/web/scrapers/spdx.ts +108 -0
- package/src/web/scrapers/spotify.ts +198 -0
- package/src/web/scrapers/stackoverflow.ts +120 -0
- package/src/web/scrapers/terraform.ts +277 -0
- package/src/web/scrapers/tldr.ts +47 -0
- package/src/web/scrapers/twitter.ts +93 -0
- package/src/web/scrapers/types.ts +267 -0
- package/src/web/scrapers/utils.ts +109 -0
- package/src/web/scrapers/vimeo.ts +133 -0
- package/src/web/scrapers/vscode-marketplace.ts +187 -0
- package/src/web/scrapers/w3c.ts +156 -0
- package/src/web/scrapers/wikidata.ts +344 -0
- package/src/web/scrapers/wikipedia.ts +84 -0
- package/src/web/scrapers/youtube.ts +313 -0
- package/src/web/search/errors.ts +120 -0
- package/src/web/search/index.ts +346 -0
- package/src/web/search/params.ts +129 -0
- package/src/web/search/provider.ts +97 -0
- package/src/web/search/providers/anthropic.ts +339 -0
- package/src/web/search/providers/base.ts +35 -0
- package/src/web/search/providers/brave.ts +150 -0
- package/src/web/search/providers/codex.ts +402 -0
- package/src/web/search/providers/exa.ts +264 -0
- package/src/web/search/providers/firecrawl.ts +115 -0
- package/src/web/search/providers/gemini.ts +566 -0
- package/src/web/search/providers/jina.ts +99 -0
- package/src/web/search/providers/kagi.ts +70 -0
- package/src/web/search/providers/kimi.ts +157 -0
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/providers/perplexity.ts +546 -0
- package/src/web/search/providers/synthetic.ts +113 -0
- package/src/web/search/providers/tavily.ts +162 -0
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +311 -0
- package/src/web/search/render.ts +348 -0
- package/src/web/search/types.ts +438 -0
- package/src/web/search/utils.ts +17 -0
|
@@ -0,0 +1,2265 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
type Api,
|
|
4
|
+
type AssistantMessageEventStream,
|
|
5
|
+
type Context,
|
|
6
|
+
createModelManager,
|
|
7
|
+
DEFAULT_LOCAL_TOKEN,
|
|
8
|
+
enrichModelThinking,
|
|
9
|
+
getBundledModels,
|
|
10
|
+
getBundledProviders,
|
|
11
|
+
googleAntigravityModelManagerOptions,
|
|
12
|
+
googleGeminiCliModelManagerOptions,
|
|
13
|
+
type Model,
|
|
14
|
+
type ModelManagerOptions,
|
|
15
|
+
type ModelRefreshStrategy,
|
|
16
|
+
type OAuthCredentials,
|
|
17
|
+
type OAuthLoginCallbacks,
|
|
18
|
+
openaiCodexModelManagerOptions,
|
|
19
|
+
PROVIDER_DESCRIPTORS,
|
|
20
|
+
readModelCache,
|
|
21
|
+
registerCustomApi,
|
|
22
|
+
registerOAuthProvider,
|
|
23
|
+
type SimpleStreamOptions,
|
|
24
|
+
type ThinkingConfig,
|
|
25
|
+
unregisterCustomApis,
|
|
26
|
+
unregisterOAuthProviders,
|
|
27
|
+
} from "@f5xc-salesdemos/pi-ai";
|
|
28
|
+
import { $env, isRecord, logger } from "@f5xc-salesdemos/pi-utils";
|
|
29
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
30
|
+
import { type ConfigError, ConfigFile } from "../config";
|
|
31
|
+
import { hasLiteLLMEnv, probeAndUpgradeLiteLLMConfig, startupHealthCheck } from "../config/auto-config";
|
|
32
|
+
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
33
|
+
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
34
|
+
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
35
|
+
import {
|
|
36
|
+
buildCanonicalModelIndex,
|
|
37
|
+
type CanonicalModelIndex,
|
|
38
|
+
type CanonicalModelRecord,
|
|
39
|
+
type CanonicalModelVariant,
|
|
40
|
+
formatCanonicalVariantSelector,
|
|
41
|
+
type ModelEquivalenceConfig,
|
|
42
|
+
} from "./model-equivalence";
|
|
43
|
+
import { type Settings, settings } from "./settings";
|
|
44
|
+
|
|
45
|
+
export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
|
|
46
|
+
|
|
47
|
+
export const kNoAuth = "N/A";
|
|
48
|
+
|
|
49
|
+
export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
|
|
50
|
+
return Boolean(apiKey) && apiKey !== kNoAuth;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "task";
|
|
54
|
+
|
|
55
|
+
export interface ModelRoleInfo {
|
|
56
|
+
tag?: string;
|
|
57
|
+
name: string;
|
|
58
|
+
color?: ThemeColor;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
62
|
+
default: { tag: "DEFAULT", name: "Default", color: "success" },
|
|
63
|
+
smol: { tag: "SMOL", name: "Fast", color: "warning" },
|
|
64
|
+
slow: { tag: "SLOW", name: "Thinking", color: "accent" },
|
|
65
|
+
vision: { tag: "VISION", name: "Vision", color: "error" },
|
|
66
|
+
plan: { tag: "PLAN", name: "Architect", color: "muted" },
|
|
67
|
+
designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
|
|
68
|
+
commit: { tag: "COMMIT", name: "Commit", color: "dim" },
|
|
69
|
+
task: { tag: "TASK", name: "Subtask", color: "muted" },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "designer", "commit", "task"];
|
|
73
|
+
|
|
74
|
+
/** Alias for ModelRoleInfo - used for both built-in and custom roles */
|
|
75
|
+
export type RoleInfo = ModelRoleInfo;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Return the canonical set of known roles for selector/carousel UI.
|
|
79
|
+
*
|
|
80
|
+
* Built-ins always come first. Configured cycle order, model assignments, and
|
|
81
|
+
* tag metadata can introduce additional custom roles without requiring duplicate
|
|
82
|
+
* entries across settings.
|
|
83
|
+
*/
|
|
84
|
+
export function getKnownRoleIds(settings: Settings): string[] {
|
|
85
|
+
const roles = [...MODEL_ROLE_IDS] as string[];
|
|
86
|
+
const seen = new Set<string>(roles);
|
|
87
|
+
const addRole = (role: string) => {
|
|
88
|
+
if (seen.has(role)) return;
|
|
89
|
+
seen.add(role);
|
|
90
|
+
roles.push(role);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (const role of settings.get("cycleOrder")) addRole(role);
|
|
94
|
+
for (const role of Object.keys(settings.getModelRoles())) addRole(role);
|
|
95
|
+
for (const role of Object.keys(settings.get("modelTags"))) addRole(role);
|
|
96
|
+
|
|
97
|
+
return roles;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get role info for a role name (built-in or custom).
|
|
102
|
+
* Configured metadata overrides built-in defaults when present.
|
|
103
|
+
*/
|
|
104
|
+
export function getRoleInfo(role: string, settings: Settings): RoleInfo {
|
|
105
|
+
const builtIn = role in MODEL_ROLES ? MODEL_ROLES[role as ModelRole] : undefined;
|
|
106
|
+
const configured = settings.get("modelTags")[role];
|
|
107
|
+
|
|
108
|
+
if (configured) {
|
|
109
|
+
return {
|
|
110
|
+
tag: builtIn?.tag,
|
|
111
|
+
name: configured.name || builtIn?.name || role,
|
|
112
|
+
color: configured.color && isValidThemeColor(configured.color) ? configured.color : builtIn?.color,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (builtIn) return builtIn;
|
|
117
|
+
|
|
118
|
+
return { name: role, color: "muted" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const OpenRouterRoutingSchema = Type.Object({
|
|
122
|
+
only: Type.Optional(Type.Array(Type.String())),
|
|
123
|
+
order: Type.Optional(Type.Array(Type.String())),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Schema for Vercel AI Gateway routing preferences
|
|
127
|
+
const VercelGatewayRoutingSchema = Type.Object({
|
|
128
|
+
only: Type.Optional(Type.Array(Type.String())),
|
|
129
|
+
order: Type.Optional(Type.Array(Type.String())),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Schema for OpenAI compatibility settings
|
|
133
|
+
const ReasoningEffortMapSchema = Type.Object({
|
|
134
|
+
minimal: Type.Optional(Type.String()),
|
|
135
|
+
low: Type.Optional(Type.String()),
|
|
136
|
+
medium: Type.Optional(Type.String()),
|
|
137
|
+
high: Type.Optional(Type.String()),
|
|
138
|
+
xhigh: Type.Optional(Type.String()),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const OpenAICompatSchema = Type.Object({
|
|
142
|
+
supportsStore: Type.Optional(Type.Boolean()),
|
|
143
|
+
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
|
144
|
+
supportsReasoningEffort: Type.Optional(Type.Boolean()),
|
|
145
|
+
reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
|
|
146
|
+
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
|
|
147
|
+
supportsUsageInStreaming: Type.Optional(Type.Boolean()),
|
|
148
|
+
requiresToolResultName: Type.Optional(Type.Boolean()),
|
|
149
|
+
requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
|
|
150
|
+
requiresThinkingAsText: Type.Optional(Type.Boolean()),
|
|
151
|
+
thinkingFormat: Type.Optional(
|
|
152
|
+
Type.Union([
|
|
153
|
+
Type.Literal("openai"),
|
|
154
|
+
Type.Literal("openrouter"),
|
|
155
|
+
Type.Literal("zai"),
|
|
156
|
+
Type.Literal("qwen"),
|
|
157
|
+
Type.Literal("qwen-chat-template"),
|
|
158
|
+
]),
|
|
159
|
+
),
|
|
160
|
+
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
|
|
161
|
+
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
|
|
162
|
+
extraBody: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
163
|
+
supportsStrictMode: Type.Optional(Type.Boolean()),
|
|
164
|
+
toolStrictMode: Type.Optional(Type.Union([Type.Literal("all_strict"), Type.Literal("none")])),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const EffortSchema = Type.Union([
|
|
168
|
+
Type.Literal("minimal"),
|
|
169
|
+
Type.Literal("low"),
|
|
170
|
+
Type.Literal("medium"),
|
|
171
|
+
Type.Literal("high"),
|
|
172
|
+
Type.Literal("xhigh"),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const ThinkingControlModeSchema = Type.Union([
|
|
176
|
+
Type.Literal("effort"),
|
|
177
|
+
Type.Literal("budget"),
|
|
178
|
+
Type.Literal("google-level"),
|
|
179
|
+
Type.Literal("anthropic-adaptive"),
|
|
180
|
+
Type.Literal("anthropic-budget-effort"),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const ModelThinkingSchema = Type.Object({
|
|
184
|
+
minLevel: EffortSchema,
|
|
185
|
+
maxLevel: EffortSchema,
|
|
186
|
+
mode: ThinkingControlModeSchema,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Schema for custom model definition
|
|
190
|
+
// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
|
|
191
|
+
const ModelDefinitionSchema = Type.Object({
|
|
192
|
+
id: Type.String({ minLength: 1 }),
|
|
193
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
194
|
+
api: Type.Optional(
|
|
195
|
+
Type.Union([
|
|
196
|
+
Type.Literal("openai-completions"),
|
|
197
|
+
Type.Literal("openai-responses"),
|
|
198
|
+
Type.Literal("openai-codex-responses"),
|
|
199
|
+
Type.Literal("azure-openai-responses"),
|
|
200
|
+
Type.Literal("anthropic-messages"),
|
|
201
|
+
Type.Literal("google-generative-ai"),
|
|
202
|
+
Type.Literal("google-vertex"),
|
|
203
|
+
]),
|
|
204
|
+
),
|
|
205
|
+
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
|
206
|
+
reasoning: Type.Optional(Type.Boolean()),
|
|
207
|
+
thinking: Type.Optional(ModelThinkingSchema),
|
|
208
|
+
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
|
209
|
+
cost: Type.Optional(
|
|
210
|
+
Type.Object({
|
|
211
|
+
input: Type.Number(),
|
|
212
|
+
output: Type.Number(),
|
|
213
|
+
cacheRead: Type.Number(),
|
|
214
|
+
cacheWrite: Type.Number(),
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
premiumMultiplier: Type.Optional(Type.Number()),
|
|
218
|
+
contextWindow: Type.Optional(Type.Number()),
|
|
219
|
+
maxTokens: Type.Optional(Type.Number()),
|
|
220
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
221
|
+
compat: Type.Optional(OpenAICompatSchema),
|
|
222
|
+
contextPromotionTarget: Type.Optional(Type.String({ minLength: 1 })),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Schema for per-model overrides (all fields optional, merged with built-in model)
|
|
226
|
+
const ModelOverrideSchema = Type.Object({
|
|
227
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
228
|
+
reasoning: Type.Optional(Type.Boolean()),
|
|
229
|
+
thinking: Type.Optional(ModelThinkingSchema),
|
|
230
|
+
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
|
231
|
+
cost: Type.Optional(
|
|
232
|
+
Type.Object({
|
|
233
|
+
input: Type.Optional(Type.Number()),
|
|
234
|
+
output: Type.Optional(Type.Number()),
|
|
235
|
+
cacheRead: Type.Optional(Type.Number()),
|
|
236
|
+
cacheWrite: Type.Optional(Type.Number()),
|
|
237
|
+
}),
|
|
238
|
+
),
|
|
239
|
+
premiumMultiplier: Type.Optional(Type.Number()),
|
|
240
|
+
contextWindow: Type.Optional(Type.Number()),
|
|
241
|
+
maxTokens: Type.Optional(Type.Number()),
|
|
242
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
243
|
+
compat: Type.Optional(OpenAICompatSchema),
|
|
244
|
+
contextPromotionTarget: Type.Optional(Type.String({ minLength: 1 })),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
type ModelOverride = Static<typeof ModelOverrideSchema>;
|
|
248
|
+
|
|
249
|
+
const ProviderDiscoverySchema = Type.Object({
|
|
250
|
+
type: Type.Union([
|
|
251
|
+
Type.Literal("ollama"),
|
|
252
|
+
Type.Literal("llama.cpp"),
|
|
253
|
+
Type.Literal("lm-studio"),
|
|
254
|
+
Type.Literal("openai-compat"),
|
|
255
|
+
]),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none")]);
|
|
259
|
+
|
|
260
|
+
const ProviderConfigSchema = Type.Object({
|
|
261
|
+
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
|
262
|
+
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
|
263
|
+
api: Type.Optional(
|
|
264
|
+
Type.Union([
|
|
265
|
+
Type.Literal("openai-completions"),
|
|
266
|
+
Type.Literal("openai-responses"),
|
|
267
|
+
Type.Literal("openai-codex-responses"),
|
|
268
|
+
Type.Literal("azure-openai-responses"),
|
|
269
|
+
Type.Literal("anthropic-messages"),
|
|
270
|
+
Type.Literal("google-generative-ai"),
|
|
271
|
+
Type.Literal("google-vertex"),
|
|
272
|
+
]),
|
|
273
|
+
),
|
|
274
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
275
|
+
compat: Type.Optional(OpenAICompatSchema),
|
|
276
|
+
authHeader: Type.Optional(Type.Boolean()),
|
|
277
|
+
auth: Type.Optional(ProviderAuthSchema),
|
|
278
|
+
discovery: Type.Optional(ProviderDiscoverySchema),
|
|
279
|
+
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
|
|
280
|
+
modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const EquivalenceConfigSchema = Type.Object({
|
|
284
|
+
overrides: Type.Optional(Type.Record(Type.String(), Type.String({ minLength: 1 }))),
|
|
285
|
+
exclude: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const ModelsConfigSchema = Type.Object({
|
|
289
|
+
providers: Type.Optional(Type.Record(Type.String(), ProviderConfigSchema)),
|
|
290
|
+
equivalence: Type.Optional(EquivalenceConfigSchema),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
type ModelsConfig = Static<typeof ModelsConfigSchema>;
|
|
294
|
+
|
|
295
|
+
type ProviderAuthMode = Static<typeof ProviderAuthSchema>;
|
|
296
|
+
type ProviderDiscovery = Static<typeof ProviderDiscoverySchema>;
|
|
297
|
+
|
|
298
|
+
type ProviderValidationMode = "models-config" | "runtime-register";
|
|
299
|
+
|
|
300
|
+
interface ProviderValidationModel {
|
|
301
|
+
id: string;
|
|
302
|
+
api?: Api;
|
|
303
|
+
contextWindow?: number;
|
|
304
|
+
maxTokens?: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface ProviderValidationConfig {
|
|
308
|
+
baseUrl?: string;
|
|
309
|
+
apiKey?: string;
|
|
310
|
+
api?: Api;
|
|
311
|
+
auth?: ProviderAuthMode;
|
|
312
|
+
oauthConfigured?: boolean;
|
|
313
|
+
discovery?: ProviderDiscovery;
|
|
314
|
+
compat?: Model<Api>["compat"];
|
|
315
|
+
modelOverrides?: Record<string, unknown>;
|
|
316
|
+
models: ProviderValidationModel[];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function validateProviderConfiguration(
|
|
320
|
+
providerName: string,
|
|
321
|
+
config: ProviderValidationConfig,
|
|
322
|
+
mode: ProviderValidationMode,
|
|
323
|
+
): void {
|
|
324
|
+
const hasProviderApi = !!config.api;
|
|
325
|
+
const models = config.models;
|
|
326
|
+
|
|
327
|
+
if (models.length === 0) {
|
|
328
|
+
if (mode === "models-config") {
|
|
329
|
+
const hasModelOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0;
|
|
330
|
+
if (!config.baseUrl && !config.compat && !hasModelOverrides && !config.discovery) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Provider ${providerName}: must specify "baseUrl", "compat", "modelOverrides", "discovery", or "models"`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
if (!config.baseUrl) {
|
|
338
|
+
throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
|
|
339
|
+
}
|
|
340
|
+
const requiresAuth =
|
|
341
|
+
mode === "runtime-register"
|
|
342
|
+
? !config.apiKey && !config.oauthConfigured
|
|
343
|
+
: !config.apiKey && (config.auth ?? "apiKey") !== "none";
|
|
344
|
+
if (requiresAuth) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
mode === "runtime-register"
|
|
347
|
+
? `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`
|
|
348
|
+
: `Provider ${providerName}: "apiKey" is required when defining custom models unless auth is "none".`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (mode === "models-config" && config.discovery && !config.api) {
|
|
354
|
+
throw new Error(`Provider ${providerName}: "api" is required when discovery is enabled at provider level.`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const modelDef of models) {
|
|
358
|
+
if (!hasProviderApi && !modelDef.api) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
mode === "runtime-register"
|
|
361
|
+
? `Provider ${providerName}, model ${modelDef.id}: no "api" specified.`
|
|
362
|
+
: `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (!modelDef.id) {
|
|
366
|
+
throw new Error(`Provider ${providerName}: model missing "id"`);
|
|
367
|
+
}
|
|
368
|
+
if (mode === "models-config") {
|
|
369
|
+
if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0) {
|
|
370
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
|
|
371
|
+
}
|
|
372
|
+
if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0) {
|
|
373
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
|
|
380
|
+
"models",
|
|
381
|
+
config => {
|
|
382
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
383
|
+
validateProviderConfiguration(
|
|
384
|
+
providerName,
|
|
385
|
+
{
|
|
386
|
+
baseUrl: providerConfig.baseUrl,
|
|
387
|
+
apiKey: providerConfig.apiKey,
|
|
388
|
+
api: providerConfig.api as Api | undefined,
|
|
389
|
+
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
|
|
390
|
+
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
|
|
391
|
+
compat: providerConfig.compat,
|
|
392
|
+
modelOverrides: providerConfig.modelOverrides,
|
|
393
|
+
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
394
|
+
},
|
|
395
|
+
"models-config",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
/** Provider override config (baseUrl, headers, apiKey, compat) without custom models */
|
|
402
|
+
interface ProviderOverride {
|
|
403
|
+
baseUrl?: string;
|
|
404
|
+
headers?: Record<string, string>;
|
|
405
|
+
apiKey?: string;
|
|
406
|
+
compat?: Model<Api>["compat"];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
interface DiscoveryProviderConfig {
|
|
410
|
+
provider: string;
|
|
411
|
+
api: Api;
|
|
412
|
+
baseUrl?: string;
|
|
413
|
+
headers?: Record<string, string>;
|
|
414
|
+
compat?: Model<Api>["compat"];
|
|
415
|
+
discovery: ProviderDiscovery;
|
|
416
|
+
optional?: boolean;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export type ProviderDiscoveryStatus = "idle" | "ok" | "cached" | "unavailable" | "unauthenticated";
|
|
420
|
+
|
|
421
|
+
export interface ProviderDiscoveryState {
|
|
422
|
+
provider: string;
|
|
423
|
+
status: ProviderDiscoveryStatus;
|
|
424
|
+
optional: boolean;
|
|
425
|
+
stale: boolean;
|
|
426
|
+
fetchedAt?: number;
|
|
427
|
+
models: string[];
|
|
428
|
+
error?: string;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface CanonicalModelQueryOptions {
|
|
432
|
+
availableOnly?: boolean;
|
|
433
|
+
candidates?: readonly Model<Api>[];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Result of loading custom models from models.json */
|
|
437
|
+
interface CustomModelsResult {
|
|
438
|
+
models?: CustomModelOverlay[];
|
|
439
|
+
overrides?: Map<string, ProviderOverride>;
|
|
440
|
+
modelOverrides?: Map<string, Map<string, ModelOverride>>;
|
|
441
|
+
keylessProviders?: Set<string>;
|
|
442
|
+
discoverableProviders?: DiscoveryProviderConfig[];
|
|
443
|
+
configuredProviders?: Set<string>;
|
|
444
|
+
equivalence?: ModelEquivalenceConfig;
|
|
445
|
+
error?: ConfigError;
|
|
446
|
+
found: boolean;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
type OllamaDiscoveredModelMetadata = {
|
|
450
|
+
reasoning: boolean;
|
|
451
|
+
input: ("text" | "image")[];
|
|
452
|
+
contextWindow?: number;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
type LlamaCppDiscoveredServerMetadata = {
|
|
456
|
+
contextWindow?: number;
|
|
457
|
+
input?: ("text" | "image")[];
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Resolve an API key config value to an actual key.
|
|
462
|
+
* Checks environment variable first, then treats as literal.
|
|
463
|
+
*/
|
|
464
|
+
export function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
465
|
+
const envValue = Bun.env[keyConfig];
|
|
466
|
+
if (envValue) return envValue;
|
|
467
|
+
return keyConfig;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+$/;
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Resolve an API key that came from YAML config (models.yml).
|
|
474
|
+
* Unlike resolveApiKeyConfig, this returns undefined for unresolved env var
|
|
475
|
+
* names to prevent sending literal names like "LITELLM_API_KEY" as Bearer tokens.
|
|
476
|
+
*/
|
|
477
|
+
export function resolveYamlApiKeyConfig(keyConfig: string): string | undefined {
|
|
478
|
+
const envValue = Bun.env[keyConfig];
|
|
479
|
+
if (envValue) return envValue;
|
|
480
|
+
if (ENV_VAR_NAME_RE.test(keyConfig)) return undefined;
|
|
481
|
+
return keyConfig;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
485
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
if (typeof value === "string" && value.trim()) {
|
|
489
|
+
const parsed = Number(value);
|
|
490
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
491
|
+
return parsed;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function extractOllamaContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
498
|
+
const modelInfo = payload.model_info;
|
|
499
|
+
if (isRecord(modelInfo)) {
|
|
500
|
+
for (const [key, value] of Object.entries(modelInfo)) {
|
|
501
|
+
if (key === "context_length" || key.endsWith(".context_length")) {
|
|
502
|
+
const contextWindow = toPositiveNumberOrUndefined(value);
|
|
503
|
+
if (contextWindow !== undefined) {
|
|
504
|
+
return contextWindow;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const parameters = payload.parameters;
|
|
511
|
+
if (typeof parameters !== "string") {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
const match = parameters.match(/(?:^|\n)\s*num_ctx\s+(\d+)\s*(?:$|\n)/m);
|
|
515
|
+
return match ? toPositiveNumberOrUndefined(match[1]) : undefined;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function extractLlamaCppContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
519
|
+
const generationSettings = payload.default_generation_settings;
|
|
520
|
+
if (isRecord(generationSettings)) {
|
|
521
|
+
const contextWindow = toPositiveNumberOrUndefined(generationSettings.n_ctx);
|
|
522
|
+
if (contextWindow !== undefined) {
|
|
523
|
+
return contextWindow;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return toPositiveNumberOrUndefined(payload.n_ctx);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function extractLlamaCppInputCapabilities(payload: Record<string, unknown>): ("text" | "image")[] | undefined {
|
|
530
|
+
const modalities = payload.modalities;
|
|
531
|
+
if (!isRecord(modalities)) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
return modalities.vision === true ? ["text", "image"] : ["text"];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function extractGoogleOAuthToken(value: string | undefined): string | undefined {
|
|
538
|
+
if (!isAuthenticated(value)) return undefined;
|
|
539
|
+
try {
|
|
540
|
+
const parsed = JSON.parse(value) as { token?: unknown };
|
|
541
|
+
if (Object.hasOwn(parsed, "token")) {
|
|
542
|
+
if (typeof parsed.token !== "string") {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
const token = parsed.token.trim();
|
|
546
|
+
return token.length > 0 ? token : undefined;
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
// OAuth values for Google providers are expected to be JSON, but custom setups may already provide raw token.
|
|
550
|
+
}
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: string): OAuthCredential[] {
|
|
555
|
+
const providerEntry = authStorage.getAll()[provider];
|
|
556
|
+
if (!providerEntry) {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
const entries = Array.isArray(providerEntry) ? providerEntry : [providerEntry];
|
|
560
|
+
return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function resolveOAuthAccountIdForAccessToken(
|
|
564
|
+
authStorage: AuthStorage,
|
|
565
|
+
provider: string,
|
|
566
|
+
accessToken: string,
|
|
567
|
+
): string | undefined {
|
|
568
|
+
const oauthCredentials = getOAuthCredentialsForProvider(authStorage, provider);
|
|
569
|
+
const matchingCredential = oauthCredentials.find(credential => credential.access === accessToken);
|
|
570
|
+
if (matchingCredential) {
|
|
571
|
+
return matchingCredential.accountId;
|
|
572
|
+
}
|
|
573
|
+
if (oauthCredentials.length === 1) {
|
|
574
|
+
return oauthCredentials[0].accountId;
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function mergeCompat(
|
|
580
|
+
baseCompat: Model<Api>["compat"],
|
|
581
|
+
overrideCompat: ModelOverride["compat"],
|
|
582
|
+
): Model<Api>["compat"] | undefined {
|
|
583
|
+
if (!overrideCompat) return baseCompat;
|
|
584
|
+
const base = baseCompat ?? {};
|
|
585
|
+
const override = overrideCompat;
|
|
586
|
+
const merged: NonNullable<Model<Api>["compat"]> = { ...base, ...override };
|
|
587
|
+
if (baseCompat?.reasoningEffortMap || overrideCompat.reasoningEffortMap) {
|
|
588
|
+
merged.reasoningEffortMap = { ...baseCompat?.reasoningEffortMap, ...overrideCompat.reasoningEffortMap };
|
|
589
|
+
}
|
|
590
|
+
if (baseCompat?.openRouterRouting || overrideCompat.openRouterRouting) {
|
|
591
|
+
merged.openRouterRouting = { ...baseCompat?.openRouterRouting, ...overrideCompat.openRouterRouting };
|
|
592
|
+
}
|
|
593
|
+
if (baseCompat?.vercelGatewayRouting || overrideCompat.vercelGatewayRouting) {
|
|
594
|
+
merged.vercelGatewayRouting = { ...baseCompat?.vercelGatewayRouting, ...overrideCompat.vercelGatewayRouting };
|
|
595
|
+
}
|
|
596
|
+
if (baseCompat?.extraBody || overrideCompat.extraBody) {
|
|
597
|
+
merged.extraBody = { ...baseCompat?.extraBody, ...overrideCompat.extraBody };
|
|
598
|
+
}
|
|
599
|
+
return merged;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
|
603
|
+
const result = { ...model };
|
|
604
|
+
if (override.name !== undefined) result.name = override.name;
|
|
605
|
+
if (override.reasoning !== undefined) result.reasoning = override.reasoning;
|
|
606
|
+
if (override.thinking !== undefined) result.thinking = override.thinking as ThinkingConfig;
|
|
607
|
+
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
|
608
|
+
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
|
609
|
+
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
610
|
+
if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
|
|
611
|
+
if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
|
|
612
|
+
if (override.cost) {
|
|
613
|
+
result.cost = {
|
|
614
|
+
input: override.cost.input ?? model.cost.input,
|
|
615
|
+
output: override.cost.output ?? model.cost.output,
|
|
616
|
+
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
|
|
617
|
+
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (override.headers) {
|
|
621
|
+
result.headers = { ...model.headers, ...override.headers };
|
|
622
|
+
}
|
|
623
|
+
result.compat = mergeCompat(model.compat, override.compat);
|
|
624
|
+
return enrichModelThinking(result);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
interface CustomModelDefinitionLike {
|
|
628
|
+
id: string;
|
|
629
|
+
name?: string;
|
|
630
|
+
api?: Api;
|
|
631
|
+
baseUrl?: string;
|
|
632
|
+
reasoning?: boolean;
|
|
633
|
+
thinking?: ThinkingConfig;
|
|
634
|
+
input?: ("text" | "image")[];
|
|
635
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
636
|
+
contextWindow?: number;
|
|
637
|
+
maxTokens?: number;
|
|
638
|
+
headers?: Record<string, string>;
|
|
639
|
+
compat?: Model<Api>["compat"];
|
|
640
|
+
contextPromotionTarget?: string;
|
|
641
|
+
premiumMultiplier?: number;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
interface CustomModelBuildOptions {
|
|
645
|
+
useDefaults: boolean;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
type CustomModelOverlay = {
|
|
649
|
+
id: string;
|
|
650
|
+
provider: string;
|
|
651
|
+
api: Api;
|
|
652
|
+
baseUrl: string;
|
|
653
|
+
name?: string;
|
|
654
|
+
reasoning?: boolean;
|
|
655
|
+
thinking?: ThinkingConfig;
|
|
656
|
+
input?: ("text" | "image")[];
|
|
657
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
658
|
+
contextWindow?: number;
|
|
659
|
+
maxTokens?: number;
|
|
660
|
+
headers?: Record<string, string>;
|
|
661
|
+
compat?: Model<Api>["compat"];
|
|
662
|
+
contextPromotionTarget?: string;
|
|
663
|
+
premiumMultiplier?: number;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
function mergeCustomModelHeaders(
|
|
667
|
+
providerHeaders: Record<string, string> | undefined,
|
|
668
|
+
modelHeaders: Record<string, string> | undefined,
|
|
669
|
+
authHeader: boolean | undefined,
|
|
670
|
+
apiKeyConfig: string | undefined,
|
|
671
|
+
): Record<string, string> | undefined {
|
|
672
|
+
let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
|
|
673
|
+
if (authHeader && apiKeyConfig) {
|
|
674
|
+
const resolvedKey = resolveApiKeyConfig(apiKeyConfig);
|
|
675
|
+
if (resolvedKey) {
|
|
676
|
+
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return headers;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function buildCustomModelOverlay(
|
|
683
|
+
providerName: string,
|
|
684
|
+
providerBaseUrl: string,
|
|
685
|
+
providerApi: Api | undefined,
|
|
686
|
+
providerHeaders: Record<string, string> | undefined,
|
|
687
|
+
providerApiKey: string | undefined,
|
|
688
|
+
authHeader: boolean | undefined,
|
|
689
|
+
providerCompat: Model<Api>["compat"] | undefined,
|
|
690
|
+
modelDef: CustomModelDefinitionLike,
|
|
691
|
+
): CustomModelOverlay | undefined {
|
|
692
|
+
const api = modelDef.api ?? providerApi;
|
|
693
|
+
if (!api) return undefined;
|
|
694
|
+
return {
|
|
695
|
+
id: modelDef.id,
|
|
696
|
+
provider: providerName,
|
|
697
|
+
api,
|
|
698
|
+
baseUrl: modelDef.baseUrl ?? providerBaseUrl,
|
|
699
|
+
name: modelDef.name,
|
|
700
|
+
reasoning: modelDef.reasoning,
|
|
701
|
+
thinking: modelDef.thinking as ThinkingConfig | undefined,
|
|
702
|
+
input: modelDef.input as ("text" | "image")[] | undefined,
|
|
703
|
+
cost: modelDef.cost,
|
|
704
|
+
contextWindow: modelDef.contextWindow,
|
|
705
|
+
maxTokens: modelDef.maxTokens,
|
|
706
|
+
headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
|
|
707
|
+
compat: mergeCompat(providerCompat, modelDef.compat),
|
|
708
|
+
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
709
|
+
premiumMultiplier: modelDef.premiumMultiplier,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
|
|
714
|
+
if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
|
|
715
|
+
return model;
|
|
716
|
+
}
|
|
717
|
+
return { ...model, contextWindow: 1_000_000 };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
|
|
721
|
+
const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
|
|
722
|
+
const cost =
|
|
723
|
+
resolvedModel.cost ?? (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
|
724
|
+
const input = resolvedModel.input ?? (options.useDefaults ? ["text"] : undefined);
|
|
725
|
+
return enrichModelThinking({
|
|
726
|
+
id: resolvedModel.id,
|
|
727
|
+
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
728
|
+
api: resolvedModel.api,
|
|
729
|
+
provider: resolvedModel.provider,
|
|
730
|
+
baseUrl: resolvedModel.baseUrl,
|
|
731
|
+
reasoning: resolvedModel.reasoning ?? (options.useDefaults ? false : undefined),
|
|
732
|
+
thinking: resolvedModel.thinking,
|
|
733
|
+
input: input as ("text" | "image")[],
|
|
734
|
+
cost,
|
|
735
|
+
contextWindow: resolvedModel.contextWindow ?? (options.useDefaults ? 128000 : undefined),
|
|
736
|
+
maxTokens: resolvedModel.maxTokens ?? (options.useDefaults ? 16384 : undefined),
|
|
737
|
+
headers: resolvedModel.headers,
|
|
738
|
+
compat: resolvedModel.compat,
|
|
739
|
+
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
740
|
+
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
741
|
+
} as Model<Api>);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function normalizeSuppressedSelector(selector: string): string {
|
|
745
|
+
const trimmed = selector.trim();
|
|
746
|
+
if (!trimmed) return trimmed;
|
|
747
|
+
const parsed = parseModelString(trimmed);
|
|
748
|
+
if (!parsed) return trimmed;
|
|
749
|
+
return `${parsed.provider}/${parsed.id}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function getDisabledProviderIdsFromSettings(): Set<string> {
|
|
753
|
+
try {
|
|
754
|
+
return new Set(settings.get("disabledProviders"));
|
|
755
|
+
} catch {
|
|
756
|
+
return new Set();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function getConfiguredProviderOrderFromSettings(): string[] {
|
|
761
|
+
try {
|
|
762
|
+
return settings.get("modelProviderOrder");
|
|
763
|
+
} catch {
|
|
764
|
+
return [];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
770
|
+
*/
|
|
771
|
+
export class ModelRegistry {
|
|
772
|
+
#models: Model<Api>[] = [];
|
|
773
|
+
#canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
|
|
774
|
+
#customProviderApiKeys: Map<string, string> = new Map();
|
|
775
|
+
#keylessProviders: Set<string> = new Set();
|
|
776
|
+
#discoverableProviders: DiscoveryProviderConfig[] = [];
|
|
777
|
+
#customModelOverlays: CustomModelOverlay[] = [];
|
|
778
|
+
#providerOverrides: Map<string, ProviderOverride> = new Map();
|
|
779
|
+
#modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
|
|
780
|
+
#equivalenceConfig: ModelEquivalenceConfig | undefined;
|
|
781
|
+
#configError: ConfigError | undefined = undefined;
|
|
782
|
+
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
783
|
+
#registeredProviderSources: Set<string> = new Set();
|
|
784
|
+
#providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
|
|
785
|
+
#cacheDbPath?: string;
|
|
786
|
+
#suppressedSelectors: Map<string, number> = new Map();
|
|
787
|
+
#backgroundRefresh?: Promise<void>;
|
|
788
|
+
#lastDiscoveryWarnings: Map<string, string> = new Map();
|
|
789
|
+
#hasProbed = false;
|
|
790
|
+
// Runtime extension model overlays — persist across refresh() cycles so that
|
|
791
|
+
// models registered by extensions survive the model selector's offline reload.
|
|
792
|
+
#runtimeModelOverlays: CustomModelOverlay[] = [];
|
|
793
|
+
#runtimeProviderApiKeys: Map<string, string> = new Map();
|
|
794
|
+
#runtimeProvidersBySource: Map<string, Set<string>> = new Map();
|
|
795
|
+
#runtimeProviderSourceByName: Map<string, string> = new Map();
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @param authStorage - Auth storage for API key resolution
|
|
799
|
+
*/
|
|
800
|
+
constructor(
|
|
801
|
+
readonly authStorage: AuthStorage,
|
|
802
|
+
modelsPath?: string,
|
|
803
|
+
) {
|
|
804
|
+
this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
|
|
805
|
+
this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
|
|
806
|
+
// Set up fallback resolver for custom provider API keys
|
|
807
|
+
this.authStorage.setFallbackResolver(provider => {
|
|
808
|
+
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
809
|
+
if (keyConfig) {
|
|
810
|
+
return resolveApiKeyConfig(keyConfig);
|
|
811
|
+
}
|
|
812
|
+
return undefined;
|
|
813
|
+
});
|
|
814
|
+
// Load models synchronously in constructor
|
|
815
|
+
this.#loadModels();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Reload models from disk (built-in + custom from models.json).
|
|
820
|
+
*/
|
|
821
|
+
async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
|
|
822
|
+
// On first refresh, probe LiteLLM proxy and upgrade config with discovery if available
|
|
823
|
+
if (!this.#hasProbed && hasLiteLLMEnv()) {
|
|
824
|
+
this.#hasProbed = true;
|
|
825
|
+
const upgraded = await probeAndUpgradeLiteLLMConfig(this.#modelsConfigFile.path());
|
|
826
|
+
if (upgraded) {
|
|
827
|
+
this.#modelsConfigFile.invalidate();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
this.#reloadStaticModels();
|
|
831
|
+
this.#suppressedSelectors.clear();
|
|
832
|
+
await this.#refreshRuntimeDiscoveries(strategy);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void {
|
|
836
|
+
if (this.#backgroundRefresh) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const refreshPromise = this.refresh(strategy)
|
|
840
|
+
.catch(error => {
|
|
841
|
+
logger.warn("background model refresh failed", {
|
|
842
|
+
error: error instanceof Error ? error.message : String(error),
|
|
843
|
+
});
|
|
844
|
+
})
|
|
845
|
+
.finally(() => {
|
|
846
|
+
if (this.#backgroundRefresh === refreshPromise) {
|
|
847
|
+
this.#backgroundRefresh = undefined;
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
this.#backgroundRefresh = refreshPromise;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Await the in-flight background refresh if one is running.
|
|
855
|
+
* Returns immediately if no background refresh is in progress.
|
|
856
|
+
*/
|
|
857
|
+
async awaitBackgroundRefresh(): Promise<void> {
|
|
858
|
+
if (this.#backgroundRefresh) {
|
|
859
|
+
await this.#backgroundRefresh;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Check if any non-optional discoverable provider has no cached models yet.
|
|
865
|
+
* Returns true on first run when the model cache is empty.
|
|
866
|
+
*/
|
|
867
|
+
hasUncachedDiscoverableProviders(): boolean {
|
|
868
|
+
for (const [, state] of this.#providerDiscoveryStates) {
|
|
869
|
+
if (state.status === "idle" && !state.optional) {
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
|
877
|
+
this.#reloadStaticModels();
|
|
878
|
+
for (const selector of this.#suppressedSelectors.keys()) {
|
|
879
|
+
if (selector.startsWith(`${providerId}/`)) {
|
|
880
|
+
this.#suppressedSelectors.delete(selector);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
#reloadStaticModels(): void {
|
|
887
|
+
this.#modelsConfigFile.invalidate();
|
|
888
|
+
this.#customProviderApiKeys.clear();
|
|
889
|
+
this.#keylessProviders.clear();
|
|
890
|
+
this.#discoverableProviders = [];
|
|
891
|
+
// Restore runtime API keys before #loadModels — survives because
|
|
892
|
+
// #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
|
|
893
|
+
for (const [k, v] of this.#runtimeProviderApiKeys) {
|
|
894
|
+
this.#customProviderApiKeys.set(k, v);
|
|
895
|
+
}
|
|
896
|
+
this.#providerOverrides.clear();
|
|
897
|
+
this.#modelOverrides.clear();
|
|
898
|
+
this.#equivalenceConfig = undefined;
|
|
899
|
+
this.#configError = undefined;
|
|
900
|
+
this.#providerDiscoveryStates.clear();
|
|
901
|
+
this.#loadModels();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Get any error from loading models.json (undefined if no error).
|
|
906
|
+
*/
|
|
907
|
+
getError(): ConfigError | undefined {
|
|
908
|
+
return this.#configError;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
#loadModels() {
|
|
912
|
+
// Load custom models from models.json first (to know which providers to override)
|
|
913
|
+
const {
|
|
914
|
+
models: customModels = [],
|
|
915
|
+
overrides = new Map(),
|
|
916
|
+
modelOverrides = new Map(),
|
|
917
|
+
keylessProviders = new Set(),
|
|
918
|
+
discoverableProviders = [],
|
|
919
|
+
configuredProviders = new Set(),
|
|
920
|
+
equivalence,
|
|
921
|
+
error: configError,
|
|
922
|
+
} = this.#loadCustomModels();
|
|
923
|
+
this.#configError = configError;
|
|
924
|
+
this.#keylessProviders = keylessProviders;
|
|
925
|
+
this.#discoverableProviders = discoverableProviders;
|
|
926
|
+
this.#customModelOverlays = customModels;
|
|
927
|
+
this.#providerOverrides = overrides;
|
|
928
|
+
this.#modelOverrides = modelOverrides;
|
|
929
|
+
this.#equivalenceConfig = equivalence;
|
|
930
|
+
|
|
931
|
+
this.#addImplicitDiscoverableProviders(configuredProviders);
|
|
932
|
+
const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
|
|
933
|
+
const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
|
|
934
|
+
const resolvedDefaults = this.#mergeResolvedModels(builtInModels, cachedDiscoveries);
|
|
935
|
+
const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
|
|
936
|
+
// Merge runtime extension models so they survive refresh() cycles
|
|
937
|
+
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
938
|
+
|
|
939
|
+
this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
940
|
+
this.#rebuildCanonicalIndex();
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Load built-in models, applying provider-level overrides only.
|
|
944
|
+
* Per-model overrides are applied later by #applyModelOverrides. */
|
|
945
|
+
#loadBuiltInModels(overrides: Map<string, ProviderOverride>): Model<Api>[] {
|
|
946
|
+
return getBundledProviders().flatMap(provider => {
|
|
947
|
+
const models = getBundledModels(provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[];
|
|
948
|
+
const providerOverride = overrides.get(provider);
|
|
949
|
+
|
|
950
|
+
return models.map(m => {
|
|
951
|
+
if (!providerOverride) return m;
|
|
952
|
+
return {
|
|
953
|
+
...m,
|
|
954
|
+
baseUrl: providerOverride.baseUrl ?? m.baseUrl,
|
|
955
|
+
headers: providerOverride.headers ? { ...m.headers, ...providerOverride.headers } : m.headers,
|
|
956
|
+
compat: mergeCompat(m.compat, providerOverride.compat),
|
|
957
|
+
};
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
|
963
|
+
const merged = [...baseModels];
|
|
964
|
+
for (const replacementModel of replacementModels) {
|
|
965
|
+
const existingIndex = merged.findIndex(
|
|
966
|
+
m => m.provider === replacementModel.provider && m.id === replacementModel.id,
|
|
967
|
+
);
|
|
968
|
+
if (existingIndex >= 0) {
|
|
969
|
+
merged[existingIndex] = replacementModel;
|
|
970
|
+
} else {
|
|
971
|
+
merged.push(replacementModel);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return merged;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Merge custom models with built-in, replacing by provider+id match */
|
|
978
|
+
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
|
979
|
+
const merged = [...builtInModels];
|
|
980
|
+
for (const customModel of customModels) {
|
|
981
|
+
const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id);
|
|
982
|
+
if (existingIndex >= 0) {
|
|
983
|
+
const existingModel = merged[existingIndex];
|
|
984
|
+
merged[existingIndex] = enrichModelThinking({
|
|
985
|
+
...existingModel,
|
|
986
|
+
id: customModel.id,
|
|
987
|
+
provider: customModel.provider,
|
|
988
|
+
api: customModel.api,
|
|
989
|
+
baseUrl: customModel.baseUrl,
|
|
990
|
+
name: customModel.name ?? existingModel.name,
|
|
991
|
+
reasoning: customModel.reasoning ?? existingModel.reasoning,
|
|
992
|
+
thinking: customModel.thinking ?? existingModel.thinking,
|
|
993
|
+
input: customModel.input ?? existingModel.input,
|
|
994
|
+
cost: customModel.cost ?? existingModel.cost,
|
|
995
|
+
contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
|
|
996
|
+
maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
|
|
997
|
+
// Same-id custom definitions replace bundled transport behavior. Provider-level
|
|
998
|
+
// headers/compat were already folded into customModel during parsing; do not
|
|
999
|
+
// re-merge bundled transport metadata here.
|
|
1000
|
+
headers: customModel.headers,
|
|
1001
|
+
compat: customModel.compat,
|
|
1002
|
+
contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
|
1003
|
+
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
1004
|
+
} as Model<Api>);
|
|
1005
|
+
} else {
|
|
1006
|
+
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return merged;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
#loadCachedDiscoverableModels(): Model<Api>[] {
|
|
1013
|
+
const cachedModels: Model<Api>[] = [];
|
|
1014
|
+
for (const providerConfig of this.#discoverableProviders) {
|
|
1015
|
+
const cache = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1016
|
+
if (!cache) {
|
|
1017
|
+
this.#providerDiscoveryStates.set(providerConfig.provider, {
|
|
1018
|
+
provider: providerConfig.provider,
|
|
1019
|
+
status: "idle",
|
|
1020
|
+
optional: providerConfig.optional ?? false,
|
|
1021
|
+
stale: false,
|
|
1022
|
+
models: [],
|
|
1023
|
+
});
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const models = this.#applyProviderModelOverrides(
|
|
1027
|
+
providerConfig.provider,
|
|
1028
|
+
this.#normalizeDiscoverableModels(
|
|
1029
|
+
providerConfig,
|
|
1030
|
+
this.#applyProviderCompat(providerConfig.compat, cache.models),
|
|
1031
|
+
),
|
|
1032
|
+
);
|
|
1033
|
+
cachedModels.push(...models);
|
|
1034
|
+
this.#providerDiscoveryStates.set(providerConfig.provider, {
|
|
1035
|
+
provider: providerConfig.provider,
|
|
1036
|
+
status: "cached",
|
|
1037
|
+
optional: providerConfig.optional ?? false,
|
|
1038
|
+
stale: !cache.fresh || !cache.authoritative,
|
|
1039
|
+
fetchedAt: cache.updatedAt,
|
|
1040
|
+
models: models.map(model => model.id),
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
return cachedModels;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
#applyProviderCompat(compat: Model<Api>["compat"] | undefined, models: Model<Api>[]): Model<Api>[] {
|
|
1047
|
+
if (!compat) return models;
|
|
1048
|
+
return models.map(model => ({ ...model, compat: mergeCompat(model.compat, compat) }));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
#normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
1052
|
+
if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
|
|
1053
|
+
return models;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return models.map(model => (model.api === "openai-completions" ? { ...model, api: "openai-responses" } : model));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
#addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
|
|
1060
|
+
if (!configuredProviders.has("ollama")) {
|
|
1061
|
+
this.#discoverableProviders.push({
|
|
1062
|
+
provider: "ollama",
|
|
1063
|
+
api: "openai-responses",
|
|
1064
|
+
baseUrl: Bun.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434",
|
|
1065
|
+
discovery: { type: "ollama" },
|
|
1066
|
+
optional: true,
|
|
1067
|
+
});
|
|
1068
|
+
this.#keylessProviders.add("ollama");
|
|
1069
|
+
}
|
|
1070
|
+
if (!configuredProviders.has("llama.cpp")) {
|
|
1071
|
+
this.#discoverableProviders.push({
|
|
1072
|
+
provider: "llama.cpp",
|
|
1073
|
+
api: "openai-responses",
|
|
1074
|
+
baseUrl: Bun.env.LLAMA_CPP_BASE_URL || "http://127.0.0.1:8080",
|
|
1075
|
+
discovery: { type: "llama.cpp" },
|
|
1076
|
+
optional: true,
|
|
1077
|
+
});
|
|
1078
|
+
// Only mark as keyless if no API key is configured
|
|
1079
|
+
if (!this.authStorage.hasAuth("llama.cpp")) {
|
|
1080
|
+
this.#keylessProviders.add("llama.cpp");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (!configuredProviders.has("lm-studio")) {
|
|
1084
|
+
this.#discoverableProviders.push({
|
|
1085
|
+
provider: "lm-studio",
|
|
1086
|
+
api: "openai-completions",
|
|
1087
|
+
baseUrl: Bun.env.LM_STUDIO_BASE_URL || "http://127.0.0.1:1234/v1",
|
|
1088
|
+
discovery: { type: "lm-studio" },
|
|
1089
|
+
optional: true,
|
|
1090
|
+
});
|
|
1091
|
+
this.#keylessProviders.add("lm-studio");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
#loadCustomModels(): CustomModelsResult {
|
|
1096
|
+
let result = this.#modelsConfigFile.tryLoad();
|
|
1097
|
+
|
|
1098
|
+
// Self-healing: detect missing, corrupt, or drifted config and auto-repair from env vars
|
|
1099
|
+
if (result.status !== "ok" || result.value) {
|
|
1100
|
+
const repaired = startupHealthCheck(
|
|
1101
|
+
result.status,
|
|
1102
|
+
this.#modelsConfigFile.path(),
|
|
1103
|
+
result.status === "ok" && result.value ? result.value.providers : undefined,
|
|
1104
|
+
);
|
|
1105
|
+
if (repaired) {
|
|
1106
|
+
this.#modelsConfigFile.invalidate();
|
|
1107
|
+
result = this.#modelsConfigFile.tryLoad();
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const { value, error, status } = result;
|
|
1112
|
+
|
|
1113
|
+
if (status === "error") {
|
|
1114
|
+
return {
|
|
1115
|
+
models: [],
|
|
1116
|
+
overrides: new Map(),
|
|
1117
|
+
modelOverrides: new Map(),
|
|
1118
|
+
keylessProviders: new Set(),
|
|
1119
|
+
discoverableProviders: [],
|
|
1120
|
+
configuredProviders: new Set(),
|
|
1121
|
+
error,
|
|
1122
|
+
found: true,
|
|
1123
|
+
};
|
|
1124
|
+
} else if (status === "not-found") {
|
|
1125
|
+
return {
|
|
1126
|
+
models: [],
|
|
1127
|
+
overrides: new Map(),
|
|
1128
|
+
modelOverrides: new Map(),
|
|
1129
|
+
keylessProviders: new Set(),
|
|
1130
|
+
discoverableProviders: [],
|
|
1131
|
+
configuredProviders: new Set(),
|
|
1132
|
+
found: false,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const overrides = new Map<string, ProviderOverride>();
|
|
1137
|
+
const allModelOverrides = new Map<string, Map<string, ModelOverride>>();
|
|
1138
|
+
const keylessProviders = new Set<string>();
|
|
1139
|
+
const discoverableProviders: DiscoveryProviderConfig[] = [];
|
|
1140
|
+
const providerEntries = Object.entries(value.providers ?? {});
|
|
1141
|
+
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
1142
|
+
|
|
1143
|
+
for (const [providerName, providerConfig] of providerEntries) {
|
|
1144
|
+
// Always set overrides when baseUrl/headers/apiKey/compat are present
|
|
1145
|
+
if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey || providerConfig.compat) {
|
|
1146
|
+
overrides.set(providerName, {
|
|
1147
|
+
baseUrl: providerConfig.baseUrl,
|
|
1148
|
+
headers: providerConfig.headers,
|
|
1149
|
+
apiKey: providerConfig.apiKey,
|
|
1150
|
+
compat: providerConfig.compat,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
|
|
1155
|
+
if (authMode === "none") {
|
|
1156
|
+
keylessProviders.add(providerName);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (providerConfig.discovery && providerConfig.api) {
|
|
1160
|
+
discoverableProviders.push({
|
|
1161
|
+
provider: providerName,
|
|
1162
|
+
api: providerConfig.api as Api,
|
|
1163
|
+
baseUrl: providerConfig.baseUrl,
|
|
1164
|
+
headers: providerConfig.headers,
|
|
1165
|
+
compat: providerConfig.compat,
|
|
1166
|
+
discovery: providerConfig.discovery,
|
|
1167
|
+
optional: false,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Always store API key for fallback resolver
|
|
1172
|
+
if (providerConfig.apiKey) {
|
|
1173
|
+
this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Parse per-model overrides
|
|
1177
|
+
if (providerConfig.modelOverrides) {
|
|
1178
|
+
const perModel = new Map<string, ModelOverride>();
|
|
1179
|
+
for (const [modelId, override] of Object.entries(providerConfig.modelOverrides)) {
|
|
1180
|
+
perModel.set(modelId, override);
|
|
1181
|
+
}
|
|
1182
|
+
allModelOverrides.set(providerName, perModel);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
return {
|
|
1187
|
+
models: this.#parseModels(value),
|
|
1188
|
+
overrides,
|
|
1189
|
+
modelOverrides: allModelOverrides,
|
|
1190
|
+
keylessProviders,
|
|
1191
|
+
discoverableProviders,
|
|
1192
|
+
configuredProviders,
|
|
1193
|
+
equivalence: value.equivalence,
|
|
1194
|
+
found: true,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async #refreshRuntimeDiscoveries(
|
|
1199
|
+
strategy: ModelRefreshStrategy,
|
|
1200
|
+
providerFilter?: ReadonlySet<string>,
|
|
1201
|
+
): Promise<void> {
|
|
1202
|
+
const selectedDiscoverableProviders = providerFilter
|
|
1203
|
+
? this.#discoverableProviders.filter(provider => providerFilter.has(provider.provider))
|
|
1204
|
+
: this.#discoverableProviders;
|
|
1205
|
+
const configuredDiscoveriesPromise =
|
|
1206
|
+
selectedDiscoverableProviders.length === 0
|
|
1207
|
+
? Promise.resolve<Model<Api>[]>([])
|
|
1208
|
+
: Promise.all(
|
|
1209
|
+
selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
|
|
1210
|
+
).then(results => results.flat());
|
|
1211
|
+
const [configuredDiscovered, builtInDiscovered] = await Promise.all([
|
|
1212
|
+
configuredDiscoveriesPromise,
|
|
1213
|
+
this.#discoverBuiltInProviderModels(strategy, providerFilter),
|
|
1214
|
+
]);
|
|
1215
|
+
const discovered = [...configuredDiscovered, ...builtInDiscovered];
|
|
1216
|
+
if (discovered.length === 0) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const discoveredModels = this.#applyHardcodedModelPolicies(
|
|
1220
|
+
discovered.map(model => {
|
|
1221
|
+
const existing = this.find(model.provider, model.id);
|
|
1222
|
+
if (existing) {
|
|
1223
|
+
return {
|
|
1224
|
+
...model,
|
|
1225
|
+
baseUrl: existing.baseUrl,
|
|
1226
|
+
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
const providerOverride = this.#providerOverrides.get(model.provider);
|
|
1230
|
+
return providerOverride
|
|
1231
|
+
? {
|
|
1232
|
+
...model,
|
|
1233
|
+
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
1234
|
+
headers: providerOverride.headers
|
|
1235
|
+
? { ...model.headers, ...providerOverride.headers }
|
|
1236
|
+
: model.headers,
|
|
1237
|
+
}
|
|
1238
|
+
: model;
|
|
1239
|
+
}),
|
|
1240
|
+
);
|
|
1241
|
+
const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
|
|
1242
|
+
const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
|
|
1243
|
+
// Merge runtime extension models so they survive online discovery completion
|
|
1244
|
+
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
1245
|
+
this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
1246
|
+
this.#rebuildCanonicalIndex();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async #discoverProviderModels(
|
|
1250
|
+
providerConfig: DiscoveryProviderConfig,
|
|
1251
|
+
strategy: ModelRefreshStrategy,
|
|
1252
|
+
): Promise<Model<Api>[]> {
|
|
1253
|
+
const cached = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1254
|
+
const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
|
|
1255
|
+
if (requiresAuth) {
|
|
1256
|
+
const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
|
|
1257
|
+
if (!isAuthenticated(apiKey)) {
|
|
1258
|
+
this.#providerDiscoveryStates.set(providerConfig.provider, {
|
|
1259
|
+
provider: providerConfig.provider,
|
|
1260
|
+
status: "unauthenticated",
|
|
1261
|
+
optional: providerConfig.optional ?? false,
|
|
1262
|
+
stale: cached !== null,
|
|
1263
|
+
fetchedAt: cached?.updatedAt,
|
|
1264
|
+
models: cached?.models.map(model => model.id) ?? [],
|
|
1265
|
+
});
|
|
1266
|
+
this.#lastDiscoveryWarnings.delete(providerConfig.provider);
|
|
1267
|
+
return cached?.models ?? [];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const providerId = providerConfig.provider;
|
|
1272
|
+
let discoveryError: string | undefined;
|
|
1273
|
+
const fetchDynamicModels = async (): Promise<readonly Model<Api>[] | null> => {
|
|
1274
|
+
try {
|
|
1275
|
+
const models = await this.#discoverModelsByProviderType(providerConfig);
|
|
1276
|
+
this.#lastDiscoveryWarnings.delete(providerId);
|
|
1277
|
+
return models;
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
discoveryError = error instanceof Error ? error.message : String(error);
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const manager = createModelManager<Api>({
|
|
1285
|
+
providerId,
|
|
1286
|
+
staticModels: [],
|
|
1287
|
+
cacheDbPath: this.#cacheDbPath,
|
|
1288
|
+
cacheTtlMs: 24 * 60 * 60 * 1000,
|
|
1289
|
+
fetchDynamicModels,
|
|
1290
|
+
});
|
|
1291
|
+
const result = await manager.refresh(strategy);
|
|
1292
|
+
const status = discoveryError
|
|
1293
|
+
? result.models.length > 0
|
|
1294
|
+
? "cached"
|
|
1295
|
+
: "unavailable"
|
|
1296
|
+
: result.models.length > 0 && strategy !== "offline"
|
|
1297
|
+
? "ok"
|
|
1298
|
+
: cached
|
|
1299
|
+
? "cached"
|
|
1300
|
+
: "idle";
|
|
1301
|
+
this.#providerDiscoveryStates.set(providerId, {
|
|
1302
|
+
provider: providerId,
|
|
1303
|
+
status,
|
|
1304
|
+
optional: providerConfig.optional ?? false,
|
|
1305
|
+
stale: result.stale || status === "cached",
|
|
1306
|
+
fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
|
|
1307
|
+
models: result.models.map(model => model.id),
|
|
1308
|
+
error: discoveryError,
|
|
1309
|
+
});
|
|
1310
|
+
if (discoveryError) {
|
|
1311
|
+
this.#warnProviderDiscoveryFailure(providerConfig, discoveryError);
|
|
1312
|
+
}
|
|
1313
|
+
return this.#applyProviderModelOverrides(
|
|
1314
|
+
providerId,
|
|
1315
|
+
this.#normalizeDiscoverableModels(
|
|
1316
|
+
providerConfig,
|
|
1317
|
+
this.#applyProviderCompat(providerConfig.compat, result.models),
|
|
1318
|
+
),
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
#discoverModelsByProviderType(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1323
|
+
switch (providerConfig.discovery.type) {
|
|
1324
|
+
case "ollama":
|
|
1325
|
+
return this.#discoverOllamaModels(providerConfig);
|
|
1326
|
+
case "llama.cpp":
|
|
1327
|
+
return this.#discoverLlamaCppModels(providerConfig);
|
|
1328
|
+
case "lm-studio":
|
|
1329
|
+
return this.#discoverLmStudioModels(providerConfig);
|
|
1330
|
+
case "openai-compat":
|
|
1331
|
+
return this.#discoverOpenAICompatModels(providerConfig);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
#warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
|
|
1336
|
+
const previous = this.#lastDiscoveryWarnings.get(providerConfig.provider);
|
|
1337
|
+
if (previous === error) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
this.#lastDiscoveryWarnings.set(providerConfig.provider, error);
|
|
1341
|
+
logger.warn("model discovery failed for provider", {
|
|
1342
|
+
provider: providerConfig.provider,
|
|
1343
|
+
url: providerConfig.baseUrl,
|
|
1344
|
+
error,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async #discoverBuiltInProviderModels(
|
|
1349
|
+
strategy: ModelRefreshStrategy,
|
|
1350
|
+
providerFilter?: ReadonlySet<string>,
|
|
1351
|
+
): Promise<Model<Api>[]> {
|
|
1352
|
+
// Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
|
|
1353
|
+
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(p => p.provider));
|
|
1354
|
+
|
|
1355
|
+
// When a LiteLLM proxy is configured, providers whose baseUrl points to the
|
|
1356
|
+
// LiteLLM proxy are proxied through it. Their built-in discovery would query
|
|
1357
|
+
// the proxy's model listing endpoint, which may return model IDs the proxy
|
|
1358
|
+
// can't serve for chat. Skip them — the litellm discovery provider handles
|
|
1359
|
+
// model listing instead. Providers with other custom baseUrls (not LiteLLM)
|
|
1360
|
+
// still need built-in discovery to discover new models at their endpoint.
|
|
1361
|
+
const liteLLMBaseUrl = hasLiteLLMEnv() ? $env.LITELLM_BASE_URL?.trim().replace(/\/+$/, "") : undefined;
|
|
1362
|
+
const proxiedProviders = liteLLMBaseUrl
|
|
1363
|
+
? new Set(
|
|
1364
|
+
[...this.#providerOverrides.keys()].filter(id => {
|
|
1365
|
+
const override = this.#providerOverrides.get(id);
|
|
1366
|
+
return override?.baseUrl?.startsWith(liteLLMBaseUrl);
|
|
1367
|
+
}),
|
|
1368
|
+
)
|
|
1369
|
+
: new Set<string>();
|
|
1370
|
+
|
|
1371
|
+
const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(opts => {
|
|
1372
|
+
if (configuredDiscoveryProviders.has(opts.providerId)) {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
if (proxiedProviders.has(opts.providerId)) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
return providerFilter ? providerFilter.has(opts.providerId) : true;
|
|
1379
|
+
});
|
|
1380
|
+
if (managerOptions.length === 0) {
|
|
1381
|
+
return [];
|
|
1382
|
+
}
|
|
1383
|
+
const discoveries = await Promise.all(
|
|
1384
|
+
managerOptions.map(options => this.#discoverWithModelManager(options, strategy)),
|
|
1385
|
+
);
|
|
1386
|
+
return discoveries.flat();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
async #collectBuiltInModelManagerOptions(): Promise<ModelManagerOptions<Api>[]> {
|
|
1390
|
+
const specialProviderDescriptors: Array<{
|
|
1391
|
+
providerId: string;
|
|
1392
|
+
resolveKey: (value: string | undefined) => string | undefined;
|
|
1393
|
+
createOptions: (key: string) => ModelManagerOptions<Api>;
|
|
1394
|
+
}> = [
|
|
1395
|
+
{
|
|
1396
|
+
providerId: "google-antigravity",
|
|
1397
|
+
resolveKey: extractGoogleOAuthToken,
|
|
1398
|
+
createOptions: oauthToken =>
|
|
1399
|
+
googleAntigravityModelManagerOptions({
|
|
1400
|
+
oauthToken,
|
|
1401
|
+
endpoint: this.getProviderBaseUrl("google-antigravity"),
|
|
1402
|
+
}),
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
providerId: "google-gemini-cli",
|
|
1406
|
+
resolveKey: extractGoogleOAuthToken,
|
|
1407
|
+
createOptions: oauthToken =>
|
|
1408
|
+
googleGeminiCliModelManagerOptions({
|
|
1409
|
+
oauthToken,
|
|
1410
|
+
endpoint: this.getProviderBaseUrl("google-gemini-cli"),
|
|
1411
|
+
}),
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
providerId: "openai-codex",
|
|
1415
|
+
resolveKey: value => value,
|
|
1416
|
+
createOptions: accessToken => {
|
|
1417
|
+
const accountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", accessToken);
|
|
1418
|
+
return openaiCodexModelManagerOptions({
|
|
1419
|
+
accessToken,
|
|
1420
|
+
accountId,
|
|
1421
|
+
});
|
|
1422
|
+
},
|
|
1423
|
+
},
|
|
1424
|
+
];
|
|
1425
|
+
// Use peekApiKey to avoid OAuth token refresh during discovery.
|
|
1426
|
+
// The token is only needed if the dynamic fetch fires (cache miss),
|
|
1427
|
+
// and failures there are handled gracefully.
|
|
1428
|
+
const peekKey = (descriptor: { providerId: string }) => this.#peekApiKeyForProvider(descriptor.providerId);
|
|
1429
|
+
const [standardProviderKeys, specialKeys] = await Promise.all([
|
|
1430
|
+
Promise.all(PROVIDER_DESCRIPTORS.map(peekKey)),
|
|
1431
|
+
Promise.all(specialProviderDescriptors.map(peekKey)),
|
|
1432
|
+
]);
|
|
1433
|
+
const options: ModelManagerOptions<Api>[] = [];
|
|
1434
|
+
for (let i = 0; i < PROVIDER_DESCRIPTORS.length; i++) {
|
|
1435
|
+
const descriptor = PROVIDER_DESCRIPTORS[i];
|
|
1436
|
+
const apiKey = standardProviderKeys[i];
|
|
1437
|
+
if (isAuthenticated(apiKey) || descriptor.allowUnauthenticated) {
|
|
1438
|
+
options.push(
|
|
1439
|
+
descriptor.createModelManagerOptions({
|
|
1440
|
+
apiKey: isAuthenticated(apiKey) ? apiKey : undefined,
|
|
1441
|
+
baseUrl: this.getProviderBaseUrl(descriptor.providerId),
|
|
1442
|
+
}),
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
for (let i = 0; i < specialProviderDescriptors.length; i++) {
|
|
1448
|
+
const descriptor = specialProviderDescriptors[i];
|
|
1449
|
+
const key = descriptor.resolveKey(specialKeys[i]);
|
|
1450
|
+
if (!isAuthenticated(key)) {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
options.push(descriptor.createOptions(key));
|
|
1454
|
+
}
|
|
1455
|
+
return options;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
async #discoverWithModelManager(
|
|
1459
|
+
options: ModelManagerOptions<Api>,
|
|
1460
|
+
strategy: ModelRefreshStrategy,
|
|
1461
|
+
): Promise<Model<Api>[]> {
|
|
1462
|
+
try {
|
|
1463
|
+
const manager = createModelManager({ ...options, cacheDbPath: this.#cacheDbPath });
|
|
1464
|
+
const result = await manager.refresh(strategy);
|
|
1465
|
+
return result.models.map(model =>
|
|
1466
|
+
model.provider === options.providerId ? model : { ...model, provider: options.providerId },
|
|
1467
|
+
);
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
logger.warn("model discovery failed for provider", {
|
|
1470
|
+
provider: options.providerId,
|
|
1471
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1472
|
+
});
|
|
1473
|
+
return [];
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async #discoverOllamaModelMetadata(
|
|
1478
|
+
endpoint: string,
|
|
1479
|
+
modelId: string,
|
|
1480
|
+
headers: Record<string, string> | undefined,
|
|
1481
|
+
): Promise<OllamaDiscoveredModelMetadata | null> {
|
|
1482
|
+
const showUrl = `${endpoint}/api/show`;
|
|
1483
|
+
try {
|
|
1484
|
+
const response = await fetch(showUrl, {
|
|
1485
|
+
method: "POST",
|
|
1486
|
+
headers: { ...(headers ?? {}), "Content-Type": "application/json" },
|
|
1487
|
+
body: JSON.stringify({ model: modelId }),
|
|
1488
|
+
signal: AbortSignal.timeout(150),
|
|
1489
|
+
});
|
|
1490
|
+
if (!response.ok) {
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
const payload = (await response.json()) as unknown;
|
|
1494
|
+
if (!isRecord(payload)) {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
const contextWindow = extractOllamaContextWindow(payload);
|
|
1498
|
+
const capabilities = payload.capabilities;
|
|
1499
|
+
if (Array.isArray(capabilities)) {
|
|
1500
|
+
const normalized = new Set(
|
|
1501
|
+
capabilities.flatMap(capability => (typeof capability === "string" ? [capability.toLowerCase()] : [])),
|
|
1502
|
+
);
|
|
1503
|
+
const supportsVision = normalized.has("vision") || normalized.has("image");
|
|
1504
|
+
return {
|
|
1505
|
+
reasoning: normalized.has("thinking"),
|
|
1506
|
+
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1507
|
+
contextWindow,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
if (!isRecord(capabilities)) {
|
|
1511
|
+
return {
|
|
1512
|
+
reasoning: false,
|
|
1513
|
+
input: ["text"],
|
|
1514
|
+
contextWindow,
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
const supportsVision = capabilities.vision === true || capabilities.image === true;
|
|
1518
|
+
return {
|
|
1519
|
+
reasoning: capabilities.thinking === true,
|
|
1520
|
+
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1521
|
+
contextWindow,
|
|
1522
|
+
};
|
|
1523
|
+
} catch {
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
async #discoverOllamaModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1529
|
+
const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
|
|
1530
|
+
const tagsUrl = `${endpoint}/api/tags`;
|
|
1531
|
+
const headers = { ...(providerConfig.headers ?? {}) };
|
|
1532
|
+
const response = await fetch(tagsUrl, {
|
|
1533
|
+
headers,
|
|
1534
|
+
signal: AbortSignal.timeout(250),
|
|
1535
|
+
});
|
|
1536
|
+
if (!response.ok) {
|
|
1537
|
+
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
1538
|
+
}
|
|
1539
|
+
const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
1540
|
+
const entries = (payload.models ?? []).flatMap(item => {
|
|
1541
|
+
const id = item.model || item.name;
|
|
1542
|
+
return id ? [{ id, name: item.name || id }] : [];
|
|
1543
|
+
});
|
|
1544
|
+
const metadataById = new Map(
|
|
1545
|
+
await Promise.all(
|
|
1546
|
+
entries.map(
|
|
1547
|
+
async entry => [entry.id, await this.#discoverOllamaModelMetadata(endpoint, entry.id, headers)] as const,
|
|
1548
|
+
),
|
|
1549
|
+
),
|
|
1550
|
+
);
|
|
1551
|
+
const discovered = entries.map(entry => {
|
|
1552
|
+
const metadata = metadataById.get(entry.id);
|
|
1553
|
+
return enrichModelThinking({
|
|
1554
|
+
id: entry.id,
|
|
1555
|
+
name: entry.name,
|
|
1556
|
+
api: providerConfig.api,
|
|
1557
|
+
provider: providerConfig.provider,
|
|
1558
|
+
baseUrl: `${endpoint}/v1`,
|
|
1559
|
+
reasoning: metadata?.reasoning ?? false,
|
|
1560
|
+
input: metadata?.input ?? ["text"],
|
|
1561
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1562
|
+
contextWindow: metadata?.contextWindow ?? 128000,
|
|
1563
|
+
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, 8192),
|
|
1564
|
+
headers: providerConfig.headers,
|
|
1565
|
+
});
|
|
1566
|
+
});
|
|
1567
|
+
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
async #discoverLlamaCppServerMetadata(
|
|
1571
|
+
baseUrl: string,
|
|
1572
|
+
headers: Record<string, string> | undefined,
|
|
1573
|
+
): Promise<LlamaCppDiscoveredServerMetadata | null> {
|
|
1574
|
+
const propsUrl = `${this.#toLlamaCppNativeBaseUrl(baseUrl)}/props`;
|
|
1575
|
+
try {
|
|
1576
|
+
const response = await fetch(propsUrl, {
|
|
1577
|
+
headers,
|
|
1578
|
+
signal: AbortSignal.timeout(150),
|
|
1579
|
+
});
|
|
1580
|
+
if (!response.ok) {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
const payload = (await response.json()) as unknown;
|
|
1584
|
+
if (!isRecord(payload)) {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
return {
|
|
1588
|
+
contextWindow: extractLlamaCppContextWindow(payload),
|
|
1589
|
+
input: extractLlamaCppInputCapabilities(payload),
|
|
1590
|
+
};
|
|
1591
|
+
} catch {
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
async #discoverLlamaCppModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1597
|
+
const baseUrl = this.#normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
|
|
1598
|
+
const modelsUrl = `${baseUrl}/models`;
|
|
1599
|
+
|
|
1600
|
+
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1601
|
+
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1602
|
+
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1603
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const [response, serverMetadata] = await Promise.all([
|
|
1607
|
+
fetch(modelsUrl, {
|
|
1608
|
+
headers,
|
|
1609
|
+
signal: AbortSignal.timeout(250),
|
|
1610
|
+
}),
|
|
1611
|
+
this.#discoverLlamaCppServerMetadata(baseUrl, headers),
|
|
1612
|
+
]);
|
|
1613
|
+
if (!response.ok) {
|
|
1614
|
+
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1615
|
+
}
|
|
1616
|
+
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1617
|
+
const models = payload.data ?? [];
|
|
1618
|
+
const discovered: Model<Api>[] = [];
|
|
1619
|
+
for (const item of models) {
|
|
1620
|
+
const id = item.id;
|
|
1621
|
+
if (!id) continue;
|
|
1622
|
+
discovered.push(
|
|
1623
|
+
enrichModelThinking({
|
|
1624
|
+
id,
|
|
1625
|
+
name: id,
|
|
1626
|
+
api: providerConfig.api,
|
|
1627
|
+
provider: providerConfig.provider,
|
|
1628
|
+
baseUrl,
|
|
1629
|
+
reasoning: false,
|
|
1630
|
+
input: serverMetadata?.input ?? ["text"],
|
|
1631
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1632
|
+
contextWindow: serverMetadata?.contextWindow ?? 128000,
|
|
1633
|
+
maxTokens: Math.min(serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY, 8192),
|
|
1634
|
+
headers,
|
|
1635
|
+
compat: {
|
|
1636
|
+
supportsStore: false,
|
|
1637
|
+
supportsDeveloperRole: false,
|
|
1638
|
+
supportsReasoningEffort: false,
|
|
1639
|
+
},
|
|
1640
|
+
}),
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
async #discoverLmStudioModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1647
|
+
const baseUrl = this.#normalizeLmStudioBaseUrl(providerConfig.baseUrl);
|
|
1648
|
+
const modelsUrl = `${baseUrl}/models`;
|
|
1649
|
+
|
|
1650
|
+
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1651
|
+
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1652
|
+
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1653
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const response = await fetch(modelsUrl, {
|
|
1657
|
+
headers,
|
|
1658
|
+
signal: AbortSignal.timeout(250),
|
|
1659
|
+
});
|
|
1660
|
+
if (!response.ok) {
|
|
1661
|
+
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1662
|
+
}
|
|
1663
|
+
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1664
|
+
const models = payload.data ?? [];
|
|
1665
|
+
const discovered: Model<Api>[] = [];
|
|
1666
|
+
for (const item of models) {
|
|
1667
|
+
const id = item.id;
|
|
1668
|
+
if (!id) continue;
|
|
1669
|
+
discovered.push(
|
|
1670
|
+
enrichModelThinking({
|
|
1671
|
+
id,
|
|
1672
|
+
name: id,
|
|
1673
|
+
api: providerConfig.api,
|
|
1674
|
+
provider: providerConfig.provider,
|
|
1675
|
+
baseUrl,
|
|
1676
|
+
reasoning: false,
|
|
1677
|
+
input: ["text"],
|
|
1678
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1679
|
+
contextWindow: 128000,
|
|
1680
|
+
maxTokens: 8192,
|
|
1681
|
+
headers,
|
|
1682
|
+
compat: {
|
|
1683
|
+
supportsStore: false,
|
|
1684
|
+
supportsDeveloperRole: false,
|
|
1685
|
+
supportsReasoningEffort: false,
|
|
1686
|
+
},
|
|
1687
|
+
}),
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
async #discoverOpenAICompatModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1694
|
+
const baseUrl = (providerConfig.baseUrl ?? "").replace(/\/+$/, "");
|
|
1695
|
+
if (!baseUrl) {
|
|
1696
|
+
throw new Error("openai-compat discovery requires a baseUrl");
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1700
|
+
const apiKey =
|
|
1701
|
+
this.#customProviderApiKeys.get(providerConfig.provider) ??
|
|
1702
|
+
(await this.#peekApiKeyForProvider(providerConfig.provider));
|
|
1703
|
+
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1704
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const modelsUrl = `${baseUrl}/models`;
|
|
1708
|
+
const response = await fetch(modelsUrl, {
|
|
1709
|
+
headers: { Accept: "application/json", ...headers },
|
|
1710
|
+
signal: AbortSignal.timeout(3000),
|
|
1711
|
+
});
|
|
1712
|
+
if (!response.ok) {
|
|
1713
|
+
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1714
|
+
}
|
|
1715
|
+
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1716
|
+
const items = payload.data ?? [];
|
|
1717
|
+
const discovered: Model<Api>[] = [];
|
|
1718
|
+
for (const item of items) {
|
|
1719
|
+
const id = item.id;
|
|
1720
|
+
if (!id) continue;
|
|
1721
|
+
discovered.push(
|
|
1722
|
+
enrichModelThinking({
|
|
1723
|
+
id,
|
|
1724
|
+
name: id,
|
|
1725
|
+
api: providerConfig.api,
|
|
1726
|
+
provider: providerConfig.provider,
|
|
1727
|
+
baseUrl,
|
|
1728
|
+
reasoning: false,
|
|
1729
|
+
input: ["text"],
|
|
1730
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1731
|
+
contextWindow: 128000,
|
|
1732
|
+
maxTokens: 8192,
|
|
1733
|
+
headers,
|
|
1734
|
+
}),
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
#normalizeLlamaCppBaseUrl(baseUrl?: string): string {
|
|
1741
|
+
const defaultBaseUrl = "http://127.0.0.1:8080";
|
|
1742
|
+
const raw = baseUrl || defaultBaseUrl;
|
|
1743
|
+
try {
|
|
1744
|
+
const parsed = new URL(raw);
|
|
1745
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
1746
|
+
return `${parsed.protocol}//${parsed.host}${trimmedPath}`;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return raw;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
#toLlamaCppNativeBaseUrl(baseUrl: string): string {
|
|
1753
|
+
try {
|
|
1754
|
+
const parsed = new URL(baseUrl);
|
|
1755
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
1756
|
+
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath.slice(0, -3) || "/" : trimmedPath || "/";
|
|
1757
|
+
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
1758
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
1759
|
+
} catch {
|
|
1760
|
+
return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
#normalizeLmStudioBaseUrl(baseUrl?: string): string {
|
|
1765
|
+
const defaultBaseUrl = "http://127.0.0.1:1234/v1";
|
|
1766
|
+
const raw = baseUrl || defaultBaseUrl;
|
|
1767
|
+
try {
|
|
1768
|
+
const parsed = new URL(raw);
|
|
1769
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
1770
|
+
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
|
|
1771
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
1772
|
+
} catch {
|
|
1773
|
+
return raw;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
#normalizeOllamaBaseUrl(baseUrl?: string): string {
|
|
1777
|
+
const raw = baseUrl || "http://127.0.0.1:11434";
|
|
1778
|
+
try {
|
|
1779
|
+
const parsed = new URL(raw);
|
|
1780
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
1781
|
+
} catch {
|
|
1782
|
+
return "http://127.0.0.1:11434";
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
#applyProviderModelOverrides(provider: string, models: Model<Api>[]): Model<Api>[] {
|
|
1787
|
+
const overrides = this.#modelOverrides.get(provider);
|
|
1788
|
+
if (!overrides || overrides.size === 0) return models;
|
|
1789
|
+
return models.map(model => {
|
|
1790
|
+
const override = overrides.get(model.id);
|
|
1791
|
+
if (!override) return model;
|
|
1792
|
+
return applyModelOverride(model, override);
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
#applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
|
|
1797
|
+
if (overrides.size === 0) return models;
|
|
1798
|
+
return models.map(model => {
|
|
1799
|
+
const providerOverrides = overrides.get(model.provider);
|
|
1800
|
+
if (!providerOverrides) return model;
|
|
1801
|
+
const override = providerOverrides.get(model.id);
|
|
1802
|
+
if (!override) return model;
|
|
1803
|
+
return applyModelOverride(model, override);
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
#applyHardcodedModelPolicies(models: Model<Api>[]): Model<Api>[] {
|
|
1807
|
+
return models.map(model => {
|
|
1808
|
+
if (model.id !== "gpt-5.4" || model.provider === "github-copilot") {
|
|
1809
|
+
return model;
|
|
1810
|
+
}
|
|
1811
|
+
const overrides = this.#modelOverrides.get(model.provider)?.get(model.id);
|
|
1812
|
+
if (!overrides) {
|
|
1813
|
+
return applyModelOverride(model, { contextWindow: 1_000_000 });
|
|
1814
|
+
}
|
|
1815
|
+
return applyModelOverride(model, {
|
|
1816
|
+
contextWindow: overrides.contextWindow ?? 1_000_000,
|
|
1817
|
+
...overrides,
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
#rebuildCanonicalIndex(): void {
|
|
1823
|
+
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
#parseModels(config: ModelsConfig): CustomModelOverlay[] {
|
|
1827
|
+
const models: CustomModelOverlay[] = [];
|
|
1828
|
+
|
|
1829
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
1830
|
+
const modelDefs = providerConfig.models ?? [];
|
|
1831
|
+
if (modelDefs.length === 0) continue; // Override-only, no custom models
|
|
1832
|
+
if (providerConfig.apiKey) {
|
|
1833
|
+
this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
1834
|
+
}
|
|
1835
|
+
for (const modelDef of modelDefs) {
|
|
1836
|
+
const model = buildCustomModelOverlay(
|
|
1837
|
+
providerName,
|
|
1838
|
+
providerConfig.baseUrl!,
|
|
1839
|
+
providerConfig.api as Api | undefined,
|
|
1840
|
+
providerConfig.headers,
|
|
1841
|
+
providerConfig.apiKey,
|
|
1842
|
+
providerConfig.authHeader,
|
|
1843
|
+
providerConfig.compat,
|
|
1844
|
+
modelDef as CustomModelDefinitionLike,
|
|
1845
|
+
);
|
|
1846
|
+
if (!model) continue;
|
|
1847
|
+
models.push(model);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return models;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Get all models (built-in + custom).
|
|
1855
|
+
* If models.json had errors, returns only built-in models.
|
|
1856
|
+
*/
|
|
1857
|
+
getAll(): Model<Api>[] {
|
|
1858
|
+
return this.#models;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
#isModelAvailable(model: Model<Api>): boolean {
|
|
1862
|
+
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
1863
|
+
return (
|
|
1864
|
+
!disabledProviders.has(model.provider) &&
|
|
1865
|
+
(this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
#filterCanonicalVariants(
|
|
1870
|
+
record: CanonicalModelRecord,
|
|
1871
|
+
options: CanonicalModelQueryOptions | undefined,
|
|
1872
|
+
): CanonicalModelVariant[] {
|
|
1873
|
+
const candidateKeys = options?.candidates
|
|
1874
|
+
? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
|
|
1875
|
+
: undefined;
|
|
1876
|
+
return record.variants.filter(variant => {
|
|
1877
|
+
if (candidateKeys && !candidateKeys.has(variant.selector)) {
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1880
|
+
if (options?.availableOnly && !this.#isModelAvailable(variant.model)) {
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
return true;
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
#providerRank(models: readonly Model<Api>[]): Map<string, number> {
|
|
1888
|
+
const configuredProviders = getConfiguredProviderOrderFromSettings();
|
|
1889
|
+
const result = new Map<string, number>();
|
|
1890
|
+
let nextRank = 0;
|
|
1891
|
+
for (const provider of configuredProviders) {
|
|
1892
|
+
const normalized = provider.trim().toLowerCase();
|
|
1893
|
+
if (!normalized || result.has(normalized)) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
result.set(normalized, nextRank);
|
|
1897
|
+
nextRank += 1;
|
|
1898
|
+
}
|
|
1899
|
+
for (const model of models) {
|
|
1900
|
+
const normalized = model.provider.toLowerCase();
|
|
1901
|
+
if (result.has(normalized)) {
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
result.set(normalized, nextRank);
|
|
1905
|
+
nextRank += 1;
|
|
1906
|
+
}
|
|
1907
|
+
return result;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
#resolveCanonicalVariant(
|
|
1911
|
+
variants: readonly CanonicalModelVariant[],
|
|
1912
|
+
allCandidates: readonly Model<Api>[],
|
|
1913
|
+
): CanonicalModelVariant | undefined {
|
|
1914
|
+
if (variants.length === 0) {
|
|
1915
|
+
return undefined;
|
|
1916
|
+
}
|
|
1917
|
+
const providerRank = this.#providerRank(allCandidates);
|
|
1918
|
+
const modelOrder = new Map<string, number>();
|
|
1919
|
+
for (let index = 0; index < allCandidates.length; index += 1) {
|
|
1920
|
+
modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
|
|
1921
|
+
}
|
|
1922
|
+
const sourceRank: Record<CanonicalModelVariant["source"], number> = {
|
|
1923
|
+
override: 1,
|
|
1924
|
+
bundled: 1,
|
|
1925
|
+
heuristic: 2,
|
|
1926
|
+
fallback: 3,
|
|
1927
|
+
};
|
|
1928
|
+
return [...variants].sort((left, right) => {
|
|
1929
|
+
const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
1930
|
+
const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
1931
|
+
if (leftProviderRank !== rightProviderRank) {
|
|
1932
|
+
return leftProviderRank - rightProviderRank;
|
|
1933
|
+
}
|
|
1934
|
+
const leftExact = left.model.id === left.canonicalId ? 0 : 1;
|
|
1935
|
+
const rightExact = right.model.id === right.canonicalId ? 0 : 1;
|
|
1936
|
+
if (leftExact !== rightExact) {
|
|
1937
|
+
return leftExact - rightExact;
|
|
1938
|
+
}
|
|
1939
|
+
if (sourceRank[left.source] !== sourceRank[right.source]) {
|
|
1940
|
+
return sourceRank[left.source] - sourceRank[right.source];
|
|
1941
|
+
}
|
|
1942
|
+
if (left.model.id.length !== right.model.id.length) {
|
|
1943
|
+
return left.model.id.length - right.model.id.length;
|
|
1944
|
+
}
|
|
1945
|
+
const leftOrder = modelOrder.get(left.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
1946
|
+
const rightOrder = modelOrder.get(right.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
1947
|
+
return leftOrder - rightOrder;
|
|
1948
|
+
})[0];
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
|
|
1952
|
+
const records: CanonicalModelRecord[] = [];
|
|
1953
|
+
for (const record of this.#canonicalIndex.records) {
|
|
1954
|
+
const variants = this.#filterCanonicalVariants(record, options);
|
|
1955
|
+
if (variants.length === 0) {
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
records.push({
|
|
1959
|
+
id: record.id,
|
|
1960
|
+
name: record.name,
|
|
1961
|
+
variants,
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
return records;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
|
|
1968
|
+
const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
|
|
1969
|
+
if (!record) {
|
|
1970
|
+
return [];
|
|
1971
|
+
}
|
|
1972
|
+
return this.#filterCanonicalVariants(record, options);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
|
|
1976
|
+
const variants = this.getCanonicalVariants(canonicalId, options);
|
|
1977
|
+
if (variants.length === 0) {
|
|
1978
|
+
return undefined;
|
|
1979
|
+
}
|
|
1980
|
+
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
1981
|
+
return this.#resolveCanonicalVariant(variants, candidates)?.model;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
getCanonicalId(model: Model<Api>): string | undefined {
|
|
1985
|
+
return this.#canonicalIndex.bySelector.get(formatCanonicalVariantSelector(model).toLowerCase());
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* Get only models that have auth configured.
|
|
1990
|
+
* This is a fast check that doesn't refresh OAuth tokens.
|
|
1991
|
+
*/
|
|
1992
|
+
getAvailable(): Model<Api>[] {
|
|
1993
|
+
return this.#models.filter(model => this.#isModelAvailable(model));
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
getDiscoverableProviders(): string[] {
|
|
1997
|
+
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
1998
|
+
return this.#discoverableProviders
|
|
1999
|
+
.filter(provider => !disabledProviders.has(provider.provider))
|
|
2000
|
+
.map(provider => provider.provider);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
|
|
2004
|
+
return this.#providerDiscoveryStates.get(provider);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Find a model by provider and ID.
|
|
2009
|
+
*/
|
|
2010
|
+
find(provider: string, modelId: string): Model<Api> | undefined {
|
|
2011
|
+
return resolveProviderModelReference(provider, modelId, this.#models);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Get the base URL associated with a provider, if any model defines one.
|
|
2016
|
+
*/
|
|
2017
|
+
getProviderBaseUrl(provider: string): string | undefined {
|
|
2018
|
+
return this.#models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* Get API key for a model.
|
|
2023
|
+
*/
|
|
2024
|
+
async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
|
|
2025
|
+
if (this.#keylessProviders.has(model.provider)) {
|
|
2026
|
+
return kNoAuth;
|
|
2027
|
+
}
|
|
2028
|
+
return this.authStorage.getApiKey(model.provider, sessionId, { baseUrl: model.baseUrl, modelId: model.id });
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Get API key for a provider (e.g., "openai").
|
|
2033
|
+
*/
|
|
2034
|
+
async getApiKeyForProvider(provider: string, sessionId?: string, baseUrl?: string): Promise<string | undefined> {
|
|
2035
|
+
if (this.#keylessProviders.has(provider)) {
|
|
2036
|
+
return kNoAuth;
|
|
2037
|
+
}
|
|
2038
|
+
return this.authStorage.getApiKey(provider, sessionId, { baseUrl });
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
|
|
2042
|
+
if (this.#keylessProviders.has(provider)) {
|
|
2043
|
+
return kNoAuth;
|
|
2044
|
+
}
|
|
2045
|
+
return this.authStorage.peekApiKey(provider);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* Check if a model is using OAuth credentials (subscription).
|
|
2050
|
+
*/
|
|
2051
|
+
isUsingOAuth(model: Model<Api>): boolean {
|
|
2052
|
+
return this.authStorage.hasOAuth(model.provider);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* Remove custom API/OAuth registrations for a specific extension source.
|
|
2057
|
+
*/
|
|
2058
|
+
clearSourceRegistrations(sourceId: string): void {
|
|
2059
|
+
unregisterCustomApis(sourceId);
|
|
2060
|
+
unregisterOAuthProviders(sourceId);
|
|
2061
|
+
const sourceProviders = this.#runtimeProvidersBySource.get(sourceId);
|
|
2062
|
+
if (!sourceProviders || sourceProviders.size === 0) {
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
this.#runtimeProvidersBySource.delete(sourceId);
|
|
2066
|
+
for (const providerName of sourceProviders) {
|
|
2067
|
+
if (this.#runtimeProviderSourceByName.get(providerName) !== sourceId) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
this.#runtimeProviderSourceByName.delete(providerName);
|
|
2071
|
+
this.#runtimeProviderApiKeys.delete(providerName);
|
|
2072
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
|
|
2073
|
+
}
|
|
2074
|
+
this.#reloadStaticModels();
|
|
2075
|
+
this.#rebuildCanonicalIndex();
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* Remove registrations for extension sources that are no longer active.
|
|
2080
|
+
*/
|
|
2081
|
+
syncExtensionSources(activeSourceIds: string[]): void {
|
|
2082
|
+
const activeSources = new Set(activeSourceIds);
|
|
2083
|
+
for (const sourceId of this.#registeredProviderSources) {
|
|
2084
|
+
if (activeSources.has(sourceId)) {
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
this.clearSourceRegistrations(sourceId);
|
|
2088
|
+
this.#registeredProviderSources.delete(sourceId);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Register a provider dynamically (from extensions).
|
|
2094
|
+
*
|
|
2095
|
+
* If provider has models: replaces all existing models for this provider.
|
|
2096
|
+
* If provider has only baseUrl/headers: overrides existing models' URLs.
|
|
2097
|
+
* If provider has streamSimple: registers a custom API streaming function.
|
|
2098
|
+
* If provider has oauth: registers OAuth provider for /login support.
|
|
2099
|
+
*/
|
|
2100
|
+
registerProvider(providerName: string, config: ProviderConfigInput, sourceId?: string): void {
|
|
2101
|
+
if (config.streamSimple && !config.api) {
|
|
2102
|
+
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
validateProviderConfiguration(
|
|
2106
|
+
providerName,
|
|
2107
|
+
{
|
|
2108
|
+
baseUrl: config.baseUrl,
|
|
2109
|
+
apiKey: config.apiKey,
|
|
2110
|
+
api: config.api,
|
|
2111
|
+
oauthConfigured: Boolean(config.oauth),
|
|
2112
|
+
models: (config.models ?? []) as ProviderValidationModel[],
|
|
2113
|
+
},
|
|
2114
|
+
"runtime-register",
|
|
2115
|
+
);
|
|
2116
|
+
|
|
2117
|
+
if (config.streamSimple && config.api) {
|
|
2118
|
+
const streamSimple = config.streamSimple;
|
|
2119
|
+
registerCustomApi(config.api, streamSimple, sourceId, (model, context, options) =>
|
|
2120
|
+
streamSimple(model, context, options as SimpleStreamOptions),
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
if (config.oauth) {
|
|
2125
|
+
registerOAuthProvider({
|
|
2126
|
+
...config.oauth,
|
|
2127
|
+
id: providerName,
|
|
2128
|
+
sourceId,
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
if (sourceId) {
|
|
2133
|
+
this.#registeredProviderSources.add(sourceId);
|
|
2134
|
+
const previousSourceId = this.#runtimeProviderSourceByName.get(providerName);
|
|
2135
|
+
if (previousSourceId && previousSourceId !== sourceId) {
|
|
2136
|
+
const previousProviders = this.#runtimeProvidersBySource.get(previousSourceId);
|
|
2137
|
+
previousProviders?.delete(providerName);
|
|
2138
|
+
if (previousProviders && previousProviders.size === 0) {
|
|
2139
|
+
this.#runtimeProvidersBySource.delete(previousSourceId);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const sourceProviders = this.#runtimeProvidersBySource.get(sourceId) ?? new Set<string>();
|
|
2143
|
+
sourceProviders.add(providerName);
|
|
2144
|
+
this.#runtimeProvidersBySource.set(sourceId, sourceProviders);
|
|
2145
|
+
this.#runtimeProviderSourceByName.set(providerName, sourceId);
|
|
2146
|
+
}
|
|
2147
|
+
if (config.apiKey) {
|
|
2148
|
+
this.#customProviderApiKeys.set(providerName, config.apiKey);
|
|
2149
|
+
// Persist runtime API keys so they survive #reloadStaticModels() cycles
|
|
2150
|
+
this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if (config.models && config.models.length > 0) {
|
|
2154
|
+
// Build model overlays that persist across refresh() cycles
|
|
2155
|
+
const newOverlays: CustomModelOverlay[] = [];
|
|
2156
|
+
for (const modelDef of config.models) {
|
|
2157
|
+
const overlay = buildCustomModelOverlay(
|
|
2158
|
+
providerName,
|
|
2159
|
+
config.baseUrl!,
|
|
2160
|
+
config.api,
|
|
2161
|
+
config.headers,
|
|
2162
|
+
config.apiKey,
|
|
2163
|
+
config.authHeader,
|
|
2164
|
+
config.compat,
|
|
2165
|
+
modelDef as CustomModelDefinitionLike,
|
|
2166
|
+
);
|
|
2167
|
+
if (!overlay) {
|
|
2168
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
2169
|
+
}
|
|
2170
|
+
newOverlays.push(overlay);
|
|
2171
|
+
}
|
|
2172
|
+
// Store as runtime overlays so they survive #reloadStaticModels()
|
|
2173
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(m => m.provider !== providerName);
|
|
2174
|
+
this.#runtimeModelOverlays.push(...newOverlays);
|
|
2175
|
+
|
|
2176
|
+
// Also update #models immediately for the current cycle
|
|
2177
|
+
const nextModels = this.#models.filter(m => m.provider !== providerName);
|
|
2178
|
+
for (const overlay of newOverlays) {
|
|
2179
|
+
nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
if (config.oauth?.modifyModels) {
|
|
2183
|
+
const credential = this.authStorage.getOAuthCredential(providerName);
|
|
2184
|
+
if (credential) {
|
|
2185
|
+
this.#models = config.oauth.modifyModels(nextModels, credential);
|
|
2186
|
+
this.#rebuildCanonicalIndex();
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
this.#models = nextModels;
|
|
2192
|
+
this.#rebuildCanonicalIndex();
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (config.baseUrl) {
|
|
2197
|
+
this.#models = this.#models.map(m => {
|
|
2198
|
+
if (m.provider !== providerName) return m;
|
|
2199
|
+
return {
|
|
2200
|
+
...m,
|
|
2201
|
+
baseUrl: config.baseUrl ?? m.baseUrl,
|
|
2202
|
+
headers: config.headers ? { ...m.headers, ...config.headers } : m.headers,
|
|
2203
|
+
};
|
|
2204
|
+
});
|
|
2205
|
+
this.#rebuildCanonicalIndex();
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Suppress a specific model selector (e.g., "provider/id") until a specific timestamp.
|
|
2211
|
+
*/
|
|
2212
|
+
suppressSelector(selector: string, untilMs: number): void {
|
|
2213
|
+
this.#suppressedSelectors.set(normalizeSuppressedSelector(selector), untilMs);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Check if a model selector is currently suppressed due to rate limits.
|
|
2218
|
+
*/
|
|
2219
|
+
isSelectorSuppressed(selector: string): boolean {
|
|
2220
|
+
const normalizedSelector = normalizeSuppressedSelector(selector);
|
|
2221
|
+
const suppressedUntil = this.#suppressedSelectors.get(normalizedSelector);
|
|
2222
|
+
if (!suppressedUntil) return false;
|
|
2223
|
+
if (suppressedUntil <= Date.now()) {
|
|
2224
|
+
this.#suppressedSelectors.delete(normalizedSelector);
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2227
|
+
return true;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Input type for registerProvider API (from extensions).
|
|
2233
|
+
*/
|
|
2234
|
+
export interface ProviderConfigInput {
|
|
2235
|
+
baseUrl?: string;
|
|
2236
|
+
apiKey?: string;
|
|
2237
|
+
api?: Api;
|
|
2238
|
+
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
2239
|
+
headers?: Record<string, string>;
|
|
2240
|
+
compat?: Model<Api>["compat"];
|
|
2241
|
+
authHeader?: boolean;
|
|
2242
|
+
oauth?: {
|
|
2243
|
+
name: string;
|
|
2244
|
+
login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
|
|
2245
|
+
refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
|
|
2246
|
+
getApiKey?(credentials: OAuthCredentials): string;
|
|
2247
|
+
modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
|
|
2248
|
+
};
|
|
2249
|
+
models?: Array<{
|
|
2250
|
+
id: string;
|
|
2251
|
+
name: string;
|
|
2252
|
+
api?: Api;
|
|
2253
|
+
baseUrl?: string;
|
|
2254
|
+
reasoning: boolean;
|
|
2255
|
+
thinking?: ThinkingConfig;
|
|
2256
|
+
input: ("text" | "image")[];
|
|
2257
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
2258
|
+
contextWindow: number;
|
|
2259
|
+
maxTokens: number;
|
|
2260
|
+
headers?: Record<string, string>;
|
|
2261
|
+
compat?: Model<Api>["compat"];
|
|
2262
|
+
contextPromotionTarget?: string;
|
|
2263
|
+
premiumMultiplier?: number;
|
|
2264
|
+
}>;
|
|
2265
|
+
}
|