@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11
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 +117 -0
- package/dist/cli.js +23087 -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 +1 -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/commands/launch.d.ts +1 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -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 +20 -219
- package/dist/types/config/model-resolver.d.ts +16 -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 +12 -16
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/debug/terminal-info.d.ts +0 -1
- 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/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -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/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 +31 -26
- 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/interactive-mode.d.ts +1 -1
- 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 +2 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- 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/sqlite-reader.d.ts +3 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- 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/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 +1 -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 +7 -12
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +69 -5
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +1 -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 +1 -1
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +308 -1025
- package/src/config/model-resolver.ts +113 -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 +18 -14
- package/src/config/settings.ts +37 -1
- 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/debug/terminal-info.ts +0 -3
- package/src/edit/diff.ts +95 -18
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +8 -2
- 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 +2 -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 +49 -23
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +0 -2
- package/src/eval/py/kernel.ts +19 -0
- package/src/eval/py/runner.py +107 -3
- package/src/exec/bash-executor.ts +3 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/types.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- 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 +10 -10
- 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 +165 -17
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +2 -1
- package/src/mnemopi/backend.ts +25 -3
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +19 -13
- 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 +1 -1
- 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 +66 -54
- 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 +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +373 -141
- 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 +10 -3
- package/src/modes/controllers/event-controller.ts +73 -49
- package/src/modes/controllers/input-controller.ts +5 -5
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -5
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +5 -19
- 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 +2 -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 +15 -26
- 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 +8 -10
- 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 +6 -2
- package/src/sdk.ts +23 -10
- package/src/session/agent-session.ts +44 -10
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +23 -2
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/ssh/connection-manager.ts +27 -0
- package/src/task/commands.ts +2 -1
- package/src/task/discovery.ts +17 -24
- package/src/task/executor.ts +61 -53
- package/src/task/index.ts +137 -60
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +2 -2
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +2 -1
- package/src/tiny/title-client.ts +32 -14
- 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 +54 -13
- 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 +3 -9
- 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 +51 -12
- 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/file-mentions.ts +2 -1
- 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/anthropic.ts +8 -2
- 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,16 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import type { Api, Context, Model, ModelSpec, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
|
|
4
|
+
import type { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
|
|
5
|
+
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
6
|
+
import { isVertexExpressOpenAIUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
7
|
+
import { readModelCache } from "@oh-my-pi/pi-catalog/model-cache";
|
|
8
|
+
import {
|
|
9
|
+
createModelManager,
|
|
10
|
+
type ModelManagerOptions,
|
|
11
|
+
type ModelRefreshStrategy,
|
|
12
|
+
} from "@oh-my-pi/pi-catalog/model-manager";
|
|
13
|
+
import { getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/models";
|
|
7
14
|
import {
|
|
8
15
|
googleAntigravityModelManagerOptions,
|
|
9
16
|
googleGeminiCliModelManagerOptions,
|
|
@@ -11,79 +18,12 @@ import {
|
|
|
11
18
|
PROVIDER_DESCRIPTORS,
|
|
12
19
|
UNK_CONTEXT_WINDOW,
|
|
13
20
|
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";
|
|
21
|
+
} from "@oh-my-pi/pi-catalog/provider-models";
|
|
17
22
|
|
|
18
23
|
// Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
|
|
19
24
|
// any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
|
|
20
25
|
const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
|
|
21
26
|
|
|
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
27
|
const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
|
|
88
28
|
"google-antigravity",
|
|
89
29
|
"google-gemini-cli",
|
|
@@ -98,35 +38,37 @@ const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
|
|
|
98
38
|
import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
99
39
|
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
100
40
|
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
41
|
import {
|
|
108
42
|
buildCanonicalModelIndex,
|
|
43
|
+
buildCanonicalModelOrder,
|
|
44
|
+
buildModelProviderPriorityRank,
|
|
109
45
|
type CanonicalModelIndex,
|
|
110
46
|
type CanonicalModelRecord,
|
|
111
47
|
type CanonicalModelVariant,
|
|
48
|
+
type CanonicalVariantPreferences,
|
|
112
49
|
formatCanonicalVariantSelector,
|
|
50
|
+
getBundledCanonicalReferenceData,
|
|
51
|
+
getBundledModelReferenceIndex,
|
|
113
52
|
type ModelEquivalenceConfig,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
} from "./
|
|
121
|
-
import {
|
|
53
|
+
resolveCanonicalVariant,
|
|
54
|
+
resolveModelReference,
|
|
55
|
+
} from "@oh-my-pi/pi-catalog/identity";
|
|
56
|
+
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
57
|
+
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
58
|
+
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
59
|
+
import { type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
|
|
60
|
+
import type { ConfigError, ConfigFile } from "./config-file";
|
|
122
61
|
import {
|
|
123
|
-
|
|
124
|
-
type
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
62
|
+
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
63
|
+
type DiscoveryContext,
|
|
64
|
+
type DiscoveryProviderConfig,
|
|
65
|
+
discoverModelsByProviderType,
|
|
66
|
+
getImplicitOllamaBaseUrl,
|
|
67
|
+
getOllamaContextLengthOverride,
|
|
68
|
+
} from "./model-discovery";
|
|
69
|
+
import { ModelsConfigFile, type ProviderValidationModel, validateProviderConfiguration } from "./models-config";
|
|
70
|
+
import type { ModelOverride, ModelsConfig, ProviderAuthMode } from "./models-config-schema";
|
|
71
|
+
import { settings } from "./settings";
|
|
130
72
|
|
|
131
73
|
export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
|
|
132
74
|
|
|
@@ -136,196 +78,13 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
|
|
|
136
78
|
return Boolean(apiKey) && apiKey !== kNoAuth;
|
|
137
79
|
}
|
|
138
80
|
|
|
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
81
|
/** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
|
|
323
82
|
interface ProviderOverride {
|
|
324
83
|
baseUrl?: string;
|
|
325
84
|
headers?: Record<string, string>;
|
|
326
85
|
apiKey?: string;
|
|
327
86
|
authHeader?: boolean;
|
|
328
|
-
compat?:
|
|
87
|
+
compat?: ModelSpec<Api>["compat"];
|
|
329
88
|
transport?: Model<Api>["transport"];
|
|
330
89
|
}
|
|
331
90
|
|
|
@@ -351,19 +110,21 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
351
110
|
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
|
|
352
111
|
): Model<TApi> {
|
|
353
112
|
if (existing) {
|
|
354
|
-
return {
|
|
113
|
+
return buildModel({
|
|
355
114
|
...model,
|
|
356
115
|
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
|
357
116
|
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
|
358
|
-
|
|
117
|
+
compat: model.compatConfig,
|
|
118
|
+
} as ModelSpec<TApi>);
|
|
359
119
|
}
|
|
360
120
|
if (providerOverride) {
|
|
361
|
-
return {
|
|
121
|
+
return buildModel({
|
|
362
122
|
...model,
|
|
363
123
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
364
124
|
headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
|
|
365
125
|
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
366
|
-
|
|
126
|
+
compat: model.compatConfig,
|
|
127
|
+
} as ModelSpec<TApi>);
|
|
367
128
|
}
|
|
368
129
|
return model;
|
|
369
130
|
}
|
|
@@ -378,7 +139,7 @@ function isAuthoritativeProjectCatalogModel(model: Model<Api>): boolean {
|
|
|
378
139
|
return (
|
|
379
140
|
model.provider === "google-vertex" &&
|
|
380
141
|
model.api === "openai-completions" &&
|
|
381
|
-
model.baseUrl
|
|
142
|
+
isVertexExpressOpenAIUrl(model.baseUrl)
|
|
382
143
|
);
|
|
383
144
|
}
|
|
384
145
|
|
|
@@ -396,14 +157,32 @@ function dropProviderModels(models: readonly Model<Api>[], providers: ReadonlySe
|
|
|
396
157
|
return models.filter(model => !providers.has(model.provider));
|
|
397
158
|
}
|
|
398
159
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Merge `incoming` entries into a copy of `base`, keyed by `provider`+`id`.
|
|
162
|
+
* Matches are replaced with `combine(existing, entry)`; new entries are
|
|
163
|
+
* appended as `combine(undefined, entry)`.
|
|
164
|
+
*/
|
|
165
|
+
function mergeByModelKey<T extends { provider: string; id: string }>(
|
|
166
|
+
base: readonly Model<Api>[],
|
|
167
|
+
incoming: readonly T[],
|
|
168
|
+
combine: (existing: Model<Api> | undefined, entry: T) => Model<Api>,
|
|
169
|
+
): Model<Api>[] {
|
|
170
|
+
const merged = [...base];
|
|
171
|
+
const indexByKey = new Map<string, number>();
|
|
172
|
+
for (let i = 0; i < merged.length; i += 1) {
|
|
173
|
+
indexByKey.set(`${merged[i].provider}\u0000${merged[i].id}`, i);
|
|
174
|
+
}
|
|
175
|
+
for (const entry of incoming) {
|
|
176
|
+
const key = `${entry.provider}\u0000${entry.id}`;
|
|
177
|
+
const existingIndex = indexByKey.get(key);
|
|
178
|
+
if (existingIndex !== undefined) {
|
|
179
|
+
merged[existingIndex] = combine(merged[existingIndex], entry);
|
|
180
|
+
} else {
|
|
181
|
+
merged.push(combine(undefined, entry));
|
|
182
|
+
indexByKey.set(key, merged.length - 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return merged;
|
|
407
186
|
}
|
|
408
187
|
|
|
409
188
|
interface BuiltInDiscoveryResult {
|
|
@@ -428,6 +207,12 @@ export interface CanonicalModelQueryOptions {
|
|
|
428
207
|
candidates?: readonly Model<Api>[];
|
|
429
208
|
}
|
|
430
209
|
|
|
210
|
+
/** A canonical record (with query-filtered variants) plus the variant model selected for it. */
|
|
211
|
+
export interface CanonicalModelSelection {
|
|
212
|
+
record: CanonicalModelRecord;
|
|
213
|
+
model: Model<Api>;
|
|
214
|
+
}
|
|
215
|
+
|
|
431
216
|
/** Result of loading custom models from models.json */
|
|
432
217
|
interface CustomModelsResult {
|
|
433
218
|
models?: CustomModelOverlay[];
|
|
@@ -441,17 +226,6 @@ interface CustomModelsResult {
|
|
|
441
226
|
found: boolean;
|
|
442
227
|
}
|
|
443
228
|
|
|
444
|
-
type OllamaDiscoveredModelMetadata = {
|
|
445
|
-
reasoning: boolean;
|
|
446
|
-
input: ("text" | "image")[];
|
|
447
|
-
contextWindow?: number;
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
type LlamaCppDiscoveredServerMetadata = {
|
|
451
|
-
contextWindow?: number;
|
|
452
|
-
input?: ("text" | "image")[];
|
|
453
|
-
};
|
|
454
|
-
|
|
455
229
|
/**
|
|
456
230
|
* Resolve an API key config value to an actual key.
|
|
457
231
|
* Checks environment variable first, then treats as literal.
|
|
@@ -462,59 +236,6 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
|
462
236
|
return keyConfig;
|
|
463
237
|
}
|
|
464
238
|
|
|
465
|
-
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
466
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
467
|
-
return value;
|
|
468
|
-
}
|
|
469
|
-
if (typeof value === "string" && value.trim()) {
|
|
470
|
-
const parsed = Number(value);
|
|
471
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
472
|
-
return parsed;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function extractOllamaContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
479
|
-
const modelInfo = payload.model_info;
|
|
480
|
-
if (isRecord(modelInfo)) {
|
|
481
|
-
for (const [key, value] of Object.entries(modelInfo)) {
|
|
482
|
-
if (key === "context_length" || key.endsWith(".context_length")) {
|
|
483
|
-
const contextWindow = toPositiveNumberOrUndefined(value);
|
|
484
|
-
if (contextWindow !== undefined) {
|
|
485
|
-
return contextWindow;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const parameters = payload.parameters;
|
|
492
|
-
if (typeof parameters !== "string") {
|
|
493
|
-
return undefined;
|
|
494
|
-
}
|
|
495
|
-
const match = parameters.match(/(?:^|\n)\s*num_ctx\s+(\d+)\s*(?:$|\n)/m);
|
|
496
|
-
return match ? toPositiveNumberOrUndefined(match[1]) : undefined;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function extractLlamaCppContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
500
|
-
const generationSettings = payload.default_generation_settings;
|
|
501
|
-
if (isRecord(generationSettings)) {
|
|
502
|
-
const contextWindow = toPositiveNumberOrUndefined(generationSettings.n_ctx);
|
|
503
|
-
if (contextWindow !== undefined) {
|
|
504
|
-
return contextWindow;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return toPositiveNumberOrUndefined(payload.n_ctx);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function extractLlamaCppInputCapabilities(payload: Record<string, unknown>): ("text" | "image")[] | undefined {
|
|
511
|
-
const modalities = payload.modalities;
|
|
512
|
-
if (!isRecord(modalities)) {
|
|
513
|
-
return undefined;
|
|
514
|
-
}
|
|
515
|
-
return modalities.vision === true ? ["text", "image"] : ["text"];
|
|
516
|
-
}
|
|
517
|
-
|
|
518
239
|
function extractGoogleOAuthToken(value: string | undefined): string | undefined {
|
|
519
240
|
if (!isAuthenticated(value)) return undefined;
|
|
520
241
|
try {
|
|
@@ -573,73 +294,99 @@ function mergeCompat<TBase extends object, TOverride extends object>(
|
|
|
573
294
|
return merged as TBase & TOverride;
|
|
574
295
|
}
|
|
575
296
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
584
|
-
if (override.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = override.omitMaxOutputTokens;
|
|
585
|
-
if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
|
|
586
|
-
if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
|
|
587
|
-
if (override.cost) {
|
|
588
|
-
result.cost = {
|
|
589
|
-
input: override.cost.input ?? model.cost.input,
|
|
590
|
-
output: override.cost.output ?? model.cost.output,
|
|
591
|
-
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
|
|
592
|
-
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
if (override.headers) {
|
|
596
|
-
result.headers = { ...model.headers, ...override.headers };
|
|
597
|
-
}
|
|
598
|
-
result.compat = mergeCompat(model.compat, override.compat);
|
|
599
|
-
return enrichModelThinking(result);
|
|
297
|
+
/**
|
|
298
|
+
* Project a built model back to spec shape for the model-manager/cache
|
|
299
|
+
* boundary: sparse compat comes from `compatConfig`, never from the resolved
|
|
300
|
+
* record.
|
|
301
|
+
*/
|
|
302
|
+
function toModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
|
|
303
|
+
return { ...model, compat: model.compatConfig } as ModelSpec<TApi>;
|
|
600
304
|
}
|
|
601
305
|
|
|
602
|
-
|
|
603
|
-
|
|
306
|
+
/**
|
|
307
|
+
* The patchable subset of `Model` fields shared by `modelOverrides` entries,
|
|
308
|
+
* custom model definitions, and parsed custom-model overlays. `undefined`
|
|
309
|
+
* always means "leave the base value alone".
|
|
310
|
+
*/
|
|
311
|
+
interface ModelPatch {
|
|
604
312
|
name?: string;
|
|
605
|
-
api?: Api;
|
|
606
|
-
baseUrl?: string;
|
|
607
313
|
reasoning?: boolean;
|
|
608
314
|
thinking?: ThinkingConfig;
|
|
609
315
|
input?: ("text" | "image")[];
|
|
610
|
-
cost?:
|
|
316
|
+
cost?: Partial<Model<Api>["cost"]>;
|
|
611
317
|
contextWindow?: number;
|
|
612
318
|
maxTokens?: number;
|
|
613
319
|
omitMaxOutputTokens?: boolean;
|
|
614
320
|
headers?: Record<string, string>;
|
|
615
|
-
compat?:
|
|
321
|
+
compat?: ModelSpec<Api>["compat"];
|
|
616
322
|
contextPromotionTarget?: string;
|
|
617
323
|
premiumMultiplier?: number;
|
|
618
324
|
}
|
|
619
325
|
|
|
326
|
+
/**
|
|
327
|
+
* How a patch treats the base model's transport metadata (headers/compat):
|
|
328
|
+
* - `merge`: fold the patch into the base's (modelOverrides semantics).
|
|
329
|
+
* - `replace`: the patch owns transport wholesale — same-id custom definitions
|
|
330
|
+
* already folded provider-level headers/compat in during parsing, so bundled
|
|
331
|
+
* transport metadata must not be re-merged (see `#mergeCustomModels`).
|
|
332
|
+
*/
|
|
333
|
+
type ModelTransportPolicy = "merge" | "replace";
|
|
334
|
+
|
|
335
|
+
function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTransportPolicy): Model<Api> {
|
|
336
|
+
const result = { ...base };
|
|
337
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
338
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
339
|
+
if (patch.thinking !== undefined) result.thinking = patch.thinking;
|
|
340
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
341
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
342
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
343
|
+
if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
|
|
344
|
+
if (patch.contextPromotionTarget !== undefined) result.contextPromotionTarget = patch.contextPromotionTarget;
|
|
345
|
+
if (patch.premiumMultiplier !== undefined) result.premiumMultiplier = patch.premiumMultiplier;
|
|
346
|
+
if (patch.cost) {
|
|
347
|
+
result.cost = {
|
|
348
|
+
input: patch.cost.input ?? base.cost.input,
|
|
349
|
+
output: patch.cost.output ?? base.cost.output,
|
|
350
|
+
cacheRead: patch.cost.cacheRead ?? base.cost.cacheRead,
|
|
351
|
+
cacheWrite: patch.cost.cacheWrite ?? base.cost.cacheWrite,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
let compat: ModelSpec<Api>["compat"];
|
|
355
|
+
if (transport === "merge") {
|
|
356
|
+
if (patch.headers) {
|
|
357
|
+
result.headers = { ...base.headers, ...patch.headers };
|
|
358
|
+
}
|
|
359
|
+
compat = mergeCompat(base.compatConfig, patch.compat);
|
|
360
|
+
} else {
|
|
361
|
+
result.headers = patch.headers;
|
|
362
|
+
compat = patch.compat;
|
|
363
|
+
}
|
|
364
|
+
return buildModel({ ...result, compat } as ModelSpec<Api>);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
|
368
|
+
return applyModelPatch(model, override as ModelPatch, "merge");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
interface CustomModelDefinitionLike extends ModelPatch {
|
|
372
|
+
id: string;
|
|
373
|
+
api?: Api;
|
|
374
|
+
baseUrl?: string;
|
|
375
|
+
cost?: Model<Api>["cost"];
|
|
376
|
+
}
|
|
377
|
+
|
|
620
378
|
interface CustomModelBuildOptions {
|
|
621
379
|
useDefaults: boolean;
|
|
622
380
|
}
|
|
623
381
|
|
|
624
|
-
|
|
382
|
+
interface CustomModelOverlay extends ModelPatch {
|
|
625
383
|
id: string;
|
|
626
384
|
provider: string;
|
|
627
385
|
api: Api;
|
|
628
386
|
baseUrl: string;
|
|
629
|
-
|
|
630
|
-
reasoning?: boolean;
|
|
631
|
-
thinking?: ThinkingConfig;
|
|
632
|
-
input?: ("text" | "image")[];
|
|
633
|
-
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
634
|
-
contextWindow?: number;
|
|
635
|
-
maxTokens?: number;
|
|
636
|
-
omitMaxOutputTokens?: boolean;
|
|
637
|
-
headers?: Record<string, string>;
|
|
638
|
-
compat?: Model<Api>["compat"];
|
|
639
|
-
contextPromotionTarget?: string;
|
|
640
|
-
premiumMultiplier?: number;
|
|
387
|
+
cost?: Model<Api>["cost"];
|
|
641
388
|
isOAuth?: boolean;
|
|
642
|
-
}
|
|
389
|
+
}
|
|
643
390
|
|
|
644
391
|
function mergeCustomModelHeaders(
|
|
645
392
|
providerHeaders: Record<string, string> | undefined,
|
|
@@ -686,7 +433,7 @@ function buildCustomModelOverlay(
|
|
|
686
433
|
providerHeaders: Record<string, string> | undefined,
|
|
687
434
|
providerApiKey: string | undefined,
|
|
688
435
|
authHeader: boolean | undefined,
|
|
689
|
-
providerCompat:
|
|
436
|
+
providerCompat: ModelSpec<Api>["compat"] | undefined,
|
|
690
437
|
providerAuth: ProviderAuthMode | undefined,
|
|
691
438
|
modelDef: CustomModelDefinitionLike,
|
|
692
439
|
): CustomModelOverlay | undefined {
|
|
@@ -699,8 +446,8 @@ function buildCustomModelOverlay(
|
|
|
699
446
|
baseUrl: modelDef.baseUrl ?? providerBaseUrl,
|
|
700
447
|
name: modelDef.name,
|
|
701
448
|
reasoning: modelDef.reasoning,
|
|
702
|
-
thinking: modelDef.thinking
|
|
703
|
-
input: modelDef.input
|
|
449
|
+
thinking: modelDef.thinking,
|
|
450
|
+
input: modelDef.input,
|
|
704
451
|
cost: modelDef.cost,
|
|
705
452
|
contextWindow: modelDef.contextWindow,
|
|
706
453
|
maxTokens: modelDef.maxTokens,
|
|
@@ -713,125 +460,6 @@ function buildCustomModelOverlay(
|
|
|
713
460
|
};
|
|
714
461
|
}
|
|
715
462
|
|
|
716
|
-
// Custom provider entries often front a known upstream model through a local proxy.
|
|
717
|
-
// Use bundled metadata for missing pricing/capability fields, but keep the custom transport.
|
|
718
|
-
function shouldReplaceCustomReference(existing: Model<Api> | undefined, candidate: Model<Api>): boolean {
|
|
719
|
-
if (!existing) return true;
|
|
720
|
-
if (candidate.contextWindow !== existing.contextWindow) {
|
|
721
|
-
return candidate.contextWindow > existing.contextWindow;
|
|
722
|
-
}
|
|
723
|
-
if (candidate.maxTokens !== existing.maxTokens) {
|
|
724
|
-
return candidate.maxTokens > existing.maxTokens;
|
|
725
|
-
}
|
|
726
|
-
const existingHasCachePricing = existing.cost.cacheRead > 0 || existing.cost.cacheWrite > 0;
|
|
727
|
-
const candidateHasCachePricing = candidate.cost.cacheRead > 0 || candidate.cost.cacheWrite > 0;
|
|
728
|
-
if (candidateHasCachePricing !== existingHasCachePricing) {
|
|
729
|
-
return candidateHasCachePricing;
|
|
730
|
-
}
|
|
731
|
-
return existing.provider !== "openai" && candidate.provider === "openai";
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function normalizeCustomReferenceKey(value: string): string {
|
|
735
|
-
return value.trim().toLowerCase();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function buildCustomReferenceMap(): Map<string, Model<Api>> {
|
|
739
|
-
const references = new Map<string, Model<Api>>();
|
|
740
|
-
for (const provider of getBundledProviders()) {
|
|
741
|
-
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
|
742
|
-
const candidate = model as Model<Api>;
|
|
743
|
-
const key = normalizeCustomReferenceKey(candidate.id);
|
|
744
|
-
if (shouldReplaceCustomReference(references.get(key), candidate)) {
|
|
745
|
-
references.set(key, candidate);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return references;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function buildCustomReferenceSuffixAliasMap(exactReferences: ReadonlyMap<string, Model<Api>>): Map<string, Model<Api>> {
|
|
753
|
-
const aliases = new Map<string, Model<Api>>();
|
|
754
|
-
for (const reference of exactReferences.values()) {
|
|
755
|
-
const slashIndex = reference.id.lastIndexOf("/");
|
|
756
|
-
if (slashIndex === -1) {
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
const suffix = reference.id.slice(slashIndex + 1);
|
|
760
|
-
const alias = getLongestModelLikeIdSegment(suffix);
|
|
761
|
-
if (!alias) {
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
if (shouldReplaceCustomReference(aliases.get(alias), reference)) {
|
|
765
|
-
aliases.set(alias, reference);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
return aliases;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const customReferenceMap = buildCustomReferenceMap();
|
|
772
|
-
const customReferenceSuffixAliasMap = buildCustomReferenceSuffixAliasMap(customReferenceMap);
|
|
773
|
-
|
|
774
|
-
const CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN =
|
|
775
|
-
/[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4|search)$/i;
|
|
776
|
-
|
|
777
|
-
function stripCustomReferenceTrailingMarker(candidate: string): string | undefined {
|
|
778
|
-
const match = CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN.exec(candidate);
|
|
779
|
-
return match ? candidate.slice(0, match.index) : undefined;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function getCustomReferenceCandidateIds(modelId: string): string[] {
|
|
783
|
-
const candidates = new Set<string>();
|
|
784
|
-
const queue = [modelId];
|
|
785
|
-
for (let index = 0; index < queue.length; index += 1) {
|
|
786
|
-
const candidate = queue[index]?.trim();
|
|
787
|
-
if (!candidate || candidates.has(candidate)) continue;
|
|
788
|
-
candidates.add(candidate);
|
|
789
|
-
|
|
790
|
-
for (const stripped of getBracketStrippedModelIdCandidates(candidate)) {
|
|
791
|
-
queue.push(stripped);
|
|
792
|
-
}
|
|
793
|
-
for (const segment of getModelLikeIdSegments(candidate)) {
|
|
794
|
-
queue.push(segment);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
for (const suffix of [":cloud", "-cloud"] as const) {
|
|
798
|
-
if (candidate.toLowerCase().endsWith(suffix)) {
|
|
799
|
-
queue.push(candidate.slice(0, -suffix.length));
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const slashIndex = candidate.lastIndexOf("/");
|
|
804
|
-
if (slashIndex !== -1) {
|
|
805
|
-
queue.push(candidate.slice(slashIndex + 1));
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
const colonToDash = candidate.replace(/:/g, "-");
|
|
809
|
-
if (colonToDash !== candidate) {
|
|
810
|
-
queue.push(colonToDash);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const lowercased = candidate.toLowerCase();
|
|
814
|
-
if (lowercased !== candidate) {
|
|
815
|
-
queue.push(lowercased);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const strippedMarker = stripCustomReferenceTrailingMarker(candidate);
|
|
819
|
-
if (strippedMarker) {
|
|
820
|
-
queue.push(strippedMarker);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
return [...candidates];
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
|
|
827
|
-
for (const candidate of getCustomReferenceCandidateIds(modelId)) {
|
|
828
|
-
const key = normalizeCustomReferenceKey(candidate);
|
|
829
|
-
const reference = customReferenceMap.get(key) ?? customReferenceSuffixAliasMap.get(key);
|
|
830
|
-
if (reference) return reference;
|
|
831
|
-
}
|
|
832
|
-
return undefined;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
463
|
function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
|
|
836
464
|
if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
|
|
837
465
|
return model;
|
|
@@ -841,13 +469,15 @@ function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomMo
|
|
|
841
469
|
|
|
842
470
|
function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
|
|
843
471
|
const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
|
|
844
|
-
const reference = options.useDefaults
|
|
472
|
+
const reference = options.useDefaults
|
|
473
|
+
? resolveModelReference(resolvedModel.id, getBundledModelReferenceIndex())
|
|
474
|
+
: undefined;
|
|
845
475
|
const cost =
|
|
846
476
|
resolvedModel.cost ??
|
|
847
477
|
reference?.cost ??
|
|
848
478
|
(options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
|
849
479
|
const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
|
|
850
|
-
return
|
|
480
|
+
return buildModel({
|
|
851
481
|
id: resolvedModel.id,
|
|
852
482
|
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
853
483
|
api: resolvedModel.api,
|
|
@@ -862,11 +492,11 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
862
492
|
maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : undefined),
|
|
863
493
|
headers: resolvedModel.headers,
|
|
864
494
|
omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
|
|
865
|
-
compat: mergeCompat(reference?.
|
|
495
|
+
compat: mergeCompat(reference?.compatConfig, resolvedModel.compat),
|
|
866
496
|
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
867
497
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
868
498
|
isOAuth: resolvedModel.isOAuth,
|
|
869
|
-
} as
|
|
499
|
+
} as ModelSpec<Api>);
|
|
870
500
|
}
|
|
871
501
|
|
|
872
502
|
function normalizeSuppressedSelector(selector: string): string {
|
|
@@ -1127,84 +757,46 @@ export class ModelRegistry {
|
|
|
1127
757
|
return models.map(m => {
|
|
1128
758
|
if (!providerOverride) return m;
|
|
1129
759
|
const withTransportOverride = this.#applyProviderTransportOverride(m, providerOverride);
|
|
1130
|
-
return {
|
|
760
|
+
return buildModel({
|
|
1131
761
|
...withTransportOverride,
|
|
1132
|
-
compat: mergeCompat(m.
|
|
1133
|
-
};
|
|
762
|
+
compat: mergeCompat(m.compatConfig, providerOverride.compat),
|
|
763
|
+
} as ModelSpec<Api>);
|
|
1134
764
|
});
|
|
1135
765
|
});
|
|
1136
766
|
}
|
|
1137
767
|
|
|
1138
768
|
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
merged[existingIndex] = {
|
|
1151
|
-
...replacementModel,
|
|
1152
|
-
contextWindow:
|
|
1153
|
-
replacementModel.contextWindow === UNK_CONTEXT_WINDOW
|
|
1154
|
-
? existing.contextWindow
|
|
1155
|
-
: replacementModel.contextWindow,
|
|
1156
|
-
maxTokens:
|
|
1157
|
-
replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
|
|
1158
|
-
};
|
|
1159
|
-
} else {
|
|
1160
|
-
merged.push(replacementModel);
|
|
1161
|
-
indexByKey.set(key, merged.length - 1);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
return merged;
|
|
769
|
+
return mergeByModelKey(baseModels, replacementModels, (existing, replacementModel) => {
|
|
770
|
+
if (!existing) return replacementModel;
|
|
771
|
+
return {
|
|
772
|
+
...replacementModel,
|
|
773
|
+
contextWindow:
|
|
774
|
+
replacementModel.contextWindow === UNK_CONTEXT_WINDOW
|
|
775
|
+
? existing.contextWindow
|
|
776
|
+
: replacementModel.contextWindow,
|
|
777
|
+
maxTokens: replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
|
|
778
|
+
};
|
|
779
|
+
});
|
|
1165
780
|
}
|
|
1166
781
|
|
|
1167
782
|
/** Merge custom models with built-in, replacing by provider+id match */
|
|
1168
783
|
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
for (const customModel of customModels) {
|
|
1176
|
-
const key = `${customModel.provider}\u0000${customModel.id}`;
|
|
1177
|
-
const existingIndex = indexByKey.get(key);
|
|
1178
|
-
if (existingIndex !== undefined) {
|
|
1179
|
-
const existingModel = merged[existingIndex];
|
|
1180
|
-
merged[existingIndex] = enrichModelThinking({
|
|
784
|
+
return mergeByModelKey(builtInModels, customModels, (existingModel, customModel) => {
|
|
785
|
+
if (!existingModel) return finalizeCustomModel(customModel, { useDefaults: true });
|
|
786
|
+
// Same-id custom definitions replace bundled transport behavior, so the
|
|
787
|
+
// patch is applied with the `replace` transport policy.
|
|
788
|
+
return applyModelPatch(
|
|
789
|
+
{
|
|
1181
790
|
...existingModel,
|
|
1182
791
|
id: customModel.id,
|
|
1183
792
|
provider: customModel.provider,
|
|
1184
793
|
api: customModel.api,
|
|
1185
794
|
baseUrl: customModel.baseUrl,
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
|
|
1192
|
-
maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
|
|
1193
|
-
omitMaxOutputTokens: customModel.omitMaxOutputTokens ?? existingModel.omitMaxOutputTokens,
|
|
1194
|
-
// Same-id custom definitions replace bundled transport behavior. Provider-level
|
|
1195
|
-
// headers/compat were already folded into customModel during parsing; do not
|
|
1196
|
-
// re-merge bundled transport metadata here.
|
|
1197
|
-
headers: customModel.headers,
|
|
1198
|
-
compat: customModel.compat,
|
|
1199
|
-
contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
|
1200
|
-
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
1201
|
-
} as Model<Api>);
|
|
1202
|
-
} else {
|
|
1203
|
-
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
1204
|
-
indexByKey.set(key, merged.length - 1);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
return merged;
|
|
795
|
+
},
|
|
796
|
+
customModel,
|
|
797
|
+
"replace",
|
|
798
|
+
);
|
|
799
|
+
});
|
|
1208
800
|
}
|
|
1209
801
|
|
|
1210
802
|
#loadCachedStandardProviderModels(): { models: Model<Api>[]; authoritativeFreshProviders: Set<string> } {
|
|
@@ -1230,8 +822,13 @@ export class ModelRegistry {
|
|
|
1230
822
|
? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
|
|
1231
823
|
: models;
|
|
1232
824
|
const withCompat = providerOverride?.compat
|
|
1233
|
-
? withTransport.map(model =>
|
|
1234
|
-
|
|
825
|
+
? withTransport.map(model =>
|
|
826
|
+
buildModel({
|
|
827
|
+
...model,
|
|
828
|
+
compat: mergeCompat(model.compat, providerOverride.compat),
|
|
829
|
+
} as ModelSpec<Api>),
|
|
830
|
+
)
|
|
831
|
+
: withTransport.map(model => buildModel(model));
|
|
1235
832
|
cachedModels.push(...this.#applyProviderModelOverrides(providerId, withCompat));
|
|
1236
833
|
}
|
|
1237
834
|
return { models: cachedModels, authoritativeFreshProviders };
|
|
@@ -1255,7 +852,10 @@ export class ModelRegistry {
|
|
|
1255
852
|
providerConfig.provider,
|
|
1256
853
|
this.#normalizeDiscoverableModels(
|
|
1257
854
|
providerConfig,
|
|
1258
|
-
this.#applyProviderCompat(
|
|
855
|
+
this.#applyProviderCompat(
|
|
856
|
+
providerConfig.compat,
|
|
857
|
+
cache.models.map(model => buildModel(model)),
|
|
858
|
+
),
|
|
1259
859
|
),
|
|
1260
860
|
);
|
|
1261
861
|
cachedModels.push(...models);
|
|
@@ -1271,9 +871,11 @@ export class ModelRegistry {
|
|
|
1271
871
|
return cachedModels;
|
|
1272
872
|
}
|
|
1273
873
|
|
|
1274
|
-
#applyProviderCompat(compat:
|
|
874
|
+
#applyProviderCompat(compat: ModelSpec<Api>["compat"] | undefined, models: Model<Api>[]): Model<Api>[] {
|
|
1275
875
|
if (!compat) return models;
|
|
1276
|
-
return models.map(model =>
|
|
876
|
+
return models.map(model =>
|
|
877
|
+
buildModel({ ...model, compat: mergeCompat(model.compatConfig, compat) } as ModelSpec<Api>),
|
|
878
|
+
);
|
|
1277
879
|
}
|
|
1278
880
|
|
|
1279
881
|
#normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
@@ -1283,7 +885,14 @@ export class ModelRegistry {
|
|
|
1283
885
|
|
|
1284
886
|
const contextLengthOverride = getOllamaContextLengthOverride();
|
|
1285
887
|
return models.map(model => {
|
|
1286
|
-
const normalized =
|
|
888
|
+
const normalized =
|
|
889
|
+
model.api === "openai-completions"
|
|
890
|
+
? buildModel({
|
|
891
|
+
...model,
|
|
892
|
+
api: "openai-responses" as const,
|
|
893
|
+
compat: model.compatConfig,
|
|
894
|
+
} as ModelSpec<Api>)
|
|
895
|
+
: model;
|
|
1287
896
|
if (contextLengthOverride === undefined) {
|
|
1288
897
|
return normalized;
|
|
1289
898
|
}
|
|
@@ -1506,17 +1115,20 @@ export class ModelRegistry {
|
|
|
1506
1115
|
models: cached?.models.map(model => model.id) ?? [],
|
|
1507
1116
|
});
|
|
1508
1117
|
this.#lastDiscoveryWarnings.delete(providerConfig.provider);
|
|
1509
|
-
return cached
|
|
1118
|
+
return cached ? cached.models.map(model => buildModel(model)) : [];
|
|
1510
1119
|
}
|
|
1511
1120
|
}
|
|
1512
1121
|
|
|
1513
1122
|
const providerId = providerConfig.provider;
|
|
1514
1123
|
let discoveryError: string | undefined;
|
|
1515
|
-
const fetchDynamicModels = async (): Promise<readonly
|
|
1124
|
+
const fetchDynamicModels = async (): Promise<readonly ModelSpec<Api>[] | null> => {
|
|
1516
1125
|
try {
|
|
1517
|
-
const models =
|
|
1126
|
+
const models = this.#applyProviderModelOverrides(
|
|
1127
|
+
providerId,
|
|
1128
|
+
await discoverModelsByProviderType(providerConfig, this.#discoveryContext()),
|
|
1129
|
+
);
|
|
1518
1130
|
this.#lastDiscoveryWarnings.delete(providerId);
|
|
1519
|
-
return models;
|
|
1131
|
+
return models.map(toModelSpec);
|
|
1520
1132
|
} catch (error) {
|
|
1521
1133
|
discoveryError = error instanceof Error ? error.message : String(error);
|
|
1522
1134
|
return null;
|
|
@@ -1563,18 +1175,14 @@ export class ModelRegistry {
|
|
|
1563
1175
|
);
|
|
1564
1176
|
}
|
|
1565
1177
|
|
|
1566
|
-
#
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
return
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
return this.#discoverOpenAIModelsList(providerConfig);
|
|
1575
|
-
case "proxy":
|
|
1576
|
-
return this.#discoverProxyModels(providerConfig);
|
|
1577
|
-
}
|
|
1178
|
+
#discoveryContext(): DiscoveryContext {
|
|
1179
|
+
return {
|
|
1180
|
+
fetch: this.#fetch,
|
|
1181
|
+
getBearerApiKey: async provider => {
|
|
1182
|
+
const apiKey = await this.authStorage.getApiKey(provider);
|
|
1183
|
+
return apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1578
1186
|
}
|
|
1579
1187
|
|
|
1580
1188
|
#warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
|
|
@@ -1726,361 +1334,6 @@ export class ModelRegistry {
|
|
|
1726
1334
|
}
|
|
1727
1335
|
}
|
|
1728
1336
|
|
|
1729
|
-
async #discoverOllamaModelMetadata(
|
|
1730
|
-
endpoint: string,
|
|
1731
|
-
modelId: string,
|
|
1732
|
-
headers: Record<string, string> | undefined,
|
|
1733
|
-
): Promise<OllamaDiscoveredModelMetadata | null> {
|
|
1734
|
-
const showUrl = `${endpoint}/api/show`;
|
|
1735
|
-
try {
|
|
1736
|
-
const response = await this.#fetch(showUrl, {
|
|
1737
|
-
method: "POST",
|
|
1738
|
-
headers: { ...(headers ?? {}), "Content-Type": "application/json" },
|
|
1739
|
-
body: JSON.stringify({ model: modelId }),
|
|
1740
|
-
signal: AbortSignal.timeout(150),
|
|
1741
|
-
});
|
|
1742
|
-
if (!response.ok) {
|
|
1743
|
-
return null;
|
|
1744
|
-
}
|
|
1745
|
-
const payload = (await response.json()) as unknown;
|
|
1746
|
-
if (!isRecord(payload)) {
|
|
1747
|
-
return null;
|
|
1748
|
-
}
|
|
1749
|
-
const contextWindow = extractOllamaContextWindow(payload);
|
|
1750
|
-
const capabilities = payload.capabilities;
|
|
1751
|
-
if (Array.isArray(capabilities)) {
|
|
1752
|
-
const normalized = new Set(
|
|
1753
|
-
capabilities.flatMap(capability => (typeof capability === "string" ? [capability.toLowerCase()] : [])),
|
|
1754
|
-
);
|
|
1755
|
-
const supportsVision = normalized.has("vision") || normalized.has("image");
|
|
1756
|
-
return {
|
|
1757
|
-
reasoning: normalized.has("thinking"),
|
|
1758
|
-
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1759
|
-
contextWindow,
|
|
1760
|
-
};
|
|
1761
|
-
}
|
|
1762
|
-
if (!isRecord(capabilities)) {
|
|
1763
|
-
return {
|
|
1764
|
-
reasoning: false,
|
|
1765
|
-
input: ["text"],
|
|
1766
|
-
contextWindow,
|
|
1767
|
-
};
|
|
1768
|
-
}
|
|
1769
|
-
const supportsVision = capabilities.vision === true || capabilities.image === true;
|
|
1770
|
-
return {
|
|
1771
|
-
reasoning: capabilities.thinking === true,
|
|
1772
|
-
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1773
|
-
contextWindow,
|
|
1774
|
-
};
|
|
1775
|
-
} catch {
|
|
1776
|
-
return null;
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
async #discoverOllamaModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1781
|
-
const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
|
|
1782
|
-
const tagsUrl = `${endpoint}/api/tags`;
|
|
1783
|
-
const headers = { ...(providerConfig.headers ?? {}) };
|
|
1784
|
-
const response = await this.#fetch(tagsUrl, {
|
|
1785
|
-
headers,
|
|
1786
|
-
signal: AbortSignal.timeout(250),
|
|
1787
|
-
});
|
|
1788
|
-
if (!response.ok) {
|
|
1789
|
-
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
1790
|
-
}
|
|
1791
|
-
const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
1792
|
-
const entries = (payload.models ?? []).flatMap(item => {
|
|
1793
|
-
const id = item.model || item.name;
|
|
1794
|
-
return id ? [{ id, name: item.name || id }] : [];
|
|
1795
|
-
});
|
|
1796
|
-
const metadataById = new Map(
|
|
1797
|
-
await Promise.all(
|
|
1798
|
-
entries.map(
|
|
1799
|
-
async entry => [entry.id, await this.#discoverOllamaModelMetadata(endpoint, entry.id, headers)] as const,
|
|
1800
|
-
),
|
|
1801
|
-
),
|
|
1802
|
-
);
|
|
1803
|
-
const discovered = entries.map(entry => {
|
|
1804
|
-
const metadata = metadataById.get(entry.id);
|
|
1805
|
-
return enrichModelThinking({
|
|
1806
|
-
id: entry.id,
|
|
1807
|
-
name: entry.name,
|
|
1808
|
-
api: providerConfig.api,
|
|
1809
|
-
provider: providerConfig.provider,
|
|
1810
|
-
baseUrl: `${endpoint}/v1`,
|
|
1811
|
-
reasoning: metadata?.reasoning ?? false,
|
|
1812
|
-
input: metadata?.input ?? ["text"],
|
|
1813
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1814
|
-
contextWindow: metadata?.contextWindow ?? 128000,
|
|
1815
|
-
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
1816
|
-
headers: providerConfig.headers,
|
|
1817
|
-
});
|
|
1818
|
-
});
|
|
1819
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
async #discoverLlamaCppServerMetadata(
|
|
1823
|
-
baseUrl: string,
|
|
1824
|
-
headers: Record<string, string> | undefined,
|
|
1825
|
-
): Promise<LlamaCppDiscoveredServerMetadata | null> {
|
|
1826
|
-
const propsUrl = `${this.#toLlamaCppNativeBaseUrl(baseUrl)}/props`;
|
|
1827
|
-
try {
|
|
1828
|
-
const response = await this.#fetch(propsUrl, {
|
|
1829
|
-
headers,
|
|
1830
|
-
signal: AbortSignal.timeout(150),
|
|
1831
|
-
});
|
|
1832
|
-
if (!response.ok) {
|
|
1833
|
-
return null;
|
|
1834
|
-
}
|
|
1835
|
-
const payload = (await response.json()) as unknown;
|
|
1836
|
-
if (!isRecord(payload)) {
|
|
1837
|
-
return null;
|
|
1838
|
-
}
|
|
1839
|
-
return {
|
|
1840
|
-
contextWindow: extractLlamaCppContextWindow(payload),
|
|
1841
|
-
input: extractLlamaCppInputCapabilities(payload),
|
|
1842
|
-
};
|
|
1843
|
-
} catch {
|
|
1844
|
-
return null;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
async #discoverLlamaCppModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1849
|
-
const baseUrl = this.#normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
|
|
1850
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1851
|
-
|
|
1852
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1853
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1854
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1855
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
const [response, serverMetadata] = await Promise.all([
|
|
1859
|
-
this.#fetch(modelsUrl, {
|
|
1860
|
-
headers,
|
|
1861
|
-
signal: AbortSignal.timeout(250),
|
|
1862
|
-
}),
|
|
1863
|
-
this.#discoverLlamaCppServerMetadata(baseUrl, headers),
|
|
1864
|
-
]);
|
|
1865
|
-
if (!response.ok) {
|
|
1866
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1867
|
-
}
|
|
1868
|
-
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1869
|
-
const models = payload.data ?? [];
|
|
1870
|
-
const discovered: Model<Api>[] = [];
|
|
1871
|
-
for (const item of models) {
|
|
1872
|
-
const id = item.id;
|
|
1873
|
-
if (!id) continue;
|
|
1874
|
-
discovered.push(
|
|
1875
|
-
enrichModelThinking({
|
|
1876
|
-
id,
|
|
1877
|
-
name: id,
|
|
1878
|
-
api: providerConfig.api,
|
|
1879
|
-
provider: providerConfig.provider,
|
|
1880
|
-
baseUrl,
|
|
1881
|
-
reasoning: false,
|
|
1882
|
-
input: serverMetadata?.input ?? ["text"],
|
|
1883
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1884
|
-
contextWindow: serverMetadata?.contextWindow ?? 128000,
|
|
1885
|
-
maxTokens: Math.min(
|
|
1886
|
-
serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY,
|
|
1887
|
-
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
1888
|
-
),
|
|
1889
|
-
headers,
|
|
1890
|
-
compat: {
|
|
1891
|
-
supportsStore: false,
|
|
1892
|
-
supportsDeveloperRole: false,
|
|
1893
|
-
supportsReasoningEffort: false,
|
|
1894
|
-
},
|
|
1895
|
-
}),
|
|
1896
|
-
);
|
|
1897
|
-
}
|
|
1898
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
async #discoverOpenAIModelsList(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1902
|
-
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
1903
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1904
|
-
|
|
1905
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1906
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1907
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1908
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
const response = await this.#fetch(modelsUrl, {
|
|
1912
|
-
headers,
|
|
1913
|
-
signal: AbortSignal.timeout(10_000),
|
|
1914
|
-
});
|
|
1915
|
-
if (!response.ok) {
|
|
1916
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1917
|
-
}
|
|
1918
|
-
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1919
|
-
const models = payload.data ?? [];
|
|
1920
|
-
const discovered: Model<Api>[] = [];
|
|
1921
|
-
for (const item of models) {
|
|
1922
|
-
const id = item.id;
|
|
1923
|
-
if (!id) continue;
|
|
1924
|
-
discovered.push(
|
|
1925
|
-
enrichModelThinking({
|
|
1926
|
-
id,
|
|
1927
|
-
name: id,
|
|
1928
|
-
api: providerConfig.api,
|
|
1929
|
-
provider: providerConfig.provider,
|
|
1930
|
-
baseUrl,
|
|
1931
|
-
reasoning: false,
|
|
1932
|
-
input: ["text"],
|
|
1933
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1934
|
-
contextWindow: 128000,
|
|
1935
|
-
maxTokens: discoveryDefaultMaxTokens(providerConfig.api),
|
|
1936
|
-
headers,
|
|
1937
|
-
compat: {
|
|
1938
|
-
supportsStore: false,
|
|
1939
|
-
supportsDeveloperRole: false,
|
|
1940
|
-
supportsReasoningEffort: false,
|
|
1941
|
-
},
|
|
1942
|
-
}),
|
|
1943
|
-
);
|
|
1944
|
-
}
|
|
1945
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
/**
|
|
1949
|
-
* Discover models from an Anthropic+OpenAI-compatible reseller proxy that
|
|
1950
|
-
* exposes both `/v1/messages` and `/v1/chat/completions`, advertising each
|
|
1951
|
-
* model's wire capabilities through `supported_endpoint_types` on
|
|
1952
|
-
* `GET /v1/models` (new-api / one-api-style proxies).
|
|
1953
|
-
*
|
|
1954
|
-
* Routing per model:
|
|
1955
|
-
* supported_endpoint_types: ["anthropic", ...] -> api: "anthropic-messages"
|
|
1956
|
-
* supported_endpoint_types: ["openai"] -> api: "openai-completions"
|
|
1957
|
-
* missing / neither -> provider-level api fallback
|
|
1958
|
-
*
|
|
1959
|
-
* Anthropic models share the same baseUrl; the Anthropic SDK strips a
|
|
1960
|
-
* trailing `/v1` itself before appending `/v1/messages`, so the discovery
|
|
1961
|
-
* URL (which ends in `/v1`) round-trips correctly.
|
|
1962
|
-
*/
|
|
1963
|
-
async #discoverProxyModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1964
|
-
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
1965
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1966
|
-
|
|
1967
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1968
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1969
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1970
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
const response = await this.#fetch(modelsUrl, {
|
|
1974
|
-
headers,
|
|
1975
|
-
signal: AbortSignal.timeout(10_000),
|
|
1976
|
-
});
|
|
1977
|
-
if (!response.ok) {
|
|
1978
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1979
|
-
}
|
|
1980
|
-
const payload = (await response.json()) as {
|
|
1981
|
-
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>;
|
|
1982
|
-
};
|
|
1983
|
-
const items = payload.data ?? [];
|
|
1984
|
-
const discovered: Model<Api>[] = [];
|
|
1985
|
-
for (const item of items) {
|
|
1986
|
-
const id = item.id;
|
|
1987
|
-
if (!id) continue;
|
|
1988
|
-
const endpoints = item.supported_endpoint_types ?? [];
|
|
1989
|
-
const api: Api | undefined = endpoints.includes("anthropic")
|
|
1990
|
-
? "anthropic-messages"
|
|
1991
|
-
: endpoints.includes("openai")
|
|
1992
|
-
? "openai-completions"
|
|
1993
|
-
: providerConfig.api;
|
|
1994
|
-
if (!api) continue;
|
|
1995
|
-
const isAnthropic = api === "anthropic-messages";
|
|
1996
|
-
const reference = resolveCustomModelReference(id);
|
|
1997
|
-
const discoveryName = typeof item.name === "string" ? item.name.trim() : "";
|
|
1998
|
-
const displayName =
|
|
1999
|
-
reference?.name ??
|
|
2000
|
-
(discoveryName && discoveryName !== id ? discoveryName : undefined) ??
|
|
2001
|
-
stripBracketedModelIdAffixes(id) ??
|
|
2002
|
-
id;
|
|
2003
|
-
discovered.push(
|
|
2004
|
-
enrichModelThinking({
|
|
2005
|
-
id,
|
|
2006
|
-
name: displayName,
|
|
2007
|
-
api,
|
|
2008
|
-
provider: providerConfig.provider,
|
|
2009
|
-
baseUrl,
|
|
2010
|
-
reasoning: reference?.reasoning ?? false,
|
|
2011
|
-
thinking: reference?.thinking,
|
|
2012
|
-
input: reference?.input ?? ["text"],
|
|
2013
|
-
// Proxy pricing is provider-specific and usually does not match
|
|
2014
|
-
// upstream bundled catalogs, so keep costs local-unknown even when
|
|
2015
|
-
// we successfully recover the upstream model identity.
|
|
2016
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2017
|
-
contextWindow: reference?.contextWindow ?? 128000,
|
|
2018
|
-
maxTokens: reference?.maxTokens ?? discoveryDefaultMaxTokens(api),
|
|
2019
|
-
headers,
|
|
2020
|
-
// OpenAI-compat fields are no-ops on anthropic models; the
|
|
2021
|
-
// Anthropic SDK ignores them. Provider-level disableStrictTools
|
|
2022
|
-
// flows in via #applyProviderCompat for the third-party-Anthropic
|
|
2023
|
-
// path. Cross-wire bundled compat is intentionally not copied:
|
|
2024
|
-
// request-shaping fields are provider-wire specific.
|
|
2025
|
-
compat: isAnthropic
|
|
2026
|
-
? undefined
|
|
2027
|
-
: {
|
|
2028
|
-
supportsStore: false,
|
|
2029
|
-
supportsDeveloperRole: false,
|
|
2030
|
-
supportsReasoningEffort: false,
|
|
2031
|
-
},
|
|
2032
|
-
}),
|
|
2033
|
-
);
|
|
2034
|
-
}
|
|
2035
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
#normalizeLlamaCppBaseUrl(baseUrl?: string): string {
|
|
2039
|
-
const defaultBaseUrl = "http://127.0.0.1:8080";
|
|
2040
|
-
const raw = baseUrl || defaultBaseUrl;
|
|
2041
|
-
try {
|
|
2042
|
-
const parsed = new URL(raw);
|
|
2043
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2044
|
-
return `${parsed.protocol}//${parsed.host}${trimmedPath}`;
|
|
2045
|
-
} catch {
|
|
2046
|
-
return raw;
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
#toLlamaCppNativeBaseUrl(baseUrl: string): string {
|
|
2051
|
-
try {
|
|
2052
|
-
const parsed = new URL(baseUrl);
|
|
2053
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2054
|
-
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath.slice(0, -3) || "/" : trimmedPath || "/";
|
|
2055
|
-
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2056
|
-
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2057
|
-
} catch {
|
|
2058
|
-
return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
#normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
|
|
2063
|
-
const defaultBaseUrl = "http://127.0.0.1:1234/v1";
|
|
2064
|
-
const raw = baseUrl || defaultBaseUrl;
|
|
2065
|
-
try {
|
|
2066
|
-
const parsed = new URL(raw);
|
|
2067
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2068
|
-
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
|
|
2069
|
-
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2070
|
-
} catch {
|
|
2071
|
-
return raw;
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
#normalizeOllamaBaseUrl(baseUrl?: string): string {
|
|
2075
|
-
const raw = baseUrl || DEFAULT_OLLAMA_BASE_URL;
|
|
2076
|
-
try {
|
|
2077
|
-
const parsed = new URL(raw);
|
|
2078
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
2079
|
-
} catch {
|
|
2080
|
-
return DEFAULT_OLLAMA_BASE_URL;
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
1337
|
#applyProviderModelOverrides(provider: string, models: Model<Api>[]): Model<Api>[] {
|
|
2085
1338
|
const overrides = this.#modelOverrides.get(provider);
|
|
2086
1339
|
if (!overrides || overrides.size === 0) return models;
|
|
@@ -2158,7 +1411,11 @@ export class ModelRegistry {
|
|
|
2158
1411
|
this.#rebuildPending = true;
|
|
2159
1412
|
return;
|
|
2160
1413
|
}
|
|
2161
|
-
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1414
|
+
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1415
|
+
this.#models,
|
|
1416
|
+
getBundledCanonicalReferenceData(),
|
|
1417
|
+
this.#equivalenceConfig,
|
|
1418
|
+
);
|
|
2162
1419
|
this.#rebuildPending = false;
|
|
2163
1420
|
}
|
|
2164
1421
|
|
|
@@ -2172,7 +1429,11 @@ export class ModelRegistry {
|
|
|
2172
1429
|
}
|
|
2173
1430
|
if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
|
|
2174
1431
|
this.#rebuildPending = false;
|
|
2175
|
-
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1432
|
+
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1433
|
+
this.#models,
|
|
1434
|
+
getBundledCanonicalReferenceData(),
|
|
1435
|
+
this.#equivalenceConfig,
|
|
1436
|
+
);
|
|
2176
1437
|
}
|
|
2177
1438
|
}
|
|
2178
1439
|
|
|
@@ -2217,81 +1478,73 @@ export class ModelRegistry {
|
|
|
2217
1478
|
return this.#models;
|
|
2218
1479
|
}
|
|
2219
1480
|
|
|
2220
|
-
|
|
1481
|
+
/**
|
|
1482
|
+
* Availability predicate with per-provider memoization. Auth lookups
|
|
1483
|
+
* (`authStorage.hasAuth`) and the disabled-provider set are resolved once
|
|
1484
|
+
* per provider instead of once per model, which matters when filtering the
|
|
1485
|
+
* full bundled catalog (thousands of models, ~50 providers).
|
|
1486
|
+
*/
|
|
1487
|
+
#createAvailabilityCheck(): (model: Model<Api>) => boolean {
|
|
2221
1488
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
1489
|
+
const byProvider = new Map<string, boolean>();
|
|
1490
|
+
return model => {
|
|
1491
|
+
let available = byProvider.get(model.provider);
|
|
1492
|
+
if (available === undefined) {
|
|
1493
|
+
available =
|
|
1494
|
+
!disabledProviders.has(model.provider) &&
|
|
1495
|
+
(this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider));
|
|
1496
|
+
byProvider.set(model.provider, available);
|
|
1497
|
+
}
|
|
1498
|
+
return available;
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Build the shared per-query filter state for canonical model queries.
|
|
1504
|
+
* Hoisted out of the per-record loop: building the candidate-selector set
|
|
1505
|
+
* and availability memo once per query instead of once per record is what
|
|
1506
|
+
* keeps `getCanonicalModelSelections` linear instead of O(records × candidates).
|
|
1507
|
+
*/
|
|
1508
|
+
#canonicalQueryFilters(options: CanonicalModelQueryOptions | undefined): {
|
|
1509
|
+
candidateKeys: Set<string> | undefined;
|
|
1510
|
+
isAvailable: ((model: Model<Api>) => boolean) | undefined;
|
|
1511
|
+
} {
|
|
1512
|
+
return {
|
|
1513
|
+
candidateKeys: options?.candidates
|
|
1514
|
+
? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
|
|
1515
|
+
: undefined,
|
|
1516
|
+
isAvailable: options?.availableOnly ? this.#createAvailabilityCheck() : undefined,
|
|
1517
|
+
};
|
|
2226
1518
|
}
|
|
2227
1519
|
|
|
2228
1520
|
#filterCanonicalVariants(
|
|
2229
1521
|
record: CanonicalModelRecord,
|
|
2230
|
-
|
|
1522
|
+
candidateKeys: ReadonlySet<string> | undefined,
|
|
1523
|
+
isAvailable: ((model: Model<Api>) => boolean) | undefined,
|
|
2231
1524
|
): CanonicalModelVariant[] {
|
|
2232
|
-
const candidateKeys = options?.candidates
|
|
2233
|
-
? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
|
|
2234
|
-
: undefined;
|
|
2235
1525
|
return record.variants.filter(variant => {
|
|
2236
1526
|
if (candidateKeys && !candidateKeys.has(variant.selector)) {
|
|
2237
1527
|
return false;
|
|
2238
1528
|
}
|
|
2239
|
-
if (
|
|
1529
|
+
if (isAvailable && !isAvailable(variant.model)) {
|
|
2240
1530
|
return false;
|
|
2241
1531
|
}
|
|
2242
1532
|
return true;
|
|
2243
1533
|
});
|
|
2244
1534
|
}
|
|
2245
1535
|
|
|
2246
|
-
#
|
|
2247
|
-
return
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
#resolveCanonicalVariant(
|
|
2251
|
-
variants: readonly CanonicalModelVariant[],
|
|
2252
|
-
allCandidates: readonly Model<Api>[],
|
|
2253
|
-
): CanonicalModelVariant | undefined {
|
|
2254
|
-
if (variants.length === 0) {
|
|
2255
|
-
return undefined;
|
|
2256
|
-
}
|
|
2257
|
-
const providerRank = this.#providerRank();
|
|
2258
|
-
const modelOrder = new Map<string, number>();
|
|
2259
|
-
for (let index = 0; index < allCandidates.length; index += 1) {
|
|
2260
|
-
modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
|
|
2261
|
-
}
|
|
2262
|
-
const sourceRank: Record<CanonicalModelVariant["source"], number> = {
|
|
2263
|
-
override: 1,
|
|
2264
|
-
bundled: 1,
|
|
2265
|
-
heuristic: 2,
|
|
2266
|
-
fallback: 3,
|
|
1536
|
+
#variantPreferences(candidates: readonly Model<Api>[]): CanonicalVariantPreferences {
|
|
1537
|
+
return {
|
|
1538
|
+
modelOrder: buildCanonicalModelOrder(candidates),
|
|
1539
|
+
providerRank: buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings()),
|
|
2267
1540
|
};
|
|
2268
|
-
return [...variants].sort((left, right) => {
|
|
2269
|
-
const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2270
|
-
const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2271
|
-
if (leftProviderRank !== rightProviderRank) {
|
|
2272
|
-
return leftProviderRank - rightProviderRank;
|
|
2273
|
-
}
|
|
2274
|
-
const leftExact = left.model.id === left.canonicalId ? 0 : 1;
|
|
2275
|
-
const rightExact = right.model.id === right.canonicalId ? 0 : 1;
|
|
2276
|
-
if (leftExact !== rightExact) {
|
|
2277
|
-
return leftExact - rightExact;
|
|
2278
|
-
}
|
|
2279
|
-
if (sourceRank[left.source] !== sourceRank[right.source]) {
|
|
2280
|
-
return sourceRank[left.source] - sourceRank[right.source];
|
|
2281
|
-
}
|
|
2282
|
-
if (left.model.id.length !== right.model.id.length) {
|
|
2283
|
-
return left.model.id.length - right.model.id.length;
|
|
2284
|
-
}
|
|
2285
|
-
const leftOrder = modelOrder.get(left.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
2286
|
-
const rightOrder = modelOrder.get(right.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
2287
|
-
return leftOrder - rightOrder;
|
|
2288
|
-
})[0];
|
|
2289
1541
|
}
|
|
2290
1542
|
|
|
2291
1543
|
getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
|
|
1544
|
+
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
2292
1545
|
const records: CanonicalModelRecord[] = [];
|
|
2293
1546
|
for (const record of this.#canonicalIndex.records) {
|
|
2294
|
-
const variants = this.#filterCanonicalVariants(record,
|
|
1547
|
+
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
2295
1548
|
if (variants.length === 0) {
|
|
2296
1549
|
continue;
|
|
2297
1550
|
}
|
|
@@ -2304,12 +1557,42 @@ export class ModelRegistry {
|
|
|
2304
1557
|
return records;
|
|
2305
1558
|
}
|
|
2306
1559
|
|
|
1560
|
+
/**
|
|
1561
|
+
* One-pass equivalent of `getCanonicalModels` + `resolveCanonicalModel` per
|
|
1562
|
+
* record. The per-query state (candidate-selector set, availability memo,
|
|
1563
|
+
* provider rank, candidate order) is built once, so the whole catalog
|
|
1564
|
+
* resolves in O(records + candidates) instead of O(records × candidates).
|
|
1565
|
+
* This is the path the model selector hydrates from synchronously on open.
|
|
1566
|
+
*/
|
|
1567
|
+
getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[] {
|
|
1568
|
+
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
1569
|
+
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
1570
|
+
const preferences = this.#variantPreferences(candidates);
|
|
1571
|
+
const selections: CanonicalModelSelection[] = [];
|
|
1572
|
+
for (const record of this.#canonicalIndex.records) {
|
|
1573
|
+
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
1574
|
+
if (variants.length === 0) {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
const resolved = resolveCanonicalVariant(variants, preferences);
|
|
1578
|
+
if (!resolved) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
selections.push({
|
|
1582
|
+
record: { id: record.id, name: record.name, variants },
|
|
1583
|
+
model: resolved.model,
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
return selections;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
2307
1589
|
getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
|
|
2308
1590
|
const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
|
|
2309
1591
|
if (!record) {
|
|
2310
1592
|
return [];
|
|
2311
1593
|
}
|
|
2312
|
-
|
|
1594
|
+
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
1595
|
+
return this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
2313
1596
|
}
|
|
2314
1597
|
|
|
2315
1598
|
resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
|
|
@@ -2318,7 +1601,7 @@ export class ModelRegistry {
|
|
|
2318
1601
|
return undefined;
|
|
2319
1602
|
}
|
|
2320
1603
|
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
2321
|
-
return
|
|
1604
|
+
return resolveCanonicalVariant(variants, this.#variantPreferences(candidates))?.model;
|
|
2322
1605
|
}
|
|
2323
1606
|
|
|
2324
1607
|
getCanonicalId(model: Model<Api>): string | undefined {
|
|
@@ -2330,7 +1613,7 @@ export class ModelRegistry {
|
|
|
2330
1613
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
2331
1614
|
*/
|
|
2332
1615
|
getAvailable(): Model<Api>[] {
|
|
2333
|
-
return this.#models.filter(
|
|
1616
|
+
return this.#models.filter(this.#createAvailabilityCheck());
|
|
2334
1617
|
}
|
|
2335
1618
|
|
|
2336
1619
|
/**
|
|
@@ -2627,7 +1910,7 @@ export class ModelRegistry {
|
|
|
2627
1910
|
);
|
|
2628
1911
|
if (overlay) results.push(finalizeCustomModel(overlay, { useDefaults: true }));
|
|
2629
1912
|
}
|
|
2630
|
-
return results;
|
|
1913
|
+
return results.map(toModelSpec);
|
|
2631
1914
|
},
|
|
2632
1915
|
};
|
|
2633
1916
|
this.#runtimeModelManagers.set(providerName, { options: managerOptions, sourceId: sourceId ?? "" });
|
|
@@ -2701,7 +1984,7 @@ export interface ProviderConfigInput {
|
|
|
2701
1984
|
api?: Api;
|
|
2702
1985
|
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
2703
1986
|
headers?: Record<string, string>;
|
|
2704
|
-
compat?:
|
|
1987
|
+
compat?: ModelSpec<Api>["compat"];
|
|
2705
1988
|
authHeader?: boolean;
|
|
2706
1989
|
/** Streaming transport override — see {@link Model.transport}. */
|
|
2707
1990
|
transport?: Model<Api>["transport"];
|
|
@@ -2733,7 +2016,7 @@ export interface ProviderConfigInput {
|
|
|
2733
2016
|
contextWindow: number;
|
|
2734
2017
|
maxTokens: number;
|
|
2735
2018
|
headers?: Record<string, string>;
|
|
2736
|
-
compat?:
|
|
2019
|
+
compat?: ModelSpec<Api>["compat"];
|
|
2737
2020
|
contextPromotionTarget?: string;
|
|
2738
2021
|
premiumMultiplier?: number;
|
|
2739
2022
|
}>;
|