@oh-my-pi/pi-coding-agent 15.10.10 → 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 +95 -4
- 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 +7 -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 -7
- 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/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 +25 -6
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/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/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/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 +2 -1
- 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 +231 -1019
- 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 -4
- 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/edit/diff.ts +47 -3
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +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 +40 -22
- 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 +8 -8
- 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 +4 -2
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +258 -53
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +10 -3
- package/src/modes/controllers/event-controller.ts +73 -4
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +3 -9
- 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 +13 -24
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +5 -7
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +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/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 +26 -11
- 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 +38 -8
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/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/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 {
|
|
@@ -447,17 +226,6 @@ interface CustomModelsResult {
|
|
|
447
226
|
found: boolean;
|
|
448
227
|
}
|
|
449
228
|
|
|
450
|
-
type OllamaDiscoveredModelMetadata = {
|
|
451
|
-
reasoning: boolean;
|
|
452
|
-
input: ("text" | "image")[];
|
|
453
|
-
contextWindow?: number;
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
type LlamaCppDiscoveredServerMetadata = {
|
|
457
|
-
contextWindow?: number;
|
|
458
|
-
input?: ("text" | "image")[];
|
|
459
|
-
};
|
|
460
|
-
|
|
461
229
|
/**
|
|
462
230
|
* Resolve an API key config value to an actual key.
|
|
463
231
|
* Checks environment variable first, then treats as literal.
|
|
@@ -468,59 +236,6 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
|
468
236
|
return keyConfig;
|
|
469
237
|
}
|
|
470
238
|
|
|
471
|
-
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
472
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
473
|
-
return value;
|
|
474
|
-
}
|
|
475
|
-
if (typeof value === "string" && value.trim()) {
|
|
476
|
-
const parsed = Number(value);
|
|
477
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
478
|
-
return parsed;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return undefined;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function extractOllamaContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
485
|
-
const modelInfo = payload.model_info;
|
|
486
|
-
if (isRecord(modelInfo)) {
|
|
487
|
-
for (const [key, value] of Object.entries(modelInfo)) {
|
|
488
|
-
if (key === "context_length" || key.endsWith(".context_length")) {
|
|
489
|
-
const contextWindow = toPositiveNumberOrUndefined(value);
|
|
490
|
-
if (contextWindow !== undefined) {
|
|
491
|
-
return contextWindow;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const parameters = payload.parameters;
|
|
498
|
-
if (typeof parameters !== "string") {
|
|
499
|
-
return undefined;
|
|
500
|
-
}
|
|
501
|
-
const match = parameters.match(/(?:^|\n)\s*num_ctx\s+(\d+)\s*(?:$|\n)/m);
|
|
502
|
-
return match ? toPositiveNumberOrUndefined(match[1]) : undefined;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function extractLlamaCppContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
506
|
-
const generationSettings = payload.default_generation_settings;
|
|
507
|
-
if (isRecord(generationSettings)) {
|
|
508
|
-
const contextWindow = toPositiveNumberOrUndefined(generationSettings.n_ctx);
|
|
509
|
-
if (contextWindow !== undefined) {
|
|
510
|
-
return contextWindow;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return toPositiveNumberOrUndefined(payload.n_ctx);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function extractLlamaCppInputCapabilities(payload: Record<string, unknown>): ("text" | "image")[] | undefined {
|
|
517
|
-
const modalities = payload.modalities;
|
|
518
|
-
if (!isRecord(modalities)) {
|
|
519
|
-
return undefined;
|
|
520
|
-
}
|
|
521
|
-
return modalities.vision === true ? ["text", "image"] : ["text"];
|
|
522
|
-
}
|
|
523
|
-
|
|
524
239
|
function extractGoogleOAuthToken(value: string | undefined): string | undefined {
|
|
525
240
|
if (!isAuthenticated(value)) return undefined;
|
|
526
241
|
try {
|
|
@@ -579,73 +294,99 @@ function mergeCompat<TBase extends object, TOverride extends object>(
|
|
|
579
294
|
return merged as TBase & TOverride;
|
|
580
295
|
}
|
|
581
296
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
590
|
-
if (override.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = override.omitMaxOutputTokens;
|
|
591
|
-
if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
|
|
592
|
-
if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
|
|
593
|
-
if (override.cost) {
|
|
594
|
-
result.cost = {
|
|
595
|
-
input: override.cost.input ?? model.cost.input,
|
|
596
|
-
output: override.cost.output ?? model.cost.output,
|
|
597
|
-
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
|
|
598
|
-
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
if (override.headers) {
|
|
602
|
-
result.headers = { ...model.headers, ...override.headers };
|
|
603
|
-
}
|
|
604
|
-
result.compat = mergeCompat(model.compat, override.compat);
|
|
605
|
-
return enrichModelThinking(result);
|
|
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>;
|
|
606
304
|
}
|
|
607
305
|
|
|
608
|
-
|
|
609
|
-
|
|
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 {
|
|
610
312
|
name?: string;
|
|
611
|
-
api?: Api;
|
|
612
|
-
baseUrl?: string;
|
|
613
313
|
reasoning?: boolean;
|
|
614
314
|
thinking?: ThinkingConfig;
|
|
615
315
|
input?: ("text" | "image")[];
|
|
616
|
-
cost?:
|
|
316
|
+
cost?: Partial<Model<Api>["cost"]>;
|
|
617
317
|
contextWindow?: number;
|
|
618
318
|
maxTokens?: number;
|
|
619
319
|
omitMaxOutputTokens?: boolean;
|
|
620
320
|
headers?: Record<string, string>;
|
|
621
|
-
compat?:
|
|
321
|
+
compat?: ModelSpec<Api>["compat"];
|
|
622
322
|
contextPromotionTarget?: string;
|
|
623
323
|
premiumMultiplier?: number;
|
|
624
324
|
}
|
|
625
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
|
+
|
|
626
378
|
interface CustomModelBuildOptions {
|
|
627
379
|
useDefaults: boolean;
|
|
628
380
|
}
|
|
629
381
|
|
|
630
|
-
|
|
382
|
+
interface CustomModelOverlay extends ModelPatch {
|
|
631
383
|
id: string;
|
|
632
384
|
provider: string;
|
|
633
385
|
api: Api;
|
|
634
386
|
baseUrl: string;
|
|
635
|
-
|
|
636
|
-
reasoning?: boolean;
|
|
637
|
-
thinking?: ThinkingConfig;
|
|
638
|
-
input?: ("text" | "image")[];
|
|
639
|
-
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
640
|
-
contextWindow?: number;
|
|
641
|
-
maxTokens?: number;
|
|
642
|
-
omitMaxOutputTokens?: boolean;
|
|
643
|
-
headers?: Record<string, string>;
|
|
644
|
-
compat?: Model<Api>["compat"];
|
|
645
|
-
contextPromotionTarget?: string;
|
|
646
|
-
premiumMultiplier?: number;
|
|
387
|
+
cost?: Model<Api>["cost"];
|
|
647
388
|
isOAuth?: boolean;
|
|
648
|
-
}
|
|
389
|
+
}
|
|
649
390
|
|
|
650
391
|
function mergeCustomModelHeaders(
|
|
651
392
|
providerHeaders: Record<string, string> | undefined,
|
|
@@ -692,7 +433,7 @@ function buildCustomModelOverlay(
|
|
|
692
433
|
providerHeaders: Record<string, string> | undefined,
|
|
693
434
|
providerApiKey: string | undefined,
|
|
694
435
|
authHeader: boolean | undefined,
|
|
695
|
-
providerCompat:
|
|
436
|
+
providerCompat: ModelSpec<Api>["compat"] | undefined,
|
|
696
437
|
providerAuth: ProviderAuthMode | undefined,
|
|
697
438
|
modelDef: CustomModelDefinitionLike,
|
|
698
439
|
): CustomModelOverlay | undefined {
|
|
@@ -705,8 +446,8 @@ function buildCustomModelOverlay(
|
|
|
705
446
|
baseUrl: modelDef.baseUrl ?? providerBaseUrl,
|
|
706
447
|
name: modelDef.name,
|
|
707
448
|
reasoning: modelDef.reasoning,
|
|
708
|
-
thinking: modelDef.thinking
|
|
709
|
-
input: modelDef.input
|
|
449
|
+
thinking: modelDef.thinking,
|
|
450
|
+
input: modelDef.input,
|
|
710
451
|
cost: modelDef.cost,
|
|
711
452
|
contextWindow: modelDef.contextWindow,
|
|
712
453
|
maxTokens: modelDef.maxTokens,
|
|
@@ -719,125 +460,6 @@ function buildCustomModelOverlay(
|
|
|
719
460
|
};
|
|
720
461
|
}
|
|
721
462
|
|
|
722
|
-
// Custom provider entries often front a known upstream model through a local proxy.
|
|
723
|
-
// Use bundled metadata for missing pricing/capability fields, but keep the custom transport.
|
|
724
|
-
function shouldReplaceCustomReference(existing: Model<Api> | undefined, candidate: Model<Api>): boolean {
|
|
725
|
-
if (!existing) return true;
|
|
726
|
-
if (candidate.contextWindow !== existing.contextWindow) {
|
|
727
|
-
return candidate.contextWindow > existing.contextWindow;
|
|
728
|
-
}
|
|
729
|
-
if (candidate.maxTokens !== existing.maxTokens) {
|
|
730
|
-
return candidate.maxTokens > existing.maxTokens;
|
|
731
|
-
}
|
|
732
|
-
const existingHasCachePricing = existing.cost.cacheRead > 0 || existing.cost.cacheWrite > 0;
|
|
733
|
-
const candidateHasCachePricing = candidate.cost.cacheRead > 0 || candidate.cost.cacheWrite > 0;
|
|
734
|
-
if (candidateHasCachePricing !== existingHasCachePricing) {
|
|
735
|
-
return candidateHasCachePricing;
|
|
736
|
-
}
|
|
737
|
-
return existing.provider !== "openai" && candidate.provider === "openai";
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function normalizeCustomReferenceKey(value: string): string {
|
|
741
|
-
return value.trim().toLowerCase();
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function buildCustomReferenceMap(): Map<string, Model<Api>> {
|
|
745
|
-
const references = new Map<string, Model<Api>>();
|
|
746
|
-
for (const provider of getBundledProviders()) {
|
|
747
|
-
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
|
748
|
-
const candidate = model as Model<Api>;
|
|
749
|
-
const key = normalizeCustomReferenceKey(candidate.id);
|
|
750
|
-
if (shouldReplaceCustomReference(references.get(key), candidate)) {
|
|
751
|
-
references.set(key, candidate);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
return references;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function buildCustomReferenceSuffixAliasMap(exactReferences: ReadonlyMap<string, Model<Api>>): Map<string, Model<Api>> {
|
|
759
|
-
const aliases = new Map<string, Model<Api>>();
|
|
760
|
-
for (const reference of exactReferences.values()) {
|
|
761
|
-
const slashIndex = reference.id.lastIndexOf("/");
|
|
762
|
-
if (slashIndex === -1) {
|
|
763
|
-
continue;
|
|
764
|
-
}
|
|
765
|
-
const suffix = reference.id.slice(slashIndex + 1);
|
|
766
|
-
const alias = getLongestModelLikeIdSegment(suffix);
|
|
767
|
-
if (!alias) {
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
if (shouldReplaceCustomReference(aliases.get(alias), reference)) {
|
|
771
|
-
aliases.set(alias, reference);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return aliases;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const customReferenceMap = buildCustomReferenceMap();
|
|
778
|
-
const customReferenceSuffixAliasMap = buildCustomReferenceSuffixAliasMap(customReferenceMap);
|
|
779
|
-
|
|
780
|
-
const CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN =
|
|
781
|
-
/[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4|search)$/i;
|
|
782
|
-
|
|
783
|
-
function stripCustomReferenceTrailingMarker(candidate: string): string | undefined {
|
|
784
|
-
const match = CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN.exec(candidate);
|
|
785
|
-
return match ? candidate.slice(0, match.index) : undefined;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function getCustomReferenceCandidateIds(modelId: string): string[] {
|
|
789
|
-
const candidates = new Set<string>();
|
|
790
|
-
const queue = [modelId];
|
|
791
|
-
for (let index = 0; index < queue.length; index += 1) {
|
|
792
|
-
const candidate = queue[index]?.trim();
|
|
793
|
-
if (!candidate || candidates.has(candidate)) continue;
|
|
794
|
-
candidates.add(candidate);
|
|
795
|
-
|
|
796
|
-
for (const stripped of getBracketStrippedModelIdCandidates(candidate)) {
|
|
797
|
-
queue.push(stripped);
|
|
798
|
-
}
|
|
799
|
-
for (const segment of getModelLikeIdSegments(candidate)) {
|
|
800
|
-
queue.push(segment);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
for (const suffix of [":cloud", "-cloud"] as const) {
|
|
804
|
-
if (candidate.toLowerCase().endsWith(suffix)) {
|
|
805
|
-
queue.push(candidate.slice(0, -suffix.length));
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const slashIndex = candidate.lastIndexOf("/");
|
|
810
|
-
if (slashIndex !== -1) {
|
|
811
|
-
queue.push(candidate.slice(slashIndex + 1));
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const colonToDash = candidate.replace(/:/g, "-");
|
|
815
|
-
if (colonToDash !== candidate) {
|
|
816
|
-
queue.push(colonToDash);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const lowercased = candidate.toLowerCase();
|
|
820
|
-
if (lowercased !== candidate) {
|
|
821
|
-
queue.push(lowercased);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
const strippedMarker = stripCustomReferenceTrailingMarker(candidate);
|
|
825
|
-
if (strippedMarker) {
|
|
826
|
-
queue.push(strippedMarker);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return [...candidates];
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
|
|
833
|
-
for (const candidate of getCustomReferenceCandidateIds(modelId)) {
|
|
834
|
-
const key = normalizeCustomReferenceKey(candidate);
|
|
835
|
-
const reference = customReferenceMap.get(key) ?? customReferenceSuffixAliasMap.get(key);
|
|
836
|
-
if (reference) return reference;
|
|
837
|
-
}
|
|
838
|
-
return undefined;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
463
|
function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
|
|
842
464
|
if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
|
|
843
465
|
return model;
|
|
@@ -847,13 +469,15 @@ function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomMo
|
|
|
847
469
|
|
|
848
470
|
function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
|
|
849
471
|
const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
|
|
850
|
-
const reference = options.useDefaults
|
|
472
|
+
const reference = options.useDefaults
|
|
473
|
+
? resolveModelReference(resolvedModel.id, getBundledModelReferenceIndex())
|
|
474
|
+
: undefined;
|
|
851
475
|
const cost =
|
|
852
476
|
resolvedModel.cost ??
|
|
853
477
|
reference?.cost ??
|
|
854
478
|
(options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
|
855
479
|
const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
|
|
856
|
-
return
|
|
480
|
+
return buildModel({
|
|
857
481
|
id: resolvedModel.id,
|
|
858
482
|
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
859
483
|
api: resolvedModel.api,
|
|
@@ -868,11 +492,11 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
868
492
|
maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : undefined),
|
|
869
493
|
headers: resolvedModel.headers,
|
|
870
494
|
omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
|
|
871
|
-
compat: mergeCompat(reference?.
|
|
495
|
+
compat: mergeCompat(reference?.compatConfig, resolvedModel.compat),
|
|
872
496
|
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
873
497
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
874
498
|
isOAuth: resolvedModel.isOAuth,
|
|
875
|
-
} as
|
|
499
|
+
} as ModelSpec<Api>);
|
|
876
500
|
}
|
|
877
501
|
|
|
878
502
|
function normalizeSuppressedSelector(selector: string): string {
|
|
@@ -1133,84 +757,46 @@ export class ModelRegistry {
|
|
|
1133
757
|
return models.map(m => {
|
|
1134
758
|
if (!providerOverride) return m;
|
|
1135
759
|
const withTransportOverride = this.#applyProviderTransportOverride(m, providerOverride);
|
|
1136
|
-
return {
|
|
760
|
+
return buildModel({
|
|
1137
761
|
...withTransportOverride,
|
|
1138
|
-
compat: mergeCompat(m.
|
|
1139
|
-
};
|
|
762
|
+
compat: mergeCompat(m.compatConfig, providerOverride.compat),
|
|
763
|
+
} as ModelSpec<Api>);
|
|
1140
764
|
});
|
|
1141
765
|
});
|
|
1142
766
|
}
|
|
1143
767
|
|
|
1144
768
|
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
merged[existingIndex] = {
|
|
1157
|
-
...replacementModel,
|
|
1158
|
-
contextWindow:
|
|
1159
|
-
replacementModel.contextWindow === UNK_CONTEXT_WINDOW
|
|
1160
|
-
? existing.contextWindow
|
|
1161
|
-
: replacementModel.contextWindow,
|
|
1162
|
-
maxTokens:
|
|
1163
|
-
replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
|
|
1164
|
-
};
|
|
1165
|
-
} else {
|
|
1166
|
-
merged.push(replacementModel);
|
|
1167
|
-
indexByKey.set(key, merged.length - 1);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
return merged;
|
|
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
|
+
});
|
|
1171
780
|
}
|
|
1172
781
|
|
|
1173
782
|
/** Merge custom models with built-in, replacing by provider+id match */
|
|
1174
783
|
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
for (const customModel of customModels) {
|
|
1182
|
-
const key = `${customModel.provider}\u0000${customModel.id}`;
|
|
1183
|
-
const existingIndex = indexByKey.get(key);
|
|
1184
|
-
if (existingIndex !== undefined) {
|
|
1185
|
-
const existingModel = merged[existingIndex];
|
|
1186
|
-
merged[existingIndex] = enrichModelThinking({
|
|
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
|
+
{
|
|
1187
790
|
...existingModel,
|
|
1188
791
|
id: customModel.id,
|
|
1189
792
|
provider: customModel.provider,
|
|
1190
793
|
api: customModel.api,
|
|
1191
794
|
baseUrl: customModel.baseUrl,
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
|
|
1198
|
-
maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
|
|
1199
|
-
omitMaxOutputTokens: customModel.omitMaxOutputTokens ?? existingModel.omitMaxOutputTokens,
|
|
1200
|
-
// Same-id custom definitions replace bundled transport behavior. Provider-level
|
|
1201
|
-
// headers/compat were already folded into customModel during parsing; do not
|
|
1202
|
-
// re-merge bundled transport metadata here.
|
|
1203
|
-
headers: customModel.headers,
|
|
1204
|
-
compat: customModel.compat,
|
|
1205
|
-
contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
|
1206
|
-
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
1207
|
-
} as Model<Api>);
|
|
1208
|
-
} else {
|
|
1209
|
-
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
1210
|
-
indexByKey.set(key, merged.length - 1);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
return merged;
|
|
795
|
+
},
|
|
796
|
+
customModel,
|
|
797
|
+
"replace",
|
|
798
|
+
);
|
|
799
|
+
});
|
|
1214
800
|
}
|
|
1215
801
|
|
|
1216
802
|
#loadCachedStandardProviderModels(): { models: Model<Api>[]; authoritativeFreshProviders: Set<string> } {
|
|
@@ -1236,8 +822,13 @@ export class ModelRegistry {
|
|
|
1236
822
|
? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
|
|
1237
823
|
: models;
|
|
1238
824
|
const withCompat = providerOverride?.compat
|
|
1239
|
-
? withTransport.map(model =>
|
|
1240
|
-
|
|
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));
|
|
1241
832
|
cachedModels.push(...this.#applyProviderModelOverrides(providerId, withCompat));
|
|
1242
833
|
}
|
|
1243
834
|
return { models: cachedModels, authoritativeFreshProviders };
|
|
@@ -1261,7 +852,10 @@ export class ModelRegistry {
|
|
|
1261
852
|
providerConfig.provider,
|
|
1262
853
|
this.#normalizeDiscoverableModels(
|
|
1263
854
|
providerConfig,
|
|
1264
|
-
this.#applyProviderCompat(
|
|
855
|
+
this.#applyProviderCompat(
|
|
856
|
+
providerConfig.compat,
|
|
857
|
+
cache.models.map(model => buildModel(model)),
|
|
858
|
+
),
|
|
1265
859
|
),
|
|
1266
860
|
);
|
|
1267
861
|
cachedModels.push(...models);
|
|
@@ -1277,9 +871,11 @@ export class ModelRegistry {
|
|
|
1277
871
|
return cachedModels;
|
|
1278
872
|
}
|
|
1279
873
|
|
|
1280
|
-
#applyProviderCompat(compat:
|
|
874
|
+
#applyProviderCompat(compat: ModelSpec<Api>["compat"] | undefined, models: Model<Api>[]): Model<Api>[] {
|
|
1281
875
|
if (!compat) return models;
|
|
1282
|
-
return models.map(model =>
|
|
876
|
+
return models.map(model =>
|
|
877
|
+
buildModel({ ...model, compat: mergeCompat(model.compatConfig, compat) } as ModelSpec<Api>),
|
|
878
|
+
);
|
|
1283
879
|
}
|
|
1284
880
|
|
|
1285
881
|
#normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
@@ -1289,7 +885,14 @@ export class ModelRegistry {
|
|
|
1289
885
|
|
|
1290
886
|
const contextLengthOverride = getOllamaContextLengthOverride();
|
|
1291
887
|
return models.map(model => {
|
|
1292
|
-
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;
|
|
1293
896
|
if (contextLengthOverride === undefined) {
|
|
1294
897
|
return normalized;
|
|
1295
898
|
}
|
|
@@ -1512,17 +1115,20 @@ export class ModelRegistry {
|
|
|
1512
1115
|
models: cached?.models.map(model => model.id) ?? [],
|
|
1513
1116
|
});
|
|
1514
1117
|
this.#lastDiscoveryWarnings.delete(providerConfig.provider);
|
|
1515
|
-
return cached
|
|
1118
|
+
return cached ? cached.models.map(model => buildModel(model)) : [];
|
|
1516
1119
|
}
|
|
1517
1120
|
}
|
|
1518
1121
|
|
|
1519
1122
|
const providerId = providerConfig.provider;
|
|
1520
1123
|
let discoveryError: string | undefined;
|
|
1521
|
-
const fetchDynamicModels = async (): Promise<readonly
|
|
1124
|
+
const fetchDynamicModels = async (): Promise<readonly ModelSpec<Api>[] | null> => {
|
|
1522
1125
|
try {
|
|
1523
|
-
const models =
|
|
1126
|
+
const models = this.#applyProviderModelOverrides(
|
|
1127
|
+
providerId,
|
|
1128
|
+
await discoverModelsByProviderType(providerConfig, this.#discoveryContext()),
|
|
1129
|
+
);
|
|
1524
1130
|
this.#lastDiscoveryWarnings.delete(providerId);
|
|
1525
|
-
return models;
|
|
1131
|
+
return models.map(toModelSpec);
|
|
1526
1132
|
} catch (error) {
|
|
1527
1133
|
discoveryError = error instanceof Error ? error.message : String(error);
|
|
1528
1134
|
return null;
|
|
@@ -1569,18 +1175,14 @@ export class ModelRegistry {
|
|
|
1569
1175
|
);
|
|
1570
1176
|
}
|
|
1571
1177
|
|
|
1572
|
-
#
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
return
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
return this.#discoverOpenAIModelsList(providerConfig);
|
|
1581
|
-
case "proxy":
|
|
1582
|
-
return this.#discoverProxyModels(providerConfig);
|
|
1583
|
-
}
|
|
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
|
+
};
|
|
1584
1186
|
}
|
|
1585
1187
|
|
|
1586
1188
|
#warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
|
|
@@ -1732,361 +1334,6 @@ export class ModelRegistry {
|
|
|
1732
1334
|
}
|
|
1733
1335
|
}
|
|
1734
1336
|
|
|
1735
|
-
async #discoverOllamaModelMetadata(
|
|
1736
|
-
endpoint: string,
|
|
1737
|
-
modelId: string,
|
|
1738
|
-
headers: Record<string, string> | undefined,
|
|
1739
|
-
): Promise<OllamaDiscoveredModelMetadata | null> {
|
|
1740
|
-
const showUrl = `${endpoint}/api/show`;
|
|
1741
|
-
try {
|
|
1742
|
-
const response = await this.#fetch(showUrl, {
|
|
1743
|
-
method: "POST",
|
|
1744
|
-
headers: { ...(headers ?? {}), "Content-Type": "application/json" },
|
|
1745
|
-
body: JSON.stringify({ model: modelId }),
|
|
1746
|
-
signal: AbortSignal.timeout(150),
|
|
1747
|
-
});
|
|
1748
|
-
if (!response.ok) {
|
|
1749
|
-
return null;
|
|
1750
|
-
}
|
|
1751
|
-
const payload = (await response.json()) as unknown;
|
|
1752
|
-
if (!isRecord(payload)) {
|
|
1753
|
-
return null;
|
|
1754
|
-
}
|
|
1755
|
-
const contextWindow = extractOllamaContextWindow(payload);
|
|
1756
|
-
const capabilities = payload.capabilities;
|
|
1757
|
-
if (Array.isArray(capabilities)) {
|
|
1758
|
-
const normalized = new Set(
|
|
1759
|
-
capabilities.flatMap(capability => (typeof capability === "string" ? [capability.toLowerCase()] : [])),
|
|
1760
|
-
);
|
|
1761
|
-
const supportsVision = normalized.has("vision") || normalized.has("image");
|
|
1762
|
-
return {
|
|
1763
|
-
reasoning: normalized.has("thinking"),
|
|
1764
|
-
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1765
|
-
contextWindow,
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
if (!isRecord(capabilities)) {
|
|
1769
|
-
return {
|
|
1770
|
-
reasoning: false,
|
|
1771
|
-
input: ["text"],
|
|
1772
|
-
contextWindow,
|
|
1773
|
-
};
|
|
1774
|
-
}
|
|
1775
|
-
const supportsVision = capabilities.vision === true || capabilities.image === true;
|
|
1776
|
-
return {
|
|
1777
|
-
reasoning: capabilities.thinking === true,
|
|
1778
|
-
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1779
|
-
contextWindow,
|
|
1780
|
-
};
|
|
1781
|
-
} catch {
|
|
1782
|
-
return null;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
async #discoverOllamaModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1787
|
-
const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
|
|
1788
|
-
const tagsUrl = `${endpoint}/api/tags`;
|
|
1789
|
-
const headers = { ...(providerConfig.headers ?? {}) };
|
|
1790
|
-
const response = await this.#fetch(tagsUrl, {
|
|
1791
|
-
headers,
|
|
1792
|
-
signal: AbortSignal.timeout(250),
|
|
1793
|
-
});
|
|
1794
|
-
if (!response.ok) {
|
|
1795
|
-
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
1796
|
-
}
|
|
1797
|
-
const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
1798
|
-
const entries = (payload.models ?? []).flatMap(item => {
|
|
1799
|
-
const id = item.model || item.name;
|
|
1800
|
-
return id ? [{ id, name: item.name || id }] : [];
|
|
1801
|
-
});
|
|
1802
|
-
const metadataById = new Map(
|
|
1803
|
-
await Promise.all(
|
|
1804
|
-
entries.map(
|
|
1805
|
-
async entry => [entry.id, await this.#discoverOllamaModelMetadata(endpoint, entry.id, headers)] as const,
|
|
1806
|
-
),
|
|
1807
|
-
),
|
|
1808
|
-
);
|
|
1809
|
-
const discovered = entries.map(entry => {
|
|
1810
|
-
const metadata = metadataById.get(entry.id);
|
|
1811
|
-
return enrichModelThinking({
|
|
1812
|
-
id: entry.id,
|
|
1813
|
-
name: entry.name,
|
|
1814
|
-
api: providerConfig.api,
|
|
1815
|
-
provider: providerConfig.provider,
|
|
1816
|
-
baseUrl: `${endpoint}/v1`,
|
|
1817
|
-
reasoning: metadata?.reasoning ?? false,
|
|
1818
|
-
input: metadata?.input ?? ["text"],
|
|
1819
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1820
|
-
contextWindow: metadata?.contextWindow ?? 128000,
|
|
1821
|
-
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
1822
|
-
headers: providerConfig.headers,
|
|
1823
|
-
});
|
|
1824
|
-
});
|
|
1825
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
async #discoverLlamaCppServerMetadata(
|
|
1829
|
-
baseUrl: string,
|
|
1830
|
-
headers: Record<string, string> | undefined,
|
|
1831
|
-
): Promise<LlamaCppDiscoveredServerMetadata | null> {
|
|
1832
|
-
const propsUrl = `${this.#toLlamaCppNativeBaseUrl(baseUrl)}/props`;
|
|
1833
|
-
try {
|
|
1834
|
-
const response = await this.#fetch(propsUrl, {
|
|
1835
|
-
headers,
|
|
1836
|
-
signal: AbortSignal.timeout(150),
|
|
1837
|
-
});
|
|
1838
|
-
if (!response.ok) {
|
|
1839
|
-
return null;
|
|
1840
|
-
}
|
|
1841
|
-
const payload = (await response.json()) as unknown;
|
|
1842
|
-
if (!isRecord(payload)) {
|
|
1843
|
-
return null;
|
|
1844
|
-
}
|
|
1845
|
-
return {
|
|
1846
|
-
contextWindow: extractLlamaCppContextWindow(payload),
|
|
1847
|
-
input: extractLlamaCppInputCapabilities(payload),
|
|
1848
|
-
};
|
|
1849
|
-
} catch {
|
|
1850
|
-
return null;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
async #discoverLlamaCppModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1855
|
-
const baseUrl = this.#normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
|
|
1856
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1857
|
-
|
|
1858
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1859
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1860
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1861
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
const [response, serverMetadata] = await Promise.all([
|
|
1865
|
-
this.#fetch(modelsUrl, {
|
|
1866
|
-
headers,
|
|
1867
|
-
signal: AbortSignal.timeout(250),
|
|
1868
|
-
}),
|
|
1869
|
-
this.#discoverLlamaCppServerMetadata(baseUrl, headers),
|
|
1870
|
-
]);
|
|
1871
|
-
if (!response.ok) {
|
|
1872
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1873
|
-
}
|
|
1874
|
-
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1875
|
-
const models = payload.data ?? [];
|
|
1876
|
-
const discovered: Model<Api>[] = [];
|
|
1877
|
-
for (const item of models) {
|
|
1878
|
-
const id = item.id;
|
|
1879
|
-
if (!id) continue;
|
|
1880
|
-
discovered.push(
|
|
1881
|
-
enrichModelThinking({
|
|
1882
|
-
id,
|
|
1883
|
-
name: id,
|
|
1884
|
-
api: providerConfig.api,
|
|
1885
|
-
provider: providerConfig.provider,
|
|
1886
|
-
baseUrl,
|
|
1887
|
-
reasoning: false,
|
|
1888
|
-
input: serverMetadata?.input ?? ["text"],
|
|
1889
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1890
|
-
contextWindow: serverMetadata?.contextWindow ?? 128000,
|
|
1891
|
-
maxTokens: Math.min(
|
|
1892
|
-
serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY,
|
|
1893
|
-
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
1894
|
-
),
|
|
1895
|
-
headers,
|
|
1896
|
-
compat: {
|
|
1897
|
-
supportsStore: false,
|
|
1898
|
-
supportsDeveloperRole: false,
|
|
1899
|
-
supportsReasoningEffort: false,
|
|
1900
|
-
},
|
|
1901
|
-
}),
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
1904
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
async #discoverOpenAIModelsList(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1908
|
-
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
1909
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1910
|
-
|
|
1911
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1912
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1913
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1914
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
const response = await this.#fetch(modelsUrl, {
|
|
1918
|
-
headers,
|
|
1919
|
-
signal: AbortSignal.timeout(10_000),
|
|
1920
|
-
});
|
|
1921
|
-
if (!response.ok) {
|
|
1922
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1923
|
-
}
|
|
1924
|
-
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
1925
|
-
const models = payload.data ?? [];
|
|
1926
|
-
const discovered: Model<Api>[] = [];
|
|
1927
|
-
for (const item of models) {
|
|
1928
|
-
const id = item.id;
|
|
1929
|
-
if (!id) continue;
|
|
1930
|
-
discovered.push(
|
|
1931
|
-
enrichModelThinking({
|
|
1932
|
-
id,
|
|
1933
|
-
name: id,
|
|
1934
|
-
api: providerConfig.api,
|
|
1935
|
-
provider: providerConfig.provider,
|
|
1936
|
-
baseUrl,
|
|
1937
|
-
reasoning: false,
|
|
1938
|
-
input: ["text"],
|
|
1939
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1940
|
-
contextWindow: 128000,
|
|
1941
|
-
maxTokens: discoveryDefaultMaxTokens(providerConfig.api),
|
|
1942
|
-
headers,
|
|
1943
|
-
compat: {
|
|
1944
|
-
supportsStore: false,
|
|
1945
|
-
supportsDeveloperRole: false,
|
|
1946
|
-
supportsReasoningEffort: false,
|
|
1947
|
-
},
|
|
1948
|
-
}),
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1951
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
/**
|
|
1955
|
-
* Discover models from an Anthropic+OpenAI-compatible reseller proxy that
|
|
1956
|
-
* exposes both `/v1/messages` and `/v1/chat/completions`, advertising each
|
|
1957
|
-
* model's wire capabilities through `supported_endpoint_types` on
|
|
1958
|
-
* `GET /v1/models` (new-api / one-api-style proxies).
|
|
1959
|
-
*
|
|
1960
|
-
* Routing per model:
|
|
1961
|
-
* supported_endpoint_types: ["anthropic", ...] -> api: "anthropic-messages"
|
|
1962
|
-
* supported_endpoint_types: ["openai"] -> api: "openai-completions"
|
|
1963
|
-
* missing / neither -> provider-level api fallback
|
|
1964
|
-
*
|
|
1965
|
-
* Anthropic models share the same baseUrl; the Anthropic SDK strips a
|
|
1966
|
-
* trailing `/v1` itself before appending `/v1/messages`, so the discovery
|
|
1967
|
-
* URL (which ends in `/v1`) round-trips correctly.
|
|
1968
|
-
*/
|
|
1969
|
-
async #discoverProxyModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
1970
|
-
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
1971
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
1972
|
-
|
|
1973
|
-
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
1974
|
-
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
1975
|
-
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
1976
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
const response = await this.#fetch(modelsUrl, {
|
|
1980
|
-
headers,
|
|
1981
|
-
signal: AbortSignal.timeout(10_000),
|
|
1982
|
-
});
|
|
1983
|
-
if (!response.ok) {
|
|
1984
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
1985
|
-
}
|
|
1986
|
-
const payload = (await response.json()) as {
|
|
1987
|
-
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>;
|
|
1988
|
-
};
|
|
1989
|
-
const items = payload.data ?? [];
|
|
1990
|
-
const discovered: Model<Api>[] = [];
|
|
1991
|
-
for (const item of items) {
|
|
1992
|
-
const id = item.id;
|
|
1993
|
-
if (!id) continue;
|
|
1994
|
-
const endpoints = item.supported_endpoint_types ?? [];
|
|
1995
|
-
const api: Api | undefined = endpoints.includes("anthropic")
|
|
1996
|
-
? "anthropic-messages"
|
|
1997
|
-
: endpoints.includes("openai")
|
|
1998
|
-
? "openai-completions"
|
|
1999
|
-
: providerConfig.api;
|
|
2000
|
-
if (!api) continue;
|
|
2001
|
-
const isAnthropic = api === "anthropic-messages";
|
|
2002
|
-
const reference = resolveCustomModelReference(id);
|
|
2003
|
-
const discoveryName = typeof item.name === "string" ? item.name.trim() : "";
|
|
2004
|
-
const displayName =
|
|
2005
|
-
reference?.name ??
|
|
2006
|
-
(discoveryName && discoveryName !== id ? discoveryName : undefined) ??
|
|
2007
|
-
stripBracketedModelIdAffixes(id) ??
|
|
2008
|
-
id;
|
|
2009
|
-
discovered.push(
|
|
2010
|
-
enrichModelThinking({
|
|
2011
|
-
id,
|
|
2012
|
-
name: displayName,
|
|
2013
|
-
api,
|
|
2014
|
-
provider: providerConfig.provider,
|
|
2015
|
-
baseUrl,
|
|
2016
|
-
reasoning: reference?.reasoning ?? false,
|
|
2017
|
-
thinking: reference?.thinking,
|
|
2018
|
-
input: reference?.input ?? ["text"],
|
|
2019
|
-
// Proxy pricing is provider-specific and usually does not match
|
|
2020
|
-
// upstream bundled catalogs, so keep costs local-unknown even when
|
|
2021
|
-
// we successfully recover the upstream model identity.
|
|
2022
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2023
|
-
contextWindow: reference?.contextWindow ?? 128000,
|
|
2024
|
-
maxTokens: reference?.maxTokens ?? discoveryDefaultMaxTokens(api),
|
|
2025
|
-
headers,
|
|
2026
|
-
// OpenAI-compat fields are no-ops on anthropic models; the
|
|
2027
|
-
// Anthropic SDK ignores them. Provider-level disableStrictTools
|
|
2028
|
-
// flows in via #applyProviderCompat for the third-party-Anthropic
|
|
2029
|
-
// path. Cross-wire bundled compat is intentionally not copied:
|
|
2030
|
-
// request-shaping fields are provider-wire specific.
|
|
2031
|
-
compat: isAnthropic
|
|
2032
|
-
? undefined
|
|
2033
|
-
: {
|
|
2034
|
-
supportsStore: false,
|
|
2035
|
-
supportsDeveloperRole: false,
|
|
2036
|
-
supportsReasoningEffort: false,
|
|
2037
|
-
},
|
|
2038
|
-
}),
|
|
2039
|
-
);
|
|
2040
|
-
}
|
|
2041
|
-
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
#normalizeLlamaCppBaseUrl(baseUrl?: string): string {
|
|
2045
|
-
const defaultBaseUrl = "http://127.0.0.1:8080";
|
|
2046
|
-
const raw = baseUrl || defaultBaseUrl;
|
|
2047
|
-
try {
|
|
2048
|
-
const parsed = new URL(raw);
|
|
2049
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2050
|
-
return `${parsed.protocol}//${parsed.host}${trimmedPath}`;
|
|
2051
|
-
} catch {
|
|
2052
|
-
return raw;
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
#toLlamaCppNativeBaseUrl(baseUrl: string): string {
|
|
2057
|
-
try {
|
|
2058
|
-
const parsed = new URL(baseUrl);
|
|
2059
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2060
|
-
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath.slice(0, -3) || "/" : trimmedPath || "/";
|
|
2061
|
-
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2062
|
-
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2063
|
-
} catch {
|
|
2064
|
-
return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
#normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
|
|
2069
|
-
const defaultBaseUrl = "http://127.0.0.1:1234/v1";
|
|
2070
|
-
const raw = baseUrl || defaultBaseUrl;
|
|
2071
|
-
try {
|
|
2072
|
-
const parsed = new URL(raw);
|
|
2073
|
-
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2074
|
-
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
|
|
2075
|
-
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2076
|
-
} catch {
|
|
2077
|
-
return raw;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
#normalizeOllamaBaseUrl(baseUrl?: string): string {
|
|
2081
|
-
const raw = baseUrl || DEFAULT_OLLAMA_BASE_URL;
|
|
2082
|
-
try {
|
|
2083
|
-
const parsed = new URL(raw);
|
|
2084
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
2085
|
-
} catch {
|
|
2086
|
-
return DEFAULT_OLLAMA_BASE_URL;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
1337
|
#applyProviderModelOverrides(provider: string, models: Model<Api>[]): Model<Api>[] {
|
|
2091
1338
|
const overrides = this.#modelOverrides.get(provider);
|
|
2092
1339
|
if (!overrides || overrides.size === 0) return models;
|
|
@@ -2164,7 +1411,11 @@ export class ModelRegistry {
|
|
|
2164
1411
|
this.#rebuildPending = true;
|
|
2165
1412
|
return;
|
|
2166
1413
|
}
|
|
2167
|
-
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1414
|
+
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1415
|
+
this.#models,
|
|
1416
|
+
getBundledCanonicalReferenceData(),
|
|
1417
|
+
this.#equivalenceConfig,
|
|
1418
|
+
);
|
|
2168
1419
|
this.#rebuildPending = false;
|
|
2169
1420
|
}
|
|
2170
1421
|
|
|
@@ -2178,7 +1429,11 @@ export class ModelRegistry {
|
|
|
2178
1429
|
}
|
|
2179
1430
|
if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
|
|
2180
1431
|
this.#rebuildPending = false;
|
|
2181
|
-
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1432
|
+
this.#canonicalIndex = buildCanonicalModelIndex(
|
|
1433
|
+
this.#models,
|
|
1434
|
+
getBundledCanonicalReferenceData(),
|
|
1435
|
+
this.#equivalenceConfig,
|
|
1436
|
+
);
|
|
2182
1437
|
}
|
|
2183
1438
|
}
|
|
2184
1439
|
|
|
@@ -2278,53 +1533,11 @@ export class ModelRegistry {
|
|
|
2278
1533
|
});
|
|
2279
1534
|
}
|
|
2280
1535
|
|
|
2281
|
-
#
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
}
|
|
2286
|
-
return modelOrder;
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
#providerRank(): Map<string, number> {
|
|
2290
|
-
return buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings());
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
#resolveCanonicalVariant(
|
|
2294
|
-
variants: readonly CanonicalModelVariant[],
|
|
2295
|
-
modelOrder: ReadonlyMap<string, number>,
|
|
2296
|
-
providerRank: ReadonlyMap<string, number>,
|
|
2297
|
-
): CanonicalModelVariant | undefined {
|
|
2298
|
-
if (variants.length === 0) {
|
|
2299
|
-
return undefined;
|
|
2300
|
-
}
|
|
2301
|
-
const sourceRank: Record<CanonicalModelVariant["source"], number> = {
|
|
2302
|
-
override: 1,
|
|
2303
|
-
bundled: 1,
|
|
2304
|
-
heuristic: 2,
|
|
2305
|
-
fallback: 3,
|
|
1536
|
+
#variantPreferences(candidates: readonly Model<Api>[]): CanonicalVariantPreferences {
|
|
1537
|
+
return {
|
|
1538
|
+
modelOrder: buildCanonicalModelOrder(candidates),
|
|
1539
|
+
providerRank: buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings()),
|
|
2306
1540
|
};
|
|
2307
|
-
return [...variants].sort((left, right) => {
|
|
2308
|
-
const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2309
|
-
const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
|
|
2310
|
-
if (leftProviderRank !== rightProviderRank) {
|
|
2311
|
-
return leftProviderRank - rightProviderRank;
|
|
2312
|
-
}
|
|
2313
|
-
const leftExact = left.model.id === left.canonicalId ? 0 : 1;
|
|
2314
|
-
const rightExact = right.model.id === right.canonicalId ? 0 : 1;
|
|
2315
|
-
if (leftExact !== rightExact) {
|
|
2316
|
-
return leftExact - rightExact;
|
|
2317
|
-
}
|
|
2318
|
-
if (sourceRank[left.source] !== sourceRank[right.source]) {
|
|
2319
|
-
return sourceRank[left.source] - sourceRank[right.source];
|
|
2320
|
-
}
|
|
2321
|
-
if (left.model.id.length !== right.model.id.length) {
|
|
2322
|
-
return left.model.id.length - right.model.id.length;
|
|
2323
|
-
}
|
|
2324
|
-
const leftOrder = modelOrder.get(left.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
2325
|
-
const rightOrder = modelOrder.get(right.selector) ?? Number.MAX_SAFE_INTEGER;
|
|
2326
|
-
return leftOrder - rightOrder;
|
|
2327
|
-
})[0];
|
|
2328
1541
|
}
|
|
2329
1542
|
|
|
2330
1543
|
getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
|
|
@@ -2354,15 +1567,14 @@ export class ModelRegistry {
|
|
|
2354
1567
|
getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[] {
|
|
2355
1568
|
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
2356
1569
|
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
2357
|
-
const
|
|
2358
|
-
const providerRank = this.#providerRank();
|
|
1570
|
+
const preferences = this.#variantPreferences(candidates);
|
|
2359
1571
|
const selections: CanonicalModelSelection[] = [];
|
|
2360
1572
|
for (const record of this.#canonicalIndex.records) {
|
|
2361
1573
|
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
2362
1574
|
if (variants.length === 0) {
|
|
2363
1575
|
continue;
|
|
2364
1576
|
}
|
|
2365
|
-
const resolved =
|
|
1577
|
+
const resolved = resolveCanonicalVariant(variants, preferences);
|
|
2366
1578
|
if (!resolved) {
|
|
2367
1579
|
continue;
|
|
2368
1580
|
}
|
|
@@ -2389,7 +1601,7 @@ export class ModelRegistry {
|
|
|
2389
1601
|
return undefined;
|
|
2390
1602
|
}
|
|
2391
1603
|
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
2392
|
-
return
|
|
1604
|
+
return resolveCanonicalVariant(variants, this.#variantPreferences(candidates))?.model;
|
|
2393
1605
|
}
|
|
2394
1606
|
|
|
2395
1607
|
getCanonicalId(model: Model<Api>): string | undefined {
|
|
@@ -2698,7 +1910,7 @@ export class ModelRegistry {
|
|
|
2698
1910
|
);
|
|
2699
1911
|
if (overlay) results.push(finalizeCustomModel(overlay, { useDefaults: true }));
|
|
2700
1912
|
}
|
|
2701
|
-
return results;
|
|
1913
|
+
return results.map(toModelSpec);
|
|
2702
1914
|
},
|
|
2703
1915
|
};
|
|
2704
1916
|
this.#runtimeModelManagers.set(providerName, { options: managerOptions, sourceId: sourceId ?? "" });
|
|
@@ -2772,7 +1984,7 @@ export interface ProviderConfigInput {
|
|
|
2772
1984
|
api?: Api;
|
|
2773
1985
|
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
2774
1986
|
headers?: Record<string, string>;
|
|
2775
|
-
compat?:
|
|
1987
|
+
compat?: ModelSpec<Api>["compat"];
|
|
2776
1988
|
authHeader?: boolean;
|
|
2777
1989
|
/** Streaming transport override — see {@link Model.transport}. */
|
|
2778
1990
|
transport?: Model<Api>["transport"];
|
|
@@ -2804,7 +2016,7 @@ export interface ProviderConfigInput {
|
|
|
2804
2016
|
contextWindow: number;
|
|
2805
2017
|
maxTokens: number;
|
|
2806
2018
|
headers?: Record<string, string>;
|
|
2807
|
-
compat?:
|
|
2019
|
+
compat?: ModelSpec<Api>["compat"];
|
|
2808
2020
|
contextPromotionTarget?: string;
|
|
2809
2021
|
premiumMultiplier?: number;
|
|
2810
2022
|
}>;
|