@gajae-code/coding-agent 0.18.10 → 0.18.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +295 -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/cli/accounts-cli.d.ts +2 -2
- 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 +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -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/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- 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/theme/theme.d.ts +1 -0
- 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/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- 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 +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -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/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 +58 -2
- 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 +13 -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 +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- 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/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- 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/web/scrapers/types.d.ts +8 -1
- 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 +8 -8
- 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 +22 -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 +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- 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 +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- 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/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -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/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/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- 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/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/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.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 +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- 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/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- 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/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,
|
|
@@ -52,6 +54,7 @@ import {
|
|
|
52
54
|
} from "@gajae-code/ai/utils/discovery/openai-compatible";
|
|
53
55
|
// Internal-only: registerFinalizedModelClone is not part of the public @gajae-code/ai surface
|
|
54
56
|
import { registerFinalizedModelClone } from "@gajae-code/ai/utils/trusted-model-clone";
|
|
57
|
+
import { JSONC, YAML } from "bun";
|
|
55
58
|
|
|
56
59
|
// Sentinels for local-only OAuth tokens — declared inline to avoid loading provider
|
|
57
60
|
// modules at startup. Must match the provider OAuth modules.
|
|
@@ -77,13 +80,15 @@ import {
|
|
|
77
80
|
projectActiveProviderDescriptors,
|
|
78
81
|
} from "../sdk/providers";
|
|
79
82
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
83
|
+
import { writeExplicitOwnerOnlyTextSync } from "../session/session-storage";
|
|
80
84
|
import type { ActiveSearchModelContext, WebSearchMode } from "../web/search/types";
|
|
81
85
|
import { type BillingPath, deriveBillingPath } from "./billing-path";
|
|
82
|
-
import { ConfigError, ConfigFile } from "./config-file";
|
|
86
|
+
import { ConfigError, ConfigFile, type LoadResult } from "./config-file";
|
|
83
87
|
import { isAuthenticated, kNoAuth } from "./model-auth";
|
|
84
88
|
import { type ConfiguredModelBindings, ModelBindingsApplier } from "./model-bindings-applier";
|
|
85
89
|
import { ModelDiscoveryManager, type ProviderDiscoveryState } from "./model-discovery-manager";
|
|
86
90
|
import {
|
|
91
|
+
type AcceptedModelPresetRegistry,
|
|
87
92
|
loadAcceptedModelPresetProfiles,
|
|
88
93
|
type ModelPresetRegistryDependencies,
|
|
89
94
|
type ModelPresetRegistryRefreshResult,
|
|
@@ -449,6 +454,20 @@ function assertRequestTransformSupportedForKnownProvider(providerName: string, s
|
|
|
449
454
|
}
|
|
450
455
|
}
|
|
451
456
|
|
|
457
|
+
function assertStreamIdleTimeoutSupportedForKnownProvider(providerName: string, source: string): void {
|
|
458
|
+
const apis = getKnownProviderApis(providerName);
|
|
459
|
+
if (apis.size === 0) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Provider ${providerName}: ${source} requires an OpenAI-compatible "api" when the provider is not built in.`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (![...apis].some(isRequestTransformApi)) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Provider ${providerName}: ${source} is only supported with openai-completions or openai-responses APIs.`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
452
471
|
function assertRequestTransformSupportedForModelApi(
|
|
453
472
|
providerName: string,
|
|
454
473
|
modelId: string,
|
|
@@ -463,8 +482,54 @@ function assertRequestTransformSupportedForModelApi(
|
|
|
463
482
|
}
|
|
464
483
|
|
|
465
484
|
function getKnownProviderModelApi(providerName: string, modelId: string): Api | undefined {
|
|
466
|
-
return getBundledModels(providerName as Parameters<typeof getBundledModels>[0]).find(
|
|
467
|
-
|
|
485
|
+
return getBundledModels(providerName as Parameters<typeof getBundledModels>[0]).find(
|
|
486
|
+
model => model.id.toLowerCase() === modelId.toLowerCase(),
|
|
487
|
+
)?.api as Api | undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function resolveModelApiOverrideFromDiscovery(
|
|
491
|
+
providerApi: Api,
|
|
492
|
+
discovery: ProviderDiscovery,
|
|
493
|
+
modelId: string,
|
|
494
|
+
entry?: { id?: unknown; owned_by?: unknown },
|
|
495
|
+
allowModelIdDetection = true,
|
|
496
|
+
): Api | undefined {
|
|
497
|
+
// Explicit per-prefix routing always wins over model-id detection.
|
|
498
|
+
const normalizedModelId = modelId.toLowerCase();
|
|
499
|
+
let matchedPrefixLength = -1;
|
|
500
|
+
let prefixApi: Api | undefined;
|
|
501
|
+
for (const [prefix, routedApi] of Object.entries(discovery.apiByModelPrefix ?? {})) {
|
|
502
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
503
|
+
if (normalizedModelId.startsWith(normalizedPrefix) && normalizedPrefix.length > matchedPrefixLength) {
|
|
504
|
+
prefixApi = routedApi;
|
|
505
|
+
matchedPrefixLength = normalizedPrefix.length;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (prefixApi !== undefined) return prefixApi;
|
|
509
|
+
|
|
510
|
+
// An OpenAI-shaped models list can advertise Anthropic models that must use
|
|
511
|
+
// the Anthropic transport even when the provider's default is OpenAI-family.
|
|
512
|
+
if (discovery.type === "openai-models-list") {
|
|
513
|
+
let candidateEntry = entry;
|
|
514
|
+
if (candidateEntry === undefined && allowModelIdDetection) {
|
|
515
|
+
candidateEntry = { id: modelId };
|
|
516
|
+
}
|
|
517
|
+
const detected = candidateEntry === undefined ? undefined : detectDiscoveredApiFamily(candidateEntry);
|
|
518
|
+
if (detected === "anthropic-messages") return "anthropic-messages";
|
|
519
|
+
if (detected === "openai-completions") {
|
|
520
|
+
return isOpenAIFamilyApi(providerApi) ? providerApi : "openai-completions";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function resolveModelApiFromDiscovery(
|
|
527
|
+
providerApi: Api,
|
|
528
|
+
discovery: ProviderDiscovery,
|
|
529
|
+
modelId: string,
|
|
530
|
+
entry?: { id?: unknown; owned_by?: unknown },
|
|
531
|
+
): Api {
|
|
532
|
+
return resolveModelApiOverrideFromDiscovery(providerApi, discovery, modelId, entry) ?? providerApi;
|
|
468
533
|
}
|
|
469
534
|
|
|
470
535
|
function isCanonicalOpenAIAffinityBaseUrl(baseUrl: string | undefined): boolean {
|
|
@@ -513,6 +578,7 @@ interface ProviderValidationModel {
|
|
|
513
578
|
maxTokens?: number;
|
|
514
579
|
compat?: Model<Api>["compat"];
|
|
515
580
|
requestTransform?: ModelRequestTransform;
|
|
581
|
+
streamIdleTimeoutMs?: number;
|
|
516
582
|
}
|
|
517
583
|
|
|
518
584
|
interface ProviderValidationConfig {
|
|
@@ -521,6 +587,7 @@ interface ProviderValidationConfig {
|
|
|
521
587
|
apiKey?: string;
|
|
522
588
|
apiKeyEnv?: string;
|
|
523
589
|
api?: Api;
|
|
590
|
+
transport?: Model<Api>["transport"];
|
|
524
591
|
auth?: ProviderAuthMode;
|
|
525
592
|
oauthConfigured?: boolean;
|
|
526
593
|
discovery?: ProviderDiscovery;
|
|
@@ -528,6 +595,7 @@ interface ProviderValidationConfig {
|
|
|
528
595
|
requestTransform?: ModelRequestTransform;
|
|
529
596
|
disableStrictTools?: boolean;
|
|
530
597
|
cacheRetention?: CacheRetention;
|
|
598
|
+
streamIdleTimeoutMs?: number;
|
|
531
599
|
openaiCompat?: { baseUrl: string; apiKey?: string; apiKeyEnv?: string };
|
|
532
600
|
modelOverrides?: Record<string, unknown>;
|
|
533
601
|
models: ProviderValidationModel[];
|
|
@@ -543,7 +611,20 @@ function validateProviderConfiguration(
|
|
|
543
611
|
mode: ProviderValidationMode,
|
|
544
612
|
): void {
|
|
545
613
|
const hasProviderApi = !!config.api;
|
|
614
|
+
const configuredApi = config.api ?? (config.openaiCompat ? "openai-completions" : undefined);
|
|
546
615
|
const models = config.models;
|
|
616
|
+
if (
|
|
617
|
+
config.transport === "pi-native" &&
|
|
618
|
+
(config.streamIdleTimeoutMs !== undefined ||
|
|
619
|
+
models.some(model => model.streamIdleTimeoutMs !== undefined) ||
|
|
620
|
+
Object.values(config.modelOverrides ?? {}).some(
|
|
621
|
+
override => (override as ModelOverride).streamIdleTimeoutMs !== undefined,
|
|
622
|
+
))
|
|
623
|
+
) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`Provider ${providerName}: "streamIdleTimeoutMs" is not supported with "transport: pi-native"; configure it on the auth-gateway's upstream model.`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
547
628
|
|
|
548
629
|
if (models.length === 0) {
|
|
549
630
|
if (mode === "models-config") {
|
|
@@ -557,12 +638,13 @@ function validateProviderConfiguration(
|
|
|
557
638
|
!config.disableStrictTools &&
|
|
558
639
|
!config.requestTransform &&
|
|
559
640
|
!config.cacheRetention &&
|
|
641
|
+
config.streamIdleTimeoutMs === undefined &&
|
|
560
642
|
!config.openaiCompat &&
|
|
561
643
|
!hasModelOverrides &&
|
|
562
644
|
!config.discovery
|
|
563
645
|
) {
|
|
564
646
|
throw new Error(
|
|
565
|
-
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "disableStrictTools", "modelOverrides", "discovery", "openaiCompat", or "models"`,
|
|
647
|
+
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "streamIdleTimeoutMs", "disableStrictTools", "modelOverrides", "discovery", "openaiCompat", or "models"`,
|
|
566
648
|
);
|
|
567
649
|
}
|
|
568
650
|
}
|
|
@@ -623,10 +705,55 @@ function validateProviderConfiguration(
|
|
|
623
705
|
}
|
|
624
706
|
for (const [modelId, rawOverride] of Object.entries(config.modelOverrides ?? {})) {
|
|
625
707
|
const override = rawOverride as ModelOverride;
|
|
708
|
+
const configuredModel = models.find(model => model.id.toLowerCase() === modelId.toLowerCase());
|
|
709
|
+
const discoveryProviderApi =
|
|
710
|
+
(config.api as Api | undefined) ??
|
|
711
|
+
dominantOpenAIFamilyModelApi(models) ??
|
|
712
|
+
(config.openaiCompat ? "openai-completions" : undefined);
|
|
713
|
+
const implicitDiscoveryBaseUrl = config.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName);
|
|
714
|
+
const implicitDiscovery: ProviderDiscovery | undefined =
|
|
715
|
+
!config.discovery &&
|
|
716
|
+
(config.openaiCompat !== undefined ||
|
|
717
|
+
(providerName !== "vllm" &&
|
|
718
|
+
providerName !== "sglang" &&
|
|
719
|
+
discoveryProviderApi !== undefined &&
|
|
720
|
+
isOpenAIFamilyApi(discoveryProviderApi) &&
|
|
721
|
+
implicitDiscoveryBaseUrl !== undefined))
|
|
722
|
+
? { type: "openai-models-list" }
|
|
723
|
+
: undefined;
|
|
724
|
+
const effectiveDiscovery = config.discovery ?? implicitDiscovery;
|
|
725
|
+
const discoveryApi =
|
|
726
|
+
configuredModel || !effectiveDiscovery || !discoveryProviderApi
|
|
727
|
+
? undefined
|
|
728
|
+
: resolveModelApiOverrideFromDiscovery(discoveryProviderApi, effectiveDiscovery, modelId);
|
|
729
|
+
const discoveryTimeoutApi =
|
|
730
|
+
configuredModel || !effectiveDiscovery || !discoveryProviderApi
|
|
731
|
+
? undefined
|
|
732
|
+
: resolveModelApiOverrideFromDiscovery(discoveryProviderApi, effectiveDiscovery, modelId, undefined, false);
|
|
733
|
+
const bundledModelApi = getKnownProviderModelApi(providerName, modelId);
|
|
734
|
+
// An OpenAI models-list row may override both its ID-based family and the
|
|
735
|
+
// provider default via `owned_by`. Defer ambiguous timeout validation until
|
|
736
|
+
// discovery supplies that metadata; applying the override validates the
|
|
737
|
+
// resolved model API before it can take effect.
|
|
738
|
+
const deferTimeoutValidation =
|
|
739
|
+
!configuredModel &&
|
|
740
|
+
effectiveDiscovery?.type === "openai-models-list" &&
|
|
741
|
+
discoveryTimeoutApi === undefined &&
|
|
742
|
+
bundledModelApi === undefined;
|
|
626
743
|
const effectiveApi =
|
|
627
|
-
|
|
628
|
-
config.api ??
|
|
629
|
-
|
|
744
|
+
configuredModel?.api ??
|
|
745
|
+
(configuredModel ? config.api : undefined) ??
|
|
746
|
+
discoveryApi ??
|
|
747
|
+
bundledModelApi ??
|
|
748
|
+
config.api;
|
|
749
|
+
const effectiveTimeoutApi =
|
|
750
|
+
configuredModel?.api ??
|
|
751
|
+
(configuredModel ? config.api : undefined) ??
|
|
752
|
+
discoveryTimeoutApi ??
|
|
753
|
+
bundledModelApi ??
|
|
754
|
+
(deferTimeoutValidation
|
|
755
|
+
? undefined
|
|
756
|
+
: (config.api ?? (config.openaiCompat ? "openai-completions" : undefined)));
|
|
630
757
|
if (override.compat?.supportsResponsesSessionAffinity !== undefined) {
|
|
631
758
|
assertResponsesSessionAffinitySupported(
|
|
632
759
|
providerName,
|
|
@@ -635,6 +762,21 @@ function validateProviderConfiguration(
|
|
|
635
762
|
`modelOverrides ${modelId} "compat.supportsResponsesSessionAffinity"`,
|
|
636
763
|
);
|
|
637
764
|
}
|
|
765
|
+
if (override.streamIdleTimeoutMs !== undefined) {
|
|
766
|
+
if (effectiveTimeoutApi) {
|
|
767
|
+
assertRequestTransformSupportedForModelApi(
|
|
768
|
+
providerName,
|
|
769
|
+
modelId,
|
|
770
|
+
effectiveTimeoutApi,
|
|
771
|
+
'modelOverrides "streamIdleTimeoutMs"',
|
|
772
|
+
);
|
|
773
|
+
} else if (!deferTimeoutValidation) {
|
|
774
|
+
assertRequestTransformSupportedForKnownProvider(
|
|
775
|
+
providerName,
|
|
776
|
+
`modelOverrides ${modelId} "streamIdleTimeoutMs"`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
638
780
|
if (!override.requestTransform) continue;
|
|
639
781
|
if (effectiveApi) {
|
|
640
782
|
assertRequestTransformSupportedForModelApi(
|
|
@@ -657,6 +799,54 @@ function validateProviderConfiguration(
|
|
|
657
799
|
assertRequestTransformSupportedForKnownProvider(providerName, '"requestTransform"');
|
|
658
800
|
}
|
|
659
801
|
}
|
|
802
|
+
if (config.streamIdleTimeoutMs !== undefined) {
|
|
803
|
+
const bundledApis = getKnownProviderApis(providerName);
|
|
804
|
+
const hasCompatibleBundledApi = [...bundledApis].some(isRequestTransformApi);
|
|
805
|
+
const hasCompatibleModelApi = models.some(model => {
|
|
806
|
+
const api = model.api ?? config.api;
|
|
807
|
+
return api !== undefined && isRequestTransformApi(api);
|
|
808
|
+
});
|
|
809
|
+
const hasCompatibleDiscoveryApi =
|
|
810
|
+
config.openaiCompat !== undefined ||
|
|
811
|
+
(config.discovery !== undefined &&
|
|
812
|
+
((configuredApi !== undefined && isRequestTransformApi(configuredApi)) ||
|
|
813
|
+
Object.values(config.discovery.apiByModelPrefix ?? {}).some(isRequestTransformApi)));
|
|
814
|
+
const implicitDiscoveryApi =
|
|
815
|
+
(config.api as Api | undefined) ??
|
|
816
|
+
dominantOpenAIFamilyModelApi(models) ??
|
|
817
|
+
(config.openaiCompat ? "openai-completions" : undefined);
|
|
818
|
+
const implicitDiscoveryBaseUrl = config.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName);
|
|
819
|
+
const hasCompatibleImplicitDiscoveryApi =
|
|
820
|
+
!config.discovery &&
|
|
821
|
+
(config.openaiCompat !== undefined ||
|
|
822
|
+
(providerName !== "vllm" &&
|
|
823
|
+
providerName !== "sglang" &&
|
|
824
|
+
implicitDiscoveryApi !== undefined &&
|
|
825
|
+
isOpenAIFamilyApi(implicitDiscoveryApi) &&
|
|
826
|
+
implicitDiscoveryBaseUrl !== undefined));
|
|
827
|
+
const hasCompatibleDiscoveryRoute = hasCompatibleDiscoveryApi || hasCompatibleImplicitDiscoveryApi;
|
|
828
|
+
if (models.length > 0) {
|
|
829
|
+
if (!hasCompatibleModelApi && !hasCompatibleBundledApi && !hasCompatibleDiscoveryRoute) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`Provider ${providerName}: "streamIdleTimeoutMs" is only supported with openai-completions or openai-responses APIs.`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
if (!hasCompatibleBundledApi && !hasCompatibleDiscoveryRoute) {
|
|
836
|
+
if (bundledApis.size > 0) {
|
|
837
|
+
assertStreamIdleTimeoutSupportedForKnownProvider(providerName, '"streamIdleTimeoutMs"');
|
|
838
|
+
} else if (configuredApi) {
|
|
839
|
+
if (!isRequestTransformApi(configuredApi)) {
|
|
840
|
+
throw new Error(
|
|
841
|
+
`Provider ${providerName}: "streamIdleTimeoutMs" is only supported with openai-completions or openai-responses APIs.`,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
assertStreamIdleTimeoutSupportedForKnownProvider(providerName, '"streamIdleTimeoutMs"');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
660
850
|
|
|
661
851
|
for (const modelDef of models) {
|
|
662
852
|
if (!hasProviderApi && !modelDef.api) {
|
|
@@ -670,6 +860,14 @@ function validateProviderConfiguration(
|
|
|
670
860
|
throw new Error(`Provider ${providerName}: model missing "id"`);
|
|
671
861
|
}
|
|
672
862
|
const effectiveApi = modelDef.api ?? config.api;
|
|
863
|
+
if (modelDef.streamIdleTimeoutMs !== undefined && effectiveApi) {
|
|
864
|
+
assertRequestTransformSupportedForModelApi(
|
|
865
|
+
providerName,
|
|
866
|
+
modelDef.id,
|
|
867
|
+
effectiveApi,
|
|
868
|
+
'model "streamIdleTimeoutMs"',
|
|
869
|
+
);
|
|
870
|
+
}
|
|
673
871
|
const modelCompat = modelDef.compat;
|
|
674
872
|
if (modelCompat && "supportsResponsesSessionAffinity" in modelCompat) {
|
|
675
873
|
assertResponsesSessionAffinitySupported(
|
|
@@ -706,34 +904,83 @@ function validateProviderConfiguration(
|
|
|
706
904
|
}
|
|
707
905
|
}
|
|
708
906
|
|
|
907
|
+
function validateModelsConfig(config: ModelsConfig): void {
|
|
908
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
909
|
+
validateProviderConfiguration(
|
|
910
|
+
providerName,
|
|
911
|
+
{
|
|
912
|
+
baseUrl: providerConfig.baseUrl,
|
|
913
|
+
headers: providerConfig.headers,
|
|
914
|
+
apiKey: providerConfig.apiKey,
|
|
915
|
+
apiKeyEnv: providerConfig.apiKeyEnv,
|
|
916
|
+
api: providerConfig.api as Api | undefined,
|
|
917
|
+
transport: providerConfig.transport,
|
|
918
|
+
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
|
|
919
|
+
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
|
|
920
|
+
compat: providerConfig.compat,
|
|
921
|
+
requestTransform: providerConfig.requestTransform,
|
|
922
|
+
disableStrictTools: providerConfig.disableStrictTools,
|
|
923
|
+
cacheRetention: providerConfig.cacheRetention,
|
|
924
|
+
streamIdleTimeoutMs: providerConfig.streamIdleTimeoutMs,
|
|
925
|
+
openaiCompat: providerConfig.openaiCompat,
|
|
926
|
+
modelOverrides: providerConfig.modelOverrides,
|
|
927
|
+
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
928
|
+
},
|
|
929
|
+
"models-config",
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
709
934
|
export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
|
|
710
935
|
"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
|
-
},
|
|
936
|
+
validateModelsConfig,
|
|
735
937
|
);
|
|
736
938
|
|
|
939
|
+
function parseModelsConfigSnapshot(snapshot: { path: string; text: string | null }): LoadResult<ModelsConfig> {
|
|
940
|
+
if (snapshot.text === null) return { status: "not-found" };
|
|
941
|
+
try {
|
|
942
|
+
const content = snapshot.text.trim();
|
|
943
|
+
let raw: unknown;
|
|
944
|
+
if (snapshot.path.endsWith(".json") || snapshot.path.endsWith(".jsonc")) {
|
|
945
|
+
raw = JSONC.parse(content);
|
|
946
|
+
} else if (snapshot.path.endsWith(".yml") || snapshot.path.endsWith(".yaml")) {
|
|
947
|
+
raw = YAML.parse(content);
|
|
948
|
+
} else {
|
|
949
|
+
throw new Error(`Invalid config file path: ${snapshot.path}`);
|
|
950
|
+
}
|
|
951
|
+
const parsed = ModelsConfigSchema.safeParse(raw);
|
|
952
|
+
if (!parsed.success) {
|
|
953
|
+
return {
|
|
954
|
+
status: "error",
|
|
955
|
+
error: new ConfigError(
|
|
956
|
+
"models",
|
|
957
|
+
parsed.error.issues.slice(0, 50).map(issue => ({
|
|
958
|
+
instancePath: issue.path.length === 0 ? "" : `/${issue.path.map(String).join("/")}`,
|
|
959
|
+
message: issue.message,
|
|
960
|
+
})),
|
|
961
|
+
),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
validateModelsConfig(parsed.data);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
return {
|
|
968
|
+
status: "error",
|
|
969
|
+
error:
|
|
970
|
+
error instanceof ConfigError
|
|
971
|
+
? error
|
|
972
|
+
: new ConfigError("models", undefined, { err: error, stage: "Validate(models)" }),
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
return { status: "ok", value: parsed.data };
|
|
976
|
+
} catch (error) {
|
|
977
|
+
return {
|
|
978
|
+
status: "error",
|
|
979
|
+
error: new ConfigError("models", undefined, { err: error, stage: "Unexpected" }),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
737
984
|
/** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
|
|
738
985
|
interface ProviderOverride {
|
|
739
986
|
api?: Api;
|
|
@@ -745,6 +992,7 @@ interface ProviderOverride {
|
|
|
745
992
|
transport?: Model<Api>["transport"];
|
|
746
993
|
requestTransform?: ModelRequestTransform;
|
|
747
994
|
cacheRetention?: CacheRetention;
|
|
995
|
+
streamIdleTimeoutMs?: number;
|
|
748
996
|
isOAuth?: boolean;
|
|
749
997
|
}
|
|
750
998
|
|
|
@@ -778,6 +1026,7 @@ function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Parti
|
|
|
778
1026
|
contextWindow: model.contextWindow,
|
|
779
1027
|
maxTokens: model.maxTokens,
|
|
780
1028
|
};
|
|
1029
|
+
if (model.streamIdleTimeoutMs !== undefined) metadata.streamIdleTimeoutMs = model.streamIdleTimeoutMs;
|
|
781
1030
|
if (model.longContextPricing !== undefined) metadata.longContextPricing = model.longContextPricing;
|
|
782
1031
|
if (model.premiumMultiplier !== undefined) metadata.premiumMultiplier = model.premiumMultiplier;
|
|
783
1032
|
if (model.priority !== undefined) metadata.priority = model.priority;
|
|
@@ -941,10 +1190,15 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
941
1190
|
existing: Model<Api> | undefined,
|
|
942
1191
|
providerOverride?: Pick<
|
|
943
1192
|
ProviderOverride,
|
|
944
|
-
"baseUrl" | "headers" | "transport" | "requestTransform" | "cacheRetention"
|
|
1193
|
+
"baseUrl" | "headers" | "transport" | "requestTransform" | "cacheRetention" | "streamIdleTimeoutMs"
|
|
945
1194
|
>,
|
|
946
1195
|
): Model<TApi> {
|
|
947
1196
|
if (existing) {
|
|
1197
|
+
const streamIdleTimeoutMs =
|
|
1198
|
+
model.streamIdleTimeoutMs ??
|
|
1199
|
+
(isRequestTransformApi(model.api)
|
|
1200
|
+
? (existing.streamIdleTimeoutMs ?? providerOverride?.streamIdleTimeoutMs)
|
|
1201
|
+
: undefined);
|
|
948
1202
|
return {
|
|
949
1203
|
...model,
|
|
950
1204
|
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
|
@@ -954,15 +1208,20 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
954
1208
|
providerOverride?.requestTransform,
|
|
955
1209
|
),
|
|
956
1210
|
cacheRetention: model.cacheRetention ?? existing.cacheRetention ?? providerOverride?.cacheRetention,
|
|
1211
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
957
1212
|
};
|
|
958
1213
|
}
|
|
959
1214
|
if (providerOverride) {
|
|
1215
|
+
const streamIdleTimeoutMs =
|
|
1216
|
+
model.streamIdleTimeoutMs ??
|
|
1217
|
+
(isRequestTransformApi(model.api) ? providerOverride.streamIdleTimeoutMs : undefined);
|
|
960
1218
|
return {
|
|
961
1219
|
...model,
|
|
962
1220
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
963
1221
|
headers: mergeCaseInsensitiveHeaders(model.headers, providerOverride.headers),
|
|
964
1222
|
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
965
1223
|
requestTransform: mergeRequestTransform(model.requestTransform, providerOverride.requestTransform),
|
|
1224
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
966
1225
|
};
|
|
967
1226
|
}
|
|
968
1227
|
return model;
|
|
@@ -1278,6 +1537,20 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
|
|
1278
1537
|
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
|
1279
1538
|
if (override.output !== undefined) result.output = override.output as ("text" | "image")[];
|
|
1280
1539
|
if (override.cacheRetention !== undefined) result.cacheRetention = override.cacheRetention;
|
|
1540
|
+
if (override.streamIdleTimeoutMs !== undefined) {
|
|
1541
|
+
if (isRequestTransformApi(model.api)) {
|
|
1542
|
+
result.streamIdleTimeoutMs = override.streamIdleTimeoutMs;
|
|
1543
|
+
} else {
|
|
1544
|
+
// Discovery ownership can reveal an incompatible route after config
|
|
1545
|
+
// validation, including while hydrating a cached model at startup.
|
|
1546
|
+
// Reject only this override so the otherwise-valid model stays usable.
|
|
1547
|
+
logger.warn("model stream idle timeout override ignored for incompatible API", {
|
|
1548
|
+
model: model.id,
|
|
1549
|
+
provider: model.provider,
|
|
1550
|
+
api: model.api,
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1281
1554
|
const contextWindowOverride = toPositiveNumberOrUndefined(override.contextWindow);
|
|
1282
1555
|
if (contextWindowOverride !== undefined) {
|
|
1283
1556
|
result.contextWindow = contextWindowOverride;
|
|
@@ -1367,6 +1640,7 @@ interface CustomModelDefinitionLike {
|
|
|
1367
1640
|
wireModelId?: string;
|
|
1368
1641
|
requestTransform?: ModelRequestTransform;
|
|
1369
1642
|
cacheRetention?: CacheRetention;
|
|
1643
|
+
streamIdleTimeoutMs?: number;
|
|
1370
1644
|
}
|
|
1371
1645
|
|
|
1372
1646
|
interface CustomModelBuildOptions {
|
|
@@ -1394,6 +1668,7 @@ type CustomModelOverlay = {
|
|
|
1394
1668
|
wireModelId?: string;
|
|
1395
1669
|
requestTransform?: ModelRequestTransform;
|
|
1396
1670
|
cacheRetention?: CacheRetention;
|
|
1671
|
+
streamIdleTimeoutMs?: number;
|
|
1397
1672
|
transport?: Model<Api>["transport"];
|
|
1398
1673
|
isOAuth?: boolean;
|
|
1399
1674
|
};
|
|
@@ -1508,9 +1783,13 @@ function buildCustomModelOverlay(
|
|
|
1508
1783
|
providerCacheRetention: CacheRetention | undefined,
|
|
1509
1784
|
providerTransport: Model<Api>["transport"] | undefined,
|
|
1510
1785
|
modelDef: CustomModelDefinitionLike,
|
|
1786
|
+
providerStreamIdleTimeoutMs?: number,
|
|
1511
1787
|
): CustomModelOverlay | undefined {
|
|
1512
1788
|
const api = modelDef.api ?? providerApi;
|
|
1513
1789
|
if (!api) return undefined;
|
|
1790
|
+
const streamIdleTimeoutMs = isRequestTransformApi(api)
|
|
1791
|
+
? (modelDef.streamIdleTimeoutMs ?? providerStreamIdleTimeoutMs)
|
|
1792
|
+
: undefined;
|
|
1514
1793
|
return {
|
|
1515
1794
|
id: modelDef.id,
|
|
1516
1795
|
provider: providerName,
|
|
@@ -1535,6 +1814,7 @@ function buildCustomModelOverlay(
|
|
|
1535
1814
|
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
1536
1815
|
premiumMultiplier: modelDef.premiumMultiplier,
|
|
1537
1816
|
cacheRetention: modelDef.cacheRetention ?? providerCacheRetention,
|
|
1817
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
1538
1818
|
transport: providerTransport,
|
|
1539
1819
|
isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
|
|
1540
1820
|
};
|
|
@@ -1671,6 +1951,9 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
1671
1951
|
wireModelId: resolvedModel.wireModelId,
|
|
1672
1952
|
requestTransform: resolvedModel.requestTransform,
|
|
1673
1953
|
cacheRetention: resolvedModel.cacheRetention ?? reference?.cacheRetention,
|
|
1954
|
+
...(resolvedModel.streamIdleTimeoutMs !== undefined
|
|
1955
|
+
? { streamIdleTimeoutMs: resolvedModel.streamIdleTimeoutMs }
|
|
1956
|
+
: {}),
|
|
1674
1957
|
transport: resolvedModel.transport,
|
|
1675
1958
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
1676
1959
|
isOAuth: resolvedModel.isOAuth,
|
|
@@ -1742,6 +2025,201 @@ interface ConfiguredDiscoveryResult {
|
|
|
1742
2025
|
|
|
1743
2026
|
type ProviderRefreshFence = { providerId: string; generation: number } | { generations: ReadonlyMap<string, number> };
|
|
1744
2027
|
|
|
2028
|
+
interface StagedConfigApiKey {
|
|
2029
|
+
readonly apiKey: string;
|
|
2030
|
+
readonly envSourced: boolean;
|
|
2031
|
+
// The staged AuthStorage adapter needs only the first two fields; registry-owned entries
|
|
2032
|
+
// also track whether the key came from config, runtime registration, or both.
|
|
2033
|
+
readonly configSourced?: boolean;
|
|
2034
|
+
readonly runtimeSourced?: boolean;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
interface StagedAuthStorageState {
|
|
2038
|
+
readonly authStorage: AuthStorage;
|
|
2039
|
+
readonly configApiKeys: Map<object, Map<string, StagedConfigApiKey>>;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function createStagedAuthStorage(base: AuthStorage): StagedAuthStorageState {
|
|
2043
|
+
const configApiKeys = new Map<object, Map<string, StagedConfigApiKey>>();
|
|
2044
|
+
const fallbackResolvers = new Map<object, (provider: string) => string | undefined>();
|
|
2045
|
+
const lookupOwner = {};
|
|
2046
|
+
const getConfigApiKey = (provider: string, owner: object | undefined) =>
|
|
2047
|
+
(owner && configApiKeys.get(owner)?.get(resolveOAuthStorageProvider(provider))) || undefined;
|
|
2048
|
+
const peekApiKey = async (
|
|
2049
|
+
provider: string,
|
|
2050
|
+
options?: AuthApiKeyOptions & { sessionId?: string },
|
|
2051
|
+
): Promise<string | undefined> => {
|
|
2052
|
+
if (base.hasRuntimeApiKey(provider)) return base.peekApiKey(provider, options);
|
|
2053
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2054
|
+
if (
|
|
2055
|
+
configured &&
|
|
2056
|
+
base.resolveEffectiveCredentialSelector(provider, options?.sessionId, options?.credentialSelector)
|
|
2057
|
+
) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
`Credential selector cannot be used for ${provider} while a config API key override is active`,
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
if (configured && !configured.envSourced) return configured.apiKey;
|
|
2063
|
+
if (configured?.envSourced) {
|
|
2064
|
+
const credentialType = base.getEffectiveCredentialType(provider, options?.sessionId, {
|
|
2065
|
+
owner: lookupOwner,
|
|
2066
|
+
credentialSelector: options?.credentialSelector,
|
|
2067
|
+
});
|
|
2068
|
+
if (credentialType !== "api_key") return configured.apiKey;
|
|
2069
|
+
}
|
|
2070
|
+
const storedKey = await base.peekApiKey(provider, { ...options, owner: lookupOwner });
|
|
2071
|
+
if (storedKey !== undefined) return storedKey;
|
|
2072
|
+
if (configured?.envSourced) return configured.apiKey;
|
|
2073
|
+
return (options?.owner && fallbackResolvers.get(options.owner)?.(provider)) ?? undefined;
|
|
2074
|
+
};
|
|
2075
|
+
const staged = new Proxy(base, {
|
|
2076
|
+
get(target, property) {
|
|
2077
|
+
if (property === "setConfigApiKey") {
|
|
2078
|
+
return (provider: string, apiKey: string, options: { envSourced?: boolean; owner?: object } = {}) => {
|
|
2079
|
+
if (!options.owner) return;
|
|
2080
|
+
const keys = configApiKeys.get(options.owner) ?? new Map<string, StagedConfigApiKey>();
|
|
2081
|
+
keys.set(resolveOAuthStorageProvider(provider), {
|
|
2082
|
+
apiKey,
|
|
2083
|
+
envSourced: options.envSourced === true,
|
|
2084
|
+
});
|
|
2085
|
+
configApiKeys.set(options.owner, keys);
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
if (property === "removeConfigApiKey") {
|
|
2089
|
+
return (provider: string, owner?: object) => {
|
|
2090
|
+
if (owner) configApiKeys.get(owner)?.delete(resolveOAuthStorageProvider(provider));
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
if (property === "clearConfigApiKeys") {
|
|
2094
|
+
return (owner?: object) => {
|
|
2095
|
+
if (owner) configApiKeys.delete(owner);
|
|
2096
|
+
else configApiKeys.clear();
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
if (property === "setFallbackResolver") {
|
|
2100
|
+
return (resolver: (provider: string) => string | undefined, owner?: object) => {
|
|
2101
|
+
if (!owner) return () => {};
|
|
2102
|
+
fallbackResolvers.set(owner, resolver);
|
|
2103
|
+
return () => {
|
|
2104
|
+
if (fallbackResolvers.get(owner) === resolver) fallbackResolvers.delete(owner);
|
|
2105
|
+
};
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
if (property === "onGenerationChanged") return () => () => {};
|
|
2109
|
+
if (property === "peekApiKey") return peekApiKey;
|
|
2110
|
+
if (property === "getApiKey")
|
|
2111
|
+
return (provider: string, sessionId?: string, options?: AuthApiKeyOptions) =>
|
|
2112
|
+
peekApiKey(provider, { ...options, sessionId });
|
|
2113
|
+
if (property === "hasConfigApiKey") {
|
|
2114
|
+
return (provider: string, owner?: object) =>
|
|
2115
|
+
getConfigApiKey(provider, owner) !== undefined || base.hasConfigApiKey(provider, lookupOwner);
|
|
2116
|
+
}
|
|
2117
|
+
if (property === "hasAuth") {
|
|
2118
|
+
return (provider: string, sessionId?: string, options?: { owner?: object }) => {
|
|
2119
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2120
|
+
if (configured && base.resolveEffectiveCredentialSelector(provider, sessionId)) return false;
|
|
2121
|
+
return configured !== undefined || base.hasAuth(provider, sessionId, { owner: lookupOwner });
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
if (property === "hasUsableAuth") {
|
|
2125
|
+
return (
|
|
2126
|
+
provider: string,
|
|
2127
|
+
options?: { owner?: object; sessionId?: string; credentialSelector?: AuthCredentialSelector },
|
|
2128
|
+
) => {
|
|
2129
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2130
|
+
if (
|
|
2131
|
+
configured &&
|
|
2132
|
+
base.resolveEffectiveCredentialSelector(provider, options?.sessionId, options?.credentialSelector)
|
|
2133
|
+
) {
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
return (
|
|
2137
|
+
configured !== undefined ||
|
|
2138
|
+
base.hasUsableAuth(provider, {
|
|
2139
|
+
owner: lookupOwner,
|
|
2140
|
+
sessionId: options?.sessionId,
|
|
2141
|
+
credentialSelector: options?.credentialSelector,
|
|
2142
|
+
})
|
|
2143
|
+
);
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
if (property === "getEffectiveCredentialType") {
|
|
2147
|
+
return (
|
|
2148
|
+
provider: string,
|
|
2149
|
+
sessionId?: string,
|
|
2150
|
+
options?: { owner?: object; credentialSelector?: AuthCredentialSelector },
|
|
2151
|
+
) => {
|
|
2152
|
+
if (base.hasRuntimeApiKey(provider))
|
|
2153
|
+
return base.getEffectiveCredentialType(provider, sessionId, options);
|
|
2154
|
+
const configured = getConfigApiKey(provider, options?.owner);
|
|
2155
|
+
if (
|
|
2156
|
+
configured &&
|
|
2157
|
+
base.resolveEffectiveCredentialSelector(provider, sessionId, options?.credentialSelector)
|
|
2158
|
+
) {
|
|
2159
|
+
return undefined;
|
|
2160
|
+
}
|
|
2161
|
+
if (
|
|
2162
|
+
configured &&
|
|
2163
|
+
!(configured.envSourced && base.getSessionCredentialType(provider, sessionId) === "api_key")
|
|
2164
|
+
) {
|
|
2165
|
+
return "api_key";
|
|
2166
|
+
}
|
|
2167
|
+
return base.getEffectiveCredentialType(provider, sessionId, {
|
|
2168
|
+
owner: lookupOwner,
|
|
2169
|
+
credentialSelector: options?.credentialSelector,
|
|
2170
|
+
});
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
if (property === "getProviderEvidenceGeneration") {
|
|
2174
|
+
return (provider: string, resolvedApiKey?: string, owner?: object) => {
|
|
2175
|
+
const configured = getConfigApiKey(provider, owner);
|
|
2176
|
+
const effectiveApiKey = base.hasRuntimeApiKey(provider)
|
|
2177
|
+
? resolvedApiKey
|
|
2178
|
+
: configured && !configured.envSourced
|
|
2179
|
+
? configured.apiKey
|
|
2180
|
+
: (resolvedApiKey ?? configured?.apiKey);
|
|
2181
|
+
return base.getProviderEvidenceGeneration(
|
|
2182
|
+
provider,
|
|
2183
|
+
effectiveApiKey,
|
|
2184
|
+
lookupOwner,
|
|
2185
|
+
configured ? { apiKey: configured.apiKey } : undefined,
|
|
2186
|
+
);
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
const value = Reflect.get(target, property, target);
|
|
2190
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
2191
|
+
},
|
|
2192
|
+
}) as AuthStorage;
|
|
2193
|
+
return { authStorage: staged, configApiKeys };
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
export interface ModelsConfigReloadCandidate {
|
|
2197
|
+
readonly valid: boolean;
|
|
2198
|
+
readonly changed: boolean;
|
|
2199
|
+
readonly diagnostics: { readonly valid: boolean; readonly errors: readonly unknown[] };
|
|
2200
|
+
readonly registry: ModelRegistry;
|
|
2201
|
+
isCurrent(): boolean;
|
|
2202
|
+
commit(): void;
|
|
2203
|
+
rollback(): void;
|
|
2204
|
+
finalize(): void;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
interface ModelRegistryInternalOptions {
|
|
2208
|
+
readonly configFile: ConfigFile<ModelsConfig>;
|
|
2209
|
+
readonly configSource: LoadResult<ModelsConfig>;
|
|
2210
|
+
readonly runtimeSource: ModelRegistry;
|
|
2211
|
+
readonly acceptedPresets: AcceptedModelPresetRegistry;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
interface RegistryLeaseWaiter {
|
|
2215
|
+
readonly kind: "consumer" | "publication";
|
|
2216
|
+
readonly signal?: AbortSignal;
|
|
2217
|
+
readonly resolve: (release: () => void) => void;
|
|
2218
|
+
readonly reject: (reason: unknown) => void;
|
|
2219
|
+
settled: boolean;
|
|
2220
|
+
abort?: () => void;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
1745
2223
|
/**
|
|
1746
2224
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
1747
2225
|
*/
|
|
@@ -1793,9 +2271,14 @@ export class ModelRegistry {
|
|
|
1793
2271
|
#modelProfiles: Map<string, ModelProfileDefinition> = mergeModelProfiles();
|
|
1794
2272
|
#configError: ConfigError | undefined = undefined;
|
|
1795
2273
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
2274
|
+
#modelsConfigSource: LoadResult<ModelsConfig> | undefined;
|
|
2275
|
+
#acceptedConfigValue: ModelsConfig | undefined;
|
|
2276
|
+
#acceptedModelsConfigPresent = false;
|
|
2277
|
+
#acceptedPresets: AcceptedModelPresetRegistry | undefined;
|
|
1796
2278
|
#settings: Pick<Settings, "get" | "getGlobal">;
|
|
1797
2279
|
#ownershipSettings: Pick<Settings, "commitAtomicBatchWithCurrent"> | undefined;
|
|
1798
|
-
readonly #authStorageConfigOwner: object
|
|
2280
|
+
readonly #authStorageConfigOwner: object;
|
|
2281
|
+
#ownedConfigApiKeys = new Map<string, StagedConfigApiKey>();
|
|
1799
2282
|
#disposeAuthStorageFallbackResolver: (() => void) | undefined;
|
|
1800
2283
|
#lastStaticLoadMtime: number | null = null;
|
|
1801
2284
|
#lastStaticLoadEnvironmentFingerprint: string | undefined;
|
|
@@ -1837,6 +2320,11 @@ export class ModelRegistry {
|
|
|
1837
2320
|
#configuredApiKeyEnvNames: Set<string> = new Set();
|
|
1838
2321
|
#optionalAuthPreflightGenerations = new Map<string, number>();
|
|
1839
2322
|
#optionalAuthPreflightEpoch = 0;
|
|
2323
|
+
#stagedReloadCandidate = false;
|
|
2324
|
+
#leaseWaiters: RegistryLeaseWaiter[] = [];
|
|
2325
|
+
#activeConsumerLeases = 0;
|
|
2326
|
+
#publicationFenceHeld = false;
|
|
2327
|
+
#consumerContext = new AsyncLocalStorage<{ active: boolean }>();
|
|
1840
2328
|
|
|
1841
2329
|
/**
|
|
1842
2330
|
* @param authStorage - Auth storage for API key resolution
|
|
@@ -1846,15 +2334,22 @@ export class ModelRegistry {
|
|
|
1846
2334
|
modelsPath?: string,
|
|
1847
2335
|
registrySettings?: Pick<Settings, "get" | "getGlobal"> & Partial<Pick<Settings, "commitAtomicBatchWithCurrent">>,
|
|
1848
2336
|
modelPresetRegistryDependencies: ModelPresetRegistryDependencies = {},
|
|
2337
|
+
internalOptions?: ModelRegistryInternalOptions,
|
|
1849
2338
|
) {
|
|
2339
|
+
this.#authStorageConfigOwner = internalOptions ? internalOptions.runtimeSource.#authStorageConfigOwner : {};
|
|
1850
2340
|
this.#settings = registrySettings ?? settings;
|
|
1851
|
-
this.#ownershipSettings =
|
|
1852
|
-
?
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2341
|
+
this.#ownershipSettings = internalOptions
|
|
2342
|
+
? undefined
|
|
2343
|
+
: registrySettings
|
|
2344
|
+
? typeof registrySettings.commitAtomicBatchWithCurrent === "function"
|
|
2345
|
+
? (registrySettings as Pick<Settings, "commitAtomicBatchWithCurrent">)
|
|
2346
|
+
: undefined
|
|
2347
|
+
: settings;
|
|
1856
2348
|
const configuredAgentDir = path.resolve(modelPresetRegistryDependencies.agentDir ?? getAgentDir());
|
|
1857
|
-
this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
|
|
2349
|
+
this.#modelsConfigFile = internalOptions?.configFile ?? ModelsConfigFile.relocate(modelsPath);
|
|
2350
|
+
this.#modelsConfigSource = internalOptions?.configSource;
|
|
2351
|
+
this.#acceptedPresets = internalOptions?.acceptedPresets;
|
|
2352
|
+
this.#stagedReloadCandidate = internalOptions !== undefined;
|
|
1858
2353
|
this.#modelPresetRegistryAgentDir = modelPresetRegistryDependencies.agentDir
|
|
1859
2354
|
? configuredAgentDir
|
|
1860
2355
|
: modelsPath && path.isAbsolute(modelsPath)
|
|
@@ -1863,6 +2358,10 @@ export class ModelRegistry {
|
|
|
1863
2358
|
const { agentDir: _agentDir, ...registryDependencies } = modelPresetRegistryDependencies;
|
|
1864
2359
|
this.#modelPresetRegistryDependencies = registryDependencies;
|
|
1865
2360
|
this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
|
|
2361
|
+
if (internalOptions) {
|
|
2362
|
+
this.#copyRuntimeStateFrom(internalOptions.runtimeSource);
|
|
2363
|
+
this.#clearOwnedConfigApiKeys();
|
|
2364
|
+
}
|
|
1866
2365
|
// Set up fallback resolver for custom provider API keys
|
|
1867
2366
|
this.#disposeAuthStorageFallbackResolver = this.authStorage.setFallbackResolver(provider => {
|
|
1868
2367
|
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
@@ -1871,32 +2370,620 @@ export class ModelRegistry {
|
|
|
1871
2370
|
this.#unsubscribeAuthGeneration = this.authStorage.onGenerationChanged(() => this.#invalidateAvailableModels());
|
|
1872
2371
|
// Load models synchronously in constructor
|
|
1873
2372
|
this.#loadModels();
|
|
1874
|
-
this.#
|
|
2373
|
+
if (internalOptions) this.#reapplyRuntimeAuthConfiguration();
|
|
2374
|
+
if (!internalOptions) {
|
|
2375
|
+
this.#cancelModelPresetRegistryRefresh = refreshModelPresetRegistryInBackground(
|
|
2376
|
+
{
|
|
2377
|
+
...this.#modelPresetRegistryDependencies,
|
|
2378
|
+
agentDir: this.#modelPresetRegistryAgentDir,
|
|
2379
|
+
knownManifestSha256: this.#loadedModelPresetRegistryManifestSha256,
|
|
2380
|
+
},
|
|
2381
|
+
() => {
|
|
2382
|
+
void this.#enqueueCatalogMutation(() => {
|
|
2383
|
+
if (this.#disposed) return;
|
|
2384
|
+
this.#reloadStaticModels();
|
|
2385
|
+
this.#modelBindingsApplier.apply();
|
|
2386
|
+
this.#notifyCatalogChanged();
|
|
2387
|
+
}).catch(() => undefined);
|
|
2388
|
+
},
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
#copyRuntimeStateFrom(source: ModelRegistry): void {
|
|
2394
|
+
this.#sessionCanonicalVariants = new Map(source.#sessionCanonicalVariants);
|
|
2395
|
+
this.#suppressedSelectors = new Map(source.#suppressedSelectors);
|
|
2396
|
+
this.#selectorCircuits = new Map(
|
|
2397
|
+
[...source.#selectorCircuits].map(([selector, circuit]) => [selector, structuredClone(circuit)]),
|
|
2398
|
+
);
|
|
2399
|
+
this.#runtimeModelOverlays = [...source.#runtimeModelOverlays];
|
|
2400
|
+
this.#runtimeProviderApiKeys = new Map(source.#runtimeProviderApiKeys);
|
|
2401
|
+
this.#runtimeProviderResolvedApiKeys = new Map(source.#runtimeProviderResolvedApiKeys);
|
|
2402
|
+
this.#runtimeProviderCredentialInstalled = new Set(source.#runtimeProviderCredentialInstalled);
|
|
2403
|
+
this.#runtimeProviderApiKeyEnvNames = new Map(source.#runtimeProviderApiKeyEnvNames);
|
|
2404
|
+
this.#runtimeProviderOverrides = new Map(source.#runtimeProviderOverrides);
|
|
2405
|
+
this.#runtimeProviderAuthHeaders = new Map(source.#runtimeProviderAuthHeaders);
|
|
2406
|
+
this.#generatedAuthHeaderProviders = new Set(source.#generatedAuthHeaderProviders);
|
|
2407
|
+
this.#runtimeProvidersBySource = new Map(
|
|
2408
|
+
[...source.#runtimeProvidersBySource].map(([id, providers]) => [id, new Set(providers)]),
|
|
2409
|
+
);
|
|
2410
|
+
this.#runtimeProviderSourceByName = new Map(source.#runtimeProviderSourceByName);
|
|
2411
|
+
this.#registeredProviderSources = new Set(source.#registeredProviderSources);
|
|
2412
|
+
this.#descriptorDiscoveryEvidence = new Map(source.#descriptorDiscoveryEvidence);
|
|
2413
|
+
this.#descriptorDiscoveryGenerations = new Map(source.#descriptorDiscoveryGenerations);
|
|
2414
|
+
this.#configuredDiscoveryEvidence = new Map(source.#configuredDiscoveryEvidence);
|
|
2415
|
+
this.#providerEvidenceApiKeys = new Map(source.#providerEvidenceApiKeys);
|
|
2416
|
+
this.#credentiallessAuthFallbackProviders = new Map(source.#credentiallessAuthFallbackProviders);
|
|
2417
|
+
this.#optionalAuthPreflightGenerations = new Map(source.#optionalAuthPreflightGenerations);
|
|
2418
|
+
this.#optionalAuthPreflightEpoch = source.#optionalAuthPreflightEpoch;
|
|
2419
|
+
this.#ownedConfigApiKeys = new Map(source.#ownedConfigApiKeys);
|
|
2420
|
+
this.#acceptedPresets = source.#acceptedPresets;
|
|
2421
|
+
this.#loadedModelPresetRegistryManifestSha256 = source.#loadedModelPresetRegistryManifestSha256;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
#setOwnedConfigApiKey(
|
|
2425
|
+
provider: string,
|
|
2426
|
+
apiKey: string,
|
|
2427
|
+
envSourced = false,
|
|
2428
|
+
source: "config" | "runtime" = "config",
|
|
2429
|
+
): void {
|
|
2430
|
+
const key = resolveOAuthStorageProvider(provider);
|
|
2431
|
+
const previous = this.#ownedConfigApiKeys.get(key);
|
|
2432
|
+
this.#ownedConfigApiKeys.set(key, {
|
|
2433
|
+
apiKey,
|
|
2434
|
+
envSourced,
|
|
2435
|
+
configSourced: source === "config" || previous?.configSourced === true,
|
|
2436
|
+
runtimeSourced: source === "runtime" || previous?.runtimeSourced === true,
|
|
2437
|
+
});
|
|
2438
|
+
this.authStorage.setConfigApiKey(provider, apiKey, {
|
|
2439
|
+
envSourced,
|
|
2440
|
+
owner: this.#authStorageConfigOwner,
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
#removeOwnedConfigApiKey(provider: string): void {
|
|
2445
|
+
this.#ownedConfigApiKeys.delete(resolveOAuthStorageProvider(provider));
|
|
2446
|
+
this.authStorage.removeConfigApiKey(provider, this.#authStorageConfigOwner);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
#clearOwnedConfigApiKeys(): void {
|
|
2450
|
+
this.#ownedConfigApiKeys.clear();
|
|
2451
|
+
this.authStorage.clearConfigApiKeys(this.#authStorageConfigOwner);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
#reapplyRuntimeAuthConfiguration(): void {
|
|
2455
|
+
for (const [provider, apiKeyConfig] of this.#runtimeProviderApiKeys) {
|
|
2456
|
+
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
2457
|
+
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
2458
|
+
: resolveApiKeyConfig(apiKeyConfig);
|
|
2459
|
+
if (!resolved) continue;
|
|
2460
|
+
this.#customProviderApiKeys.set(provider, resolved);
|
|
2461
|
+
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
2462
|
+
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
2463
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
#swapReloadableState(other: ModelRegistry): void {
|
|
2468
|
+
[this.#models, other.#models] = [other.#models, this.#models];
|
|
2469
|
+
[this.#canonicalIndex, other.#canonicalIndex] = [other.#canonicalIndex, this.#canonicalIndex];
|
|
2470
|
+
[this.#availableModelsCache, other.#availableModelsCache] = [
|
|
2471
|
+
other.#availableModelsCache,
|
|
2472
|
+
this.#availableModelsCache,
|
|
2473
|
+
];
|
|
2474
|
+
[this.#availableModelsDisabledProviders, other.#availableModelsDisabledProviders] = [
|
|
2475
|
+
other.#availableModelsDisabledProviders,
|
|
2476
|
+
this.#availableModelsDisabledProviders,
|
|
2477
|
+
];
|
|
2478
|
+
[this.#availableModelsEnvFingerprint, other.#availableModelsEnvFingerprint] = [
|
|
2479
|
+
other.#availableModelsEnvFingerprint,
|
|
2480
|
+
this.#availableModelsEnvFingerprint,
|
|
2481
|
+
];
|
|
2482
|
+
[this.#customProviderApiKeys, other.#customProviderApiKeys] = [
|
|
2483
|
+
other.#customProviderApiKeys,
|
|
2484
|
+
this.#customProviderApiKeys,
|
|
2485
|
+
];
|
|
2486
|
+
[this.#customProviderApiKeyEnvNames, other.#customProviderApiKeyEnvNames] = [
|
|
2487
|
+
other.#customProviderApiKeyEnvNames,
|
|
2488
|
+
this.#customProviderApiKeyEnvNames,
|
|
2489
|
+
];
|
|
2490
|
+
[this.#customProviderAuthHeaders, other.#customProviderAuthHeaders] = [
|
|
2491
|
+
other.#customProviderAuthHeaders,
|
|
2492
|
+
this.#customProviderAuthHeaders,
|
|
2493
|
+
];
|
|
2494
|
+
[this.#providerWebSearchModes, other.#providerWebSearchModes] = [
|
|
2495
|
+
other.#providerWebSearchModes,
|
|
2496
|
+
this.#providerWebSearchModes,
|
|
2497
|
+
];
|
|
2498
|
+
[this.#keylessProviders, other.#keylessProviders] = [other.#keylessProviders, this.#keylessProviders];
|
|
2499
|
+
[this.#optionalAuthProviders, other.#optionalAuthProviders] = [
|
|
2500
|
+
other.#optionalAuthProviders,
|
|
2501
|
+
this.#optionalAuthProviders,
|
|
2502
|
+
];
|
|
2503
|
+
[this.#credentiallessAuthFallbackProviders, other.#credentiallessAuthFallbackProviders] = [
|
|
2504
|
+
other.#credentiallessAuthFallbackProviders,
|
|
2505
|
+
this.#credentiallessAuthFallbackProviders,
|
|
2506
|
+
];
|
|
2507
|
+
[this.#providerEvidenceApiKeys, other.#providerEvidenceApiKeys] = [
|
|
2508
|
+
other.#providerEvidenceApiKeys,
|
|
2509
|
+
this.#providerEvidenceApiKeys,
|
|
2510
|
+
];
|
|
2511
|
+
[this.#providerActivity, other.#providerActivity] = [other.#providerActivity, this.#providerActivity];
|
|
2512
|
+
[this.#configuredProviderIds, other.#configuredProviderIds] = [
|
|
2513
|
+
other.#configuredProviderIds,
|
|
2514
|
+
this.#configuredProviderIds,
|
|
2515
|
+
];
|
|
2516
|
+
[this.#configuredDiscoveryProviderIds, other.#configuredDiscoveryProviderIds] = [
|
|
2517
|
+
other.#configuredDiscoveryProviderIds,
|
|
2518
|
+
this.#configuredDiscoveryProviderIds,
|
|
2519
|
+
];
|
|
2520
|
+
[this.#descriptorDiscoveryEvidence, other.#descriptorDiscoveryEvidence] = [
|
|
2521
|
+
other.#descriptorDiscoveryEvidence,
|
|
2522
|
+
this.#descriptorDiscoveryEvidence,
|
|
2523
|
+
];
|
|
2524
|
+
[this.#descriptorDiscoveryGenerations, other.#descriptorDiscoveryGenerations] = [
|
|
2525
|
+
other.#descriptorDiscoveryGenerations,
|
|
2526
|
+
this.#descriptorDiscoveryGenerations,
|
|
2527
|
+
];
|
|
2528
|
+
[this.#configuredDiscoveryEvidence, other.#configuredDiscoveryEvidence] = [
|
|
2529
|
+
other.#configuredDiscoveryEvidence,
|
|
2530
|
+
this.#configuredDiscoveryEvidence,
|
|
2531
|
+
];
|
|
2532
|
+
[this.#discoveryManager, other.#discoveryManager] = [other.#discoveryManager, this.#discoveryManager];
|
|
2533
|
+
[this.#customModelOverlays, other.#customModelOverlays] = [other.#customModelOverlays, this.#customModelOverlays];
|
|
2534
|
+
[this.#providerOverrides, other.#providerOverrides] = [other.#providerOverrides, this.#providerOverrides];
|
|
2535
|
+
[this.#modelOverrides, other.#modelOverrides] = [other.#modelOverrides, this.#modelOverrides];
|
|
2536
|
+
[this.#codexContextWindowOverrides, other.#codexContextWindowOverrides] = [
|
|
2537
|
+
other.#codexContextWindowOverrides,
|
|
2538
|
+
this.#codexContextWindowOverrides,
|
|
2539
|
+
];
|
|
2540
|
+
[this.#equivalenceConfig, other.#equivalenceConfig] = [other.#equivalenceConfig, this.#equivalenceConfig];
|
|
2541
|
+
[this.#acceptedPresets, other.#acceptedPresets] = [other.#acceptedPresets, this.#acceptedPresets];
|
|
2542
|
+
[this.#modelProfiles, other.#modelProfiles] = [other.#modelProfiles, this.#modelProfiles];
|
|
2543
|
+
[this.#configError, other.#configError] = [other.#configError, this.#configError];
|
|
2544
|
+
[this.#ownedConfigApiKeys, other.#ownedConfigApiKeys] = [other.#ownedConfigApiKeys, this.#ownedConfigApiKeys];
|
|
2545
|
+
[this.#lastStaticLoadMtime, other.#lastStaticLoadMtime] = [other.#lastStaticLoadMtime, this.#lastStaticLoadMtime];
|
|
2546
|
+
[this.#lastStaticLoadEnvironmentFingerprint, other.#lastStaticLoadEnvironmentFingerprint] = [
|
|
2547
|
+
other.#lastStaticLoadEnvironmentFingerprint,
|
|
2548
|
+
this.#lastStaticLoadEnvironmentFingerprint,
|
|
2549
|
+
];
|
|
2550
|
+
[this.#lastModelPresetRegistryFingerprint, other.#lastModelPresetRegistryFingerprint] = [
|
|
2551
|
+
other.#lastModelPresetRegistryFingerprint,
|
|
2552
|
+
this.#lastModelPresetRegistryFingerprint,
|
|
2553
|
+
];
|
|
2554
|
+
[this.#loadedModelPresetRegistryManifestSha256, other.#loadedModelPresetRegistryManifestSha256] = [
|
|
2555
|
+
other.#loadedModelPresetRegistryManifestSha256,
|
|
2556
|
+
this.#loadedModelPresetRegistryManifestSha256,
|
|
2557
|
+
];
|
|
2558
|
+
[this.#staticModelsLoaded, other.#staticModelsLoaded] = [other.#staticModelsLoaded, this.#staticModelsLoaded];
|
|
2559
|
+
[this.#lastDisabledProviderKey, other.#lastDisabledProviderKey] = [
|
|
2560
|
+
other.#lastDisabledProviderKey,
|
|
2561
|
+
this.#lastDisabledProviderKey,
|
|
2562
|
+
];
|
|
2563
|
+
[this.#runtimeModelOverlays, other.#runtimeModelOverlays] = [
|
|
2564
|
+
other.#runtimeModelOverlays,
|
|
2565
|
+
this.#runtimeModelOverlays,
|
|
2566
|
+
];
|
|
2567
|
+
[this.#runtimeProviderApiKeys, other.#runtimeProviderApiKeys] = [
|
|
2568
|
+
other.#runtimeProviderApiKeys,
|
|
2569
|
+
this.#runtimeProviderApiKeys,
|
|
2570
|
+
];
|
|
2571
|
+
[this.#runtimeProviderResolvedApiKeys, other.#runtimeProviderResolvedApiKeys] = [
|
|
2572
|
+
other.#runtimeProviderResolvedApiKeys,
|
|
2573
|
+
this.#runtimeProviderResolvedApiKeys,
|
|
2574
|
+
];
|
|
2575
|
+
[this.#runtimeProviderCredentialInstalled, other.#runtimeProviderCredentialInstalled] = [
|
|
2576
|
+
other.#runtimeProviderCredentialInstalled,
|
|
2577
|
+
this.#runtimeProviderCredentialInstalled,
|
|
2578
|
+
];
|
|
2579
|
+
[this.#runtimeProviderApiKeyEnvNames, other.#runtimeProviderApiKeyEnvNames] = [
|
|
2580
|
+
other.#runtimeProviderApiKeyEnvNames,
|
|
2581
|
+
this.#runtimeProviderApiKeyEnvNames,
|
|
2582
|
+
];
|
|
2583
|
+
[this.#runtimeProviderOverrides, other.#runtimeProviderOverrides] = [
|
|
2584
|
+
other.#runtimeProviderOverrides,
|
|
2585
|
+
this.#runtimeProviderOverrides,
|
|
2586
|
+
];
|
|
2587
|
+
[this.#runtimeProviderAuthHeaders, other.#runtimeProviderAuthHeaders] = [
|
|
2588
|
+
other.#runtimeProviderAuthHeaders,
|
|
2589
|
+
this.#runtimeProviderAuthHeaders,
|
|
2590
|
+
];
|
|
2591
|
+
[this.#generatedAuthHeaderProviders, other.#generatedAuthHeaderProviders] = [
|
|
2592
|
+
other.#generatedAuthHeaderProviders,
|
|
2593
|
+
this.#generatedAuthHeaderProviders,
|
|
2594
|
+
];
|
|
2595
|
+
[this.#generatedAuthHeaders, other.#generatedAuthHeaders] = [
|
|
2596
|
+
other.#generatedAuthHeaders,
|
|
2597
|
+
this.#generatedAuthHeaders,
|
|
2598
|
+
];
|
|
2599
|
+
[this.#runtimeProvidersBySource, other.#runtimeProvidersBySource] = [
|
|
2600
|
+
other.#runtimeProvidersBySource,
|
|
2601
|
+
this.#runtimeProvidersBySource,
|
|
2602
|
+
];
|
|
2603
|
+
[this.#runtimeProviderSourceByName, other.#runtimeProviderSourceByName] = [
|
|
2604
|
+
other.#runtimeProviderSourceByName,
|
|
2605
|
+
this.#runtimeProviderSourceByName,
|
|
2606
|
+
];
|
|
2607
|
+
[this.#registeredProviderSources, other.#registeredProviderSources] = [
|
|
2608
|
+
other.#registeredProviderSources,
|
|
2609
|
+
this.#registeredProviderSources,
|
|
2610
|
+
];
|
|
2611
|
+
[this.#registryModelKeys, other.#registryModelKeys] = [other.#registryModelKeys, this.#registryModelKeys];
|
|
2612
|
+
[this.#configuredApiKeyEnvNames, other.#configuredApiKeyEnvNames] = [
|
|
2613
|
+
other.#configuredApiKeyEnvNames,
|
|
2614
|
+
this.#configuredApiKeyEnvNames,
|
|
2615
|
+
];
|
|
2616
|
+
[this.#optionalAuthPreflightGenerations, other.#optionalAuthPreflightGenerations] = [
|
|
2617
|
+
other.#optionalAuthPreflightGenerations,
|
|
2618
|
+
this.#optionalAuthPreflightGenerations,
|
|
2619
|
+
];
|
|
2620
|
+
[this.#optionalAuthPreflightEpoch, other.#optionalAuthPreflightEpoch] = [
|
|
2621
|
+
other.#optionalAuthPreflightEpoch,
|
|
2622
|
+
this.#optionalAuthPreflightEpoch,
|
|
2623
|
+
];
|
|
2624
|
+
const previousBindings = this.#modelBindingsApplier.getBindings();
|
|
2625
|
+
this.#modelBindingsApplier.setBindings(other.#modelBindingsApplier.getBindings());
|
|
2626
|
+
other.#modelBindingsApplier.setBindings(previousBindings);
|
|
2627
|
+
this.#modelBindingsApplier.apply();
|
|
2628
|
+
this.#modelsConfigFile.invalidate();
|
|
2629
|
+
this.#availableModelsCache = undefined;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
#reconcileOwnedConfigApiKeys(
|
|
2633
|
+
configs: ReadonlyMap<string, StagedConfigApiKey>,
|
|
2634
|
+
previousConfigs: ReadonlyMap<string, StagedConfigApiKey>,
|
|
2635
|
+
): void {
|
|
2636
|
+
for (const [provider, previous] of previousConfigs) {
|
|
2637
|
+
const next = configs.get(provider);
|
|
2638
|
+
if (next?.apiKey === previous.apiKey && next.envSourced === previous.envSourced) continue;
|
|
2639
|
+
this.authStorage.removeConfigApiKey(provider, this.#authStorageConfigOwner);
|
|
2640
|
+
}
|
|
2641
|
+
for (const [provider, config] of configs) {
|
|
2642
|
+
const previous = previousConfigs.get(provider);
|
|
2643
|
+
if (previous?.apiKey === config.apiKey && previous.envSourced === config.envSourced) continue;
|
|
2644
|
+
this.authStorage.setConfigApiKey(provider, config.apiKey, {
|
|
2645
|
+
envSourced: config.envSourced,
|
|
2646
|
+
owner: this.#authStorageConfigOwner,
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
getModelsConfigPath(): string | undefined {
|
|
2652
|
+
return this.#modelsConfigFile.path();
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
async stageModelsConfigReload(
|
|
2656
|
+
snapshot: {
|
|
2657
|
+
path: string;
|
|
2658
|
+
text: string | null;
|
|
2659
|
+
identity: string;
|
|
2660
|
+
},
|
|
2661
|
+
registrySettings: Pick<Settings, "get" | "getGlobal"> = this.#settings,
|
|
2662
|
+
): Promise<ModelsConfigReloadCandidate> {
|
|
2663
|
+
if (this.#disposed) throw new Error("Model registry is disposed");
|
|
2664
|
+
const pathMatches = path.resolve(snapshot.path) === path.resolve(this.#modelsConfigFile.path());
|
|
2665
|
+
const source = pathMatches
|
|
2666
|
+
? parseModelsConfigSnapshot(snapshot)
|
|
2667
|
+
: {
|
|
2668
|
+
status: "error" as const,
|
|
2669
|
+
error: new ConfigError("models", undefined, {
|
|
2670
|
+
err: new Error("Models config path changed while the watcher was active"),
|
|
2671
|
+
stage: "Snapshot",
|
|
2672
|
+
}),
|
|
2673
|
+
};
|
|
2674
|
+
const accepted = source.status === "ok" || (source.status === "not-found" && !this.#acceptedModelsConfigPresent);
|
|
2675
|
+
const diagnostics = {
|
|
2676
|
+
valid: accepted,
|
|
2677
|
+
errors: accepted
|
|
2678
|
+
? []
|
|
2679
|
+
: [source.status === "error" ? source.error : new Error("Cannot delete an accepted models config")],
|
|
2680
|
+
};
|
|
2681
|
+
const stagedAuthStorage = createStagedAuthStorage(this.authStorage);
|
|
2682
|
+
stagedAuthStorage.configApiKeys.set(this.#authStorageConfigOwner, new Map(this.#ownedConfigApiKeys));
|
|
2683
|
+
const acceptedPresets =
|
|
2684
|
+
this.#acceptedPresets ??
|
|
2685
|
+
loadAcceptedModelPresetProfiles(this.#modelPresetRegistryAgentDir, this.#modelPresetRegistryDependencies);
|
|
2686
|
+
const registry = new ModelRegistry(
|
|
2687
|
+
stagedAuthStorage.authStorage,
|
|
2688
|
+
this.#modelsConfigFile.path(),
|
|
2689
|
+
registrySettings,
|
|
2690
|
+
{ ...this.#modelPresetRegistryDependencies, automaticRefresh: false },
|
|
1875
2691
|
{
|
|
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);
|
|
2692
|
+
configFile: this.#modelsConfigFile,
|
|
2693
|
+
configSource: source,
|
|
2694
|
+
runtimeSource: this,
|
|
2695
|
+
acceptedPresets,
|
|
1887
2696
|
},
|
|
1888
2697
|
);
|
|
2698
|
+
if (registry.getError()) {
|
|
2699
|
+
diagnostics.valid = false;
|
|
2700
|
+
diagnostics.errors.push(registry.getError()!);
|
|
2701
|
+
}
|
|
2702
|
+
for (const [provider, apiKey] of registry.#ownedConfigApiKeys) {
|
|
2703
|
+
if (apiKey.runtimeSourced === true && apiKey.configSourced !== true) continue;
|
|
2704
|
+
if (!this.authStorage.hasAnyCredentialPin(provider)) continue;
|
|
2705
|
+
diagnostics.valid = false;
|
|
2706
|
+
diagnostics.errors.push(
|
|
2707
|
+
new Error(
|
|
2708
|
+
`Cannot configure an API key override for ${provider} while a credential pin is active or unavailable`,
|
|
2709
|
+
),
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
let committed = false;
|
|
2713
|
+
let finished = false;
|
|
2714
|
+
const previousConfigPresent = this.#acceptedModelsConfigPresent;
|
|
2715
|
+
const previousConfigValue = this.#acceptedConfigValue;
|
|
2716
|
+
const nextConfigValue = source.status === "ok" ? source.value : undefined;
|
|
2717
|
+
const changed = !isDeepStrictEqual(previousConfigValue, nextConfigValue);
|
|
2718
|
+
const unchangedProviders = new Set(
|
|
2719
|
+
[...this.#providerActivity.keys()].filter(
|
|
2720
|
+
provider =>
|
|
2721
|
+
isDeepStrictEqual(previousConfigValue?.providers?.[provider], nextConfigValue?.providers?.[provider]) &&
|
|
2722
|
+
isDeepStrictEqual(this.#providerOverrides.get(provider), registry.#providerOverrides.get(provider)) &&
|
|
2723
|
+
isDeepStrictEqual(this.#modelOverrides.get(provider), registry.#modelOverrides.get(provider)) &&
|
|
2724
|
+
this.#customProviderApiKeys.get(provider) === registry.#customProviderApiKeys.get(provider) &&
|
|
2725
|
+
isDeepStrictEqual(this.#ownedConfigApiKeys.get(provider), registry.#ownedConfigApiKeys.get(provider)),
|
|
2726
|
+
),
|
|
2727
|
+
);
|
|
2728
|
+
const unchangedProviderSnapshots = new Map(
|
|
2729
|
+
[...unchangedProviders].map(provider => [
|
|
2730
|
+
provider,
|
|
2731
|
+
{
|
|
2732
|
+
models: this.#models.filter(model => model.provider === provider),
|
|
2733
|
+
discoveryState: this.#discoveryManager.getState(provider),
|
|
2734
|
+
configuredEvidence: this.#configuredDiscoveryEvidence.get(provider),
|
|
2735
|
+
descriptorEvidence: this.#descriptorDiscoveryEvidence.get(provider),
|
|
2736
|
+
descriptorGeneration: this.#descriptorDiscoveryGenerations.get(provider),
|
|
2737
|
+
providerEvidenceApiKey: this.#providerEvidenceApiKeys.get(provider),
|
|
2738
|
+
credentiallessFallback: this.#credentiallessAuthFallbackProviders.get(provider),
|
|
2739
|
+
optionalAuthPreflightGeneration: this.#optionalAuthPreflightGenerations.get(provider),
|
|
2740
|
+
activity: this.#providerActivity.get(provider),
|
|
2741
|
+
},
|
|
2742
|
+
]),
|
|
2743
|
+
);
|
|
2744
|
+
const copyProviderEntry = <T>(target: Map<string, T>, source: ReadonlyMap<string, T>, provider: string) => {
|
|
2745
|
+
const value = source.get(provider);
|
|
2746
|
+
if (value === undefined) target.delete(provider);
|
|
2747
|
+
else target.set(provider, value);
|
|
2748
|
+
};
|
|
2749
|
+
const stagedModelKeys = new Set(registry.#models.map(model => `${model.provider}/${model.id}`));
|
|
2750
|
+
for (const model of this.#models) {
|
|
2751
|
+
if (!unchangedProviders.has(model.provider) || stagedModelKeys.has(`${model.provider}/${model.id}`)) continue;
|
|
2752
|
+
registry.#models.push(model);
|
|
2753
|
+
}
|
|
2754
|
+
registry.#discoveryManager.retainStatesFrom(this.#discoveryManager, unchangedProviders);
|
|
2755
|
+
const retainedActivity = new Map(registry.#providerActivity);
|
|
2756
|
+
for (const provider of unchangedProviders) {
|
|
2757
|
+
copyProviderEntry(registry.#configuredDiscoveryEvidence, this.#configuredDiscoveryEvidence, provider);
|
|
2758
|
+
copyProviderEntry(registry.#descriptorDiscoveryEvidence, this.#descriptorDiscoveryEvidence, provider);
|
|
2759
|
+
copyProviderEntry(registry.#descriptorDiscoveryGenerations, this.#descriptorDiscoveryGenerations, provider);
|
|
2760
|
+
copyProviderEntry(registry.#providerEvidenceApiKeys, this.#providerEvidenceApiKeys, provider);
|
|
2761
|
+
copyProviderEntry(retainedActivity, this.#providerActivity, provider);
|
|
2762
|
+
}
|
|
2763
|
+
registry.#providerActivity = retainedActivity;
|
|
2764
|
+
registry.#rebuildCanonicalIndex();
|
|
2765
|
+
const rebaseProviderDiscoveryAfterQueuedRefresh = () => {
|
|
2766
|
+
const changedProviders = new Set(
|
|
2767
|
+
[...unchangedProviders].filter(provider => {
|
|
2768
|
+
const snapshot = unchangedProviderSnapshots.get(provider)!;
|
|
2769
|
+
return (
|
|
2770
|
+
!isDeepStrictEqual(
|
|
2771
|
+
snapshot.models,
|
|
2772
|
+
this.#models.filter(model => model.provider === provider),
|
|
2773
|
+
) ||
|
|
2774
|
+
!isDeepStrictEqual(snapshot.discoveryState, this.#discoveryManager.getState(provider)) ||
|
|
2775
|
+
!isDeepStrictEqual(snapshot.configuredEvidence, this.#configuredDiscoveryEvidence.get(provider)) ||
|
|
2776
|
+
!isDeepStrictEqual(snapshot.descriptorEvidence, this.#descriptorDiscoveryEvidence.get(provider)) ||
|
|
2777
|
+
snapshot.descriptorGeneration !== this.#descriptorDiscoveryGenerations.get(provider) ||
|
|
2778
|
+
snapshot.providerEvidenceApiKey !== this.#providerEvidenceApiKeys.get(provider) ||
|
|
2779
|
+
snapshot.credentiallessFallback !== this.#credentiallessAuthFallbackProviders.get(provider) ||
|
|
2780
|
+
snapshot.optionalAuthPreflightGeneration !== this.#optionalAuthPreflightGenerations.get(provider) ||
|
|
2781
|
+
!isDeepStrictEqual(snapshot.activity, this.#providerActivity.get(provider))
|
|
2782
|
+
);
|
|
2783
|
+
}),
|
|
2784
|
+
);
|
|
2785
|
+
if (changedProviders.size === 0) return;
|
|
2786
|
+
|
|
2787
|
+
registry.#models = registry.#models.filter(model => !changedProviders.has(model.provider));
|
|
2788
|
+
registry.#models.push(...this.#models.filter(model => changedProviders.has(model.provider)));
|
|
2789
|
+
for (const provider of changedProviders) registry.#discoveryManager.invalidate(provider);
|
|
2790
|
+
registry.#discoveryManager.retainStatesFrom(this.#discoveryManager, changedProviders);
|
|
2791
|
+
const providerActivity = new Map(registry.#providerActivity);
|
|
2792
|
+
for (const provider of changedProviders) {
|
|
2793
|
+
copyProviderEntry(registry.#configuredDiscoveryEvidence, this.#configuredDiscoveryEvidence, provider);
|
|
2794
|
+
copyProviderEntry(registry.#descriptorDiscoveryEvidence, this.#descriptorDiscoveryEvidence, provider);
|
|
2795
|
+
copyProviderEntry(registry.#descriptorDiscoveryGenerations, this.#descriptorDiscoveryGenerations, provider);
|
|
2796
|
+
copyProviderEntry(registry.#providerEvidenceApiKeys, this.#providerEvidenceApiKeys, provider);
|
|
2797
|
+
copyProviderEntry(
|
|
2798
|
+
registry.#credentiallessAuthFallbackProviders,
|
|
2799
|
+
this.#credentiallessAuthFallbackProviders,
|
|
2800
|
+
provider,
|
|
2801
|
+
);
|
|
2802
|
+
copyProviderEntry(
|
|
2803
|
+
registry.#optionalAuthPreflightGenerations,
|
|
2804
|
+
this.#optionalAuthPreflightGenerations,
|
|
2805
|
+
provider,
|
|
2806
|
+
);
|
|
2807
|
+
copyProviderEntry(providerActivity, this.#providerActivity, provider);
|
|
2808
|
+
}
|
|
2809
|
+
registry.#providerActivity = providerActivity;
|
|
2810
|
+
registry.#rebuildCanonicalIndex();
|
|
2811
|
+
};
|
|
2812
|
+
const expectedGeneration = this.#catalogRefreshGeneration;
|
|
2813
|
+
const isCurrent = () => !this.#disposed && this.#catalogRefreshGeneration === expectedGeneration;
|
|
2814
|
+
const commit = () => {
|
|
2815
|
+
if (finished) throw new Error("Models config reload candidate is no longer active");
|
|
2816
|
+
if (!diagnostics.valid) throw new Error("Cannot commit an invalid models config reload candidate");
|
|
2817
|
+
if (committed) return;
|
|
2818
|
+
if (!isCurrent()) {
|
|
2819
|
+
throw new Error("Model catalog changed during configuration preflight");
|
|
2820
|
+
}
|
|
2821
|
+
for (const [provider, apiKey] of registry.#ownedConfigApiKeys) {
|
|
2822
|
+
if (apiKey.runtimeSourced === true && apiKey.configSourced !== true) continue;
|
|
2823
|
+
if (this.authStorage.hasAnyCredentialPin(provider)) {
|
|
2824
|
+
throw new Error(
|
|
2825
|
+
`Cannot configure an API key override for ${provider} while a credential pin is active or unavailable`,
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
rebaseProviderDiscoveryAfterQueuedRefresh();
|
|
2830
|
+
this.#swapReloadableState(registry);
|
|
2831
|
+
try {
|
|
2832
|
+
this.#reconcileOwnedConfigApiKeys(this.#ownedConfigApiKeys, registry.#ownedConfigApiKeys);
|
|
2833
|
+
this.#acceptedModelsConfigPresent = source.status === "ok";
|
|
2834
|
+
this.#acceptedConfigValue = nextConfigValue;
|
|
2835
|
+
// The captured source may precede the current disk revision.
|
|
2836
|
+
this.#lastStaticLoadMtime = null;
|
|
2837
|
+
committed = true;
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
this.#swapReloadableState(registry);
|
|
2840
|
+
this.#reconcileOwnedConfigApiKeys(this.#ownedConfigApiKeys, registry.#ownedConfigApiKeys);
|
|
2841
|
+
throw error;
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
const rollback = () => {
|
|
2845
|
+
if (finished) return;
|
|
2846
|
+
if (committed) {
|
|
2847
|
+
this.#swapReloadableState(registry);
|
|
2848
|
+
this.#reconcileOwnedConfigApiKeys(this.#ownedConfigApiKeys, registry.#ownedConfigApiKeys);
|
|
2849
|
+
this.#acceptedModelsConfigPresent = previousConfigPresent;
|
|
2850
|
+
this.#acceptedConfigValue = previousConfigValue;
|
|
2851
|
+
committed = false;
|
|
2852
|
+
}
|
|
2853
|
+
finished = true;
|
|
2854
|
+
void registry.dispose();
|
|
2855
|
+
};
|
|
2856
|
+
const finalize = () => {
|
|
2857
|
+
if (finished) return;
|
|
2858
|
+
if (!committed) throw new Error("Cannot finalize an unpublished models configuration");
|
|
2859
|
+
finished = true;
|
|
2860
|
+
this.#catalogRefreshGeneration++;
|
|
2861
|
+
this.#notifyCatalogChanged();
|
|
2862
|
+
void registry.dispose();
|
|
2863
|
+
};
|
|
2864
|
+
return { valid: diagnostics.valid, changed, diagnostics, registry, isCurrent, commit, rollback, finalize };
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
#acquireLease(kind: RegistryLeaseWaiter["kind"], signal?: AbortSignal): Promise<() => void> {
|
|
2868
|
+
if (this.#disposed) return Promise.reject(new Error("Model registry is disposed"));
|
|
2869
|
+
if (signal?.aborted) return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
2870
|
+
if (
|
|
2871
|
+
!this.#publicationFenceHeld &&
|
|
2872
|
+
this.#leaseWaiters.length === 0 &&
|
|
2873
|
+
(kind === "consumer" || this.#activeConsumerLeases === 0)
|
|
2874
|
+
) {
|
|
2875
|
+
return Promise.resolve(this.#grantLease(kind));
|
|
2876
|
+
}
|
|
2877
|
+
const { promise, resolve, reject } = Promise.withResolvers<() => void>();
|
|
2878
|
+
const waiter: RegistryLeaseWaiter = { kind, signal, resolve, reject, settled: false };
|
|
2879
|
+
if (signal) {
|
|
2880
|
+
waiter.abort = () => {
|
|
2881
|
+
if (waiter.settled) return;
|
|
2882
|
+
waiter.settled = true;
|
|
2883
|
+
this.#leaseWaiters = this.#leaseWaiters.filter(item => item !== waiter);
|
|
2884
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
2885
|
+
this.#drainLeaseWaiters();
|
|
2886
|
+
};
|
|
2887
|
+
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
2888
|
+
}
|
|
2889
|
+
this.#leaseWaiters.push(waiter);
|
|
2890
|
+
this.#drainLeaseWaiters();
|
|
2891
|
+
return promise;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
#grantLease(kind: RegistryLeaseWaiter["kind"]): () => void {
|
|
2895
|
+
if (kind === "publication") this.#publicationFenceHeld = true;
|
|
2896
|
+
else this.#activeConsumerLeases += 1;
|
|
2897
|
+
let released = false;
|
|
2898
|
+
return () => {
|
|
2899
|
+
if (released) return;
|
|
2900
|
+
released = true;
|
|
2901
|
+
if (kind === "publication") this.#publicationFenceHeld = false;
|
|
2902
|
+
else this.#activeConsumerLeases -= 1;
|
|
2903
|
+
this.#drainLeaseWaiters();
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
#drainLeaseWaiters(): void {
|
|
2908
|
+
if (this.#publicationFenceHeld) return;
|
|
2909
|
+
while (this.#leaseWaiters.length > 0) {
|
|
2910
|
+
const waiter = this.#leaseWaiters[0]!;
|
|
2911
|
+
if (waiter.kind === "publication") {
|
|
2912
|
+
if (this.#activeConsumerLeases > 0) return;
|
|
2913
|
+
this.#leaseWaiters.shift();
|
|
2914
|
+
waiter.settled = true;
|
|
2915
|
+
if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
2916
|
+
waiter.resolve(this.#grantLease("publication"));
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
this.#leaseWaiters.shift();
|
|
2920
|
+
waiter.settled = true;
|
|
2921
|
+
if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
2922
|
+
waiter.resolve(this.#grantLease("consumer"));
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
async withConsumerLease<T>(body: () => T | Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
2927
|
+
signal?.throwIfAborted();
|
|
2928
|
+
if (this.#consumerContext.getStore()?.active) return await body();
|
|
2929
|
+
const release = await this.#acquireLease("consumer", signal);
|
|
2930
|
+
const owner = { active: true };
|
|
2931
|
+
try {
|
|
2932
|
+
return await this.#consumerContext.run(owner, body);
|
|
2933
|
+
} finally {
|
|
2934
|
+
owner.active = false;
|
|
2935
|
+
release();
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
async acquirePublicationFence(signal?: AbortSignal): Promise<() => void> {
|
|
2940
|
+
if (this.#consumerContext.getStore()?.active) {
|
|
2941
|
+
throw new Error("Cannot publish configuration while holding a model consumer lease");
|
|
2942
|
+
}
|
|
2943
|
+
const release = await this.#acquireLease("publication", signal);
|
|
2944
|
+
const gate = Promise.withResolvers<void>();
|
|
2945
|
+
const entered = Promise.withResolvers<void>();
|
|
2946
|
+
const barrier = this.#enqueueCatalogMutation(async () => {
|
|
2947
|
+
entered.resolve();
|
|
2948
|
+
await gate.promise;
|
|
2949
|
+
});
|
|
2950
|
+
const cancelled = Promise.withResolvers<void>();
|
|
2951
|
+
const abort = () => cancelled.reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
2952
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2953
|
+
try {
|
|
2954
|
+
if (signal?.aborted) abort();
|
|
2955
|
+
await Promise.race([entered.promise, cancelled.promise]);
|
|
2956
|
+
signal?.throwIfAborted();
|
|
2957
|
+
} catch (error) {
|
|
2958
|
+
gate.resolve();
|
|
2959
|
+
void barrier.finally(release);
|
|
2960
|
+
throw error;
|
|
2961
|
+
} finally {
|
|
2962
|
+
signal?.removeEventListener("abort", abort);
|
|
2963
|
+
}
|
|
2964
|
+
let released = false;
|
|
2965
|
+
return () => {
|
|
2966
|
+
if (released) return;
|
|
2967
|
+
released = true;
|
|
2968
|
+
gate.resolve();
|
|
2969
|
+
void barrier.finally(release);
|
|
2970
|
+
};
|
|
1889
2971
|
}
|
|
1890
2972
|
|
|
1891
2973
|
dispose(): Promise<void> {
|
|
1892
2974
|
if (this.#disposePromise) return this.#disposePromise;
|
|
1893
2975
|
this.#disposed = true;
|
|
2976
|
+
for (const waiter of this.#leaseWaiters.splice(0)) {
|
|
2977
|
+
waiter.settled = true;
|
|
2978
|
+
if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
2979
|
+
waiter.reject(new Error("Model registry is disposed"));
|
|
2980
|
+
}
|
|
1894
2981
|
this.#catalogRefreshGeneration++;
|
|
1895
2982
|
const awaitRefreshDisposal = this.#cancelModelPresetRegistryRefresh?.() ?? Promise.resolve();
|
|
1896
2983
|
this.#cancelModelPresetRegistryRefresh = undefined;
|
|
1897
2984
|
this.#unsubscribeAuthGeneration?.();
|
|
1898
2985
|
this.#unsubscribeAuthGeneration = undefined;
|
|
1899
|
-
this
|
|
2986
|
+
this.#clearOwnedConfigApiKeys();
|
|
1900
2987
|
this.#disposeAuthStorageFallbackResolver?.();
|
|
1901
2988
|
this.#disposeAuthStorageFallbackResolver = undefined;
|
|
1902
2989
|
this.#catalogChangeListeners.clear();
|
|
@@ -2141,6 +3228,11 @@ export class ModelRegistry {
|
|
|
2141
3228
|
);
|
|
2142
3229
|
return JSON.stringify({
|
|
2143
3230
|
apiKeyEnv: [...this.#configuredApiKeyEnvNames].sort().map(name => [name, Bun.env[name] ?? ""]),
|
|
3231
|
+
credentialPins: this.#stagedReloadCandidate
|
|
3232
|
+
? []
|
|
3233
|
+
: [...this.#configuredProviderIds]
|
|
3234
|
+
.sort()
|
|
3235
|
+
.map(provider => [provider, this.authStorage.hasAnyCredentialPin(provider)]),
|
|
2144
3236
|
implicitEndpoints: [
|
|
2145
3237
|
["OLLAMA_BASE_URL", Bun.env.OLLAMA_BASE_URL || ""],
|
|
2146
3238
|
["LLAMA_CPP_BASE_URL", Bun.env.LLAMA_CPP_BASE_URL || ""],
|
|
@@ -2171,7 +3263,7 @@ export class ModelRegistry {
|
|
|
2171
3263
|
environmentFingerprint === this.#lastStaticLoadEnvironmentFingerprint &&
|
|
2172
3264
|
modelPresetRegistryFingerprint === this.#lastModelPresetRegistryFingerprint
|
|
2173
3265
|
) {
|
|
2174
|
-
//
|
|
3266
|
+
// Config, settings-derived provider state, and effective credential selectors are unchanged.
|
|
2175
3267
|
return;
|
|
2176
3268
|
}
|
|
2177
3269
|
this.#modelsConfigFile.invalidate();
|
|
@@ -2189,7 +3281,7 @@ export class ModelRegistry {
|
|
|
2189
3281
|
// Drop config-sourced apiKeys from AuthStorage before reload; entries
|
|
2190
3282
|
// removed from models.yml must actually disappear from the resolver, not
|
|
2191
3283
|
// linger from the previous parse. The post-load setters below repopulate.
|
|
2192
|
-
this
|
|
3284
|
+
this.#clearOwnedConfigApiKeys();
|
|
2193
3285
|
// Runtime provider keys are reapplied after #loadModels so they retain
|
|
2194
3286
|
// registration-time precedence over colliding static provider keys.
|
|
2195
3287
|
this.#providerOverrides.clear();
|
|
@@ -2202,7 +3294,7 @@ export class ModelRegistry {
|
|
|
2202
3294
|
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
2203
3295
|
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
2204
3296
|
: resolveApiKeyConfig(apiKeyConfig);
|
|
2205
|
-
if (!resolved) {
|
|
3297
|
+
if (!resolved || this.authStorage.hasAnyCredentialPin(provider)) {
|
|
2206
3298
|
this.#runtimeProviderCredentialInstalled.delete(provider);
|
|
2207
3299
|
const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
|
|
2208
3300
|
if (authHeader === true) {
|
|
@@ -2225,7 +3317,7 @@ export class ModelRegistry {
|
|
|
2225
3317
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
2226
3318
|
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
2227
3319
|
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
2228
|
-
this
|
|
3320
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
2229
3321
|
const override = this.#runtimeProviderOverrides.get(provider);
|
|
2230
3322
|
if (override) this.#runtimeProviderOverrides.set(provider, { ...override, apiKey: resolved });
|
|
2231
3323
|
const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
|
|
@@ -2258,11 +3350,11 @@ export class ModelRegistry {
|
|
|
2258
3350
|
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
2259
3351
|
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
2260
3352
|
: resolveApiKeyConfig(apiKeyConfig);
|
|
2261
|
-
if (!resolved) continue;
|
|
3353
|
+
if (!resolved || this.authStorage.hasAnyCredentialPin(provider)) continue;
|
|
2262
3354
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
2263
3355
|
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
2264
3356
|
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
2265
|
-
this
|
|
3357
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
2266
3358
|
}
|
|
2267
3359
|
this.#lastDisabledProviderKey = disabledProviderKey;
|
|
2268
3360
|
}
|
|
@@ -2298,10 +3390,10 @@ export class ModelRegistry {
|
|
|
2298
3390
|
this.#codexContextWindowOverrides = this.#collectCodexContextWindowOverrides();
|
|
2299
3391
|
this.#equivalenceConfig = equivalence;
|
|
2300
3392
|
this.#modelBindingsApplier.setBindings(modelBindings);
|
|
2301
|
-
const acceptedPresets =
|
|
2302
|
-
this.#
|
|
2303
|
-
this.#modelPresetRegistryDependencies
|
|
2304
|
-
|
|
3393
|
+
const acceptedPresets = this.#stagedReloadCandidate
|
|
3394
|
+
? this.#acceptedPresets!
|
|
3395
|
+
: loadAcceptedModelPresetProfiles(this.#modelPresetRegistryAgentDir, this.#modelPresetRegistryDependencies);
|
|
3396
|
+
this.#acceptedPresets = acceptedPresets;
|
|
2305
3397
|
const acceptedRegistryError = acceptedPresets.error
|
|
2306
3398
|
? new ConfigError("model-preset-registry", undefined, {
|
|
2307
3399
|
err: new Error(acceptedPresets.error),
|
|
@@ -2476,6 +3568,9 @@ export class ModelRegistry {
|
|
|
2476
3568
|
const inheritedCompat = explicitTransportMatches
|
|
2477
3569
|
? explicitTransport?.compat
|
|
2478
3570
|
: commonRegistryTransportCompat(transportTemplates);
|
|
3571
|
+
const streamIdleTimeoutMs = isRequestTransformApi(registryModel.api)
|
|
3572
|
+
? (registryModel.streamIdleTimeoutMs ?? explicitTransport?.streamIdleTimeoutMs)
|
|
3573
|
+
: undefined;
|
|
2479
3574
|
const hydratedRegistryModel = {
|
|
2480
3575
|
...registryModel,
|
|
2481
3576
|
baseUrl: transportSource.baseUrl!,
|
|
@@ -2483,6 +3578,7 @@ export class ModelRegistry {
|
|
|
2483
3578
|
transport: transportSource.transport,
|
|
2484
3579
|
requestTransform: transportSource.requestTransform,
|
|
2485
3580
|
cacheRetention: transportSource.cacheRetention,
|
|
3581
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
2486
3582
|
isOAuth: transportSource.isOAuth,
|
|
2487
3583
|
compat:
|
|
2488
3584
|
inheritedCompat || registryModel.compat || explicitTransport?.compat
|
|
@@ -2498,14 +3594,23 @@ export class ModelRegistry {
|
|
|
2498
3594
|
continue;
|
|
2499
3595
|
}
|
|
2500
3596
|
const existing = merged[existingIndex]!;
|
|
3597
|
+
const { streamIdleTimeoutMs: existingStreamIdleTimeoutMs, ...existingWithoutStreamIdleTimeout } = existing;
|
|
2501
3598
|
const effectiveApi = explicitTransport?.api ?? existing.api;
|
|
2502
3599
|
const registryApiMatchesEffectiveTransport = registryModel.api === effectiveApi;
|
|
2503
3600
|
const registryMetadata = registryApiMatchesEffectiveTransport
|
|
2504
3601
|
? registryModel
|
|
2505
3602
|
: registryModelMetadataWithoutApiSpecificFields(registryModel);
|
|
3603
|
+
const {
|
|
3604
|
+
streamIdleTimeoutMs: registryMetadataStreamIdleTimeoutMs,
|
|
3605
|
+
...registryMetadataWithoutStreamIdleTimeout
|
|
3606
|
+
} = registryMetadata;
|
|
3607
|
+
const streamIdleTimeoutMs = isRequestTransformApi(effectiveApi)
|
|
3608
|
+
? (registryMetadataStreamIdleTimeoutMs ?? existingStreamIdleTimeoutMs)
|
|
3609
|
+
: undefined;
|
|
2506
3610
|
merged[existingIndex] = {
|
|
2507
|
-
...
|
|
2508
|
-
...
|
|
3611
|
+
...existingWithoutStreamIdleTimeout,
|
|
3612
|
+
...registryMetadataWithoutStreamIdleTimeout,
|
|
3613
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
2509
3614
|
api: explicitTransport?.api ?? existing.api,
|
|
2510
3615
|
baseUrl: existing.baseUrl,
|
|
2511
3616
|
headers: existing.headers,
|
|
@@ -2542,9 +3647,14 @@ export class ModelRegistry {
|
|
|
2542
3647
|
const existingIndex = indexByKey.get(key);
|
|
2543
3648
|
if (existingIndex !== undefined) {
|
|
2544
3649
|
const existingModel = merged[existingIndex];
|
|
3650
|
+
const { streamIdleTimeoutMs: existingStreamIdleTimeoutMs, ...existingModelWithoutStreamIdleTimeout } =
|
|
3651
|
+
existingModel;
|
|
2545
3652
|
const referenceModel = resolveCustomModelReference(customModel.id);
|
|
3653
|
+
const streamIdleTimeoutMs = isRequestTransformApi(customModel.api)
|
|
3654
|
+
? (customModel.streamIdleTimeoutMs ?? existingStreamIdleTimeoutMs)
|
|
3655
|
+
: undefined;
|
|
2546
3656
|
merged[existingIndex] = enrichModelThinking({
|
|
2547
|
-
...
|
|
3657
|
+
...existingModelWithoutStreamIdleTimeout,
|
|
2548
3658
|
id: customModel.id,
|
|
2549
3659
|
provider: customModel.provider,
|
|
2550
3660
|
api: customModel.api,
|
|
@@ -2568,6 +3678,7 @@ export class ModelRegistry {
|
|
|
2568
3678
|
wireModelId: customModel.wireModelId,
|
|
2569
3679
|
requestTransform: customModel.requestTransform,
|
|
2570
3680
|
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
3681
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
2571
3682
|
} as Model<Api>);
|
|
2572
3683
|
} else {
|
|
2573
3684
|
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
@@ -2740,10 +3851,12 @@ export class ModelRegistry {
|
|
|
2740
3851
|
});
|
|
2741
3852
|
}
|
|
2742
3853
|
#sanitizeDiscoverableModelsForCache(_providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
2743
|
-
return this.#stripModelBaseUrlQueries(models).map(model =>
|
|
2744
|
-
...model,
|
|
2745
|
-
|
|
2746
|
-
|
|
3854
|
+
return this.#stripModelBaseUrlQueries(models).map(model => {
|
|
3855
|
+
const cachedModel = { ...model, headers: undefined };
|
|
3856
|
+
// Model idle timeouts come from local config and must be re-derived on cache load.
|
|
3857
|
+
delete cachedModel.streamIdleTimeoutMs;
|
|
3858
|
+
return cachedModel;
|
|
3859
|
+
});
|
|
2747
3860
|
}
|
|
2748
3861
|
#stripModelBaseUrlQueries(models: readonly Model<Api>[]): Model<Api>[] {
|
|
2749
3862
|
return models.map(model => createTrustedStrippedModelClone(model));
|
|
@@ -2824,7 +3937,10 @@ export class ModelRegistry {
|
|
|
2824
3937
|
|
|
2825
3938
|
#loadCustomModels(): CustomModelsResult {
|
|
2826
3939
|
this.#configuredApiKeyEnvNames.clear();
|
|
2827
|
-
const
|
|
3940
|
+
const loaded = this.#modelsConfigSource ?? this.#modelsConfigFile.tryLoad();
|
|
3941
|
+
const { value, error, status } = loaded;
|
|
3942
|
+
this.#acceptedConfigValue = status === "ok" ? value : undefined;
|
|
3943
|
+
if (status === "ok") this.#acceptedModelsConfigPresent = true;
|
|
2828
3944
|
|
|
2829
3945
|
if (status === "error") {
|
|
2830
3946
|
return {
|
|
@@ -2859,6 +3975,7 @@ export class ModelRegistry {
|
|
|
2859
3975
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
2860
3976
|
|
|
2861
3977
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
3978
|
+
const credentialPinActive = !this.#stagedReloadCandidate && this.authStorage.hasAnyCredentialPin(providerName);
|
|
2862
3979
|
const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
|
|
2863
3980
|
const isOAuth = resolveCustomModelIsOAuth(
|
|
2864
3981
|
(providerConfig.api as Api | undefined) ?? "openai-completions",
|
|
@@ -2873,9 +3990,11 @@ export class ModelRegistry {
|
|
|
2873
3990
|
if (providerConfig.openaiCompat?.apiKey)
|
|
2874
3991
|
this.#configuredApiKeyEnvNames.add(providerConfig.openaiCompat.apiKey);
|
|
2875
3992
|
if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
|
|
2876
|
-
const providerApiKeyConfig =
|
|
2877
|
-
?
|
|
2878
|
-
:
|
|
3993
|
+
const providerApiKeyConfig = credentialPinActive
|
|
3994
|
+
? undefined
|
|
3995
|
+
: providerConfig.apiKey
|
|
3996
|
+
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
3997
|
+
: resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
|
|
2879
3998
|
const localOpenAICompat = providerConfig.openaiCompat;
|
|
2880
3999
|
const rotatingApiKeyEnv = providerConfig.apiKey
|
|
2881
4000
|
? undefined
|
|
@@ -2883,18 +4002,21 @@ export class ModelRegistry {
|
|
|
2883
4002
|
if (rotatingApiKeyEnv) this.#customProviderApiKeyEnvNames.set(providerName, rotatingApiKeyEnv);
|
|
2884
4003
|
if (providerConfig.authHeader !== undefined)
|
|
2885
4004
|
this.#customProviderAuthHeaders.set(providerName, providerConfig.authHeader);
|
|
2886
|
-
const localOpenAICompatApiKeyConfig =
|
|
2887
|
-
|
|
2888
|
-
?
|
|
2889
|
-
|
|
2890
|
-
|
|
4005
|
+
const localOpenAICompatApiKeyConfig =
|
|
4006
|
+
localOpenAICompat && !credentialPinActive
|
|
4007
|
+
? localOpenAICompat.apiKey
|
|
4008
|
+
? resolveApiKeyConfig(localOpenAICompat.apiKey)
|
|
4009
|
+
: resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
|
|
4010
|
+
: undefined;
|
|
2891
4011
|
if (localOpenAICompat) {
|
|
2892
4012
|
const localOpenAICompatBaseUrl = normalizeLocalOpenAICompatBaseUrl(localOpenAICompat.baseUrl);
|
|
2893
|
-
const localCompatResolvedKey =
|
|
2894
|
-
?
|
|
2895
|
-
: localOpenAICompat.
|
|
2896
|
-
?
|
|
2897
|
-
:
|
|
4013
|
+
const localCompatResolvedKey = credentialPinActive
|
|
4014
|
+
? undefined
|
|
4015
|
+
: localOpenAICompat.apiKey
|
|
4016
|
+
? resolveApiKeyConfig(localOpenAICompat.apiKey)
|
|
4017
|
+
: localOpenAICompat.apiKeyEnv
|
|
4018
|
+
? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
|
|
4019
|
+
: undefined;
|
|
2898
4020
|
overrides.set(providerName, {
|
|
2899
4021
|
api: "openai-completions",
|
|
2900
4022
|
baseUrl: localOpenAICompatBaseUrl,
|
|
@@ -2920,10 +4042,7 @@ export class ModelRegistry {
|
|
|
2920
4042
|
});
|
|
2921
4043
|
if (localCompatResolvedKey) {
|
|
2922
4044
|
this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
|
|
2923
|
-
this
|
|
2924
|
-
envSourced: !localOpenAICompat.apiKey,
|
|
2925
|
-
owner: this.#authStorageConfigOwner,
|
|
2926
|
-
});
|
|
4045
|
+
this.#setOwnedConfigApiKey(providerName, localCompatResolvedKey, !localOpenAICompat.apiKey);
|
|
2927
4046
|
} else {
|
|
2928
4047
|
keylessProviders.add(providerName);
|
|
2929
4048
|
this.#optionalAuthProviders.add(providerName);
|
|
@@ -2940,10 +4059,11 @@ export class ModelRegistry {
|
|
|
2940
4059
|
providerConfig.disableStrictTools ||
|
|
2941
4060
|
providerConfig.requestTransform ||
|
|
2942
4061
|
providerConfig.transport ||
|
|
2943
|
-
providerConfig.cacheRetention
|
|
4062
|
+
providerConfig.cacheRetention ||
|
|
4063
|
+
providerConfig.streamIdleTimeoutMs !== undefined
|
|
2944
4064
|
) {
|
|
2945
4065
|
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
2946
|
-
|
|
4066
|
+
const providerOverride: ProviderOverride = {
|
|
2947
4067
|
api: providerConfig.api as Api | undefined,
|
|
2948
4068
|
baseUrl: providerConfig.baseUrl,
|
|
2949
4069
|
headers: providerConfig.headers,
|
|
@@ -2953,8 +4073,14 @@ export class ModelRegistry {
|
|
|
2953
4073
|
transport: providerConfig.transport,
|
|
2954
4074
|
requestTransform: providerConfig.requestTransform,
|
|
2955
4075
|
cacheRetention: providerConfig.cacheRetention,
|
|
4076
|
+
streamIdleTimeoutMs: providerConfig.streamIdleTimeoutMs,
|
|
2956
4077
|
isOAuth,
|
|
2957
|
-
}
|
|
4078
|
+
};
|
|
4079
|
+
const existingOverride = overrides.get(providerName);
|
|
4080
|
+
overrides.set(
|
|
4081
|
+
providerName,
|
|
4082
|
+
existingOverride ? this.#mergeProviderOverride(existingOverride, providerOverride) : providerOverride,
|
|
4083
|
+
);
|
|
2958
4084
|
}
|
|
2959
4085
|
|
|
2960
4086
|
if (authMode === "none") {
|
|
@@ -3006,22 +4132,20 @@ export class ModelRegistry {
|
|
|
3006
4132
|
});
|
|
3007
4133
|
}
|
|
3008
4134
|
|
|
3009
|
-
// Store API
|
|
3010
|
-
//
|
|
3011
|
-
//
|
|
3012
|
-
// must authenticate the outbound request.
|
|
4135
|
+
// Store API keys for fallback resolution and register config overrides.
|
|
4136
|
+
// A pin in any session suppresses shared config keys so a reload cannot
|
|
4137
|
+
// override another session's selected account.
|
|
3013
4138
|
if (providerConfig.apiKey || providerConfig.apiKeyEnv) {
|
|
3014
|
-
const resolved =
|
|
3015
|
-
?
|
|
3016
|
-
: providerConfig.
|
|
3017
|
-
?
|
|
3018
|
-
:
|
|
4139
|
+
const resolved = credentialPinActive
|
|
4140
|
+
? undefined
|
|
4141
|
+
: providerConfig.apiKey
|
|
4142
|
+
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
4143
|
+
: providerConfig.apiKeyEnv
|
|
4144
|
+
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
4145
|
+
: undefined;
|
|
3019
4146
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
3020
4147
|
if (resolved) {
|
|
3021
|
-
this
|
|
3022
|
-
envSourced: !providerConfig.apiKey,
|
|
3023
|
-
owner: this.#authStorageConfigOwner,
|
|
3024
|
-
});
|
|
4148
|
+
this.#setOwnedConfigApiKey(providerName, resolved, !providerConfig.apiKey);
|
|
3025
4149
|
}
|
|
3026
4150
|
}
|
|
3027
4151
|
|
|
@@ -3205,8 +4329,15 @@ export class ModelRegistry {
|
|
|
3205
4329
|
}
|
|
3206
4330
|
|
|
3207
4331
|
async #writeCheckedModelsConfig(config: ModelsConfig): Promise<void> {
|
|
3208
|
-
|
|
3209
|
-
|
|
4332
|
+
const configPath = this.#modelsConfigFile.path();
|
|
4333
|
+
const parentPath = path.dirname(configPath);
|
|
4334
|
+
await fs.mkdir(parentPath, { recursive: true, mode: 0o700 });
|
|
4335
|
+
// The owner-only writer rejects a symlink in any path component and rejects
|
|
4336
|
+
// "..". Resolve only the parent so an alias directory and a lexical ".."
|
|
4337
|
+
// still reach the same file. The final name stays lexical, so a symlink
|
|
4338
|
+
// at models.yml is still opened with O_NOFOLLOW.
|
|
4339
|
+
const parent = await fs.realpath(parentPath);
|
|
4340
|
+
writeExplicitOwnerOnlyTextSync(path.join(parent, path.basename(configPath)), Bun.YAML.stringify(config, null, 2));
|
|
3210
4341
|
this.#modelsConfigFile.invalidate();
|
|
3211
4342
|
this.#reloadStaticModels();
|
|
3212
4343
|
}
|
|
@@ -4319,36 +5450,7 @@ export class ModelRegistry {
|
|
|
4319
5450
|
modelId: string,
|
|
4320
5451
|
entry?: { id?: unknown; owned_by?: unknown },
|
|
4321
5452
|
): 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;
|
|
5453
|
+
return resolveModelApiFromDiscovery(providerConfig.api, providerConfig.discovery, modelId, entry);
|
|
4352
5454
|
}
|
|
4353
5455
|
|
|
4354
5456
|
async #discoverModelsDevProvider(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
@@ -4823,6 +5925,7 @@ export class ModelRegistry {
|
|
|
4823
5925
|
transport: override.transport ?? baseOverride?.transport,
|
|
4824
5926
|
requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
|
|
4825
5927
|
cacheRetention: override.cacheRetention ?? baseOverride?.cacheRetention,
|
|
5928
|
+
streamIdleTimeoutMs: override.streamIdleTimeoutMs ?? baseOverride?.streamIdleTimeoutMs,
|
|
4826
5929
|
isOAuth: override.isOAuth ?? baseOverride?.isOAuth,
|
|
4827
5930
|
};
|
|
4828
5931
|
}
|
|
@@ -4838,6 +5941,7 @@ export class ModelRegistry {
|
|
|
4838
5941
|
| "transport"
|
|
4839
5942
|
| "requestTransform"
|
|
4840
5943
|
| "cacheRetention"
|
|
5944
|
+
| "streamIdleTimeoutMs"
|
|
4841
5945
|
>,
|
|
4842
5946
|
): T {
|
|
4843
5947
|
const overrideHeaders = (
|
|
@@ -4872,6 +5976,8 @@ export class ModelRegistry {
|
|
|
4872
5976
|
delete overrideHeaders[GENERATED_AUTH_HEADER];
|
|
4873
5977
|
}
|
|
4874
5978
|
const headers = mergeAuthHeader(overrideHeaders, canInject, canInject ? override.apiKey : undefined);
|
|
5979
|
+
const streamIdleTimeoutMs =
|
|
5980
|
+
entry.streamIdleTimeoutMs ?? (isRequestTransformApi(entry.api) ? override.streamIdleTimeoutMs : undefined);
|
|
4875
5981
|
const result = {
|
|
4876
5982
|
...entry,
|
|
4877
5983
|
compat: mergeProviderCompat(entry.compat, override.compat),
|
|
@@ -4885,6 +5991,7 @@ export class ModelRegistry {
|
|
|
4885
5991
|
override.requestTransform,
|
|
4886
5992
|
),
|
|
4887
5993
|
cacheRetention: entry.cacheRetention ?? override.cacheRetention,
|
|
5994
|
+
...(streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs } : {}),
|
|
4888
5995
|
};
|
|
4889
5996
|
if (!canInject && explicitAuthKey !== undefined) {
|
|
4890
5997
|
delete (result.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
|
|
@@ -5143,19 +6250,17 @@ export class ModelRegistry {
|
|
|
5143
6250
|
for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
5144
6251
|
const modelDefs = providerConfig.models ?? [];
|
|
5145
6252
|
if (modelDefs.length === 0) continue; // Override-only, no custom models
|
|
5146
|
-
|
|
5147
|
-
|
|
6253
|
+
const credentialPinActive = !this.#stagedReloadCandidate && this.authStorage.hasAnyCredentialPin(providerName);
|
|
6254
|
+
const apiKey = credentialPinActive
|
|
6255
|
+
? undefined
|
|
6256
|
+
: providerConfig.apiKey
|
|
5148
6257
|
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
5149
6258
|
: providerConfig.apiKeyEnv
|
|
5150
6259
|
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
5151
6260
|
: undefined;
|
|
5152
|
-
|
|
5153
|
-
if (
|
|
5154
|
-
|
|
5155
|
-
envSourced: !providerConfig.apiKey,
|
|
5156
|
-
owner: this.#authStorageConfigOwner,
|
|
5157
|
-
});
|
|
5158
|
-
}
|
|
6261
|
+
if (providerConfig.apiKey || providerConfig.apiKeyEnv) {
|
|
6262
|
+
if (apiKey) this.#customProviderApiKeys.set(providerName, apiKey);
|
|
6263
|
+
if (apiKey) this.#setOwnedConfigApiKey(providerName, apiKey, !providerConfig.apiKey);
|
|
5159
6264
|
}
|
|
5160
6265
|
for (const modelDef of modelDefs) {
|
|
5161
6266
|
const providerCompat = providerConfig.disableStrictTools
|
|
@@ -5166,11 +6271,7 @@ export class ModelRegistry {
|
|
|
5166
6271
|
providerConfig.baseUrl!,
|
|
5167
6272
|
providerConfig.api as Api | undefined,
|
|
5168
6273
|
providerConfig.headers,
|
|
5169
|
-
|
|
5170
|
-
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
5171
|
-
: providerConfig.apiKeyEnv
|
|
5172
|
-
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
5173
|
-
: undefined,
|
|
6274
|
+
apiKey,
|
|
5174
6275
|
providerConfig.authHeader,
|
|
5175
6276
|
providerCompat,
|
|
5176
6277
|
providerConfig.requestTransform,
|
|
@@ -5178,6 +6279,7 @@ export class ModelRegistry {
|
|
|
5178
6279
|
providerConfig.cacheRetention,
|
|
5179
6280
|
providerConfig.transport,
|
|
5180
6281
|
modelDef as CustomModelDefinitionLike,
|
|
6282
|
+
providerConfig.streamIdleTimeoutMs,
|
|
5181
6283
|
);
|
|
5182
6284
|
if (!model) continue;
|
|
5183
6285
|
models.push(model);
|
|
@@ -5199,6 +6301,67 @@ export class ModelRegistry {
|
|
|
5199
6301
|
return [...this.#configuredProviderIds];
|
|
5200
6302
|
}
|
|
5201
6303
|
|
|
6304
|
+
/** Configured API-key sources in models.yml precedence order, excluding absent environment values. */
|
|
6305
|
+
getProviderConfigApiKeySources(
|
|
6306
|
+
provider: string,
|
|
6307
|
+
): Array<{ kind: "environment"; name: string; provider: string } | { kind: "models-yml"; provider: string }> {
|
|
6308
|
+
const normalizedProvider = resolveOAuthStorageProvider(provider);
|
|
6309
|
+
const configuredProviders = this.#acceptedConfigValue?.providers;
|
|
6310
|
+
const configuredAliases = Object.entries(configuredProviders ?? {}).filter(
|
|
6311
|
+
([configuredProvider]) => resolveOAuthStorageProvider(configuredProvider) === normalizedProvider,
|
|
6312
|
+
);
|
|
6313
|
+
const sources: Array<
|
|
6314
|
+
{ kind: "environment"; name: string; provider: string } | { kind: "models-yml"; provider: string }
|
|
6315
|
+
> = [];
|
|
6316
|
+
const addSource = (
|
|
6317
|
+
configuredProvider: string,
|
|
6318
|
+
apiKey: string | undefined,
|
|
6319
|
+
apiKeyEnv: string | undefined,
|
|
6320
|
+
): void => {
|
|
6321
|
+
if (apiKey) {
|
|
6322
|
+
if (Bun.env[apiKey]) sources.push({ kind: "environment", name: apiKey, provider: configuredProvider });
|
|
6323
|
+
else sources.push({ kind: "models-yml", provider: configuredProvider });
|
|
6324
|
+
return;
|
|
6325
|
+
}
|
|
6326
|
+
if (apiKeyEnv && $rotatingCredentialEnv(apiKeyEnv)) {
|
|
6327
|
+
sources.push({ kind: "environment", name: apiKeyEnv, provider: configuredProvider });
|
|
6328
|
+
}
|
|
6329
|
+
};
|
|
6330
|
+
for (const [configuredProvider, configured] of configuredAliases) {
|
|
6331
|
+
addSource(configuredProvider, configured.apiKey, configured.apiKeyEnv);
|
|
6332
|
+
addSource(configuredProvider, configured.openaiCompat?.apiKey, configured.openaiCompat?.apiKeyEnv);
|
|
6333
|
+
}
|
|
6334
|
+
return sources.filter(
|
|
6335
|
+
(source, index) =>
|
|
6336
|
+
source.kind === "models-yml" ||
|
|
6337
|
+
sources.findIndex(
|
|
6338
|
+
candidate =>
|
|
6339
|
+
candidate.kind === "environment" &&
|
|
6340
|
+
candidate.name === source.name &&
|
|
6341
|
+
candidate.provider === source.provider,
|
|
6342
|
+
) === index,
|
|
6343
|
+
);
|
|
6344
|
+
}
|
|
6345
|
+
|
|
6346
|
+
/** Environment variable name for a provider API key supplied by runtime setup. */
|
|
6347
|
+
getProviderRuntimeApiKeyEnvName(provider: string): string | undefined {
|
|
6348
|
+
const normalizedProvider = resolveOAuthStorageProvider(provider);
|
|
6349
|
+
return (
|
|
6350
|
+
this.#runtimeProviderApiKeyEnvNames.get(provider) ??
|
|
6351
|
+
[...this.#runtimeProviderApiKeyEnvNames].find(
|
|
6352
|
+
([registeredProvider]) => resolveOAuthStorageProvider(registeredProvider) === normalizedProvider,
|
|
6353
|
+
)?.[1]
|
|
6354
|
+
);
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
/** Whether a runtime extension's provider API key is currently installed in AuthStorage. */
|
|
6358
|
+
hasRuntimeProviderApiKey(provider: string): boolean {
|
|
6359
|
+
const normalizedProvider = resolveOAuthStorageProvider(provider);
|
|
6360
|
+
return [...this.#runtimeProviderCredentialInstalled].some(
|
|
6361
|
+
registeredProvider => resolveOAuthStorageProvider(registeredProvider) === normalizedProvider,
|
|
6362
|
+
);
|
|
6363
|
+
}
|
|
6364
|
+
|
|
5202
6365
|
/**
|
|
5203
6366
|
* Whether this registry can serve a provider id from any active source.
|
|
5204
6367
|
*
|
|
@@ -5986,6 +7149,19 @@ export class ModelRegistry {
|
|
|
5986
7149
|
return apiKey;
|
|
5987
7150
|
}
|
|
5988
7151
|
|
|
7152
|
+
/** Check credential availability without resolving keys or refreshing OAuth tokens. */
|
|
7153
|
+
hasUsableAuthForProvider(provider: string, sessionId?: string): boolean {
|
|
7154
|
+
return this.authStorage.hasUsableAuth(provider, {
|
|
7155
|
+
owner: this.#authStorageConfigOwner,
|
|
7156
|
+
sessionId,
|
|
7157
|
+
});
|
|
7158
|
+
}
|
|
7159
|
+
|
|
7160
|
+
/** @internal Whether this registry is an unpublished staged reload candidate. */
|
|
7161
|
+
isStagedReloadCandidate(): boolean {
|
|
7162
|
+
return this.#stagedReloadCandidate;
|
|
7163
|
+
}
|
|
7164
|
+
|
|
5989
7165
|
#refreshRotatingConfigApiKey(provider: string, forceStatic = false): void {
|
|
5990
7166
|
const runtimeOwned = !forceStatic && this.#runtimeProviderApiKeys.has(provider);
|
|
5991
7167
|
const envName = runtimeOwned
|
|
@@ -5993,6 +7169,7 @@ export class ModelRegistry {
|
|
|
5993
7169
|
: this.#customProviderApiKeyEnvNames.get(provider);
|
|
5994
7170
|
if (!envName) return;
|
|
5995
7171
|
const resolved = $rotatingCredentialEnv(envName);
|
|
7172
|
+
if (resolved !== undefined && this.authStorage.hasAnyCredentialPin(provider)) return;
|
|
5996
7173
|
const previous = runtimeOwned
|
|
5997
7174
|
? this.#runtimeProviderResolvedApiKeys.get(provider)
|
|
5998
7175
|
: this.#customProviderApiKeys.get(provider);
|
|
@@ -6006,7 +7183,7 @@ export class ModelRegistry {
|
|
|
6006
7183
|
return;
|
|
6007
7184
|
}
|
|
6008
7185
|
this.#customProviderApiKeys.delete(provider);
|
|
6009
|
-
this
|
|
7186
|
+
this.#removeOwnedConfigApiKey(provider);
|
|
6010
7187
|
} else {
|
|
6011
7188
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
6012
7189
|
if (runtimeOwned) {
|
|
@@ -6014,12 +7191,9 @@ export class ModelRegistry {
|
|
|
6014
7191
|
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
6015
7192
|
}
|
|
6016
7193
|
if (runtimeOwned) {
|
|
6017
|
-
this
|
|
7194
|
+
this.#setOwnedConfigApiKey(provider, resolved, false, "runtime");
|
|
6018
7195
|
} else {
|
|
6019
|
-
this
|
|
6020
|
-
envSourced: true,
|
|
6021
|
-
owner: this.#authStorageConfigOwner,
|
|
6022
|
-
});
|
|
7196
|
+
this.#setOwnedConfigApiKey(provider, resolved, true);
|
|
6023
7197
|
}
|
|
6024
7198
|
}
|
|
6025
7199
|
const authHeader =
|
|
@@ -6166,7 +7340,7 @@ export class ModelRegistry {
|
|
|
6166
7340
|
this.#runtimeProviderOverrides.delete(providerName);
|
|
6167
7341
|
this.#runtimeProviderAuthHeaders.delete(providerName);
|
|
6168
7342
|
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
|
|
6169
|
-
this
|
|
7343
|
+
this.#removeOwnedConfigApiKey(providerName);
|
|
6170
7344
|
this.#clearDescriptorDiscoveryEvidence(providerName);
|
|
6171
7345
|
}
|
|
6172
7346
|
|
|
@@ -6174,6 +7348,7 @@ export class ModelRegistry {
|
|
|
6174
7348
|
* Remove custom API/OAuth registrations for a specific extension source.
|
|
6175
7349
|
*/
|
|
6176
7350
|
clearSourceRegistrations(sourceId: string): void {
|
|
7351
|
+
this.#catalogRefreshGeneration++;
|
|
6177
7352
|
unregisterCustomApis(sourceId);
|
|
6178
7353
|
unregisterOAuthProviders(sourceId);
|
|
6179
7354
|
const sourceProviders = this.#runtimeProvidersBySource.get(sourceId);
|
|
@@ -6217,6 +7392,7 @@ export class ModelRegistry {
|
|
|
6217
7392
|
* If provider has oauth: registers OAuth provider for /login support.
|
|
6218
7393
|
*/
|
|
6219
7394
|
registerProvider(providerName: string, config: ProviderConfigInput, sourceId?: string): void {
|
|
7395
|
+
this.#catalogRefreshGeneration++;
|
|
6220
7396
|
if (config.streamSimple && !config.api) {
|
|
6221
7397
|
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
|
6222
7398
|
}
|
|
@@ -6304,7 +7480,7 @@ export class ModelRegistry {
|
|
|
6304
7480
|
this.#runtimeProviderResolvedApiKeys.set(providerName, resolved);
|
|
6305
7481
|
this.#runtimeProviderCredentialInstalled.add(providerName);
|
|
6306
7482
|
if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
|
|
6307
|
-
this
|
|
7483
|
+
this.#setOwnedConfigApiKey(providerName, resolved, false, "runtime");
|
|
6308
7484
|
}
|
|
6309
7485
|
if (config.oauth && !config.apiKey && this.#runtimeProviderApiKeys.has(providerName)) {
|
|
6310
7486
|
const previousApiKey = this.#runtimeProviderResolvedApiKeys.get(providerName);
|
|
@@ -6314,7 +7490,7 @@ export class ModelRegistry {
|
|
|
6314
7490
|
this.#runtimeProviderApiKeyEnvNames.delete(providerName);
|
|
6315
7491
|
this.#runtimeProviderAuthHeaders.delete(providerName);
|
|
6316
7492
|
this.#customProviderApiKeys.delete(providerName);
|
|
6317
|
-
this
|
|
7493
|
+
this.#removeOwnedConfigApiKey(providerName);
|
|
6318
7494
|
this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
|
|
6319
7495
|
if (overlay.provider !== providerName) return overlay;
|
|
6320
7496
|
const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
|