@vandeepunk/pi-coding-agent 0.0.6 → 1.0.0
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 +814 -16
- package/README.md +66 -27
- package/dist/bun/cli.d.ts +3 -0
- package/dist/bun/cli.d.ts.map +1 -0
- package/dist/bun/cli.js +7 -0
- package/dist/bun/cli.js.map +1 -0
- package/dist/bun/register-bedrock.d.ts +2 -0
- package/dist/bun/register-bedrock.d.ts.map +1 -0
- package/dist/bun/register-bedrock.js +4 -0
- package/dist/bun/register-bedrock.js.map +1 -0
- package/dist/cli/args.d.ts +9 -4
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +61 -22
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +4 -0
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/initial-message.d.ts +18 -0
- package/dist/cli/initial-message.d.ts.map +1 -0
- package/dist/cli/initial-message.js +22 -0
- package/dist/cli/initial-message.js.map +1 -0
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +2 -1
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +83 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -0
- package/dist/core/agent-session-runtime.js +236 -0
- package/dist/core/agent-session-runtime.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +86 -0
- package/dist/core/agent-session-services.d.ts.map +1 -0
- package/dist/core/agent-session-services.js +116 -0
- package/dist/core/agent-session-services.js.map +1 -0
- package/dist/core/agent-session.d.ts +63 -49
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +599 -370
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +38 -8
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +220 -96
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts +6 -7
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +27 -114
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +2 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +3 -2
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +31 -25
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +3 -0
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +16 -1
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +7 -3
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/index.d.ts +7 -4
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +10 -8
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/template.css +43 -13
- package/dist/core/export-html/template.html +1 -0
- package/dist/core/export-html/template.js +118 -14
- package/dist/core/export-html/tool-renderer.d.ts +9 -4
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +48 -10
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/index.d.ts +5 -4
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +2 -2
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +49 -13
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +13 -11
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +139 -64
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +174 -34
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +10 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts +4 -11
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +6 -86
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +18 -2
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +220 -40
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/keybindings.d.ts +283 -50
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +221 -134
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-registry.d.ts +33 -3
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +165 -97
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +35 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +205 -32
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts +6 -0
- package/dist/core/output-guard.d.ts.map +1 -0
- package/dist/core/output-guard.js +59 -0
- package/dist/core/output-guard.js.map +1 -0
- package/dist/core/package-manager.d.ts +43 -2
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +541 -102
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +5 -4
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +35 -37
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +6 -0
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +75 -8
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/resource-loader.d.ts +6 -5
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +166 -119
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +11 -8
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +31 -29
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-cwd.d.ts +19 -0
- package/dist/core/session-cwd.d.ts.map +1 -0
- package/dist/core/session-cwd.js +38 -0
- package/dist/core/session-cwd.js.map +1 -0
- package/dist/core/session-manager.d.ts +11 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +39 -25
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +60 -10
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +291 -140
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +5 -3
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +54 -9
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts +2 -3
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +3 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/source-info.d.ts +18 -0
- package/dist/core/source-info.d.ts.map +1 -0
- package/dist/core/source-info.js +19 -0
- package/dist/core/source-info.js.map +1 -0
- package/dist/core/system-prompt.d.ts +4 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +31 -52
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/timings.d.ts +1 -0
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +6 -0
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/bash.d.ts +24 -6
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +225 -115
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +23 -1
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +151 -57
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +20 -6
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +111 -61
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts +6 -0
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/core/tools/file-mutation-queue.js +37 -0
- package/dist/core/tools/file-mutation-queue.js.map +1 -0
- package/dist/core/tools/find.d.ts +11 -4
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +82 -30
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +15 -4
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +83 -29
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +63 -21
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +51 -26
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +9 -3
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +67 -13
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +10 -3
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +110 -51
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +21 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -0
- package/dist/core/tools/render-utils.js +49 -0
- package/dist/core/tools/render-utils.js.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.js +32 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
- package/dist/core/tools/write.d.ts +9 -3
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +168 -30
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +354 -379
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +31 -11
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +14 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +0 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +22 -9
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +1 -1
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +2 -2
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +2 -2
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +14 -14
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +6 -6
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +18 -9
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +5 -5
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +8 -8
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +21 -40
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +3 -36
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +5 -44
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +7 -7
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +13 -9
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +7 -7
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +4 -4
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +33 -36
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +25 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.js +5 -1
- package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/theme-selector.js +5 -1
- package/dist/modes/interactive/components/theme-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +5 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +17 -29
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +139 -501
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +25 -4
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +184 -34
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +6 -6
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +12 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +25 -17
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +669 -385
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +3 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +83 -48
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +2 -2
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +90 -79
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/jsonl.d.ts +17 -0
- package/dist/modes/rpc/jsonl.d.ts.map +1 -0
- package/dist/modes/rpc/jsonl.js +49 -0
- package/dist/modes/rpc/jsonl.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +8 -11
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -2
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +130 -87
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -4
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +4 -0
- package/dist/package-manager-cli.d.ts.map +1 -0
- package/dist/package-manager-cli.js +234 -0
- package/dist/package-manager-cli.js.map +1 -0
- package/dist/utils/child-process.d.ts +11 -0
- package/dist/utils/child-process.d.ts.map +1 -0
- package/dist/utils/child-process.js +78 -0
- package/dist/utils/child-process.js.map +1 -0
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +94 -11
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +27 -16
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/exif-orientation.d.ts +5 -0
- package/dist/utils/exif-orientation.d.ts.map +1 -0
- package/dist/utils/exif-orientation.js +158 -0
- package/dist/utils/exif-orientation.js.map +1 -0
- package/dist/utils/git.d.ts +5 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +14 -3
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +5 -1
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts +5 -5
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +51 -95
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/paths.d.ts +7 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +19 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +67 -22
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/compaction.md +6 -2
- package/docs/custom-provider.md +57 -9
- package/docs/development.md +3 -1
- package/docs/extensions.md +437 -67
- package/docs/json.md +5 -2
- package/docs/keybindings.md +108 -107
- package/docs/models.md +50 -2
- package/docs/packages.md +17 -10
- package/docs/prompt-templates.md +6 -6
- package/docs/providers.md +10 -1
- package/docs/rpc.md +78 -18
- package/docs/sdk.md +261 -96
- package/docs/settings.md +28 -3
- package/docs/skills.md +9 -4
- package/docs/terminal-setup.md +39 -3
- package/docs/tmux.md +61 -0
- package/docs/tree.md +15 -3
- package/docs/tui.md +2 -2
- package/examples/extensions/README.md +3 -0
- package/examples/extensions/antigravity-image-gen.ts +12 -7
- package/examples/extensions/built-in-tool-renderer.ts +246 -0
- package/examples/extensions/commands.ts +3 -3
- package/examples/extensions/custom-compaction.ts +17 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/doom-overlay/doom/build.sh +2 -2
- package/examples/extensions/dynamic-tools.ts +74 -0
- package/examples/extensions/handoff.ts +5 -2
- package/examples/extensions/hello.ts +18 -17
- package/examples/extensions/hidden-thinking-label.ts +53 -0
- package/examples/extensions/minimal-mode.ts +14 -14
- package/examples/extensions/overlay-qa-tests.ts +468 -1
- package/examples/extensions/preset.ts +2 -3
- package/examples/extensions/provider-payload.ts +14 -0
- package/examples/extensions/qna.ts +5 -2
- package/examples/extensions/question.ts +2 -2
- package/examples/extensions/questionnaire.ts +2 -2
- package/examples/extensions/rpc-demo.ts +3 -9
- package/examples/extensions/sandbox/index.ts +6 -3
- package/examples/extensions/status-line.ts +0 -8
- package/examples/extensions/subagent/README.md +4 -4
- package/examples/extensions/subagent/agents.ts +2 -3
- package/examples/extensions/subagent/index.ts +30 -8
- package/examples/extensions/summarize.ts +15 -4
- package/examples/extensions/todo.ts +2 -4
- package/examples/extensions/tool-override.ts +10 -9
- package/examples/extensions/tools.ts +0 -5
- package/examples/extensions/trigger-compact.ts +11 -1
- package/examples/extensions/truncated-tool.ts +8 -5
- package/examples/extensions/widget-placement.ts +4 -12
- package/examples/extensions/with-deps/index.ts +1 -5
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/02-custom-model.ts +2 -2
- package/examples/sdk/04-skills.ts +8 -2
- package/examples/sdk/08-prompt-templates.ts +4 -3
- package/examples/sdk/09-api-keys-and-oauth.ts +5 -5
- package/examples/sdk/10-settings.ts +13 -0
- package/examples/sdk/12-full-control.ts +2 -3
- package/examples/sdk/13-session-runtime.ts +67 -0
- package/examples/sdk/README.md +10 -7
- package/package.json +98 -94
- /package/examples/extensions/subagent/{commands → prompts}/implement-and-review.md +0 -0
- /package/examples/extensions/subagent/{commands → prompts}/implement.md +0 -0
- /package/examples/extensions/subagent/{commands → prompts}/scout-and-plan.md +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/core/model-registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model registry - manages built-in and custom models, provides API key resolution.\n */\n\nimport {\n\ttype Api,\n\ttype AssistantMessageEventStream,\n\ttype Context,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OAuthProviderInterface,\n\ttype OpenAICompletionsCompat,\n\ttype OpenAIResponsesCompat,\n\tregisterApiProvider,\n\tregisterOAuthProvider,\n\ttype SimpleStreamOptions,\n} from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport type { AuthStorage } from \"./auth-storage.js\";\nimport { clearConfigValueCache, resolveConfigValue, resolveHeaders } from \"./resolve-config-value.js\";\n\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for OpenRouter routing preferences\nconst OpenRouterRoutingSchema = Type.Object({\n\tonly: Type.Optional(Type.Array(Type.String())),\n\torder: Type.Optional(Type.Array(Type.String())),\n});\n\n// Schema for Vercel AI Gateway routing preferences\nconst VercelGatewayRoutingSchema = Type.Object({\n\tonly: Type.Optional(Type.Array(Type.String())),\n\torder: Type.Optional(Type.Array(Type.String())),\n});\n\n// Schema for OpenAI compatibility settings\nconst OpenAICompletionsCompatSchema = Type.Object({\n\tsupportsStore: Type.Optional(Type.Boolean()),\n\tsupportsDeveloperRole: Type.Optional(Type.Boolean()),\n\tsupportsReasoningEffort: Type.Optional(Type.Boolean()),\n\tsupportsUsageInStreaming: Type.Optional(Type.Boolean()),\n\tmaxTokensField: Type.Optional(Type.Union([Type.Literal(\"max_completion_tokens\"), Type.Literal(\"max_tokens\")])),\n\trequiresToolResultName: Type.Optional(Type.Boolean()),\n\trequiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),\n\trequiresThinkingAsText: Type.Optional(Type.Boolean()),\n\trequiresMistralToolIds: Type.Optional(Type.Boolean()),\n\tthinkingFormat: Type.Optional(Type.Union([Type.Literal(\"openai\"), Type.Literal(\"zai\"), Type.Literal(\"qwen\")])),\n\topenRouterRouting: Type.Optional(OpenRouterRoutingSchema),\n\tvercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),\n});\n\nconst OpenAIResponsesCompatSchema = Type.Object({\n\t// Reserved for future use\n});\n\nconst OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);\n\n// Schema for custom model definition\n// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.Optional(Type.String({ minLength: 1 })),\n\tapi: Type.Optional(Type.String({ minLength: 1 })),\n\treasoning: Type.Optional(Type.Boolean()),\n\tinput: Type.Optional(Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")]))),\n\tcost: Type.Optional(\n\t\tType.Object({\n\t\t\tinput: Type.Number(),\n\t\t\toutput: Type.Number(),\n\t\t\tcacheRead: Type.Number(),\n\t\t\tcacheWrite: Type.Number(),\n\t\t}),\n\t),\n\tcontextWindow: Type.Optional(Type.Number()),\n\tmaxTokens: Type.Optional(Type.Number()),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\n// Schema for per-model overrides (all fields optional, merged with built-in model)\nconst ModelOverrideSchema = Type.Object({\n\tname: Type.Optional(Type.String({ minLength: 1 })),\n\treasoning: Type.Optional(Type.Boolean()),\n\tinput: Type.Optional(Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")]))),\n\tcost: Type.Optional(\n\t\tType.Object({\n\t\t\tinput: Type.Optional(Type.Number()),\n\t\t\toutput: Type.Optional(Type.Number()),\n\t\t\tcacheRead: Type.Optional(Type.Number()),\n\t\t\tcacheWrite: Type.Optional(Type.Number()),\n\t\t}),\n\t),\n\tcontextWindow: Type.Optional(Type.Number()),\n\tmaxTokens: Type.Optional(Type.Number()),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\ntype ModelOverride = Static<typeof ModelOverrideSchema>;\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.Optional(Type.String({ minLength: 1 })),\n\tapiKey: Type.Optional(Type.String({ minLength: 1 })),\n\tapi: Type.Optional(Type.String({ minLength: 1 })),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: Type.Optional(Type.Array(ModelDefinitionSchema)),\n\tmodelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\n\n/** Provider override config (baseUrl, headers, apiKey) without custom models */\ninterface ProviderOverride {\n\tbaseUrl?: string;\n\theaders?: Record<string, string>;\n\tapiKey?: string;\n}\n\n/** Result of loading custom models from models.json */\ninterface CustomModelsResult {\n\tmodels: Model<Api>[];\n\t/** Providers with baseUrl/headers/apiKey overrides for built-in models */\n\toverrides: Map<string, ProviderOverride>;\n\t/** Per-model overrides: provider -> modelId -> override */\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>;\n\terror: string | undefined;\n}\n\nfunction emptyCustomModelsResult(error?: string): CustomModelsResult {\n\treturn { models: [], overrides: new Map(), modelOverrides: new Map(), error };\n}\n\nfunction mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as OpenAICompletionsCompat | OpenAIResponsesCompat | undefined;\n\tconst override = overrideCompat as OpenAICompletionsCompat | OpenAIResponsesCompat;\n\tconst merged = { ...base, ...override } as OpenAICompletionsCompat | OpenAIResponsesCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\n/**\n * Deep merge a model override into a model.\n * Handles nested objects (cost, compat) by merging rather than replacing.\n */\nfunction applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\t// Simple field overrides\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\t// Merge cost (partial override)\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\t// Merge headers\n\tif (override.headers) {\n\t\tconst resolvedHeaders = resolveHeaders(override.headers);\n\t\tresult.headers = resolvedHeaders ? { ...model.headers, ...resolvedHeaders } : model.headers;\n\t}\n\n\t// Deep merge compat\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\n\treturn result;\n}\n\n/** Clear the config value command cache. Exported for testing. */\nexport const clearApiKeyCache = clearConfigValueCache;\n\n/**\n * Model registry - loads and manages models, resolves API keys via AuthStorage.\n */\nexport class ModelRegistry {\n\tprivate models: Model<Api>[] = [];\n\tprivate customProviderApiKeys: Map<string, string> = new Map();\n\tprivate registeredProviders: Map<string, ProviderConfigInput> = new Map();\n\tprivate loadError: string | undefined = undefined;\n\n\tconstructor(\n\t\treadonly authStorage: AuthStorage,\n\t\tprivate modelsJsonPath: string | undefined = join(getAgentDir(), \"models.json\"),\n\t) {\n\t\t// Set up fallback resolver for custom provider API keys\n\t\tthis.authStorage.setFallbackResolver((provider) => {\n\t\t\tconst keyConfig = this.customProviderApiKeys.get(provider);\n\t\t\tif (keyConfig) {\n\t\t\t\treturn resolveConfigValue(keyConfig);\n\t\t\t}\n\t\t\treturn undefined;\n\t\t});\n\n\t\t// Load models\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Reload models from disk (built-in + custom from models.json).\n\t */\n\trefresh(): void {\n\t\tthis.customProviderApiKeys.clear();\n\t\tthis.loadError = undefined;\n\t\tthis.loadModels();\n\n\t\tfor (const [providerName, config] of this.registeredProviders.entries()) {\n\t\t\tthis.applyProviderConfig(providerName, config);\n\t\t}\n\t}\n\n\t/**\n\t * Get any error from loading models.json (undefined if no error).\n\t */\n\tgetError(): string | undefined {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load custom models and overrides from models.json\n\t\tconst {\n\t\t\tmodels: customModels,\n\t\t\toverrides,\n\t\t\tmodelOverrides,\n\t\t\terror,\n\t\t} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();\n\n\t\tif (error) {\n\t\t\tthis.loadError = error;\n\t\t\t// Keep built-in models even if custom models failed to load\n\t\t}\n\n\t\tconst builtInModels = this.loadBuiltInModels(overrides, modelOverrides);\n\t\tlet combined = this.mergeCustomModels(builtInModels, customModels);\n\n\t\t// Let OAuth providers modify their models (e.g., update baseUrl)\n\t\tfor (const oauthProvider of this.authStorage.getOAuthProviders()) {\n\t\t\tconst cred = this.authStorage.get(oauthProvider.id);\n\t\t\tif (cred?.type === \"oauth\" && oauthProvider.modifyModels) {\n\t\t\t\tcombined = oauthProvider.modifyModels(combined, cred);\n\t\t\t}\n\t\t}\n\n\t\tthis.models = combined;\n\t}\n\n\t/** Load built-in models and apply provider/model overrides */\n\tprivate loadBuiltInModels(\n\t\toverrides: Map<string, ProviderOverride>,\n\t\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n\t): Model<Api>[] {\n\t\treturn getProviders().flatMap((provider) => {\n\t\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\t\tconst providerOverride = overrides.get(provider);\n\t\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\t\treturn models.map((m) => {\n\t\t\t\tlet model = m;\n\n\t\t\t\t// Apply provider-level baseUrl/headers override\n\t\t\t\tif (providerOverride) {\n\t\t\t\t\tconst resolvedHeaders = resolveHeaders(providerOverride.headers);\n\t\t\t\t\tmodel = {\n\t\t\t\t\t\t...model,\n\t\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\t\theaders: resolvedHeaders ? { ...model.headers, ...resolvedHeaders } : model.headers,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Apply per-model override\n\t\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\t\tif (modelOverride) {\n\t\t\t\t\tmodel = applyModelOverride(model, modelOverride);\n\t\t\t\t}\n\n\t\t\t\treturn model;\n\t\t\t});\n\t\t});\n\t}\n\n\t/** Merge custom models into built-in list by provider+id (custom wins on conflicts). */\n\tprivate mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\t\tconst merged = [...builtInModels];\n\t\tfor (const customModel of customModels) {\n\t\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\t\tif (existingIndex >= 0) {\n\t\t\t\tmerged[existingIndex] = customModel;\n\t\t\t} else {\n\t\t\t\tmerged.push(customModel);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tprivate loadCustomModels(modelsJsonPath: string): CustomModelsResult {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn emptyCustomModelsResult();\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(modelsJsonPath, \"utf-8\");\n\t\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t\t// Validate schema\n\t\t\tconst ajv = new Ajv();\n\t\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\t\tif (!validate(config)) {\n\t\t\t\tconst errors =\n\t\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\t\"Unknown schema error\";\n\t\t\t\treturn emptyCustomModelsResult(`Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\n\t\t\tconst overrides = new Map<string, ProviderOverride>();\n\t\t\tconst modelOverrides = new Map<string, Map<string, ModelOverride>>();\n\n\t\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\t\t// Apply provider-level baseUrl/headers/apiKey override to built-in models when configured.\n\t\t\t\tif (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey) {\n\t\t\t\t\toverrides.set(providerName, {\n\t\t\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\t\t\theaders: providerConfig.headers,\n\t\t\t\t\t\tapiKey: providerConfig.apiKey,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// Store API key for fallback resolver.\n\t\t\t\tif (providerConfig.apiKey) {\n\t\t\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\t\t\t\t}\n\n\t\t\t\tif (providerConfig.modelOverrides) {\n\t\t\t\t\tmodelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { models: this.parseModels(config), overrides, modelOverrides, error: undefined };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\t\t\treturn emptyCustomModelsResult(\n\t\t\t\t`Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate validateConfig(config: ModelsConfig): void {\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst hasProviderApi = !!providerConfig.api;\n\t\t\tconst models = providerConfig.models ?? [];\n\t\t\tconst hasModelOverrides =\n\t\t\t\tproviderConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;\n\n\t\t\tif (models.length === 0) {\n\t\t\t\t// Override-only config: needs baseUrl OR modelOverrides (or both)\n\t\t\t\tif (!providerConfig.baseUrl && !hasModelOverrides) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: must specify \"baseUrl\", \"modelOverrides\", or \"models\".`);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Custom models are merged into provider models and require endpoint + auth.\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t\tif (!providerConfig.apiKey) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const modelDef of models) {\n\t\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. Set at provider or model level.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\t\t// Validate contextWindow/maxTokens only if provided (they have defaults)\n\t\t\t\tif (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\t\tif (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate parseModels(config: ModelsConfig): Model<Api>[] {\n\t\tconst models: Model<Api>[] = [];\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst modelDefs = providerConfig.models ?? [];\n\t\t\tif (modelDefs.length === 0) continue; // Override-only, no custom models\n\n\t\t\t// Store API key config for fallback resolver\n\t\t\tif (providerConfig.apiKey) {\n\t\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\t\t\t}\n\n\t\t\tfor (const modelDef of modelDefs) {\n\t\t\t\tconst api = modelDef.api || providerConfig.api;\n\t\t\t\tif (!api) continue;\n\n\t\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\t\t// Resolve env vars and shell commands in header values\n\t\t\t\tconst providerHeaders = resolveHeaders(providerConfig.headers);\n\t\t\t\tconst modelHeaders = resolveHeaders(modelDef.headers);\n\t\t\t\tlet headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;\n\n\t\t\t\t// If authHeader is true, add Authorization header with resolved API key\n\t\t\t\tif (providerConfig.authHeader && providerConfig.apiKey) {\n\t\t\t\t\tconst resolvedKey = resolveConfigValue(providerConfig.apiKey);\n\t\t\t\t\tif (resolvedKey) {\n\t\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// baseUrl is validated to exist for providers with models\n\t\t\t\t// Apply defaults for optional fields\n\t\t\t\tconst defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\n\t\t\t\tmodels.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name ?? modelDef.id,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: providerConfig.baseUrl!,\n\t\t\t\t\treasoning: modelDef.reasoning ?? false,\n\t\t\t\t\tinput: (modelDef.input ?? [\"text\"]) as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost ?? defaultCost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow ?? 128000,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens ?? 16384,\n\t\t\t\t\theaders,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t}\n\n\t\treturn models;\n\t}\n\n\t/**\n\t * Get all models (built-in + custom).\n\t * If models.json had errors, returns only built-in models.\n\t */\n\tgetAll(): Model<Api>[] {\n\t\treturn this.models;\n\t}\n\n\t/**\n\t * Get only models that have auth configured.\n\t * This is a fast check that doesn't refresh OAuth tokens.\n\t */\n\tgetAvailable(): Model<Api>[] {\n\t\treturn this.models.filter((m) => this.authStorage.hasAuth(m.provider));\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | undefined {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId);\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\tasync getApiKey(model: Model<Api>): Promise<string | undefined> {\n\t\treturn this.authStorage.getApiKey(model.provider);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t */\n\tasync getApiKeyForProvider(provider: string): Promise<string | undefined> {\n\t\treturn this.authStorage.getApiKey(provider);\n\t}\n\n\t/**\n\t * Check if a model is using OAuth credentials (subscription).\n\t */\n\tisUsingOAuth(model: Model<Api>): boolean {\n\t\tconst cred = this.authStorage.get(model.provider);\n\t\treturn cred?.type === \"oauth\";\n\t}\n\n\t/**\n\t * Register a provider dynamically (from extensions).\n\t *\n\t * If provider has models: replaces all existing models for this provider.\n\t * If provider has only baseUrl/headers: overrides existing models' URLs.\n\t * If provider has oauth: registers OAuth provider for /login support.\n\t */\n\tregisterProvider(providerName: string, config: ProviderConfigInput): void {\n\t\tthis.registeredProviders.set(providerName, config);\n\t\tthis.applyProviderConfig(providerName, config);\n\t}\n\n\tprivate applyProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\t// Register OAuth provider if provided\n\t\tif (config.oauth) {\n\t\t\t// Ensure the OAuth provider ID matches the provider name\n\t\t\tconst oauthProvider: OAuthProviderInterface = {\n\t\t\t\t...config.oauth,\n\t\t\t\tid: providerName,\n\t\t\t};\n\t\t\tregisterOAuthProvider(oauthProvider);\n\t\t}\n\n\t\tif (config.streamSimple) {\n\t\t\tif (!config.api) {\n\t\t\t\tthrow new Error(`Provider ${providerName}: \"api\" is required when registering streamSimple.`);\n\t\t\t}\n\t\t\tconst streamSimple = config.streamSimple;\n\t\t\tregisterApiProvider({\n\t\t\t\tapi: config.api,\n\t\t\t\tstream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),\n\t\t\t\tstreamSimple,\n\t\t\t});\n\t\t}\n\n\t\t// Store API key for auth resolution\n\t\tif (config.apiKey) {\n\t\t\tthis.customProviderApiKeys.set(providerName, config.apiKey);\n\t\t}\n\n\t\tif (config.models && config.models.length > 0) {\n\t\t\t// Full replacement: remove existing models for this provider\n\t\t\tthis.models = this.models.filter((m) => m.provider !== providerName);\n\n\t\t\t// Validate required fields\n\t\t\tif (!config.baseUrl) {\n\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining models.`);\n\t\t\t}\n\t\t\tif (!config.apiKey && !config.oauth) {\n\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" or \"oauth\" is required when defining models.`);\n\t\t\t}\n\n\t\t\t// Parse and add new models\n\t\t\tfor (const modelDef of config.models) {\n\t\t\t\tconst api = modelDef.api || config.api;\n\t\t\t\tif (!api) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified.`);\n\t\t\t\t}\n\n\t\t\t\t// Merge headers\n\t\t\t\tconst providerHeaders = resolveHeaders(config.headers);\n\t\t\t\tconst modelHeaders = resolveHeaders(modelDef.headers);\n\t\t\t\tlet headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;\n\n\t\t\t\t// If authHeader is true, add Authorization header\n\t\t\t\tif (config.authHeader && config.apiKey) {\n\t\t\t\t\tconst resolvedKey = resolveConfigValue(config.apiKey);\n\t\t\t\t\tif (resolvedKey) {\n\t\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.models.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: config.baseUrl,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\n\t\t\t// Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl)\n\t\t\tif (config.oauth?.modifyModels) {\n\t\t\t\tconst cred = this.authStorage.get(providerName);\n\t\t\t\tif (cred?.type === \"oauth\") {\n\t\t\t\t\tthis.models = config.oauth.modifyModels(this.models, cred);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (config.baseUrl) {\n\t\t\t// Override-only: update baseUrl/headers for existing models\n\t\t\tconst resolvedHeaders = resolveHeaders(config.headers);\n\t\t\tthis.models = this.models.map((m) => {\n\t\t\t\tif (m.provider !== providerName) return m;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tbaseUrl: config.baseUrl ?? m.baseUrl,\n\t\t\t\t\theaders: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,\n\t\t\t\t};\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Input type for registerProvider API.\n */\nexport interface ProviderConfigInput {\n\tbaseUrl?: string;\n\tapiKey?: string;\n\tapi?: Api;\n\tstreamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\t/** OAuth provider for /login support */\n\toauth?: Omit<OAuthProviderInterface, \"id\">;\n\tmodels?: Array<{\n\t\tid: string;\n\t\tname: string;\n\t\tapi?: Api;\n\t\treasoning: boolean;\n\t\tinput: (\"text\" | \"image\")[];\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\t\tcontextWindow: number;\n\t\tmaxTokens: number;\n\t\theaders?: Record<string, string>;\n\t\tcompat?: Model<Api>[\"compat\"];\n\t}>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/core/model-registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,KAAK,GAAG,EACR,KAAK,2BAA2B,EAChC,KAAK,OAAO,EAIZ,KAAK,KAAK,EACV,KAAK,sBAAsB,EAK3B,KAAK,mBAAmB,EACxB,MAAM,qBAAqB,CAAC;AAO7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACN,qBAAqB,EAIrB,MAAM,2BAA2B,CAAC;AAmInC,MAAM,MAAM,mBAAmB,GAC5B;IACA,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,GACD;IACA,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACb,CAAC;AA6EL,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,8BAAwB,CAAC;AAEtD;;GAEG;AACH,qBAAa,aAAa;IAQxB,QAAQ,CAAC,WAAW,EAAE,WAAW;IACjC,OAAO,CAAC,cAAc;IARvB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,sBAAsB,CAAiD;IAC/E,OAAO,CAAC,mBAAmB,CAAkD;IAC7E,OAAO,CAAC,mBAAmB,CAA+C;IAC1E,OAAO,CAAC,SAAS,CAAiC;IAElD,OAAO,eAKN;IAED,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,EAAE,cAAc,GAAE,MAA2C,GAAG,aAAa,CAElH;IAED,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,GAAG,aAAa,CAEvD;IAED;;OAEG;IACH,OAAO,IAAI,IAAI,CAcd;IAED;;OAEG;IACH,QAAQ,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,OAAO,CAAC,UAAU;IA4BlB,8DAA8D;IAC9D,OAAO,CAAC,iBAAiB;IAgCzB,wFAAwF;IACxF,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,gBAAgB;IAqDxB,OAAO,CAAC,cAAc;IA2CtB,OAAO,CAAC,WAAW;IAqCnB;;;OAGG;IACH,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAErB;IAED;;;OAGG;IACH,YAAY,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAE3B;IAED;;OAEG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAE9D;IAED;;OAEG;IACH,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAK5C;IAED,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,0BAA0B;IAmBlC,OAAO,CAAC,iBAAiB;IASzB;;OAEG;IACG,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAuCzE;IAED;;OAEG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQxE;IAED;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAGvC;IAED;;;;;;OAMG;IACH,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAIxE;IAED;;;;;;;;OAQG;IACH,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAI7C;IAED,OAAO,CAAC,sBAAsB;IAwB9B,OAAO,CAAC,mBAAmB;CAoE3B;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,2BAA2B,CAAC;IACnH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;IAC3C,MAAM,CAAC,EAAE,KAAK,CAAC;QACd,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,GAAG,CAAC;QACV,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,CAAC,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC;QAC5B,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC;QAC/E,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,MAAM,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;KAC9B,CAAC,CAAC;CACH","sourcesContent":["/**\n * Model registry - manages built-in and custom models, provides API key resolution.\n */\n\nimport {\n\ttype Api,\n\ttype AssistantMessageEventStream,\n\ttype Context,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OAuthProviderInterface,\n\ttype OpenAICompletionsCompat,\n\ttype OpenAIResponsesCompat,\n\tregisterApiProvider,\n\tresetApiProviders,\n\ttype SimpleStreamOptions,\n} from \"@mariozechner/pi-ai\";\nimport { registerOAuthProvider, resetOAuthProviders } from \"@mariozechner/pi-ai/oauth\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport type { AuthStorage } from \"./auth-storage.js\";\nimport {\n\tclearConfigValueCache,\n\tresolveConfigValueOrThrow,\n\tresolveConfigValueUncached,\n\tresolveHeadersOrThrow,\n} from \"./resolve-config-value.js\";\n\nconst Ajv = (AjvModule as any).default || AjvModule;\nconst ajv = new Ajv();\n\n// Schema for OpenRouter routing preferences\nconst OpenRouterRoutingSchema = Type.Object({\n\tonly: Type.Optional(Type.Array(Type.String())),\n\torder: Type.Optional(Type.Array(Type.String())),\n});\n\n// Schema for Vercel AI Gateway routing preferences\nconst VercelGatewayRoutingSchema = Type.Object({\n\tonly: Type.Optional(Type.Array(Type.String())),\n\torder: Type.Optional(Type.Array(Type.String())),\n});\n\n// Schema for OpenAI compatibility settings\nconst ReasoningEffortMapSchema = Type.Object({\n\tminimal: Type.Optional(Type.String()),\n\tlow: Type.Optional(Type.String()),\n\tmedium: Type.Optional(Type.String()),\n\thigh: Type.Optional(Type.String()),\n\txhigh: Type.Optional(Type.String()),\n});\n\nconst OpenAICompletionsCompatSchema = Type.Object({\n\tsupportsStore: Type.Optional(Type.Boolean()),\n\tsupportsDeveloperRole: Type.Optional(Type.Boolean()),\n\tsupportsReasoningEffort: Type.Optional(Type.Boolean()),\n\treasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),\n\tsupportsUsageInStreaming: Type.Optional(Type.Boolean()),\n\tmaxTokensField: Type.Optional(Type.Union([Type.Literal(\"max_completion_tokens\"), Type.Literal(\"max_tokens\")])),\n\trequiresToolResultName: Type.Optional(Type.Boolean()),\n\trequiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),\n\trequiresThinkingAsText: Type.Optional(Type.Boolean()),\n\tthinkingFormat: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai\"),\n\t\t\tType.Literal(\"openrouter\"),\n\t\t\tType.Literal(\"zai\"),\n\t\t\tType.Literal(\"qwen\"),\n\t\t\tType.Literal(\"qwen-chat-template\"),\n\t\t]),\n\t),\n\topenRouterRouting: Type.Optional(OpenRouterRoutingSchema),\n\tvercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),\n\tsupportsStrictMode: Type.Optional(Type.Boolean()),\n});\n\nconst OpenAIResponsesCompatSchema = Type.Object({\n\t// Reserved for future use\n});\n\nconst OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);\n\n// Schema for custom model definition\n// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.Optional(Type.String({ minLength: 1 })),\n\tapi: Type.Optional(Type.String({ minLength: 1 })),\n\tbaseUrl: Type.Optional(Type.String({ minLength: 1 })),\n\treasoning: Type.Optional(Type.Boolean()),\n\tinput: Type.Optional(Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")]))),\n\tcost: Type.Optional(\n\t\tType.Object({\n\t\t\tinput: Type.Number(),\n\t\t\toutput: Type.Number(),\n\t\t\tcacheRead: Type.Number(),\n\t\t\tcacheWrite: Type.Number(),\n\t\t}),\n\t),\n\tcontextWindow: Type.Optional(Type.Number()),\n\tmaxTokens: Type.Optional(Type.Number()),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\n// Schema for per-model overrides (all fields optional, merged with built-in model)\nconst ModelOverrideSchema = Type.Object({\n\tname: Type.Optional(Type.String({ minLength: 1 })),\n\treasoning: Type.Optional(Type.Boolean()),\n\tinput: Type.Optional(Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")]))),\n\tcost: Type.Optional(\n\t\tType.Object({\n\t\t\tinput: Type.Optional(Type.Number()),\n\t\t\toutput: Type.Optional(Type.Number()),\n\t\t\tcacheRead: Type.Optional(Type.Number()),\n\t\t\tcacheWrite: Type.Optional(Type.Number()),\n\t\t}),\n\t),\n\tcontextWindow: Type.Optional(Type.Number()),\n\tmaxTokens: Type.Optional(Type.Number()),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\ntype ModelOverride = Static<typeof ModelOverrideSchema>;\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.Optional(Type.String({ minLength: 1 })),\n\tapiKey: Type.Optional(Type.String({ minLength: 1 })),\n\tapi: Type.Optional(Type.String({ minLength: 1 })),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: Type.Optional(Type.Array(ModelDefinitionSchema)),\n\tmodelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\najv.addSchema(ModelsConfigSchema, \"ModelsConfig\");\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\n\n/** Provider override config (baseUrl, compat) without request auth/headers */\ninterface ProviderOverride {\n\tbaseUrl?: string;\n\tcompat?: Model<Api>[\"compat\"];\n}\n\ninterface ProviderRequestConfig {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n}\n\nexport type ResolvedRequestAuth =\n\t| {\n\t\t\tok: true;\n\t\t\tapiKey?: string;\n\t\t\theaders?: Record<string, string>;\n\t }\n\t| {\n\t\t\tok: false;\n\t\t\terror: string;\n\t };\n\n/** Result of loading custom models from models.json */\ninterface CustomModelsResult {\n\tmodels: Model<Api>[];\n\t/** Providers with baseUrl/headers/apiKey overrides for built-in models */\n\toverrides: Map<string, ProviderOverride>;\n\t/** Per-model overrides: provider -> modelId -> override */\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>;\n\terror: string | undefined;\n}\n\nfunction emptyCustomModelsResult(error?: string): CustomModelsResult {\n\treturn { models: [], overrides: new Map(), modelOverrides: new Map(), error };\n}\n\nfunction mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as OpenAICompletionsCompat | OpenAIResponsesCompat | undefined;\n\tconst override = overrideCompat as OpenAICompletionsCompat | OpenAIResponsesCompat;\n\tconst merged = { ...base, ...override } as OpenAICompletionsCompat | OpenAIResponsesCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\n/**\n * Deep merge a model override into a model.\n * Handles nested objects (cost, compat) by merging rather than replacing.\n */\nfunction applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\t// Simple field overrides\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\t// Merge cost (partial override)\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\t// Deep merge compat\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\n\treturn result;\n}\n\n/** Clear the config value command cache. Exported for testing. */\nexport const clearApiKeyCache = clearConfigValueCache;\n\n/**\n * Model registry - loads and manages models, resolves API keys via AuthStorage.\n */\nexport class ModelRegistry {\n\tprivate models: Model<Api>[] = [];\n\tprivate providerRequestConfigs: Map<string, ProviderRequestConfig> = new Map();\n\tprivate modelRequestHeaders: Map<string, Record<string, string>> = new Map();\n\tprivate registeredProviders: Map<string, ProviderConfigInput> = new Map();\n\tprivate loadError: string | undefined = undefined;\n\n\tprivate constructor(\n\t\treadonly authStorage: AuthStorage,\n\t\tprivate modelsJsonPath: string | undefined,\n\t) {\n\t\tthis.loadModels();\n\t}\n\n\tstatic create(authStorage: AuthStorage, modelsJsonPath: string = join(getAgentDir(), \"models.json\")): ModelRegistry {\n\t\treturn new ModelRegistry(authStorage, modelsJsonPath);\n\t}\n\n\tstatic inMemory(authStorage: AuthStorage): ModelRegistry {\n\t\treturn new ModelRegistry(authStorage, undefined);\n\t}\n\n\t/**\n\t * Reload models from disk (built-in + custom from models.json).\n\t */\n\trefresh(): void {\n\t\tthis.providerRequestConfigs.clear();\n\t\tthis.modelRequestHeaders.clear();\n\t\tthis.loadError = undefined;\n\n\t\t// Ensure dynamic API/OAuth registrations are rebuilt from current provider state.\n\t\tresetApiProviders();\n\t\tresetOAuthProviders();\n\n\t\tthis.loadModels();\n\n\t\tfor (const [providerName, config] of this.registeredProviders.entries()) {\n\t\t\tthis.applyProviderConfig(providerName, config);\n\t\t}\n\t}\n\n\t/**\n\t * Get any error from loading models.json (undefined if no error).\n\t */\n\tgetError(): string | undefined {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load custom models and overrides from models.json\n\t\tconst {\n\t\t\tmodels: customModels,\n\t\t\toverrides,\n\t\t\tmodelOverrides,\n\t\t\terror,\n\t\t} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();\n\n\t\tif (error) {\n\t\t\tthis.loadError = error;\n\t\t\t// Keep built-in models even if custom models failed to load\n\t\t}\n\n\t\tconst builtInModels = this.loadBuiltInModels(overrides, modelOverrides);\n\t\tlet combined = this.mergeCustomModels(builtInModels, customModels);\n\n\t\t// Let OAuth providers modify their models (e.g., update baseUrl)\n\t\tfor (const oauthProvider of this.authStorage.getOAuthProviders()) {\n\t\t\tconst cred = this.authStorage.get(oauthProvider.id);\n\t\t\tif (cred?.type === \"oauth\" && oauthProvider.modifyModels) {\n\t\t\t\tcombined = oauthProvider.modifyModels(combined, cred);\n\t\t\t}\n\t\t}\n\n\t\tthis.models = combined;\n\t}\n\n\t/** Load built-in models and apply provider/model overrides */\n\tprivate loadBuiltInModels(\n\t\toverrides: Map<string, ProviderOverride>,\n\t\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n\t): Model<Api>[] {\n\t\treturn getProviders().flatMap((provider) => {\n\t\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\t\tconst providerOverride = overrides.get(provider);\n\t\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\t\treturn models.map((m) => {\n\t\t\t\tlet model = m;\n\n\t\t\t\t// Apply provider-level baseUrl/headers/compat override\n\t\t\t\tif (providerOverride) {\n\t\t\t\t\tmodel = {\n\t\t\t\t\t\t...model,\n\t\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\t\tcompat: mergeCompat(model.compat, providerOverride.compat),\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Apply per-model override\n\t\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\t\tif (modelOverride) {\n\t\t\t\t\tmodel = applyModelOverride(model, modelOverride);\n\t\t\t\t}\n\n\t\t\t\treturn model;\n\t\t\t});\n\t\t});\n\t}\n\n\t/** Merge custom models into built-in list by provider+id (custom wins on conflicts). */\n\tprivate mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\t\tconst merged = [...builtInModels];\n\t\tfor (const customModel of customModels) {\n\t\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\t\tif (existingIndex >= 0) {\n\t\t\t\tmerged[existingIndex] = customModel;\n\t\t\t} else {\n\t\t\t\tmerged.push(customModel);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tprivate loadCustomModels(modelsJsonPath: string): CustomModelsResult {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn emptyCustomModelsResult();\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(modelsJsonPath, \"utf-8\");\n\t\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t\t// Validate schema\n\t\t\tconst validate = ajv.getSchema(\"ModelsConfig\")!;\n\t\t\tif (!validate(config)) {\n\t\t\t\tconst errors =\n\t\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\t\"Unknown schema error\";\n\t\t\t\treturn emptyCustomModelsResult(`Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\n\t\t\tconst overrides = new Map<string, ProviderOverride>();\n\t\t\tconst modelOverrides = new Map<string, Map<string, ModelOverride>>();\n\n\t\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\t\tif (providerConfig.baseUrl || providerConfig.compat) {\n\t\t\t\t\toverrides.set(providerName, {\n\t\t\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\t\t\tcompat: providerConfig.compat,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tthis.storeProviderRequestConfig(providerName, providerConfig);\n\n\t\t\t\tif (providerConfig.modelOverrides) {\n\t\t\t\t\tmodelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));\n\t\t\t\t\tfor (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides)) {\n\t\t\t\t\t\tthis.storeModelHeaders(providerName, modelId, modelOverride.headers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { models: this.parseModels(config), overrides, modelOverrides, error: undefined };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\t\t\treturn emptyCustomModelsResult(\n\t\t\t\t`Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate validateConfig(config: ModelsConfig): void {\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst hasProviderApi = !!providerConfig.api;\n\t\t\tconst models = providerConfig.models ?? [];\n\t\t\tconst hasModelOverrides =\n\t\t\t\tproviderConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;\n\n\t\t\tif (models.length === 0) {\n\t\t\t\t// Override-only config: needs baseUrl, compat, modelOverrides, or some combination.\n\t\t\t\tif (!providerConfig.baseUrl && !providerConfig.compat && !hasModelOverrides) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}: must specify \"baseUrl\", \"compat\", \"modelOverrides\", or \"models\".`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Custom models are merged into provider models and require endpoint + auth.\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t\tif (!providerConfig.apiKey) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const modelDef of models) {\n\t\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. Set at provider or model level.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\t\t// Validate contextWindow/maxTokens only if provided (they have defaults)\n\t\t\t\tif (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\t\tif (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate parseModels(config: ModelsConfig): Model<Api>[] {\n\t\tconst models: Model<Api>[] = [];\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst modelDefs = providerConfig.models ?? [];\n\t\t\tif (modelDefs.length === 0) continue; // Override-only, no custom models\n\n\t\t\tfor (const modelDef of modelDefs) {\n\t\t\t\tconst api = modelDef.api || providerConfig.api;\n\t\t\t\tif (!api) continue;\n\n\t\t\t\tconst compat = mergeCompat(providerConfig.compat, modelDef.compat);\n\t\t\t\tthis.storeModelHeaders(providerName, modelDef.id, modelDef.headers);\n\n\t\t\t\t// Provider baseUrl is required when custom models are defined.\n\t\t\t\t// Individual models can override it with modelDef.baseUrl.\n\t\t\t\tconst defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\n\t\t\t\tmodels.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name ?? modelDef.id,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: modelDef.baseUrl ?? providerConfig.baseUrl!,\n\t\t\t\t\treasoning: modelDef.reasoning ?? false,\n\t\t\t\t\tinput: (modelDef.input ?? [\"text\"]) as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost ?? defaultCost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow ?? 128000,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens ?? 16384,\n\t\t\t\t\theaders: undefined,\n\t\t\t\t\tcompat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t}\n\n\t\treturn models;\n\t}\n\n\t/**\n\t * Get all models (built-in + custom).\n\t * If models.json had errors, returns only built-in models.\n\t */\n\tgetAll(): Model<Api>[] {\n\t\treturn this.models;\n\t}\n\n\t/**\n\t * Get only models that have auth configured.\n\t * This is a fast check that doesn't refresh OAuth tokens.\n\t */\n\tgetAvailable(): Model<Api>[] {\n\t\treturn this.models.filter((m) => this.hasConfiguredAuth(m));\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | undefined {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId);\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\thasConfiguredAuth(model: Model<Api>): boolean {\n\t\treturn (\n\t\t\tthis.authStorage.hasAuth(model.provider) ||\n\t\t\tthis.providerRequestConfigs.get(model.provider)?.apiKey !== undefined\n\t\t);\n\t}\n\n\tprivate getModelRequestKey(provider: string, modelId: string): string {\n\t\treturn `${provider}:${modelId}`;\n\t}\n\n\tprivate storeProviderRequestConfig(\n\t\tproviderName: string,\n\t\tconfig: {\n\t\t\tapiKey?: string;\n\t\t\theaders?: Record<string, string>;\n\t\t\tauthHeader?: boolean;\n\t\t},\n\t): void {\n\t\tif (!config.apiKey && !config.headers && !config.authHeader) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.providerRequestConfigs.set(providerName, {\n\t\t\tapiKey: config.apiKey,\n\t\t\theaders: config.headers,\n\t\t\tauthHeader: config.authHeader,\n\t\t});\n\t}\n\n\tprivate storeModelHeaders(providerName: string, modelId: string, headers?: Record<string, string>): void {\n\t\tconst key = this.getModelRequestKey(providerName, modelId);\n\t\tif (!headers || Object.keys(headers).length === 0) {\n\t\t\tthis.modelRequestHeaders.delete(key);\n\t\t\treturn;\n\t\t}\n\t\tthis.modelRequestHeaders.set(key, headers);\n\t}\n\n\t/**\n\t * Get API key and request headers for a model.\n\t */\n\tasync getApiKeyAndHeaders(model: Model<Api>): Promise<ResolvedRequestAuth> {\n\t\ttry {\n\t\t\tconst providerConfig = this.providerRequestConfigs.get(model.provider);\n\t\t\tconst apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider, { includeFallback: false });\n\t\t\tconst apiKey =\n\t\t\t\tapiKeyFromAuthStorage ??\n\t\t\t\t(providerConfig?.apiKey\n\t\t\t\t\t? resolveConfigValueOrThrow(providerConfig.apiKey, `API key for provider \"${model.provider}\"`)\n\t\t\t\t\t: undefined);\n\n\t\t\tconst providerHeaders = resolveHeadersOrThrow(providerConfig?.headers, `provider \"${model.provider}\"`);\n\t\t\tconst modelHeaders = resolveHeadersOrThrow(\n\t\t\t\tthis.modelRequestHeaders.get(this.getModelRequestKey(model.provider, model.id)),\n\t\t\t\t`model \"${model.provider}/${model.id}\"`,\n\t\t\t);\n\n\t\t\tlet headers =\n\t\t\t\tmodel.headers || providerHeaders || modelHeaders\n\t\t\t\t\t? { ...model.headers, ...providerHeaders, ...modelHeaders }\n\t\t\t\t\t: undefined;\n\n\t\t\tif (providerConfig?.authHeader) {\n\t\t\t\tif (!apiKey) {\n\t\t\t\t\treturn { ok: false, error: `No API key found for \"${model.provider}\"` };\n\t\t\t\t}\n\t\t\t\theaders = { ...headers, Authorization: `Bearer ${apiKey}` };\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tapiKey,\n\t\t\t\theaders: headers && Object.keys(headers).length > 0 ? headers : undefined,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t */\n\tasync getApiKeyForProvider(provider: string): Promise<string | undefined> {\n\t\tconst apiKey = await this.authStorage.getApiKey(provider, { includeFallback: false });\n\t\tif (apiKey !== undefined) {\n\t\t\treturn apiKey;\n\t\t}\n\n\t\tconst providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey;\n\t\treturn providerApiKey ? resolveConfigValueUncached(providerApiKey) : undefined;\n\t}\n\n\t/**\n\t * Check if a model is using OAuth credentials (subscription).\n\t */\n\tisUsingOAuth(model: Model<Api>): boolean {\n\t\tconst cred = this.authStorage.get(model.provider);\n\t\treturn cred?.type === \"oauth\";\n\t}\n\n\t/**\n\t * Register a provider dynamically (from extensions).\n\t *\n\t * If provider has models: replaces all existing models for this provider.\n\t * If provider has only baseUrl/headers: overrides existing models' URLs.\n\t * If provider has oauth: registers OAuth provider for /login support.\n\t */\n\tregisterProvider(providerName: string, config: ProviderConfigInput): void {\n\t\tthis.validateProviderConfig(providerName, config);\n\t\tthis.applyProviderConfig(providerName, config);\n\t\tthis.registeredProviders.set(providerName, config);\n\t}\n\n\t/**\n\t * Unregister a previously registered provider.\n\t *\n\t * Removes the provider from the registry and reloads models from disk so that\n\t * built-in models overridden by this provider are restored to their original state.\n\t * Also resets dynamic OAuth and API stream registrations before reapplying\n\t * remaining dynamic providers.\n\t * Has no effect if the provider was never registered.\n\t */\n\tunregisterProvider(providerName: string): void {\n\t\tif (!this.registeredProviders.has(providerName)) return;\n\t\tthis.registeredProviders.delete(providerName);\n\t\tthis.refresh();\n\t}\n\n\tprivate validateProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\tif (config.streamSimple && !config.api) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"api\" is required when registering streamSimple.`);\n\t\t}\n\n\t\tif (!config.models || config.models.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!config.baseUrl) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining models.`);\n\t\t}\n\t\tif (!config.apiKey && !config.oauth) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" or \"oauth\" is required when defining models.`);\n\t\t}\n\n\t\tfor (const modelDef of config.models) {\n\t\t\tconst api = modelDef.api || config.api;\n\t\t\tif (!api) {\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified.`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applyProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\t// Register OAuth provider if provided\n\t\tif (config.oauth) {\n\t\t\t// Ensure the OAuth provider ID matches the provider name\n\t\t\tconst oauthProvider: OAuthProviderInterface = {\n\t\t\t\t...config.oauth,\n\t\t\t\tid: providerName,\n\t\t\t};\n\t\t\tregisterOAuthProvider(oauthProvider);\n\t\t}\n\n\t\tif (config.streamSimple) {\n\t\t\tconst streamSimple = config.streamSimple;\n\t\t\tregisterApiProvider(\n\t\t\t\t{\n\t\t\t\t\tapi: config.api!,\n\t\t\t\t\tstream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),\n\t\t\t\t\tstreamSimple,\n\t\t\t\t},\n\t\t\t\t`provider:${providerName}`,\n\t\t\t);\n\t\t}\n\n\t\tthis.storeProviderRequestConfig(providerName, config);\n\n\t\tif (config.models && config.models.length > 0) {\n\t\t\t// Full replacement: remove existing models for this provider\n\t\t\tthis.models = this.models.filter((m) => m.provider !== providerName);\n\n\t\t\t// Parse and add new models\n\t\t\tfor (const modelDef of config.models) {\n\t\t\t\tconst api = modelDef.api || config.api;\n\t\t\t\tthis.storeModelHeaders(providerName, modelDef.id, modelDef.headers);\n\n\t\t\t\tthis.models.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: config.baseUrl!,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders: undefined,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\n\t\t\t// Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl)\n\t\t\tif (config.oauth?.modifyModels) {\n\t\t\t\tconst cred = this.authStorage.get(providerName);\n\t\t\t\tif (cred?.type === \"oauth\") {\n\t\t\t\t\tthis.models = config.oauth.modifyModels(this.models, cred);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (config.baseUrl || config.headers) {\n\t\t\t// Override-only: update baseUrl for existing models. Request headers are resolved per request.\n\t\t\tthis.models = this.models.map((m) => {\n\t\t\t\tif (m.provider !== providerName) return m;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tbaseUrl: config.baseUrl ?? m.baseUrl,\n\t\t\t\t};\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Input type for registerProvider API.\n */\nexport interface ProviderConfigInput {\n\tbaseUrl?: string;\n\tapiKey?: string;\n\tapi?: Api;\n\tstreamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\t/** OAuth provider for /login support */\n\toauth?: Omit<OAuthProviderInterface, \"id\">;\n\tmodels?: Array<{\n\t\tid: string;\n\t\tname: string;\n\t\tapi?: Api;\n\t\tbaseUrl?: string;\n\t\treasoning: boolean;\n\t\tinput: (\"text\" | \"image\")[];\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\t\tcontextWindow: number;\n\t\tmaxTokens: number;\n\t\theaders?: Record<string, string>;\n\t\tcompat?: Model<Api>[\"compat\"];\n\t}>;\n}\n"]}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model registry - manages built-in and custom models, provides API key resolution.
|
|
3
3
|
*/
|
|
4
|
-
import { getModels, getProviders, registerApiProvider,
|
|
4
|
+
import { getModels, getProviders, registerApiProvider, resetApiProviders, } from "@mariozechner/pi-ai";
|
|
5
|
+
import { registerOAuthProvider, resetOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
|
5
6
|
import { Type } from "@sinclair/typebox";
|
|
6
7
|
import AjvModule from "ajv";
|
|
7
8
|
import { existsSync, readFileSync } from "fs";
|
|
8
9
|
import { join } from "path";
|
|
9
10
|
import { getAgentDir } from "../config.js";
|
|
10
|
-
import { clearConfigValueCache,
|
|
11
|
+
import { clearConfigValueCache, resolveConfigValueOrThrow, resolveConfigValueUncached, resolveHeadersOrThrow, } from "./resolve-config-value.js";
|
|
11
12
|
const Ajv = AjvModule.default || AjvModule;
|
|
13
|
+
const ajv = new Ajv();
|
|
12
14
|
// Schema for OpenRouter routing preferences
|
|
13
15
|
const OpenRouterRoutingSchema = Type.Object({
|
|
14
16
|
only: Type.Optional(Type.Array(Type.String())),
|
|
@@ -20,19 +22,33 @@ const VercelGatewayRoutingSchema = Type.Object({
|
|
|
20
22
|
order: Type.Optional(Type.Array(Type.String())),
|
|
21
23
|
});
|
|
22
24
|
// Schema for OpenAI compatibility settings
|
|
25
|
+
const ReasoningEffortMapSchema = Type.Object({
|
|
26
|
+
minimal: Type.Optional(Type.String()),
|
|
27
|
+
low: Type.Optional(Type.String()),
|
|
28
|
+
medium: Type.Optional(Type.String()),
|
|
29
|
+
high: Type.Optional(Type.String()),
|
|
30
|
+
xhigh: Type.Optional(Type.String()),
|
|
31
|
+
});
|
|
23
32
|
const OpenAICompletionsCompatSchema = Type.Object({
|
|
24
33
|
supportsStore: Type.Optional(Type.Boolean()),
|
|
25
34
|
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
|
26
35
|
supportsReasoningEffort: Type.Optional(Type.Boolean()),
|
|
36
|
+
reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
|
|
27
37
|
supportsUsageInStreaming: Type.Optional(Type.Boolean()),
|
|
28
38
|
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
|
|
29
39
|
requiresToolResultName: Type.Optional(Type.Boolean()),
|
|
30
40
|
requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
|
|
31
41
|
requiresThinkingAsText: Type.Optional(Type.Boolean()),
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
thinkingFormat: Type.Optional(Type.Union([
|
|
43
|
+
Type.Literal("openai"),
|
|
44
|
+
Type.Literal("openrouter"),
|
|
45
|
+
Type.Literal("zai"),
|
|
46
|
+
Type.Literal("qwen"),
|
|
47
|
+
Type.Literal("qwen-chat-template"),
|
|
48
|
+
])),
|
|
34
49
|
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
|
|
35
50
|
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
|
|
51
|
+
supportsStrictMode: Type.Optional(Type.Boolean()),
|
|
36
52
|
});
|
|
37
53
|
const OpenAIResponsesCompatSchema = Type.Object({});
|
|
38
54
|
const OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);
|
|
@@ -42,6 +58,7 @@ const ModelDefinitionSchema = Type.Object({
|
|
|
42
58
|
id: Type.String({ minLength: 1 }),
|
|
43
59
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
44
60
|
api: Type.Optional(Type.String({ minLength: 1 })),
|
|
61
|
+
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
|
45
62
|
reasoning: Type.Optional(Type.Boolean()),
|
|
46
63
|
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
|
47
64
|
cost: Type.Optional(Type.Object({
|
|
@@ -76,6 +93,7 @@ const ProviderConfigSchema = Type.Object({
|
|
|
76
93
|
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
|
77
94
|
api: Type.Optional(Type.String({ minLength: 1 })),
|
|
78
95
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
96
|
+
compat: Type.Optional(OpenAICompatSchema),
|
|
79
97
|
authHeader: Type.Optional(Type.Boolean()),
|
|
80
98
|
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
|
|
81
99
|
modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
|
|
@@ -83,6 +101,7 @@ const ProviderConfigSchema = Type.Object({
|
|
|
83
101
|
const ModelsConfigSchema = Type.Object({
|
|
84
102
|
providers: Type.Record(Type.String(), ProviderConfigSchema),
|
|
85
103
|
});
|
|
104
|
+
ajv.addSchema(ModelsConfigSchema, "ModelsConfig");
|
|
86
105
|
function emptyCustomModelsResult(error) {
|
|
87
106
|
return { models: [], overrides: new Map(), modelOverrides: new Map(), error };
|
|
88
107
|
}
|
|
@@ -135,11 +154,6 @@ function applyModelOverride(model, override) {
|
|
|
135
154
|
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
|
136
155
|
};
|
|
137
156
|
}
|
|
138
|
-
// Merge headers
|
|
139
|
-
if (override.headers) {
|
|
140
|
-
const resolvedHeaders = resolveHeaders(override.headers);
|
|
141
|
-
result.headers = resolvedHeaders ? { ...model.headers, ...resolvedHeaders } : model.headers;
|
|
142
|
-
}
|
|
143
157
|
// Deep merge compat
|
|
144
158
|
result.compat = mergeCompat(model.compat, override.compat);
|
|
145
159
|
return result;
|
|
@@ -153,29 +167,31 @@ export class ModelRegistry {
|
|
|
153
167
|
authStorage;
|
|
154
168
|
modelsJsonPath;
|
|
155
169
|
models = [];
|
|
156
|
-
|
|
170
|
+
providerRequestConfigs = new Map();
|
|
171
|
+
modelRequestHeaders = new Map();
|
|
157
172
|
registeredProviders = new Map();
|
|
158
173
|
loadError = undefined;
|
|
159
|
-
constructor(authStorage, modelsJsonPath
|
|
174
|
+
constructor(authStorage, modelsJsonPath) {
|
|
160
175
|
this.authStorage = authStorage;
|
|
161
176
|
this.modelsJsonPath = modelsJsonPath;
|
|
162
|
-
// Set up fallback resolver for custom provider API keys
|
|
163
|
-
this.authStorage.setFallbackResolver((provider) => {
|
|
164
|
-
const keyConfig = this.customProviderApiKeys.get(provider);
|
|
165
|
-
if (keyConfig) {
|
|
166
|
-
return resolveConfigValue(keyConfig);
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
169
|
-
});
|
|
170
|
-
// Load models
|
|
171
177
|
this.loadModels();
|
|
172
178
|
}
|
|
179
|
+
static create(authStorage, modelsJsonPath = join(getAgentDir(), "models.json")) {
|
|
180
|
+
return new ModelRegistry(authStorage, modelsJsonPath);
|
|
181
|
+
}
|
|
182
|
+
static inMemory(authStorage) {
|
|
183
|
+
return new ModelRegistry(authStorage, undefined);
|
|
184
|
+
}
|
|
173
185
|
/**
|
|
174
186
|
* Reload models from disk (built-in + custom from models.json).
|
|
175
187
|
*/
|
|
176
188
|
refresh() {
|
|
177
|
-
this.
|
|
189
|
+
this.providerRequestConfigs.clear();
|
|
190
|
+
this.modelRequestHeaders.clear();
|
|
178
191
|
this.loadError = undefined;
|
|
192
|
+
// Ensure dynamic API/OAuth registrations are rebuilt from current provider state.
|
|
193
|
+
resetApiProviders();
|
|
194
|
+
resetOAuthProviders();
|
|
179
195
|
this.loadModels();
|
|
180
196
|
for (const [providerName, config] of this.registeredProviders.entries()) {
|
|
181
197
|
this.applyProviderConfig(providerName, config);
|
|
@@ -213,13 +229,12 @@ export class ModelRegistry {
|
|
|
213
229
|
const perModelOverrides = modelOverrides.get(provider);
|
|
214
230
|
return models.map((m) => {
|
|
215
231
|
let model = m;
|
|
216
|
-
// Apply provider-level baseUrl/headers override
|
|
232
|
+
// Apply provider-level baseUrl/headers/compat override
|
|
217
233
|
if (providerOverride) {
|
|
218
|
-
const resolvedHeaders = resolveHeaders(providerOverride.headers);
|
|
219
234
|
model = {
|
|
220
235
|
...model,
|
|
221
236
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
222
|
-
|
|
237
|
+
compat: mergeCompat(model.compat, providerOverride.compat),
|
|
223
238
|
};
|
|
224
239
|
}
|
|
225
240
|
// Apply per-model override
|
|
@@ -253,8 +268,7 @@ export class ModelRegistry {
|
|
|
253
268
|
const content = readFileSync(modelsJsonPath, "utf-8");
|
|
254
269
|
const config = JSON.parse(content);
|
|
255
270
|
// Validate schema
|
|
256
|
-
const
|
|
257
|
-
const validate = ajv.compile(ModelsConfigSchema);
|
|
271
|
+
const validate = ajv.getSchema("ModelsConfig");
|
|
258
272
|
if (!validate(config)) {
|
|
259
273
|
const errors = validate.errors?.map((e) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
|
|
260
274
|
"Unknown schema error";
|
|
@@ -265,20 +279,18 @@ export class ModelRegistry {
|
|
|
265
279
|
const overrides = new Map();
|
|
266
280
|
const modelOverrides = new Map();
|
|
267
281
|
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
268
|
-
|
|
269
|
-
if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey) {
|
|
282
|
+
if (providerConfig.baseUrl || providerConfig.compat) {
|
|
270
283
|
overrides.set(providerName, {
|
|
271
284
|
baseUrl: providerConfig.baseUrl,
|
|
272
|
-
|
|
273
|
-
apiKey: providerConfig.apiKey,
|
|
285
|
+
compat: providerConfig.compat,
|
|
274
286
|
});
|
|
275
287
|
}
|
|
276
|
-
|
|
277
|
-
if (providerConfig.apiKey) {
|
|
278
|
-
this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
279
|
-
}
|
|
288
|
+
this.storeProviderRequestConfig(providerName, providerConfig);
|
|
280
289
|
if (providerConfig.modelOverrides) {
|
|
281
290
|
modelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));
|
|
291
|
+
for (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides)) {
|
|
292
|
+
this.storeModelHeaders(providerName, modelId, modelOverride.headers);
|
|
293
|
+
}
|
|
282
294
|
}
|
|
283
295
|
}
|
|
284
296
|
return { models: this.parseModels(config), overrides, modelOverrides, error: undefined };
|
|
@@ -296,9 +308,9 @@ export class ModelRegistry {
|
|
|
296
308
|
const models = providerConfig.models ?? [];
|
|
297
309
|
const hasModelOverrides = providerConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;
|
|
298
310
|
if (models.length === 0) {
|
|
299
|
-
// Override-only config: needs baseUrl
|
|
300
|
-
if (!providerConfig.baseUrl && !hasModelOverrides) {
|
|
301
|
-
throw new Error(`Provider ${providerName}: must specify "baseUrl", "modelOverrides", or "models".`);
|
|
311
|
+
// Override-only config: needs baseUrl, compat, modelOverrides, or some combination.
|
|
312
|
+
if (!providerConfig.baseUrl && !providerConfig.compat && !hasModelOverrides) {
|
|
313
|
+
throw new Error(`Provider ${providerName}: must specify "baseUrl", "compat", "modelOverrides", or "models".`);
|
|
302
314
|
}
|
|
303
315
|
}
|
|
304
316
|
else {
|
|
@@ -331,42 +343,28 @@ export class ModelRegistry {
|
|
|
331
343
|
const modelDefs = providerConfig.models ?? [];
|
|
332
344
|
if (modelDefs.length === 0)
|
|
333
345
|
continue; // Override-only, no custom models
|
|
334
|
-
// Store API key config for fallback resolver
|
|
335
|
-
if (providerConfig.apiKey) {
|
|
336
|
-
this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
337
|
-
}
|
|
338
346
|
for (const modelDef of modelDefs) {
|
|
339
347
|
const api = modelDef.api || providerConfig.api;
|
|
340
348
|
if (!api)
|
|
341
349
|
continue;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
|
|
347
|
-
// If authHeader is true, add Authorization header with resolved API key
|
|
348
|
-
if (providerConfig.authHeader && providerConfig.apiKey) {
|
|
349
|
-
const resolvedKey = resolveConfigValue(providerConfig.apiKey);
|
|
350
|
-
if (resolvedKey) {
|
|
351
|
-
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
// baseUrl is validated to exist for providers with models
|
|
355
|
-
// Apply defaults for optional fields
|
|
350
|
+
const compat = mergeCompat(providerConfig.compat, modelDef.compat);
|
|
351
|
+
this.storeModelHeaders(providerName, modelDef.id, modelDef.headers);
|
|
352
|
+
// Provider baseUrl is required when custom models are defined.
|
|
353
|
+
// Individual models can override it with modelDef.baseUrl.
|
|
356
354
|
const defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
357
355
|
models.push({
|
|
358
356
|
id: modelDef.id,
|
|
359
357
|
name: modelDef.name ?? modelDef.id,
|
|
360
358
|
api: api,
|
|
361
359
|
provider: providerName,
|
|
362
|
-
baseUrl: providerConfig.baseUrl,
|
|
360
|
+
baseUrl: modelDef.baseUrl ?? providerConfig.baseUrl,
|
|
363
361
|
reasoning: modelDef.reasoning ?? false,
|
|
364
362
|
input: (modelDef.input ?? ["text"]),
|
|
365
363
|
cost: modelDef.cost ?? defaultCost,
|
|
366
364
|
contextWindow: modelDef.contextWindow ?? 128000,
|
|
367
365
|
maxTokens: modelDef.maxTokens ?? 16384,
|
|
368
|
-
headers,
|
|
369
|
-
compat
|
|
366
|
+
headers: undefined,
|
|
367
|
+
compat,
|
|
370
368
|
});
|
|
371
369
|
}
|
|
372
370
|
}
|
|
@@ -384,7 +382,7 @@ export class ModelRegistry {
|
|
|
384
382
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
385
383
|
*/
|
|
386
384
|
getAvailable() {
|
|
387
|
-
return this.models.filter((m) => this.
|
|
385
|
+
return this.models.filter((m) => this.hasConfiguredAuth(m));
|
|
388
386
|
}
|
|
389
387
|
/**
|
|
390
388
|
* Find a model by provider and ID.
|
|
@@ -395,14 +393,76 @@ export class ModelRegistry {
|
|
|
395
393
|
/**
|
|
396
394
|
* Get API key for a model.
|
|
397
395
|
*/
|
|
398
|
-
|
|
399
|
-
return this.authStorage.
|
|
396
|
+
hasConfiguredAuth(model) {
|
|
397
|
+
return (this.authStorage.hasAuth(model.provider) ||
|
|
398
|
+
this.providerRequestConfigs.get(model.provider)?.apiKey !== undefined);
|
|
399
|
+
}
|
|
400
|
+
getModelRequestKey(provider, modelId) {
|
|
401
|
+
return `${provider}:${modelId}`;
|
|
402
|
+
}
|
|
403
|
+
storeProviderRequestConfig(providerName, config) {
|
|
404
|
+
if (!config.apiKey && !config.headers && !config.authHeader) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
this.providerRequestConfigs.set(providerName, {
|
|
408
|
+
apiKey: config.apiKey,
|
|
409
|
+
headers: config.headers,
|
|
410
|
+
authHeader: config.authHeader,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
storeModelHeaders(providerName, modelId, headers) {
|
|
414
|
+
const key = this.getModelRequestKey(providerName, modelId);
|
|
415
|
+
if (!headers || Object.keys(headers).length === 0) {
|
|
416
|
+
this.modelRequestHeaders.delete(key);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.modelRequestHeaders.set(key, headers);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get API key and request headers for a model.
|
|
423
|
+
*/
|
|
424
|
+
async getApiKeyAndHeaders(model) {
|
|
425
|
+
try {
|
|
426
|
+
const providerConfig = this.providerRequestConfigs.get(model.provider);
|
|
427
|
+
const apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider, { includeFallback: false });
|
|
428
|
+
const apiKey = apiKeyFromAuthStorage ??
|
|
429
|
+
(providerConfig?.apiKey
|
|
430
|
+
? resolveConfigValueOrThrow(providerConfig.apiKey, `API key for provider "${model.provider}"`)
|
|
431
|
+
: undefined);
|
|
432
|
+
const providerHeaders = resolveHeadersOrThrow(providerConfig?.headers, `provider "${model.provider}"`);
|
|
433
|
+
const modelHeaders = resolveHeadersOrThrow(this.modelRequestHeaders.get(this.getModelRequestKey(model.provider, model.id)), `model "${model.provider}/${model.id}"`);
|
|
434
|
+
let headers = model.headers || providerHeaders || modelHeaders
|
|
435
|
+
? { ...model.headers, ...providerHeaders, ...modelHeaders }
|
|
436
|
+
: undefined;
|
|
437
|
+
if (providerConfig?.authHeader) {
|
|
438
|
+
if (!apiKey) {
|
|
439
|
+
return { ok: false, error: `No API key found for "${model.provider}"` };
|
|
440
|
+
}
|
|
441
|
+
headers = { ...headers, Authorization: `Bearer ${apiKey}` };
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
ok: true,
|
|
445
|
+
apiKey,
|
|
446
|
+
headers: headers && Object.keys(headers).length > 0 ? headers : undefined,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
error: error instanceof Error ? error.message : String(error),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
400
455
|
}
|
|
401
456
|
/**
|
|
402
457
|
* Get API key for a provider.
|
|
403
458
|
*/
|
|
404
459
|
async getApiKeyForProvider(provider) {
|
|
405
|
-
|
|
460
|
+
const apiKey = await this.authStorage.getApiKey(provider, { includeFallback: false });
|
|
461
|
+
if (apiKey !== undefined) {
|
|
462
|
+
return apiKey;
|
|
463
|
+
}
|
|
464
|
+
const providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey;
|
|
465
|
+
return providerApiKey ? resolveConfigValueUncached(providerApiKey) : undefined;
|
|
406
466
|
}
|
|
407
467
|
/**
|
|
408
468
|
* Check if a model is using OAuth credentials (subscription).
|
|
@@ -419,8 +479,44 @@ export class ModelRegistry {
|
|
|
419
479
|
* If provider has oauth: registers OAuth provider for /login support.
|
|
420
480
|
*/
|
|
421
481
|
registerProvider(providerName, config) {
|
|
422
|
-
this.
|
|
482
|
+
this.validateProviderConfig(providerName, config);
|
|
423
483
|
this.applyProviderConfig(providerName, config);
|
|
484
|
+
this.registeredProviders.set(providerName, config);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Unregister a previously registered provider.
|
|
488
|
+
*
|
|
489
|
+
* Removes the provider from the registry and reloads models from disk so that
|
|
490
|
+
* built-in models overridden by this provider are restored to their original state.
|
|
491
|
+
* Also resets dynamic OAuth and API stream registrations before reapplying
|
|
492
|
+
* remaining dynamic providers.
|
|
493
|
+
* Has no effect if the provider was never registered.
|
|
494
|
+
*/
|
|
495
|
+
unregisterProvider(providerName) {
|
|
496
|
+
if (!this.registeredProviders.has(providerName))
|
|
497
|
+
return;
|
|
498
|
+
this.registeredProviders.delete(providerName);
|
|
499
|
+
this.refresh();
|
|
500
|
+
}
|
|
501
|
+
validateProviderConfig(providerName, config) {
|
|
502
|
+
if (config.streamSimple && !config.api) {
|
|
503
|
+
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
|
504
|
+
}
|
|
505
|
+
if (!config.models || config.models.length === 0) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (!config.baseUrl) {
|
|
509
|
+
throw new Error(`Provider ${providerName}: "baseUrl" is required when defining models.`);
|
|
510
|
+
}
|
|
511
|
+
if (!config.apiKey && !config.oauth) {
|
|
512
|
+
throw new Error(`Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`);
|
|
513
|
+
}
|
|
514
|
+
for (const modelDef of config.models) {
|
|
515
|
+
const api = modelDef.api || config.api;
|
|
516
|
+
if (!api) {
|
|
517
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
424
520
|
}
|
|
425
521
|
applyProviderConfig(providerName, config) {
|
|
426
522
|
// Register OAuth provider if provided
|
|
@@ -433,47 +529,21 @@ export class ModelRegistry {
|
|
|
433
529
|
registerOAuthProvider(oauthProvider);
|
|
434
530
|
}
|
|
435
531
|
if (config.streamSimple) {
|
|
436
|
-
if (!config.api) {
|
|
437
|
-
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
|
438
|
-
}
|
|
439
532
|
const streamSimple = config.streamSimple;
|
|
440
533
|
registerApiProvider({
|
|
441
534
|
api: config.api,
|
|
442
535
|
stream: (model, context, options) => streamSimple(model, context, options),
|
|
443
536
|
streamSimple,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
// Store API key for auth resolution
|
|
447
|
-
if (config.apiKey) {
|
|
448
|
-
this.customProviderApiKeys.set(providerName, config.apiKey);
|
|
537
|
+
}, `provider:${providerName}`);
|
|
449
538
|
}
|
|
539
|
+
this.storeProviderRequestConfig(providerName, config);
|
|
450
540
|
if (config.models && config.models.length > 0) {
|
|
451
541
|
// Full replacement: remove existing models for this provider
|
|
452
542
|
this.models = this.models.filter((m) => m.provider !== providerName);
|
|
453
|
-
// Validate required fields
|
|
454
|
-
if (!config.baseUrl) {
|
|
455
|
-
throw new Error(`Provider ${providerName}: "baseUrl" is required when defining models.`);
|
|
456
|
-
}
|
|
457
|
-
if (!config.apiKey && !config.oauth) {
|
|
458
|
-
throw new Error(`Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`);
|
|
459
|
-
}
|
|
460
543
|
// Parse and add new models
|
|
461
544
|
for (const modelDef of config.models) {
|
|
462
545
|
const api = modelDef.api || config.api;
|
|
463
|
-
|
|
464
|
-
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
465
|
-
}
|
|
466
|
-
// Merge headers
|
|
467
|
-
const providerHeaders = resolveHeaders(config.headers);
|
|
468
|
-
const modelHeaders = resolveHeaders(modelDef.headers);
|
|
469
|
-
let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
|
|
470
|
-
// If authHeader is true, add Authorization header
|
|
471
|
-
if (config.authHeader && config.apiKey) {
|
|
472
|
-
const resolvedKey = resolveConfigValue(config.apiKey);
|
|
473
|
-
if (resolvedKey) {
|
|
474
|
-
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
475
|
-
}
|
|
476
|
-
}
|
|
546
|
+
this.storeModelHeaders(providerName, modelDef.id, modelDef.headers);
|
|
477
547
|
this.models.push({
|
|
478
548
|
id: modelDef.id,
|
|
479
549
|
name: modelDef.name,
|
|
@@ -485,7 +555,7 @@ export class ModelRegistry {
|
|
|
485
555
|
cost: modelDef.cost,
|
|
486
556
|
contextWindow: modelDef.contextWindow,
|
|
487
557
|
maxTokens: modelDef.maxTokens,
|
|
488
|
-
headers,
|
|
558
|
+
headers: undefined,
|
|
489
559
|
compat: modelDef.compat,
|
|
490
560
|
});
|
|
491
561
|
}
|
|
@@ -497,16 +567,14 @@ export class ModelRegistry {
|
|
|
497
567
|
}
|
|
498
568
|
}
|
|
499
569
|
}
|
|
500
|
-
else if (config.baseUrl) {
|
|
501
|
-
// Override-only: update baseUrl
|
|
502
|
-
const resolvedHeaders = resolveHeaders(config.headers);
|
|
570
|
+
else if (config.baseUrl || config.headers) {
|
|
571
|
+
// Override-only: update baseUrl for existing models. Request headers are resolved per request.
|
|
503
572
|
this.models = this.models.map((m) => {
|
|
504
573
|
if (m.provider !== providerName)
|
|
505
574
|
return m;
|
|
506
575
|
return {
|
|
507
576
|
...m,
|
|
508
577
|
baseUrl: config.baseUrl ?? m.baseUrl,
|
|
509
|
-
headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,
|
|
510
578
|
};
|
|
511
579
|
});
|
|
512
580
|
}
|