@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.12
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 +142 -7
- package/dist/cli.js +23108 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +2 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +5 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +8 -219
- package/dist/types/config/model-resolver.d.ts +34 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +41 -8
- package/dist/types/config/settings.d.ts +8 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +6 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +25 -6
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +8 -3
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +15 -3
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/index.d.ts +2 -2
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +4 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +2 -1
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +30 -0
- package/src/cli.ts +76 -13
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +5 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +4 -3
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +320 -1041
- package/src/config/model-resolver.ts +173 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +61 -19
- package/src/config/settings.ts +98 -4
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/edit/diff.ts +47 -3
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +47 -4
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +3 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +40 -22
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -3
- package/src/eval/py/kernel.ts +43 -4
- package/src/eval/py/runner.py +107 -3
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +85 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +6 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/bank.ts +17 -2
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +183 -23
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +4 -1
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +176 -7
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +32 -28
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +4 -2
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +119 -35
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +258 -53
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +11 -4
- package/src/modes/controllers/event-controller.ts +73 -4
- package/src/modes/controllers/input-controller.ts +2 -1
- package/src/modes/controllers/mcp-command-controller.ts +39 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +42 -18
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +13 -24
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +5 -7
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +31 -11
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +223 -64
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +188 -11
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +40 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/ssh/connection-manager.ts +27 -0
- package/src/system-prompt.ts +14 -0
- package/src/task/commands.ts +2 -1
- package/src/task/executor.ts +74 -65
- package/src/task/index.ts +146 -68
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +20 -5
- package/src/task/types.ts +9 -0
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +9 -1
- package/src/tiny/title-client.ts +60 -16
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +100 -18
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +14 -13
- package/src/tools/index.ts +13 -1
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +38 -8
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/file-mentions.ts +2 -1
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import type { Api, Context, Model, ModelSpec, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
|
|
5
|
+
import type { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
|
|
6
|
+
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
7
|
+
import { isVertexExpressOpenAIUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
8
|
+
import { readModelCache } from "@oh-my-pi/pi-catalog/model-cache";
|
|
9
|
+
import {
|
|
10
|
+
createModelManager,
|
|
11
|
+
type ModelManagerOptions,
|
|
12
|
+
type ModelRefreshStrategy,
|
|
13
|
+
} from "@oh-my-pi/pi-catalog/model-manager";
|
|
14
|
+
import { getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/models";
|
|
7
15
|
import {
|
|
8
16
|
googleAntigravityModelManagerOptions,
|
|
9
17
|
googleGeminiCliModelManagerOptions,
|
|
@@ -11,79 +19,12 @@ import {
|
|
|
11
19
|
PROVIDER_DESCRIPTORS,
|
|
12
20
|
UNK_CONTEXT_WINDOW,
|
|
13
21
|
UNK_MAX_TOKENS,
|
|
14
|
-
} from "@oh-my-pi/pi-
|
|
15
|
-
import type { Api, Context, Model, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
|
|
16
|
-
import type { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
|
|
22
|
+
} from "@oh-my-pi/pi-catalog/provider-models";
|
|
17
23
|
|
|
18
24
|
// Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
|
|
19
25
|
// any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
|
|
20
26
|
const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
|
|
21
27
|
|
|
22
|
-
// Default cap on `max_tokens` for auto-discovered models that do not advertise
|
|
23
|
-
// their own output limit (OpenAI-models-list, Ollama, llama.cpp, new-api/
|
|
24
|
-
// one-api proxies). 32K matches the upper end of what mainstream
|
|
25
|
-
// OpenAI-compatible providers (DeepSeek, MiMo, OpenRouter, etc.) actually
|
|
26
|
-
// accept and keeps `min(contextWindow, …)` honoring smaller local windows.
|
|
27
|
-
// Conservative caps below this caused providers to drop the connection
|
|
28
|
-
// mid-stream when models hit the cap on legitimate large tool calls (see
|
|
29
|
-
// issue #1528: `write` payloads >~5KB on deepseek-v4-pro surfaced as
|
|
30
|
-
// "socket connection was closed unexpectedly").
|
|
31
|
-
const DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
32
|
-
|
|
33
|
-
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
34
|
-
const OLLAMA_HOST_DEFAULT_PORT = "11434";
|
|
35
|
-
|
|
36
|
-
function normalizeOllamaHostEnv(value: string | undefined): string | undefined {
|
|
37
|
-
const trimmed = value?.trim();
|
|
38
|
-
if (!trimmed) return undefined;
|
|
39
|
-
const candidate = trimmed.includes("://")
|
|
40
|
-
? trimmed
|
|
41
|
-
: trimmed.startsWith("//")
|
|
42
|
-
? `http:${trimmed}`
|
|
43
|
-
: trimmed.startsWith(":")
|
|
44
|
-
? `http://127.0.0.1${trimmed}`
|
|
45
|
-
: `http://${trimmed}`;
|
|
46
|
-
try {
|
|
47
|
-
const parsed = new URL(candidate);
|
|
48
|
-
if (!parsed.hostname || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
if (!parsed.port && parsed.protocol === "http:") {
|
|
52
|
-
parsed.port = OLLAMA_HOST_DEFAULT_PORT;
|
|
53
|
-
}
|
|
54
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
55
|
-
} catch {
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function getImplicitOllamaBaseUrl(): string {
|
|
61
|
-
const baseUrl = Bun.env.OLLAMA_BASE_URL?.trim();
|
|
62
|
-
return baseUrl || normalizeOllamaHostEnv(Bun.env.OLLAMA_HOST) || DEFAULT_OLLAMA_BASE_URL;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getOllamaContextLengthOverride(): number | undefined {
|
|
66
|
-
const value = Bun.env.OLLAMA_CONTEXT_LENGTH?.trim();
|
|
67
|
-
if (!value) return undefined;
|
|
68
|
-
const parsed = Number(value);
|
|
69
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Anthropic-safe variant of the discovery cap. The Anthropic stream converter
|
|
73
|
-
// in `packages/ai/src/providers/anthropic.ts` derives the request limit as
|
|
74
|
-
// `(model.maxTokens / 3) | 0`, so the 32K default would surface as 10,922
|
|
75
|
-
// requested output tokens — above the 8,192 hard cap on classic Claude 3.x
|
|
76
|
-
// Sonnet/Haiku/Opus endpoints. Discovered models routed through
|
|
77
|
-
// `anthropic-messages` (proxy `supported_endpoint_types: ["anthropic"]` or a
|
|
78
|
-
// custom provider with `api: anthropic-messages` + openai-models-list
|
|
79
|
-
// discovery) fall back to this conservative value.
|
|
80
|
-
const DISCOVERY_DEFAULT_MAX_TOKENS_ANTHROPIC = 8_192;
|
|
81
|
-
|
|
82
|
-
/** Routes discovered-model `maxTokens` defaults around Anthropic's 3× output divisor. */
|
|
83
|
-
function discoveryDefaultMaxTokens(api: Api | undefined): number {
|
|
84
|
-
return api === "anthropic-messages" ? DISCOVERY_DEFAULT_MAX_TOKENS_ANTHROPIC : DISCOVERY_DEFAULT_MAX_TOKENS;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
28
|
const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
|
|
88
29
|
"google-antigravity",
|
|
89
30
|
"google-gemini-cli",
|
|
@@ -98,35 +39,37 @@ const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
|
|
|
98
39
|
import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
99
40
|
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
100
41
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
|
|
101
|
-
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
102
|
-
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
103
|
-
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
104
|
-
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
105
|
-
import { type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
|
|
106
|
-
import { type ConfigError, ConfigFile } from "./config-file";
|
|
107
42
|
import {
|
|
108
43
|
buildCanonicalModelIndex,
|
|
44
|
+
buildCanonicalModelOrder,
|
|
45
|
+
buildModelProviderPriorityRank,
|
|
109
46
|
type CanonicalModelIndex,
|
|
110
47
|
type CanonicalModelRecord,
|
|
111
48
|
type CanonicalModelVariant,
|
|
49
|
+
type CanonicalVariantPreferences,
|
|
112
50
|
formatCanonicalVariantSelector,
|
|
51
|
+
getBundledCanonicalReferenceData,
|
|
52
|
+
getBundledModelReferenceIndex,
|
|
113
53
|
type ModelEquivalenceConfig,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
} from "./
|
|
121
|
-
import {
|
|
54
|
+
resolveCanonicalVariant,
|
|
55
|
+
resolveModelReference,
|
|
56
|
+
} from "@oh-my-pi/pi-catalog/identity";
|
|
57
|
+
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
58
|
+
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
59
|
+
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
60
|
+
import { type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
|
|
61
|
+
import type { ConfigError, ConfigFile } from "./config-file";
|
|
122
62
|
import {
|
|
123
|
-
|
|
124
|
-
type
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
63
|
+
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
64
|
+
type DiscoveryContext,
|
|
65
|
+
type DiscoveryProviderConfig,
|
|
66
|
+
discoverModelsByProviderType,
|
|
67
|
+
getImplicitOllamaBaseUrl,
|
|
68
|
+
getOllamaContextLengthOverride,
|
|
69
|
+
} from "./model-discovery";
|
|
70
|
+
import { ModelsConfigFile, type ProviderValidationModel, validateProviderConfiguration } from "./models-config";
|
|
71
|
+
import type { ModelOverride, ModelsConfig, ProviderAuthMode } from "./models-config-schema";
|
|
72
|
+
import { settings } from "./settings";
|
|
130
73
|
|
|
131
74
|
export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
|
|
132
75
|
|
|
@@ -136,196 +79,13 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
|
|
|
136
79
|
return Boolean(apiKey) && apiKey !== kNoAuth;
|
|
137
80
|
}
|
|
138
81
|
|
|
139
|
-
export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "task";
|
|
140
|
-
|
|
141
|
-
export interface ModelRoleInfo {
|
|
142
|
-
tag?: string;
|
|
143
|
-
name: string;
|
|
144
|
-
color?: ThemeColor;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
148
|
-
default: { tag: "DEFAULT", name: "Default", color: "success" },
|
|
149
|
-
smol: { tag: "SMOL", name: "Fast", color: "warning" },
|
|
150
|
-
slow: { tag: "SLOW", name: "Thinking", color: "accent" },
|
|
151
|
-
vision: { tag: "VISION", name: "Vision", color: "error" },
|
|
152
|
-
plan: { tag: "PLAN", name: "Architect", color: "muted" },
|
|
153
|
-
designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
|
|
154
|
-
commit: { tag: "COMMIT", name: "Commit", color: "dim" },
|
|
155
|
-
task: { tag: "TASK", name: "Subtask", color: "muted" },
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "designer", "commit", "task"];
|
|
159
|
-
|
|
160
|
-
/** Alias for ModelRoleInfo - used for both built-in and custom roles */
|
|
161
|
-
export type RoleInfo = ModelRoleInfo;
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Return the canonical set of known roles for selector/carousel UI.
|
|
165
|
-
*
|
|
166
|
-
* Built-ins always come first. Configured cycle order, model assignments, and
|
|
167
|
-
* tag metadata can introduce additional custom roles without requiring duplicate
|
|
168
|
-
* entries across settings.
|
|
169
|
-
*/
|
|
170
|
-
export function getKnownRoleIds(settings: Settings): string[] {
|
|
171
|
-
const roles = [...MODEL_ROLE_IDS] as string[];
|
|
172
|
-
const seen = new Set<string>(roles);
|
|
173
|
-
const addRole = (role: string) => {
|
|
174
|
-
if (seen.has(role)) return;
|
|
175
|
-
seen.add(role);
|
|
176
|
-
roles.push(role);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
for (const role of settings.get("cycleOrder")) addRole(role);
|
|
180
|
-
for (const role of Object.keys(settings.getModelRoles())) addRole(role);
|
|
181
|
-
for (const role of Object.keys(settings.get("modelTags"))) addRole(role);
|
|
182
|
-
|
|
183
|
-
return roles;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Get role info for a role name (built-in or custom).
|
|
188
|
-
* Configured metadata overrides built-in defaults when present.
|
|
189
|
-
*/
|
|
190
|
-
export function getRoleInfo(role: string, settings: Settings): RoleInfo {
|
|
191
|
-
const builtIn = role in MODEL_ROLES ? MODEL_ROLES[role as ModelRole] : undefined;
|
|
192
|
-
const configured = settings.get("modelTags")[role];
|
|
193
|
-
|
|
194
|
-
if (configured) {
|
|
195
|
-
return {
|
|
196
|
-
tag: builtIn?.tag,
|
|
197
|
-
name: configured.name || builtIn?.name || role,
|
|
198
|
-
color: configured.color && isValidThemeColor(configured.color) ? configured.color : builtIn?.color,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (builtIn) return builtIn;
|
|
203
|
-
|
|
204
|
-
return { name: role, color: "muted" };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
type ProviderValidationMode = "models-config" | "runtime-register";
|
|
208
|
-
|
|
209
|
-
interface ProviderValidationModel {
|
|
210
|
-
id: string;
|
|
211
|
-
api?: Api;
|
|
212
|
-
contextWindow?: number;
|
|
213
|
-
maxTokens?: number;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
interface ProviderValidationConfig {
|
|
217
|
-
baseUrl?: string;
|
|
218
|
-
headers?: Record<string, string>;
|
|
219
|
-
apiKey?: string;
|
|
220
|
-
api?: Api;
|
|
221
|
-
auth?: ProviderAuthMode;
|
|
222
|
-
oauthConfigured?: boolean;
|
|
223
|
-
discovery?: ProviderDiscovery;
|
|
224
|
-
compat?: Model<Api>["compat"];
|
|
225
|
-
disableStrictTools?: boolean;
|
|
226
|
-
modelOverrides?: Record<string, unknown>;
|
|
227
|
-
models: ProviderValidationModel[];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function validateProviderConfiguration(
|
|
231
|
-
providerName: string,
|
|
232
|
-
config: ProviderValidationConfig,
|
|
233
|
-
mode: ProviderValidationMode,
|
|
234
|
-
): void {
|
|
235
|
-
const hasProviderApi = !!config.api;
|
|
236
|
-
const models = config.models;
|
|
237
|
-
|
|
238
|
-
if (models.length === 0) {
|
|
239
|
-
if (mode === "models-config") {
|
|
240
|
-
const hasModelOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0;
|
|
241
|
-
if (
|
|
242
|
-
!config.baseUrl &&
|
|
243
|
-
!config.headers &&
|
|
244
|
-
!config.compat &&
|
|
245
|
-
!config.apiKey &&
|
|
246
|
-
!config.disableStrictTools &&
|
|
247
|
-
!hasModelOverrides &&
|
|
248
|
-
!config.discovery
|
|
249
|
-
) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
} else {
|
|
256
|
-
if (!config.baseUrl) {
|
|
257
|
-
throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
|
|
258
|
-
}
|
|
259
|
-
const requiresAuth =
|
|
260
|
-
mode === "runtime-register"
|
|
261
|
-
? !config.apiKey && !config.oauthConfigured
|
|
262
|
-
: !config.apiKey && (config.auth ?? "apiKey") !== "none";
|
|
263
|
-
if (requiresAuth) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
mode === "runtime-register"
|
|
266
|
-
? `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`
|
|
267
|
-
: `Provider ${providerName}: "apiKey" is required when defining custom models unless auth is "none".`,
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (mode === "models-config" && config.discovery && !config.api && config.discovery.type !== "proxy") {
|
|
273
|
-
throw new Error(`Provider ${providerName}: "api" is required when discovery is enabled at provider level.`);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const modelDef of models) {
|
|
277
|
-
if (!hasProviderApi && !modelDef.api) {
|
|
278
|
-
throw new Error(
|
|
279
|
-
mode === "runtime-register"
|
|
280
|
-
? `Provider ${providerName}, model ${modelDef.id}: no "api" specified.`
|
|
281
|
-
: `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
if (!modelDef.id) {
|
|
285
|
-
throw new Error(`Provider ${providerName}: model missing "id"`);
|
|
286
|
-
}
|
|
287
|
-
if (mode === "models-config") {
|
|
288
|
-
if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0) {
|
|
289
|
-
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
|
|
290
|
-
}
|
|
291
|
-
if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0) {
|
|
292
|
-
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
|
|
299
|
-
"models",
|
|
300
|
-
config => {
|
|
301
|
-
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
302
|
-
validateProviderConfiguration(
|
|
303
|
-
providerName,
|
|
304
|
-
{
|
|
305
|
-
baseUrl: providerConfig.baseUrl,
|
|
306
|
-
headers: providerConfig.headers,
|
|
307
|
-
apiKey: providerConfig.apiKey,
|
|
308
|
-
api: providerConfig.api as Api | undefined,
|
|
309
|
-
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
|
|
310
|
-
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
|
|
311
|
-
compat: providerConfig.compat,
|
|
312
|
-
disableStrictTools: providerConfig.disableStrictTools,
|
|
313
|
-
modelOverrides: providerConfig.modelOverrides,
|
|
314
|
-
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
315
|
-
},
|
|
316
|
-
"models-config",
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
);
|
|
321
|
-
|
|
322
82
|
/** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
|
|
323
83
|
interface ProviderOverride {
|
|
324
84
|
baseUrl?: string;
|
|
325
85
|
headers?: Record<string, string>;
|
|
326
86
|
apiKey?: string;
|
|
327
87
|
authHeader?: boolean;
|
|
328
|
-
compat?:
|
|
88
|
+
compat?: ModelSpec<Api>["compat"];
|
|
329
89
|
transport?: Model<Api>["transport"];
|
|
330
90
|
}
|
|
331
91
|
|
|
@@ -351,19 +111,21 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
351
111
|
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
|
|
352
112
|
): Model<TApi> {
|
|
353
113
|
if (existing) {
|
|
354
|
-
return {
|
|
114
|
+
return buildModel({
|
|
355
115
|
...model,
|
|
356
116
|
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
|
357
117
|
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
|
358
|
-
|
|
118
|
+
compat: model.compatConfig,
|
|
119
|
+
} as ModelSpec<TApi>);
|
|
359
120
|
}
|
|
360
121
|
if (providerOverride) {
|
|
361
|
-
return {
|
|
122
|
+
return buildModel({
|
|
362
123
|
...model,
|
|
363
124
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
364
125
|
headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
|
|
365
126
|
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
366
|
-
|
|
127
|
+
compat: model.compatConfig,
|
|
128
|
+
} as ModelSpec<TApi>);
|
|
367
129
|
}
|
|
368
130
|
return model;
|
|
369
131
|
}
|
|
@@ -378,7 +140,7 @@ function isAuthoritativeProjectCatalogModel(model: Model<Api>): boolean {
|
|
|
378
140
|
return (
|
|
379
141
|
model.provider === "google-vertex" &&
|
|
380
142
|
model.api === "openai-completions" &&
|
|
381
|
-
model.baseUrl
|
|
143
|
+
isVertexExpressOpenAIUrl(model.baseUrl)
|
|
382
144
|
);
|
|
383
145
|
}
|
|
384
146
|
|
|
@@ -396,14 +158,32 @@ function dropProviderModels(models: readonly Model<Api>[], providers: ReadonlySe
|
|
|
396
158
|
return models.filter(model => !providers.has(model.provider));
|
|
397
159
|
}
|
|
398
160
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Merge `incoming` entries into a copy of `base`, keyed by `provider`+`id`.
|
|
163
|
+
* Matches are replaced with `combine(existing, entry)`; new entries are
|
|
164
|
+
* appended as `combine(undefined, entry)`.
|
|
165
|
+
*/
|
|
166
|
+
function mergeByModelKey<T extends { provider: string; id: string }>(
|
|
167
|
+
base: readonly Model<Api>[],
|
|
168
|
+
incoming: readonly T[],
|
|
169
|
+
combine: (existing: Model<Api> | undefined, entry: T) => Model<Api>,
|
|
170
|
+
): Model<Api>[] {
|
|
171
|
+
const merged = [...base];
|
|
172
|
+
const indexByKey = new Map<string, number>();
|
|
173
|
+
for (let i = 0; i < merged.length; i += 1) {
|
|
174
|
+
indexByKey.set(`${merged[i].provider}\u0000${merged[i].id}`, i);
|
|
175
|
+
}
|
|
176
|
+
for (const entry of incoming) {
|
|
177
|
+
const key = `${entry.provider}\u0000${entry.id}`;
|
|
178
|
+
const existingIndex = indexByKey.get(key);
|
|
179
|
+
if (existingIndex !== undefined) {
|
|
180
|
+
merged[existingIndex] = combine(merged[existingIndex], entry);
|
|
181
|
+
} else {
|
|
182
|
+
merged.push(combine(undefined, entry));
|
|
183
|
+
indexByKey.set(key, merged.length - 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return merged;
|
|
407
187
|
}
|
|
408
188
|
|
|
409
189
|
interface BuiltInDiscoveryResult {
|
|
@@ -447,78 +227,50 @@ interface CustomModelsResult {
|
|
|
447
227
|
found: boolean;
|
|
448
228
|
}
|
|
449
229
|
|
|
450
|
-
|
|
451
|
-
reasoning: boolean;
|
|
452
|
-
input: ("text" | "image")[];
|
|
453
|
-
contextWindow?: number;
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
type LlamaCppDiscoveredServerMetadata = {
|
|
457
|
-
contextWindow?: number;
|
|
458
|
-
input?: ("text" | "image")[];
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Resolve an API key config value to an actual key.
|
|
463
|
-
* Checks environment variable first, then treats as literal.
|
|
464
|
-
*/
|
|
465
|
-
function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
466
|
-
const envValue = Bun.env[keyConfig];
|
|
467
|
-
if (envValue) return envValue;
|
|
468
|
-
return keyConfig;
|
|
469
|
-
}
|
|
230
|
+
const commandValueCache = new Map<string, string>();
|
|
470
231
|
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
return value;
|
|
474
|
-
}
|
|
475
|
-
if (typeof value === "string" && value.trim()) {
|
|
476
|
-
const parsed = Number(value);
|
|
477
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
478
|
-
return parsed;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return undefined;
|
|
232
|
+
function isCommandConfigValue(valueConfig: string | undefined): valueConfig is string {
|
|
233
|
+
return valueConfig?.startsWith("!") === true;
|
|
482
234
|
}
|
|
483
235
|
|
|
484
|
-
function
|
|
485
|
-
const
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const parameters = payload.parameters;
|
|
498
|
-
if (typeof parameters !== "string") {
|
|
236
|
+
function resolveCommandConfig(command: string): string | undefined {
|
|
237
|
+
const cached = commandValueCache.get(command);
|
|
238
|
+
if (cached !== undefined) return cached;
|
|
239
|
+
try {
|
|
240
|
+
const stdout = execSync(command, { encoding: "utf8", timeout: 10_000, windowsHide: true });
|
|
241
|
+
const trimmed = stdout.trim();
|
|
242
|
+
if (trimmed.length === 0) return undefined;
|
|
243
|
+
commandValueCache.set(command, trimmed);
|
|
244
|
+
return trimmed;
|
|
245
|
+
} catch {
|
|
499
246
|
return undefined;
|
|
500
247
|
}
|
|
501
|
-
const match = parameters.match(/(?:^|\n)\s*num_ctx\s+(\d+)\s*(?:$|\n)/m);
|
|
502
|
-
return match ? toPositiveNumberOrUndefined(match[1]) : undefined;
|
|
503
248
|
}
|
|
504
249
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
250
|
+
interface CommandApiKeyResolution {
|
|
251
|
+
configured: boolean;
|
|
252
|
+
value?: string;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Resolve a models.yml secret/config value to an actual value.
|
|
256
|
+
* `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
|
|
257
|
+
* checked first and the input falls back to a literal value.
|
|
258
|
+
*/
|
|
259
|
+
function resolveConfigValue(valueConfig: string): string | undefined {
|
|
260
|
+
if (valueConfig.startsWith("!")) return resolveCommandConfig(valueConfig.slice(1).trim());
|
|
261
|
+
const envValue = Bun.env[valueConfig];
|
|
262
|
+
if (envValue) return envValue;
|
|
263
|
+
return valueConfig;
|
|
514
264
|
}
|
|
515
265
|
|
|
516
|
-
function
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
266
|
+
function resolveConfigHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
267
|
+
if (!headers) return undefined;
|
|
268
|
+
const resolved: Record<string, string> = {};
|
|
269
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
270
|
+
const next = resolveConfigValue(value);
|
|
271
|
+
if (next) resolved[key] = next;
|
|
520
272
|
}
|
|
521
|
-
return
|
|
273
|
+
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
522
274
|
}
|
|
523
275
|
|
|
524
276
|
function extractGoogleOAuthToken(value: string | undefined): string | undefined {
|
|
@@ -579,73 +331,99 @@ function mergeCompat<TBase extends object, TOverride extends object>(
|
|
|
579
331
|
return merged as TBase & TOverride;
|
|
580
332
|
}
|
|
581
333
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
590
|
-
if (override.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = override.omitMaxOutputTokens;
|
|
591
|
-
if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
|
|
592
|
-
if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
|
|
593
|
-
if (override.cost) {
|
|
594
|
-
result.cost = {
|
|
595
|
-
input: override.cost.input ?? model.cost.input,
|
|
596
|
-
output: override.cost.output ?? model.cost.output,
|
|
597
|
-
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
|
|
598
|
-
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
if (override.headers) {
|
|
602
|
-
result.headers = { ...model.headers, ...override.headers };
|
|
603
|
-
}
|
|
604
|
-
result.compat = mergeCompat(model.compat, override.compat);
|
|
605
|
-
return enrichModelThinking(result);
|
|
334
|
+
/**
|
|
335
|
+
* Project a built model back to spec shape for the model-manager/cache
|
|
336
|
+
* boundary: sparse compat comes from `compatConfig`, never from the resolved
|
|
337
|
+
* record.
|
|
338
|
+
*/
|
|
339
|
+
function toModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
|
|
340
|
+
return { ...model, compat: model.compatConfig } as ModelSpec<TApi>;
|
|
606
341
|
}
|
|
607
342
|
|
|
608
|
-
|
|
609
|
-
|
|
343
|
+
/**
|
|
344
|
+
* The patchable subset of `Model` fields shared by `modelOverrides` entries,
|
|
345
|
+
* custom model definitions, and parsed custom-model overlays. `undefined`
|
|
346
|
+
* always means "leave the base value alone".
|
|
347
|
+
*/
|
|
348
|
+
interface ModelPatch {
|
|
610
349
|
name?: string;
|
|
611
|
-
api?: Api;
|
|
612
|
-
baseUrl?: string;
|
|
613
350
|
reasoning?: boolean;
|
|
614
351
|
thinking?: ThinkingConfig;
|
|
615
352
|
input?: ("text" | "image")[];
|
|
616
|
-
cost?:
|
|
353
|
+
cost?: Partial<Model<Api>["cost"]>;
|
|
617
354
|
contextWindow?: number;
|
|
618
355
|
maxTokens?: number;
|
|
619
356
|
omitMaxOutputTokens?: boolean;
|
|
620
357
|
headers?: Record<string, string>;
|
|
621
|
-
compat?:
|
|
358
|
+
compat?: ModelSpec<Api>["compat"];
|
|
622
359
|
contextPromotionTarget?: string;
|
|
623
360
|
premiumMultiplier?: number;
|
|
624
361
|
}
|
|
625
362
|
|
|
363
|
+
/**
|
|
364
|
+
* How a patch treats the base model's transport metadata (headers/compat):
|
|
365
|
+
* - `merge`: fold the patch into the base's (modelOverrides semantics).
|
|
366
|
+
* - `replace`: the patch owns transport wholesale — same-id custom definitions
|
|
367
|
+
* already folded provider-level headers/compat in during parsing, so bundled
|
|
368
|
+
* transport metadata must not be re-merged (see `#mergeCustomModels`).
|
|
369
|
+
*/
|
|
370
|
+
type ModelTransportPolicy = "merge" | "replace";
|
|
371
|
+
|
|
372
|
+
function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTransportPolicy): Model<Api> {
|
|
373
|
+
const result = { ...base };
|
|
374
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
375
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
376
|
+
if (patch.thinking !== undefined) result.thinking = patch.thinking;
|
|
377
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
378
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
379
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
380
|
+
if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
|
|
381
|
+
if (patch.contextPromotionTarget !== undefined) result.contextPromotionTarget = patch.contextPromotionTarget;
|
|
382
|
+
if (patch.premiumMultiplier !== undefined) result.premiumMultiplier = patch.premiumMultiplier;
|
|
383
|
+
if (patch.cost) {
|
|
384
|
+
result.cost = {
|
|
385
|
+
input: patch.cost.input ?? base.cost.input,
|
|
386
|
+
output: patch.cost.output ?? base.cost.output,
|
|
387
|
+
cacheRead: patch.cost.cacheRead ?? base.cost.cacheRead,
|
|
388
|
+
cacheWrite: patch.cost.cacheWrite ?? base.cost.cacheWrite,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
let compat: ModelSpec<Api>["compat"];
|
|
392
|
+
if (transport === "merge") {
|
|
393
|
+
if (patch.headers) {
|
|
394
|
+
result.headers = { ...base.headers, ...patch.headers };
|
|
395
|
+
}
|
|
396
|
+
compat = mergeCompat(base.compatConfig, patch.compat);
|
|
397
|
+
} else {
|
|
398
|
+
result.headers = patch.headers;
|
|
399
|
+
compat = patch.compat;
|
|
400
|
+
}
|
|
401
|
+
return buildModel({ ...result, compat } as ModelSpec<Api>);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
|
405
|
+
return applyModelPatch(model, override as ModelPatch, "merge");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
interface CustomModelDefinitionLike extends ModelPatch {
|
|
409
|
+
id: string;
|
|
410
|
+
api?: Api;
|
|
411
|
+
baseUrl?: string;
|
|
412
|
+
cost?: Model<Api>["cost"];
|
|
413
|
+
}
|
|
414
|
+
|
|
626
415
|
interface CustomModelBuildOptions {
|
|
627
416
|
useDefaults: boolean;
|
|
628
417
|
}
|
|
629
418
|
|
|
630
|
-
|
|
419
|
+
interface CustomModelOverlay extends ModelPatch {
|
|
631
420
|
id: string;
|
|
632
421
|
provider: string;
|
|
633
422
|
api: Api;
|
|
634
423
|
baseUrl: string;
|
|
635
|
-
|
|
636
|
-
reasoning?: boolean;
|
|
637
|
-
thinking?: ThinkingConfig;
|
|
638
|
-
input?: ("text" | "image")[];
|
|
639
|
-
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
640
|
-
contextWindow?: number;
|
|
641
|
-
maxTokens?: number;
|
|
642
|
-
omitMaxOutputTokens?: boolean;
|
|
643
|
-
headers?: Record<string, string>;
|
|
644
|
-
compat?: Model<Api>["compat"];
|
|
645
|
-
contextPromotionTarget?: string;
|
|
646
|
-
premiumMultiplier?: number;
|
|
424
|
+
cost?: Model<Api>["cost"];
|
|
647
425
|
isOAuth?: boolean;
|
|
648
|
-
}
|
|
426
|
+
}
|
|
649
427
|
|
|
650
428
|
function mergeCustomModelHeaders(
|
|
651
429
|
providerHeaders: Record<string, string> | undefined,
|
|
@@ -653,7 +431,8 @@ function mergeCustomModelHeaders(
|
|
|
653
431
|
authHeader: boolean | undefined,
|
|
654
432
|
apiKeyConfig: string | undefined,
|
|
655
433
|
): Record<string, string> | undefined {
|
|
656
|
-
|
|
434
|
+
const resolvedModelHeaders = resolveConfigHeaders(modelHeaders);
|
|
435
|
+
return mergeAuthHeader({ ...providerHeaders, ...resolvedModelHeaders }, authHeader, apiKeyConfig);
|
|
657
436
|
}
|
|
658
437
|
|
|
659
438
|
function mergeAuthHeader(
|
|
@@ -665,7 +444,7 @@ function mergeAuthHeader(
|
|
|
665
444
|
if (!authHeader || !apiKeyConfig) {
|
|
666
445
|
return nextHeaders;
|
|
667
446
|
}
|
|
668
|
-
const resolvedKey =
|
|
447
|
+
const resolvedKey = resolveConfigValue(apiKeyConfig);
|
|
669
448
|
return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
|
|
670
449
|
}
|
|
671
450
|
|
|
@@ -692,7 +471,7 @@ function buildCustomModelOverlay(
|
|
|
692
471
|
providerHeaders: Record<string, string> | undefined,
|
|
693
472
|
providerApiKey: string | undefined,
|
|
694
473
|
authHeader: boolean | undefined,
|
|
695
|
-
providerCompat:
|
|
474
|
+
providerCompat: ModelSpec<Api>["compat"] | undefined,
|
|
696
475
|
providerAuth: ProviderAuthMode | undefined,
|
|
697
476
|
modelDef: CustomModelDefinitionLike,
|
|
698
477
|
): CustomModelOverlay | undefined {
|
|
@@ -705,8 +484,8 @@ function buildCustomModelOverlay(
|
|
|
705
484
|
baseUrl: modelDef.baseUrl ?? providerBaseUrl,
|
|
706
485
|
name: modelDef.name,
|
|
707
486
|
reasoning: modelDef.reasoning,
|
|
708
|
-
thinking: modelDef.thinking
|
|
709
|
-
input: modelDef.input
|
|
487
|
+
thinking: modelDef.thinking,
|
|
488
|
+
input: modelDef.input,
|
|
710
489
|
cost: modelDef.cost,
|
|
711
490
|
contextWindow: modelDef.contextWindow,
|
|
712
491
|
maxTokens: modelDef.maxTokens,
|
|
@@ -719,125 +498,6 @@ function buildCustomModelOverlay(
|
|
|
719
498
|
};
|
|
720
499
|
}
|
|
721
500
|
|
|
722
|
-
// Custom provider entries often front a known upstream model through a local proxy.
|
|
723
|
-
// Use bundled metadata for missing pricing/capability fields, but keep the custom transport.
|
|
724
|
-
function shouldReplaceCustomReference(existing: Model<Api> | undefined, candidate: Model<Api>): boolean {
|
|
725
|
-
if (!existing) return true;
|
|
726
|
-
if (candidate.contextWindow !== existing.contextWindow) {
|
|
727
|
-
return candidate.contextWindow > existing.contextWindow;
|
|
728
|
-
}
|
|
729
|
-
if (candidate.maxTokens !== existing.maxTokens) {
|
|
730
|
-
return candidate.maxTokens > existing.maxTokens;
|
|
731
|
-
}
|
|
732
|
-
const existingHasCachePricing = existing.cost.cacheRead > 0 || existing.cost.cacheWrite > 0;
|
|
733
|
-
const candidateHasCachePricing = candidate.cost.cacheRead > 0 || candidate.cost.cacheWrite > 0;
|
|
734
|
-
if (candidateHasCachePricing !== existingHasCachePricing) {
|
|
735
|
-
return candidateHasCachePricing;
|
|
736
|
-
}
|
|
737
|
-
return existing.provider !== "openai" && candidate.provider === "openai";
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function normalizeCustomReferenceKey(value: string): string {
|
|
741
|
-
return value.trim().toLowerCase();
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function buildCustomReferenceMap(): Map<string, Model<Api>> {
|
|
745
|
-
const references = new Map<string, Model<Api>>();
|
|
746
|
-
for (const provider of getBundledProviders()) {
|
|
747
|
-
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
|
748
|
-
const candidate = model as Model<Api>;
|
|
749
|
-
const key = normalizeCustomReferenceKey(candidate.id);
|
|
750
|
-
if (shouldReplaceCustomReference(references.get(key), candidate)) {
|
|
751
|
-
references.set(key, candidate);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
return references;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function buildCustomReferenceSuffixAliasMap(exactReferences: ReadonlyMap<string, Model<Api>>): Map<string, Model<Api>> {
|
|
759
|
-
const aliases = new Map<string, Model<Api>>();
|
|
760
|
-
for (const reference of exactReferences.values()) {
|
|
761
|
-
const slashIndex = reference.id.lastIndexOf("/");
|
|
762
|
-
if (slashIndex === -1) {
|
|
763
|
-
continue;
|
|
764
|
-
}
|
|
765
|
-
const suffix = reference.id.slice(slashIndex + 1);
|
|
766
|
-
const alias = getLongestModelLikeIdSegment(suffix);
|
|
767
|
-
if (!alias) {
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
if (shouldReplaceCustomReference(aliases.get(alias), reference)) {
|
|
771
|
-
aliases.set(alias, reference);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return aliases;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const customReferenceMap = buildCustomReferenceMap();
|
|
778
|
-
const customReferenceSuffixAliasMap = buildCustomReferenceSuffixAliasMap(customReferenceMap);
|
|
779
|
-
|
|
780
|
-
const CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN =
|
|
781
|
-
/[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4|search)$/i;
|
|
782
|
-
|
|
783
|
-
function stripCustomReferenceTrailingMarker(candidate: string): string | undefined {
|
|
784
|
-
const match = CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN.exec(candidate);
|
|
785
|
-
return match ? candidate.slice(0, match.index) : undefined;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function getCustomReferenceCandidateIds(modelId: string): string[] {
|
|
789
|
-
const candidates = new Set<string>();
|
|
790
|
-
const queue = [modelId];
|
|
791
|
-
for (let index = 0; index < queue.length; index += 1) {
|
|
792
|
-
const candidate = queue[index]?.trim();
|
|
793
|
-
if (!candidate || candidates.has(candidate)) continue;
|
|
794
|
-
candidates.add(candidate);
|
|
795
|
-
|
|
796
|
-
for (const stripped of getBracketStrippedModelIdCandidates(candidate)) {
|
|
797
|
-
queue.push(stripped);
|
|
798
|
-
}
|
|
799
|
-
for (const segment of getModelLikeIdSegments(candidate)) {
|
|
800
|
-
queue.push(segment);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
for (const suffix of [":cloud", "-cloud"] as const) {
|
|
804
|
-
if (candidate.toLowerCase().endsWith(suffix)) {
|
|
805
|
-
queue.push(candidate.slice(0, -suffix.length));
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const slashIndex = candidate.lastIndexOf("/");
|
|
810
|
-
if (slashIndex !== -1) {
|
|
811
|
-
queue.push(candidate.slice(slashIndex + 1));
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const colonToDash = candidate.replace(/:/g, "-");
|
|
815
|
-
if (colonToDash !== candidate) {
|
|
816
|
-
queue.push(colonToDash);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const lowercased = candidate.toLowerCase();
|
|
820
|
-
if (lowercased !== candidate) {
|
|
821
|
-
queue.push(lowercased);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
const strippedMarker = stripCustomReferenceTrailingMarker(candidate);
|
|
825
|
-
if (strippedMarker) {
|
|
826
|
-
queue.push(strippedMarker);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return [...candidates];
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
|
|
833
|
-
for (const candidate of getCustomReferenceCandidateIds(modelId)) {
|
|
834
|
-
const key = normalizeCustomReferenceKey(candidate);
|
|
835
|
-
const reference = customReferenceMap.get(key) ?? customReferenceSuffixAliasMap.get(key);
|
|
836
|
-
if (reference) return reference;
|
|
837
|
-
}
|
|
838
|
-
return undefined;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
501
|
function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
|
|
842
502
|
if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
|
|
843
503
|
return model;
|
|
@@ -847,13 +507,15 @@ function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomMo
|
|
|
847
507
|
|
|
848
508
|
function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
|
|
849
509
|
const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
|
|
850
|
-
const reference = options.useDefaults
|
|
510
|
+
const reference = options.useDefaults
|
|
511
|
+
? resolveModelReference(resolvedModel.id, getBundledModelReferenceIndex())
|
|
512
|
+
: undefined;
|
|
851
513
|
const cost =
|
|
852
514
|
resolvedModel.cost ??
|
|
853
515
|
reference?.cost ??
|
|
854
516
|
(options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
|
855
517
|
const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
|
|
856
|
-
return
|
|
518
|
+
return buildModel({
|
|
857
519
|
id: resolvedModel.id,
|
|
858
520
|
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
859
521
|
api: resolvedModel.api,
|
|
@@ -868,11 +530,11 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
868
530
|
maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : undefined),
|
|
869
531
|
headers: resolvedModel.headers,
|
|
870
532
|
omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
|
|
871
|
-
compat: mergeCompat(reference?.
|
|
533
|
+
compat: mergeCompat(reference?.compatConfig, resolvedModel.compat),
|
|
872
534
|
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
873
535
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
874
536
|
isOAuth: resolvedModel.isOAuth,
|
|
875
|
-
} as
|
|
537
|
+
} as ModelSpec<Api>);
|
|
876
538
|
}
|
|
877
539
|
|
|
878
540
|
function normalizeSuppressedSelector(selector: string): string {
|
|
@@ -935,6 +597,28 @@ export class ModelRegistry {
|
|
|
935
597
|
#rebuildSuspended: number = 0;
|
|
936
598
|
#fetch: FetchImpl;
|
|
937
599
|
|
|
600
|
+
#resolveCommandBackedApiKey(provider: string): CommandApiKeyResolution {
|
|
601
|
+
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
602
|
+
if (!isCommandConfigValue(keyConfig)) return { configured: false };
|
|
603
|
+
const value = resolveConfigValue(keyConfig);
|
|
604
|
+
if (value) {
|
|
605
|
+
this.authStorage.setConfigApiKey(provider, value);
|
|
606
|
+
return { configured: true, value };
|
|
607
|
+
}
|
|
608
|
+
this.authStorage.removeConfigApiKey(provider);
|
|
609
|
+
return { configured: true };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
#installProviderApiKey(provider: string, keyConfig: string): void {
|
|
613
|
+
this.#customProviderApiKeys.set(provider, keyConfig);
|
|
614
|
+
const resolved = resolveConfigValue(keyConfig);
|
|
615
|
+
if (resolved) {
|
|
616
|
+
this.authStorage.setConfigApiKey(provider, resolved);
|
|
617
|
+
} else if (isCommandConfigValue(keyConfig)) {
|
|
618
|
+
this.authStorage.removeConfigApiKey(provider);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
938
622
|
/**
|
|
939
623
|
* @param authStorage - Auth storage for API key resolution
|
|
940
624
|
*
|
|
@@ -955,10 +639,8 @@ export class ModelRegistry {
|
|
|
955
639
|
// Set up fallback resolver for custom provider API keys
|
|
956
640
|
this.authStorage.setFallbackResolver(provider => {
|
|
957
641
|
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
958
|
-
if (keyConfig)
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
return undefined;
|
|
642
|
+
if (!keyConfig) return undefined;
|
|
643
|
+
return resolveConfigValue(keyConfig);
|
|
962
644
|
});
|
|
963
645
|
// Load models synchronously in constructor.
|
|
964
646
|
this.#loadModels();
|
|
@@ -1049,7 +731,7 @@ export class ModelRegistry {
|
|
|
1049
731
|
// Restore runtime API keys before #loadModels — survives because
|
|
1050
732
|
// #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
|
|
1051
733
|
for (const [k, v] of this.#runtimeProviderApiKeys) {
|
|
1052
|
-
this.#
|
|
734
|
+
this.#installProviderApiKey(k, v);
|
|
1053
735
|
}
|
|
1054
736
|
this.#providerOverrides.clear();
|
|
1055
737
|
this.#modelOverrides.clear();
|
|
@@ -1133,84 +815,46 @@ export class ModelRegistry {
|
|
|
1133
815
|
return models.map(m => {
|
|
1134
816
|
if (!providerOverride) return m;
|
|
1135
817
|
const withTransportOverride = this.#applyProviderTransportOverride(m, providerOverride);
|
|
1136
|
-
return {
|
|
818
|
+
return buildModel({
|
|
1137
819
|
...withTransportOverride,
|
|
1138
|
-
compat: mergeCompat(m.
|
|
1139
|
-
};
|
|
820
|
+
compat: mergeCompat(m.compatConfig, providerOverride.compat),
|
|
821
|
+
} as ModelSpec<Api>);
|
|
1140
822
|
});
|
|
1141
823
|
});
|
|
1142
824
|
}
|
|
1143
825
|
|
|
1144
826
|
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
merged[existingIndex] = {
|
|
1157
|
-
...replacementModel,
|
|
1158
|
-
contextWindow:
|
|
1159
|
-
replacementModel.contextWindow === UNK_CONTEXT_WINDOW
|
|
1160
|
-
? existing.contextWindow
|
|
1161
|
-
: replacementModel.contextWindow,
|
|
1162
|
-
maxTokens:
|
|
1163
|
-
replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
|
|
1164
|
-
};
|
|
1165
|
-
} else {
|
|
1166
|
-
merged.push(replacementModel);
|
|
1167
|
-
indexByKey.set(key, merged.length - 1);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
return merged;
|
|
827
|
+
return mergeByModelKey(baseModels, replacementModels, (existing, replacementModel) => {
|
|
828
|
+
if (!existing) return replacementModel;
|
|
829
|
+
return {
|
|
830
|
+
...replacementModel,
|
|
831
|
+
contextWindow:
|
|
832
|
+
replacementModel.contextWindow === UNK_CONTEXT_WINDOW
|
|
833
|
+
? existing.contextWindow
|
|
834
|
+
: replacementModel.contextWindow,
|
|
835
|
+
maxTokens: replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
|
|
836
|
+
};
|
|
837
|
+
});
|
|
1171
838
|
}
|
|
1172
839
|
|
|
1173
840
|
/** Merge custom models with built-in, replacing by provider+id match */
|
|
1174
841
|
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
for (const customModel of customModels) {
|
|
1182
|
-
const key = `${customModel.provider}\u0000${customModel.id}`;
|
|
1183
|
-
const existingIndex = indexByKey.get(key);
|
|
1184
|
-
if (existingIndex !== undefined) {
|
|
1185
|
-
const existingModel = merged[existingIndex];
|
|
1186
|
-
merged[existingIndex] = enrichModelThinking({
|
|
842
|
+
return mergeByModelKey(builtInModels, customModels, (existingModel, customModel) => {
|
|
843
|
+
if (!existingModel) return finalizeCustomModel(customModel, { useDefaults: true });
|
|
844
|
+
// Same-id custom definitions replace bundled transport behavior, so the
|
|
845
|
+
// patch is applied with the `replace` transport policy.
|
|
846
|
+
return applyModelPatch(
|
|
847
|
+
{
|
|
1187
848
|
...existingModel,
|
|
1188
849
|
id: customModel.id,
|
|
1189
850
|
provider: customModel.provider,
|
|
1190
851
|
api: customModel.api,
|
|
1191
852
|
baseUrl: customModel.baseUrl,
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
|
|
1198
|
-
maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
|
|
1199
|
-
omitMaxOutputTokens: customModel.omitMaxOutputTokens ?? existingModel.omitMaxOutputTokens,
|
|
1200
|
-
// Same-id custom definitions replace bundled transport behavior. Provider-level
|
|
1201
|
-
// headers/compat were already folded into customModel during parsing; do not
|
|
1202
|
-
// re-merge bundled transport metadata here.
|
|
1203
|
-
headers: customModel.headers,
|
|
1204
|
-
compat: customModel.compat,
|
|
1205
|
-
contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
|
1206
|
-
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
1207
|
-
} as Model<Api>);
|
|
1208
|
-
} else {
|
|
1209
|
-
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
1210
|
-
indexByKey.set(key, merged.length - 1);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
return merged;
|
|
853
|
+
},
|
|
854
|
+
customModel,
|
|
855
|
+
"replace",
|
|
856
|
+
);
|
|
857
|
+
});
|
|
1214
858
|
}
|
|
1215
859
|
|
|
1216
860
|
#loadCachedStandardProviderModels(): { models: Model<Api>[]; authoritativeFreshProviders: Set<string> } {
|
|
@@ -1236,8 +880,13 @@ export class ModelRegistry {
|
|
|
1236
880
|
? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
|
|
1237
881
|
: models;
|
|
1238
882
|
const withCompat = providerOverride?.compat
|
|
1239
|
-
? withTransport.map(model =>
|
|
1240
|
-
|
|
883
|
+
? withTransport.map(model =>
|
|
884
|
+
buildModel({
|
|
885
|
+
...model,
|
|
886
|
+
compat: mergeCompat(model.compat, providerOverride.compat),
|
|
887
|
+
} as ModelSpec<Api>),
|
|
888
|
+
)
|
|
889
|
+
: withTransport.map(model => buildModel(model));
|
|
1241
890
|
cachedModels.push(...this.#applyProviderModelOverrides(providerId, withCompat));
|
|
1242
891
|
}
|
|
1243
892
|
return { models: cachedModels, authoritativeFreshProviders };
|
|
@@ -1261,7 +910,10 @@ export class ModelRegistry {
|
|
|
1261
910
|
providerConfig.provider,
|
|
1262
911
|
this.#normalizeDiscoverableModels(
|
|
1263
912
|
providerConfig,
|
|
1264
|
-
this.#applyProviderCompat(
|
|
913
|
+
this.#applyProviderCompat(
|
|
914
|
+
providerConfig.compat,
|
|
915
|
+
cache.models.map(model => buildModel(model)),
|
|
916
|
+
),
|
|
1265
917
|
),
|
|
1266
918
|
);
|
|
1267
919
|
cachedModels.push(...models);
|
|
@@ -1277,9 +929,11 @@ export class ModelRegistry {
|
|
|
1277
929
|
return cachedModels;
|
|
1278
930
|
}
|
|
1279
931
|
|
|
1280
|
-
#applyProviderCompat(compat:
|
|
932
|
+
#applyProviderCompat(compat: ModelSpec<Api>["compat"] | undefined, models: Model<Api>[]): Model<Api>[] {
|
|
1281
933
|
if (!compat) return models;
|
|
1282
|
-
return models.map(model =>
|
|
934
|
+
return models.map(model =>
|
|
935
|
+
buildModel({ ...model, compat: mergeCompat(model.compatConfig, compat) } as ModelSpec<Api>),
|
|
936
|
+
);
|
|
1283
937
|
}
|
|
1284
938
|
|
|
1285
939
|
#normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
@@ -1289,7 +943,14 @@ export class ModelRegistry {
|
|
|
1289
943
|
|
|
1290
944
|
const contextLengthOverride = getOllamaContextLengthOverride();
|
|
1291
945
|
return models.map(model => {
|
|
1292
|
-
const normalized =
|
|
946
|
+
const normalized =
|
|
947
|
+
model.api === "openai-completions"
|
|
948
|
+
? buildModel({
|
|
949
|
+
...model,
|
|
950
|
+
api: "openai-responses" as const,
|
|
951
|
+
compat: model.compatConfig,
|
|
952
|
+
} as ModelSpec<Api>)
|
|
953
|
+
: model;
|
|
1293
954
|
if (contextLengthOverride === undefined) {
|
|
1294
955
|
return normalized;
|
|
1295
956
|
}
|
|
@@ -1372,10 +1033,11 @@ export class ModelRegistry {
|
|
|
1372
1033
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
1373
1034
|
|
|
1374
1035
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
1036
|
+
const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
|
|
1375
1037
|
// Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
|
|
1376
1038
|
if (
|
|
1377
1039
|
providerConfig.baseUrl ||
|
|
1378
|
-
|
|
1040
|
+
resolvedProviderHeaders ||
|
|
1379
1041
|
providerConfig.apiKey ||
|
|
1380
1042
|
providerConfig.authHeader !== undefined ||
|
|
1381
1043
|
providerConfig.compat ||
|
|
@@ -1385,7 +1047,7 @@ export class ModelRegistry {
|
|
|
1385
1047
|
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
1386
1048
|
overrides.set(providerName, {
|
|
1387
1049
|
baseUrl: providerConfig.baseUrl,
|
|
1388
|
-
headers:
|
|
1050
|
+
headers: resolvedProviderHeaders,
|
|
1389
1051
|
apiKey: providerConfig.apiKey,
|
|
1390
1052
|
authHeader: providerConfig.authHeader,
|
|
1391
1053
|
compat: mergeCompat(providerConfig.compat, disableStrictCompat),
|
|
@@ -1407,7 +1069,7 @@ export class ModelRegistry {
|
|
|
1407
1069
|
// fallback for entries that don't advertise one.
|
|
1408
1070
|
api: (providerConfig.api ?? "openai-completions") as Api,
|
|
1409
1071
|
baseUrl: providerConfig.baseUrl,
|
|
1410
|
-
headers:
|
|
1072
|
+
headers: resolvedProviderHeaders,
|
|
1411
1073
|
compat: mergeCompat(providerConfig.compat, disableStrictCompat),
|
|
1412
1074
|
discovery: providerConfig.discovery,
|
|
1413
1075
|
optional: false,
|
|
@@ -1419,16 +1081,17 @@ export class ModelRegistry {
|
|
|
1419
1081
|
// bearer in models.yml (e.g. for an auth-gateway baseUrl), that bearer
|
|
1420
1082
|
// must authenticate the outbound request.
|
|
1421
1083
|
if (providerConfig.apiKey) {
|
|
1422
|
-
this.#
|
|
1423
|
-
const resolved = resolveApiKeyConfig(providerConfig.apiKey);
|
|
1424
|
-
if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
|
|
1084
|
+
this.#installProviderApiKey(providerName, providerConfig.apiKey);
|
|
1425
1085
|
}
|
|
1426
1086
|
|
|
1427
1087
|
// Parse per-model overrides
|
|
1428
1088
|
if (providerConfig.modelOverrides) {
|
|
1429
1089
|
const perModel = new Map<string, ModelOverride>();
|
|
1430
1090
|
for (const [modelId, override] of Object.entries(providerConfig.modelOverrides)) {
|
|
1431
|
-
perModel.set(
|
|
1091
|
+
perModel.set(
|
|
1092
|
+
modelId,
|
|
1093
|
+
override.headers ? { ...override, headers: resolveConfigHeaders(override.headers) } : override,
|
|
1094
|
+
);
|
|
1432
1095
|
}
|
|
1433
1096
|
allModelOverrides.set(providerName, perModel);
|
|
1434
1097
|
}
|
|
@@ -1512,17 +1175,20 @@ export class ModelRegistry {
|
|
|
1512
1175
|
models: cached?.models.map(model => model.id) ?? [],
|
|
1513
1176
|
});
|
|
1514
1177
|
this.#lastDiscoveryWarnings.delete(providerConfig.provider);
|
|
1515
|
-
return cached
|
|
1178
|
+
return cached ? cached.models.map(model => buildModel(model)) : [];
|
|
1516
1179
|
}
|
|
1517
1180
|
}
|
|
1518
1181
|
|
|
1519
1182
|
const providerId = providerConfig.provider;
|
|
1520
1183
|
let discoveryError: string | undefined;
|
|
1521
|
-
const fetchDynamicModels = async (): Promise<readonly
|
|
1184
|
+
const fetchDynamicModels = async (): Promise<readonly ModelSpec<Api>[] | null> => {
|
|
1522
1185
|
try {
|
|
1523
|
-
const models =
|
|
1186
|
+
const models = this.#applyProviderModelOverrides(
|
|
1187
|
+
providerId,
|
|
1188
|
+
await discoverModelsByProviderType(providerConfig, this.#discoveryContext()),
|
|
1189
|
+
);
|
|
1524
1190
|
this.#lastDiscoveryWarnings.delete(providerId);
|
|
1525
|
-
return models;
|
|
1191
|
+
return models.map(toModelSpec);
|
|
1526
1192
|
} catch (error) {
|
|
1527
1193
|
discoveryError = error instanceof Error ? error.message : String(error);
|
|
1528
1194
|
return null;
|
|
@@ -1569,18 +1235,14 @@ export class ModelRegistry {
|
|
|
1569
1235
|
);
|
|
1570
1236
|
}
|
|
1571
1237
|
|
|
1572
|
-
#
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
return
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
return this.#discoverOpenAIModelsList(providerConfig);
|
|
1581
|
-
case "proxy":
|
|
1582
|
-
return this.#discoverProxyModels(providerConfig);
|
|
1583
|
-
}
|
|
1238
|
+
#discoveryContext(): DiscoveryContext {
|
|
1239
|
+
return {
|
|
1240
|
+
fetch: this.#fetch,
|
|
1241
|
+
getBearerApiKey: async provider => {
|
|
1242
|
+
const apiKey = await this.getApiKeyForProvider(provider);
|
|
1243
|
+
return apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1584
1246
|
}
|
|
1585
1247
|
|
|
1586
1248
|
#warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
|
|
@@ -1732,361 +1394,6 @@ export class ModelRegistry {
|
|
|
1732
1394
|
}
|
|
1733
1395
|
}
|
|
1734
1396
|
|
|
1735
|
-
async #discoverOllamaModelMetadata(
|
|
1736
|
-
endpoint: string,
|
|
1737
|
-
modelId: string,
|
|
1738
|
-
headers: Record<string, string> | undefined,
|
|
1739
|
-
): Promise<OllamaDiscoveredModelMetadata | null> {
|
|
1740
|
-
const showUrl = `${endpoint}/api/show`;
|
|
1741
|
-
try {
|
|
1742
|
-
const response = await this.#fetch(showUrl, {
|
|
1743
|
-
method: "POST",
|
|
1744
|
-
headers: { ...(headers ?? {}), "Content-Type": "application/json" },
|
|
1745
|
-
body: JSON.stringify({ model: modelId }),
|
|
1746
|
-
signal: AbortSignal.timeout(150),
|
|
1747
|
-
});
|
|
1748
|
-
if (!response.ok) {
|
|
1749
|
-
return null;
|
|
1750
|
-
}
|
|
1751
|
-
const payload = (await response.json()) as unknown;
|
|
1752
|
-
if (!isRecord(payload)) {
|
|
1753
|
-
return null;
|
|
1754
|
-
}
|
|
1755
|
-
const contextWindow = extractOllamaContextWindow(payload);
|
|
1756
|
-
const capabilities = payload.capabilities;
|
|
1757
|
-
if (Array.isArray(capabilities)) {
|
|
1758
|
-
const normalized = new Set(
|
|
1759
|
-
capabilities.flatMap(capability => (typeof capability === "string" ? [capability.toLowerCase()] : [])),
|
|
1760
|
-
);
|
|
1761
|
-
const supportsVision = normalized.has("vision") || normalized.has("image");
|
|
1762
|
-
return {
|
|
1763
|
-
reasoning: normalized.has("thinking"),
|
|
1764
|
-
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1765
|
-
contextWindow,
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
if (!isRecord(capabilities)) {
|
|
1769
|
-
return {
|
|
1770
|
-
reasoning: false,
|
|
1771
|
-
input: ["text"],
|
|
1772
|
-
contextWindow,
|
|
1773
|
-
};
|
|
1774
|
-
}
|
|
1775
|
-
const supportsVision = capabilities.vision === true || capabilities.image === true;
|
|
1776
|
-
return {
|
|
1777
|
-
reasoning: capabilities.thinking === true,
|
|
1778
|
-
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1779
|
-
contextWindow,
|
|
1780
|
-
};
|
|
1781
|
-
} catch {
|
|
1782
|
-
return null;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
async #discoverOllamaModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1787
|
-
const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
|
|
1788
|
-
const tagsUrl = `${endpoint}/api/tags`;
|
|
1789
|
-
const headers = { ...(providerConfig.headers ?? {}) };
|
|
1790
|
-
const response = await this.#fetch(tagsUrl, {
|
|
1791
|
-
headers,
|
|
1792
|
-
signal: AbortSignal.timeout(250),
|
|
1793
|
-
});
|
|
1794
|
-
if (!response.ok) {
|
|
1795
|
-
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
1796
|
-
}
|
|
1797
|
-
const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
1798
|
-
const entries = (payload.models ?? []).flatMap(item => {
|
|
1799
|
-
const id = item.model || item.name;
|
|
1800
|
-
return id ? [{ id, name: item.name || id }] : [];
|
|
1801
|
-
});
|
|
1802
|
-
const metadataById = new Map(
|
|
1803
|
-
await Promise.all(
|
|
1804
|
-
entries.map(
|
|
1805
|
-
async entry => [entry.id, await this.#discoverOllamaModelMetadata(endpoint, entry.id, headers)] as const,
|
|
1806
|
-
),
|
|
1807
|
-
),
|
|
1808
|
-
);
|
|
1809
|
-
const discovered = entries.map(entry => {
|
|
1810
|
-
const metadata = metadataById.get(entry.id);
|
|
1811
|
-
return enrichModelThinking({
|
|
1812
|
-
id: entry.id,
|
|
1813
|
-
name: entry.name,
|
|
1814
|
-
api: providerConfig.api,
|
|
1815
|
-
provider: providerConfig.provider,
|
|
1816
|
-
baseUrl: `${endpoint}/v1`,
|
|
1817
|
-
reasoning: metadata?.reasoning ?? false,
|
|
1818
|
-
input: metadata?.input ?? ["text"],
|
|
1819
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1820
|
-
contextWindow: metadata?.contextWindow ?? 128000,
|
|
1821
|
-
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
1822
|
-
headers: providerConfig.headers,
|
|
1823
|
-
});
|
|
1824
|
-
});
|
|
1825
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
async #discoverLlamaCppServerMetadata(
|
|
1829
|
-
baseUrl: string,
|
|
1830
|
-
headers: Record<string, string> | undefined,
|
|
1831
|
-
): Promise<LlamaCppDiscoveredServerMetadata | null> {
|
|
1832
|
-
const propsUrl = `${this.#toLlamaCppNativeBaseUrl(baseUrl)}/props`;
|
|
1833
|
-
try {
|
|
1834
|
-
const response = await this.#fetch(propsUrl, {
|
|
1835
|
-
headers,
|
|
1836
|
-
signal: AbortSignal.timeout(150),
|
|
1837
|
-
});
|
|
1838
|
-
if (!response.ok) {
|
|
1839
|
-
return null;
|
|
1840
|
-
}
|
|
1841
|
-
const payload = (await response.json()) as unknown;
|
|
1842
|
-
if (!isRecord(payload)) {
|
|
1843
|
-
return null;
|
|
1844
|
-
}
|
|
1845
|
-
return {
|
|
1846
|
-
contextWindow: extractLlamaCppContextWindow(payload),
|
|
1847
|
-
input: extractLlamaCppInputCapabilities(payload),
|
|
1848
|
-
};
|
|
1849
|
-
} catch {
|
|
1850
|
-
return null;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
async #discoverLlamaCppModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1855
|
-
const baseUrl = this.#normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
|
|
1856
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1857
|
-
|
|
1858
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1859
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1860
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1861
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
const [response, serverMetadata] = await Promise.all([
|
|
1865
|
-
this.#fetch(modelsUrl, {
|
|
1866
|
-
headers,
|
|
1867
|
-
signal: AbortSignal.timeout(250),
|
|
1868
|
-
}),
|
|
1869
|
-
this.#discoverLlamaCppServerMetadata(baseUrl, headers),
|
|
1870
|
-
]);
|
|
1871
|
-
if (!response.ok) {
|
|
1872
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1873
|
-
}
|
|
1874
|
-
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1875
|
-
const models = payload.data ?? [];
|
|
1876
|
-
const discovered: Model<Api>[] = [];
|
|
1877
|
-
for (const item of models) {
|
|
1878
|
-
const id = item.id;
|
|
1879
|
-
if (!id) continue;
|
|
1880
|
-
discovered.push(
|
|
1881
|
-
enrichModelThinking({
|
|
1882
|
-
id,
|
|
1883
|
-
name: id,
|
|
1884
|
-
api: providerConfig.api,
|
|
1885
|
-
provider: providerConfig.provider,
|
|
1886
|
-
baseUrl,
|
|
1887
|
-
reasoning: false,
|
|
1888
|
-
input: serverMetadata?.input ?? ["text"],
|
|
1889
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1890
|
-
contextWindow: serverMetadata?.contextWindow ?? 128000,
|
|
1891
|
-
maxTokens: Math.min(
|
|
1892
|
-
serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY,
|
|
1893
|
-
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
1894
|
-
),
|
|
1895
|
-
headers,
|
|
1896
|
-
compat: {
|
|
1897
|
-
supportsStore: false,
|
|
1898
|
-
supportsDeveloperRole: false,
|
|
1899
|
-
supportsReasoningEffort: false,
|
|
1900
|
-
},
|
|
1901
|
-
}),
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
1904
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
async #discoverOpenAIModelsList(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1908
|
-
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
1909
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1910
|
-
|
|
1911
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1912
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1913
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1914
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
const response = await this.#fetch(modelsUrl, {
|
|
1918
|
-
headers,
|
|
1919
|
-
signal: AbortSignal.timeout(10_000),
|
|
1920
|
-
});
|
|
1921
|
-
if (!response.ok) {
|
|
1922
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1923
|
-
}
|
|
1924
|
-
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1925
|
-
const models = payload.data ?? [];
|
|
1926
|
-
const discovered: Model<Api>[] = [];
|
|
1927
|
-
for (const item of models) {
|
|
1928
|
-
const id = item.id;
|
|
1929
|
-
if (!id) continue;
|
|
1930
|
-
discovered.push(
|
|
1931
|
-
enrichModelThinking({
|
|
1932
|
-
id,
|
|
1933
|
-
name: id,
|
|
1934
|
-
api: providerConfig.api,
|
|
1935
|
-
provider: providerConfig.provider,
|
|
1936
|
-
baseUrl,
|
|
1937
|
-
reasoning: false,
|
|
1938
|
-
input: ["text"],
|
|
1939
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1940
|
-
contextWindow: 128000,
|
|
1941
|
-
maxTokens: discoveryDefaultMaxTokens(providerConfig.api),
|
|
1942
|
-
headers,
|
|
1943
|
-
compat: {
|
|
1944
|
-
supportsStore: false,
|
|
1945
|
-
supportsDeveloperRole: false,
|
|
1946
|
-
supportsReasoningEffort: false,
|
|
1947
|
-
},
|
|
1948
|
-
}),
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1951
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
/**
|
|
1955
|
-
* Discover models from an Anthropic+OpenAI-compatible reseller proxy that
|
|
1956
|
-
* exposes both `/v1/messages` and `/v1/chat/completions`, advertising each
|
|
1957
|
-
* model's wire capabilities through `supported_endpoint_types` on
|
|
1958
|
-
* `GET /v1/models` (new-api / one-api-style proxies).
|
|
1959
|
-
*
|
|
1960
|
-
* Routing per model:
|
|
1961
|
-
* supported_endpoint_types: ["anthropic", ...] -> api: "anthropic-messages"
|
|
1962
|
-
* supported_endpoint_types: ["openai"] -> api: "openai-completions"
|
|
1963
|
-
* missing / neither -> provider-level api fallback
|
|
1964
|
-
*
|
|
1965
|
-
* Anthropic models share the same baseUrl; the Anthropic SDK strips a
|
|
1966
|
-
* trailing `/v1` itself before appending `/v1/messages`, so the discovery
|
|
1967
|
-
* URL (which ends in `/v1`) round-trips correctly.
|
|
1968
|
-
*/
|
|
1969
|
-
async #discoverProxyModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1970
|
-
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
1971
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1972
|
-
|
|
1973
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1974
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1975
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1976
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
const response = await this.#fetch(modelsUrl, {
|
|
1980
|
-
headers,
|
|
1981
|
-
signal: AbortSignal.timeout(10_000),
|
|
1982
|
-
});
|
|
1983
|
-
if (!response.ok) {
|
|
1984
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1985
|
-
}
|
|
1986
|
-
const payload = (await response.json()) as {
|
|
1987
|
-
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>;
|
|
1988
|
-
};
|
|
1989
|
-
const items = payload.data ?? [];
|
|
1990
|
-
const discovered: Model<Api>[] = [];
|
|
1991
|
-
for (const item of items) {
|
|
1992
|
-
const id = item.id;
|
|
1993
|
-
if (!id) continue;
|
|
1994
|
-
const endpoints = item.supported_endpoint_types ?? [];
|
|
1995
|
-
const api: Api | undefined = endpoints.includes("anthropic")
|
|
1996
|
-
? "anthropic-messages"
|
|
1997
|
-
: endpoints.includes("openai")
|
|
1998
|
-
? "openai-completions"
|
|
1999
|
-
: providerConfig.api;
|
|
2000
|
-
if (!api) continue;
|
|
2001
|
-
const isAnthropic = api === "anthropic-messages";
|
|
2002
|
-
const reference = resolveCustomModelReference(id);
|
|
2003
|
-
const discoveryName = typeof item.name === "string" ? item.name.trim() : "";
|
|
2004
|
-
const displayName =
|
|
2005
|
-
reference?.name ??
|
|
2006
|
-
(discoveryName && discoveryName !== id ? discoveryName : undefined) ??
|
|
2007
|
-
stripBracketedModelIdAffixes(id) ??
|
|
2008
|
-
id;
|
|
2009
|
-
discovered.push(
|
|
2010
|
-
enrichModelThinking({
|
|
2011
|
-
id,
|
|
2012
|
-
name: displayName,
|
|
2013
|
-
api,
|
|
2014
|
-
provider: providerConfig.provider,
|
|
2015
|
-
baseUrl,
|
|
2016
|
-
reasoning: reference?.reasoning ?? false,
|
|
2017
|
-
thinking: reference?.thinking,
|
|
2018
|
-
input: reference?.input ?? ["text"],
|
|
2019
|
-
// Proxy pricing is provider-specific and usually does not match
|
|
2020
|
-
// upstream bundled catalogs, so keep costs local-unknown even when
|
|
2021
|
-
// we successfully recover the upstream model identity.
|
|
2022
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2023
|
-
contextWindow: reference?.contextWindow ?? 128000,
|
|
2024
|
-
maxTokens: reference?.maxTokens ?? discoveryDefaultMaxTokens(api),
|
|
2025
|
-
headers,
|
|
2026
|
-
// OpenAI-compat fields are no-ops on anthropic models; the
|
|
2027
|
-
// Anthropic SDK ignores them. Provider-level disableStrictTools
|
|
2028
|
-
// flows in via #applyProviderCompat for the third-party-Anthropic
|
|
2029
|
-
// path. Cross-wire bundled compat is intentionally not copied:
|
|
2030
|
-
// request-shaping fields are provider-wire specific.
|
|
2031
|
-
compat: isAnthropic
|
|
2032
|
-
? undefined
|
|
2033
|
-
: {
|
|
2034
|
-
supportsStore: false,
|
|
2035
|
-
supportsDeveloperRole: false,
|
|
2036
|
-
supportsReasoningEffort: false,
|
|
2037
|
-
},
|
|
2038
|
-
}),
|
|
2039
|
-
);
|
|
2040
|
-
}
|
|
2041
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
#normalizeLlamaCppBaseUrl(baseUrl?: string): string {
|
|
2045
|
-
const defaultBaseUrl = "http://127.0.0.1:8080";
|
|
2046
|
-
const raw = baseUrl || defaultBaseUrl;
|
|
2047
|
-
try {
|
|
2048
|
-
const parsed = new URL(raw);
|
|
2049
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2050
|
-
return `${parsed.protocol}//${parsed.host}${trimmedPath}`;
|
|
2051
|
-
} catch {
|
|
2052
|
-
return raw;
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
#toLlamaCppNativeBaseUrl(baseUrl: string): string {
|
|
2057
|
-
try {
|
|
2058
|
-
const parsed = new URL(baseUrl);
|
|
2059
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2060
|
-
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath.slice(0, -3) || "/" : trimmedPath || "/";
|
|
2061
|
-
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2062
|
-
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2063
|
-
} catch {
|
|
2064
|
-
return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
#normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
|
|
2069
|
-
const defaultBaseUrl = "http://127.0.0.1:1234/v1";
|
|
2070
|
-
const raw = baseUrl || defaultBaseUrl;
|
|
2071
|
-
try {
|
|
2072
|
-
const parsed = new URL(raw);
|
|
2073
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2074
|
-
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
|
|
2075
|
-
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2076
|
-
} catch {
|
|
2077
|
-
return raw;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
#normalizeOllamaBaseUrl(baseUrl?: string): string {
|
|
2081
|
-
const raw = baseUrl || DEFAULT_OLLAMA_BASE_URL;
|
|
2082
|
-
try {
|
|
2083
|
-
const parsed = new URL(raw);
|
|
2084
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
2085
|
-
} catch {
|
|
2086
|
-
return DEFAULT_OLLAMA_BASE_URL;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
1397
|
#applyProviderModelOverrides(provider: string, models: Model<Api>[]): Model<Api>[] {
|
|
2091
1398
|
const overrides = this.#modelOverrides.get(provider);
|
|
2092
1399
|
if (!overrides || overrides.size === 0) return models;
|
|
@@ -2164,7 +1471,11 @@ export class ModelRegistry {
|
|
|
2164
1471
|
this.#rebuildPending = true;
|
|
2165
1472
|
return;
|
|
2166
1473
|
}
|
|
2167
|
-
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1474
|
+
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1475
|
+
this.#models,
|
|
1476
|
+
getBundledCanonicalReferenceData(),
|
|
1477
|
+
this.#equivalenceConfig,
|
|
1478
|
+
);
|
|
2168
1479
|
this.#rebuildPending = false;
|
|
2169
1480
|
}
|
|
2170
1481
|
|
|
@@ -2178,7 +1489,11 @@ export class ModelRegistry {
|
|
|
2178
1489
|
}
|
|
2179
1490
|
if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
|
|
2180
1491
|
this.#rebuildPending = false;
|
|
2181
|
-
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1492
|
+
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1493
|
+
this.#models,
|
|
1494
|
+
getBundledCanonicalReferenceData(),
|
|
1495
|
+
this.#equivalenceConfig,
|
|
1496
|
+
);
|
|
2182
1497
|
}
|
|
2183
1498
|
}
|
|
2184
1499
|
|
|
@@ -2188,10 +1503,9 @@ export class ModelRegistry {
|
|
|
2188
1503
|
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
2189
1504
|
const modelDefs = providerConfig.models ?? [];
|
|
2190
1505
|
if (modelDefs.length === 0) continue; // Override-only, no custom models
|
|
1506
|
+
const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
|
|
2191
1507
|
if (providerConfig.apiKey) {
|
|
2192
|
-
this.#
|
|
2193
|
-
const resolved = resolveApiKeyConfig(providerConfig.apiKey);
|
|
2194
|
-
if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
|
|
1508
|
+
this.#installProviderApiKey(providerName, providerConfig.apiKey);
|
|
2195
1509
|
}
|
|
2196
1510
|
for (const modelDef of modelDefs) {
|
|
2197
1511
|
const providerCompat = providerConfig.disableStrictTools
|
|
@@ -2201,7 +1515,7 @@ export class ModelRegistry {
|
|
|
2201
1515
|
providerName,
|
|
2202
1516
|
providerConfig.baseUrl!,
|
|
2203
1517
|
providerConfig.api as Api | undefined,
|
|
2204
|
-
|
|
1518
|
+
resolvedProviderHeaders,
|
|
2205
1519
|
providerConfig.apiKey,
|
|
2206
1520
|
providerConfig.authHeader,
|
|
2207
1521
|
providerCompat,
|
|
@@ -2278,53 +1592,11 @@ export class ModelRegistry {
|
|
|
2278
1592
|
});
|
|
2279
1593
|
}
|
|
2280
1594
|
|
|
2281
|
-
#
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
}
|
|
2286
|
-
return modelOrder;
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
#providerRank(): Map<string, number> {
|
|
2290
|
-
return buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings());
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
#resolveCanonicalVariant(
|
|
2294
|
-
variants: readonly CanonicalModelVariant[],
|
|
2295
|
-
modelOrder: ReadonlyMap<string, number>,
|
|
2296
|
-
providerRank: ReadonlyMap<string, number>,
|
|
2297
|
-
): CanonicalModelVariant | undefined {
|
|
2298
|
-
if (variants.length === 0) {
|
|
2299
|
-
return undefined;
|
|
2300
|
-
}
|
|
2301
|
-
const sourceRank: Record<CanonicalModelVariant["source"], number> = {
|
|
2302
|
-
override: 1,
|
|
2303
|
-
bundled: 1,
|
|
2304
|
-
heuristic: 2,
|
|
2305
|
-
fallback: 3,
|
|
1595
|
+
#variantPreferences(candidates: readonly Model<Api>[]): CanonicalVariantPreferences {
|
|
1596
|
+
return {
|
|
1597
|
+
modelOrder: buildCanonicalModelOrder(candidates),
|
|
1598
|
+
providerRank: buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings()),
|
|
2306
1599
|
};
|
|
2307
|
-
return [...variants].sort((left, right) => {
|
|
2308
|
-
const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2309
|
-
const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2310
|
-
if (leftProviderRank !== rightProviderRank) {
|
|
2311
|
-
return leftProviderRank - rightProviderRank;
|
|
2312
|
-
}
|
|
2313
|
-
const leftExact = left.model.id === left.canonicalId ? 0 : 1;
|
|
2314
|
-
const rightExact = right.model.id === right.canonicalId ? 0 : 1;
|
|
2315
|
-
if (leftExact !== rightExact) {
|
|
2316
|
-
return leftExact - rightExact;
|
|
2317
|
-
}
|
|
2318
|
-
if (sourceRank[left.source] !== sourceRank[right.source]) {
|
|
2319
|
-
return sourceRank[left.source] - sourceRank[right.source];
|
|
2320
|
-
}
|
|
2321
|
-
if (left.model.id.length !== right.model.id.length) {
|
|
2322
|
-
return left.model.id.length - right.model.id.length;
|
|
2323
|
-
}
|
|
2324
|
-
const leftOrder = modelOrder.get(left.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
2325
|
-
const rightOrder = modelOrder.get(right.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
2326
|
-
return leftOrder - rightOrder;
|
|
2327
|
-
})[0];
|
|
2328
1600
|
}
|
|
2329
1601
|
|
|
2330
1602
|
getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
|
|
@@ -2354,15 +1626,14 @@ export class ModelRegistry {
|
|
|
2354
1626
|
getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[] {
|
|
2355
1627
|
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
2356
1628
|
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
2357
|
-
const
|
|
2358
|
-
const providerRank = this.#providerRank();
|
|
1629
|
+
const preferences = this.#variantPreferences(candidates);
|
|
2359
1630
|
const selections: CanonicalModelSelection[] = [];
|
|
2360
1631
|
for (const record of this.#canonicalIndex.records) {
|
|
2361
1632
|
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
2362
1633
|
if (variants.length === 0) {
|
|
2363
1634
|
continue;
|
|
2364
1635
|
}
|
|
2365
|
-
const resolved =
|
|
1636
|
+
const resolved = resolveCanonicalVariant(variants, preferences);
|
|
2366
1637
|
if (!resolved) {
|
|
2367
1638
|
continue;
|
|
2368
1639
|
}
|
|
@@ -2389,7 +1660,7 @@ export class ModelRegistry {
|
|
|
2389
1660
|
return undefined;
|
|
2390
1661
|
}
|
|
2391
1662
|
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
2392
|
-
return
|
|
1663
|
+
return resolveCanonicalVariant(variants, this.#variantPreferences(candidates))?.model;
|
|
2393
1664
|
}
|
|
2394
1665
|
|
|
2395
1666
|
getCanonicalId(model: Model<Api>): string | undefined {
|
|
@@ -2414,7 +1685,10 @@ export class ModelRegistry {
|
|
|
2414
1685
|
* as providers with stored credentials. See issue #993.
|
|
2415
1686
|
*/
|
|
2416
1687
|
hasConfiguredAuth(model: Model<Api>): boolean {
|
|
2417
|
-
|
|
1688
|
+
const commandKey = this.#resolveCommandBackedApiKey(model.provider);
|
|
1689
|
+
return (
|
|
1690
|
+
commandKey.configured || this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider)
|
|
1691
|
+
);
|
|
2418
1692
|
}
|
|
2419
1693
|
|
|
2420
1694
|
getDiscoverableProviders(): string[] {
|
|
@@ -2446,6 +1720,8 @@ export class ModelRegistry {
|
|
|
2446
1720
|
* Get API key for a model.
|
|
2447
1721
|
*/
|
|
2448
1722
|
async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
|
|
1723
|
+
const commandKey = this.#resolveCommandBackedApiKey(model.provider);
|
|
1724
|
+
if (commandKey.configured) return commandKey.value;
|
|
2449
1725
|
if (this.#keylessProviders.has(model.provider) && !this.authStorage.hasAuth(model.provider)) {
|
|
2450
1726
|
return kNoAuth;
|
|
2451
1727
|
}
|
|
@@ -2462,13 +1738,16 @@ export class ModelRegistry {
|
|
|
2462
1738
|
async getApiKeyForProvider(
|
|
2463
1739
|
provider: string,
|
|
2464
1740
|
sessionId?: string,
|
|
2465
|
-
options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
|
|
1741
|
+
options?: { baseUrl?: string; modelId?: string; forceRefresh?: boolean; signal?: AbortSignal },
|
|
2466
1742
|
): Promise<string | undefined> {
|
|
1743
|
+
const commandKey = this.#resolveCommandBackedApiKey(provider);
|
|
1744
|
+
if (commandKey.configured) return commandKey.value;
|
|
2467
1745
|
if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
|
|
2468
1746
|
return kNoAuth;
|
|
2469
1747
|
}
|
|
2470
1748
|
return this.authStorage.getApiKey(provider, sessionId, {
|
|
2471
1749
|
baseUrl: options?.baseUrl,
|
|
1750
|
+
modelId: options?.modelId,
|
|
2472
1751
|
forceRefresh: options?.forceRefresh,
|
|
2473
1752
|
signal: options?.signal,
|
|
2474
1753
|
});
|
|
@@ -2484,6 +1763,8 @@ export class ModelRegistry {
|
|
|
2484
1763
|
}
|
|
2485
1764
|
|
|
2486
1765
|
async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
|
|
1766
|
+
const commandKey = this.#resolveCommandBackedApiKey(provider);
|
|
1767
|
+
if (commandKey.configured) return commandKey.value;
|
|
2487
1768
|
if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
|
|
2488
1769
|
return kNoAuth;
|
|
2489
1770
|
}
|
|
@@ -2607,11 +1888,9 @@ export class ModelRegistry {
|
|
|
2607
1888
|
}
|
|
2608
1889
|
|
|
2609
1890
|
if (config.apiKey) {
|
|
2610
|
-
this.#
|
|
1891
|
+
this.#installProviderApiKey(providerName, config.apiKey);
|
|
2611
1892
|
// Persist runtime API keys so they survive #reloadStaticModels() cycles
|
|
2612
1893
|
this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
|
|
2613
|
-
const resolved = resolveApiKeyConfig(config.apiKey);
|
|
2614
|
-
if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
|
|
2615
1894
|
}
|
|
2616
1895
|
|
|
2617
1896
|
if (config.models && config.models.length > 0) {
|
|
@@ -2680,7 +1959,7 @@ export class ModelRegistry {
|
|
|
2680
1959
|
cacheTtlMs: 24 * 60 * 60 * 1000,
|
|
2681
1960
|
dynamicModelsAuthoritative: true,
|
|
2682
1961
|
fetchDynamicModels: async () => {
|
|
2683
|
-
const apiKey = await this
|
|
1962
|
+
const apiKey = await this.#peekApiKeyForProvider(providerName);
|
|
2684
1963
|
const resolvedKey = isAuthenticated(apiKey) ? apiKey : undefined;
|
|
2685
1964
|
const modelDefs = await fetcher(resolvedKey);
|
|
2686
1965
|
const results: Model<Api>[] = [];
|
|
@@ -2698,7 +1977,7 @@ export class ModelRegistry {
|
|
|
2698
1977
|
);
|
|
2699
1978
|
if (overlay) results.push(finalizeCustomModel(overlay, { useDefaults: true }));
|
|
2700
1979
|
}
|
|
2701
|
-
return results;
|
|
1980
|
+
return results.map(toModelSpec);
|
|
2702
1981
|
},
|
|
2703
1982
|
};
|
|
2704
1983
|
this.#runtimeModelManagers.set(providerName, { options: managerOptions, sourceId: sourceId ?? "" });
|
|
@@ -2772,7 +2051,7 @@ export interface ProviderConfigInput {
|
|
|
2772
2051
|
api?: Api;
|
|
2773
2052
|
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
2774
2053
|
headers?: Record<string, string>;
|
|
2775
|
-
compat?:
|
|
2054
|
+
compat?: ModelSpec<Api>["compat"];
|
|
2776
2055
|
authHeader?: boolean;
|
|
2777
2056
|
/** Streaming transport override — see {@link Model.transport}. */
|
|
2778
2057
|
transport?: Model<Api>["transport"];
|
|
@@ -2804,7 +2083,7 @@ export interface ProviderConfigInput {
|
|
|
2804
2083
|
contextWindow: number;
|
|
2805
2084
|
maxTokens: number;
|
|
2806
2085
|
headers?: Record<string, string>;
|
|
2807
|
-
compat?:
|
|
2086
|
+
compat?: ModelSpec<Api>["compat"];
|
|
2808
2087
|
contextPromotionTarget?: string;
|
|
2809
2088
|
premiumMultiplier?: number;
|
|
2810
2089
|
}>;
|