@gajae-code/coding-agent 0.18.11 → 0.19.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 +760 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +21 -3
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +13 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +53 -1
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +44 -0
- package/dist/types/config/file-lock.d.ts +52 -3
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +79 -9
- package/dist/types/config/model-registry.d.ts +72 -8
- package/dist/types/config/model-resolver.d.ts +7 -4
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +57 -4
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +14 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +42 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +82 -3
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
- package/dist/types/sdk/broker/broker.d.ts +29 -2
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +141 -9
- package/dist/types/sdk/broker/hop.d.ts +45 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +6 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +44 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +40 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +107 -9
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +29 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +15 -2
- package/dist/types/tools/fetch.d.ts +7 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +10 -9
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +38 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +58 -22
- package/src/capability/index.ts +2 -2
- package/src/capability/rule.ts +11 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -3
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +19 -6
- package/src/cli/public-command-errors.ts +6 -0
- package/src/cli/root-flags.ts +2 -4
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +471 -13
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +277 -50
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/message.ts +10 -3
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +50 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +742 -0
- package/src/config/file-lock.ts +1615 -328
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +889 -148
- package/src/config/model-registry.ts +1706 -205
- package/src/config/model-resolver.ts +52 -22
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +73 -4
- package/src/config/settings.ts +460 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +7 -1
- package/src/customization/import.ts +279 -8
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +89 -6
- package/src/debug/report-bundle.ts +90 -2
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +25 -16
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +204 -22
- package/src/edit/modes/patch.ts +214 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +86 -6
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +10 -1
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +37 -7
- package/src/gjc-runtime/launch-tmux.ts +30 -15
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +17 -12
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +23 -22
- package/src/internal-urls/local-protocol.ts +167 -8
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +574 -2
- package/src/migrate/adapters/claude-code.ts +162 -5
- package/src/migrate/adapters/codex.ts +77 -7
- package/src/migrate/adapters/index.ts +258 -5
- package/src/migrate/adapters/opencode.ts +10 -1
- package/src/migrate/executor.ts +222 -7
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +63 -2
- package/src/migrate/types.ts +84 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +300 -9
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +66 -11
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +76 -7
- package/src/modes/interactive-mode.ts +123 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/theme/theme.ts +15 -0
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +40 -3
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +194 -21
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +10 -5
- package/src/sdk/broker/broker.ts +484 -85
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1214 -268
- package/src/sdk/broker/hop.ts +286 -0
- package/src/sdk/broker/lifecycle-ledger.ts +38 -11
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +91 -16
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +460 -35
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +23 -6
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1737 -129
- package/src/sdk/host/session-runtime.ts +684 -83
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +15 -1
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +81 -12
- package/src/sdk/protocol/operation-inventory.generated.json +261 -2
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +237 -97
- package/src/session/agent-session.ts +2726 -479
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +173 -57
- package/src/session/internal/managed-session-storage.ts +85 -12
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +326 -19
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/redact.ts +53 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +2145 -206
- package/src/slash-commands/builtin-registry.ts +21 -6
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +115 -20
- package/src/task/index.ts +389 -175
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +15 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +214 -6
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +113 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +16 -8
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +50 -6
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +93 -4
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +74 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +26 -37
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +118 -5
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +32 -4
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +42 -3
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +12 -4
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +75 -7
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +72 -22
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import * as crypto from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
@@ -5,6 +6,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
5
6
|
import {
|
|
6
7
|
type Api,
|
|
7
8
|
type AssistantMessageEventStream,
|
|
9
|
+
type AuthApiKeyOptions,
|
|
8
10
|
type AuthCredentialSelector,
|
|
9
11
|
applyFinalCodexGpt56ContextCap,
|
|
10
12
|
applyGeneratedModelPolicies,
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
} from "@gajae-code/ai/core";
|
|
44
46
|
import { fetchModelsDevPayload } from "@gajae-code/ai/provider-models/openai-compat";
|
|
45
47
|
import { isDirectXaiReasoningEffortModel } from "@gajae-code/ai/providers/openai-completions-compat";
|
|
48
|
+
import { getAntigravityDiscoveryUserAgent } from "@gajae-code/ai/utils/discovery/antigravity";
|
|
46
49
|
import {
|
|
47
50
|
detectDiscoveredApiFamily,
|
|
48
51
|
isSafeCatalogModelId,
|
|
@@ -52,6 +55,7 @@ import {
|
|
|
52
55
|
} from "@gajae-code/ai/utils/discovery/openai-compatible";
|
|
53
56
|
// Internal-only: registerFinalizedModelClone is not part of the public @gajae-code/ai surface
|
|
54
57
|
import { registerFinalizedModelClone } from "@gajae-code/ai/utils/trusted-model-clone";
|
|
58
|
+
import { JSONC, YAML } from "bun";
|
|
55
59
|
|
|
56
60
|
// Sentinels for local-only OAuth tokens — declared inline to avoid loading provider
|
|
57
61
|
// modules at startup. Must match the provider OAuth modules.
|
|
@@ -77,13 +81,15 @@ import {
|
|
|
77
81
|
projectActiveProviderDescriptors,
|
|
78
82
|
} from "../sdk/providers";
|
|
79
83
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
84
|
+
import { writeExplicitOwnerOnlyTextSync } from "../session/session-storage";
|
|
80
85
|
import type { ActiveSearchModelContext, WebSearchMode } from "../web/search/types";
|
|
81
86
|
import { type BillingPath, deriveBillingPath } from "./billing-path";
|
|
82
|
-
import { ConfigError, ConfigFile } from "./config-file";
|
|
87
|
+
import { ConfigError, ConfigFile, type LoadResult } from "./config-file";
|
|
83
88
|
import { isAuthenticated, kNoAuth } from "./model-auth";
|
|
84
89
|
import { type ConfiguredModelBindings, ModelBindingsApplier } from "./model-bindings-applier";
|
|
85
90
|
import { ModelDiscoveryManager, type ProviderDiscoveryState } from "./model-discovery-manager";
|
|
86
91
|
import {
|
|
92
|
+
type AcceptedModelPresetRegistry,
|
|
87
93
|
loadAcceptedModelPresetProfiles,
|
|
88
94
|
type ModelPresetRegistryDependencies,
|
|
89
95
|
type ModelPresetRegistryRefreshResult,
|
|
@@ -449,6 +455,20 @@ function assertRequestTransformSupportedForKnownProvider(providerName: string, s
|
|
|
449
455
|
}
|
|
450
456
|
}
|
|
451
457
|
|
|
458
|
+
function assertStreamIdleTimeoutSupportedForKnownProvider(providerName: string, source: string): void {
|
|
459
|
+
const apis = getKnownProviderApis(providerName);
|
|
460
|
+
if (apis.size === 0) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Provider ${providerName}: ${source} requires an OpenAI-compatible "api" when the provider is not built in.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (![...apis].some(isRequestTransformApi)) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Provider ${providerName}: ${source} is only supported with openai-completions or openai-responses APIs.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
452
472
|
function assertRequestTransformSupportedForModelApi(
|
|
453
473
|
providerName: string,
|
|
454
474
|
modelId: string,
|
|
@@ -463,8 +483,54 @@ function assertRequestTransformSupportedForModelApi(
|
|
|
463
483
|
}
|
|
464
484
|
|
|
465
485
|
function getKnownProviderModelApi(providerName: string, modelId: string): Api | undefined {
|
|
466
|
-
return getBundledModels(providerName as Parameters<typeof getBundledModels>[0]).find(
|
|
467
|
-
|
|
486
|
+
return getBundledModels(providerName as Parameters<typeof getBundledModels>[0]).find(
|
|
487
|
+
model => model.id.toLowerCase() === modelId.toLowerCase(),
|
|
488
|
+
)?.api as Api | undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function resolveModelApiOverrideFromDiscovery(
|
|
492
|
+
providerApi: Api,
|
|
493
|
+
discovery: ProviderDiscovery,
|
|
494
|
+
modelId: string,
|
|
495
|
+
entry?: { id?: unknown; owned_by?: unknown },
|
|
496
|
+
allowModelIdDetection = true,
|
|
497
|
+
): Api | undefined {
|
|
498
|
+
// Explicit per-prefix routing always wins over model-id detection.
|
|
499
|
+
const normalizedModelId = modelId.toLowerCase();
|
|
500
|
+
let matchedPrefixLength = -1;
|
|
501
|
+
let prefixApi: Api | undefined;
|
|
502
|
+
for (const [prefix, routedApi] of Object.entries(discovery.apiByModelPrefix ?? {})) {
|
|
503
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
504
|
+
if (normalizedModelId.startsWith(normalizedPrefix) && normalizedPrefix.length > matchedPrefixLength) {
|
|
505
|
+
prefixApi = routedApi;
|
|
506
|
+
matchedPrefixLength = normalizedPrefix.length;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (prefixApi !== undefined) return prefixApi;
|
|
510
|
+
|
|
511
|
+
// An OpenAI-shaped models list can advertise Anthropic models that must use
|
|
512
|
+
// the Anthropic transport even when the provider's default is OpenAI-family.
|
|
513
|
+
if (discovery.type === "openai-models-list") {
|
|
514
|
+
let candidateEntry = entry;
|
|
515
|
+
if (candidateEntry === undefined && allowModelIdDetection) {
|
|
516
|
+
candidateEntry = { id: modelId };
|
|
517
|
+
}
|
|
518
|
+
const detected = candidateEntry === undefined ? undefined : detectDiscoveredApiFamily(candidateEntry);
|
|
519
|
+
if (detected === "anthropic-messages") return "anthropic-messages";
|
|
520
|
+
if (detected === "openai-completions") {
|
|
521
|
+
return isOpenAIFamilyApi(providerApi) ? providerApi : "openai-completions";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function resolveModelApiFromDiscovery(
|
|
528
|
+
providerApi: Api,
|
|
529
|
+
discovery: ProviderDiscovery,
|
|
530
|
+
modelId: string,
|
|
531
|
+
entry?: { id?: unknown; owned_by?: unknown },
|
|
532
|
+
): Api {
|
|
533
|
+
return resolveModelApiOverrideFromDiscovery(providerApi, discovery, modelId, entry) ?? providerApi;
|
|
468
534
|
}
|
|
469
535
|
|
|
470
536
|
function isCanonicalOpenAIAffinityBaseUrl(baseUrl: string | undefined): boolean {
|
|
@@ -513,6 +579,7 @@ interface ProviderValidationModel {
|
|
|
513
579
|
maxTokens?: number;
|
|
514
580
|
compat?: Model<Api>["compat"];
|
|
515
581
|
requestTransform?: ModelRequestTransform;
|
|
582
|
+
streamIdleTimeoutMs?: number;
|
|
516
583
|
}
|
|
517
584
|
|
|
518
585
|
interface ProviderValidationConfig {
|
|
@@ -521,6 +588,7 @@ interface ProviderValidationConfig {
|
|
|
521
588
|
apiKey?: string;
|
|
522
589
|
apiKeyEnv?: string;
|
|
523
590
|
api?: Api;
|
|
591
|
+
transport?: Model<Api>["transport"];
|
|
524
592
|
auth?: ProviderAuthMode;
|
|
525
593
|
oauthConfigured?: boolean;
|
|
526
594
|
discovery?: ProviderDiscovery;
|
|
@@ -528,6 +596,7 @@ interface ProviderValidationConfig {
|
|
|
528
596
|
requestTransform?: ModelRequestTransform;
|
|
529
597
|
disableStrictTools?: boolean;
|
|
530
598
|
cacheRetention?: CacheRetention;
|
|
599
|
+
streamIdleTimeoutMs?: number;
|
|
531
600
|
openaiCompat?: { baseUrl: string; apiKey?: string; apiKeyEnv?: string };
|
|
532
601
|
modelOverrides?: Record<string, unknown>;
|
|
533
602
|
models: ProviderValidationModel[];
|
|
@@ -543,7 +612,20 @@ function validateProviderConfiguration(
|
|
|
543
612
|
mode: ProviderValidationMode,
|
|
544
613
|
): void {
|
|
545
614
|
const hasProviderApi = !!config.api;
|
|
615
|
+
const configuredApi = config.api ?? (config.openaiCompat ? "openai-completions" : undefined);
|
|
546
616
|
const models = config.models;
|
|
617
|
+
if (
|
|
618
|
+
config.transport === "pi-native" &&
|
|
619
|
+
(config.streamIdleTimeoutMs !== undefined ||
|
|
620
|
+
models.some(model => model.streamIdleTimeoutMs !== undefined) ||
|
|
621
|
+
Object.values(config.modelOverrides ?? {}).some(
|
|
622
|
+
override => (override as ModelOverride).streamIdleTimeoutMs !== undefined,
|
|
623
|
+
))
|
|
624
|
+
) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`Provider ${providerName}: "streamIdleTimeoutMs" is not supported with "transport: pi-native"; configure it on the auth-gateway's upstream model.`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
547
629
|
|
|
548
630
|
if (models.length === 0) {
|
|
549
631
|
if (mode === "models-config") {
|
|
@@ -557,12 +639,13 @@ function validateProviderConfiguration(
|
|
|
557
639
|
!config.disableStrictTools &&
|
|
558
640
|
!config.requestTransform &&
|
|
559
641
|
!config.cacheRetention &&
|
|
642
|
+
config.streamIdleTimeoutMs === undefined &&
|
|
560
643
|
!config.openaiCompat &&
|
|
561
644
|
!hasModelOverrides &&
|
|
562
645
|
!config.discovery
|
|
563
646
|
) {
|
|
564
647
|
throw new Error(
|
|
565
|
-
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "disableStrictTools", "modelOverrides", "discovery", "openaiCompat", or "models"`,
|
|
648
|
+
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "streamIdleTimeoutMs", "disableStrictTools", "modelOverrides", "discovery", "openaiCompat", or "models"`,
|
|
566
649
|
);
|
|
567
650
|
}
|
|
568
651
|
}
|
|
@@ -623,10 +706,55 @@ function validateProviderConfiguration(
|
|
|
623
706
|
}
|
|
624
707
|
for (const [modelId, rawOverride] of Object.entries(config.modelOverrides ?? {})) {
|
|
625
708
|
const override = rawOverride as ModelOverride;
|
|
709
|
+
const configuredModel = models.find(model => model.id.toLowerCase() === modelId.toLowerCase());
|
|
710
|
+
const discoveryProviderApi =
|
|
711
|
+
(config.api as Api | undefined) ??
|
|
712
|
+
dominantOpenAIFamilyModelApi(models) ??
|
|
713
|
+
(config.openaiCompat ? "openai-completions" : undefined);
|
|
714
|
+
const implicitDiscoveryBaseUrl = config.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName);
|
|
715
|
+
const implicitDiscovery: ProviderDiscovery | undefined =
|
|
716
|
+
!config.discovery &&
|
|
717
|
+
(config.openaiCompat !== undefined ||
|
|
718
|
+
(providerName !== "vllm" &&
|
|
719
|
+
providerName !== "sglang" &&
|
|
720
|
+
discoveryProviderApi !== undefined &&
|
|
721
|
+
isOpenAIFamilyApi(discoveryProviderApi) &&
|
|
722
|
+
implicitDiscoveryBaseUrl !== undefined))
|
|
723
|
+
? { type: "openai-models-list" }
|
|
724
|
+
: undefined;
|
|
725
|
+
const effectiveDiscovery = config.discovery ?? implicitDiscovery;
|
|
726
|
+
const discoveryApi =
|
|
727
|
+
configuredModel || !effectiveDiscovery || !discoveryProviderApi
|
|
728
|
+
? undefined
|
|
729
|
+
: resolveModelApiOverrideFromDiscovery(discoveryProviderApi, effectiveDiscovery, modelId);
|
|
730
|
+
const discoveryTimeoutApi =
|
|
731
|
+
configuredModel || !effectiveDiscovery || !discoveryProviderApi
|
|
732
|
+
? undefined
|
|
733
|
+
: resolveModelApiOverrideFromDiscovery(discoveryProviderApi, effectiveDiscovery, modelId, undefined, false);
|
|
734
|
+
const bundledModelApi = getKnownProviderModelApi(providerName, modelId);
|
|
735
|
+
// An OpenAI models-list row may override both its ID-based family and the
|
|
736
|
+
// provider default via `owned_by`. Defer ambiguous timeout validation until
|
|
737
|
+
// discovery supplies that metadata; applying the override validates the
|
|
738
|
+
// resolved model API before it can take effect.
|
|
739
|
+
const deferTimeoutValidation =
|
|
740
|
+
!configuredModel &&
|
|
741
|
+
effectiveDiscovery?.type === "openai-models-list" &&
|
|
742
|
+
discoveryTimeoutApi === undefined &&
|
|
743
|
+
bundledModelApi === undefined;
|
|
626
744
|
const effectiveApi =
|
|
627
|
-
|
|
628
|
-
config.api ??
|
|
629
|
-
|
|
745
|
+
configuredModel?.api ??
|
|
746
|
+
(configuredModel ? config.api : undefined) ??
|
|
747
|
+
discoveryApi ??
|
|
748
|
+
bundledModelApi ??
|
|
749
|
+
config.api;
|
|
750
|
+
const effectiveTimeoutApi =
|
|
751
|
+
configuredModel?.api ??
|
|
752
|
+
(configuredModel ? config.api : undefined) ??
|
|
753
|
+
discoveryTimeoutApi ??
|
|
754
|
+
bundledModelApi ??
|
|
755
|
+
(deferTimeoutValidation
|
|
756
|
+
? undefined
|
|
757
|
+
: (config.api ?? (config.openaiCompat ? "openai-completions" : undefined)));
|
|
630
758
|
if (override.compat?.supportsResponsesSessionAffinity !== undefined) {
|
|
631
759
|
assertResponsesSessionAffinitySupported(
|
|
632
760
|
providerName,
|
|
@@ -635,6 +763,21 @@ function validateProviderConfiguration(
|
|
|
635
763
|
`modelOverrides ${modelId} "compat.supportsResponsesSessionAffinity"`,
|
|
636
764
|
);
|
|
637
765
|
}
|
|
766
|
+
if (override.streamIdleTimeoutMs !== undefined) {
|
|
767
|
+
if (effectiveTimeoutApi) {
|
|
768
|
+
assertRequestTransformSupportedForModelApi(
|
|
769
|
+
providerName,
|
|
770
|
+
modelId,
|
|
771
|
+
effectiveTimeoutApi,
|
|
772
|
+
'modelOverrides "streamIdleTimeoutMs"',
|
|
773
|
+
);
|
|
774
|
+
} else if (!deferTimeoutValidation) {
|
|
775
|
+
assertRequestTransformSupportedForKnownProvider(
|
|
776
|
+
providerName,
|
|
777
|
+
`modelOverrides ${modelId} "streamIdleTimeoutMs"`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
638
781
|
if (!override.requestTransform) continue;
|
|
639
782
|
if (effectiveApi) {
|
|
640
783
|
assertRequestTransformSupportedForModelApi(
|
|
@@ -657,6 +800,54 @@ function validateProviderConfiguration(
|
|
|
657
800
|
assertRequestTransformSupportedForKnownProvider(providerName, '"requestTransform"');
|
|
658
801
|
}
|
|
659
802
|
}
|
|
803
|
+
if (config.streamIdleTimeoutMs !== undefined) {
|
|
804
|
+
const bundledApis = getKnownProviderApis(providerName);
|
|
805
|
+
const hasCompatibleBundledApi = [...bundledApis].some(isRequestTransformApi);
|
|
806
|
+
const hasCompatibleModelApi = models.some(model => {
|
|
807
|
+
const api = model.api ?? config.api;
|
|
808
|
+
return api !== undefined && isRequestTransformApi(api);
|
|
809
|
+
});
|
|
810
|
+
const hasCompatibleDiscoveryApi =
|
|
811
|
+
config.openaiCompat !== undefined ||
|
|
812
|
+
(config.discovery !== undefined &&
|
|
813
|
+
((configuredApi !== undefined && isRequestTransformApi(configuredApi)) ||
|
|
814
|
+
Object.values(config.discovery.apiByModelPrefix ?? {}).some(isRequestTransformApi)));
|
|
815
|
+
const implicitDiscoveryApi =
|
|
816
|
+
(config.api as Api | undefined) ??
|
|
817
|
+
dominantOpenAIFamilyModelApi(models) ??
|
|
818
|
+
(config.openaiCompat ? "openai-completions" : undefined);
|
|
819
|
+
const implicitDiscoveryBaseUrl = config.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName);
|
|
820
|
+
const hasCompatibleImplicitDiscoveryApi =
|
|
821
|
+
!config.discovery &&
|
|
822
|
+
(config.openaiCompat !== undefined ||
|
|
823
|
+
(providerName !== "vllm" &&
|
|
824
|
+
providerName !== "sglang" &&
|
|
825
|
+
implicitDiscoveryApi !== undefined &&
|
|
826
|
+
isOpenAIFamilyApi(implicitDiscoveryApi) &&
|
|
827
|
+
implicitDiscoveryBaseUrl !== undefined));
|
|
828
|
+
const hasCompatibleDiscoveryRoute = hasCompatibleDiscoveryApi || hasCompatibleImplicitDiscoveryApi;
|
|
829
|
+
if (models.length > 0) {
|
|
830
|
+
if (!hasCompatibleModelApi && !hasCompatibleBundledApi && !hasCompatibleDiscoveryRoute) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Provider ${providerName}: "streamIdleTimeoutMs" is only supported with openai-completions or openai-responses APIs.`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
if (!hasCompatibleBundledApi && !hasCompatibleDiscoveryRoute) {
|
|
837
|
+
if (bundledApis.size > 0) {
|
|
838
|
+
assertStreamIdleTimeoutSupportedForKnownProvider(providerName, '"streamIdleTimeoutMs"');
|
|
839
|
+
} else if (configuredApi) {
|
|
840
|
+
if (!isRequestTransformApi(configuredApi)) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
`Provider ${providerName}: "streamIdleTimeoutMs" is only supported with openai-completions or openai-responses APIs.`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
} else {
|
|
846
|
+
assertStreamIdleTimeoutSupportedForKnownProvider(providerName, '"streamIdleTimeoutMs"');
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
660
851
|
|
|
661
852
|
for (const modelDef of models) {
|
|
662
853
|
if (!hasProviderApi && !modelDef.api) {
|
|
@@ -670,6 +861,14 @@ function validateProviderConfiguration(
|
|
|
670
861
|
throw new Error(`Provider ${providerName}: model missing "id"`);
|
|
671
862
|
}
|
|
672
863
|
const effectiveApi = modelDef.api ?? config.api;
|
|
864
|
+
if (modelDef.streamIdleTimeoutMs !== undefined && effectiveApi) {
|
|
865
|
+
assertRequestTransformSupportedForModelApi(
|
|
866
|
+
providerName,
|
|
867
|
+
modelDef.id,
|
|
868
|
+
effectiveApi,
|
|
869
|
+
'model "streamIdleTimeoutMs"',
|
|
870
|
+
);
|
|
871
|
+
}
|
|
673
872
|
const modelCompat = modelDef.compat;
|
|
674
873
|
if (modelCompat && "supportsResponsesSessionAffinity" in modelCompat) {
|
|
675
874
|
assertResponsesSessionAffinitySupported(
|
|
@@ -706,34 +905,83 @@ function validateProviderConfiguration(
|
|
|
706
905
|
}
|
|
707
906
|
}
|
|
708
907
|
|
|
908
|
+
function validateModelsConfig(config: ModelsConfig): void {
|
|
909
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
910
|
+
validateProviderConfiguration(
|
|
911
|
+
providerName,
|
|
912
|
+
{
|
|
913
|
+
baseUrl: providerConfig.baseUrl,
|
|
914
|
+
headers: providerConfig.headers,
|
|
915
|
+
apiKey: providerConfig.apiKey,
|
|
916
|
+
apiKeyEnv: providerConfig.apiKeyEnv,
|
|
917
|
+
api: providerConfig.api as Api | undefined,
|
|
918
|
+
transport: providerConfig.transport,
|
|
919
|
+
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
|
|
920
|
+
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
|
|
921
|
+
compat: providerConfig.compat,
|
|
922
|
+
requestTransform: providerConfig.requestTransform,
|
|
923
|
+
disableStrictTools: providerConfig.disableStrictTools,
|
|
924
|
+
cacheRetention: providerConfig.cacheRetention,
|
|
925
|
+
streamIdleTimeoutMs: providerConfig.streamIdleTimeoutMs,
|
|
926
|
+
openaiCompat: providerConfig.openaiCompat,
|
|
927
|
+
modelOverrides: providerConfig.modelOverrides,
|
|
928
|
+
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
929
|
+
},
|
|
930
|
+
"models-config",
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
709
935
|
export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
|
|
710
936
|
"models",
|
|
711
|
-
|
|
712
|
-
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
713
|
-
validateProviderConfiguration(
|
|
714
|
-
providerName,
|
|
715
|
-
{
|
|
716
|
-
baseUrl: providerConfig.baseUrl,
|
|
717
|
-
headers: providerConfig.headers,
|
|
718
|
-
apiKey: providerConfig.apiKey,
|
|
719
|
-
apiKeyEnv: providerConfig.apiKeyEnv,
|
|
720
|
-
api: providerConfig.api as Api | undefined,
|
|
721
|
-
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
|
|
722
|
-
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
|
|
723
|
-
compat: providerConfig.compat,
|
|
724
|
-
requestTransform: providerConfig.requestTransform,
|
|
725
|
-
disableStrictTools: providerConfig.disableStrictTools,
|
|
726
|
-
cacheRetention: providerConfig.cacheRetention,
|
|
727
|
-
openaiCompat: providerConfig.openaiCompat,
|
|
728
|
-
modelOverrides: providerConfig.modelOverrides,
|
|
729
|
-
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
730
|
-
},
|
|
731
|
-
"models-config",
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
},
|
|
937
|
+
validateModelsConfig,
|
|
735
938
|
);
|
|
736
939
|
|
|
940
|
+
function parseModelsConfigSnapshot(snapshot: { path: string; text: string | null }): LoadResult<ModelsConfig> {
|
|
941
|
+
if (snapshot.text === null) return { status: "not-found" };
|
|
942
|
+
try {
|
|
943
|
+
const content = snapshot.text.trim();
|
|
944
|
+
let raw: unknown;
|
|
945
|
+
if (snapshot.path.endsWith(".json") || snapshot.path.endsWith(".jsonc")) {
|
|
946
|
+
raw = JSONC.parse(content);
|
|
947
|
+
} else if (snapshot.path.endsWith(".yml") || snapshot.path.endsWith(".yaml")) {
|
|
948
|
+
raw = YAML.parse(content);
|
|
949
|
+
} else {
|
|
950
|
+
throw new Error(`Invalid config file path: ${snapshot.path}`);
|
|
951
|
+
}
|
|
952
|
+
const parsed = ModelsConfigSchema.safeParse(raw);
|
|
953
|
+
if (!parsed.success) {
|
|
954
|
+
return {
|
|
955
|
+
status: "error",
|
|
956
|
+
error: new ConfigError(
|
|
957
|
+
"models",
|
|
958
|
+
parsed.error.issues.slice(0, 50).map(issue => ({
|
|
959
|
+
instancePath: issue.path.length === 0 ? "" : `/${issue.path.map(String).join("/")}`,
|
|
960
|
+
message: issue.message,
|
|
961
|
+
})),
|
|
962
|
+
),
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
validateModelsConfig(parsed.data);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
return {
|
|
969
|
+
status: "error",
|
|
970
|
+
error:
|
|
971
|
+
error instanceof ConfigError
|
|
972
|
+
? error
|
|
973
|
+
: new ConfigError("models", undefined, { err: error, stage: "Validate(models)" }),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
return { status: "ok", value: parsed.data };
|
|
977
|
+
} catch (error) {
|
|
978
|
+
return {
|
|
979
|
+
status: "error",
|
|
980
|
+
error: new ConfigError("models", undefined, { err: error, stage: "Unexpected" }),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
737
985
|
/** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
|
|
738
986
|
interface ProviderOverride {
|
|
739
987
|
api?: Api;
|
|
@@ -745,6 +993,7 @@ interface ProviderOverride {
|
|
|
745
993
|
transport?: Model<Api>["transport"];
|
|
746
994
|
requestTransform?: ModelRequestTransform;
|
|
747
995
|
cacheRetention?: CacheRetention;
|
|
996
|
+
streamIdleTimeoutMs?: number;
|
|
748
997
|
isOAuth?: boolean;
|
|
749
998
|
}
|
|
750
999
|
|
|
@@ -778,6 +1027,7 @@ function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Parti
|
|
|
778
1027
|
contextWindow: model.contextWindow,
|
|
779
1028
|
maxTokens: model.maxTokens,
|
|
780
1029
|
};
|
|
1030
|
+
if (model.streamIdleTimeoutMs !== undefined) metadata.streamIdleTimeoutMs = model.streamIdleTimeoutMs;
|
|
781
1031
|
if (model.longContextPricing !== undefined) metadata.longContextPricing = model.longContextPricing;
|
|
782
1032
|
if (model.premiumMultiplier !== undefined) metadata.premiumMultiplier = model.premiumMultiplier;
|
|
783
1033
|
if (model.priority !== undefined) metadata.priority = model.priority;
|
|
@@ -866,7 +1116,8 @@ function filterMaterializedRegistryProfiles(
|
|
|
866
1116
|
type SpecialProviderDescriptor = {
|
|
867
1117
|
providerId: string;
|
|
868
1118
|
resolveKey: (value: string | undefined) => string | undefined;
|
|
869
|
-
|
|
1119
|
+
resolveProjectId?: (key: string, rawValue: string | undefined) => string | undefined;
|
|
1120
|
+
createOptions: (key: string, projectId?: string) => ModelManagerOptions<Api>;
|
|
870
1121
|
};
|
|
871
1122
|
|
|
872
1123
|
const PROVIDER_BASE_URL_ENV_ALIASES: Record<string, readonly string[]> = {
|
|
@@ -941,10 +1192,15 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
941
1192
|
existing: Model<Api> | undefined,
|
|
942
1193
|
providerOverride?: Pick<
|
|
943
1194
|
ProviderOverride,
|
|
944
|
-
"baseUrl" | "headers" | "transport" | "requestTransform" | "cacheRetention"
|
|
1195
|
+
"baseUrl" | "headers" | "transport" | "requestTransform" | "cacheRetention" | "streamIdleTimeoutMs"
|
|
945
1196
|
>,
|
|
946
1197
|
): Model<TApi> {
|
|
947
1198
|
if (existing) {
|
|
1199
|
+
const streamIdleTimeoutMs =
|
|
1200
|
+
model.streamIdleTimeoutMs ??
|
|
1201
|
+
(isRequestTransformApi(model.api)
|
|
1202
|
+
? (existing.streamIdleTimeoutMs ?? providerOverride?.streamIdleTimeoutMs)
|
|
1203
|
+
: undefined);
|
|
948
1204
|
return {
|
|
949
1205
|
...model,
|
|
950
1206
|
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
|
@@ -954,15 +1210,20 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
954
1210
|
providerOverride?.requestTransform,
|
|
955
1211
|
),
|
|
956
1212
|
cacheRetention: model.cacheRetention ?? existing.cacheRetention ?? providerOverride?.cacheRetention,
|
|
1213
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
957
1214
|
};
|
|
958
1215
|
}
|
|
959
1216
|
if (providerOverride) {
|
|
1217
|
+
const streamIdleTimeoutMs =
|
|
1218
|
+
model.streamIdleTimeoutMs ??
|
|
1219
|
+
(isRequestTransformApi(model.api) ? providerOverride.streamIdleTimeoutMs : undefined);
|
|
960
1220
|
return {
|
|
961
1221
|
...model,
|
|
962
1222
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
963
1223
|
headers: mergeCaseInsensitiveHeaders(model.headers, providerOverride.headers),
|
|
964
1224
|
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
965
1225
|
requestTransform: mergeRequestTransform(model.requestTransform, providerOverride.requestTransform),
|
|
1226
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
966
1227
|
};
|
|
967
1228
|
}
|
|
968
1229
|
return model;
|
|
@@ -1038,17 +1299,33 @@ function fingerprintConfiguredDiscoveryRequestShape(
|
|
|
1038
1299
|
return crypto.createHash("sha256").update("gajae:model-discovery-provenance\0").update(context).digest("hex");
|
|
1039
1300
|
}
|
|
1040
1301
|
|
|
1041
|
-
function fingerprintDescriptorDiscoveryProvenance(
|
|
1302
|
+
function fingerprintDescriptorDiscoveryProvenance(
|
|
1303
|
+
providerId: string,
|
|
1304
|
+
authEvidence: string,
|
|
1305
|
+
endpoint: string,
|
|
1306
|
+
projectId?: string,
|
|
1307
|
+
): string | undefined {
|
|
1042
1308
|
try {
|
|
1043
1309
|
const parsed = new URL(endpoint);
|
|
1044
1310
|
if (parsed.username || parsed.password || parsed.search) return undefined;
|
|
1045
1311
|
} catch {
|
|
1046
1312
|
return undefined;
|
|
1047
1313
|
}
|
|
1314
|
+
const discoveryIdentity =
|
|
1315
|
+
providerId === "google-antigravity" || providerId === "google-gemini-cli"
|
|
1316
|
+
? getAntigravityDiscoveryUserAgent()
|
|
1317
|
+
: undefined;
|
|
1048
1318
|
return crypto
|
|
1049
1319
|
.createHash("sha256")
|
|
1050
1320
|
.update("gajae:model-discovery-provenance\0")
|
|
1051
|
-
.update(
|
|
1321
|
+
.update(
|
|
1322
|
+
JSON.stringify({
|
|
1323
|
+
authEvidence,
|
|
1324
|
+
endpoint,
|
|
1325
|
+
...(projectId !== undefined ? { projectId } : {}),
|
|
1326
|
+
...(discoveryIdentity !== undefined ? { discoveryIdentity } : {}),
|
|
1327
|
+
}),
|
|
1328
|
+
)
|
|
1052
1329
|
.digest("hex");
|
|
1053
1330
|
}
|
|
1054
1331
|
|
|
@@ -1185,6 +1462,48 @@ function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: stri
|
|
|
1185
1462
|
return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
|
|
1186
1463
|
}
|
|
1187
1464
|
|
|
1465
|
+
/** Internal pure parser shared by discovery; exported for regression coverage. */
|
|
1466
|
+
export function extractGoogleOAuthProjectId(value: string | undefined, accessToken: string): string | undefined {
|
|
1467
|
+
if (!value) return undefined;
|
|
1468
|
+
try {
|
|
1469
|
+
const parsed = JSON.parse(value) as { token?: unknown; projectId?: unknown; project_id?: unknown };
|
|
1470
|
+
if (typeof parsed.token !== "string" || parsed.token.trim() !== accessToken) return undefined;
|
|
1471
|
+
const projectId = typeof parsed.projectId === "string" ? parsed.projectId : parsed.project_id;
|
|
1472
|
+
return typeof projectId === "string" ? projectId.trim() || undefined : undefined;
|
|
1473
|
+
} catch {
|
|
1474
|
+
return undefined;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function resolveGoogleOAuthProjectId(
|
|
1479
|
+
authStorage: AuthStorage,
|
|
1480
|
+
provider: string,
|
|
1481
|
+
accessToken: string,
|
|
1482
|
+
rawValue?: string,
|
|
1483
|
+
sessionId?: string,
|
|
1484
|
+
owner?: object,
|
|
1485
|
+
): string | undefined {
|
|
1486
|
+
const configuredProjectId = extractGoogleOAuthProjectId(rawValue, accessToken);
|
|
1487
|
+
if (authStorage.getEffectiveCredentialType(provider, sessionId, owner ? { owner } : undefined) !== "oauth") {
|
|
1488
|
+
return configuredProjectId;
|
|
1489
|
+
}
|
|
1490
|
+
const selector = authStorage.resolveEffectiveCredentialSelector(provider, sessionId);
|
|
1491
|
+
if (selector) {
|
|
1492
|
+
const selectedCredential = authStorage.getOAuthCredential(provider, sessionId, owner ? { owner } : undefined);
|
|
1493
|
+
if (selectedCredential?.access !== accessToken) return configuredProjectId;
|
|
1494
|
+
return selectedCredential.projectId?.trim() || configuredProjectId;
|
|
1495
|
+
}
|
|
1496
|
+
const matchingCredentials = getOAuthCredentialsForProvider(authStorage, provider).filter(
|
|
1497
|
+
credential => credential.access === accessToken,
|
|
1498
|
+
);
|
|
1499
|
+
if (matchingCredentials.length === 0) return configuredProjectId;
|
|
1500
|
+
const projectId = matchingCredentials[0]?.projectId?.trim();
|
|
1501
|
+
if (matchingCredentials.some(credential => (credential.projectId?.trim() ?? "") !== (projectId ?? ""))) {
|
|
1502
|
+
return configuredProjectId;
|
|
1503
|
+
}
|
|
1504
|
+
return projectId || configuredProjectId;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1188
1507
|
function resolveOAuthAccountIdForAccessToken(
|
|
1189
1508
|
authStorage: AuthStorage,
|
|
1190
1509
|
provider: string,
|
|
@@ -1278,6 +1597,20 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
|
|
1278
1597
|
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
|
1279
1598
|
if (override.output !== undefined) result.output = override.output as ("text" | "image")[];
|
|
1280
1599
|
if (override.cacheRetention !== undefined) result.cacheRetention = override.cacheRetention;
|
|
1600
|
+
if (override.streamIdleTimeoutMs !== undefined) {
|
|
1601
|
+
if (isRequestTransformApi(model.api)) {
|
|
1602
|
+
result.streamIdleTimeoutMs = override.streamIdleTimeoutMs;
|
|
1603
|
+
} else {
|
|
1604
|
+
// Discovery ownership can reveal an incompatible route after config
|
|
1605
|
+
// validation, including while hydrating a cached model at startup.
|
|
1606
|
+
// Reject only this override so the otherwise-valid model stays usable.
|
|
1607
|
+
logger.warn("model stream idle timeout override ignored for incompatible API", {
|
|
1608
|
+
model: model.id,
|
|
1609
|
+
provider: model.provider,
|
|
1610
|
+
api: model.api,
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1281
1614
|
const contextWindowOverride = toPositiveNumberOrUndefined(override.contextWindow);
|
|
1282
1615
|
if (contextWindowOverride !== undefined) {
|
|
1283
1616
|
result.contextWindow = contextWindowOverride;
|
|
@@ -1367,6 +1700,7 @@ interface CustomModelDefinitionLike {
|
|
|
1367
1700
|
wireModelId?: string;
|
|
1368
1701
|
requestTransform?: ModelRequestTransform;
|
|
1369
1702
|
cacheRetention?: CacheRetention;
|
|
1703
|
+
streamIdleTimeoutMs?: number;
|
|
1370
1704
|
}
|
|
1371
1705
|
|
|
1372
1706
|
interface CustomModelBuildOptions {
|
|
@@ -1394,6 +1728,7 @@ type CustomModelOverlay = {
|
|
|
1394
1728
|
wireModelId?: string;
|
|
1395
1729
|
requestTransform?: ModelRequestTransform;
|
|
1396
1730
|
cacheRetention?: CacheRetention;
|
|
1731
|
+
streamIdleTimeoutMs?: number;
|
|
1397
1732
|
transport?: Model<Api>["transport"];
|
|
1398
1733
|
isOAuth?: boolean;
|
|
1399
1734
|
};
|
|
@@ -1508,9 +1843,13 @@ function buildCustomModelOverlay(
|
|
|
1508
1843
|
providerCacheRetention: CacheRetention | undefined,
|
|
1509
1844
|
providerTransport: Model<Api>["transport"] | undefined,
|
|
1510
1845
|
modelDef: CustomModelDefinitionLike,
|
|
1846
|
+
providerStreamIdleTimeoutMs?: number,
|
|
1511
1847
|
): CustomModelOverlay | undefined {
|
|
1512
1848
|
const api = modelDef.api ?? providerApi;
|
|
1513
1849
|
if (!api) return undefined;
|
|
1850
|
+
const streamIdleTimeoutMs = isRequestTransformApi(api)
|
|
1851
|
+
? (modelDef.streamIdleTimeoutMs ?? providerStreamIdleTimeoutMs)
|
|
1852
|
+
: undefined;
|
|
1514
1853
|
return {
|
|
1515
1854
|
id: modelDef.id,
|
|
1516
1855
|
provider: providerName,
|
|
@@ -1535,6 +1874,7 @@ function buildCustomModelOverlay(
|
|
|
1535
1874
|
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
1536
1875
|
premiumMultiplier: modelDef.premiumMultiplier,
|
|
1537
1876
|
cacheRetention: modelDef.cacheRetention ?? providerCacheRetention,
|
|
1877
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
1538
1878
|
transport: providerTransport,
|
|
1539
1879
|
isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
|
|
1540
1880
|
};
|
|
@@ -1671,6 +2011,9 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
1671
2011
|
wireModelId: resolvedModel.wireModelId,
|
|
1672
2012
|
requestTransform: resolvedModel.requestTransform,
|
|
1673
2013
|
cacheRetention: resolvedModel.cacheRetention ?? reference?.cacheRetention,
|
|
2014
|
+
...(resolvedModel.streamIdleTimeoutMs !== undefined
|
|
2015
|
+
? { streamIdleTimeoutMs: resolvedModel.streamIdleTimeoutMs }
|
|
2016
|
+
: {}),
|
|
1674
2017
|
transport: resolvedModel.transport,
|
|
1675
2018
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
1676
2019
|
isOAuth: resolvedModel.isOAuth,
|
|
@@ -1727,6 +2070,12 @@ interface ModelManagerDiscoveryOptions {
|
|
|
1727
2070
|
apiKey: string | undefined;
|
|
1728
2071
|
endpoint: string;
|
|
1729
2072
|
endpointContainsUserinfo: boolean;
|
|
2073
|
+
projectId?: string;
|
|
2074
|
+
isCurrentContext?: () => boolean;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
interface GoogleSessionDiscoveryCatalog {
|
|
2078
|
+
models: Model<Api>[];
|
|
1730
2079
|
}
|
|
1731
2080
|
|
|
1732
2081
|
interface ConfiguredDiscoveryResult {
|
|
@@ -1742,6 +2091,201 @@ interface ConfiguredDiscoveryResult {
|
|
|
1742
2091
|
|
|
1743
2092
|
type ProviderRefreshFence = { providerId: string; generation: number } | { generations: ReadonlyMap<string, number> };
|
|
1744
2093
|
|
|
2094
|
+
interface StagedConfigApiKey {
|
|
2095
|
+
readonly apiKey: string;
|
|
2096
|
+
readonly envSourced: boolean;
|
|
2097
|
+
// The staged AuthStorage adapter needs only the first two fields; registry-owned entries
|
|
2098
|
+
// also track whether the key came from config, runtime registration, or both.
|
|
2099
|
+
readonly configSourced?: boolean;
|
|
2100
|
+
readonly runtimeSourced?: boolean;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
interface StagedAuthStorageState {
|
|
2104
|
+
readonly authStorage: AuthStorage;
|
|
2105
|
+
readonly configApiKeys: Map<object, Map<string, StagedConfigApiKey>>;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function createStagedAuthStorage(base: AuthStorage): StagedAuthStorageState {
|
|
2109
|
+
const configApiKeys = new Map<object, Map<string, StagedConfigApiKey>>();
|
|
2110
|
+
const fallbackResolvers = new Map<object, (provider: string) => string | undefined>();
|
|
2111
|
+
const lookupOwner = {};
|
|
2112
|
+
const getConfigApiKey = (provider: string, owner: object | undefined) =>
|
|
2113
|
+
(owner && configApiKeys.get(owner)?.get(resolveOAuthStorageProvider(provider))) || undefined;
|
|
2114
|
+
const peekApiKey = async (
|
|
2115
|
+
provider: string,
|
|
2116
|
+
options?: AuthApiKeyOptions & { sessionId?: string },
|
|
2117
|
+
): Promise<string | undefined> => {
|
|
2118
|
+
if (base.hasRuntimeApiKey(provider)) return base.peekApiKey(provider, options);
|
|
2119
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2120
|
+
if (
|
|
2121
|
+
configured &&
|
|
2122
|
+
base.resolveEffectiveCredentialSelector(provider, options?.sessionId, options?.credentialSelector)
|
|
2123
|
+
) {
|
|
2124
|
+
throw new Error(
|
|
2125
|
+
`Credential selector cannot be used for ${provider} while a config API key override is active`,
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
if (configured && !configured.envSourced) return configured.apiKey;
|
|
2129
|
+
if (configured?.envSourced) {
|
|
2130
|
+
const credentialType = base.getEffectiveCredentialType(provider, options?.sessionId, {
|
|
2131
|
+
owner: lookupOwner,
|
|
2132
|
+
credentialSelector: options?.credentialSelector,
|
|
2133
|
+
});
|
|
2134
|
+
if (credentialType !== "api_key") return configured.apiKey;
|
|
2135
|
+
}
|
|
2136
|
+
const storedKey = await base.peekApiKey(provider, { ...options, owner: lookupOwner });
|
|
2137
|
+
if (storedKey !== undefined) return storedKey;
|
|
2138
|
+
if (configured?.envSourced) return configured.apiKey;
|
|
2139
|
+
return (options?.owner && fallbackResolvers.get(options.owner)?.(provider)) ?? undefined;
|
|
2140
|
+
};
|
|
2141
|
+
const staged = new Proxy(base, {
|
|
2142
|
+
get(target, property) {
|
|
2143
|
+
if (property === "setConfigApiKey") {
|
|
2144
|
+
return (provider: string, apiKey: string, options: { envSourced?: boolean; owner?: object } = {}) => {
|
|
2145
|
+
if (!options.owner) return;
|
|
2146
|
+
const keys = configApiKeys.get(options.owner) ?? new Map<string, StagedConfigApiKey>();
|
|
2147
|
+
keys.set(resolveOAuthStorageProvider(provider), {
|
|
2148
|
+
apiKey,
|
|
2149
|
+
envSourced: options.envSourced === true,
|
|
2150
|
+
});
|
|
2151
|
+
configApiKeys.set(options.owner, keys);
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
if (property === "removeConfigApiKey") {
|
|
2155
|
+
return (provider: string, owner?: object) => {
|
|
2156
|
+
if (owner) configApiKeys.get(owner)?.delete(resolveOAuthStorageProvider(provider));
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
if (property === "clearConfigApiKeys") {
|
|
2160
|
+
return (owner?: object) => {
|
|
2161
|
+
if (owner) configApiKeys.delete(owner);
|
|
2162
|
+
else configApiKeys.clear();
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
if (property === "setFallbackResolver") {
|
|
2166
|
+
return (resolver: (provider: string) => string | undefined, owner?: object) => {
|
|
2167
|
+
if (!owner) return () => {};
|
|
2168
|
+
fallbackResolvers.set(owner, resolver);
|
|
2169
|
+
return () => {
|
|
2170
|
+
if (fallbackResolvers.get(owner) === resolver) fallbackResolvers.delete(owner);
|
|
2171
|
+
};
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
if (property === "onGenerationChanged") return () => () => {};
|
|
2175
|
+
if (property === "peekApiKey") return peekApiKey;
|
|
2176
|
+
if (property === "getApiKey")
|
|
2177
|
+
return (provider: string, sessionId?: string, options?: AuthApiKeyOptions) =>
|
|
2178
|
+
peekApiKey(provider, { ...options, sessionId });
|
|
2179
|
+
if (property === "hasConfigApiKey") {
|
|
2180
|
+
return (provider: string, owner?: object) =>
|
|
2181
|
+
getConfigApiKey(provider, owner) !== undefined || base.hasConfigApiKey(provider, lookupOwner);
|
|
2182
|
+
}
|
|
2183
|
+
if (property === "hasAuth") {
|
|
2184
|
+
return (provider: string, sessionId?: string, options?: { owner?: object }) => {
|
|
2185
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2186
|
+
if (configured && base.resolveEffectiveCredentialSelector(provider, sessionId)) return false;
|
|
2187
|
+
return configured !== undefined || base.hasAuth(provider, sessionId, { owner: lookupOwner });
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
if (property === "hasUsableAuth") {
|
|
2191
|
+
return (
|
|
2192
|
+
provider: string,
|
|
2193
|
+
options?: { owner?: object; sessionId?: string; credentialSelector?: AuthCredentialSelector },
|
|
2194
|
+
) => {
|
|
2195
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2196
|
+
if (
|
|
2197
|
+
configured &&
|
|
2198
|
+
base.resolveEffectiveCredentialSelector(provider, options?.sessionId, options?.credentialSelector)
|
|
2199
|
+
) {
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2202
|
+
return (
|
|
2203
|
+
configured !== undefined ||
|
|
2204
|
+
base.hasUsableAuth(provider, {
|
|
2205
|
+
owner: lookupOwner,
|
|
2206
|
+
sessionId: options?.sessionId,
|
|
2207
|
+
credentialSelector: options?.credentialSelector,
|
|
2208
|
+
})
|
|
2209
|
+
);
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
if (property === "getEffectiveCredentialType") {
|
|
2213
|
+
return (
|
|
2214
|
+
provider: string,
|
|
2215
|
+
sessionId?: string,
|
|
2216
|
+
options?: { owner?: object; credentialSelector?: AuthCredentialSelector },
|
|
2217
|
+
) => {
|
|
2218
|
+
if (base.hasRuntimeApiKey(provider))
|
|
2219
|
+
return base.getEffectiveCredentialType(provider, sessionId, options);
|
|
2220
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2221
|
+
if (
|
|
2222
|
+
configured &&
|
|
2223
|
+
base.resolveEffectiveCredentialSelector(provider, sessionId, options?.credentialSelector)
|
|
2224
|
+
) {
|
|
2225
|
+
return undefined;
|
|
2226
|
+
}
|
|
2227
|
+
if (
|
|
2228
|
+
configured &&
|
|
2229
|
+
!(configured.envSourced && base.getSessionCredentialType(provider, sessionId) === "api_key")
|
|
2230
|
+
) {
|
|
2231
|
+
return "api_key";
|
|
2232
|
+
}
|
|
2233
|
+
return base.getEffectiveCredentialType(provider, sessionId, {
|
|
2234
|
+
owner: lookupOwner,
|
|
2235
|
+
credentialSelector: options?.credentialSelector,
|
|
2236
|
+
});
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
if (property === "getProviderEvidenceGeneration") {
|
|
2240
|
+
return (provider: string, resolvedApiKey?: string, owner?: object) => {
|
|
2241
|
+
const configured = getConfigApiKey(provider, owner);
|
|
2242
|
+
const effectiveApiKey = base.hasRuntimeApiKey(provider)
|
|
2243
|
+
? resolvedApiKey
|
|
2244
|
+
: configured && !configured.envSourced
|
|
2245
|
+
? configured.apiKey
|
|
2246
|
+
: (resolvedApiKey ?? configured?.apiKey);
|
|
2247
|
+
return base.getProviderEvidenceGeneration(
|
|
2248
|
+
provider,
|
|
2249
|
+
effectiveApiKey,
|
|
2250
|
+
lookupOwner,
|
|
2251
|
+
configured ? { apiKey: configured.apiKey } : undefined,
|
|
2252
|
+
);
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
const value = Reflect.get(target, property, target);
|
|
2256
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
2257
|
+
},
|
|
2258
|
+
}) as AuthStorage;
|
|
2259
|
+
return { authStorage: staged, configApiKeys };
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
export interface ModelsConfigReloadCandidate {
|
|
2263
|
+
readonly valid: boolean;
|
|
2264
|
+
readonly changed: boolean;
|
|
2265
|
+
readonly diagnostics: { readonly valid: boolean; readonly errors: readonly unknown[] };
|
|
2266
|
+
readonly registry: ModelRegistry;
|
|
2267
|
+
isCurrent(): boolean;
|
|
2268
|
+
commit(): void;
|
|
2269
|
+
rollback(): void;
|
|
2270
|
+
finalize(): void;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
interface ModelRegistryInternalOptions {
|
|
2274
|
+
readonly configFile: ConfigFile<ModelsConfig>;
|
|
2275
|
+
readonly configSource: LoadResult<ModelsConfig>;
|
|
2276
|
+
readonly runtimeSource: ModelRegistry;
|
|
2277
|
+
readonly acceptedPresets: AcceptedModelPresetRegistry;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
interface RegistryLeaseWaiter {
|
|
2281
|
+
readonly kind: "consumer" | "publication";
|
|
2282
|
+
readonly signal?: AbortSignal;
|
|
2283
|
+
readonly resolve: (release: () => void) => void;
|
|
2284
|
+
readonly reject: (reason: unknown) => void;
|
|
2285
|
+
settled: boolean;
|
|
2286
|
+
abort?: () => void;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
1745
2289
|
/**
|
|
1746
2290
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
1747
2291
|
*/
|
|
@@ -1754,9 +2298,11 @@ export class ModelRegistry {
|
|
|
1754
2298
|
bySelector: new Map(),
|
|
1755
2299
|
aliases: new Map(),
|
|
1756
2300
|
};
|
|
1757
|
-
#availableModelsCache
|
|
1758
|
-
|
|
1759
|
-
|
|
2301
|
+
#availableModelsCache = new Map<
|
|
2302
|
+
string,
|
|
2303
|
+
{ models: Model<Api>[]; disabledProviders: string; envFingerprint: string }
|
|
2304
|
+
>();
|
|
2305
|
+
#googleSessionDiscoveryCatalogs = new Map<string, Map<string, GoogleSessionDiscoveryCatalog>>();
|
|
1760
2306
|
#sessionCanonicalVariants = new Map<string, string>();
|
|
1761
2307
|
#customProviderApiKeys: Map<string, string> = new Map();
|
|
1762
2308
|
#customProviderApiKeyEnvNames: Map<string, string> = new Map();
|
|
@@ -1776,6 +2322,7 @@ export class ModelRegistry {
|
|
|
1776
2322
|
modelIds: ReadonlySet<string>;
|
|
1777
2323
|
authGeneration: string;
|
|
1778
2324
|
endpoint: string;
|
|
2325
|
+
projectId?: string;
|
|
1779
2326
|
}
|
|
1780
2327
|
>();
|
|
1781
2328
|
#descriptorDiscoveryGenerations = new Map<string, number>();
|
|
@@ -1793,9 +2340,14 @@ export class ModelRegistry {
|
|
|
1793
2340
|
#modelProfiles: Map<string, ModelProfileDefinition> = mergeModelProfiles();
|
|
1794
2341
|
#configError: ConfigError | undefined = undefined;
|
|
1795
2342
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
2343
|
+
#modelsConfigSource: LoadResult<ModelsConfig> | undefined;
|
|
2344
|
+
#acceptedConfigValue: ModelsConfig | undefined;
|
|
2345
|
+
#acceptedModelsConfigPresent = false;
|
|
2346
|
+
#acceptedPresets: AcceptedModelPresetRegistry | undefined;
|
|
1796
2347
|
#settings: Pick<Settings, "get" | "getGlobal">;
|
|
1797
2348
|
#ownershipSettings: Pick<Settings, "commitAtomicBatchWithCurrent"> | undefined;
|
|
1798
|
-
readonly #authStorageConfigOwner: object
|
|
2349
|
+
readonly #authStorageConfigOwner: object;
|
|
2350
|
+
#ownedConfigApiKeys = new Map<string, StagedConfigApiKey>();
|
|
1799
2351
|
#disposeAuthStorageFallbackResolver: (() => void) | undefined;
|
|
1800
2352
|
#lastStaticLoadMtime: number | null = null;
|
|
1801
2353
|
#lastStaticLoadEnvironmentFingerprint: string | undefined;
|
|
@@ -1812,7 +2364,7 @@ export class ModelRegistry {
|
|
|
1812
2364
|
#cacheDbPath?: string;
|
|
1813
2365
|
#suppressedSelectors: Map<string, number> = new Map();
|
|
1814
2366
|
#selectorCircuits: Map<string, SelectorCircuit> = new Map();
|
|
1815
|
-
#
|
|
2367
|
+
#backgroundRefreshes: Map<string, Promise<void>> = new Map();
|
|
1816
2368
|
#catalogMutationTail: Promise<void> = Promise.resolve();
|
|
1817
2369
|
#pendingCatalogMutations = 0;
|
|
1818
2370
|
#catalogRefreshGeneration = 0;
|
|
@@ -1837,6 +2389,11 @@ export class ModelRegistry {
|
|
|
1837
2389
|
#configuredApiKeyEnvNames: Set<string> = new Set();
|
|
1838
2390
|
#optionalAuthPreflightGenerations = new Map<string, number>();
|
|
1839
2391
|
#optionalAuthPreflightEpoch = 0;
|
|
2392
|
+
#stagedReloadCandidate = false;
|
|
2393
|
+
#leaseWaiters: RegistryLeaseWaiter[] = [];
|
|
2394
|
+
#activeConsumerLeases = 0;
|
|
2395
|
+
#publicationFenceHeld = false;
|
|
2396
|
+
#consumerContext = new AsyncLocalStorage<{ active: boolean }>();
|
|
1840
2397
|
|
|
1841
2398
|
/**
|
|
1842
2399
|
* @param authStorage - Auth storage for API key resolution
|
|
@@ -1846,15 +2403,22 @@ export class ModelRegistry {
|
|
|
1846
2403
|
modelsPath?: string,
|
|
1847
2404
|
registrySettings?: Pick<Settings, "get" | "getGlobal"> & Partial<Pick<Settings, "commitAtomicBatchWithCurrent">>,
|
|
1848
2405
|
modelPresetRegistryDependencies: ModelPresetRegistryDependencies = {},
|
|
2406
|
+
internalOptions?: ModelRegistryInternalOptions,
|
|
1849
2407
|
) {
|
|
2408
|
+
this.#authStorageConfigOwner = internalOptions ? internalOptions.runtimeSource.#authStorageConfigOwner : {};
|
|
1850
2409
|
this.#settings = registrySettings ?? settings;
|
|
1851
|
-
this.#ownershipSettings =
|
|
1852
|
-
?
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2410
|
+
this.#ownershipSettings = internalOptions
|
|
2411
|
+
? undefined
|
|
2412
|
+
: registrySettings
|
|
2413
|
+
? typeof registrySettings.commitAtomicBatchWithCurrent === "function"
|
|
2414
|
+
? (registrySettings as Pick<Settings, "commitAtomicBatchWithCurrent">)
|
|
2415
|
+
: undefined
|
|
2416
|
+
: settings;
|
|
1856
2417
|
const configuredAgentDir = path.resolve(modelPresetRegistryDependencies.agentDir ?? getAgentDir());
|
|
1857
|
-
this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
|
|
2418
|
+
this.#modelsConfigFile = internalOptions?.configFile ?? ModelsConfigFile.relocate(modelsPath);
|
|
2419
|
+
this.#modelsConfigSource = internalOptions?.configSource;
|
|
2420
|
+
this.#acceptedPresets = internalOptions?.acceptedPresets;
|
|
2421
|
+
this.#stagedReloadCandidate = internalOptions !== undefined;
|
|
1858
2422
|
this.#modelPresetRegistryAgentDir = modelPresetRegistryDependencies.agentDir
|
|
1859
2423
|
? configuredAgentDir
|
|
1860
2424
|
: modelsPath && path.isAbsolute(modelsPath)
|
|
@@ -1863,6 +2427,10 @@ export class ModelRegistry {
|
|
|
1863
2427
|
const { agentDir: _agentDir, ...registryDependencies } = modelPresetRegistryDependencies;
|
|
1864
2428
|
this.#modelPresetRegistryDependencies = registryDependencies;
|
|
1865
2429
|
this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
|
|
2430
|
+
if (internalOptions) {
|
|
2431
|
+
this.#copyRuntimeStateFrom(internalOptions.runtimeSource);
|
|
2432
|
+
this.#clearOwnedConfigApiKeys();
|
|
2433
|
+
}
|
|
1866
2434
|
// Set up fallback resolver for custom provider API keys
|
|
1867
2435
|
this.#disposeAuthStorageFallbackResolver = this.authStorage.setFallbackResolver(provider => {
|
|
1868
2436
|
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
@@ -1871,32 +2439,623 @@ export class ModelRegistry {
|
|
|
1871
2439
|
this.#unsubscribeAuthGeneration = this.authStorage.onGenerationChanged(() => this.#invalidateAvailableModels());
|
|
1872
2440
|
// Load models synchronously in constructor
|
|
1873
2441
|
this.#loadModels();
|
|
1874
|
-
this.#
|
|
2442
|
+
if (internalOptions) this.#reapplyRuntimeAuthConfiguration();
|
|
2443
|
+
if (!internalOptions) {
|
|
2444
|
+
this.#cancelModelPresetRegistryRefresh = refreshModelPresetRegistryInBackground(
|
|
2445
|
+
{
|
|
2446
|
+
...this.#modelPresetRegistryDependencies,
|
|
2447
|
+
agentDir: this.#modelPresetRegistryAgentDir,
|
|
2448
|
+
knownManifestSha256: this.#loadedModelPresetRegistryManifestSha256,
|
|
2449
|
+
},
|
|
2450
|
+
() => {
|
|
2451
|
+
void this.#enqueueCatalogMutation(() => {
|
|
2452
|
+
if (this.#disposed) return;
|
|
2453
|
+
this.#reloadStaticModels();
|
|
2454
|
+
this.#modelBindingsApplier.apply();
|
|
2455
|
+
this.#notifyCatalogChanged();
|
|
2456
|
+
}).catch(() => undefined);
|
|
2457
|
+
},
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
#copyRuntimeStateFrom(source: ModelRegistry): void {
|
|
2463
|
+
this.#sessionCanonicalVariants = new Map(source.#sessionCanonicalVariants);
|
|
2464
|
+
this.#googleSessionDiscoveryCatalogs = new Map(
|
|
2465
|
+
[...source.#googleSessionDiscoveryCatalogs].map(([provider, catalogs]) => [
|
|
2466
|
+
provider,
|
|
2467
|
+
new Map([...catalogs].map(([identity, catalog]) => [identity, { models: [...catalog.models] }])),
|
|
2468
|
+
]),
|
|
2469
|
+
);
|
|
2470
|
+
this.#suppressedSelectors = new Map(source.#suppressedSelectors);
|
|
2471
|
+
this.#selectorCircuits = new Map(
|
|
2472
|
+
[...source.#selectorCircuits].map(([selector, circuit]) => [selector, structuredClone(circuit)]),
|
|
2473
|
+
);
|
|
2474
|
+
this.#runtimeModelOverlays = [...source.#runtimeModelOverlays];
|
|
2475
|
+
this.#runtimeProviderApiKeys = new Map(source.#runtimeProviderApiKeys);
|
|
2476
|
+
this.#runtimeProviderResolvedApiKeys = new Map(source.#runtimeProviderResolvedApiKeys);
|
|
2477
|
+
this.#runtimeProviderCredentialInstalled = new Set(source.#runtimeProviderCredentialInstalled);
|
|
2478
|
+
this.#runtimeProviderApiKeyEnvNames = new Map(source.#runtimeProviderApiKeyEnvNames);
|
|
2479
|
+
this.#runtimeProviderOverrides = new Map(source.#runtimeProviderOverrides);
|
|
2480
|
+
this.#runtimeProviderAuthHeaders = new Map(source.#runtimeProviderAuthHeaders);
|
|
2481
|
+
this.#generatedAuthHeaderProviders = new Set(source.#generatedAuthHeaderProviders);
|
|
2482
|
+
this.#runtimeProvidersBySource = new Map(
|
|
2483
|
+
[...source.#runtimeProvidersBySource].map(([id, providers]) => [id, new Set(providers)]),
|
|
2484
|
+
);
|
|
2485
|
+
this.#runtimeProviderSourceByName = new Map(source.#runtimeProviderSourceByName);
|
|
2486
|
+
this.#registeredProviderSources = new Set(source.#registeredProviderSources);
|
|
2487
|
+
this.#descriptorDiscoveryEvidence = new Map(source.#descriptorDiscoveryEvidence);
|
|
2488
|
+
this.#descriptorDiscoveryGenerations = new Map(source.#descriptorDiscoveryGenerations);
|
|
2489
|
+
this.#configuredDiscoveryEvidence = new Map(source.#configuredDiscoveryEvidence);
|
|
2490
|
+
this.#providerEvidenceApiKeys = new Map(source.#providerEvidenceApiKeys);
|
|
2491
|
+
this.#credentiallessAuthFallbackProviders = new Map(source.#credentiallessAuthFallbackProviders);
|
|
2492
|
+
this.#optionalAuthPreflightGenerations = new Map(source.#optionalAuthPreflightGenerations);
|
|
2493
|
+
this.#optionalAuthPreflightEpoch = source.#optionalAuthPreflightEpoch;
|
|
2494
|
+
this.#ownedConfigApiKeys = new Map(source.#ownedConfigApiKeys);
|
|
2495
|
+
this.#acceptedPresets = source.#acceptedPresets;
|
|
2496
|
+
this.#loadedModelPresetRegistryManifestSha256 = source.#loadedModelPresetRegistryManifestSha256;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
#setOwnedConfigApiKey(
|
|
2500
|
+
provider: string,
|
|
2501
|
+
apiKey: string,
|
|
2502
|
+
envSourced = false,
|
|
2503
|
+
source: "config" | "runtime" = "config",
|
|
2504
|
+
): void {
|
|
2505
|
+
const key = resolveOAuthStorageProvider(provider);
|
|
2506
|
+
const previous = this.#ownedConfigApiKeys.get(key);
|
|
2507
|
+
this.#ownedConfigApiKeys.set(key, {
|
|
2508
|
+
apiKey,
|
|
2509
|
+
envSourced,
|
|
2510
|
+
configSourced: source === "config" || previous?.configSourced === true,
|
|
2511
|
+
runtimeSourced: source === "runtime" || previous?.runtimeSourced === true,
|
|
2512
|
+
});
|
|
2513
|
+
this.authStorage.setConfigApiKey(provider, apiKey, {
|
|
2514
|
+
envSourced,
|
|
2515
|
+
owner: this.#authStorageConfigOwner,
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
#removeOwnedConfigApiKey(provider: string): void {
|
|
2520
|
+
this.#ownedConfigApiKeys.delete(resolveOAuthStorageProvider(provider));
|
|
2521
|
+
this.authStorage.removeConfigApiKey(provider, this.#authStorageConfigOwner);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
#clearOwnedConfigApiKeys(): void {
|
|
2525
|
+
this.#ownedConfigApiKeys.clear();
|
|
2526
|
+
this.authStorage.clearConfigApiKeys(this.#authStorageConfigOwner);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
#reapplyRuntimeAuthConfiguration(): void {
|
|
2530
|
+
for (const [provider, apiKeyConfig] of this.#runtimeProviderApiKeys) {
|
|
2531
|
+
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
2532
|
+
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
2533
|
+
: resolveApiKeyConfig(apiKeyConfig);
|
|
2534
|
+
if (!resolved) continue;
|
|
2535
|
+
this.#customProviderApiKeys.set(provider, resolved);
|
|
2536
|
+
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
2537
|
+
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
2538
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
#swapReloadableState(other: ModelRegistry): void {
|
|
2543
|
+
[this.#models, other.#models] = [other.#models, this.#models];
|
|
2544
|
+
[this.#canonicalIndex, other.#canonicalIndex] = [other.#canonicalIndex, this.#canonicalIndex];
|
|
2545
|
+
[this.#availableModelsCache, other.#availableModelsCache] = [
|
|
2546
|
+
other.#availableModelsCache,
|
|
2547
|
+
this.#availableModelsCache,
|
|
2548
|
+
];
|
|
2549
|
+
[this.#googleSessionDiscoveryCatalogs, other.#googleSessionDiscoveryCatalogs] = [
|
|
2550
|
+
other.#googleSessionDiscoveryCatalogs,
|
|
2551
|
+
this.#googleSessionDiscoveryCatalogs,
|
|
2552
|
+
];
|
|
2553
|
+
[this.#customProviderApiKeys, other.#customProviderApiKeys] = [
|
|
2554
|
+
other.#customProviderApiKeys,
|
|
2555
|
+
this.#customProviderApiKeys,
|
|
2556
|
+
];
|
|
2557
|
+
[this.#customProviderApiKeyEnvNames, other.#customProviderApiKeyEnvNames] = [
|
|
2558
|
+
other.#customProviderApiKeyEnvNames,
|
|
2559
|
+
this.#customProviderApiKeyEnvNames,
|
|
2560
|
+
];
|
|
2561
|
+
[this.#customProviderAuthHeaders, other.#customProviderAuthHeaders] = [
|
|
2562
|
+
other.#customProviderAuthHeaders,
|
|
2563
|
+
this.#customProviderAuthHeaders,
|
|
2564
|
+
];
|
|
2565
|
+
[this.#providerWebSearchModes, other.#providerWebSearchModes] = [
|
|
2566
|
+
other.#providerWebSearchModes,
|
|
2567
|
+
this.#providerWebSearchModes,
|
|
2568
|
+
];
|
|
2569
|
+
[this.#keylessProviders, other.#keylessProviders] = [other.#keylessProviders, this.#keylessProviders];
|
|
2570
|
+
[this.#optionalAuthProviders, other.#optionalAuthProviders] = [
|
|
2571
|
+
other.#optionalAuthProviders,
|
|
2572
|
+
this.#optionalAuthProviders,
|
|
2573
|
+
];
|
|
2574
|
+
[this.#credentiallessAuthFallbackProviders, other.#credentiallessAuthFallbackProviders] = [
|
|
2575
|
+
other.#credentiallessAuthFallbackProviders,
|
|
2576
|
+
this.#credentiallessAuthFallbackProviders,
|
|
2577
|
+
];
|
|
2578
|
+
[this.#providerEvidenceApiKeys, other.#providerEvidenceApiKeys] = [
|
|
2579
|
+
other.#providerEvidenceApiKeys,
|
|
2580
|
+
this.#providerEvidenceApiKeys,
|
|
2581
|
+
];
|
|
2582
|
+
[this.#providerActivity, other.#providerActivity] = [other.#providerActivity, this.#providerActivity];
|
|
2583
|
+
[this.#configuredProviderIds, other.#configuredProviderIds] = [
|
|
2584
|
+
other.#configuredProviderIds,
|
|
2585
|
+
this.#configuredProviderIds,
|
|
2586
|
+
];
|
|
2587
|
+
[this.#configuredDiscoveryProviderIds, other.#configuredDiscoveryProviderIds] = [
|
|
2588
|
+
other.#configuredDiscoveryProviderIds,
|
|
2589
|
+
this.#configuredDiscoveryProviderIds,
|
|
2590
|
+
];
|
|
2591
|
+
[this.#descriptorDiscoveryEvidence, other.#descriptorDiscoveryEvidence] = [
|
|
2592
|
+
other.#descriptorDiscoveryEvidence,
|
|
2593
|
+
this.#descriptorDiscoveryEvidence,
|
|
2594
|
+
];
|
|
2595
|
+
[this.#descriptorDiscoveryGenerations, other.#descriptorDiscoveryGenerations] = [
|
|
2596
|
+
other.#descriptorDiscoveryGenerations,
|
|
2597
|
+
this.#descriptorDiscoveryGenerations,
|
|
2598
|
+
];
|
|
2599
|
+
[this.#configuredDiscoveryEvidence, other.#configuredDiscoveryEvidence] = [
|
|
2600
|
+
other.#configuredDiscoveryEvidence,
|
|
2601
|
+
this.#configuredDiscoveryEvidence,
|
|
2602
|
+
];
|
|
2603
|
+
[this.#discoveryManager, other.#discoveryManager] = [other.#discoveryManager, this.#discoveryManager];
|
|
2604
|
+
[this.#customModelOverlays, other.#customModelOverlays] = [other.#customModelOverlays, this.#customModelOverlays];
|
|
2605
|
+
[this.#providerOverrides, other.#providerOverrides] = [other.#providerOverrides, this.#providerOverrides];
|
|
2606
|
+
[this.#modelOverrides, other.#modelOverrides] = [other.#modelOverrides, this.#modelOverrides];
|
|
2607
|
+
[this.#codexContextWindowOverrides, other.#codexContextWindowOverrides] = [
|
|
2608
|
+
other.#codexContextWindowOverrides,
|
|
2609
|
+
this.#codexContextWindowOverrides,
|
|
2610
|
+
];
|
|
2611
|
+
[this.#equivalenceConfig, other.#equivalenceConfig] = [other.#equivalenceConfig, this.#equivalenceConfig];
|
|
2612
|
+
[this.#acceptedPresets, other.#acceptedPresets] = [other.#acceptedPresets, this.#acceptedPresets];
|
|
2613
|
+
[this.#modelProfiles, other.#modelProfiles] = [other.#modelProfiles, this.#modelProfiles];
|
|
2614
|
+
[this.#configError, other.#configError] = [other.#configError, this.#configError];
|
|
2615
|
+
[this.#ownedConfigApiKeys, other.#ownedConfigApiKeys] = [other.#ownedConfigApiKeys, this.#ownedConfigApiKeys];
|
|
2616
|
+
[this.#lastStaticLoadMtime, other.#lastStaticLoadMtime] = [other.#lastStaticLoadMtime, this.#lastStaticLoadMtime];
|
|
2617
|
+
[this.#lastStaticLoadEnvironmentFingerprint, other.#lastStaticLoadEnvironmentFingerprint] = [
|
|
2618
|
+
other.#lastStaticLoadEnvironmentFingerprint,
|
|
2619
|
+
this.#lastStaticLoadEnvironmentFingerprint,
|
|
2620
|
+
];
|
|
2621
|
+
[this.#lastModelPresetRegistryFingerprint, other.#lastModelPresetRegistryFingerprint] = [
|
|
2622
|
+
other.#lastModelPresetRegistryFingerprint,
|
|
2623
|
+
this.#lastModelPresetRegistryFingerprint,
|
|
2624
|
+
];
|
|
2625
|
+
[this.#loadedModelPresetRegistryManifestSha256, other.#loadedModelPresetRegistryManifestSha256] = [
|
|
2626
|
+
other.#loadedModelPresetRegistryManifestSha256,
|
|
2627
|
+
this.#loadedModelPresetRegistryManifestSha256,
|
|
2628
|
+
];
|
|
2629
|
+
[this.#staticModelsLoaded, other.#staticModelsLoaded] = [other.#staticModelsLoaded, this.#staticModelsLoaded];
|
|
2630
|
+
[this.#lastDisabledProviderKey, other.#lastDisabledProviderKey] = [
|
|
2631
|
+
other.#lastDisabledProviderKey,
|
|
2632
|
+
this.#lastDisabledProviderKey,
|
|
2633
|
+
];
|
|
2634
|
+
[this.#runtimeModelOverlays, other.#runtimeModelOverlays] = [
|
|
2635
|
+
other.#runtimeModelOverlays,
|
|
2636
|
+
this.#runtimeModelOverlays,
|
|
2637
|
+
];
|
|
2638
|
+
[this.#runtimeProviderApiKeys, other.#runtimeProviderApiKeys] = [
|
|
2639
|
+
other.#runtimeProviderApiKeys,
|
|
2640
|
+
this.#runtimeProviderApiKeys,
|
|
2641
|
+
];
|
|
2642
|
+
[this.#runtimeProviderResolvedApiKeys, other.#runtimeProviderResolvedApiKeys] = [
|
|
2643
|
+
other.#runtimeProviderResolvedApiKeys,
|
|
2644
|
+
this.#runtimeProviderResolvedApiKeys,
|
|
2645
|
+
];
|
|
2646
|
+
[this.#runtimeProviderCredentialInstalled, other.#runtimeProviderCredentialInstalled] = [
|
|
2647
|
+
other.#runtimeProviderCredentialInstalled,
|
|
2648
|
+
this.#runtimeProviderCredentialInstalled,
|
|
2649
|
+
];
|
|
2650
|
+
[this.#runtimeProviderApiKeyEnvNames, other.#runtimeProviderApiKeyEnvNames] = [
|
|
2651
|
+
other.#runtimeProviderApiKeyEnvNames,
|
|
2652
|
+
this.#runtimeProviderApiKeyEnvNames,
|
|
2653
|
+
];
|
|
2654
|
+
[this.#runtimeProviderOverrides, other.#runtimeProviderOverrides] = [
|
|
2655
|
+
other.#runtimeProviderOverrides,
|
|
2656
|
+
this.#runtimeProviderOverrides,
|
|
2657
|
+
];
|
|
2658
|
+
[this.#runtimeProviderAuthHeaders, other.#runtimeProviderAuthHeaders] = [
|
|
2659
|
+
other.#runtimeProviderAuthHeaders,
|
|
2660
|
+
this.#runtimeProviderAuthHeaders,
|
|
2661
|
+
];
|
|
2662
|
+
[this.#generatedAuthHeaderProviders, other.#generatedAuthHeaderProviders] = [
|
|
2663
|
+
other.#generatedAuthHeaderProviders,
|
|
2664
|
+
this.#generatedAuthHeaderProviders,
|
|
2665
|
+
];
|
|
2666
|
+
[this.#generatedAuthHeaders, other.#generatedAuthHeaders] = [
|
|
2667
|
+
other.#generatedAuthHeaders,
|
|
2668
|
+
this.#generatedAuthHeaders,
|
|
2669
|
+
];
|
|
2670
|
+
[this.#runtimeProvidersBySource, other.#runtimeProvidersBySource] = [
|
|
2671
|
+
other.#runtimeProvidersBySource,
|
|
2672
|
+
this.#runtimeProvidersBySource,
|
|
2673
|
+
];
|
|
2674
|
+
[this.#runtimeProviderSourceByName, other.#runtimeProviderSourceByName] = [
|
|
2675
|
+
other.#runtimeProviderSourceByName,
|
|
2676
|
+
this.#runtimeProviderSourceByName,
|
|
2677
|
+
];
|
|
2678
|
+
[this.#registeredProviderSources, other.#registeredProviderSources] = [
|
|
2679
|
+
other.#registeredProviderSources,
|
|
2680
|
+
this.#registeredProviderSources,
|
|
2681
|
+
];
|
|
2682
|
+
[this.#registryModelKeys, other.#registryModelKeys] = [other.#registryModelKeys, this.#registryModelKeys];
|
|
2683
|
+
[this.#configuredApiKeyEnvNames, other.#configuredApiKeyEnvNames] = [
|
|
2684
|
+
other.#configuredApiKeyEnvNames,
|
|
2685
|
+
this.#configuredApiKeyEnvNames,
|
|
2686
|
+
];
|
|
2687
|
+
[this.#optionalAuthPreflightGenerations, other.#optionalAuthPreflightGenerations] = [
|
|
2688
|
+
other.#optionalAuthPreflightGenerations,
|
|
2689
|
+
this.#optionalAuthPreflightGenerations,
|
|
2690
|
+
];
|
|
2691
|
+
[this.#optionalAuthPreflightEpoch, other.#optionalAuthPreflightEpoch] = [
|
|
2692
|
+
other.#optionalAuthPreflightEpoch,
|
|
2693
|
+
this.#optionalAuthPreflightEpoch,
|
|
2694
|
+
];
|
|
2695
|
+
const previousBindings = this.#modelBindingsApplier.getBindings();
|
|
2696
|
+
this.#modelBindingsApplier.setBindings(other.#modelBindingsApplier.getBindings());
|
|
2697
|
+
other.#modelBindingsApplier.setBindings(previousBindings);
|
|
2698
|
+
this.#modelBindingsApplier.apply();
|
|
2699
|
+
this.#modelsConfigFile.invalidate();
|
|
2700
|
+
this.#invalidateAvailableModels();
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
#reconcileOwnedConfigApiKeys(
|
|
2704
|
+
configs: ReadonlyMap<string, StagedConfigApiKey>,
|
|
2705
|
+
previousConfigs: ReadonlyMap<string, StagedConfigApiKey>,
|
|
2706
|
+
): void {
|
|
2707
|
+
for (const [provider, previous] of previousConfigs) {
|
|
2708
|
+
const next = configs.get(provider);
|
|
2709
|
+
if (next?.apiKey === previous.apiKey && next.envSourced === previous.envSourced) continue;
|
|
2710
|
+
this.authStorage.removeConfigApiKey(provider, this.#authStorageConfigOwner);
|
|
2711
|
+
}
|
|
2712
|
+
for (const [provider, config] of configs) {
|
|
2713
|
+
const previous = previousConfigs.get(provider);
|
|
2714
|
+
if (previous?.apiKey === config.apiKey && previous.envSourced === config.envSourced) continue;
|
|
2715
|
+
this.authStorage.setConfigApiKey(provider, config.apiKey, {
|
|
2716
|
+
envSourced: config.envSourced,
|
|
2717
|
+
owner: this.#authStorageConfigOwner,
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
getModelsConfigPath(): string | undefined {
|
|
2723
|
+
return this.#modelsConfigFile.path();
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
async stageModelsConfigReload(
|
|
2727
|
+
snapshot: {
|
|
2728
|
+
path: string;
|
|
2729
|
+
text: string | null;
|
|
2730
|
+
identity: string;
|
|
2731
|
+
},
|
|
2732
|
+
registrySettings: Pick<Settings, "get" | "getGlobal"> = this.#settings,
|
|
2733
|
+
): Promise<ModelsConfigReloadCandidate> {
|
|
2734
|
+
if (this.#disposed) throw new Error("Model registry is disposed");
|
|
2735
|
+
const pathMatches = path.resolve(snapshot.path) === path.resolve(this.#modelsConfigFile.path());
|
|
2736
|
+
const source = pathMatches
|
|
2737
|
+
? parseModelsConfigSnapshot(snapshot)
|
|
2738
|
+
: {
|
|
2739
|
+
status: "error" as const,
|
|
2740
|
+
error: new ConfigError("models", undefined, {
|
|
2741
|
+
err: new Error("Models config path changed while the watcher was active"),
|
|
2742
|
+
stage: "Snapshot",
|
|
2743
|
+
}),
|
|
2744
|
+
};
|
|
2745
|
+
const accepted = source.status === "ok" || (source.status === "not-found" && !this.#acceptedModelsConfigPresent);
|
|
2746
|
+
const diagnostics = {
|
|
2747
|
+
valid: accepted,
|
|
2748
|
+
errors: accepted
|
|
2749
|
+
? []
|
|
2750
|
+
: [source.status === "error" ? source.error : new Error("Cannot delete an accepted models config")],
|
|
2751
|
+
};
|
|
2752
|
+
const stagedAuthStorage = createStagedAuthStorage(this.authStorage);
|
|
2753
|
+
stagedAuthStorage.configApiKeys.set(this.#authStorageConfigOwner, new Map(this.#ownedConfigApiKeys));
|
|
2754
|
+
const acceptedPresets =
|
|
2755
|
+
this.#acceptedPresets ??
|
|
2756
|
+
loadAcceptedModelPresetProfiles(this.#modelPresetRegistryAgentDir, this.#modelPresetRegistryDependencies);
|
|
2757
|
+
const registry = new ModelRegistry(
|
|
2758
|
+
stagedAuthStorage.authStorage,
|
|
2759
|
+
this.#modelsConfigFile.path(),
|
|
2760
|
+
registrySettings,
|
|
2761
|
+
{ ...this.#modelPresetRegistryDependencies, automaticRefresh: false },
|
|
1875
2762
|
{
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
() => {
|
|
1881
|
-
void this.#enqueueCatalogMutation(() => {
|
|
1882
|
-
if (this.#disposed) return;
|
|
1883
|
-
this.#reloadStaticModels();
|
|
1884
|
-
this.#modelBindingsApplier.apply();
|
|
1885
|
-
this.#notifyCatalogChanged();
|
|
1886
|
-
}).catch(() => undefined);
|
|
2763
|
+
configFile: this.#modelsConfigFile,
|
|
2764
|
+
configSource: source,
|
|
2765
|
+
runtimeSource: this,
|
|
2766
|
+
acceptedPresets,
|
|
1887
2767
|
},
|
|
1888
2768
|
);
|
|
2769
|
+
if (registry.getError()) {
|
|
2770
|
+
diagnostics.valid = false;
|
|
2771
|
+
diagnostics.errors.push(registry.getError()!);
|
|
2772
|
+
}
|
|
2773
|
+
for (const [provider, apiKey] of registry.#ownedConfigApiKeys) {
|
|
2774
|
+
if (apiKey.runtimeSourced === true && apiKey.configSourced !== true) continue;
|
|
2775
|
+
if (!this.authStorage.hasAnyCredentialPin(provider)) continue;
|
|
2776
|
+
diagnostics.valid = false;
|
|
2777
|
+
diagnostics.errors.push(
|
|
2778
|
+
new Error(
|
|
2779
|
+
`Cannot configure an API key override for ${provider} while a credential pin is active or unavailable`,
|
|
2780
|
+
),
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
let committed = false;
|
|
2784
|
+
let finished = false;
|
|
2785
|
+
const previousConfigPresent = this.#acceptedModelsConfigPresent;
|
|
2786
|
+
const previousConfigValue = this.#acceptedConfigValue;
|
|
2787
|
+
const nextConfigValue = source.status === "ok" ? source.value : undefined;
|
|
2788
|
+
const changed = !isDeepStrictEqual(previousConfigValue, nextConfigValue);
|
|
2789
|
+
const unchangedProviders = new Set(
|
|
2790
|
+
[...this.#providerActivity.keys()].filter(
|
|
2791
|
+
provider =>
|
|
2792
|
+
isDeepStrictEqual(previousConfigValue?.providers?.[provider], nextConfigValue?.providers?.[provider]) &&
|
|
2793
|
+
isDeepStrictEqual(this.#providerOverrides.get(provider), registry.#providerOverrides.get(provider)) &&
|
|
2794
|
+
isDeepStrictEqual(this.#modelOverrides.get(provider), registry.#modelOverrides.get(provider)) &&
|
|
2795
|
+
this.#customProviderApiKeys.get(provider) === registry.#customProviderApiKeys.get(provider) &&
|
|
2796
|
+
isDeepStrictEqual(this.#ownedConfigApiKeys.get(provider), registry.#ownedConfigApiKeys.get(provider)),
|
|
2797
|
+
),
|
|
2798
|
+
);
|
|
2799
|
+
const unchangedProviderSnapshots = new Map(
|
|
2800
|
+
[...unchangedProviders].map(provider => [
|
|
2801
|
+
provider,
|
|
2802
|
+
{
|
|
2803
|
+
models: this.#models.filter(model => model.provider === provider),
|
|
2804
|
+
discoveryState: this.#discoveryManager.getState(provider),
|
|
2805
|
+
configuredEvidence: this.#configuredDiscoveryEvidence.get(provider),
|
|
2806
|
+
descriptorEvidence: this.#descriptorDiscoveryEvidence.get(provider),
|
|
2807
|
+
descriptorGeneration: this.#descriptorDiscoveryGenerations.get(provider),
|
|
2808
|
+
providerEvidenceApiKey: this.#providerEvidenceApiKeys.get(provider),
|
|
2809
|
+
credentiallessFallback: this.#credentiallessAuthFallbackProviders.get(provider),
|
|
2810
|
+
optionalAuthPreflightGeneration: this.#optionalAuthPreflightGenerations.get(provider),
|
|
2811
|
+
activity: this.#providerActivity.get(provider),
|
|
2812
|
+
},
|
|
2813
|
+
]),
|
|
2814
|
+
);
|
|
2815
|
+
const copyProviderEntry = <T>(target: Map<string, T>, source: ReadonlyMap<string, T>, provider: string) => {
|
|
2816
|
+
const value = source.get(provider);
|
|
2817
|
+
if (value === undefined) target.delete(provider);
|
|
2818
|
+
else target.set(provider, value);
|
|
2819
|
+
};
|
|
2820
|
+
const stagedModelKeys = new Set(registry.#models.map(model => `${model.provider}/${model.id}`));
|
|
2821
|
+
for (const model of this.#models) {
|
|
2822
|
+
if (!unchangedProviders.has(model.provider) || stagedModelKeys.has(`${model.provider}/${model.id}`)) continue;
|
|
2823
|
+
registry.#models.push(model);
|
|
2824
|
+
}
|
|
2825
|
+
registry.#discoveryManager.retainStatesFrom(this.#discoveryManager, unchangedProviders);
|
|
2826
|
+
const retainedActivity = new Map(registry.#providerActivity);
|
|
2827
|
+
for (const provider of unchangedProviders) {
|
|
2828
|
+
copyProviderEntry(registry.#configuredDiscoveryEvidence, this.#configuredDiscoveryEvidence, provider);
|
|
2829
|
+
copyProviderEntry(registry.#descriptorDiscoveryEvidence, this.#descriptorDiscoveryEvidence, provider);
|
|
2830
|
+
copyProviderEntry(registry.#descriptorDiscoveryGenerations, this.#descriptorDiscoveryGenerations, provider);
|
|
2831
|
+
copyProviderEntry(registry.#providerEvidenceApiKeys, this.#providerEvidenceApiKeys, provider);
|
|
2832
|
+
copyProviderEntry(retainedActivity, this.#providerActivity, provider);
|
|
2833
|
+
}
|
|
2834
|
+
registry.#providerActivity = retainedActivity;
|
|
2835
|
+
registry.#rebuildCanonicalIndex();
|
|
2836
|
+
const rebaseProviderDiscoveryAfterQueuedRefresh = () => {
|
|
2837
|
+
const changedProviders = new Set(
|
|
2838
|
+
[...unchangedProviders].filter(provider => {
|
|
2839
|
+
const snapshot = unchangedProviderSnapshots.get(provider)!;
|
|
2840
|
+
return (
|
|
2841
|
+
!isDeepStrictEqual(
|
|
2842
|
+
snapshot.models,
|
|
2843
|
+
this.#models.filter(model => model.provider === provider),
|
|
2844
|
+
) ||
|
|
2845
|
+
!isDeepStrictEqual(snapshot.discoveryState, this.#discoveryManager.getState(provider)) ||
|
|
2846
|
+
!isDeepStrictEqual(snapshot.configuredEvidence, this.#configuredDiscoveryEvidence.get(provider)) ||
|
|
2847
|
+
!isDeepStrictEqual(snapshot.descriptorEvidence, this.#descriptorDiscoveryEvidence.get(provider)) ||
|
|
2848
|
+
snapshot.descriptorGeneration !== this.#descriptorDiscoveryGenerations.get(provider) ||
|
|
2849
|
+
snapshot.providerEvidenceApiKey !== this.#providerEvidenceApiKeys.get(provider) ||
|
|
2850
|
+
snapshot.credentiallessFallback !== this.#credentiallessAuthFallbackProviders.get(provider) ||
|
|
2851
|
+
snapshot.optionalAuthPreflightGeneration !== this.#optionalAuthPreflightGenerations.get(provider) ||
|
|
2852
|
+
!isDeepStrictEqual(snapshot.activity, this.#providerActivity.get(provider))
|
|
2853
|
+
);
|
|
2854
|
+
}),
|
|
2855
|
+
);
|
|
2856
|
+
if (changedProviders.size === 0) return;
|
|
2857
|
+
|
|
2858
|
+
registry.#models = registry.#models.filter(model => !changedProviders.has(model.provider));
|
|
2859
|
+
registry.#models.push(...this.#models.filter(model => changedProviders.has(model.provider)));
|
|
2860
|
+
for (const provider of changedProviders) registry.#discoveryManager.invalidate(provider);
|
|
2861
|
+
registry.#discoveryManager.retainStatesFrom(this.#discoveryManager, changedProviders);
|
|
2862
|
+
const providerActivity = new Map(registry.#providerActivity);
|
|
2863
|
+
for (const provider of changedProviders) {
|
|
2864
|
+
copyProviderEntry(registry.#configuredDiscoveryEvidence, this.#configuredDiscoveryEvidence, provider);
|
|
2865
|
+
copyProviderEntry(registry.#descriptorDiscoveryEvidence, this.#descriptorDiscoveryEvidence, provider);
|
|
2866
|
+
registry.#googleSessionDiscoveryCatalogs.delete(provider);
|
|
2867
|
+
copyProviderEntry(registry.#descriptorDiscoveryGenerations, this.#descriptorDiscoveryGenerations, provider);
|
|
2868
|
+
copyProviderEntry(registry.#providerEvidenceApiKeys, this.#providerEvidenceApiKeys, provider);
|
|
2869
|
+
copyProviderEntry(
|
|
2870
|
+
registry.#credentiallessAuthFallbackProviders,
|
|
2871
|
+
this.#credentiallessAuthFallbackProviders,
|
|
2872
|
+
provider,
|
|
2873
|
+
);
|
|
2874
|
+
copyProviderEntry(
|
|
2875
|
+
registry.#optionalAuthPreflightGenerations,
|
|
2876
|
+
this.#optionalAuthPreflightGenerations,
|
|
2877
|
+
provider,
|
|
2878
|
+
);
|
|
2879
|
+
copyProviderEntry(providerActivity, this.#providerActivity, provider);
|
|
2880
|
+
}
|
|
2881
|
+
registry.#providerActivity = providerActivity;
|
|
2882
|
+
registry.#rebuildCanonicalIndex();
|
|
2883
|
+
};
|
|
2884
|
+
const expectedGeneration = this.#catalogRefreshGeneration;
|
|
2885
|
+
const isCurrent = () => !this.#disposed && this.#catalogRefreshGeneration === expectedGeneration;
|
|
2886
|
+
const commit = () => {
|
|
2887
|
+
if (finished) throw new Error("Models config reload candidate is no longer active");
|
|
2888
|
+
if (!diagnostics.valid) throw new Error("Cannot commit an invalid models config reload candidate");
|
|
2889
|
+
if (committed) return;
|
|
2890
|
+
if (!isCurrent()) {
|
|
2891
|
+
throw new Error("Model catalog changed during configuration preflight");
|
|
2892
|
+
}
|
|
2893
|
+
for (const [provider, apiKey] of registry.#ownedConfigApiKeys) {
|
|
2894
|
+
if (apiKey.runtimeSourced === true && apiKey.configSourced !== true) continue;
|
|
2895
|
+
if (this.authStorage.hasAnyCredentialPin(provider)) {
|
|
2896
|
+
throw new Error(
|
|
2897
|
+
`Cannot configure an API key override for ${provider} while a credential pin is active or unavailable`,
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
rebaseProviderDiscoveryAfterQueuedRefresh();
|
|
2902
|
+
this.#swapReloadableState(registry);
|
|
2903
|
+
try {
|
|
2904
|
+
this.#reconcileOwnedConfigApiKeys(this.#ownedConfigApiKeys, registry.#ownedConfigApiKeys);
|
|
2905
|
+
this.#acceptedModelsConfigPresent = source.status === "ok";
|
|
2906
|
+
this.#acceptedConfigValue = nextConfigValue;
|
|
2907
|
+
// The captured source may precede the current disk revision.
|
|
2908
|
+
this.#lastStaticLoadMtime = null;
|
|
2909
|
+
committed = true;
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
this.#swapReloadableState(registry);
|
|
2912
|
+
this.#reconcileOwnedConfigApiKeys(this.#ownedConfigApiKeys, registry.#ownedConfigApiKeys);
|
|
2913
|
+
throw error;
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
const rollback = () => {
|
|
2917
|
+
if (finished) return;
|
|
2918
|
+
if (committed) {
|
|
2919
|
+
this.#swapReloadableState(registry);
|
|
2920
|
+
this.#reconcileOwnedConfigApiKeys(this.#ownedConfigApiKeys, registry.#ownedConfigApiKeys);
|
|
2921
|
+
this.#acceptedModelsConfigPresent = previousConfigPresent;
|
|
2922
|
+
this.#acceptedConfigValue = previousConfigValue;
|
|
2923
|
+
committed = false;
|
|
2924
|
+
}
|
|
2925
|
+
finished = true;
|
|
2926
|
+
void registry.dispose();
|
|
2927
|
+
};
|
|
2928
|
+
const finalize = () => {
|
|
2929
|
+
if (finished) return;
|
|
2930
|
+
if (!committed) throw new Error("Cannot finalize an unpublished models configuration");
|
|
2931
|
+
finished = true;
|
|
2932
|
+
this.#catalogRefreshGeneration++;
|
|
2933
|
+
this.#notifyCatalogChanged();
|
|
2934
|
+
void registry.dispose();
|
|
2935
|
+
};
|
|
2936
|
+
return { valid: diagnostics.valid, changed, diagnostics, registry, isCurrent, commit, rollback, finalize };
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
#acquireLease(kind: RegistryLeaseWaiter["kind"], signal?: AbortSignal): Promise<() => void> {
|
|
2940
|
+
if (this.#disposed) return Promise.reject(new Error("Model registry is disposed"));
|
|
2941
|
+
if (signal?.aborted) return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
2942
|
+
if (
|
|
2943
|
+
!this.#publicationFenceHeld &&
|
|
2944
|
+
this.#leaseWaiters.length === 0 &&
|
|
2945
|
+
(kind === "consumer" || this.#activeConsumerLeases === 0)
|
|
2946
|
+
) {
|
|
2947
|
+
return Promise.resolve(this.#grantLease(kind));
|
|
2948
|
+
}
|
|
2949
|
+
const { promise, resolve, reject } = Promise.withResolvers<() => void>();
|
|
2950
|
+
const waiter: RegistryLeaseWaiter = { kind, signal, resolve, reject, settled: false };
|
|
2951
|
+
if (signal) {
|
|
2952
|
+
waiter.abort = () => {
|
|
2953
|
+
if (waiter.settled) return;
|
|
2954
|
+
waiter.settled = true;
|
|
2955
|
+
this.#leaseWaiters = this.#leaseWaiters.filter(item => item !== waiter);
|
|
2956
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
2957
|
+
this.#drainLeaseWaiters();
|
|
2958
|
+
};
|
|
2959
|
+
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
2960
|
+
}
|
|
2961
|
+
this.#leaseWaiters.push(waiter);
|
|
2962
|
+
this.#drainLeaseWaiters();
|
|
2963
|
+
return promise;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
#grantLease(kind: RegistryLeaseWaiter["kind"]): () => void {
|
|
2967
|
+
if (kind === "publication") this.#publicationFenceHeld = true;
|
|
2968
|
+
else this.#activeConsumerLeases += 1;
|
|
2969
|
+
let released = false;
|
|
2970
|
+
return () => {
|
|
2971
|
+
if (released) return;
|
|
2972
|
+
released = true;
|
|
2973
|
+
if (kind === "publication") this.#publicationFenceHeld = false;
|
|
2974
|
+
else this.#activeConsumerLeases -= 1;
|
|
2975
|
+
this.#drainLeaseWaiters();
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
#drainLeaseWaiters(): void {
|
|
2980
|
+
if (this.#publicationFenceHeld) return;
|
|
2981
|
+
while (this.#leaseWaiters.length > 0) {
|
|
2982
|
+
const waiter = this.#leaseWaiters[0]!;
|
|
2983
|
+
if (waiter.kind === "publication") {
|
|
2984
|
+
if (this.#activeConsumerLeases > 0) return;
|
|
2985
|
+
this.#leaseWaiters.shift();
|
|
2986
|
+
waiter.settled = true;
|
|
2987
|
+
if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
2988
|
+
waiter.resolve(this.#grantLease("publication"));
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
this.#leaseWaiters.shift();
|
|
2992
|
+
waiter.settled = true;
|
|
2993
|
+
if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
2994
|
+
waiter.resolve(this.#grantLease("consumer"));
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
async withConsumerLease<T>(body: () => T | Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
2999
|
+
signal?.throwIfAborted();
|
|
3000
|
+
if (this.#consumerContext.getStore()?.active) return await body();
|
|
3001
|
+
const release = await this.#acquireLease("consumer", signal);
|
|
3002
|
+
const owner = { active: true };
|
|
3003
|
+
try {
|
|
3004
|
+
return await this.#consumerContext.run(owner, body);
|
|
3005
|
+
} finally {
|
|
3006
|
+
owner.active = false;
|
|
3007
|
+
release();
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
async acquirePublicationFence(signal?: AbortSignal): Promise<() => void> {
|
|
3012
|
+
if (this.#consumerContext.getStore()?.active) {
|
|
3013
|
+
throw new Error("Cannot publish configuration while holding a model consumer lease");
|
|
3014
|
+
}
|
|
3015
|
+
const release = await this.#acquireLease("publication", signal);
|
|
3016
|
+
const gate = Promise.withResolvers<void>();
|
|
3017
|
+
const entered = Promise.withResolvers<void>();
|
|
3018
|
+
const barrier = this.#enqueueCatalogMutation(async () => {
|
|
3019
|
+
entered.resolve();
|
|
3020
|
+
await gate.promise;
|
|
3021
|
+
});
|
|
3022
|
+
const cancelled = Promise.withResolvers<void>();
|
|
3023
|
+
const abort = () => cancelled.reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
3024
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3025
|
+
try {
|
|
3026
|
+
if (signal?.aborted) abort();
|
|
3027
|
+
await Promise.race([entered.promise, cancelled.promise]);
|
|
3028
|
+
signal?.throwIfAborted();
|
|
3029
|
+
} catch (error) {
|
|
3030
|
+
gate.resolve();
|
|
3031
|
+
void barrier.finally(release);
|
|
3032
|
+
throw error;
|
|
3033
|
+
} finally {
|
|
3034
|
+
signal?.removeEventListener("abort", abort);
|
|
3035
|
+
}
|
|
3036
|
+
let released = false;
|
|
3037
|
+
return () => {
|
|
3038
|
+
if (released) return;
|
|
3039
|
+
released = true;
|
|
3040
|
+
gate.resolve();
|
|
3041
|
+
void barrier.finally(release);
|
|
3042
|
+
};
|
|
1889
3043
|
}
|
|
1890
3044
|
|
|
1891
3045
|
dispose(): Promise<void> {
|
|
1892
3046
|
if (this.#disposePromise) return this.#disposePromise;
|
|
1893
3047
|
this.#disposed = true;
|
|
3048
|
+
for (const waiter of this.#leaseWaiters.splice(0)) {
|
|
3049
|
+
waiter.settled = true;
|
|
3050
|
+
if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
3051
|
+
waiter.reject(new Error("Model registry is disposed"));
|
|
3052
|
+
}
|
|
1894
3053
|
this.#catalogRefreshGeneration++;
|
|
1895
3054
|
const awaitRefreshDisposal = this.#cancelModelPresetRegistryRefresh?.() ?? Promise.resolve();
|
|
1896
3055
|
this.#cancelModelPresetRegistryRefresh = undefined;
|
|
1897
3056
|
this.#unsubscribeAuthGeneration?.();
|
|
1898
3057
|
this.#unsubscribeAuthGeneration = undefined;
|
|
1899
|
-
this
|
|
3058
|
+
this.#clearOwnedConfigApiKeys();
|
|
1900
3059
|
this.#disposeAuthStorageFallbackResolver?.();
|
|
1901
3060
|
this.#disposeAuthStorageFallbackResolver = undefined;
|
|
1902
3061
|
this.#catalogChangeListeners.clear();
|
|
@@ -2001,9 +3160,12 @@ export class ModelRegistry {
|
|
|
2001
3160
|
}
|
|
2002
3161
|
|
|
2003
3162
|
refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached", credentialSessionId?: string): void {
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
3163
|
+
// Each credential context has an isolated discovery catalog. Coalescing every
|
|
3164
|
+
// request behind one global promise discarded B's refresh when A was pending.
|
|
3165
|
+
// Equivalent contexts still coalesce; distinct ones queue through the catalog
|
|
3166
|
+
// mutation tail so their overlays remain independently populated.
|
|
3167
|
+
const contextKey = `${strategy}\u0000${credentialSessionId ?? ""}`;
|
|
3168
|
+
if (this.#backgroundRefreshes.has(contextKey)) return;
|
|
2007
3169
|
const refreshPromise = this.refresh(strategy, credentialSessionId)
|
|
2008
3170
|
.catch(error => {
|
|
2009
3171
|
logger.warn("background model refresh failed", {
|
|
@@ -2011,11 +3173,11 @@ export class ModelRegistry {
|
|
|
2011
3173
|
});
|
|
2012
3174
|
})
|
|
2013
3175
|
.finally(() => {
|
|
2014
|
-
if (this.#
|
|
2015
|
-
this.#
|
|
3176
|
+
if (this.#backgroundRefreshes.get(contextKey) === refreshPromise) {
|
|
3177
|
+
this.#backgroundRefreshes.delete(contextKey);
|
|
2016
3178
|
}
|
|
2017
3179
|
});
|
|
2018
|
-
this.#
|
|
3180
|
+
this.#backgroundRefreshes.set(contextKey, refreshPromise);
|
|
2019
3181
|
}
|
|
2020
3182
|
|
|
2021
3183
|
async refreshProvider(
|
|
@@ -2141,6 +3303,11 @@ export class ModelRegistry {
|
|
|
2141
3303
|
);
|
|
2142
3304
|
return JSON.stringify({
|
|
2143
3305
|
apiKeyEnv: [...this.#configuredApiKeyEnvNames].sort().map(name => [name, Bun.env[name] ?? ""]),
|
|
3306
|
+
credentialPins: this.#stagedReloadCandidate
|
|
3307
|
+
? []
|
|
3308
|
+
: [...this.#configuredProviderIds]
|
|
3309
|
+
.sort()
|
|
3310
|
+
.map(provider => [provider, this.authStorage.hasAnyCredentialPin(provider)]),
|
|
2144
3311
|
implicitEndpoints: [
|
|
2145
3312
|
["OLLAMA_BASE_URL", Bun.env.OLLAMA_BASE_URL || ""],
|
|
2146
3313
|
["LLAMA_CPP_BASE_URL", Bun.env.LLAMA_CPP_BASE_URL || ""],
|
|
@@ -2171,7 +3338,7 @@ export class ModelRegistry {
|
|
|
2171
3338
|
environmentFingerprint === this.#lastStaticLoadEnvironmentFingerprint &&
|
|
2172
3339
|
modelPresetRegistryFingerprint === this.#lastModelPresetRegistryFingerprint
|
|
2173
3340
|
) {
|
|
2174
|
-
//
|
|
3341
|
+
// Config, settings-derived provider state, and effective credential selectors are unchanged.
|
|
2175
3342
|
return;
|
|
2176
3343
|
}
|
|
2177
3344
|
this.#modelsConfigFile.invalidate();
|
|
@@ -2189,7 +3356,7 @@ export class ModelRegistry {
|
|
|
2189
3356
|
// Drop config-sourced apiKeys from AuthStorage before reload; entries
|
|
2190
3357
|
// removed from models.yml must actually disappear from the resolver, not
|
|
2191
3358
|
// linger from the previous parse. The post-load setters below repopulate.
|
|
2192
|
-
this
|
|
3359
|
+
this.#clearOwnedConfigApiKeys();
|
|
2193
3360
|
// Runtime provider keys are reapplied after #loadModels so they retain
|
|
2194
3361
|
// registration-time precedence over colliding static provider keys.
|
|
2195
3362
|
this.#providerOverrides.clear();
|
|
@@ -2202,7 +3369,7 @@ export class ModelRegistry {
|
|
|
2202
3369
|
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
2203
3370
|
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
2204
3371
|
: resolveApiKeyConfig(apiKeyConfig);
|
|
2205
|
-
if (!resolved) {
|
|
3372
|
+
if (!resolved || this.authStorage.hasAnyCredentialPin(provider)) {
|
|
2206
3373
|
this.#runtimeProviderCredentialInstalled.delete(provider);
|
|
2207
3374
|
const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
|
|
2208
3375
|
if (authHeader === true) {
|
|
@@ -2225,7 +3392,7 @@ export class ModelRegistry {
|
|
|
2225
3392
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
2226
3393
|
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
2227
3394
|
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
2228
|
-
this
|
|
3395
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
2229
3396
|
const override = this.#runtimeProviderOverrides.get(provider);
|
|
2230
3397
|
if (override) this.#runtimeProviderOverrides.set(provider, { ...override, apiKey: resolved });
|
|
2231
3398
|
const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
|
|
@@ -2258,11 +3425,11 @@ export class ModelRegistry {
|
|
|
2258
3425
|
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
2259
3426
|
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
2260
3427
|
: resolveApiKeyConfig(apiKeyConfig);
|
|
2261
|
-
if (!resolved) continue;
|
|
3428
|
+
if (!resolved || this.authStorage.hasAnyCredentialPin(provider)) continue;
|
|
2262
3429
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
2263
3430
|
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
2264
3431
|
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
2265
|
-
this
|
|
3432
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
2266
3433
|
}
|
|
2267
3434
|
this.#lastDisabledProviderKey = disabledProviderKey;
|
|
2268
3435
|
}
|
|
@@ -2298,10 +3465,10 @@ export class ModelRegistry {
|
|
|
2298
3465
|
this.#codexContextWindowOverrides = this.#collectCodexContextWindowOverrides();
|
|
2299
3466
|
this.#equivalenceConfig = equivalence;
|
|
2300
3467
|
this.#modelBindingsApplier.setBindings(modelBindings);
|
|
2301
|
-
const acceptedPresets =
|
|
2302
|
-
this.#
|
|
2303
|
-
this.#modelPresetRegistryDependencies
|
|
2304
|
-
|
|
3468
|
+
const acceptedPresets = this.#stagedReloadCandidate
|
|
3469
|
+
? this.#acceptedPresets!
|
|
3470
|
+
: loadAcceptedModelPresetProfiles(this.#modelPresetRegistryAgentDir, this.#modelPresetRegistryDependencies);
|
|
3471
|
+
this.#acceptedPresets = acceptedPresets;
|
|
2305
3472
|
const acceptedRegistryError = acceptedPresets.error
|
|
2306
3473
|
? new ConfigError("model-preset-registry", undefined, {
|
|
2307
3474
|
err: new Error(acceptedPresets.error),
|
|
@@ -2476,6 +3643,9 @@ export class ModelRegistry {
|
|
|
2476
3643
|
const inheritedCompat = explicitTransportMatches
|
|
2477
3644
|
? explicitTransport?.compat
|
|
2478
3645
|
: commonRegistryTransportCompat(transportTemplates);
|
|
3646
|
+
const streamIdleTimeoutMs = isRequestTransformApi(registryModel.api)
|
|
3647
|
+
? (registryModel.streamIdleTimeoutMs ?? explicitTransport?.streamIdleTimeoutMs)
|
|
3648
|
+
: undefined;
|
|
2479
3649
|
const hydratedRegistryModel = {
|
|
2480
3650
|
...registryModel,
|
|
2481
3651
|
baseUrl: transportSource.baseUrl!,
|
|
@@ -2483,6 +3653,7 @@ export class ModelRegistry {
|
|
|
2483
3653
|
transport: transportSource.transport,
|
|
2484
3654
|
requestTransform: transportSource.requestTransform,
|
|
2485
3655
|
cacheRetention: transportSource.cacheRetention,
|
|
3656
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
2486
3657
|
isOAuth: transportSource.isOAuth,
|
|
2487
3658
|
compat:
|
|
2488
3659
|
inheritedCompat || registryModel.compat || explicitTransport?.compat
|
|
@@ -2498,14 +3669,23 @@ export class ModelRegistry {
|
|
|
2498
3669
|
continue;
|
|
2499
3670
|
}
|
|
2500
3671
|
const existing = merged[existingIndex]!;
|
|
3672
|
+
const { streamIdleTimeoutMs: existingStreamIdleTimeoutMs, ...existingWithoutStreamIdleTimeout } = existing;
|
|
2501
3673
|
const effectiveApi = explicitTransport?.api ?? existing.api;
|
|
2502
3674
|
const registryApiMatchesEffectiveTransport = registryModel.api === effectiveApi;
|
|
2503
3675
|
const registryMetadata = registryApiMatchesEffectiveTransport
|
|
2504
3676
|
? registryModel
|
|
2505
3677
|
: registryModelMetadataWithoutApiSpecificFields(registryModel);
|
|
3678
|
+
const {
|
|
3679
|
+
streamIdleTimeoutMs: registryMetadataStreamIdleTimeoutMs,
|
|
3680
|
+
...registryMetadataWithoutStreamIdleTimeout
|
|
3681
|
+
} = registryMetadata;
|
|
3682
|
+
const streamIdleTimeoutMs = isRequestTransformApi(effectiveApi)
|
|
3683
|
+
? (registryMetadataStreamIdleTimeoutMs ?? existingStreamIdleTimeoutMs)
|
|
3684
|
+
: undefined;
|
|
2506
3685
|
merged[existingIndex] = {
|
|
2507
|
-
...
|
|
2508
|
-
...
|
|
3686
|
+
...existingWithoutStreamIdleTimeout,
|
|
3687
|
+
...registryMetadataWithoutStreamIdleTimeout,
|
|
3688
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
2509
3689
|
api: explicitTransport?.api ?? existing.api,
|
|
2510
3690
|
baseUrl: existing.baseUrl,
|
|
2511
3691
|
headers: existing.headers,
|
|
@@ -2542,9 +3722,14 @@ export class ModelRegistry {
|
|
|
2542
3722
|
const existingIndex = indexByKey.get(key);
|
|
2543
3723
|
if (existingIndex !== undefined) {
|
|
2544
3724
|
const existingModel = merged[existingIndex];
|
|
3725
|
+
const { streamIdleTimeoutMs: existingStreamIdleTimeoutMs, ...existingModelWithoutStreamIdleTimeout } =
|
|
3726
|
+
existingModel;
|
|
2545
3727
|
const referenceModel = resolveCustomModelReference(customModel.id);
|
|
3728
|
+
const streamIdleTimeoutMs = isRequestTransformApi(customModel.api)
|
|
3729
|
+
? (customModel.streamIdleTimeoutMs ?? existingStreamIdleTimeoutMs)
|
|
3730
|
+
: undefined;
|
|
2546
3731
|
merged[existingIndex] = enrichModelThinking({
|
|
2547
|
-
...
|
|
3732
|
+
...existingModelWithoutStreamIdleTimeout,
|
|
2548
3733
|
id: customModel.id,
|
|
2549
3734
|
provider: customModel.provider,
|
|
2550
3735
|
api: customModel.api,
|
|
@@ -2568,6 +3753,7 @@ export class ModelRegistry {
|
|
|
2568
3753
|
wireModelId: customModel.wireModelId,
|
|
2569
3754
|
requestTransform: customModel.requestTransform,
|
|
2570
3755
|
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
3756
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
2571
3757
|
} as Model<Api>);
|
|
2572
3758
|
} else {
|
|
2573
3759
|
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
@@ -2587,6 +3773,7 @@ export class ModelRegistry {
|
|
|
2587
3773
|
}
|
|
2588
3774
|
const cache = readModelCache<Api>(descriptor.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
2589
3775
|
const expectedProvenance = fingerprintDescriptorDiscoveryProvenance(
|
|
3776
|
+
descriptor.providerId,
|
|
2590
3777
|
authEvidence ?? this.#getProviderEvidenceGeneration(descriptor.providerId),
|
|
2591
3778
|
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? ""),
|
|
2592
3779
|
);
|
|
@@ -2740,10 +3927,12 @@ export class ModelRegistry {
|
|
|
2740
3927
|
});
|
|
2741
3928
|
}
|
|
2742
3929
|
#sanitizeDiscoverableModelsForCache(_providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
2743
|
-
return this.#stripModelBaseUrlQueries(models).map(model =>
|
|
2744
|
-
...model,
|
|
2745
|
-
|
|
2746
|
-
|
|
3930
|
+
return this.#stripModelBaseUrlQueries(models).map(model => {
|
|
3931
|
+
const cachedModel = { ...model, headers: undefined };
|
|
3932
|
+
// Model idle timeouts come from local config and must be re-derived on cache load.
|
|
3933
|
+
delete cachedModel.streamIdleTimeoutMs;
|
|
3934
|
+
return cachedModel;
|
|
3935
|
+
});
|
|
2747
3936
|
}
|
|
2748
3937
|
#stripModelBaseUrlQueries(models: readonly Model<Api>[]): Model<Api>[] {
|
|
2749
3938
|
return models.map(model => createTrustedStrippedModelClone(model));
|
|
@@ -2824,7 +4013,10 @@ export class ModelRegistry {
|
|
|
2824
4013
|
|
|
2825
4014
|
#loadCustomModels(): CustomModelsResult {
|
|
2826
4015
|
this.#configuredApiKeyEnvNames.clear();
|
|
2827
|
-
const
|
|
4016
|
+
const loaded = this.#modelsConfigSource ?? this.#modelsConfigFile.tryLoad();
|
|
4017
|
+
const { value, error, status } = loaded;
|
|
4018
|
+
this.#acceptedConfigValue = status === "ok" ? value : undefined;
|
|
4019
|
+
if (status === "ok") this.#acceptedModelsConfigPresent = true;
|
|
2828
4020
|
|
|
2829
4021
|
if (status === "error") {
|
|
2830
4022
|
return {
|
|
@@ -2859,6 +4051,7 @@ export class ModelRegistry {
|
|
|
2859
4051
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
2860
4052
|
|
|
2861
4053
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
4054
|
+
const credentialPinActive = !this.#stagedReloadCandidate && this.authStorage.hasAnyCredentialPin(providerName);
|
|
2862
4055
|
const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
|
|
2863
4056
|
const isOAuth = resolveCustomModelIsOAuth(
|
|
2864
4057
|
(providerConfig.api as Api | undefined) ?? "openai-completions",
|
|
@@ -2873,9 +4066,11 @@ export class ModelRegistry {
|
|
|
2873
4066
|
if (providerConfig.openaiCompat?.apiKey)
|
|
2874
4067
|
this.#configuredApiKeyEnvNames.add(providerConfig.openaiCompat.apiKey);
|
|
2875
4068
|
if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
|
|
2876
|
-
const providerApiKeyConfig =
|
|
2877
|
-
?
|
|
2878
|
-
:
|
|
4069
|
+
const providerApiKeyConfig = credentialPinActive
|
|
4070
|
+
? undefined
|
|
4071
|
+
: providerConfig.apiKey
|
|
4072
|
+
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
4073
|
+
: resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
|
|
2879
4074
|
const localOpenAICompat = providerConfig.openaiCompat;
|
|
2880
4075
|
const rotatingApiKeyEnv = providerConfig.apiKey
|
|
2881
4076
|
? undefined
|
|
@@ -2883,18 +4078,21 @@ export class ModelRegistry {
|
|
|
2883
4078
|
if (rotatingApiKeyEnv) this.#customProviderApiKeyEnvNames.set(providerName, rotatingApiKeyEnv);
|
|
2884
4079
|
if (providerConfig.authHeader !== undefined)
|
|
2885
4080
|
this.#customProviderAuthHeaders.set(providerName, providerConfig.authHeader);
|
|
2886
|
-
const localOpenAICompatApiKeyConfig =
|
|
2887
|
-
|
|
2888
|
-
?
|
|
2889
|
-
|
|
2890
|
-
|
|
4081
|
+
const localOpenAICompatApiKeyConfig =
|
|
4082
|
+
localOpenAICompat && !credentialPinActive
|
|
4083
|
+
? localOpenAICompat.apiKey
|
|
4084
|
+
? resolveApiKeyConfig(localOpenAICompat.apiKey)
|
|
4085
|
+
: resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
|
|
4086
|
+
: undefined;
|
|
2891
4087
|
if (localOpenAICompat) {
|
|
2892
4088
|
const localOpenAICompatBaseUrl = normalizeLocalOpenAICompatBaseUrl(localOpenAICompat.baseUrl);
|
|
2893
|
-
const localCompatResolvedKey =
|
|
2894
|
-
?
|
|
2895
|
-
: localOpenAICompat.
|
|
2896
|
-
?
|
|
2897
|
-
:
|
|
4089
|
+
const localCompatResolvedKey = credentialPinActive
|
|
4090
|
+
? undefined
|
|
4091
|
+
: localOpenAICompat.apiKey
|
|
4092
|
+
? resolveApiKeyConfig(localOpenAICompat.apiKey)
|
|
4093
|
+
: localOpenAICompat.apiKeyEnv
|
|
4094
|
+
? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
|
|
4095
|
+
: undefined;
|
|
2898
4096
|
overrides.set(providerName, {
|
|
2899
4097
|
api: "openai-completions",
|
|
2900
4098
|
baseUrl: localOpenAICompatBaseUrl,
|
|
@@ -2920,10 +4118,7 @@ export class ModelRegistry {
|
|
|
2920
4118
|
});
|
|
2921
4119
|
if (localCompatResolvedKey) {
|
|
2922
4120
|
this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
|
|
2923
|
-
this
|
|
2924
|
-
envSourced: !localOpenAICompat.apiKey,
|
|
2925
|
-
owner: this.#authStorageConfigOwner,
|
|
2926
|
-
});
|
|
4121
|
+
this.#setOwnedConfigApiKey(providerName, localCompatResolvedKey, !localOpenAICompat.apiKey);
|
|
2927
4122
|
} else {
|
|
2928
4123
|
keylessProviders.add(providerName);
|
|
2929
4124
|
this.#optionalAuthProviders.add(providerName);
|
|
@@ -2940,10 +4135,11 @@ export class ModelRegistry {
|
|
|
2940
4135
|
providerConfig.disableStrictTools ||
|
|
2941
4136
|
providerConfig.requestTransform ||
|
|
2942
4137
|
providerConfig.transport ||
|
|
2943
|
-
providerConfig.cacheRetention
|
|
4138
|
+
providerConfig.cacheRetention ||
|
|
4139
|
+
providerConfig.streamIdleTimeoutMs !== undefined
|
|
2944
4140
|
) {
|
|
2945
4141
|
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
2946
|
-
|
|
4142
|
+
const providerOverride: ProviderOverride = {
|
|
2947
4143
|
api: providerConfig.api as Api | undefined,
|
|
2948
4144
|
baseUrl: providerConfig.baseUrl,
|
|
2949
4145
|
headers: providerConfig.headers,
|
|
@@ -2953,8 +4149,14 @@ export class ModelRegistry {
|
|
|
2953
4149
|
transport: providerConfig.transport,
|
|
2954
4150
|
requestTransform: providerConfig.requestTransform,
|
|
2955
4151
|
cacheRetention: providerConfig.cacheRetention,
|
|
4152
|
+
streamIdleTimeoutMs: providerConfig.streamIdleTimeoutMs,
|
|
2956
4153
|
isOAuth,
|
|
2957
|
-
}
|
|
4154
|
+
};
|
|
4155
|
+
const existingOverride = overrides.get(providerName);
|
|
4156
|
+
overrides.set(
|
|
4157
|
+
providerName,
|
|
4158
|
+
existingOverride ? this.#mergeProviderOverride(existingOverride, providerOverride) : providerOverride,
|
|
4159
|
+
);
|
|
2958
4160
|
}
|
|
2959
4161
|
|
|
2960
4162
|
if (authMode === "none") {
|
|
@@ -3006,22 +4208,20 @@ export class ModelRegistry {
|
|
|
3006
4208
|
});
|
|
3007
4209
|
}
|
|
3008
4210
|
|
|
3009
|
-
// Store API
|
|
3010
|
-
//
|
|
3011
|
-
//
|
|
3012
|
-
// must authenticate the outbound request.
|
|
4211
|
+
// Store API keys for fallback resolution and register config overrides.
|
|
4212
|
+
// A pin in any session suppresses shared config keys so a reload cannot
|
|
4213
|
+
// override another session's selected account.
|
|
3013
4214
|
if (providerConfig.apiKey || providerConfig.apiKeyEnv) {
|
|
3014
|
-
const resolved =
|
|
3015
|
-
?
|
|
3016
|
-
: providerConfig.
|
|
3017
|
-
?
|
|
3018
|
-
:
|
|
4215
|
+
const resolved = credentialPinActive
|
|
4216
|
+
? undefined
|
|
4217
|
+
: providerConfig.apiKey
|
|
4218
|
+
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
4219
|
+
: providerConfig.apiKeyEnv
|
|
4220
|
+
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
4221
|
+
: undefined;
|
|
3019
4222
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
3020
4223
|
if (resolved) {
|
|
3021
|
-
this
|
|
3022
|
-
envSourced: !providerConfig.apiKey,
|
|
3023
|
-
owner: this.#authStorageConfigOwner,
|
|
3024
|
-
});
|
|
4224
|
+
this.#setOwnedConfigApiKey(providerName, resolved, !providerConfig.apiKey);
|
|
3025
4225
|
}
|
|
3026
4226
|
}
|
|
3027
4227
|
|
|
@@ -3205,8 +4405,15 @@ export class ModelRegistry {
|
|
|
3205
4405
|
}
|
|
3206
4406
|
|
|
3207
4407
|
async #writeCheckedModelsConfig(config: ModelsConfig): Promise<void> {
|
|
3208
|
-
|
|
3209
|
-
|
|
4408
|
+
const configPath = this.#modelsConfigFile.path();
|
|
4409
|
+
const parentPath = path.dirname(configPath);
|
|
4410
|
+
await fs.mkdir(parentPath, { recursive: true, mode: 0o700 });
|
|
4411
|
+
// The owner-only writer rejects a symlink in any path component and rejects
|
|
4412
|
+
// "..". Resolve only the parent so an alias directory and a lexical ".."
|
|
4413
|
+
// still reach the same file. The final name stays lexical, so a symlink
|
|
4414
|
+
// at models.yml is still opened with O_NOFOLLOW.
|
|
4415
|
+
const parent = await fs.realpath(parentPath);
|
|
4416
|
+
writeExplicitOwnerOnlyTextSync(path.join(parent, path.basename(configPath)), Bun.YAML.stringify(config, null, 2));
|
|
3210
4417
|
this.#modelsConfigFile.invalidate();
|
|
3211
4418
|
this.#reloadStaticModels();
|
|
3212
4419
|
}
|
|
@@ -3236,6 +4443,12 @@ export class ModelRegistry {
|
|
|
3236
4443
|
credentialSessionId?: string,
|
|
3237
4444
|
): Promise<void> {
|
|
3238
4445
|
const profileAvailabilityEvidenceBefore = this.#profileAvailabilityEvidenceFingerprint();
|
|
4446
|
+
const googleDescriptorGenerationsBefore = new Map(
|
|
4447
|
+
(["google-antigravity", "google-gemini-cli"] as const).map(provider => [
|
|
4448
|
+
provider,
|
|
4449
|
+
this.#descriptorDiscoveryGenerations.get(provider) ?? 0,
|
|
4450
|
+
]),
|
|
4451
|
+
);
|
|
3239
4452
|
const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
|
|
3240
4453
|
const selectedDiscoverableProviders = (
|
|
3241
4454
|
providerFilter
|
|
@@ -3387,6 +4600,38 @@ export class ModelRegistry {
|
|
|
3387
4600
|
return false;
|
|
3388
4601
|
}
|
|
3389
4602
|
});
|
|
4603
|
+
let sessionCatalogChanged = false;
|
|
4604
|
+
for (const provider of ["google-antigravity", "google-gemini-cli"] as const) {
|
|
4605
|
+
if (
|
|
4606
|
+
this.authStorage.getEffectiveCredentialType(provider, credentialSessionId, {
|
|
4607
|
+
owner: this.#authStorageConfigOwner,
|
|
4608
|
+
}) !== "oauth"
|
|
4609
|
+
) {
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
if (
|
|
4613
|
+
(this.#descriptorDiscoveryGenerations.get(provider) ?? 0) ===
|
|
4614
|
+
googleDescriptorGenerationsBefore.get(provider)
|
|
4615
|
+
) {
|
|
4616
|
+
continue;
|
|
4617
|
+
}
|
|
4618
|
+
const evidence = this.#descriptorDiscoveryEvidence.get(provider);
|
|
4619
|
+
if (!evidence) continue;
|
|
4620
|
+
const identity = this.#googleDescriptorContextIdentity(
|
|
4621
|
+
provider,
|
|
4622
|
+
evidence.authGeneration,
|
|
4623
|
+
evidence.endpoint,
|
|
4624
|
+
evidence.projectId,
|
|
4625
|
+
);
|
|
4626
|
+
if (!identity || identity !== this.#googleSessionDescriptorIdentity(provider, credentialSessionId)) continue;
|
|
4627
|
+
const catalogs = this.#googleSessionDiscoveryCatalogs.get(provider) ?? new Map();
|
|
4628
|
+
const models = currentBuiltInDiscovered.filter(model => model.provider === provider);
|
|
4629
|
+
const previous = catalogs.get(identity);
|
|
4630
|
+
if (models.length === 0 && evidence.fresh === false && previous !== undefined) continue;
|
|
4631
|
+
if (!isDeepStrictEqual(previous?.models, models)) sessionCatalogChanged = true;
|
|
4632
|
+
catalogs.set(identity, { models });
|
|
4633
|
+
this.#googleSessionDiscoveryCatalogs.set(provider, catalogs);
|
|
4634
|
+
}
|
|
3390
4635
|
const configuredDiscoveryEvidence = new Map(
|
|
3391
4636
|
currentConfiguredDiscoveryResults
|
|
3392
4637
|
.filter(result => result.current)
|
|
@@ -3454,6 +4699,10 @@ export class ModelRegistry {
|
|
|
3454
4699
|
);
|
|
3455
4700
|
});
|
|
3456
4701
|
}
|
|
4702
|
+
if (sessionCatalogChanged) {
|
|
4703
|
+
this.#invalidateAvailableModels();
|
|
4704
|
+
this.#notifyCatalogChanged();
|
|
4705
|
+
}
|
|
3457
4706
|
if (discovered.length === 0) {
|
|
3458
4707
|
if (
|
|
3459
4708
|
clearPublishedModelIds.size > 0 ||
|
|
@@ -3466,6 +4715,53 @@ export class ModelRegistry {
|
|
|
3466
4715
|
this.#mergeDiscoveredModels(discovered);
|
|
3467
4716
|
}
|
|
3468
4717
|
|
|
4718
|
+
#googleDescriptorContextIdentity(
|
|
4719
|
+
provider: "google-antigravity" | "google-gemini-cli",
|
|
4720
|
+
authGeneration: string,
|
|
4721
|
+
endpoint: string,
|
|
4722
|
+
projectId?: string,
|
|
4723
|
+
): string | undefined {
|
|
4724
|
+
return fingerprintDescriptorDiscoveryProvenance(provider, authGeneration, endpoint, projectId);
|
|
4725
|
+
}
|
|
4726
|
+
|
|
4727
|
+
#googleSessionDescriptorIdentity(
|
|
4728
|
+
provider: "google-antigravity" | "google-gemini-cli",
|
|
4729
|
+
credentialSessionId?: string,
|
|
4730
|
+
): string | undefined {
|
|
4731
|
+
if (
|
|
4732
|
+
this.authStorage.getEffectiveCredentialType(provider, credentialSessionId, {
|
|
4733
|
+
owner: this.#authStorageConfigOwner,
|
|
4734
|
+
}) !== "oauth"
|
|
4735
|
+
) {
|
|
4736
|
+
return undefined;
|
|
4737
|
+
}
|
|
4738
|
+
const credential = this.authStorage.getOAuthCredential(provider, credentialSessionId, {
|
|
4739
|
+
owner: this.#authStorageConfigOwner,
|
|
4740
|
+
});
|
|
4741
|
+
if (!credential) return undefined;
|
|
4742
|
+
const endpoint = this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "");
|
|
4743
|
+
try {
|
|
4744
|
+
return this.#googleDescriptorContextIdentity(
|
|
4745
|
+
provider,
|
|
4746
|
+
this.#getProviderEvidenceGeneration(provider, credential.access),
|
|
4747
|
+
endpoint,
|
|
4748
|
+
credential.projectId?.trim() || undefined,
|
|
4749
|
+
);
|
|
4750
|
+
} catch {
|
|
4751
|
+
return undefined;
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
#getGoogleSessionDiscoveryModels(credentialSessionId?: string): Model<Api>[] {
|
|
4756
|
+
const models: Model<Api>[] = [];
|
|
4757
|
+
for (const provider of ["google-antigravity", "google-gemini-cli"] as const) {
|
|
4758
|
+
const identity = this.#googleSessionDescriptorIdentity(provider, credentialSessionId);
|
|
4759
|
+
if (!identity) continue;
|
|
4760
|
+
models.push(...(this.#googleSessionDiscoveryCatalogs.get(provider)?.get(identity)?.models ?? []));
|
|
4761
|
+
}
|
|
4762
|
+
return models;
|
|
4763
|
+
}
|
|
4764
|
+
|
|
3469
4765
|
#profileAvailabilityEvidenceFingerprint(): string {
|
|
3470
4766
|
return JSON.stringify({
|
|
3471
4767
|
descriptor: [...this.#descriptorDiscoveryEvidence.entries()]
|
|
@@ -3475,6 +4771,7 @@ export class ModelRegistry {
|
|
|
3475
4771
|
evidence.fresh,
|
|
3476
4772
|
evidence.authGeneration,
|
|
3477
4773
|
evidence.endpoint,
|
|
4774
|
+
evidence.projectId,
|
|
3478
4775
|
[...evidence.modelIds].sort(),
|
|
3479
4776
|
]),
|
|
3480
4777
|
configured: [...this.#configuredDiscoveryEvidence.entries()]
|
|
@@ -3488,23 +4785,37 @@ export class ModelRegistry {
|
|
|
3488
4785
|
});
|
|
3489
4786
|
}
|
|
3490
4787
|
|
|
4788
|
+
#removeDescriptorDiscoveredModels(provider: string, modelIds: ReadonlySet<string>): boolean {
|
|
4789
|
+
const staticModelIds = this.#providerActivity.get(provider)?.staticModelIds;
|
|
4790
|
+
const previousLength = this.#models.length;
|
|
4791
|
+
this.#models = this.#models.filter(model => {
|
|
4792
|
+
if (model.provider !== provider || !modelIds.has(model.id)) return true;
|
|
4793
|
+
return staticModelIds?.has(model.id) === true;
|
|
4794
|
+
});
|
|
4795
|
+
return this.#models.length !== previousLength;
|
|
4796
|
+
}
|
|
4797
|
+
|
|
3491
4798
|
#mergeDiscoveredModels(discovered: readonly Model<Api>[]): void {
|
|
4799
|
+
this.#models = this.#resolveDiscoveredModelCatalog(this.#models, discovered);
|
|
4800
|
+
this.#rebuildCanonicalIndex();
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4803
|
+
#resolveDiscoveredModelCatalog(baseModels: readonly Model<Api>[], discovered: readonly Model<Api>[]): Model<Api>[] {
|
|
3492
4804
|
const discoveredModels = this.#applyHardcodedModelPolicies(
|
|
3493
4805
|
discovered.map(model =>
|
|
3494
4806
|
mergeDiscoveredModel(
|
|
3495
4807
|
model,
|
|
3496
|
-
|
|
4808
|
+
baseModels.find(candidate => candidate.provider === model.provider && candidate.id === model.id),
|
|
3497
4809
|
this.#resolveProviderOverride(model.provider),
|
|
3498
4810
|
),
|
|
3499
4811
|
),
|
|
3500
4812
|
);
|
|
3501
|
-
const resolved = this.#mergeResolvedModels(
|
|
4813
|
+
const resolved = this.#mergeResolvedModels([...baseModels], discoveredModels);
|
|
3502
4814
|
const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
|
|
3503
4815
|
// Merge runtime extension models so they survive online discovery completion
|
|
3504
4816
|
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
3505
4817
|
const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
3506
|
-
|
|
3507
|
-
this.#rebuildCanonicalIndex();
|
|
4818
|
+
return this.#finalizeModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
|
|
3508
4819
|
}
|
|
3509
4820
|
|
|
3510
4821
|
async #discoverProviderModels(
|
|
@@ -3835,19 +5146,39 @@ export class ModelRegistry {
|
|
|
3835
5146
|
{
|
|
3836
5147
|
providerId: "google-antigravity",
|
|
3837
5148
|
resolveKey: extractGoogleOAuthToken,
|
|
3838
|
-
|
|
5149
|
+
resolveProjectId: (oauthToken, rawValue) =>
|
|
5150
|
+
resolveGoogleOAuthProjectId(
|
|
5151
|
+
this.authStorage,
|
|
5152
|
+
"google-antigravity",
|
|
5153
|
+
oauthToken,
|
|
5154
|
+
rawValue,
|
|
5155
|
+
credentialSessionId,
|
|
5156
|
+
this.#authStorageConfigOwner,
|
|
5157
|
+
),
|
|
5158
|
+
createOptions: (oauthToken, projectId) =>
|
|
3839
5159
|
googleAntigravityModelManagerOptions({
|
|
3840
5160
|
oauthToken,
|
|
3841
5161
|
endpoint: this.getProviderBaseUrl("google-antigravity"),
|
|
5162
|
+
project: projectId,
|
|
3842
5163
|
}),
|
|
3843
5164
|
},
|
|
3844
5165
|
{
|
|
3845
5166
|
providerId: "google-gemini-cli",
|
|
3846
5167
|
resolveKey: extractGoogleOAuthToken,
|
|
3847
|
-
|
|
5168
|
+
resolveProjectId: (oauthToken, rawValue) =>
|
|
5169
|
+
resolveGoogleOAuthProjectId(
|
|
5170
|
+
this.authStorage,
|
|
5171
|
+
"google-gemini-cli",
|
|
5172
|
+
oauthToken,
|
|
5173
|
+
rawValue,
|
|
5174
|
+
credentialSessionId,
|
|
5175
|
+
this.#authStorageConfigOwner,
|
|
5176
|
+
),
|
|
5177
|
+
createOptions: (oauthToken, projectId) =>
|
|
3848
5178
|
googleGeminiCliModelManagerOptions({
|
|
3849
5179
|
oauthToken,
|
|
3850
5180
|
endpoint: this.getProviderBaseUrl("google-gemini-cli"),
|
|
5181
|
+
project: projectId,
|
|
3851
5182
|
}),
|
|
3852
5183
|
},
|
|
3853
5184
|
{
|
|
@@ -3956,7 +5287,11 @@ export class ModelRegistry {
|
|
|
3956
5287
|
authGeneration === this.#getProviderEvidenceGeneration(descriptor.providerId, apiKeyValue) &&
|
|
3957
5288
|
isAuthenticated(key)
|
|
3958
5289
|
) {
|
|
3959
|
-
const
|
|
5290
|
+
const projectId = descriptor.resolveProjectId?.(key, apiKeyValue);
|
|
5291
|
+
const isCurrentContext = descriptor.resolveProjectId
|
|
5292
|
+
? () => descriptor.resolveProjectId?.(key, apiKeyValue) === projectId
|
|
5293
|
+
: undefined;
|
|
5294
|
+
const managerOptions = descriptor.createOptions(key, projectId);
|
|
3960
5295
|
const baseUrl = this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? "";
|
|
3961
5296
|
options.push({
|
|
3962
5297
|
options: managerOptions,
|
|
@@ -3964,6 +5299,8 @@ export class ModelRegistry {
|
|
|
3964
5299
|
apiKey: apiKeyValue,
|
|
3965
5300
|
endpoint: this.#normalizeDiscoveryEvidenceEndpoint(baseUrl),
|
|
3966
5301
|
endpointContainsUserinfo: this.#urlContainsUserinfo(baseUrl),
|
|
5302
|
+
projectId,
|
|
5303
|
+
...(isCurrentContext ? { isCurrentContext } : {}),
|
|
3967
5304
|
});
|
|
3968
5305
|
}
|
|
3969
5306
|
}
|
|
@@ -3971,12 +5308,28 @@ export class ModelRegistry {
|
|
|
3971
5308
|
}
|
|
3972
5309
|
|
|
3973
5310
|
async #discoverWithModelManager(
|
|
3974
|
-
{
|
|
5311
|
+
{
|
|
5312
|
+
options,
|
|
5313
|
+
authGeneration,
|
|
5314
|
+
apiKey,
|
|
5315
|
+
endpoint,
|
|
5316
|
+
endpointContainsUserinfo,
|
|
5317
|
+
projectId,
|
|
5318
|
+
isCurrentContext,
|
|
5319
|
+
}: ModelManagerDiscoveryOptions,
|
|
3975
5320
|
strategy: ModelRefreshStrategy,
|
|
3976
5321
|
providerRefresh?: ProviderRefreshFence,
|
|
3977
5322
|
): Promise<Model<Api>[]> {
|
|
3978
5323
|
const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
|
|
3979
5324
|
this.#descriptorDiscoveryGenerations.set(options.providerId, generation);
|
|
5325
|
+
const previousEvidence = this.#descriptorDiscoveryEvidence.get(options.providerId);
|
|
5326
|
+
const discoveryContextChanged =
|
|
5327
|
+
previousEvidence !== undefined &&
|
|
5328
|
+
(previousEvidence.authGeneration !== authGeneration ||
|
|
5329
|
+
previousEvidence.endpoint !== endpoint ||
|
|
5330
|
+
previousEvidence.projectId !== projectId);
|
|
5331
|
+
const isGoogleProjectScopedProvider =
|
|
5332
|
+
options.providerId === "google-antigravity" || options.providerId === "google-gemini-cli";
|
|
3980
5333
|
const canUseCredentialDerivedXiaomiEndpoint =
|
|
3981
5334
|
options.providerId === "xiaomi" &&
|
|
3982
5335
|
apiKey?.startsWith("tp-") === true &&
|
|
@@ -3986,8 +5339,7 @@ export class ModelRegistry {
|
|
|
3986
5339
|
let credentialDerivedEndpoint: string | undefined;
|
|
3987
5340
|
const reusableCacheProvenance = endpointContainsUserinfo
|
|
3988
5341
|
? undefined
|
|
3989
|
-
: fingerprintDescriptorDiscoveryProvenance(authGeneration, endpoint);
|
|
3990
|
-
if (reusableCacheProvenance === undefined && strategy === "offline") return [];
|
|
5342
|
+
: fingerprintDescriptorDiscoveryProvenance(options.providerId, authGeneration, endpoint, projectId);
|
|
3991
5343
|
// Signed/userinfo endpoints may still publish a sanitized best-effort row,
|
|
3992
5344
|
// but each refresh gets a new opaque nonce. The nonce contains no endpoint
|
|
3993
5345
|
// secret and can never match a previous invocation, so constructor loads,
|
|
@@ -3997,6 +5349,7 @@ export class ModelRegistry {
|
|
|
3997
5349
|
const isCurrentDiscoveryContext = () =>
|
|
3998
5350
|
(this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
|
|
3999
5351
|
this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
|
|
5352
|
+
(isCurrentContext?.() ?? true) &&
|
|
4000
5353
|
(endpoint ===
|
|
4001
5354
|
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(options.providerId) ?? "") ||
|
|
4002
5355
|
(canUseCredentialDerivedXiaomiEndpoint && credentialDerivedEndpoint !== undefined));
|
|
@@ -4017,6 +5370,40 @@ export class ModelRegistry {
|
|
|
4017
5370
|
? strategy !== "online-if-uncached" || isCurrentDiscovery()
|
|
4018
5371
|
: isCurrentProviderRefresh()));
|
|
4019
5372
|
try {
|
|
5373
|
+
if (!isCurrentDiscovery()) return [];
|
|
5374
|
+
if (discoveryContextChanged && previousEvidence) {
|
|
5375
|
+
const removedPreviousModels = this.#removeDescriptorDiscoveredModels(
|
|
5376
|
+
options.providerId,
|
|
5377
|
+
previousEvidence.modelIds,
|
|
5378
|
+
);
|
|
5379
|
+
if (removedPreviousModels) {
|
|
5380
|
+
this.#rebuildProviderActivity();
|
|
5381
|
+
this.#rebuildCanonicalIndex();
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
if (strategy === "offline") {
|
|
5385
|
+
const cache =
|
|
5386
|
+
reusableCacheProvenance !== undefined && isGoogleProjectScopedProvider
|
|
5387
|
+
? readModelCache<Api>(options.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath)
|
|
5388
|
+
: null;
|
|
5389
|
+
const hasCurrentProjectCache =
|
|
5390
|
+
cache?.dynamicModelIds !== undefined && cache.dynamicModelProvenance === reusableCacheProvenance;
|
|
5391
|
+
if (reusableCacheProvenance === undefined || (isGoogleProjectScopedProvider && !hasCurrentProjectCache)) {
|
|
5392
|
+
if (discoveryContextChanged) {
|
|
5393
|
+
this.#providerEvidenceApiKeys.set(options.providerId, apiKey);
|
|
5394
|
+
this.#descriptorDiscoveryEvidence.set(options.providerId, {
|
|
5395
|
+
fresh: false,
|
|
5396
|
+
modelIds: new Set(),
|
|
5397
|
+
authGeneration,
|
|
5398
|
+
endpoint,
|
|
5399
|
+
projectId,
|
|
5400
|
+
});
|
|
5401
|
+
this.#rebuildProviderActivity();
|
|
5402
|
+
this.#rebuildCanonicalIndex();
|
|
5403
|
+
}
|
|
5404
|
+
return [];
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
4020
5407
|
const manager = createModelManager({
|
|
4021
5408
|
...options,
|
|
4022
5409
|
cacheDbPath: this.#cacheDbPath,
|
|
@@ -4048,7 +5435,9 @@ export class ModelRegistry {
|
|
|
4048
5435
|
const evidence = this.#descriptorDiscoveryEvidence.get(options.providerId);
|
|
4049
5436
|
const refreshStrategy =
|
|
4050
5437
|
strategy === "online-if-uncached" &&
|
|
4051
|
-
(evidence?.authGeneration !== authGeneration ||
|
|
5438
|
+
(evidence?.authGeneration !== authGeneration ||
|
|
5439
|
+
evidence.endpoint !== endpoint ||
|
|
5440
|
+
evidence.projectId !== projectId)
|
|
4052
5441
|
? "online"
|
|
4053
5442
|
: strategy;
|
|
4054
5443
|
const result = await manager.refresh(refreshStrategy);
|
|
@@ -4063,7 +5452,7 @@ export class ModelRegistry {
|
|
|
4063
5452
|
const preservesExistingDescriptorEvidence =
|
|
4064
5453
|
(result.dynamicModelIds === undefined && !result.fetched && !result.stale) ||
|
|
4065
5454
|
(result.stale && result.cacheFresh && result.cacheAuthoritative && evidence?.fresh === true) ||
|
|
4066
|
-
(strategy === "offline" && result.dynamicModelIds === undefined);
|
|
5455
|
+
(strategy === "offline" && result.dynamicModelIds === undefined && !discoveryContextChanged);
|
|
4067
5456
|
if (
|
|
4068
5457
|
isCurrentDiscovery() &&
|
|
4069
5458
|
!preservesExistingDescriptorEvidence &&
|
|
@@ -4071,13 +5460,15 @@ export class ModelRegistry {
|
|
|
4071
5460
|
result.dynamicModelIds !== undefined ||
|
|
4072
5461
|
result.stale ||
|
|
4073
5462
|
this.#descriptorDiscoveryEvidence.get(options.providerId)?.authGeneration !== authGeneration ||
|
|
4074
|
-
this.#descriptorDiscoveryEvidence.get(options.providerId)?.endpoint !== endpoint
|
|
5463
|
+
this.#descriptorDiscoveryEvidence.get(options.providerId)?.endpoint !== endpoint ||
|
|
5464
|
+
this.#descriptorDiscoveryEvidence.get(options.providerId)?.projectId !== projectId)
|
|
4075
5465
|
) {
|
|
4076
5466
|
this.#descriptorDiscoveryEvidence.set(options.providerId, {
|
|
4077
5467
|
fresh: result.fetched,
|
|
4078
5468
|
modelIds: new Set(models.map(model => model.id)),
|
|
4079
5469
|
authGeneration,
|
|
4080
5470
|
endpoint: this.#normalizeDiscoveryEvidenceEndpoint(models[0]?.baseUrl ?? endpoint),
|
|
5471
|
+
projectId,
|
|
4081
5472
|
});
|
|
4082
5473
|
}
|
|
4083
5474
|
if (!isCurrentDiscovery()) {
|
|
@@ -4103,6 +5494,7 @@ export class ModelRegistry {
|
|
|
4103
5494
|
modelIds: new Set(),
|
|
4104
5495
|
authGeneration,
|
|
4105
5496
|
endpoint,
|
|
5497
|
+
projectId,
|
|
4106
5498
|
});
|
|
4107
5499
|
}
|
|
4108
5500
|
logger.warn("model discovery failed for provider", {
|
|
@@ -4319,36 +5711,7 @@ export class ModelRegistry {
|
|
|
4319
5711
|
modelId: string,
|
|
4320
5712
|
entry?: { id?: unknown; owned_by?: unknown },
|
|
4321
5713
|
): Api {
|
|
4322
|
-
|
|
4323
|
-
// stated the transport for these ids, so never second-guess it.
|
|
4324
|
-
let matchedPrefixLength = -1;
|
|
4325
|
-
let prefixApi: Api | undefined;
|
|
4326
|
-
for (const [prefix, routedApi] of Object.entries(providerConfig.discovery.apiByModelPrefix ?? {})) {
|
|
4327
|
-
if (modelId.startsWith(prefix) && prefix.length > matchedPrefixLength) {
|
|
4328
|
-
prefixApi = routedApi;
|
|
4329
|
-
matchedPrefixLength = prefix.length;
|
|
4330
|
-
}
|
|
4331
|
-
}
|
|
4332
|
-
if (prefixApi !== undefined) return prefixApi;
|
|
4333
|
-
// 2. For OpenAI-compatible gateways that can front both OpenAI and
|
|
4334
|
-
// Anthropic upstreams (e.g. CLIProxyAPI), auto-detect the wire family
|
|
4335
|
-
// per model from the `/v1/models` `owned_by` owner and the model id.
|
|
4336
|
-
// A mixed gateway then routes `claude-*` through Anthropic Messages
|
|
4337
|
-
// with zero manual config. OpenAI-family models keep the provider's
|
|
4338
|
-
// configured OpenAI transport (e.g. `openai-responses` vs
|
|
4339
|
-
// `openai-completions`) rather than being forced to chat-completions,
|
|
4340
|
-
// because a `/v1/models` list cannot reveal which OpenAI API the
|
|
4341
|
-
// gateway expects. Only override to `openai-completions` when the
|
|
4342
|
-
// provider default is not itself an OpenAI-family api.
|
|
4343
|
-
if (providerConfig.discovery.type === "openai-models-list") {
|
|
4344
|
-
const detected = detectDiscoveredApiFamily(entry ?? { id: modelId });
|
|
4345
|
-
if (detected === "anthropic-messages") return "anthropic-messages";
|
|
4346
|
-
if (detected === "openai-completions") {
|
|
4347
|
-
return isOpenAIFamilyApi(providerConfig.api) ? providerConfig.api : "openai-completions";
|
|
4348
|
-
}
|
|
4349
|
-
}
|
|
4350
|
-
// 3. Fall back to the provider-level default api.
|
|
4351
|
-
return providerConfig.api;
|
|
5714
|
+
return resolveModelApiFromDiscovery(providerConfig.api, providerConfig.discovery, modelId, entry);
|
|
4352
5715
|
}
|
|
4353
5716
|
|
|
4354
5717
|
async #discoverModelsDevProvider(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
@@ -4823,6 +6186,7 @@ export class ModelRegistry {
|
|
|
4823
6186
|
transport: override.transport ?? baseOverride?.transport,
|
|
4824
6187
|
requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
|
|
4825
6188
|
cacheRetention: override.cacheRetention ?? baseOverride?.cacheRetention,
|
|
6189
|
+
streamIdleTimeoutMs: override.streamIdleTimeoutMs ?? baseOverride?.streamIdleTimeoutMs,
|
|
4826
6190
|
isOAuth: override.isOAuth ?? baseOverride?.isOAuth,
|
|
4827
6191
|
};
|
|
4828
6192
|
}
|
|
@@ -4838,6 +6202,7 @@ export class ModelRegistry {
|
|
|
4838
6202
|
| "transport"
|
|
4839
6203
|
| "requestTransform"
|
|
4840
6204
|
| "cacheRetention"
|
|
6205
|
+
| "streamIdleTimeoutMs"
|
|
4841
6206
|
>,
|
|
4842
6207
|
): T {
|
|
4843
6208
|
const overrideHeaders = (
|
|
@@ -4872,6 +6237,8 @@ export class ModelRegistry {
|
|
|
4872
6237
|
delete overrideHeaders[GENERATED_AUTH_HEADER];
|
|
4873
6238
|
}
|
|
4874
6239
|
const headers = mergeAuthHeader(overrideHeaders, canInject, canInject ? override.apiKey : undefined);
|
|
6240
|
+
const streamIdleTimeoutMs =
|
|
6241
|
+
entry.streamIdleTimeoutMs ?? (isRequestTransformApi(entry.api) ? override.streamIdleTimeoutMs : undefined);
|
|
4875
6242
|
const result = {
|
|
4876
6243
|
...entry,
|
|
4877
6244
|
compat: mergeProviderCompat(entry.compat, override.compat),
|
|
@@ -4885,6 +6252,7 @@ export class ModelRegistry {
|
|
|
4885
6252
|
override.requestTransform,
|
|
4886
6253
|
),
|
|
4887
6254
|
cacheRetention: entry.cacheRetention ?? override.cacheRetention,
|
|
6255
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
4888
6256
|
};
|
|
4889
6257
|
if (!canInject && explicitAuthKey !== undefined) {
|
|
4890
6258
|
delete (result.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
|
|
@@ -5106,9 +6474,7 @@ export class ModelRegistry {
|
|
|
5106
6474
|
}
|
|
5107
6475
|
|
|
5108
6476
|
#invalidateAvailableModels(): void {
|
|
5109
|
-
this.#availableModelsCache
|
|
5110
|
-
this.#availableModelsDisabledProviders = undefined;
|
|
5111
|
-
this.#availableModelsEnvFingerprint = undefined;
|
|
6477
|
+
this.#availableModelsCache.clear();
|
|
5112
6478
|
}
|
|
5113
6479
|
|
|
5114
6480
|
#notifyCatalogChanged(): void {
|
|
@@ -5143,19 +6509,17 @@ export class ModelRegistry {
|
|
|
5143
6509
|
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
5144
6510
|
const modelDefs = providerConfig.models ?? [];
|
|
5145
6511
|
if (modelDefs.length === 0) continue; // Override-only, no custom models
|
|
5146
|
-
|
|
5147
|
-
|
|
6512
|
+
const credentialPinActive = !this.#stagedReloadCandidate && this.authStorage.hasAnyCredentialPin(providerName);
|
|
6513
|
+
const apiKey = credentialPinActive
|
|
6514
|
+
? undefined
|
|
6515
|
+
: providerConfig.apiKey
|
|
5148
6516
|
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
5149
6517
|
: providerConfig.apiKeyEnv
|
|
5150
6518
|
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
5151
6519
|
: undefined;
|
|
5152
|
-
|
|
5153
|
-
if (
|
|
5154
|
-
|
|
5155
|
-
envSourced: !providerConfig.apiKey,
|
|
5156
|
-
owner: this.#authStorageConfigOwner,
|
|
5157
|
-
});
|
|
5158
|
-
}
|
|
6520
|
+
if (providerConfig.apiKey || providerConfig.apiKeyEnv) {
|
|
6521
|
+
if (apiKey) this.#customProviderApiKeys.set(providerName, apiKey);
|
|
6522
|
+
if (apiKey) this.#setOwnedConfigApiKey(providerName, apiKey, !providerConfig.apiKey);
|
|
5159
6523
|
}
|
|
5160
6524
|
for (const modelDef of modelDefs) {
|
|
5161
6525
|
const providerCompat = providerConfig.disableStrictTools
|
|
@@ -5166,11 +6530,7 @@ export class ModelRegistry {
|
|
|
5166
6530
|
providerConfig.baseUrl!,
|
|
5167
6531
|
providerConfig.api as Api | undefined,
|
|
5168
6532
|
providerConfig.headers,
|
|
5169
|
-
|
|
5170
|
-
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
5171
|
-
: providerConfig.apiKeyEnv
|
|
5172
|
-
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
5173
|
-
: undefined,
|
|
6533
|
+
apiKey,
|
|
5174
6534
|
providerConfig.authHeader,
|
|
5175
6535
|
providerCompat,
|
|
5176
6536
|
providerConfig.requestTransform,
|
|
@@ -5178,6 +6538,7 @@ export class ModelRegistry {
|
|
|
5178
6538
|
providerConfig.cacheRetention,
|
|
5179
6539
|
providerConfig.transport,
|
|
5180
6540
|
modelDef as CustomModelDefinitionLike,
|
|
6541
|
+
providerConfig.streamIdleTimeoutMs,
|
|
5181
6542
|
);
|
|
5182
6543
|
if (!model) continue;
|
|
5183
6544
|
models.push(model);
|
|
@@ -5188,10 +6549,11 @@ export class ModelRegistry {
|
|
|
5188
6549
|
|
|
5189
6550
|
/**
|
|
5190
6551
|
* Get all models (embedded + accepted registry + custom).
|
|
6552
|
+
* Supplying a credential session selects only that session's Google OAuth discovery catalog.
|
|
5191
6553
|
* If models.yml had errors, returns embedded plus accepted registry models.
|
|
5192
6554
|
*/
|
|
5193
|
-
getAll(): Model<Api>[] {
|
|
5194
|
-
return this.#models;
|
|
6555
|
+
getAll(credentialSessionId?: string): Model<Api>[] {
|
|
6556
|
+
return credentialSessionId === undefined ? this.#models : this.#getCredentialScopedCatalog(credentialSessionId);
|
|
5195
6557
|
}
|
|
5196
6558
|
|
|
5197
6559
|
/** Provider ids declared in models.yml, including override-only providers. */
|
|
@@ -5199,6 +6561,67 @@ export class ModelRegistry {
|
|
|
5199
6561
|
return [...this.#configuredProviderIds];
|
|
5200
6562
|
}
|
|
5201
6563
|
|
|
6564
|
+
/** Configured API-key sources in models.yml precedence order, excluding absent environment values. */
|
|
6565
|
+
getProviderConfigApiKeySources(
|
|
6566
|
+
provider: string,
|
|
6567
|
+
): Array<{ kind: "environment"; name: string; provider: string } | { kind: "models-yml"; provider: string }> {
|
|
6568
|
+
const normalizedProvider = resolveOAuthStorageProvider(provider);
|
|
6569
|
+
const configuredProviders = this.#acceptedConfigValue?.providers;
|
|
6570
|
+
const configuredAliases = Object.entries(configuredProviders ?? {}).filter(
|
|
6571
|
+
([configuredProvider]) => resolveOAuthStorageProvider(configuredProvider) === normalizedProvider,
|
|
6572
|
+
);
|
|
6573
|
+
const sources: Array<
|
|
6574
|
+
{ kind: "environment"; name: string; provider: string } | { kind: "models-yml"; provider: string }
|
|
6575
|
+
> = [];
|
|
6576
|
+
const addSource = (
|
|
6577
|
+
configuredProvider: string,
|
|
6578
|
+
apiKey: string | undefined,
|
|
6579
|
+
apiKeyEnv: string | undefined,
|
|
6580
|
+
): void => {
|
|
6581
|
+
if (apiKey) {
|
|
6582
|
+
if (Bun.env[apiKey]) sources.push({ kind: "environment", name: apiKey, provider: configuredProvider });
|
|
6583
|
+
else sources.push({ kind: "models-yml", provider: configuredProvider });
|
|
6584
|
+
return;
|
|
6585
|
+
}
|
|
6586
|
+
if (apiKeyEnv && $rotatingCredentialEnv(apiKeyEnv)) {
|
|
6587
|
+
sources.push({ kind: "environment", name: apiKeyEnv, provider: configuredProvider });
|
|
6588
|
+
}
|
|
6589
|
+
};
|
|
6590
|
+
for (const [configuredProvider, configured] of configuredAliases) {
|
|
6591
|
+
addSource(configuredProvider, configured.apiKey, configured.apiKeyEnv);
|
|
6592
|
+
addSource(configuredProvider, configured.openaiCompat?.apiKey, configured.openaiCompat?.apiKeyEnv);
|
|
6593
|
+
}
|
|
6594
|
+
return sources.filter(
|
|
6595
|
+
(source, index) =>
|
|
6596
|
+
source.kind === "models-yml" ||
|
|
6597
|
+
sources.findIndex(
|
|
6598
|
+
candidate =>
|
|
6599
|
+
candidate.kind === "environment" &&
|
|
6600
|
+
candidate.name === source.name &&
|
|
6601
|
+
candidate.provider === source.provider,
|
|
6602
|
+
) === index,
|
|
6603
|
+
);
|
|
6604
|
+
}
|
|
6605
|
+
|
|
6606
|
+
/** Environment variable name for a provider API key supplied by runtime setup. */
|
|
6607
|
+
getProviderRuntimeApiKeyEnvName(provider: string): string | undefined {
|
|
6608
|
+
const normalizedProvider = resolveOAuthStorageProvider(provider);
|
|
6609
|
+
return (
|
|
6610
|
+
this.#runtimeProviderApiKeyEnvNames.get(provider) ??
|
|
6611
|
+
[...this.#runtimeProviderApiKeyEnvNames].find(
|
|
6612
|
+
([registeredProvider]) => resolveOAuthStorageProvider(registeredProvider) === normalizedProvider,
|
|
6613
|
+
)?.[1]
|
|
6614
|
+
);
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
/** Whether a runtime extension's provider API key is currently installed in AuthStorage. */
|
|
6618
|
+
hasRuntimeProviderApiKey(provider: string): boolean {
|
|
6619
|
+
const normalizedProvider = resolveOAuthStorageProvider(provider);
|
|
6620
|
+
return [...this.#runtimeProviderCredentialInstalled].some(
|
|
6621
|
+
registeredProvider => resolveOAuthStorageProvider(registeredProvider) === normalizedProvider,
|
|
6622
|
+
);
|
|
6623
|
+
}
|
|
6624
|
+
|
|
5202
6625
|
/**
|
|
5203
6626
|
* Whether this registry can serve a provider id from any active source.
|
|
5204
6627
|
*
|
|
@@ -5218,12 +6641,16 @@ export class ModelRegistry {
|
|
|
5218
6641
|
);
|
|
5219
6642
|
}
|
|
5220
6643
|
|
|
5221
|
-
#isModelAvailable(
|
|
6644
|
+
#isModelAvailable(
|
|
6645
|
+
model: Model<Api>,
|
|
6646
|
+
disabledProviders?: ReadonlySet<string>,
|
|
6647
|
+
credentialSessionId?: string,
|
|
6648
|
+
): boolean {
|
|
5222
6649
|
const disabled = disabledProviders ?? getDisabledProviderIdsFromSettings(this.#settings);
|
|
5223
6650
|
return (
|
|
5224
6651
|
!disabled.has(model.provider) &&
|
|
5225
6652
|
(this.#keylessProviders.has(model.provider) ||
|
|
5226
|
-
this.authStorage.hasAuth(model.provider,
|
|
6653
|
+
this.authStorage.hasAuth(model.provider, credentialSessionId, { owner: this.#authStorageConfigOwner }))
|
|
5227
6654
|
);
|
|
5228
6655
|
}
|
|
5229
6656
|
/** Per-query precomputed variant filter inputs; plan fields are authoritative when provided. */
|
|
@@ -5658,23 +7085,54 @@ export class ModelRegistry {
|
|
|
5658
7085
|
* is authoritative for visibility: live discovery only enriches entries and
|
|
5659
7086
|
* adds unknown ids, and never removes a bundled model. A provider that cannot
|
|
5660
7087
|
* serve a model reports its own error when the model is used.
|
|
7088
|
+
* Without a credential session, project-scoped Google OAuth discoveries are omitted.
|
|
5661
7089
|
*/
|
|
5662
|
-
getAvailable(): Model<Api>[] {
|
|
7090
|
+
getAvailable(credentialSessionId?: string): Model<Api>[] {
|
|
5663
7091
|
this.#synchronizeEnvironmentCredentials();
|
|
5664
7092
|
const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
|
|
5665
7093
|
const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
|
|
5666
7094
|
const envFingerprint = envAvailabilityFingerprint(this.#configuredApiKeyEnvNames);
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
this.#availableModelsCache
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
7095
|
+
const cacheKey = credentialSessionId ?? "";
|
|
7096
|
+
const cached = this.#availableModelsCache.get(cacheKey);
|
|
7097
|
+
if (cached?.disabledProviders === disabledProviderKey && cached.envFingerprint === envFingerprint) {
|
|
7098
|
+
return cached.models;
|
|
7099
|
+
}
|
|
7100
|
+
const catalog = this.#getCredentialScopedCatalog(credentialSessionId);
|
|
7101
|
+
const models = catalog.filter(model => this.#isModelAvailable(model, disabledProviders, credentialSessionId));
|
|
7102
|
+
this.#availableModelsCache.set(cacheKey, {
|
|
7103
|
+
models,
|
|
7104
|
+
disabledProviders: disabledProviderKey,
|
|
7105
|
+
envFingerprint,
|
|
7106
|
+
});
|
|
7107
|
+
return models;
|
|
7108
|
+
}
|
|
7109
|
+
|
|
7110
|
+
#getCredentialScopedCatalog(credentialSessionId?: string): Model<Api>[] {
|
|
7111
|
+
const sessionScopedProviders = new Set(
|
|
7112
|
+
(["google-antigravity", "google-gemini-cli"] as const).filter(
|
|
7113
|
+
provider =>
|
|
7114
|
+
this.authStorage.getEffectiveCredentialType(provider, credentialSessionId, {
|
|
7115
|
+
owner: this.#authStorageConfigOwner,
|
|
7116
|
+
}) === "oauth",
|
|
7117
|
+
),
|
|
7118
|
+
);
|
|
7119
|
+
const commonModels = this.#models.filter(model => {
|
|
7120
|
+
if (model.provider !== "google-antigravity" && model.provider !== "google-gemini-cli") {
|
|
7121
|
+
return true;
|
|
7122
|
+
}
|
|
7123
|
+
if (!sessionScopedProviders.has(model.provider) || this.#discoveryManager.providerIds().has(model.provider))
|
|
7124
|
+
return true;
|
|
7125
|
+
return this.#providerActivity.get(model.provider)?.staticModelIds.has(model.id) === true;
|
|
7126
|
+
});
|
|
7127
|
+
// Commands such as `--list-models` read without a credential session. The
|
|
7128
|
+
// effective (default) credential still has a descriptor identity, so its
|
|
7129
|
+
// discovery overlay must be visible here instead of being dropped.
|
|
7130
|
+
const scopedDiscovered = this.#getGoogleSessionDiscoveryModels(credentialSessionId);
|
|
7131
|
+
const catalog =
|
|
7132
|
+
scopedDiscovered.length > 0
|
|
7133
|
+
? this.#resolveDiscoveredModelCatalog(commonModels, scopedDiscovered)
|
|
7134
|
+
: commonModels;
|
|
7135
|
+
return catalog;
|
|
5678
7136
|
}
|
|
5679
7137
|
|
|
5680
7138
|
#synchronizeEnvironmentCredentials(): void {
|
|
@@ -5860,8 +7318,9 @@ export class ModelRegistry {
|
|
|
5860
7318
|
/**
|
|
5861
7319
|
* Find a model by provider and ID.
|
|
5862
7320
|
*/
|
|
5863
|
-
find(provider: string, modelId: string): Model<Api> | undefined {
|
|
5864
|
-
|
|
7321
|
+
find(provider: string, modelId: string, credentialSessionId?: string): Model<Api> | undefined {
|
|
7322
|
+
const models = credentialSessionId === undefined ? this.#models : this.getAll(credentialSessionId);
|
|
7323
|
+
return resolveProviderModelReference(provider, modelId, models);
|
|
5865
7324
|
}
|
|
5866
7325
|
|
|
5867
7326
|
/**
|
|
@@ -5952,6 +7411,35 @@ export class ModelRegistry {
|
|
|
5952
7411
|
return apiKey;
|
|
5953
7412
|
}
|
|
5954
7413
|
|
|
7414
|
+
/** Get a model API key together with the stored row that resolved it, when any. */
|
|
7415
|
+
async getApiKeyWithCredentialRowId(
|
|
7416
|
+
model: Model<Api>,
|
|
7417
|
+
sessionId?: string,
|
|
7418
|
+
options: {
|
|
7419
|
+
credentialSelector?: AuthCredentialSelector;
|
|
7420
|
+
preferredCredentialSelector?: AuthCredentialSelector;
|
|
7421
|
+
signal?: AbortSignal;
|
|
7422
|
+
} = {},
|
|
7423
|
+
): Promise<{ apiKey: string | undefined; credentialRowId: number | undefined }> {
|
|
7424
|
+
this.#refreshRotatingConfigApiKey(model.provider);
|
|
7425
|
+
let credentialRowId: number | undefined;
|
|
7426
|
+
const apiKey = await this.#getApiKeyOrNoAuth(model.provider, async () => {
|
|
7427
|
+
const resolved = await this.authStorage.getApiKeyWithCredentialRowId(model.provider, sessionId, {
|
|
7428
|
+
baseUrl: model.baseUrl,
|
|
7429
|
+
modelId: model.id,
|
|
7430
|
+
credentialSelector: options.credentialSelector,
|
|
7431
|
+
preferredCredentialSelector: options.preferredCredentialSelector,
|
|
7432
|
+
owner: this.#authStorageConfigOwner,
|
|
7433
|
+
signal: options.signal,
|
|
7434
|
+
});
|
|
7435
|
+
credentialRowId = isVllmNoAuthToken(model.provider, resolved.apiKey) ? undefined : resolved.credentialRowId;
|
|
7436
|
+
return resolved.apiKey;
|
|
7437
|
+
});
|
|
7438
|
+
if (apiKey === kNoAuth) credentialRowId = undefined;
|
|
7439
|
+
this.#applyEffectiveAuthHeader(model, apiKey);
|
|
7440
|
+
return { apiKey, credentialRowId };
|
|
7441
|
+
}
|
|
7442
|
+
|
|
5955
7443
|
/**
|
|
5956
7444
|
* Get API key for a provider (e.g., "openai").
|
|
5957
7445
|
*/
|
|
@@ -5986,6 +7474,19 @@ export class ModelRegistry {
|
|
|
5986
7474
|
return apiKey;
|
|
5987
7475
|
}
|
|
5988
7476
|
|
|
7477
|
+
/** Check credential availability without resolving keys or refreshing OAuth tokens. */
|
|
7478
|
+
hasUsableAuthForProvider(provider: string, sessionId?: string): boolean {
|
|
7479
|
+
return this.authStorage.hasUsableAuth(provider, {
|
|
7480
|
+
owner: this.#authStorageConfigOwner,
|
|
7481
|
+
sessionId,
|
|
7482
|
+
});
|
|
7483
|
+
}
|
|
7484
|
+
|
|
7485
|
+
/** @internal Whether this registry is an unpublished staged reload candidate. */
|
|
7486
|
+
isStagedReloadCandidate(): boolean {
|
|
7487
|
+
return this.#stagedReloadCandidate;
|
|
7488
|
+
}
|
|
7489
|
+
|
|
5989
7490
|
#refreshRotatingConfigApiKey(provider: string, forceStatic = false): void {
|
|
5990
7491
|
const runtimeOwned = !forceStatic && this.#runtimeProviderApiKeys.has(provider);
|
|
5991
7492
|
const envName = runtimeOwned
|
|
@@ -5993,6 +7494,7 @@ export class ModelRegistry {
|
|
|
5993
7494
|
: this.#customProviderApiKeyEnvNames.get(provider);
|
|
5994
7495
|
if (!envName) return;
|
|
5995
7496
|
const resolved = $rotatingCredentialEnv(envName);
|
|
7497
|
+
if (resolved !== undefined && this.authStorage.hasAnyCredentialPin(provider)) return;
|
|
5996
7498
|
const previous = runtimeOwned
|
|
5997
7499
|
? this.#runtimeProviderResolvedApiKeys.get(provider)
|
|
5998
7500
|
: this.#customProviderApiKeys.get(provider);
|
|
@@ -6006,7 +7508,7 @@ export class ModelRegistry {
|
|
|
6006
7508
|
return;
|
|
6007
7509
|
}
|
|
6008
7510
|
this.#customProviderApiKeys.delete(provider);
|
|
6009
|
-
this
|
|
7511
|
+
this.#removeOwnedConfigApiKey(provider);
|
|
6010
7512
|
} else {
|
|
6011
7513
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
6012
7514
|
if (runtimeOwned) {
|
|
@@ -6014,12 +7516,9 @@ export class ModelRegistry {
|
|
|
6014
7516
|
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
6015
7517
|
}
|
|
6016
7518
|
if (runtimeOwned) {
|
|
6017
|
-
this
|
|
7519
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
6018
7520
|
} else {
|
|
6019
|
-
this
|
|
6020
|
-
envSourced: true,
|
|
6021
|
-
owner: this.#authStorageConfigOwner,
|
|
6022
|
-
});
|
|
7521
|
+
this.#setOwnedConfigApiKey(provider, resolved, true);
|
|
6023
7522
|
}
|
|
6024
7523
|
}
|
|
6025
7524
|
const authHeader =
|
|
@@ -6166,7 +7665,7 @@ export class ModelRegistry {
|
|
|
6166
7665
|
this.#runtimeProviderOverrides.delete(providerName);
|
|
6167
7666
|
this.#runtimeProviderAuthHeaders.delete(providerName);
|
|
6168
7667
|
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
|
|
6169
|
-
this
|
|
7668
|
+
this.#removeOwnedConfigApiKey(providerName);
|
|
6170
7669
|
this.#clearDescriptorDiscoveryEvidence(providerName);
|
|
6171
7670
|
}
|
|
6172
7671
|
|
|
@@ -6174,6 +7673,7 @@ export class ModelRegistry {
|
|
|
6174
7673
|
* Remove custom API/OAuth registrations for a specific extension source.
|
|
6175
7674
|
*/
|
|
6176
7675
|
clearSourceRegistrations(sourceId: string): void {
|
|
7676
|
+
this.#catalogRefreshGeneration++;
|
|
6177
7677
|
unregisterCustomApis(sourceId);
|
|
6178
7678
|
unregisterOAuthProviders(sourceId);
|
|
6179
7679
|
const sourceProviders = this.#runtimeProvidersBySource.get(sourceId);
|
|
@@ -6217,6 +7717,7 @@ export class ModelRegistry {
|
|
|
6217
7717
|
* If provider has oauth: registers OAuth provider for /login support.
|
|
6218
7718
|
*/
|
|
6219
7719
|
registerProvider(providerName: string, config: ProviderConfigInput, sourceId?: string): void {
|
|
7720
|
+
this.#catalogRefreshGeneration++;
|
|
6220
7721
|
if (config.streamSimple && !config.api) {
|
|
6221
7722
|
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
|
6222
7723
|
}
|
|
@@ -6304,7 +7805,7 @@ export class ModelRegistry {
|
|
|
6304
7805
|
this.#runtimeProviderResolvedApiKeys.set(providerName, resolved);
|
|
6305
7806
|
this.#runtimeProviderCredentialInstalled.add(providerName);
|
|
6306
7807
|
if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
|
|
6307
|
-
this
|
|
7808
|
+
this.#setOwnedConfigApiKey(providerName, resolved, false, "runtime");
|
|
6308
7809
|
}
|
|
6309
7810
|
if (config.oauth && !config.apiKey && this.#runtimeProviderApiKeys.has(providerName)) {
|
|
6310
7811
|
const previousApiKey = this.#runtimeProviderResolvedApiKeys.get(providerName);
|
|
@@ -6314,7 +7815,7 @@ export class ModelRegistry {
|
|
|
6314
7815
|
this.#runtimeProviderApiKeyEnvNames.delete(providerName);
|
|
6315
7816
|
this.#runtimeProviderAuthHeaders.delete(providerName);
|
|
6316
7817
|
this.#customProviderApiKeys.delete(providerName);
|
|
6317
|
-
this
|
|
7818
|
+
this.#removeOwnedConfigApiKey(providerName);
|
|
6318
7819
|
this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
|
|
6319
7820
|
if (overlay.provider !== providerName) return overlay;
|
|
6320
7821
|
const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
|