@gajae-code/coding-agent 0.15.4 → 0.15.6
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 +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
5
|
import {
|
|
5
6
|
type Api,
|
|
6
7
|
type AssistantMessageEventStream,
|
|
@@ -55,8 +56,8 @@ function normalizeVllmApiKey(provider: string, apiKey: string | undefined): stri
|
|
|
55
56
|
|
|
56
57
|
import { registerOAuthProvider, unregisterOAuthProviders } from "@gajae-code/ai/utils/oauth";
|
|
57
58
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@gajae-code/ai/utils/oauth/types";
|
|
58
|
-
import { $pickCredentialEnv, $rotatingCredentialEnv, isRecord, logger } from "@gajae-code/utils";
|
|
59
|
-
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
59
|
+
import { $pickCredentialEnv, $rotatingCredentialEnv, getAgentDir, isRecord, logger } from "@gajae-code/utils";
|
|
60
|
+
import { parseModelString, resolveProviderModelReference, splitSelectorThinkingSuffix } from "../config/model-resolver";
|
|
60
61
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
61
62
|
import {
|
|
62
63
|
type ActiveProviderDescriptor,
|
|
@@ -65,10 +66,15 @@ import {
|
|
|
65
66
|
} from "../sdk/providers";
|
|
66
67
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
67
68
|
import type { ActiveSearchModelContext, WebSearchMode } from "../web/search/types";
|
|
68
|
-
import {
|
|
69
|
+
import { ConfigError, ConfigFile } from "./config-file";
|
|
69
70
|
import { isAuthenticated, kNoAuth } from "./model-auth";
|
|
70
71
|
import { type ConfiguredModelBindings, ModelBindingsApplier } from "./model-bindings-applier";
|
|
71
72
|
import { ModelDiscoveryManager, type ProviderDiscoveryState } from "./model-discovery-manager";
|
|
73
|
+
import {
|
|
74
|
+
loadAcceptedModelPresetProfiles,
|
|
75
|
+
type ModelPresetRegistryDependencies,
|
|
76
|
+
refreshModelPresetRegistryInBackground,
|
|
77
|
+
} from "./model-preset-registry";
|
|
72
78
|
|
|
73
79
|
export type { ProviderDiscoveryState, ProviderDiscoveryStatus } from "./model-discovery-manager";
|
|
74
80
|
|
|
@@ -86,6 +92,7 @@ import {
|
|
|
86
92
|
type ModelProfileDefinition,
|
|
87
93
|
mergeModelProfiles,
|
|
88
94
|
} from "./model-profiles";
|
|
95
|
+
import { normalizeModelSelectorValue } from "./model-selector-value";
|
|
89
96
|
import {
|
|
90
97
|
GJC_MODEL_ASSIGNMENT_TARGET_IDS,
|
|
91
98
|
type ModelOverride,
|
|
@@ -111,6 +118,7 @@ export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant,
|
|
|
111
118
|
export { isAuthenticated, kNoAuth };
|
|
112
119
|
|
|
113
120
|
const MAX_SESSION_CANONICAL_VARIANTS = 64;
|
|
121
|
+
const GENERATED_AUTH_HEADER = Symbol("generated-auth-header");
|
|
114
122
|
function firstPositiveDiscoveryNumber(...values: readonly unknown[]): number | undefined {
|
|
115
123
|
for (const value of values) {
|
|
116
124
|
const number =
|
|
@@ -134,15 +142,18 @@ function stripUrlQuery(value: string): string {
|
|
|
134
142
|
return value.slice(0, queryStart) + (fragmentStart < 0 ? "" : value.slice(fragmentStart));
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
function envAvailabilityFingerprint(): string {
|
|
138
|
-
|
|
139
|
-
.filter(
|
|
140
|
-
|
|
145
|
+
function envAvailabilityFingerprint(configuredNames?: ReadonlySet<string>): string {
|
|
146
|
+
const names = new Set(
|
|
147
|
+
Object.keys(process.env).filter(
|
|
148
|
+
name =>
|
|
141
149
|
/(?:_API_KEY|_OAUTH_TOKEN|_ACCESS_TOKEN)$/.test(name) ||
|
|
142
150
|
/^(?:GH_TOKEN|GITHUB_TOKEN|HF_TOKEN|COPILOT_GITHUB_TOKEN)$/.test(name),
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
for (const name of configuredNames ?? []) names.add(name);
|
|
154
|
+
return [...names]
|
|
155
|
+
.sort((left, right) => left.localeCompare(right))
|
|
156
|
+
.map(name => `${name}=${process.env[name] ?? ""}`)
|
|
146
157
|
.join("\u0000");
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -576,6 +587,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
|
|
|
576
587
|
|
|
577
588
|
/** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
|
|
578
589
|
interface ProviderOverride {
|
|
590
|
+
api?: Api;
|
|
579
591
|
baseUrl?: string;
|
|
580
592
|
headers?: Record<string, string>;
|
|
581
593
|
apiKey?: string;
|
|
@@ -584,6 +596,82 @@ interface ProviderOverride {
|
|
|
584
596
|
transport?: Model<Api>["transport"];
|
|
585
597
|
requestTransform?: ModelRequestTransform;
|
|
586
598
|
cacheRetention?: CacheRetention;
|
|
599
|
+
isOAuth?: boolean;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function hasMatchingRegistryTransport(left: Model<Api>, right: Model<Api> | ProviderOverride): boolean {
|
|
603
|
+
return (
|
|
604
|
+
left.baseUrl === right.baseUrl &&
|
|
605
|
+
isDeepStrictEqual(left.headers, right.headers) &&
|
|
606
|
+
left.transport === right.transport &&
|
|
607
|
+
isDeepStrictEqual(left.requestTransform, right.requestTransform) &&
|
|
608
|
+
left.cacheRetention === right.cacheRetention &&
|
|
609
|
+
left.isOAuth === right.isOAuth
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function commonRegistryTransportCompat(models: readonly Model<Api>[]): Model<Api>["compat"] | undefined {
|
|
614
|
+
if (models.length === 0) return undefined;
|
|
615
|
+
const records = models.map(model => model.compat as Record<string, unknown> | undefined);
|
|
616
|
+
const keys = new Set(records.flatMap(record => (record ? Object.keys(record) : [])));
|
|
617
|
+
const common: Record<string, unknown> = {};
|
|
618
|
+
for (const key of keys) {
|
|
619
|
+
const value = records[0]?.[key];
|
|
620
|
+
if (records.every(record => record !== undefined && isDeepStrictEqual(record[key], value))) common[key] = value;
|
|
621
|
+
}
|
|
622
|
+
return Object.keys(common).length > 0 ? (common as Model<Api>["compat"]) : undefined;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Partial<Model<Api>> {
|
|
626
|
+
const metadata: Partial<Model<Api>> = {
|
|
627
|
+
name: model.name,
|
|
628
|
+
cost: model.cost,
|
|
629
|
+
contextWindow: model.contextWindow,
|
|
630
|
+
maxTokens: model.maxTokens,
|
|
631
|
+
};
|
|
632
|
+
if (model.longContextPricing !== undefined) metadata.longContextPricing = model.longContextPricing;
|
|
633
|
+
if (model.premiumMultiplier !== undefined) metadata.premiumMultiplier = model.premiumMultiplier;
|
|
634
|
+
if (model.priority !== undefined) metadata.priority = model.priority;
|
|
635
|
+
if (model.contextPromotionTarget !== undefined) metadata.contextPromotionTarget = model.contextPromotionTarget;
|
|
636
|
+
return metadata;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function registrySelectorResolvesToModel(selector: string, models: readonly Model<Api>[]): boolean {
|
|
640
|
+
if (models.some(model => model.id === selector || `${model.provider}/${model.id}` === selector)) return true;
|
|
641
|
+
const suffix = splitSelectorThinkingSuffix(selector);
|
|
642
|
+
const baseSelector = suffix.thinkingLevel === undefined ? selector : suffix.selector;
|
|
643
|
+
const parsed = parseModelString(baseSelector);
|
|
644
|
+
if (parsed) return models.some(model => model.provider === parsed.provider && model.id === parsed.id);
|
|
645
|
+
return models.some(model => model.id === baseSelector || model.id.endsWith(`/${baseSelector}`));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function filterMaterializedRegistryProfiles(
|
|
649
|
+
profiles: ReadonlyMap<string, ModelProfileDefinition>,
|
|
650
|
+
models: readonly Model<Api>[],
|
|
651
|
+
dynamicProviders: ReadonlySet<string>,
|
|
652
|
+
): Map<string, ModelProfileDefinition> {
|
|
653
|
+
const filtered = new Map<string, ModelProfileDefinition>();
|
|
654
|
+
for (const [name, profile] of profiles) {
|
|
655
|
+
if (
|
|
656
|
+
profile.source === "registry" &&
|
|
657
|
+
Object.values(profile.modelMapping).some(selectorValue => {
|
|
658
|
+
const selectors = normalizeModelSelectorValue(selectorValue);
|
|
659
|
+
return (
|
|
660
|
+
selectors.length > 0 &&
|
|
661
|
+
!selectors.some(selector => {
|
|
662
|
+
if (registrySelectorResolvesToModel(selector, models)) return true;
|
|
663
|
+
const suffix = splitSelectorThinkingSuffix(selector);
|
|
664
|
+
const parsed = parseModelString(suffix.thinkingLevel ? suffix.selector : selector);
|
|
665
|
+
return parsed !== undefined && dynamicProviders.has(parsed.provider);
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
})
|
|
669
|
+
) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
filtered.set(name, profile);
|
|
673
|
+
}
|
|
674
|
+
return filtered;
|
|
587
675
|
}
|
|
588
676
|
|
|
589
677
|
const PROVIDER_BASE_URL_ENV_ALIASES: Record<string, readonly string[]> = {
|
|
@@ -665,7 +753,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
665
753
|
return {
|
|
666
754
|
...model,
|
|
667
755
|
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
|
668
|
-
headers: existing.headers
|
|
756
|
+
headers: mergeCaseInsensitiveHeaders(existing.headers, model.headers),
|
|
669
757
|
requestTransform: mergeRequestTransform(
|
|
670
758
|
mergeRequestTransform(existing.requestTransform, model.requestTransform),
|
|
671
759
|
providerOverride?.requestTransform,
|
|
@@ -677,7 +765,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
677
765
|
return {
|
|
678
766
|
...model,
|
|
679
767
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
680
|
-
headers:
|
|
768
|
+
headers: mergeCaseInsensitiveHeaders(model.headers, providerOverride.headers),
|
|
681
769
|
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
682
770
|
requestTransform: mergeRequestTransform(model.requestTransform, providerOverride.requestTransform),
|
|
683
771
|
};
|
|
@@ -906,7 +994,11 @@ function resolveOAuthAccountIdForAccessToken(
|
|
|
906
994
|
authStorage: AuthStorage,
|
|
907
995
|
provider: string,
|
|
908
996
|
accessToken: string,
|
|
997
|
+
owner?: object,
|
|
909
998
|
): string | undefined {
|
|
999
|
+
if (authStorage.getEffectiveCredentialType(provider, undefined, owner ? { owner } : undefined) !== "oauth") {
|
|
1000
|
+
return undefined;
|
|
1001
|
+
}
|
|
910
1002
|
const oauthCredentials = getOAuthCredentialsForProvider(authStorage, provider);
|
|
911
1003
|
const matchingCredential = oauthCredentials.find(credential => credential.access === accessToken);
|
|
912
1004
|
if (matchingCredential) {
|
|
@@ -1020,6 +1112,15 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
|
|
1020
1112
|
}
|
|
1021
1113
|
if (override.headers) {
|
|
1022
1114
|
result.headers = { ...model.headers, ...override.headers };
|
|
1115
|
+
const explicitAuthKey = Object.keys(override.headers).find(key => key.toLowerCase() === "authorization");
|
|
1116
|
+
if (explicitAuthKey !== undefined) {
|
|
1117
|
+
const explicitAuthValue = override.headers[explicitAuthKey];
|
|
1118
|
+
deleteHeaderCaseInsensitive(result.headers, "Authorization");
|
|
1119
|
+
result.headers[explicitAuthKey] = explicitAuthValue;
|
|
1120
|
+
delete (result.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: boolean })[
|
|
1121
|
+
GENERATED_AUTH_HEADER
|
|
1122
|
+
];
|
|
1123
|
+
}
|
|
1023
1124
|
}
|
|
1024
1125
|
result.compat = mergeCompat(model.compat, override.compat);
|
|
1025
1126
|
return enrichModelThinking(result);
|
|
@@ -1100,7 +1201,36 @@ function mergeCustomModelHeaders(
|
|
|
1100
1201
|
authHeader: boolean | undefined,
|
|
1101
1202
|
apiKeyConfig: string | undefined,
|
|
1102
1203
|
): Record<string, string> | undefined {
|
|
1103
|
-
|
|
1204
|
+
const merged = mergeCaseInsensitiveHeaders(providerHeaders, modelHeaders);
|
|
1205
|
+
return mergeAuthHeader(merged, authHeader, apiKeyConfig);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function mergeCaseInsensitiveHeaders(
|
|
1209
|
+
baseHeaders: Record<string, string> | undefined,
|
|
1210
|
+
overrideHeaders: Record<string, string> | undefined,
|
|
1211
|
+
): (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined {
|
|
1212
|
+
const merged: Record<string, string> = {};
|
|
1213
|
+
for (const [key, value] of Object.entries(baseHeaders ?? {})) {
|
|
1214
|
+
if (key.toLowerCase() === "authorization") deleteHeaderCaseInsensitive(merged, "Authorization");
|
|
1215
|
+
merged[key] = value;
|
|
1216
|
+
}
|
|
1217
|
+
for (const [key, value] of Object.entries(overrideHeaders ?? {})) {
|
|
1218
|
+
if (key.toLowerCase() === "authorization") deleteHeaderCaseInsensitive(merged, "Authorization");
|
|
1219
|
+
merged[key] = value;
|
|
1220
|
+
}
|
|
1221
|
+
const baseGenerated = (baseHeaders as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined)?.[
|
|
1222
|
+
GENERATED_AUTH_HEADER
|
|
1223
|
+
];
|
|
1224
|
+
const overrideGenerated = (
|
|
1225
|
+
overrideHeaders as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined
|
|
1226
|
+
)?.[GENERATED_AUTH_HEADER];
|
|
1227
|
+
const survivingGenerated =
|
|
1228
|
+
headerValue(overrideHeaders, "Authorization") !== undefined ? overrideGenerated : baseGenerated;
|
|
1229
|
+
if (survivingGenerated !== undefined && headerValue(merged, "Authorization") === survivingGenerated) {
|
|
1230
|
+
(merged as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[GENERATED_AUTH_HEADER] =
|
|
1231
|
+
survivingGenerated;
|
|
1232
|
+
}
|
|
1233
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
1104
1234
|
}
|
|
1105
1235
|
|
|
1106
1236
|
function mergeAuthHeader(
|
|
@@ -1109,11 +1239,41 @@ function mergeAuthHeader(
|
|
|
1109
1239
|
apiKeyConfig: string | undefined,
|
|
1110
1240
|
): Record<string, string> | undefined {
|
|
1111
1241
|
const nextHeaders = headers && Object.keys(headers).length > 0 ? { ...headers } : undefined;
|
|
1112
|
-
if (!authHeader || !apiKeyConfig) {
|
|
1242
|
+
if (!authHeader || !apiKeyConfig || headerValue(nextHeaders, "Authorization") !== undefined) {
|
|
1113
1243
|
return nextHeaders;
|
|
1114
1244
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1245
|
+
return apiKeyConfig
|
|
1246
|
+
? ({
|
|
1247
|
+
...nextHeaders,
|
|
1248
|
+
Authorization: `Bearer ${apiKeyConfig}`,
|
|
1249
|
+
[GENERATED_AUTH_HEADER]: `Bearer ${apiKeyConfig}`,
|
|
1250
|
+
} as Record<string, string>)
|
|
1251
|
+
: nextHeaders;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function headerValue(headers: Record<string, string> | undefined, name: string): string | undefined {
|
|
1255
|
+
const lowerName = name.toLowerCase();
|
|
1256
|
+
const key = Object.keys(headers ?? {}).find(candidate => candidate.toLowerCase() === lowerName);
|
|
1257
|
+
return key === undefined ? undefined : headers?.[key];
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function deleteHeaderCaseInsensitive(headers: Record<string, string>, name: string): void {
|
|
1261
|
+
const lowerName = name.toLowerCase();
|
|
1262
|
+
for (const key of Object.keys(headers)) {
|
|
1263
|
+
if (key.toLowerCase() === lowerName) delete headers[key];
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function ownsOnlyGeneratedAuthorization(headers: Record<string, string>, generated: string | undefined): boolean {
|
|
1268
|
+
if (generated === undefined) return false;
|
|
1269
|
+
const entries = Object.entries(headers).filter(([key]) => key.toLowerCase() === "authorization");
|
|
1270
|
+
return entries.length === 1 && entries[0]![1] === generated;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function deleteAuthorizationValue(headers: Record<string, string>, value: string): void {
|
|
1274
|
+
for (const key of Object.keys(headers)) {
|
|
1275
|
+
if (key.toLowerCase() === "authorization" && headers[key] === value) delete headers[key];
|
|
1276
|
+
}
|
|
1117
1277
|
}
|
|
1118
1278
|
|
|
1119
1279
|
/**
|
|
@@ -1366,6 +1526,8 @@ interface ConfiguredDiscoveryResult {
|
|
|
1366
1526
|
clearPublishedModelIds?: readonly string[];
|
|
1367
1527
|
}
|
|
1368
1528
|
|
|
1529
|
+
type ProviderRefreshFence = { providerId: string; generation: number } | { generations: ReadonlyMap<string, number> };
|
|
1530
|
+
|
|
1369
1531
|
/**
|
|
1370
1532
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
1371
1533
|
*/
|
|
@@ -1384,6 +1546,7 @@ export class ModelRegistry {
|
|
|
1384
1546
|
#sessionCanonicalVariants = new Map<string, string>();
|
|
1385
1547
|
#customProviderApiKeys: Map<string, string> = new Map();
|
|
1386
1548
|
#customProviderApiKeyEnvNames: Map<string, string> = new Map();
|
|
1549
|
+
#customProviderAuthHeaders: Map<string, boolean> = new Map();
|
|
1387
1550
|
#providerWebSearchModes: Map<string, WebSearchMode> = new Map();
|
|
1388
1551
|
#keylessProviders: Set<string> = new Set();
|
|
1389
1552
|
#optionalAuthProviders: Set<string> = new Set();
|
|
@@ -1420,21 +1583,42 @@ export class ModelRegistry {
|
|
|
1420
1583
|
#configError: ConfigError | undefined = undefined;
|
|
1421
1584
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
1422
1585
|
#settings: Pick<Settings, "get" | "getGlobal">;
|
|
1586
|
+
readonly #authStorageConfigOwner: object = {};
|
|
1587
|
+
#disposeAuthStorageFallbackResolver: (() => void) | undefined;
|
|
1423
1588
|
#lastStaticLoadMtime: number | null = null;
|
|
1424
1589
|
#lastStaticLoadEnvironmentFingerprint: string | undefined;
|
|
1590
|
+
#lastModelPresetRegistryFingerprint: string | undefined;
|
|
1591
|
+
#loadedModelPresetRegistryManifestSha256: string | undefined;
|
|
1592
|
+
#modelPresetRegistryAgentDir: string;
|
|
1593
|
+
#modelPresetRegistryDependencies: Omit<ModelPresetRegistryDependencies, "agentDir">;
|
|
1594
|
+
#cancelModelPresetRegistryRefresh: (() => Promise<void>) | undefined;
|
|
1595
|
+
#disposePromise: Promise<void> | undefined;
|
|
1596
|
+
#unsubscribeAuthGeneration: (() => void) | undefined;
|
|
1425
1597
|
#staticModelsLoaded = false;
|
|
1426
1598
|
#lastDisabledProviderKey: string | undefined;
|
|
1427
1599
|
#registeredProviderSources: Set<string> = new Set();
|
|
1428
1600
|
#cacheDbPath?: string;
|
|
1429
1601
|
#suppressedSelectors: Map<string, number> = new Map();
|
|
1430
1602
|
#backgroundRefresh?: Promise<void>;
|
|
1603
|
+
#catalogMutationTail: Promise<void> = Promise.resolve();
|
|
1604
|
+
#pendingCatalogMutations = 0;
|
|
1605
|
+
#catalogRefreshGeneration = 0;
|
|
1606
|
+
#providerRefreshGenerations = new Map<string, number>();
|
|
1607
|
+
#disposed = false;
|
|
1431
1608
|
// Runtime extension model overlays — persist across refresh() cycles so that
|
|
1432
1609
|
// models registered by extensions survive the model selector's offline reload.
|
|
1433
1610
|
#runtimeModelOverlays: CustomModelOverlay[] = [];
|
|
1434
1611
|
#runtimeProviderApiKeys: Map<string, string> = new Map();
|
|
1612
|
+
#runtimeProviderResolvedApiKeys: Map<string, string> = new Map();
|
|
1613
|
+
#runtimeProviderCredentialInstalled: Set<string> = new Set();
|
|
1614
|
+
#runtimeProviderApiKeyEnvNames: Map<string, string> = new Map();
|
|
1435
1615
|
#runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
|
|
1616
|
+
#runtimeProviderAuthHeaders: Map<string, boolean> = new Map();
|
|
1617
|
+
#generatedAuthHeaderProviders: Set<string> = new Set();
|
|
1618
|
+
#generatedAuthHeaders: WeakMap<Model<Api>, { authorization?: string; apiKey?: string }> = new WeakMap();
|
|
1436
1619
|
#runtimeProvidersBySource: Map<string, Set<string>> = new Map();
|
|
1437
1620
|
#runtimeProviderSourceByName: Map<string, string> = new Map();
|
|
1621
|
+
#registryModelKeys: Set<string> = new Set();
|
|
1438
1622
|
#rebuildPending: boolean = false;
|
|
1439
1623
|
#rebuildSuspended: number = 0;
|
|
1440
1624
|
#configuredApiKeyEnvNames: Set<string> = new Set();
|
|
@@ -1448,18 +1632,77 @@ export class ModelRegistry {
|
|
|
1448
1632
|
readonly authStorage: AuthStorage,
|
|
1449
1633
|
modelsPath?: string,
|
|
1450
1634
|
registrySettings?: Pick<Settings, "get" | "getGlobal">,
|
|
1635
|
+
modelPresetRegistryDependencies: ModelPresetRegistryDependencies = {},
|
|
1451
1636
|
) {
|
|
1452
1637
|
this.#settings = registrySettings ?? settings;
|
|
1638
|
+
const configuredAgentDir = path.resolve(modelPresetRegistryDependencies.agentDir ?? getAgentDir());
|
|
1453
1639
|
this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
|
|
1640
|
+
this.#modelPresetRegistryAgentDir = modelPresetRegistryDependencies.agentDir
|
|
1641
|
+
? configuredAgentDir
|
|
1642
|
+
: modelsPath && path.isAbsolute(modelsPath)
|
|
1643
|
+
? path.dirname(modelsPath)
|
|
1644
|
+
: configuredAgentDir;
|
|
1645
|
+
const { agentDir: _agentDir, ...registryDependencies } = modelPresetRegistryDependencies;
|
|
1646
|
+
this.#modelPresetRegistryDependencies = registryDependencies;
|
|
1454
1647
|
this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
|
|
1455
1648
|
// Set up fallback resolver for custom provider API keys
|
|
1456
|
-
this.authStorage.setFallbackResolver(provider => {
|
|
1649
|
+
this.#disposeAuthStorageFallbackResolver = this.authStorage.setFallbackResolver(provider => {
|
|
1457
1650
|
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
1458
1651
|
return keyConfig;
|
|
1459
|
-
});
|
|
1460
|
-
this.authStorage.onGenerationChanged(() => this.#invalidateAvailableModels());
|
|
1652
|
+
}, this.#authStorageConfigOwner);
|
|
1653
|
+
this.#unsubscribeAuthGeneration = this.authStorage.onGenerationChanged(() => this.#invalidateAvailableModels());
|
|
1461
1654
|
// Load models synchronously in constructor
|
|
1462
1655
|
this.#loadModels();
|
|
1656
|
+
this.#cancelModelPresetRegistryRefresh = refreshModelPresetRegistryInBackground(
|
|
1657
|
+
{
|
|
1658
|
+
...this.#modelPresetRegistryDependencies,
|
|
1659
|
+
agentDir: this.#modelPresetRegistryAgentDir,
|
|
1660
|
+
knownManifestSha256: this.#loadedModelPresetRegistryManifestSha256,
|
|
1661
|
+
},
|
|
1662
|
+
() => {
|
|
1663
|
+
void this.#enqueueCatalogMutation(() => {
|
|
1664
|
+
if (this.#disposed) return;
|
|
1665
|
+
this.#reloadStaticModels();
|
|
1666
|
+
this.#modelBindingsApplier.apply();
|
|
1667
|
+
this.#notifyCatalogChanged();
|
|
1668
|
+
}).catch(() => undefined);
|
|
1669
|
+
},
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
dispose(): Promise<void> {
|
|
1674
|
+
if (this.#disposePromise) return this.#disposePromise;
|
|
1675
|
+
this.#disposed = true;
|
|
1676
|
+
this.#catalogRefreshGeneration++;
|
|
1677
|
+
const awaitRefreshDisposal = this.#cancelModelPresetRegistryRefresh?.() ?? Promise.resolve();
|
|
1678
|
+
this.#cancelModelPresetRegistryRefresh = undefined;
|
|
1679
|
+
this.#unsubscribeAuthGeneration?.();
|
|
1680
|
+
this.#unsubscribeAuthGeneration = undefined;
|
|
1681
|
+
this.authStorage.clearConfigApiKeys(this.#authStorageConfigOwner);
|
|
1682
|
+
this.#disposeAuthStorageFallbackResolver?.();
|
|
1683
|
+
this.#disposeAuthStorageFallbackResolver = undefined;
|
|
1684
|
+
this.#catalogChangeListeners.clear();
|
|
1685
|
+
this.#disposePromise = awaitRefreshDisposal;
|
|
1686
|
+
return this.#disposePromise;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
#enqueueCatalogMutation(operation: () => void | Promise<void>): Promise<void> {
|
|
1690
|
+
this.#pendingCatalogMutations++;
|
|
1691
|
+
let run: Promise<void>;
|
|
1692
|
+
if (this.#pendingCatalogMutations === 1) {
|
|
1693
|
+
try {
|
|
1694
|
+
run = Promise.resolve(operation());
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
run = Promise.reject(error);
|
|
1697
|
+
}
|
|
1698
|
+
} else {
|
|
1699
|
+
run = this.#catalogMutationTail.then(operation);
|
|
1700
|
+
}
|
|
1701
|
+
const completion = run.finally(() => {
|
|
1702
|
+
this.#pendingCatalogMutations--;
|
|
1703
|
+
});
|
|
1704
|
+
this.#catalogMutationTail = completion.catch(() => undefined);
|
|
1705
|
+
return completion;
|
|
1463
1706
|
}
|
|
1464
1707
|
|
|
1465
1708
|
onCatalogChanged(listener: () => void): () => void {
|
|
@@ -1471,22 +1714,34 @@ export class ModelRegistry {
|
|
|
1471
1714
|
|
|
1472
1715
|
/** Replace the read-only settings snapshot used by profile-scoped resolution. */
|
|
1473
1716
|
setScopedSettings(settingsReader: Pick<Settings, "get" | "getGlobal">): void {
|
|
1717
|
+
this.#catalogRefreshGeneration++;
|
|
1474
1718
|
this.#settings = settingsReader;
|
|
1719
|
+
this.#staticModelsLoaded = false;
|
|
1720
|
+
this.#reloadStaticModels();
|
|
1721
|
+
this.#rebuildCanonicalIndex();
|
|
1475
1722
|
}
|
|
1476
1723
|
|
|
1477
1724
|
/**
|
|
1478
|
-
* Reload models from disk (
|
|
1725
|
+
* Reload models from disk (embedded + accepted registry + custom from models.yml).
|
|
1479
1726
|
*/
|
|
1480
1727
|
async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
|
|
1481
|
-
this.#
|
|
1482
|
-
|
|
1483
|
-
this.#
|
|
1484
|
-
this.#
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
this.#
|
|
1489
|
-
|
|
1728
|
+
if (this.#disposed) return;
|
|
1729
|
+
await this.#enqueueCatalogMutation(async () => {
|
|
1730
|
+
if (this.#disposed) return;
|
|
1731
|
+
const refreshGeneration = ++this.#catalogRefreshGeneration;
|
|
1732
|
+
const providerRefreshFence: ProviderRefreshFence = {
|
|
1733
|
+
generations: new Map(this.#providerRefreshGenerations),
|
|
1734
|
+
};
|
|
1735
|
+
this.#suspendRebuild();
|
|
1736
|
+
try {
|
|
1737
|
+
this.#reloadStaticModels();
|
|
1738
|
+
this.#suppressedSelectors.clear();
|
|
1739
|
+
await this.#refreshRuntimeDiscoveries(strategy, undefined, refreshGeneration, providerRefreshFence);
|
|
1740
|
+
if (refreshGeneration === this.#catalogRefreshGeneration) this.#modelBindingsApplier.apply();
|
|
1741
|
+
} finally {
|
|
1742
|
+
this.#resumeRebuild();
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1490
1745
|
}
|
|
1491
1746
|
|
|
1492
1747
|
refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void {
|
|
@@ -1508,19 +1763,34 @@ export class ModelRegistry {
|
|
|
1508
1763
|
}
|
|
1509
1764
|
|
|
1510
1765
|
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
|
1511
|
-
this.#
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1766
|
+
if (this.#disposed) return;
|
|
1767
|
+
const providerRefreshGeneration = (this.#providerRefreshGenerations.get(providerId) ?? 0) + 1;
|
|
1768
|
+
this.#providerRefreshGenerations.set(providerId, providerRefreshGeneration);
|
|
1769
|
+
await this.#enqueueCatalogMutation(async () => {
|
|
1770
|
+
if (this.#disposed) return;
|
|
1771
|
+
const refreshGeneration = this.#catalogRefreshGeneration;
|
|
1772
|
+
this.#suspendRebuild();
|
|
1773
|
+
try {
|
|
1774
|
+
this.#reloadStaticModels();
|
|
1775
|
+
for (const selector of this.#suppressedSelectors.keys()) {
|
|
1776
|
+
if (selector.startsWith(`${providerId}/`)) {
|
|
1777
|
+
this.#suppressedSelectors.delete(selector);
|
|
1778
|
+
}
|
|
1517
1779
|
}
|
|
1780
|
+
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]), refreshGeneration, {
|
|
1781
|
+
providerId,
|
|
1782
|
+
generation: providerRefreshGeneration,
|
|
1783
|
+
});
|
|
1784
|
+
if (
|
|
1785
|
+
refreshGeneration === this.#catalogRefreshGeneration &&
|
|
1786
|
+
this.#providerRefreshGenerations.get(providerId) === providerRefreshGeneration
|
|
1787
|
+
) {
|
|
1788
|
+
this.#modelBindingsApplier.apply();
|
|
1789
|
+
}
|
|
1790
|
+
} finally {
|
|
1791
|
+
this.#resumeRebuild();
|
|
1518
1792
|
}
|
|
1519
|
-
|
|
1520
|
-
this.#modelBindingsApplier.apply();
|
|
1521
|
-
} finally {
|
|
1522
|
-
this.#resumeRebuild();
|
|
1523
|
-
}
|
|
1793
|
+
});
|
|
1524
1794
|
}
|
|
1525
1795
|
|
|
1526
1796
|
admitCachedProviderForStoredLiteralCredential(providerId: string, selector: AuthCredentialSelector): boolean {
|
|
@@ -1564,7 +1834,11 @@ export class ModelRegistry {
|
|
|
1564
1834
|
this.#discoveryManager.providerIds().has(providerId) ||
|
|
1565
1835
|
PROVIDER_DESCRIPTORS.some(descriptor => descriptor.providerId === providerId);
|
|
1566
1836
|
if (!supportsDiscovery) return undefined;
|
|
1567
|
-
const authEvidence = this.authStorage.getStoredLiteralApiKeyEvidenceGeneration(
|
|
1837
|
+
const authEvidence = this.authStorage.getStoredLiteralApiKeyEvidenceGeneration(
|
|
1838
|
+
providerId,
|
|
1839
|
+
selector,
|
|
1840
|
+
this.#authStorageConfigOwner,
|
|
1841
|
+
);
|
|
1568
1842
|
if (!authEvidence) return undefined;
|
|
1569
1843
|
return [
|
|
1570
1844
|
...this.#loadCachedStandardProviderModels(providerId, authEvidence),
|
|
@@ -1608,11 +1882,22 @@ export class ModelRegistry {
|
|
|
1608
1882
|
const currentMtime = this.#modelsConfigFile.getMtimeMs();
|
|
1609
1883
|
const disabledProviderKey = [...getDisabledProviderIdsFromSettings(this.#settings)].sort().join("\u0000");
|
|
1610
1884
|
const environmentFingerprint = this.#getStaticLoadEnvironmentFingerprint();
|
|
1885
|
+
const acceptedPresets = loadAcceptedModelPresetProfiles(
|
|
1886
|
+
this.#modelPresetRegistryAgentDir,
|
|
1887
|
+
this.#modelPresetRegistryDependencies,
|
|
1888
|
+
);
|
|
1889
|
+
const modelPresetRegistryFingerprint = JSON.stringify({
|
|
1890
|
+
revision: acceptedPresets.revision,
|
|
1891
|
+
manifestSha256: acceptedPresets.manifestSha256,
|
|
1892
|
+
disabled: acceptedPresets.disabled,
|
|
1893
|
+
error: acceptedPresets.error,
|
|
1894
|
+
});
|
|
1611
1895
|
if (
|
|
1612
1896
|
this.#staticModelsLoaded &&
|
|
1613
1897
|
currentMtime === this.#lastStaticLoadMtime &&
|
|
1614
1898
|
disabledProviderKey === this.#lastDisabledProviderKey &&
|
|
1615
|
-
environmentFingerprint === this.#lastStaticLoadEnvironmentFingerprint
|
|
1899
|
+
environmentFingerprint === this.#lastStaticLoadEnvironmentFingerprint &&
|
|
1900
|
+
modelPresetRegistryFingerprint === this.#lastModelPresetRegistryFingerprint
|
|
1616
1901
|
) {
|
|
1617
1902
|
// models.json and settings-derived implicit provider state are unchanged.
|
|
1618
1903
|
return;
|
|
@@ -1620,6 +1905,7 @@ export class ModelRegistry {
|
|
|
1620
1905
|
this.#modelsConfigFile.invalidate();
|
|
1621
1906
|
this.#customProviderApiKeys.clear();
|
|
1622
1907
|
this.#customProviderApiKeyEnvNames.clear();
|
|
1908
|
+
this.#customProviderAuthHeaders.clear();
|
|
1623
1909
|
this.#providerWebSearchModes.clear();
|
|
1624
1910
|
this.#keylessProviders.clear();
|
|
1625
1911
|
this.#optionalAuthProviders.clear();
|
|
@@ -1631,7 +1917,7 @@ export class ModelRegistry {
|
|
|
1631
1917
|
// Drop config-sourced apiKeys from AuthStorage before reload; entries
|
|
1632
1918
|
// removed from models.yml must actually disappear from the resolver, not
|
|
1633
1919
|
// linger from the previous parse. The post-load setters below repopulate.
|
|
1634
|
-
this.authStorage.clearConfigApiKeys();
|
|
1920
|
+
this.authStorage.clearConfigApiKeys(this.#authStorageConfigOwner);
|
|
1635
1921
|
// Runtime provider keys are reapplied after #loadModels so they retain
|
|
1636
1922
|
// registration-time precedence over colliding static provider keys.
|
|
1637
1923
|
this.#providerOverrides.clear();
|
|
@@ -1641,10 +1927,70 @@ export class ModelRegistry {
|
|
|
1641
1927
|
this.#configError = undefined;
|
|
1642
1928
|
this.#loadModels();
|
|
1643
1929
|
for (const [provider, apiKeyConfig] of this.#runtimeProviderApiKeys) {
|
|
1644
|
-
const resolved =
|
|
1930
|
+
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
1931
|
+
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
1932
|
+
: resolveApiKeyConfig(apiKeyConfig);
|
|
1933
|
+
if (!resolved) {
|
|
1934
|
+
this.#runtimeProviderCredentialInstalled.delete(provider);
|
|
1935
|
+
const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
|
|
1936
|
+
if (authHeader === true) {
|
|
1937
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
|
|
1938
|
+
if (overlay.provider !== provider) return overlay;
|
|
1939
|
+
const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
|
|
1940
|
+
[GENERATED_AUTH_HEADER]?: string;
|
|
1941
|
+
};
|
|
1942
|
+
const generated = headers[GENERATED_AUTH_HEADER];
|
|
1943
|
+
if (typeof generated === "string" && headers.Authorization === generated)
|
|
1944
|
+
delete headers.Authorization;
|
|
1945
|
+
delete headers[GENERATED_AUTH_HEADER];
|
|
1946
|
+
return { ...overlay, headers };
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
const override = this.#runtimeProviderOverrides.get(provider);
|
|
1950
|
+
if (override) this.#runtimeProviderOverrides.set(provider, { ...override, apiKey: "" });
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
this.#customProviderApiKeys.set(provider, resolved);
|
|
1954
|
+
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
1955
|
+
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
1956
|
+
this.authStorage.setConfigApiKey(provider, resolved, { owner: this.#authStorageConfigOwner });
|
|
1957
|
+
const override = this.#runtimeProviderOverrides.get(provider);
|
|
1958
|
+
if (override) this.#runtimeProviderOverrides.set(provider, { ...override, apiKey: resolved });
|
|
1959
|
+
const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
|
|
1960
|
+
if (authHeader === true) {
|
|
1961
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
|
|
1962
|
+
if (overlay.provider !== provider) return overlay;
|
|
1963
|
+
const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
|
|
1964
|
+
[GENERATED_AUTH_HEADER]?: string;
|
|
1965
|
+
};
|
|
1966
|
+
const generated = headers[GENERATED_AUTH_HEADER];
|
|
1967
|
+
const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
|
|
1968
|
+
const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated);
|
|
1969
|
+
if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
|
|
1970
|
+
if (resolved && (ownsAuthorization || !hadAuthorization))
|
|
1971
|
+
return {
|
|
1972
|
+
...overlay,
|
|
1973
|
+
headers: {
|
|
1974
|
+
...headers,
|
|
1975
|
+
Authorization: `Bearer ${resolved}`,
|
|
1976
|
+
[GENERATED_AUTH_HEADER]: `Bearer ${resolved}`,
|
|
1977
|
+
},
|
|
1978
|
+
};
|
|
1979
|
+
delete headers[GENERATED_AUTH_HEADER];
|
|
1980
|
+
return { ...overlay, headers };
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
this.#loadModels();
|
|
1985
|
+
for (const [provider, apiKeyConfig] of this.#runtimeProviderApiKeys) {
|
|
1986
|
+
const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
|
|
1987
|
+
? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
|
|
1988
|
+
: resolveApiKeyConfig(apiKeyConfig);
|
|
1645
1989
|
if (!resolved) continue;
|
|
1646
1990
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
1647
|
-
this.
|
|
1991
|
+
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
1992
|
+
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
1993
|
+
this.authStorage.setConfigApiKey(provider, resolved, { owner: this.#authStorageConfigOwner });
|
|
1648
1994
|
}
|
|
1649
1995
|
this.#lastDisabledProviderKey = disabledProviderKey;
|
|
1650
1996
|
}
|
|
@@ -1670,7 +2016,6 @@ export class ModelRegistry {
|
|
|
1670
2016
|
profiles,
|
|
1671
2017
|
error: configError,
|
|
1672
2018
|
} = this.#loadCustomModels();
|
|
1673
|
-
this.#configError = configError;
|
|
1674
2019
|
this.#keylessProviders = keylessProviders;
|
|
1675
2020
|
this.#discoveryManager.setProviders(discoverableProviders);
|
|
1676
2021
|
this.#configuredProviderIds = new Set(configuredProviders);
|
|
@@ -1681,25 +2026,59 @@ export class ModelRegistry {
|
|
|
1681
2026
|
this.#codexContextWindowOverrides = this.#collectCodexContextWindowOverrides();
|
|
1682
2027
|
this.#equivalenceConfig = equivalence;
|
|
1683
2028
|
this.#modelBindingsApplier.setBindings(modelBindings);
|
|
1684
|
-
|
|
2029
|
+
const acceptedPresets = loadAcceptedModelPresetProfiles(
|
|
2030
|
+
this.#modelPresetRegistryAgentDir,
|
|
2031
|
+
this.#modelPresetRegistryDependencies,
|
|
2032
|
+
);
|
|
2033
|
+
const acceptedRegistryError = acceptedPresets.error
|
|
2034
|
+
? new ConfigError("model-preset-registry", undefined, {
|
|
2035
|
+
err: new Error(acceptedPresets.error),
|
|
2036
|
+
stage: "Registry",
|
|
2037
|
+
})
|
|
2038
|
+
: undefined;
|
|
2039
|
+
this.#configError = configError ?? acceptedRegistryError;
|
|
1685
2040
|
|
|
1686
2041
|
this.#addImplicitDiscoverableProviders(configuredProviders);
|
|
1687
2042
|
const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
|
|
2043
|
+
const registryModels = this.#applyHardcodedModelPolicies(acceptedPresets.presets);
|
|
2044
|
+
const acceptedRegistryModelKeys = new Set(registryModels.map(model => `${model.provider}\u0000${model.id}`));
|
|
1688
2045
|
const cachedStandardModels = this.#applyHardcodedModelPolicies(this.#loadCachedStandardProviderModels());
|
|
1689
2046
|
const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
|
|
1690
|
-
const
|
|
2047
|
+
const resolvedProviderCatalog = this.#mergeResolvedModels(
|
|
1691
2048
|
this.#mergeResolvedModels(builtInModels, cachedStandardModels),
|
|
1692
2049
|
cachedDiscoveries,
|
|
1693
2050
|
);
|
|
2051
|
+
const resolvedDefaults = this.#mergeRegistryModelMetadata(resolvedProviderCatalog, registryModels, overrides);
|
|
2052
|
+
const materializedRegistryModelKeys = new Set(
|
|
2053
|
+
resolvedDefaults
|
|
2054
|
+
.map(model => `${model.provider}\u0000${model.id}`)
|
|
2055
|
+
.filter(key => acceptedRegistryModelKeys.has(key)),
|
|
2056
|
+
);
|
|
2057
|
+
this.#registryModelKeys = materializedRegistryModelKeys;
|
|
1694
2058
|
const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
|
|
1695
2059
|
// Merge runtime extension models so they survive refresh() cycles
|
|
1696
2060
|
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
1697
2061
|
const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
1698
2062
|
this.#models = this.#finalizeModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
|
|
2063
|
+
this.#modelProfiles = mergeModelProfiles(
|
|
2064
|
+
profiles,
|
|
2065
|
+
filterMaterializedRegistryProfiles(
|
|
2066
|
+
acceptedPresets.profiles,
|
|
2067
|
+
this.#models,
|
|
2068
|
+
new Set(acceptedPresets.dynamicProviders),
|
|
2069
|
+
),
|
|
2070
|
+
);
|
|
1699
2071
|
this.#rebuildProviderActivity();
|
|
1700
2072
|
this.#rebuildCanonicalIndex();
|
|
1701
2073
|
this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
|
|
1702
2074
|
this.#lastStaticLoadEnvironmentFingerprint = this.#getStaticLoadEnvironmentFingerprint();
|
|
2075
|
+
this.#lastModelPresetRegistryFingerprint = JSON.stringify({
|
|
2076
|
+
revision: acceptedPresets.revision,
|
|
2077
|
+
manifestSha256: acceptedPresets.manifestSha256,
|
|
2078
|
+
disabled: acceptedPresets.disabled,
|
|
2079
|
+
error: acceptedPresets.error,
|
|
2080
|
+
});
|
|
2081
|
+
this.#loadedModelPresetRegistryManifestSha256 = acceptedPresets.manifestSha256;
|
|
1703
2082
|
this.#staticModelsLoaded = true;
|
|
1704
2083
|
}
|
|
1705
2084
|
|
|
@@ -1716,6 +2095,10 @@ export class ModelRegistry {
|
|
|
1716
2095
|
}
|
|
1717
2096
|
for (const overlay of [...this.#customModelOverlays, ...this.#runtimeModelOverlays])
|
|
1718
2097
|
addStaticModel(overlay.provider, overlay.id);
|
|
2098
|
+
for (const model of this.#models) {
|
|
2099
|
+
if (this.#registryModelKeys.has(`${model.provider}\u0000${model.id}`))
|
|
2100
|
+
addStaticModel(model.provider, model.id);
|
|
2101
|
+
}
|
|
1719
2102
|
|
|
1720
2103
|
const runtimeProviderIds = new Set(this.#runtimeProviderSourceByName.keys());
|
|
1721
2104
|
const providerIds = new Set<string>([
|
|
@@ -1794,6 +2177,86 @@ export class ModelRegistry {
|
|
|
1794
2177
|
return merged;
|
|
1795
2178
|
}
|
|
1796
2179
|
|
|
2180
|
+
#mergeRegistryModelMetadata(
|
|
2181
|
+
baseModels: Model<Api>[],
|
|
2182
|
+
registryModels: Model<Api>[],
|
|
2183
|
+
providerOverrides: ReadonlyMap<string, ProviderOverride>,
|
|
2184
|
+
): Model<Api>[] {
|
|
2185
|
+
const merged = [...baseModels];
|
|
2186
|
+
const indexByKey = new Map(merged.map((model, index) => [`${model.provider}\u0000${model.id}`, index]));
|
|
2187
|
+
for (const registryModel of registryModels) {
|
|
2188
|
+
const key = `${registryModel.provider}\u0000${registryModel.id}`;
|
|
2189
|
+
const existingIndex = indexByKey.get(key);
|
|
2190
|
+
const explicitTransport = this.#resolveProviderOverride(registryModel.provider, providerOverrides);
|
|
2191
|
+
if (existingIndex === undefined) {
|
|
2192
|
+
const transportTemplates = merged.filter(
|
|
2193
|
+
model => model.provider === registryModel.provider && model.api === registryModel.api,
|
|
2194
|
+
);
|
|
2195
|
+
const transportTemplate = transportTemplates[0];
|
|
2196
|
+
const explicitTransportMatches =
|
|
2197
|
+
explicitTransport?.api === registryModel.api && explicitTransport.baseUrl !== undefined;
|
|
2198
|
+
const templatesAgree =
|
|
2199
|
+
transportTemplate !== undefined &&
|
|
2200
|
+
transportTemplates.every(template => hasMatchingRegistryTransport(transportTemplate, template));
|
|
2201
|
+
if (!explicitTransportMatches && transportTemplates.length > 0 && !templatesAgree) continue;
|
|
2202
|
+
if (!transportTemplate && !explicitTransportMatches) continue;
|
|
2203
|
+
const transportSource = explicitTransportMatches ? explicitTransport : transportTemplate!;
|
|
2204
|
+
const inheritedCompat = explicitTransportMatches
|
|
2205
|
+
? explicitTransport?.compat
|
|
2206
|
+
: commonRegistryTransportCompat(transportTemplates);
|
|
2207
|
+
const hydratedRegistryModel = {
|
|
2208
|
+
...registryModel,
|
|
2209
|
+
baseUrl: transportSource.baseUrl!,
|
|
2210
|
+
headers: transportSource.headers,
|
|
2211
|
+
transport: transportSource.transport,
|
|
2212
|
+
requestTransform: transportSource.requestTransform,
|
|
2213
|
+
cacheRetention: transportSource.cacheRetention,
|
|
2214
|
+
isOAuth: transportSource.isOAuth,
|
|
2215
|
+
compat:
|
|
2216
|
+
inheritedCompat || registryModel.compat || explicitTransport?.compat
|
|
2217
|
+
? { ...inheritedCompat, ...registryModel.compat, ...explicitTransport?.compat }
|
|
2218
|
+
: undefined,
|
|
2219
|
+
};
|
|
2220
|
+
merged.push(
|
|
2221
|
+
explicitTransportMatches
|
|
2222
|
+
? this.#applyProviderTransportOverride(hydratedRegistryModel, explicitTransport!)
|
|
2223
|
+
: hydratedRegistryModel,
|
|
2224
|
+
);
|
|
2225
|
+
indexByKey.set(key, merged.length - 1);
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
const existing = merged[existingIndex]!;
|
|
2229
|
+
const effectiveApi = explicitTransport?.api ?? existing.api;
|
|
2230
|
+
const registryApiMatchesEffectiveTransport = registryModel.api === effectiveApi;
|
|
2231
|
+
const registryMetadata = registryApiMatchesEffectiveTransport
|
|
2232
|
+
? registryModel
|
|
2233
|
+
: registryModelMetadataWithoutApiSpecificFields(registryModel);
|
|
2234
|
+
merged[existingIndex] = {
|
|
2235
|
+
...existing,
|
|
2236
|
+
...registryMetadata,
|
|
2237
|
+
api: explicitTransport?.api ?? existing.api,
|
|
2238
|
+
baseUrl: existing.baseUrl,
|
|
2239
|
+
headers: existing.headers,
|
|
2240
|
+
transport: existing.transport,
|
|
2241
|
+
requestTransform: existing.requestTransform,
|
|
2242
|
+
cacheRetention: existing.cacheRetention,
|
|
2243
|
+
isOAuth: explicitTransport?.isOAuth ?? existing.isOAuth,
|
|
2244
|
+
wireModelId: existing.wireModelId,
|
|
2245
|
+
compat:
|
|
2246
|
+
existing.compat ||
|
|
2247
|
+
(registryApiMatchesEffectiveTransport ? registryModel.compat : undefined) ||
|
|
2248
|
+
explicitTransport?.compat
|
|
2249
|
+
? {
|
|
2250
|
+
...existing.compat,
|
|
2251
|
+
...(registryApiMatchesEffectiveTransport ? registryModel.compat : undefined),
|
|
2252
|
+
...explicitTransport?.compat,
|
|
2253
|
+
}
|
|
2254
|
+
: undefined,
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
return merged;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
1797
2260
|
/** Merge custom models with built-in, replacing by provider+id match */
|
|
1798
2261
|
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
|
1799
2262
|
const merged = [...builtInModels];
|
|
@@ -2104,6 +2567,11 @@ export class ModelRegistry {
|
|
|
2104
2567
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
2105
2568
|
|
|
2106
2569
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
2570
|
+
const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
|
|
2571
|
+
const isOAuth = resolveCustomModelIsOAuth(
|
|
2572
|
+
(providerConfig.api as Api | undefined) ?? "openai-completions",
|
|
2573
|
+
providerConfig.auth as ProviderAuthMode | undefined,
|
|
2574
|
+
);
|
|
2107
2575
|
if (providerConfig.apiKeyEnv) {
|
|
2108
2576
|
this.#configuredApiKeyEnvNames.add(providerConfig.apiKeyEnv);
|
|
2109
2577
|
}
|
|
@@ -2113,12 +2581,16 @@ export class ModelRegistry {
|
|
|
2113
2581
|
if (providerConfig.openaiCompat?.apiKey)
|
|
2114
2582
|
this.#configuredApiKeyEnvNames.add(providerConfig.openaiCompat.apiKey);
|
|
2115
2583
|
if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
|
|
2116
|
-
const providerApiKeyConfig = providerConfig.apiKey
|
|
2584
|
+
const providerApiKeyConfig = providerConfig.apiKey
|
|
2585
|
+
? resolveApiKeyConfig(providerConfig.apiKey)
|
|
2586
|
+
: resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
|
|
2117
2587
|
const localOpenAICompat = providerConfig.openaiCompat;
|
|
2118
2588
|
const rotatingApiKeyEnv = providerConfig.apiKey
|
|
2119
2589
|
? undefined
|
|
2120
2590
|
: (providerConfig.apiKeyEnv ?? (localOpenAICompat?.apiKey ? undefined : localOpenAICompat?.apiKeyEnv));
|
|
2121
2591
|
if (rotatingApiKeyEnv) this.#customProviderApiKeyEnvNames.set(providerName, rotatingApiKeyEnv);
|
|
2592
|
+
if (providerConfig.authHeader !== undefined)
|
|
2593
|
+
this.#customProviderAuthHeaders.set(providerName, providerConfig.authHeader);
|
|
2122
2594
|
const localOpenAICompatApiKeyConfig = localOpenAICompat
|
|
2123
2595
|
? localOpenAICompat.apiKey
|
|
2124
2596
|
? resolveApiKeyConfig(localOpenAICompat.apiKey)
|
|
@@ -2132,8 +2604,10 @@ export class ModelRegistry {
|
|
|
2132
2604
|
? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
|
|
2133
2605
|
: undefined;
|
|
2134
2606
|
overrides.set(providerName, {
|
|
2607
|
+
api: "openai-completions",
|
|
2135
2608
|
baseUrl: localOpenAICompatBaseUrl,
|
|
2136
2609
|
apiKey: localOpenAICompatApiKeyConfig,
|
|
2610
|
+
isOAuth,
|
|
2137
2611
|
compat: {
|
|
2138
2612
|
supportsStore: false,
|
|
2139
2613
|
supportsDeveloperRole: false,
|
|
@@ -2154,7 +2628,10 @@ export class ModelRegistry {
|
|
|
2154
2628
|
});
|
|
2155
2629
|
if (localCompatResolvedKey) {
|
|
2156
2630
|
this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
|
|
2157
|
-
this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey
|
|
2631
|
+
this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey, {
|
|
2632
|
+
envSourced: !localOpenAICompat.apiKey,
|
|
2633
|
+
owner: this.#authStorageConfigOwner,
|
|
2634
|
+
});
|
|
2158
2635
|
} else {
|
|
2159
2636
|
keylessProviders.add(providerName);
|
|
2160
2637
|
this.#optionalAuthProviders.add(providerName);
|
|
@@ -2175,6 +2652,7 @@ export class ModelRegistry {
|
|
|
2175
2652
|
) {
|
|
2176
2653
|
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
2177
2654
|
overrides.set(providerName, {
|
|
2655
|
+
api: providerConfig.api as Api | undefined,
|
|
2178
2656
|
baseUrl: providerConfig.baseUrl,
|
|
2179
2657
|
headers: providerConfig.headers,
|
|
2180
2658
|
apiKey: providerApiKeyConfig,
|
|
@@ -2183,10 +2661,10 @@ export class ModelRegistry {
|
|
|
2183
2661
|
transport: providerConfig.transport,
|
|
2184
2662
|
requestTransform: providerConfig.requestTransform,
|
|
2185
2663
|
cacheRetention: providerConfig.cacheRetention,
|
|
2664
|
+
isOAuth,
|
|
2186
2665
|
});
|
|
2187
2666
|
}
|
|
2188
2667
|
|
|
2189
|
-
const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
|
|
2190
2668
|
if (authMode === "none") {
|
|
2191
2669
|
keylessProviders.add(providerName);
|
|
2192
2670
|
}
|
|
@@ -2216,7 +2694,12 @@ export class ModelRegistry {
|
|
|
2216
2694
|
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
2217
2695
|
: undefined;
|
|
2218
2696
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
2219
|
-
if (resolved)
|
|
2697
|
+
if (resolved) {
|
|
2698
|
+
this.authStorage.setConfigApiKey(providerName, resolved, {
|
|
2699
|
+
envSourced: !providerConfig.apiKey,
|
|
2700
|
+
owner: this.#authStorageConfigOwner,
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2220
2703
|
}
|
|
2221
2704
|
|
|
2222
2705
|
// Parse per-model overrides
|
|
@@ -2271,7 +2754,7 @@ export class ModelRegistry {
|
|
|
2271
2754
|
);
|
|
2272
2755
|
}
|
|
2273
2756
|
const current = loaded.value ?? this.#modelsConfigFile.createDefault();
|
|
2274
|
-
if (
|
|
2757
|
+
if (current.profiles?.[normalizedName] !== undefined) {
|
|
2275
2758
|
throw new Error(`Custom model profile already exists: ${normalizedName}. Choose a unique preset id.`);
|
|
2276
2759
|
}
|
|
2277
2760
|
const next: ModelsConfig = {
|
|
@@ -2397,6 +2880,8 @@ export class ModelRegistry {
|
|
|
2397
2880
|
async #refreshRuntimeDiscoveries(
|
|
2398
2881
|
strategy: ModelRefreshStrategy,
|
|
2399
2882
|
providerFilter?: ReadonlySet<string>,
|
|
2883
|
+
refreshGeneration = this.#catalogRefreshGeneration,
|
|
2884
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2400
2885
|
): Promise<void> {
|
|
2401
2886
|
const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
|
|
2402
2887
|
const selectedDiscoverableProviders = (
|
|
@@ -2408,12 +2893,22 @@ export class ModelRegistry {
|
|
|
2408
2893
|
selectedDiscoverableProviders.length === 0
|
|
2409
2894
|
? Promise.resolve([] as ConfiguredDiscoveryResult[])
|
|
2410
2895
|
: Promise.all(
|
|
2411
|
-
selectedDiscoverableProviders.map(provider =>
|
|
2896
|
+
selectedDiscoverableProviders.map(provider =>
|
|
2897
|
+
this.#discoverProviderModels(provider, strategy, providerRefresh),
|
|
2898
|
+
),
|
|
2412
2899
|
);
|
|
2413
2900
|
const [configuredDiscoveryResults, builtInDiscovered] = await Promise.all([
|
|
2414
2901
|
configuredDiscoveriesPromise,
|
|
2415
|
-
this.#discoverBuiltInProviderModels(strategy, providerFilter),
|
|
2902
|
+
this.#discoverBuiltInProviderModels(strategy, providerFilter, providerRefresh),
|
|
2416
2903
|
]);
|
|
2904
|
+
if (
|
|
2905
|
+
refreshGeneration !== this.#catalogRefreshGeneration ||
|
|
2906
|
+
(providerRefresh !== undefined &&
|
|
2907
|
+
"providerId" in providerRefresh &&
|
|
2908
|
+
this.#providerRefreshGenerations.get(providerRefresh.providerId) !== providerRefresh.generation)
|
|
2909
|
+
) {
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2417
2912
|
const currentConfiguredDiscoveryResults = configuredDiscoveryResults.map(result => {
|
|
2418
2913
|
const providerConfig = selectedDiscoverableProviders.find(provider => provider.provider === result.provider);
|
|
2419
2914
|
const current =
|
|
@@ -2580,6 +3075,7 @@ export class ModelRegistry {
|
|
|
2580
3075
|
async #discoverProviderModels(
|
|
2581
3076
|
providerConfig: DiscoveryProviderConfig,
|
|
2582
3077
|
strategy: ModelRefreshStrategy,
|
|
3078
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2583
3079
|
): Promise<ConfiguredDiscoveryResult> {
|
|
2584
3080
|
const provider = providerConfig.provider;
|
|
2585
3081
|
const preflightEpoch = this.#optionalAuthPreflightEpoch;
|
|
@@ -2594,7 +3090,8 @@ export class ModelRegistry {
|
|
|
2594
3090
|
let preflightCompleted = false;
|
|
2595
3091
|
const optionalAuth = this.#optionalAuthProviders.has(provider);
|
|
2596
3092
|
const shouldPreflightAuth = optionalAuth
|
|
2597
|
-
? this.authStorage.has(provider) ||
|
|
3093
|
+
? this.authStorage.has(provider) ||
|
|
3094
|
+
this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
|
|
2598
3095
|
: !this.#isCredentiallessProvider(provider);
|
|
2599
3096
|
let preflightAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
|
|
2600
3097
|
let preflightOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
|
|
@@ -2635,7 +3132,11 @@ export class ModelRegistry {
|
|
|
2635
3132
|
preflightCompleted = true;
|
|
2636
3133
|
if (!preflightFailed && optionalAuth && isCurrentPreflight()) {
|
|
2637
3134
|
this.#providerEvidenceApiKeys.set(provider, apiKey);
|
|
2638
|
-
const authGeneration = this.authStorage.getProviderEvidenceGeneration(
|
|
3135
|
+
const authGeneration = this.authStorage.getProviderEvidenceGeneration(
|
|
3136
|
+
provider,
|
|
3137
|
+
apiKey,
|
|
3138
|
+
this.#authStorageConfigOwner,
|
|
3139
|
+
);
|
|
2639
3140
|
if (apiKey === undefined) this.#credentiallessAuthFallbackProviders.set(provider, authGeneration);
|
|
2640
3141
|
else this.#credentiallessAuthFallbackProviders.delete(provider);
|
|
2641
3142
|
}
|
|
@@ -2684,6 +3185,14 @@ export class ModelRegistry {
|
|
|
2684
3185
|
const isCurrentEndpoint = () =>
|
|
2685
3186
|
endpoint ===
|
|
2686
3187
|
this.#normalizeDiscoveryEvidenceEndpoint(this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "");
|
|
3188
|
+
const isCurrentProviderRefresh = () =>
|
|
3189
|
+
providerRefresh === undefined ||
|
|
3190
|
+
("providerId" in providerRefresh
|
|
3191
|
+
? strategy !== "online-if-uncached" ||
|
|
3192
|
+
(providerRefresh.providerId === provider &&
|
|
3193
|
+
this.#providerRefreshGenerations.get(provider) === providerRefresh.generation)
|
|
3194
|
+
: (this.#providerRefreshGenerations.get(provider) ?? 0) ===
|
|
3195
|
+
(providerRefresh.generations.get(provider) ?? 0));
|
|
2687
3196
|
const evidence = this.#configuredDiscoveryEvidence.get(provider);
|
|
2688
3197
|
const cacheDynamicModelProvenance = fingerprintConfiguredDiscoveryRequestShape(
|
|
2689
3198
|
effectiveProviderConfig,
|
|
@@ -2732,7 +3241,7 @@ export class ModelRegistry {
|
|
|
2732
3241
|
),
|
|
2733
3242
|
getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
|
|
2734
3243
|
cacheDynamicModelProvenance: cacheLookupProvenance,
|
|
2735
|
-
canPublishCache: isCurrentEndpoint,
|
|
3244
|
+
canPublishCache: () => isCurrentEndpoint() && isCurrentProviderRefresh(),
|
|
2736
3245
|
});
|
|
2737
3246
|
const authGeneration =
|
|
2738
3247
|
mergeInput.authGeneration ??
|
|
@@ -2788,7 +3297,7 @@ export class ModelRegistry {
|
|
|
2788
3297
|
return {
|
|
2789
3298
|
...providerConfig,
|
|
2790
3299
|
baseUrl: effectiveBaseUrl,
|
|
2791
|
-
headers:
|
|
3300
|
+
headers: mergeCaseInsensitiveHeaders(providerConfig.headers, override?.headers),
|
|
2792
3301
|
compat: override?.compat ? mergeCompat(providerConfig.compat, override.compat) : providerConfig.compat,
|
|
2793
3302
|
requestTransform: mergeRequestTransform(providerConfig.requestTransform, override?.requestTransform),
|
|
2794
3303
|
cacheRetention: override?.cacheRetention ?? providerConfig.cacheRetention,
|
|
@@ -2818,6 +3327,7 @@ export class ModelRegistry {
|
|
|
2818
3327
|
async #discoverBuiltInProviderModels(
|
|
2819
3328
|
strategy: ModelRefreshStrategy,
|
|
2820
3329
|
providerFilter?: ReadonlySet<string>,
|
|
3330
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2821
3331
|
): Promise<Model<Api>[]> {
|
|
2822
3332
|
// Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
|
|
2823
3333
|
const configuredDiscoveryProviders = new Set(this.#discoveryManager.providers.map(p => p.provider));
|
|
@@ -2835,7 +3345,7 @@ export class ModelRegistry {
|
|
|
2835
3345
|
return [];
|
|
2836
3346
|
}
|
|
2837
3347
|
const discoveries = await Promise.all(
|
|
2838
|
-
managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy)),
|
|
3348
|
+
managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy, providerRefresh)),
|
|
2839
3349
|
);
|
|
2840
3350
|
return discoveries.flat();
|
|
2841
3351
|
}
|
|
@@ -2870,7 +3380,12 @@ export class ModelRegistry {
|
|
|
2870
3380
|
providerId: "openai-codex",
|
|
2871
3381
|
resolveKey: value => value,
|
|
2872
3382
|
createOptions: accessToken => {
|
|
2873
|
-
const accountId = resolveOAuthAccountIdForAccessToken(
|
|
3383
|
+
const accountId = resolveOAuthAccountIdForAccessToken(
|
|
3384
|
+
this.authStorage,
|
|
3385
|
+
"openai-codex",
|
|
3386
|
+
accessToken,
|
|
3387
|
+
this.#authStorageConfigOwner,
|
|
3388
|
+
);
|
|
2874
3389
|
return openaiCodexModelManagerOptions({
|
|
2875
3390
|
accessToken,
|
|
2876
3391
|
accountId,
|
|
@@ -2963,6 +3478,7 @@ export class ModelRegistry {
|
|
|
2963
3478
|
async #discoverWithModelManager(
|
|
2964
3479
|
{ options, authGeneration, apiKey, endpoint, endpointContainsUserinfo }: ModelManagerDiscoveryOptions,
|
|
2965
3480
|
strategy: ModelRefreshStrategy,
|
|
3481
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2966
3482
|
): Promise<Model<Api>[]> {
|
|
2967
3483
|
const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
|
|
2968
3484
|
this.#descriptorDiscoveryGenerations.set(options.providerId, generation);
|
|
@@ -2983,18 +3499,34 @@ export class ModelRegistry {
|
|
|
2983
3499
|
// stale fallback, and online-if-uncached reuse all fail closed.
|
|
2984
3500
|
const cacheDynamicModelProvenance =
|
|
2985
3501
|
reusableCacheProvenance ?? `gajae:non-cacheable-endpoint:${crypto.randomUUID()}`;
|
|
2986
|
-
const
|
|
3502
|
+
const isCurrentDiscoveryContext = () =>
|
|
2987
3503
|
(this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
|
|
2988
3504
|
this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
|
|
2989
3505
|
(endpoint ===
|
|
2990
3506
|
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(options.providerId) ?? "") ||
|
|
2991
3507
|
(canUseCredentialDerivedXiaomiEndpoint && credentialDerivedEndpoint !== undefined));
|
|
3508
|
+
const isCurrentProviderRefresh = () =>
|
|
3509
|
+
providerRefresh === undefined ||
|
|
3510
|
+
("providerId" in providerRefresh
|
|
3511
|
+
? providerRefresh.providerId === options.providerId &&
|
|
3512
|
+
this.#providerRefreshGenerations.get(options.providerId) === providerRefresh.generation
|
|
3513
|
+
: (this.#providerRefreshGenerations.get(options.providerId) ?? 0) ===
|
|
3514
|
+
(providerRefresh.generations.get(options.providerId) ?? 0));
|
|
3515
|
+
const isCurrentDiscovery = () =>
|
|
3516
|
+
isCurrentDiscoveryContext() &&
|
|
3517
|
+
(providerRefresh === undefined || "generations" in providerRefresh || isCurrentProviderRefresh());
|
|
3518
|
+
const canPublishCache = () =>
|
|
3519
|
+
isCurrentDiscoveryContext() &&
|
|
3520
|
+
(providerRefresh === undefined ||
|
|
3521
|
+
("providerId" in providerRefresh
|
|
3522
|
+
? strategy !== "online-if-uncached" || isCurrentDiscovery()
|
|
3523
|
+
: isCurrentProviderRefresh()));
|
|
2992
3524
|
try {
|
|
2993
3525
|
const manager = createModelManager({
|
|
2994
3526
|
...options,
|
|
2995
3527
|
cacheDbPath: this.#cacheDbPath,
|
|
2996
3528
|
cacheDynamicModelProvenance,
|
|
2997
|
-
canPublishCache
|
|
3529
|
+
canPublishCache,
|
|
2998
3530
|
...(options.fetchDynamicModels
|
|
2999
3531
|
? {
|
|
3000
3532
|
fetchDynamicModels: async () => {
|
|
@@ -3155,8 +3687,15 @@ export class ModelRegistry {
|
|
|
3155
3687
|
discoveryApiKey ??
|
|
3156
3688
|
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
3157
3689
|
? kNoAuth
|
|
3158
|
-
: await this.authStorage.getApiKey(providerConfig.provider
|
|
3159
|
-
|
|
3690
|
+
: await this.authStorage.getApiKey(providerConfig.provider, undefined, {
|
|
3691
|
+
owner: this.#authStorageConfigOwner,
|
|
3692
|
+
}));
|
|
3693
|
+
if (
|
|
3694
|
+
apiKey &&
|
|
3695
|
+
apiKey !== DEFAULT_LOCAL_TOKEN &&
|
|
3696
|
+
apiKey !== kNoAuth &&
|
|
3697
|
+
headerValue(headers, "Authorization") === undefined
|
|
3698
|
+
) {
|
|
3160
3699
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
3161
3700
|
}
|
|
3162
3701
|
const response = await fetch(tagsUrl, {
|
|
@@ -3235,8 +3774,15 @@ export class ModelRegistry {
|
|
|
3235
3774
|
discoveryApiKey ??
|
|
3236
3775
|
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
3237
3776
|
? kNoAuth
|
|
3238
|
-
: await this.authStorage.getApiKey(providerConfig.provider
|
|
3239
|
-
|
|
3777
|
+
: await this.authStorage.getApiKey(providerConfig.provider, undefined, {
|
|
3778
|
+
owner: this.#authStorageConfigOwner,
|
|
3779
|
+
}));
|
|
3780
|
+
if (
|
|
3781
|
+
apiKey &&
|
|
3782
|
+
apiKey !== DEFAULT_LOCAL_TOKEN &&
|
|
3783
|
+
apiKey !== kNoAuth &&
|
|
3784
|
+
headerValue(requestHeaders, "Authorization") === undefined
|
|
3785
|
+
) {
|
|
3240
3786
|
requestHeaders.Authorization = `Bearer ${apiKey}`;
|
|
3241
3787
|
}
|
|
3242
3788
|
|
|
@@ -3357,12 +3903,16 @@ export class ModelRegistry {
|
|
|
3357
3903
|
discoveryApiKey ??
|
|
3358
3904
|
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
3359
3905
|
? kNoAuth
|
|
3360
|
-
: await this.authStorage.getApiKey(providerConfig.provider, undefined, {
|
|
3906
|
+
: await this.authStorage.getApiKey(providerConfig.provider, undefined, {
|
|
3907
|
+
baseUrl,
|
|
3908
|
+
owner: this.#authStorageConfigOwner,
|
|
3909
|
+
}));
|
|
3361
3910
|
if (
|
|
3362
3911
|
apiKey &&
|
|
3363
3912
|
apiKey !== DEFAULT_LOCAL_TOKEN &&
|
|
3364
3913
|
!isVllmNoAuthToken(providerConfig.provider, apiKey) &&
|
|
3365
|
-
apiKey !== kNoAuth
|
|
3914
|
+
apiKey !== kNoAuth &&
|
|
3915
|
+
headerValue(requestHeaders, "Authorization") === undefined
|
|
3366
3916
|
) {
|
|
3367
3917
|
requestHeaders.Authorization = `Bearer ${apiKey}`;
|
|
3368
3918
|
}
|
|
@@ -3504,7 +4054,11 @@ export class ModelRegistry {
|
|
|
3504
4054
|
try {
|
|
3505
4055
|
fallbackMatchesCurrentEvidence =
|
|
3506
4056
|
fallbackEvidenceGeneration ===
|
|
3507
|
-
this.authStorage.getProviderEvidenceGeneration(
|
|
4057
|
+
this.authStorage.getProviderEvidenceGeneration(
|
|
4058
|
+
provider,
|
|
4059
|
+
this.#providerEvidenceApiKeys.get(provider),
|
|
4060
|
+
this.#authStorageConfigOwner,
|
|
4061
|
+
);
|
|
3508
4062
|
} catch {
|
|
3509
4063
|
// AuthStorage may be unavailable while a registry is being torn down.
|
|
3510
4064
|
}
|
|
@@ -3512,7 +4066,8 @@ export class ModelRegistry {
|
|
|
3512
4066
|
return (
|
|
3513
4067
|
this.#keylessProviders.has(provider) &&
|
|
3514
4068
|
(!this.#optionalAuthProviders.has(provider) ||
|
|
3515
|
-
(!this.authStorage.hasAuth(provider
|
|
4069
|
+
(!this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner }) &&
|
|
4070
|
+
!this.authStorage.has(provider)) ||
|
|
3516
4071
|
fallbackMatchesCurrentEvidence)
|
|
3517
4072
|
);
|
|
3518
4073
|
}
|
|
@@ -3523,6 +4078,7 @@ export class ModelRegistry {
|
|
|
3523
4078
|
return this.authStorage.getProviderEvidenceGeneration(
|
|
3524
4079
|
provider,
|
|
3525
4080
|
resolvedApiKey ?? this.#providerEvidenceApiKeys.get(provider),
|
|
4081
|
+
this.#authStorageConfigOwner,
|
|
3526
4082
|
);
|
|
3527
4083
|
}
|
|
3528
4084
|
#normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
|
|
@@ -3560,7 +4116,7 @@ export class ModelRegistry {
|
|
|
3560
4116
|
|
|
3561
4117
|
#resolveProviderOverride(
|
|
3562
4118
|
provider: string,
|
|
3563
|
-
overrides:
|
|
4119
|
+
overrides: ReadonlyMap<string, ProviderOverride> = this.#providerOverrides,
|
|
3564
4120
|
): ProviderOverride | undefined {
|
|
3565
4121
|
const explicitOverride = overrides.get(provider);
|
|
3566
4122
|
if (explicitOverride?.baseUrl) {
|
|
@@ -3587,24 +4143,19 @@ export class ModelRegistry {
|
|
|
3587
4143
|
|
|
3588
4144
|
#mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
|
|
3589
4145
|
return {
|
|
4146
|
+
api: override.api ?? baseOverride?.api,
|
|
3590
4147
|
baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
|
|
3591
4148
|
apiKey: override.apiKey ?? baseOverride?.apiKey,
|
|
3592
4149
|
authHeader: override.authHeader ?? baseOverride?.authHeader,
|
|
3593
|
-
headers:
|
|
4150
|
+
headers: mergeCaseInsensitiveHeaders(baseOverride?.headers, override.headers),
|
|
3594
4151
|
compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
|
|
3595
4152
|
transport: override.transport ?? baseOverride?.transport,
|
|
3596
4153
|
requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
|
|
3597
4154
|
cacheRetention: override.cacheRetention ?? baseOverride?.cacheRetention,
|
|
4155
|
+
isOAuth: override.isOAuth ?? baseOverride?.isOAuth,
|
|
3598
4156
|
};
|
|
3599
4157
|
}
|
|
3600
|
-
#applyProviderTransportOverride<
|
|
3601
|
-
T extends {
|
|
3602
|
-
baseUrl?: string;
|
|
3603
|
-
headers?: Record<string, string>;
|
|
3604
|
-
compat?: Model<Api>["compat"];
|
|
3605
|
-
cacheRetention?: CacheRetention;
|
|
3606
|
-
},
|
|
3607
|
-
>(
|
|
4158
|
+
#applyProviderTransportOverride<T extends Model<Api>>(
|
|
3608
4159
|
entry: T,
|
|
3609
4160
|
override: Pick<
|
|
3610
4161
|
ProviderOverride,
|
|
@@ -3618,12 +4169,39 @@ export class ModelRegistry {
|
|
|
3618
4169
|
| "cacheRetention"
|
|
3619
4170
|
>,
|
|
3620
4171
|
): T {
|
|
3621
|
-
const
|
|
3622
|
-
override.headers ? { ...entry.headers, ...override.headers } : entry.headers
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
4172
|
+
const overrideHeaders = (
|
|
4173
|
+
override.headers ? { ...entry.headers, ...override.headers } : { ...entry.headers }
|
|
4174
|
+
) as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string };
|
|
4175
|
+
const explicitAuthKey = override.headers
|
|
4176
|
+
? Object.keys(override.headers).find(key => key.toLowerCase() === "authorization")
|
|
4177
|
+
: undefined;
|
|
4178
|
+
if (explicitAuthKey !== undefined) {
|
|
4179
|
+
const explicitAuthValue = override.headers![explicitAuthKey]!;
|
|
4180
|
+
deleteHeaderCaseInsensitive(overrideHeaders, "Authorization");
|
|
4181
|
+
overrideHeaders[explicitAuthKey] = explicitAuthValue;
|
|
4182
|
+
delete (overrideHeaders as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
|
|
4183
|
+
GENERATED_AUTH_HEADER
|
|
4184
|
+
];
|
|
4185
|
+
}
|
|
4186
|
+
const existingMarker = (overrideHeaders as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
|
|
4187
|
+
GENERATED_AUTH_HEADER
|
|
4188
|
+
];
|
|
4189
|
+
const managesAuth = override.authHeader !== undefined || override.apiKey !== undefined;
|
|
4190
|
+
const canInject =
|
|
4191
|
+
managesAuth &&
|
|
4192
|
+
override.authHeader === true &&
|
|
4193
|
+
Boolean(override.apiKey) &&
|
|
4194
|
+
(headerValue(overrideHeaders, "Authorization") === undefined ||
|
|
4195
|
+
ownsOnlyGeneratedAuthorization(overrideHeaders, existingMarker));
|
|
4196
|
+
if (canInject) {
|
|
4197
|
+
deleteHeaderCaseInsensitive(overrideHeaders, "Authorization");
|
|
4198
|
+
delete overrideHeaders[GENERATED_AUTH_HEADER];
|
|
4199
|
+
} else if (managesAuth && existingMarker !== undefined) {
|
|
4200
|
+
deleteAuthorizationValue(overrideHeaders, existingMarker);
|
|
4201
|
+
delete overrideHeaders[GENERATED_AUTH_HEADER];
|
|
4202
|
+
}
|
|
4203
|
+
const headers = mergeAuthHeader(overrideHeaders, canInject, canInject ? override.apiKey : undefined);
|
|
4204
|
+
const result = {
|
|
3627
4205
|
...entry,
|
|
3628
4206
|
compat: mergeProviderCompat(entry.compat, override.compat),
|
|
3629
4207
|
baseUrl: override.baseUrl ?? entry.baseUrl,
|
|
@@ -3637,6 +4215,21 @@ export class ModelRegistry {
|
|
|
3637
4215
|
),
|
|
3638
4216
|
cacheRetention: entry.cacheRetention ?? override.cacheRetention,
|
|
3639
4217
|
};
|
|
4218
|
+
if (!canInject && explicitAuthKey !== undefined) {
|
|
4219
|
+
delete (result.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
|
|
4220
|
+
GENERATED_AUTH_HEADER
|
|
4221
|
+
];
|
|
4222
|
+
}
|
|
4223
|
+
const generatedHeader = (
|
|
4224
|
+
result.headers as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined
|
|
4225
|
+
)?.[GENERATED_AUTH_HEADER];
|
|
4226
|
+
if (generatedHeader !== undefined && result.headers?.Authorization === generatedHeader) {
|
|
4227
|
+
this.#generatedAuthHeaders.set(result, { authorization: result.headers.Authorization });
|
|
4228
|
+
} else if (explicitAuthKey === undefined) {
|
|
4229
|
+
const generated = this.#generatedAuthHeaders.get(entry);
|
|
4230
|
+
if (generated) this.#generatedAuthHeaders.set(result, generated);
|
|
4231
|
+
}
|
|
4232
|
+
return result;
|
|
3640
4233
|
}
|
|
3641
4234
|
#applyRuntimeProviderOverride(model: Model<Api>, override: ProviderOverride): Model<Api> {
|
|
3642
4235
|
const withTransportOverride = this.#applyProviderTransportOverride(
|
|
@@ -3649,26 +4242,53 @@ export class ModelRegistry {
|
|
|
3649
4242
|
? withTransportOverride
|
|
3650
4243
|
: { ...withTransportOverride, ...(sanitizedBaseUrl === undefined ? {} : { baseUrl: sanitizedBaseUrl }) };
|
|
3651
4244
|
const modelCompat = this.#modelOverrides.get(model.provider.toLowerCase())?.get(model.id.toLowerCase())?.compat;
|
|
3652
|
-
|
|
4245
|
+
const result = enrichModelThinking(
|
|
3653
4246
|
modelCompat
|
|
3654
4247
|
? { ...sanitizedTransport, compat: mergeCompat(sanitizedTransport.compat, modelCompat) }
|
|
3655
4248
|
: sanitizedTransport,
|
|
3656
4249
|
);
|
|
4250
|
+
const generated = this.#generatedAuthHeaders.get(sanitizedTransport);
|
|
4251
|
+
if (generated) this.#generatedAuthHeaders.set(result, generated);
|
|
4252
|
+
return result;
|
|
3657
4253
|
}
|
|
3658
4254
|
#applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
|
|
3659
4255
|
if (this.#runtimeProviderOverrides.size === 0) return models;
|
|
3660
4256
|
return models.map(model => {
|
|
3661
4257
|
const override = this.#runtimeProviderOverrides.get(model.provider);
|
|
3662
4258
|
if (!override) return model;
|
|
3663
|
-
|
|
4259
|
+
const runtimeCredentialRevoked =
|
|
4260
|
+
this.#runtimeProviderApiKeys.has(model.provider) &&
|
|
4261
|
+
!this.#runtimeProviderCredentialInstalled.has(model.provider);
|
|
4262
|
+
return this.#applyRuntimeProviderOverride(
|
|
4263
|
+
model,
|
|
4264
|
+
runtimeCredentialRevoked ? { ...override, apiKey: undefined, authHeader: undefined } : override,
|
|
4265
|
+
);
|
|
3664
4266
|
});
|
|
3665
4267
|
}
|
|
3666
4268
|
#finalizeModels(models: Model<Api>[]): Model<Api>[] {
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
4269
|
+
const finalized = models.map(model => enrichModelThinking({ ...this.#restoreDeclaredThinking(model) }));
|
|
4270
|
+
const result = applyFinalCodexGpt56ContextCap(finalized, undefined, this.#codexContextWindowOverrides);
|
|
4271
|
+
for (let index = 0; index < result.length; index++) {
|
|
4272
|
+
if (
|
|
4273
|
+
(result[index]?.headers as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined)?.[
|
|
4274
|
+
GENERATED_AUTH_HEADER
|
|
4275
|
+
]
|
|
4276
|
+
) {
|
|
4277
|
+
const authorization = result[index]?.headers?.Authorization;
|
|
4278
|
+
const marker = (result[index]?.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
|
|
4279
|
+
GENERATED_AUTH_HEADER
|
|
4280
|
+
];
|
|
4281
|
+
if (authorization && marker === authorization)
|
|
4282
|
+
this.#generatedAuthHeaders.set(result[index]!, { authorization });
|
|
4283
|
+
else
|
|
4284
|
+
delete (result[index]?.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
|
|
4285
|
+
GENERATED_AUTH_HEADER
|
|
4286
|
+
];
|
|
4287
|
+
}
|
|
4288
|
+
const generated = this.#generatedAuthHeaders.get(models[index]!);
|
|
4289
|
+
if (generated) this.#generatedAuthHeaders.set(result[index]!, generated);
|
|
4290
|
+
}
|
|
4291
|
+
return result;
|
|
3672
4292
|
}
|
|
3673
4293
|
#restoreDeclaredThinking(model: Model<Api>): Model<Api> {
|
|
3674
4294
|
const overrideThinking = this.#modelOverrides
|
|
@@ -3806,7 +4426,12 @@ export class ModelRegistry {
|
|
|
3806
4426
|
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
3807
4427
|
: undefined;
|
|
3808
4428
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
3809
|
-
if (resolved)
|
|
4429
|
+
if (resolved) {
|
|
4430
|
+
this.authStorage.setConfigApiKey(providerName, resolved, {
|
|
4431
|
+
envSourced: !providerConfig.apiKey,
|
|
4432
|
+
owner: this.#authStorageConfigOwner,
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
3810
4435
|
}
|
|
3811
4436
|
for (const modelDef of modelDefs) {
|
|
3812
4437
|
const providerCompat = providerConfig.disableStrictTools
|
|
@@ -3837,8 +4462,8 @@ export class ModelRegistry {
|
|
|
3837
4462
|
}
|
|
3838
4463
|
|
|
3839
4464
|
/**
|
|
3840
|
-
* Get all models (
|
|
3841
|
-
* If models.
|
|
4465
|
+
* Get all models (embedded + accepted registry + custom).
|
|
4466
|
+
* If models.yml had errors, returns embedded plus accepted registry models.
|
|
3842
4467
|
*/
|
|
3843
4468
|
getAll(): Model<Api>[] {
|
|
3844
4469
|
return this.#models;
|
|
@@ -3853,7 +4478,8 @@ export class ModelRegistry {
|
|
|
3853
4478
|
const disabled = disabledProviders ?? getDisabledProviderIdsFromSettings(this.#settings);
|
|
3854
4479
|
return (
|
|
3855
4480
|
!disabled.has(model.provider) &&
|
|
3856
|
-
(this.#keylessProviders.has(model.provider) ||
|
|
4481
|
+
(this.#keylessProviders.has(model.provider) ||
|
|
4482
|
+
this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner }))
|
|
3857
4483
|
);
|
|
3858
4484
|
}
|
|
3859
4485
|
/** Per-query precomputed variant filter inputs; plan fields are authoritative when provided. */
|
|
@@ -3907,7 +4533,9 @@ export class ModelRegistry {
|
|
|
3907
4533
|
* 6. Unknown (no credential surface)
|
|
3908
4534
|
*/
|
|
3909
4535
|
#effectiveProviderAuth(provider: string, sessionId?: string): EffectiveProviderAuth {
|
|
3910
|
-
const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId
|
|
4536
|
+
const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId, {
|
|
4537
|
+
owner: this.#authStorageConfigOwner,
|
|
4538
|
+
});
|
|
3911
4539
|
if (credentialType === "api_key") return "key";
|
|
3912
4540
|
if (credentialType === "oauth") return "oauth";
|
|
3913
4541
|
if (this.#keylessProviders.has(provider)) return "keyless";
|
|
@@ -4257,9 +4885,10 @@ export class ModelRegistry {
|
|
|
4257
4885
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
4258
4886
|
*/
|
|
4259
4887
|
getAvailable(): Model<Api>[] {
|
|
4888
|
+
this.#synchronizeEnvironmentCredentials();
|
|
4260
4889
|
const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
|
|
4261
4890
|
const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
|
|
4262
|
-
const envFingerprint = envAvailabilityFingerprint();
|
|
4891
|
+
const envFingerprint = envAvailabilityFingerprint(this.#configuredApiKeyEnvNames);
|
|
4263
4892
|
if (
|
|
4264
4893
|
this.#availableModelsCache &&
|
|
4265
4894
|
this.#availableModelsDisabledProviders === disabledProviderKey &&
|
|
@@ -4273,6 +4902,19 @@ export class ModelRegistry {
|
|
|
4273
4902
|
return this.#availableModelsCache;
|
|
4274
4903
|
}
|
|
4275
4904
|
|
|
4905
|
+
#synchronizeEnvironmentCredentials(): void {
|
|
4906
|
+
// Runtime registrations own a provider while their credential is present.
|
|
4907
|
+
// Refresh them first so a missing runtime env key can hand ownership back to
|
|
4908
|
+
// the static config before the static apiKeyEnv pass runs.
|
|
4909
|
+
for (const provider of this.#runtimeProviderApiKeyEnvNames.keys()) {
|
|
4910
|
+
this.#refreshRotatingConfigApiKey(provider);
|
|
4911
|
+
}
|
|
4912
|
+
for (const provider of this.#customProviderApiKeyEnvNames.keys()) {
|
|
4913
|
+
if (this.#runtimeProviderCredentialInstalled.has(provider)) continue;
|
|
4914
|
+
this.#refreshRotatingConfigApiKey(provider, true);
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4276
4918
|
/**
|
|
4277
4919
|
* Get authenticated models, excluding bundled entries that a fresh provider
|
|
4278
4920
|
* catalog has positively shown to be unavailable. Bundled entries remain
|
|
@@ -4342,7 +4984,10 @@ export class ModelRegistry {
|
|
|
4342
4984
|
|
|
4343
4985
|
#activeConnectionKind(model: Model<Api>): ActiveProviderDescriptor["connectionKind"] | undefined {
|
|
4344
4986
|
const evidence = this.#providerActivity.get(model.provider);
|
|
4345
|
-
if (
|
|
4987
|
+
if (
|
|
4988
|
+
!this.#isCredentiallessProvider(model.provider) &&
|
|
4989
|
+
this.authStorage.hasUsableAuth(model.provider, { owner: this.#authStorageConfigOwner })
|
|
4990
|
+
) {
|
|
4346
4991
|
if (!evidence) return undefined;
|
|
4347
4992
|
const discoveryOnly =
|
|
4348
4993
|
!evidence.staticConfigured &&
|
|
@@ -4417,14 +5062,20 @@ export class ModelRegistry {
|
|
|
4417
5062
|
* as providers with stored credentials. See issue #993.
|
|
4418
5063
|
*/
|
|
4419
5064
|
hasConfiguredAuth(model: Model<Api>): boolean {
|
|
4420
|
-
return
|
|
5065
|
+
return (
|
|
5066
|
+
this.#keylessProviders.has(model.provider) ||
|
|
5067
|
+
this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner })
|
|
5068
|
+
);
|
|
4421
5069
|
}
|
|
4422
5070
|
|
|
4423
5071
|
/**
|
|
4424
5072
|
* Check whether auth is configured for a provider.
|
|
4425
5073
|
*/
|
|
4426
5074
|
hasConfiguredProviderAuth(provider: string): boolean {
|
|
4427
|
-
return
|
|
5075
|
+
return (
|
|
5076
|
+
this.#keylessProviders.has(provider) ||
|
|
5077
|
+
this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
|
|
5078
|
+
);
|
|
4428
5079
|
}
|
|
4429
5080
|
|
|
4430
5081
|
isCredentiallessProvider(provider: string): boolean {
|
|
@@ -4459,11 +5110,24 @@ export class ModelRegistry {
|
|
|
4459
5110
|
);
|
|
4460
5111
|
}
|
|
4461
5112
|
|
|
5113
|
+
/** Opaque owner token for registry-scoped AuthStorage reads. */
|
|
5114
|
+
getAuthStorageOwner(): object {
|
|
5115
|
+
return this.#authStorageConfigOwner;
|
|
5116
|
+
}
|
|
5117
|
+
|
|
4462
5118
|
getProviderWebSearchMode(provider: string): WebSearchMode | undefined {
|
|
4463
5119
|
return this.#providerWebSearchModes.get(provider);
|
|
4464
5120
|
}
|
|
4465
5121
|
|
|
4466
5122
|
getActiveSearchModelContext(model: Model<Api>): ActiveSearchModelContext {
|
|
5123
|
+
const provider = model.provider.toLowerCase();
|
|
5124
|
+
const ownerAuthOverride =
|
|
5125
|
+
this.authStorage.hasConfigApiKey(model.provider, this.#authStorageConfigOwner) ||
|
|
5126
|
+
this.#providerOverrides.has(provider) ||
|
|
5127
|
+
this.#runtimeProviderOverrides.has(provider) ||
|
|
5128
|
+
this.#modelOverrides.get(provider)?.has(model.id.toLowerCase()) === true ||
|
|
5129
|
+
this.#customProviderAuthHeaders.has(provider) ||
|
|
5130
|
+
this.#runtimeProviderAuthHeaders.has(provider);
|
|
4467
5131
|
return {
|
|
4468
5132
|
provider: model.provider,
|
|
4469
5133
|
modelId: model.id,
|
|
@@ -4471,6 +5135,15 @@ export class ModelRegistry {
|
|
|
4471
5135
|
api: model.api,
|
|
4472
5136
|
baseUrl: model.baseUrl,
|
|
4473
5137
|
headers: model.headers,
|
|
5138
|
+
ownerAuthOverride,
|
|
5139
|
+
resolveCredentials: async ({ sessionId, signal }) => {
|
|
5140
|
+
// Resolve against the current registry model so rotating apiKeyEnv
|
|
5141
|
+
// credentials and generated Authorization headers are synchronized
|
|
5142
|
+
// immediately before native search sends its request.
|
|
5143
|
+
const currentModel = this.find(model.provider, model.id) ?? model;
|
|
5144
|
+
const apiKey = await this.getApiKey(currentModel, sessionId, { signal });
|
|
5145
|
+
return { apiKey, headers: currentModel.headers };
|
|
5146
|
+
},
|
|
4474
5147
|
webSearch: this.getProviderWebSearchMode(model.provider),
|
|
4475
5148
|
};
|
|
4476
5149
|
}
|
|
@@ -4501,15 +5174,18 @@ export class ModelRegistry {
|
|
|
4501
5174
|
} = {},
|
|
4502
5175
|
): Promise<string | undefined> {
|
|
4503
5176
|
this.#refreshRotatingConfigApiKey(model.provider);
|
|
4504
|
-
|
|
5177
|
+
const apiKey = await this.#getApiKeyOrNoAuth(model.provider, () =>
|
|
4505
5178
|
this.authStorage.getApiKey(model.provider, sessionId, {
|
|
4506
5179
|
baseUrl: model.baseUrl,
|
|
4507
5180
|
modelId: model.id,
|
|
4508
5181
|
credentialSelector: options.credentialSelector,
|
|
4509
5182
|
preferredCredentialSelector: options.preferredCredentialSelector,
|
|
5183
|
+
owner: this.#authStorageConfigOwner,
|
|
4510
5184
|
signal: options.signal,
|
|
4511
5185
|
}),
|
|
4512
5186
|
);
|
|
5187
|
+
this.#applyEffectiveAuthHeader(model, apiKey);
|
|
5188
|
+
return apiKey;
|
|
4513
5189
|
}
|
|
4514
5190
|
|
|
4515
5191
|
/**
|
|
@@ -4526,28 +5202,143 @@ export class ModelRegistry {
|
|
|
4526
5202
|
} = {},
|
|
4527
5203
|
): Promise<string | undefined> {
|
|
4528
5204
|
this.#refreshRotatingConfigApiKey(provider);
|
|
4529
|
-
|
|
5205
|
+
const apiKey = await this.#getApiKeyOrNoAuth(provider, () =>
|
|
4530
5206
|
this.authStorage.getApiKey(provider, sessionId, {
|
|
4531
5207
|
baseUrl,
|
|
4532
5208
|
credentialSelector: options.credentialSelector,
|
|
4533
5209
|
preferredCredentialSelector: options.preferredCredentialSelector,
|
|
5210
|
+
owner: this.#authStorageConfigOwner,
|
|
4534
5211
|
signal: options.signal,
|
|
4535
5212
|
}),
|
|
4536
5213
|
);
|
|
5214
|
+
if (
|
|
5215
|
+
this.#runtimeProviderAuthHeaders.get(provider) === true ||
|
|
5216
|
+
this.#customProviderAuthHeaders.get(provider) === true
|
|
5217
|
+
) {
|
|
5218
|
+
for (const model of this.#models) {
|
|
5219
|
+
if (model.provider === provider) this.#applyEffectiveAuthHeader(model, apiKey);
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
return apiKey;
|
|
4537
5223
|
}
|
|
4538
5224
|
|
|
4539
|
-
#refreshRotatingConfigApiKey(provider: string): void {
|
|
4540
|
-
const
|
|
5225
|
+
#refreshRotatingConfigApiKey(provider: string, forceStatic = false): void {
|
|
5226
|
+
const runtimeOwned = !forceStatic && this.#runtimeProviderApiKeys.has(provider);
|
|
5227
|
+
const envName = runtimeOwned
|
|
5228
|
+
? this.#runtimeProviderApiKeyEnvNames.get(provider)
|
|
5229
|
+
: this.#customProviderApiKeyEnvNames.get(provider);
|
|
4541
5230
|
if (!envName) return;
|
|
4542
5231
|
const resolved = $rotatingCredentialEnv(envName);
|
|
4543
|
-
|
|
5232
|
+
const previous = runtimeOwned
|
|
5233
|
+
? this.#runtimeProviderResolvedApiKeys.get(provider)
|
|
5234
|
+
: this.#customProviderApiKeys.get(provider);
|
|
5235
|
+
if (resolved === previous && (!runtimeOwned || this.#runtimeProviderCredentialInstalled.has(provider))) return;
|
|
4544
5236
|
if (resolved === undefined) {
|
|
5237
|
+
if (runtimeOwned) {
|
|
5238
|
+
if (!this.#runtimeProviderCredentialInstalled.has(provider)) return;
|
|
5239
|
+
this.#runtimeProviderCredentialInstalled.delete(provider);
|
|
5240
|
+
this.#staticModelsLoaded = false;
|
|
5241
|
+
this.#reloadStaticModels();
|
|
5242
|
+
return;
|
|
5243
|
+
}
|
|
4545
5244
|
this.#customProviderApiKeys.delete(provider);
|
|
4546
|
-
this.authStorage.removeConfigApiKey(provider);
|
|
5245
|
+
this.authStorage.removeConfigApiKey(provider, this.#authStorageConfigOwner);
|
|
5246
|
+
} else {
|
|
5247
|
+
this.#customProviderApiKeys.set(provider, resolved);
|
|
5248
|
+
if (runtimeOwned) {
|
|
5249
|
+
this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
|
|
5250
|
+
this.#runtimeProviderCredentialInstalled.add(provider);
|
|
5251
|
+
}
|
|
5252
|
+
if (runtimeOwned) {
|
|
5253
|
+
this.authStorage.setConfigApiKey(provider, resolved, { owner: this.#authStorageConfigOwner });
|
|
5254
|
+
} else {
|
|
5255
|
+
this.authStorage.setConfigApiKey(provider, resolved, {
|
|
5256
|
+
envSourced: true,
|
|
5257
|
+
owner: this.#authStorageConfigOwner,
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
const authHeader =
|
|
5262
|
+
this.#runtimeProviderAuthHeaders.get(provider) ?? this.#customProviderAuthHeaders.get(provider);
|
|
5263
|
+
const runtimeOverride = this.#runtimeProviderOverrides.get(provider);
|
|
5264
|
+
if (runtimeOverride) this.#runtimeProviderOverrides.set(provider, { ...runtimeOverride, apiKey: resolved ?? "" });
|
|
5265
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
|
|
5266
|
+
if (overlay.provider !== provider) return overlay;
|
|
5267
|
+
const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
|
|
5268
|
+
[GENERATED_AUTH_HEADER]?: string;
|
|
5269
|
+
};
|
|
5270
|
+
const generated = headers[GENERATED_AUTH_HEADER];
|
|
5271
|
+
const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
|
|
5272
|
+
const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated);
|
|
5273
|
+
if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
|
|
5274
|
+
else if (typeof generated === "string") deleteAuthorizationValue(headers, generated);
|
|
5275
|
+
delete headers[GENERATED_AUTH_HEADER];
|
|
5276
|
+
if (authHeader === true && resolved && (ownsAuthorization || !hadAuthorization)) {
|
|
5277
|
+
headers.Authorization = `Bearer ${resolved}`;
|
|
5278
|
+
headers[GENERATED_AUTH_HEADER] = `Bearer ${resolved}`;
|
|
5279
|
+
}
|
|
5280
|
+
return { ...overlay, headers };
|
|
5281
|
+
});
|
|
5282
|
+
if (authHeader === true) this.#generatedAuthHeaderProviders.add(provider);
|
|
5283
|
+
if (authHeader !== true && !this.#generatedAuthHeaderProviders.has(provider)) return;
|
|
5284
|
+
for (const model of this.#models) {
|
|
5285
|
+
if (model.provider !== provider) continue;
|
|
5286
|
+
const headers = { ...(model.headers ?? {}) } as Record<string, string> & {
|
|
5287
|
+
[GENERATED_AUTH_HEADER]?: string;
|
|
5288
|
+
};
|
|
5289
|
+
const generated =
|
|
5290
|
+
this.#generatedAuthHeaders.get(model) ??
|
|
5291
|
+
(typeof headers[GENERATED_AUTH_HEADER] === "string"
|
|
5292
|
+
? { authorization: headers[GENERATED_AUTH_HEADER] }
|
|
5293
|
+
: undefined);
|
|
5294
|
+
const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
|
|
5295
|
+
const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated?.authorization);
|
|
5296
|
+
if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
|
|
5297
|
+
else if (generated?.authorization) deleteAuthorizationValue(headers, generated.authorization);
|
|
5298
|
+
if (generated?.apiKey === headers["X-Api-Key"]) delete headers["X-Api-Key"];
|
|
5299
|
+
delete headers[GENERATED_AUTH_HEADER];
|
|
5300
|
+
model.headers =
|
|
5301
|
+
authHeader === true && resolved && (ownsAuthorization || !hadAuthorization)
|
|
5302
|
+
? { ...headers, Authorization: `Bearer ${resolved}`, [GENERATED_AUTH_HEADER]: `Bearer ${resolved}` }
|
|
5303
|
+
: headers;
|
|
5304
|
+
if (authHeader === true && resolved && (ownsAuthorization || !hadAuthorization))
|
|
5305
|
+
this.#generatedAuthHeaders.set(model, { authorization: model.headers.Authorization });
|
|
5306
|
+
}
|
|
5307
|
+
this.#rebuildCanonicalIndex();
|
|
5308
|
+
}
|
|
5309
|
+
|
|
5310
|
+
#applyEffectiveAuthHeader(model: Model<Api>, apiKey: string | undefined): void {
|
|
5311
|
+
const runtimeCredentialRevoked =
|
|
5312
|
+
this.#runtimeProviderApiKeys.has(model.provider) &&
|
|
5313
|
+
!this.#runtimeProviderCredentialInstalled.has(model.provider);
|
|
5314
|
+
const authHeader = runtimeCredentialRevoked
|
|
5315
|
+
? this.#customProviderAuthHeaders.get(model.provider)
|
|
5316
|
+
: (this.#runtimeProviderAuthHeaders.get(model.provider) ??
|
|
5317
|
+
this.#customProviderAuthHeaders.get(model.provider));
|
|
5318
|
+
if (authHeader === true) this.#generatedAuthHeaderProviders.add(model.provider);
|
|
5319
|
+
const headers = { ...(model.headers ?? {}) } as Record<string, string> & {
|
|
5320
|
+
[GENERATED_AUTH_HEADER]?: string;
|
|
5321
|
+
};
|
|
5322
|
+
const generated =
|
|
5323
|
+
this.#generatedAuthHeaders.get(model) ??
|
|
5324
|
+
(typeof headers[GENERATED_AUTH_HEADER] === "string"
|
|
5325
|
+
? { authorization: headers[GENERATED_AUTH_HEADER] }
|
|
5326
|
+
: undefined);
|
|
5327
|
+
const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
|
|
5328
|
+
const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated?.authorization);
|
|
5329
|
+
if (authHeader !== true && generated === undefined && !this.#generatedAuthHeaderProviders.has(model.provider))
|
|
4547
5330
|
return;
|
|
5331
|
+
if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
|
|
5332
|
+
else if (generated?.authorization) deleteAuthorizationValue(headers, generated.authorization);
|
|
5333
|
+
if (generated?.apiKey === headers["X-Api-Key"]) delete headers["X-Api-Key"];
|
|
5334
|
+
delete headers[GENERATED_AUTH_HEADER];
|
|
5335
|
+
model.headers =
|
|
5336
|
+
authHeader === true && apiKey && apiKey !== kNoAuth && (ownsAuthorization || !hadAuthorization)
|
|
5337
|
+
? { ...headers, Authorization: `Bearer ${apiKey}`, [GENERATED_AUTH_HEADER]: `Bearer ${apiKey}` }
|
|
5338
|
+
: headers;
|
|
5339
|
+
if (authHeader === true && apiKey && apiKey !== kNoAuth && (ownsAuthorization || !hadAuthorization)) {
|
|
5340
|
+
this.#generatedAuthHeaders.set(model, { authorization: model.headers.Authorization });
|
|
4548
5341
|
}
|
|
4549
|
-
this.#customProviderApiKeys.set(provider, resolved);
|
|
4550
|
-
this.authStorage.setConfigApiKey(provider, resolved);
|
|
4551
5342
|
}
|
|
4552
5343
|
|
|
4553
5344
|
async #peekApiKeyForProvider(
|
|
@@ -4563,16 +5354,21 @@ export class ModelRegistry {
|
|
|
4563
5354
|
return kNoAuth;
|
|
4564
5355
|
}
|
|
4565
5356
|
try {
|
|
4566
|
-
this.authStorage.getProviderEvidenceGeneration(provider);
|
|
5357
|
+
this.authStorage.getProviderEvidenceGeneration(provider, undefined, this.#authStorageConfigOwner);
|
|
4567
5358
|
} catch {
|
|
4568
5359
|
return undefined;
|
|
4569
5360
|
}
|
|
4570
5361
|
if (options.refreshOAuth && this.authStorage.hasOAuth(provider)) {
|
|
4571
|
-
return this.authStorage.getApiKey(provider, undefined, {
|
|
5362
|
+
return this.authStorage.getApiKey(provider, undefined, {
|
|
5363
|
+
baseUrl: options.baseUrl,
|
|
5364
|
+
owner: this.#authStorageConfigOwner,
|
|
5365
|
+
});
|
|
4572
5366
|
}
|
|
4573
5367
|
return options.ignoreCredentiallessFallback
|
|
4574
|
-
? this.authStorage.peekApiKey(provider)
|
|
4575
|
-
: this.#getApiKeyOrNoAuth(provider, () =>
|
|
5368
|
+
? this.authStorage.peekApiKey(provider, { owner: this.#authStorageConfigOwner })
|
|
5369
|
+
: this.#getApiKeyOrNoAuth(provider, () =>
|
|
5370
|
+
this.authStorage.peekApiKey(provider, { owner: this.#authStorageConfigOwner }),
|
|
5371
|
+
);
|
|
4576
5372
|
}
|
|
4577
5373
|
|
|
4578
5374
|
/**
|
|
@@ -4598,9 +5394,13 @@ export class ModelRegistry {
|
|
|
4598
5394
|
|
|
4599
5395
|
#clearRuntimeProviderState(providerName: string): void {
|
|
4600
5396
|
this.#runtimeProviderApiKeys.delete(providerName);
|
|
5397
|
+
this.#runtimeProviderResolvedApiKeys.delete(providerName);
|
|
5398
|
+
this.#runtimeProviderCredentialInstalled.delete(providerName);
|
|
5399
|
+
this.#runtimeProviderApiKeyEnvNames.delete(providerName);
|
|
4601
5400
|
this.#runtimeProviderOverrides.delete(providerName);
|
|
5401
|
+
this.#runtimeProviderAuthHeaders.delete(providerName);
|
|
4602
5402
|
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
|
|
4603
|
-
this.authStorage.removeConfigApiKey(providerName);
|
|
5403
|
+
this.authStorage.removeConfigApiKey(providerName, this.#authStorageConfigOwner);
|
|
4604
5404
|
this.#clearDescriptorDiscoveryEvidence(providerName);
|
|
4605
5405
|
}
|
|
4606
5406
|
|
|
@@ -4686,7 +5486,6 @@ export class ModelRegistry {
|
|
|
4686
5486
|
sourceId,
|
|
4687
5487
|
});
|
|
4688
5488
|
}
|
|
4689
|
-
|
|
4690
5489
|
let sourceHandoff = false;
|
|
4691
5490
|
if (sourceId) {
|
|
4692
5491
|
this.#registeredProviderSources.add(sourceId);
|
|
@@ -4709,14 +5508,89 @@ export class ModelRegistry {
|
|
|
4709
5508
|
this.#lastStaticLoadMtime = null;
|
|
4710
5509
|
this.#staticModelsLoaded = false;
|
|
4711
5510
|
this.#reloadStaticModels();
|
|
5511
|
+
if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
|
|
5512
|
+
}
|
|
5513
|
+
if (config.authHeader !== undefined) {
|
|
5514
|
+
if (config.authHeader === false) {
|
|
5515
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
|
|
5516
|
+
if (overlay.provider !== providerName) return overlay;
|
|
5517
|
+
const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
|
|
5518
|
+
[GENERATED_AUTH_HEADER]?: string;
|
|
5519
|
+
};
|
|
5520
|
+
const generated = headers[GENERATED_AUTH_HEADER];
|
|
5521
|
+
if (typeof generated === "string" && headers.Authorization === generated) delete headers.Authorization;
|
|
5522
|
+
delete headers[GENERATED_AUTH_HEADER];
|
|
5523
|
+
return { ...overlay, headers };
|
|
5524
|
+
});
|
|
5525
|
+
}
|
|
5526
|
+
this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
|
|
5527
|
+
if (config.authHeader === true) this.#generatedAuthHeaderProviders.add(providerName);
|
|
4712
5528
|
}
|
|
4713
5529
|
|
|
4714
5530
|
if (config.apiKey) {
|
|
4715
|
-
|
|
5531
|
+
const resolved = resolveApiKeyConfig(config.apiKey);
|
|
5532
|
+
if (!resolved) return;
|
|
5533
|
+
if (Bun.env[config.apiKey] !== undefined) this.#runtimeProviderApiKeyEnvNames.set(providerName, config.apiKey);
|
|
5534
|
+
else this.#runtimeProviderApiKeyEnvNames.delete(providerName);
|
|
5535
|
+
this.#customProviderApiKeys.set(providerName, resolved);
|
|
4716
5536
|
// Persist runtime API keys so they survive #reloadStaticModels() cycles
|
|
4717
5537
|
this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
|
|
4718
|
-
|
|
4719
|
-
|
|
5538
|
+
this.#runtimeProviderResolvedApiKeys.set(providerName, resolved);
|
|
5539
|
+
this.#runtimeProviderCredentialInstalled.add(providerName);
|
|
5540
|
+
if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
|
|
5541
|
+
this.authStorage.setConfigApiKey(providerName, resolved, { owner: this.#authStorageConfigOwner });
|
|
5542
|
+
}
|
|
5543
|
+
if (config.oauth && !config.apiKey && this.#runtimeProviderApiKeys.has(providerName)) {
|
|
5544
|
+
const previousApiKey = this.#runtimeProviderResolvedApiKeys.get(providerName);
|
|
5545
|
+
this.#runtimeProviderApiKeys.delete(providerName);
|
|
5546
|
+
this.#runtimeProviderResolvedApiKeys.delete(providerName);
|
|
5547
|
+
this.#runtimeProviderCredentialInstalled.delete(providerName);
|
|
5548
|
+
this.#runtimeProviderApiKeyEnvNames.delete(providerName);
|
|
5549
|
+
this.#runtimeProviderAuthHeaders.delete(providerName);
|
|
5550
|
+
this.#customProviderApiKeys.delete(providerName);
|
|
5551
|
+
this.authStorage.removeConfigApiKey(providerName, this.#authStorageConfigOwner);
|
|
5552
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
|
|
5553
|
+
if (overlay.provider !== providerName) return overlay;
|
|
5554
|
+
const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
|
|
5555
|
+
[GENERATED_AUTH_HEADER]?: string;
|
|
5556
|
+
};
|
|
5557
|
+
const generated = headers[GENERATED_AUTH_HEADER];
|
|
5558
|
+
for (const key of Object.keys(headers)) {
|
|
5559
|
+
if (key.toLowerCase() !== "authorization") continue;
|
|
5560
|
+
const value = headers[key];
|
|
5561
|
+
if (
|
|
5562
|
+
(typeof generated === "string" && value === generated) ||
|
|
5563
|
+
(previousApiKey !== undefined && value === `Bearer ${previousApiKey}`) ||
|
|
5564
|
+
(previousApiKey === undefined && value.startsWith("Bearer "))
|
|
5565
|
+
)
|
|
5566
|
+
delete headers[key];
|
|
5567
|
+
}
|
|
5568
|
+
delete headers[GENERATED_AUTH_HEADER];
|
|
5569
|
+
return { ...overlay, headers };
|
|
5570
|
+
});
|
|
5571
|
+
const previousOverride = this.#runtimeProviderOverrides.get(providerName);
|
|
5572
|
+
if (previousOverride) {
|
|
5573
|
+
const headers = { ...(previousOverride.headers ?? {}) };
|
|
5574
|
+
for (const key of Object.keys(headers)) {
|
|
5575
|
+
if (key.toLowerCase() !== "authorization") continue;
|
|
5576
|
+
const value = headers[key];
|
|
5577
|
+
if (
|
|
5578
|
+
(previousApiKey !== undefined && value === `Bearer ${previousApiKey}`) ||
|
|
5579
|
+
(previousApiKey === undefined && value.startsWith("Bearer "))
|
|
5580
|
+
)
|
|
5581
|
+
delete headers[key];
|
|
5582
|
+
}
|
|
5583
|
+
this.#runtimeProviderOverrides.set(providerName, {
|
|
5584
|
+
...previousOverride,
|
|
5585
|
+
apiKey: "",
|
|
5586
|
+
authHeader: undefined,
|
|
5587
|
+
isOAuth: true,
|
|
5588
|
+
headers,
|
|
5589
|
+
});
|
|
5590
|
+
}
|
|
5591
|
+
this.#staticModelsLoaded = false;
|
|
5592
|
+
this.#reloadStaticModels();
|
|
5593
|
+
if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
|
|
4720
5594
|
}
|
|
4721
5595
|
|
|
4722
5596
|
if (config.models && config.models.length > 0) {
|
|
@@ -4728,7 +5602,7 @@ export class ModelRegistry {
|
|
|
4728
5602
|
config.baseUrl!,
|
|
4729
5603
|
config.api,
|
|
4730
5604
|
config.headers,
|
|
4731
|
-
config.apiKey,
|
|
5605
|
+
config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
|
|
4732
5606
|
config.authHeader,
|
|
4733
5607
|
config.compat,
|
|
4734
5608
|
config.requestTransform,
|
|
@@ -4750,6 +5624,29 @@ export class ModelRegistry {
|
|
|
4750
5624
|
for (const overlay of newOverlays) {
|
|
4751
5625
|
nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
|
|
4752
5626
|
}
|
|
5627
|
+
if (
|
|
5628
|
+
config.baseUrl ||
|
|
5629
|
+
config.headers ||
|
|
5630
|
+
config.apiKey ||
|
|
5631
|
+
config.authHeader !== undefined ||
|
|
5632
|
+
config.compat !== undefined ||
|
|
5633
|
+
config.requestTransform !== undefined ||
|
|
5634
|
+
config.transport !== undefined
|
|
5635
|
+
) {
|
|
5636
|
+
this.#runtimeProviderOverrides.set(
|
|
5637
|
+
providerName,
|
|
5638
|
+
this.#mergeProviderOverride(this.#runtimeProviderOverrides.get(providerName), {
|
|
5639
|
+
api: config.api,
|
|
5640
|
+
baseUrl: this.#runtimeProviderOverrides.get(providerName)?.baseUrl ?? config.baseUrl,
|
|
5641
|
+
headers: config.headers,
|
|
5642
|
+
apiKey: config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
|
|
5643
|
+
authHeader: config.authHeader,
|
|
5644
|
+
compat: config.compat,
|
|
5645
|
+
requestTransform: config.requestTransform,
|
|
5646
|
+
transport: config.transport,
|
|
5647
|
+
}),
|
|
5648
|
+
);
|
|
5649
|
+
}
|
|
4753
5650
|
const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
|
|
4754
5651
|
const withRuntimeTransportOverride = runtimeTransportOverride
|
|
4755
5652
|
? nextModels.map(model => {
|
|
@@ -4760,7 +5657,9 @@ export class ModelRegistry {
|
|
|
4760
5657
|
const withModelOverrides = this.#applyProviderModelOverrides(providerName, withRuntimeTransportOverride);
|
|
4761
5658
|
|
|
4762
5659
|
if (config.oauth?.modifyModels) {
|
|
4763
|
-
const credential = this.authStorage.getOAuthCredential(providerName
|
|
5660
|
+
const credential = this.authStorage.getOAuthCredential(providerName, undefined, {
|
|
5661
|
+
owner: this.#authStorageConfigOwner,
|
|
5662
|
+
});
|
|
4764
5663
|
if (credential) {
|
|
4765
5664
|
this.#models = this.#finalizeModels(config.oauth.modifyModels(withModelOverrides, credential));
|
|
4766
5665
|
this.#rebuildCanonicalIndex();
|
|
@@ -4785,9 +5684,10 @@ export class ModelRegistry {
|
|
|
4785
5684
|
config.transport !== undefined
|
|
4786
5685
|
) {
|
|
4787
5686
|
const transportOverride = {
|
|
5687
|
+
api: config.api,
|
|
4788
5688
|
baseUrl: config.baseUrl,
|
|
4789
5689
|
headers: config.headers,
|
|
4790
|
-
apiKey: config.apiKey,
|
|
5690
|
+
apiKey: config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
|
|
4791
5691
|
authHeader: config.authHeader,
|
|
4792
5692
|
compat: config.compat,
|
|
4793
5693
|
requestTransform: config.requestTransform,
|
|
@@ -4802,6 +5702,11 @@ export class ModelRegistry {
|
|
|
4802
5702
|
if (m.provider !== providerName) return m;
|
|
4803
5703
|
return this.#applyRuntimeProviderOverride(m, transportOverride);
|
|
4804
5704
|
});
|
|
5705
|
+
if (config.authHeader === false) {
|
|
5706
|
+
for (const model of this.#models) {
|
|
5707
|
+
if (model.provider === providerName) this.#applyEffectiveAuthHeader(model, undefined);
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
4805
5710
|
this.#rebuildCanonicalIndex();
|
|
4806
5711
|
this.#rebuildProviderActivity();
|
|
4807
5712
|
}
|