@gajae-code/coding-agent 0.15.5 → 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 +26 -1
- 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 +2 -0
- 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 +96 -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 +2 -5
- 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/client/discovery.d.ts +1 -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 +15 -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 -7
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- 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/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 +40 -5
- 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/config/autorouting-tier-map.ts +32 -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 +51 -9
- package/src/config/model-registry.ts +1021 -122
- 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/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 +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -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/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- 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/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 +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/cli/session-cli.ts +12 -1
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -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 +371 -37
- package/src/sdk/host/session-runtime.ts +364 -52
- 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 +228 -96
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +678 -103
- package/src/session/fold-coordinator.ts +433 -0
- 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 +5 -4
- 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 +772 -67
- package/src/tools/index.ts +7 -5
- 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,
|
|
@@ -2156,6 +2630,7 @@ export class ModelRegistry {
|
|
|
2156
2630
|
this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
|
|
2157
2631
|
this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey, {
|
|
2158
2632
|
envSourced: !localOpenAICompat.apiKey,
|
|
2633
|
+
owner: this.#authStorageConfigOwner,
|
|
2159
2634
|
});
|
|
2160
2635
|
} else {
|
|
2161
2636
|
keylessProviders.add(providerName);
|
|
@@ -2177,6 +2652,7 @@ export class ModelRegistry {
|
|
|
2177
2652
|
) {
|
|
2178
2653
|
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
2179
2654
|
overrides.set(providerName, {
|
|
2655
|
+
api: providerConfig.api as Api | undefined,
|
|
2180
2656
|
baseUrl: providerConfig.baseUrl,
|
|
2181
2657
|
headers: providerConfig.headers,
|
|
2182
2658
|
apiKey: providerApiKeyConfig,
|
|
@@ -2185,10 +2661,10 @@ export class ModelRegistry {
|
|
|
2185
2661
|
transport: providerConfig.transport,
|
|
2186
2662
|
requestTransform: providerConfig.requestTransform,
|
|
2187
2663
|
cacheRetention: providerConfig.cacheRetention,
|
|
2664
|
+
isOAuth,
|
|
2188
2665
|
});
|
|
2189
2666
|
}
|
|
2190
2667
|
|
|
2191
|
-
const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
|
|
2192
2668
|
if (authMode === "none") {
|
|
2193
2669
|
keylessProviders.add(providerName);
|
|
2194
2670
|
}
|
|
@@ -2219,7 +2695,10 @@ export class ModelRegistry {
|
|
|
2219
2695
|
: undefined;
|
|
2220
2696
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
2221
2697
|
if (resolved) {
|
|
2222
|
-
this.authStorage.setConfigApiKey(providerName, resolved, {
|
|
2698
|
+
this.authStorage.setConfigApiKey(providerName, resolved, {
|
|
2699
|
+
envSourced: !providerConfig.apiKey,
|
|
2700
|
+
owner: this.#authStorageConfigOwner,
|
|
2701
|
+
});
|
|
2223
2702
|
}
|
|
2224
2703
|
}
|
|
2225
2704
|
|
|
@@ -2275,7 +2754,7 @@ export class ModelRegistry {
|
|
|
2275
2754
|
);
|
|
2276
2755
|
}
|
|
2277
2756
|
const current = loaded.value ?? this.#modelsConfigFile.createDefault();
|
|
2278
|
-
if (
|
|
2757
|
+
if (current.profiles?.[normalizedName] !== undefined) {
|
|
2279
2758
|
throw new Error(`Custom model profile already exists: ${normalizedName}. Choose a unique preset id.`);
|
|
2280
2759
|
}
|
|
2281
2760
|
const next: ModelsConfig = {
|
|
@@ -2401,6 +2880,8 @@ export class ModelRegistry {
|
|
|
2401
2880
|
async #refreshRuntimeDiscoveries(
|
|
2402
2881
|
strategy: ModelRefreshStrategy,
|
|
2403
2882
|
providerFilter?: ReadonlySet<string>,
|
|
2883
|
+
refreshGeneration = this.#catalogRefreshGeneration,
|
|
2884
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2404
2885
|
): Promise<void> {
|
|
2405
2886
|
const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
|
|
2406
2887
|
const selectedDiscoverableProviders = (
|
|
@@ -2412,12 +2893,22 @@ export class ModelRegistry {
|
|
|
2412
2893
|
selectedDiscoverableProviders.length === 0
|
|
2413
2894
|
? Promise.resolve([] as ConfiguredDiscoveryResult[])
|
|
2414
2895
|
: Promise.all(
|
|
2415
|
-
selectedDiscoverableProviders.map(provider =>
|
|
2896
|
+
selectedDiscoverableProviders.map(provider =>
|
|
2897
|
+
this.#discoverProviderModels(provider, strategy, providerRefresh),
|
|
2898
|
+
),
|
|
2416
2899
|
);
|
|
2417
2900
|
const [configuredDiscoveryResults, builtInDiscovered] = await Promise.all([
|
|
2418
2901
|
configuredDiscoveriesPromise,
|
|
2419
|
-
this.#discoverBuiltInProviderModels(strategy, providerFilter),
|
|
2902
|
+
this.#discoverBuiltInProviderModels(strategy, providerFilter, providerRefresh),
|
|
2420
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
|
+
}
|
|
2421
2912
|
const currentConfiguredDiscoveryResults = configuredDiscoveryResults.map(result => {
|
|
2422
2913
|
const providerConfig = selectedDiscoverableProviders.find(provider => provider.provider === result.provider);
|
|
2423
2914
|
const current =
|
|
@@ -2584,6 +3075,7 @@ export class ModelRegistry {
|
|
|
2584
3075
|
async #discoverProviderModels(
|
|
2585
3076
|
providerConfig: DiscoveryProviderConfig,
|
|
2586
3077
|
strategy: ModelRefreshStrategy,
|
|
3078
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2587
3079
|
): Promise<ConfiguredDiscoveryResult> {
|
|
2588
3080
|
const provider = providerConfig.provider;
|
|
2589
3081
|
const preflightEpoch = this.#optionalAuthPreflightEpoch;
|
|
@@ -2598,7 +3090,8 @@ export class ModelRegistry {
|
|
|
2598
3090
|
let preflightCompleted = false;
|
|
2599
3091
|
const optionalAuth = this.#optionalAuthProviders.has(provider);
|
|
2600
3092
|
const shouldPreflightAuth = optionalAuth
|
|
2601
|
-
? this.authStorage.has(provider) ||
|
|
3093
|
+
? this.authStorage.has(provider) ||
|
|
3094
|
+
this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
|
|
2602
3095
|
: !this.#isCredentiallessProvider(provider);
|
|
2603
3096
|
let preflightAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
|
|
2604
3097
|
let preflightOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
|
|
@@ -2639,7 +3132,11 @@ export class ModelRegistry {
|
|
|
2639
3132
|
preflightCompleted = true;
|
|
2640
3133
|
if (!preflightFailed && optionalAuth && isCurrentPreflight()) {
|
|
2641
3134
|
this.#providerEvidenceApiKeys.set(provider, apiKey);
|
|
2642
|
-
const authGeneration = this.authStorage.getProviderEvidenceGeneration(
|
|
3135
|
+
const authGeneration = this.authStorage.getProviderEvidenceGeneration(
|
|
3136
|
+
provider,
|
|
3137
|
+
apiKey,
|
|
3138
|
+
this.#authStorageConfigOwner,
|
|
3139
|
+
);
|
|
2643
3140
|
if (apiKey === undefined) this.#credentiallessAuthFallbackProviders.set(provider, authGeneration);
|
|
2644
3141
|
else this.#credentiallessAuthFallbackProviders.delete(provider);
|
|
2645
3142
|
}
|
|
@@ -2688,6 +3185,14 @@ export class ModelRegistry {
|
|
|
2688
3185
|
const isCurrentEndpoint = () =>
|
|
2689
3186
|
endpoint ===
|
|
2690
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));
|
|
2691
3196
|
const evidence = this.#configuredDiscoveryEvidence.get(provider);
|
|
2692
3197
|
const cacheDynamicModelProvenance = fingerprintConfiguredDiscoveryRequestShape(
|
|
2693
3198
|
effectiveProviderConfig,
|
|
@@ -2736,7 +3241,7 @@ export class ModelRegistry {
|
|
|
2736
3241
|
),
|
|
2737
3242
|
getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
|
|
2738
3243
|
cacheDynamicModelProvenance: cacheLookupProvenance,
|
|
2739
|
-
canPublishCache: isCurrentEndpoint,
|
|
3244
|
+
canPublishCache: () => isCurrentEndpoint() && isCurrentProviderRefresh(),
|
|
2740
3245
|
});
|
|
2741
3246
|
const authGeneration =
|
|
2742
3247
|
mergeInput.authGeneration ??
|
|
@@ -2792,7 +3297,7 @@ export class ModelRegistry {
|
|
|
2792
3297
|
return {
|
|
2793
3298
|
...providerConfig,
|
|
2794
3299
|
baseUrl: effectiveBaseUrl,
|
|
2795
|
-
headers:
|
|
3300
|
+
headers: mergeCaseInsensitiveHeaders(providerConfig.headers, override?.headers),
|
|
2796
3301
|
compat: override?.compat ? mergeCompat(providerConfig.compat, override.compat) : providerConfig.compat,
|
|
2797
3302
|
requestTransform: mergeRequestTransform(providerConfig.requestTransform, override?.requestTransform),
|
|
2798
3303
|
cacheRetention: override?.cacheRetention ?? providerConfig.cacheRetention,
|
|
@@ -2822,6 +3327,7 @@ export class ModelRegistry {
|
|
|
2822
3327
|
async #discoverBuiltInProviderModels(
|
|
2823
3328
|
strategy: ModelRefreshStrategy,
|
|
2824
3329
|
providerFilter?: ReadonlySet<string>,
|
|
3330
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2825
3331
|
): Promise<Model<Api>[]> {
|
|
2826
3332
|
// Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
|
|
2827
3333
|
const configuredDiscoveryProviders = new Set(this.#discoveryManager.providers.map(p => p.provider));
|
|
@@ -2839,7 +3345,7 @@ export class ModelRegistry {
|
|
|
2839
3345
|
return [];
|
|
2840
3346
|
}
|
|
2841
3347
|
const discoveries = await Promise.all(
|
|
2842
|
-
managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy)),
|
|
3348
|
+
managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy, providerRefresh)),
|
|
2843
3349
|
);
|
|
2844
3350
|
return discoveries.flat();
|
|
2845
3351
|
}
|
|
@@ -2874,7 +3380,12 @@ export class ModelRegistry {
|
|
|
2874
3380
|
providerId: "openai-codex",
|
|
2875
3381
|
resolveKey: value => value,
|
|
2876
3382
|
createOptions: accessToken => {
|
|
2877
|
-
const accountId = resolveOAuthAccountIdForAccessToken(
|
|
3383
|
+
const accountId = resolveOAuthAccountIdForAccessToken(
|
|
3384
|
+
this.authStorage,
|
|
3385
|
+
"openai-codex",
|
|
3386
|
+
accessToken,
|
|
3387
|
+
this.#authStorageConfigOwner,
|
|
3388
|
+
);
|
|
2878
3389
|
return openaiCodexModelManagerOptions({
|
|
2879
3390
|
accessToken,
|
|
2880
3391
|
accountId,
|
|
@@ -2967,6 +3478,7 @@ export class ModelRegistry {
|
|
|
2967
3478
|
async #discoverWithModelManager(
|
|
2968
3479
|
{ options, authGeneration, apiKey, endpoint, endpointContainsUserinfo }: ModelManagerDiscoveryOptions,
|
|
2969
3480
|
strategy: ModelRefreshStrategy,
|
|
3481
|
+
providerRefresh?: ProviderRefreshFence,
|
|
2970
3482
|
): Promise<Model<Api>[]> {
|
|
2971
3483
|
const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
|
|
2972
3484
|
this.#descriptorDiscoveryGenerations.set(options.providerId, generation);
|
|
@@ -2987,18 +3499,34 @@ export class ModelRegistry {
|
|
|
2987
3499
|
// stale fallback, and online-if-uncached reuse all fail closed.
|
|
2988
3500
|
const cacheDynamicModelProvenance =
|
|
2989
3501
|
reusableCacheProvenance ?? `gajae:non-cacheable-endpoint:${crypto.randomUUID()}`;
|
|
2990
|
-
const
|
|
3502
|
+
const isCurrentDiscoveryContext = () =>
|
|
2991
3503
|
(this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
|
|
2992
3504
|
this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
|
|
2993
3505
|
(endpoint ===
|
|
2994
3506
|
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(options.providerId) ?? "") ||
|
|
2995
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()));
|
|
2996
3524
|
try {
|
|
2997
3525
|
const manager = createModelManager({
|
|
2998
3526
|
...options,
|
|
2999
3527
|
cacheDbPath: this.#cacheDbPath,
|
|
3000
3528
|
cacheDynamicModelProvenance,
|
|
3001
|
-
canPublishCache
|
|
3529
|
+
canPublishCache,
|
|
3002
3530
|
...(options.fetchDynamicModels
|
|
3003
3531
|
? {
|
|
3004
3532
|
fetchDynamicModels: async () => {
|
|
@@ -3159,8 +3687,15 @@ export class ModelRegistry {
|
|
|
3159
3687
|
discoveryApiKey ??
|
|
3160
3688
|
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
3161
3689
|
? kNoAuth
|
|
3162
|
-
: await this.authStorage.getApiKey(providerConfig.provider
|
|
3163
|
-
|
|
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
|
+
) {
|
|
3164
3699
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
3165
3700
|
}
|
|
3166
3701
|
const response = await fetch(tagsUrl, {
|
|
@@ -3239,8 +3774,15 @@ export class ModelRegistry {
|
|
|
3239
3774
|
discoveryApiKey ??
|
|
3240
3775
|
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
3241
3776
|
? kNoAuth
|
|
3242
|
-
: await this.authStorage.getApiKey(providerConfig.provider
|
|
3243
|
-
|
|
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
|
+
) {
|
|
3244
3786
|
requestHeaders.Authorization = `Bearer ${apiKey}`;
|
|
3245
3787
|
}
|
|
3246
3788
|
|
|
@@ -3361,12 +3903,16 @@ export class ModelRegistry {
|
|
|
3361
3903
|
discoveryApiKey ??
|
|
3362
3904
|
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
3363
3905
|
? kNoAuth
|
|
3364
|
-
: await this.authStorage.getApiKey(providerConfig.provider, undefined, {
|
|
3906
|
+
: await this.authStorage.getApiKey(providerConfig.provider, undefined, {
|
|
3907
|
+
baseUrl,
|
|
3908
|
+
owner: this.#authStorageConfigOwner,
|
|
3909
|
+
}));
|
|
3365
3910
|
if (
|
|
3366
3911
|
apiKey &&
|
|
3367
3912
|
apiKey !== DEFAULT_LOCAL_TOKEN &&
|
|
3368
3913
|
!isVllmNoAuthToken(providerConfig.provider, apiKey) &&
|
|
3369
|
-
apiKey !== kNoAuth
|
|
3914
|
+
apiKey !== kNoAuth &&
|
|
3915
|
+
headerValue(requestHeaders, "Authorization") === undefined
|
|
3370
3916
|
) {
|
|
3371
3917
|
requestHeaders.Authorization = `Bearer ${apiKey}`;
|
|
3372
3918
|
}
|
|
@@ -3508,7 +4054,11 @@ export class ModelRegistry {
|
|
|
3508
4054
|
try {
|
|
3509
4055
|
fallbackMatchesCurrentEvidence =
|
|
3510
4056
|
fallbackEvidenceGeneration ===
|
|
3511
|
-
this.authStorage.getProviderEvidenceGeneration(
|
|
4057
|
+
this.authStorage.getProviderEvidenceGeneration(
|
|
4058
|
+
provider,
|
|
4059
|
+
this.#providerEvidenceApiKeys.get(provider),
|
|
4060
|
+
this.#authStorageConfigOwner,
|
|
4061
|
+
);
|
|
3512
4062
|
} catch {
|
|
3513
4063
|
// AuthStorage may be unavailable while a registry is being torn down.
|
|
3514
4064
|
}
|
|
@@ -3516,7 +4066,8 @@ export class ModelRegistry {
|
|
|
3516
4066
|
return (
|
|
3517
4067
|
this.#keylessProviders.has(provider) &&
|
|
3518
4068
|
(!this.#optionalAuthProviders.has(provider) ||
|
|
3519
|
-
(!this.authStorage.hasAuth(provider
|
|
4069
|
+
(!this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner }) &&
|
|
4070
|
+
!this.authStorage.has(provider)) ||
|
|
3520
4071
|
fallbackMatchesCurrentEvidence)
|
|
3521
4072
|
);
|
|
3522
4073
|
}
|
|
@@ -3527,6 +4078,7 @@ export class ModelRegistry {
|
|
|
3527
4078
|
return this.authStorage.getProviderEvidenceGeneration(
|
|
3528
4079
|
provider,
|
|
3529
4080
|
resolvedApiKey ?? this.#providerEvidenceApiKeys.get(provider),
|
|
4081
|
+
this.#authStorageConfigOwner,
|
|
3530
4082
|
);
|
|
3531
4083
|
}
|
|
3532
4084
|
#normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
|
|
@@ -3564,7 +4116,7 @@ export class ModelRegistry {
|
|
|
3564
4116
|
|
|
3565
4117
|
#resolveProviderOverride(
|
|
3566
4118
|
provider: string,
|
|
3567
|
-
overrides:
|
|
4119
|
+
overrides: ReadonlyMap<string, ProviderOverride> = this.#providerOverrides,
|
|
3568
4120
|
): ProviderOverride | undefined {
|
|
3569
4121
|
const explicitOverride = overrides.get(provider);
|
|
3570
4122
|
if (explicitOverride?.baseUrl) {
|
|
@@ -3591,24 +4143,19 @@ export class ModelRegistry {
|
|
|
3591
4143
|
|
|
3592
4144
|
#mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
|
|
3593
4145
|
return {
|
|
4146
|
+
api: override.api ?? baseOverride?.api,
|
|
3594
4147
|
baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
|
|
3595
4148
|
apiKey: override.apiKey ?? baseOverride?.apiKey,
|
|
3596
4149
|
authHeader: override.authHeader ?? baseOverride?.authHeader,
|
|
3597
|
-
headers:
|
|
4150
|
+
headers: mergeCaseInsensitiveHeaders(baseOverride?.headers, override.headers),
|
|
3598
4151
|
compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
|
|
3599
4152
|
transport: override.transport ?? baseOverride?.transport,
|
|
3600
4153
|
requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
|
|
3601
4154
|
cacheRetention: override.cacheRetention ?? baseOverride?.cacheRetention,
|
|
4155
|
+
isOAuth: override.isOAuth ?? baseOverride?.isOAuth,
|
|
3602
4156
|
};
|
|
3603
4157
|
}
|
|
3604
|
-
#applyProviderTransportOverride<
|
|
3605
|
-
T extends {
|
|
3606
|
-
baseUrl?: string;
|
|
3607
|
-
headers?: Record<string, string>;
|
|
3608
|
-
compat?: Model<Api>["compat"];
|
|
3609
|
-
cacheRetention?: CacheRetention;
|
|
3610
|
-
},
|
|
3611
|
-
>(
|
|
4158
|
+
#applyProviderTransportOverride<T extends Model<Api>>(
|
|
3612
4159
|
entry: T,
|
|
3613
4160
|
override: Pick<
|
|
3614
4161
|
ProviderOverride,
|
|
@@ -3622,12 +4169,39 @@ export class ModelRegistry {
|
|
|
3622
4169
|
| "cacheRetention"
|
|
3623
4170
|
>,
|
|
3624
4171
|
): T {
|
|
3625
|
-
const
|
|
3626
|
-
override.headers ? { ...entry.headers, ...override.headers } : entry.headers
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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 = {
|
|
3631
4205
|
...entry,
|
|
3632
4206
|
compat: mergeProviderCompat(entry.compat, override.compat),
|
|
3633
4207
|
baseUrl: override.baseUrl ?? entry.baseUrl,
|
|
@@ -3641,6 +4215,21 @@ export class ModelRegistry {
|
|
|
3641
4215
|
),
|
|
3642
4216
|
cacheRetention: entry.cacheRetention ?? override.cacheRetention,
|
|
3643
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;
|
|
3644
4233
|
}
|
|
3645
4234
|
#applyRuntimeProviderOverride(model: Model<Api>, override: ProviderOverride): Model<Api> {
|
|
3646
4235
|
const withTransportOverride = this.#applyProviderTransportOverride(
|
|
@@ -3653,26 +4242,53 @@ export class ModelRegistry {
|
|
|
3653
4242
|
? withTransportOverride
|
|
3654
4243
|
: { ...withTransportOverride, ...(sanitizedBaseUrl === undefined ? {} : { baseUrl: sanitizedBaseUrl }) };
|
|
3655
4244
|
const modelCompat = this.#modelOverrides.get(model.provider.toLowerCase())?.get(model.id.toLowerCase())?.compat;
|
|
3656
|
-
|
|
4245
|
+
const result = enrichModelThinking(
|
|
3657
4246
|
modelCompat
|
|
3658
4247
|
? { ...sanitizedTransport, compat: mergeCompat(sanitizedTransport.compat, modelCompat) }
|
|
3659
4248
|
: sanitizedTransport,
|
|
3660
4249
|
);
|
|
4250
|
+
const generated = this.#generatedAuthHeaders.get(sanitizedTransport);
|
|
4251
|
+
if (generated) this.#generatedAuthHeaders.set(result, generated);
|
|
4252
|
+
return result;
|
|
3661
4253
|
}
|
|
3662
4254
|
#applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
|
|
3663
4255
|
if (this.#runtimeProviderOverrides.size === 0) return models;
|
|
3664
4256
|
return models.map(model => {
|
|
3665
4257
|
const override = this.#runtimeProviderOverrides.get(model.provider);
|
|
3666
4258
|
if (!override) return model;
|
|
3667
|
-
|
|
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
|
+
);
|
|
3668
4266
|
});
|
|
3669
4267
|
}
|
|
3670
4268
|
#finalizeModels(models: Model<Api>[]): Model<Api>[] {
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
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;
|
|
3676
4292
|
}
|
|
3677
4293
|
#restoreDeclaredThinking(model: Model<Api>): Model<Api> {
|
|
3678
4294
|
const overrideThinking = this.#modelOverrides
|
|
@@ -3811,7 +4427,10 @@ export class ModelRegistry {
|
|
|
3811
4427
|
: undefined;
|
|
3812
4428
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
3813
4429
|
if (resolved) {
|
|
3814
|
-
this.authStorage.setConfigApiKey(providerName, resolved, {
|
|
4430
|
+
this.authStorage.setConfigApiKey(providerName, resolved, {
|
|
4431
|
+
envSourced: !providerConfig.apiKey,
|
|
4432
|
+
owner: this.#authStorageConfigOwner,
|
|
4433
|
+
});
|
|
3815
4434
|
}
|
|
3816
4435
|
}
|
|
3817
4436
|
for (const modelDef of modelDefs) {
|
|
@@ -3843,8 +4462,8 @@ export class ModelRegistry {
|
|
|
3843
4462
|
}
|
|
3844
4463
|
|
|
3845
4464
|
/**
|
|
3846
|
-
* Get all models (
|
|
3847
|
-
* If models.
|
|
4465
|
+
* Get all models (embedded + accepted registry + custom).
|
|
4466
|
+
* If models.yml had errors, returns embedded plus accepted registry models.
|
|
3848
4467
|
*/
|
|
3849
4468
|
getAll(): Model<Api>[] {
|
|
3850
4469
|
return this.#models;
|
|
@@ -3859,7 +4478,8 @@ export class ModelRegistry {
|
|
|
3859
4478
|
const disabled = disabledProviders ?? getDisabledProviderIdsFromSettings(this.#settings);
|
|
3860
4479
|
return (
|
|
3861
4480
|
!disabled.has(model.provider) &&
|
|
3862
|
-
(this.#keylessProviders.has(model.provider) ||
|
|
4481
|
+
(this.#keylessProviders.has(model.provider) ||
|
|
4482
|
+
this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner }))
|
|
3863
4483
|
);
|
|
3864
4484
|
}
|
|
3865
4485
|
/** Per-query precomputed variant filter inputs; plan fields are authoritative when provided. */
|
|
@@ -3913,7 +4533,9 @@ export class ModelRegistry {
|
|
|
3913
4533
|
* 6. Unknown (no credential surface)
|
|
3914
4534
|
*/
|
|
3915
4535
|
#effectiveProviderAuth(provider: string, sessionId?: string): EffectiveProviderAuth {
|
|
3916
|
-
const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId
|
|
4536
|
+
const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId, {
|
|
4537
|
+
owner: this.#authStorageConfigOwner,
|
|
4538
|
+
});
|
|
3917
4539
|
if (credentialType === "api_key") return "key";
|
|
3918
4540
|
if (credentialType === "oauth") return "oauth";
|
|
3919
4541
|
if (this.#keylessProviders.has(provider)) return "keyless";
|
|
@@ -4263,9 +4885,10 @@ export class ModelRegistry {
|
|
|
4263
4885
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
4264
4886
|
*/
|
|
4265
4887
|
getAvailable(): Model<Api>[] {
|
|
4888
|
+
this.#synchronizeEnvironmentCredentials();
|
|
4266
4889
|
const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
|
|
4267
4890
|
const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
|
|
4268
|
-
const envFingerprint = envAvailabilityFingerprint();
|
|
4891
|
+
const envFingerprint = envAvailabilityFingerprint(this.#configuredApiKeyEnvNames);
|
|
4269
4892
|
if (
|
|
4270
4893
|
this.#availableModelsCache &&
|
|
4271
4894
|
this.#availableModelsDisabledProviders === disabledProviderKey &&
|
|
@@ -4279,6 +4902,19 @@ export class ModelRegistry {
|
|
|
4279
4902
|
return this.#availableModelsCache;
|
|
4280
4903
|
}
|
|
4281
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
|
+
|
|
4282
4918
|
/**
|
|
4283
4919
|
* Get authenticated models, excluding bundled entries that a fresh provider
|
|
4284
4920
|
* catalog has positively shown to be unavailable. Bundled entries remain
|
|
@@ -4348,7 +4984,10 @@ export class ModelRegistry {
|
|
|
4348
4984
|
|
|
4349
4985
|
#activeConnectionKind(model: Model<Api>): ActiveProviderDescriptor["connectionKind"] | undefined {
|
|
4350
4986
|
const evidence = this.#providerActivity.get(model.provider);
|
|
4351
|
-
if (
|
|
4987
|
+
if (
|
|
4988
|
+
!this.#isCredentiallessProvider(model.provider) &&
|
|
4989
|
+
this.authStorage.hasUsableAuth(model.provider, { owner: this.#authStorageConfigOwner })
|
|
4990
|
+
) {
|
|
4352
4991
|
if (!evidence) return undefined;
|
|
4353
4992
|
const discoveryOnly =
|
|
4354
4993
|
!evidence.staticConfigured &&
|
|
@@ -4423,14 +5062,20 @@ export class ModelRegistry {
|
|
|
4423
5062
|
* as providers with stored credentials. See issue #993.
|
|
4424
5063
|
*/
|
|
4425
5064
|
hasConfiguredAuth(model: Model<Api>): boolean {
|
|
4426
|
-
return
|
|
5065
|
+
return (
|
|
5066
|
+
this.#keylessProviders.has(model.provider) ||
|
|
5067
|
+
this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner })
|
|
5068
|
+
);
|
|
4427
5069
|
}
|
|
4428
5070
|
|
|
4429
5071
|
/**
|
|
4430
5072
|
* Check whether auth is configured for a provider.
|
|
4431
5073
|
*/
|
|
4432
5074
|
hasConfiguredProviderAuth(provider: string): boolean {
|
|
4433
|
-
return
|
|
5075
|
+
return (
|
|
5076
|
+
this.#keylessProviders.has(provider) ||
|
|
5077
|
+
this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
|
|
5078
|
+
);
|
|
4434
5079
|
}
|
|
4435
5080
|
|
|
4436
5081
|
isCredentiallessProvider(provider: string): boolean {
|
|
@@ -4465,11 +5110,24 @@ export class ModelRegistry {
|
|
|
4465
5110
|
);
|
|
4466
5111
|
}
|
|
4467
5112
|
|
|
5113
|
+
/** Opaque owner token for registry-scoped AuthStorage reads. */
|
|
5114
|
+
getAuthStorageOwner(): object {
|
|
5115
|
+
return this.#authStorageConfigOwner;
|
|
5116
|
+
}
|
|
5117
|
+
|
|
4468
5118
|
getProviderWebSearchMode(provider: string): WebSearchMode | undefined {
|
|
4469
5119
|
return this.#providerWebSearchModes.get(provider);
|
|
4470
5120
|
}
|
|
4471
5121
|
|
|
4472
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);
|
|
4473
5131
|
return {
|
|
4474
5132
|
provider: model.provider,
|
|
4475
5133
|
modelId: model.id,
|
|
@@ -4477,6 +5135,15 @@ export class ModelRegistry {
|
|
|
4477
5135
|
api: model.api,
|
|
4478
5136
|
baseUrl: model.baseUrl,
|
|
4479
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
|
+
},
|
|
4480
5147
|
webSearch: this.getProviderWebSearchMode(model.provider),
|
|
4481
5148
|
};
|
|
4482
5149
|
}
|
|
@@ -4507,15 +5174,18 @@ export class ModelRegistry {
|
|
|
4507
5174
|
} = {},
|
|
4508
5175
|
): Promise<string | undefined> {
|
|
4509
5176
|
this.#refreshRotatingConfigApiKey(model.provider);
|
|
4510
|
-
|
|
5177
|
+
const apiKey = await this.#getApiKeyOrNoAuth(model.provider, () =>
|
|
4511
5178
|
this.authStorage.getApiKey(model.provider, sessionId, {
|
|
4512
5179
|
baseUrl: model.baseUrl,
|
|
4513
5180
|
modelId: model.id,
|
|
4514
5181
|
credentialSelector: options.credentialSelector,
|
|
4515
5182
|
preferredCredentialSelector: options.preferredCredentialSelector,
|
|
5183
|
+
owner: this.#authStorageConfigOwner,
|
|
4516
5184
|
signal: options.signal,
|
|
4517
5185
|
}),
|
|
4518
5186
|
);
|
|
5187
|
+
this.#applyEffectiveAuthHeader(model, apiKey);
|
|
5188
|
+
return apiKey;
|
|
4519
5189
|
}
|
|
4520
5190
|
|
|
4521
5191
|
/**
|
|
@@ -4532,28 +5202,143 @@ export class ModelRegistry {
|
|
|
4532
5202
|
} = {},
|
|
4533
5203
|
): Promise<string | undefined> {
|
|
4534
5204
|
this.#refreshRotatingConfigApiKey(provider);
|
|
4535
|
-
|
|
5205
|
+
const apiKey = await this.#getApiKeyOrNoAuth(provider, () =>
|
|
4536
5206
|
this.authStorage.getApiKey(provider, sessionId, {
|
|
4537
5207
|
baseUrl,
|
|
4538
5208
|
credentialSelector: options.credentialSelector,
|
|
4539
5209
|
preferredCredentialSelector: options.preferredCredentialSelector,
|
|
5210
|
+
owner: this.#authStorageConfigOwner,
|
|
4540
5211
|
signal: options.signal,
|
|
4541
5212
|
}),
|
|
4542
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;
|
|
4543
5223
|
}
|
|
4544
5224
|
|
|
4545
|
-
#refreshRotatingConfigApiKey(provider: string): void {
|
|
4546
|
-
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);
|
|
4547
5230
|
if (!envName) return;
|
|
4548
5231
|
const resolved = $rotatingCredentialEnv(envName);
|
|
4549
|
-
|
|
5232
|
+
const previous = runtimeOwned
|
|
5233
|
+
? this.#runtimeProviderResolvedApiKeys.get(provider)
|
|
5234
|
+
: this.#customProviderApiKeys.get(provider);
|
|
5235
|
+
if (resolved === previous && (!runtimeOwned || this.#runtimeProviderCredentialInstalled.has(provider))) return;
|
|
4550
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
|
+
}
|
|
4551
5244
|
this.#customProviderApiKeys.delete(provider);
|
|
4552
|
-
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))
|
|
4553
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 });
|
|
4554
5341
|
}
|
|
4555
|
-
this.#customProviderApiKeys.set(provider, resolved);
|
|
4556
|
-
this.authStorage.setConfigApiKey(provider, resolved, { envSourced: true });
|
|
4557
5342
|
}
|
|
4558
5343
|
|
|
4559
5344
|
async #peekApiKeyForProvider(
|
|
@@ -4569,16 +5354,21 @@ export class ModelRegistry {
|
|
|
4569
5354
|
return kNoAuth;
|
|
4570
5355
|
}
|
|
4571
5356
|
try {
|
|
4572
|
-
this.authStorage.getProviderEvidenceGeneration(provider);
|
|
5357
|
+
this.authStorage.getProviderEvidenceGeneration(provider, undefined, this.#authStorageConfigOwner);
|
|
4573
5358
|
} catch {
|
|
4574
5359
|
return undefined;
|
|
4575
5360
|
}
|
|
4576
5361
|
if (options.refreshOAuth && this.authStorage.hasOAuth(provider)) {
|
|
4577
|
-
return this.authStorage.getApiKey(provider, undefined, {
|
|
5362
|
+
return this.authStorage.getApiKey(provider, undefined, {
|
|
5363
|
+
baseUrl: options.baseUrl,
|
|
5364
|
+
owner: this.#authStorageConfigOwner,
|
|
5365
|
+
});
|
|
4578
5366
|
}
|
|
4579
5367
|
return options.ignoreCredentiallessFallback
|
|
4580
|
-
? this.authStorage.peekApiKey(provider)
|
|
4581
|
-
: this.#getApiKeyOrNoAuth(provider, () =>
|
|
5368
|
+
? this.authStorage.peekApiKey(provider, { owner: this.#authStorageConfigOwner })
|
|
5369
|
+
: this.#getApiKeyOrNoAuth(provider, () =>
|
|
5370
|
+
this.authStorage.peekApiKey(provider, { owner: this.#authStorageConfigOwner }),
|
|
5371
|
+
);
|
|
4582
5372
|
}
|
|
4583
5373
|
|
|
4584
5374
|
/**
|
|
@@ -4604,9 +5394,13 @@ export class ModelRegistry {
|
|
|
4604
5394
|
|
|
4605
5395
|
#clearRuntimeProviderState(providerName: string): void {
|
|
4606
5396
|
this.#runtimeProviderApiKeys.delete(providerName);
|
|
5397
|
+
this.#runtimeProviderResolvedApiKeys.delete(providerName);
|
|
5398
|
+
this.#runtimeProviderCredentialInstalled.delete(providerName);
|
|
5399
|
+
this.#runtimeProviderApiKeyEnvNames.delete(providerName);
|
|
4607
5400
|
this.#runtimeProviderOverrides.delete(providerName);
|
|
5401
|
+
this.#runtimeProviderAuthHeaders.delete(providerName);
|
|
4608
5402
|
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
|
|
4609
|
-
this.authStorage.removeConfigApiKey(providerName);
|
|
5403
|
+
this.authStorage.removeConfigApiKey(providerName, this.#authStorageConfigOwner);
|
|
4610
5404
|
this.#clearDescriptorDiscoveryEvidence(providerName);
|
|
4611
5405
|
}
|
|
4612
5406
|
|
|
@@ -4692,7 +5486,6 @@ export class ModelRegistry {
|
|
|
4692
5486
|
sourceId,
|
|
4693
5487
|
});
|
|
4694
5488
|
}
|
|
4695
|
-
|
|
4696
5489
|
let sourceHandoff = false;
|
|
4697
5490
|
if (sourceId) {
|
|
4698
5491
|
this.#registeredProviderSources.add(sourceId);
|
|
@@ -4715,14 +5508,89 @@ export class ModelRegistry {
|
|
|
4715
5508
|
this.#lastStaticLoadMtime = null;
|
|
4716
5509
|
this.#staticModelsLoaded = false;
|
|
4717
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);
|
|
4718
5528
|
}
|
|
4719
5529
|
|
|
4720
5530
|
if (config.apiKey) {
|
|
4721
|
-
|
|
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);
|
|
4722
5536
|
// Persist runtime API keys so they survive #reloadStaticModels() cycles
|
|
4723
5537
|
this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
|
|
4724
|
-
|
|
4725
|
-
|
|
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);
|
|
4726
5594
|
}
|
|
4727
5595
|
|
|
4728
5596
|
if (config.models && config.models.length > 0) {
|
|
@@ -4734,7 +5602,7 @@ export class ModelRegistry {
|
|
|
4734
5602
|
config.baseUrl!,
|
|
4735
5603
|
config.api,
|
|
4736
5604
|
config.headers,
|
|
4737
|
-
config.apiKey,
|
|
5605
|
+
config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
|
|
4738
5606
|
config.authHeader,
|
|
4739
5607
|
config.compat,
|
|
4740
5608
|
config.requestTransform,
|
|
@@ -4756,6 +5624,29 @@ export class ModelRegistry {
|
|
|
4756
5624
|
for (const overlay of newOverlays) {
|
|
4757
5625
|
nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
|
|
4758
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
|
+
}
|
|
4759
5650
|
const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
|
|
4760
5651
|
const withRuntimeTransportOverride = runtimeTransportOverride
|
|
4761
5652
|
? nextModels.map(model => {
|
|
@@ -4766,7 +5657,9 @@ export class ModelRegistry {
|
|
|
4766
5657
|
const withModelOverrides = this.#applyProviderModelOverrides(providerName, withRuntimeTransportOverride);
|
|
4767
5658
|
|
|
4768
5659
|
if (config.oauth?.modifyModels) {
|
|
4769
|
-
const credential = this.authStorage.getOAuthCredential(providerName
|
|
5660
|
+
const credential = this.authStorage.getOAuthCredential(providerName, undefined, {
|
|
5661
|
+
owner: this.#authStorageConfigOwner,
|
|
5662
|
+
});
|
|
4770
5663
|
if (credential) {
|
|
4771
5664
|
this.#models = this.#finalizeModels(config.oauth.modifyModels(withModelOverrides, credential));
|
|
4772
5665
|
this.#rebuildCanonicalIndex();
|
|
@@ -4791,9 +5684,10 @@ export class ModelRegistry {
|
|
|
4791
5684
|
config.transport !== undefined
|
|
4792
5685
|
) {
|
|
4793
5686
|
const transportOverride = {
|
|
5687
|
+
api: config.api,
|
|
4794
5688
|
baseUrl: config.baseUrl,
|
|
4795
5689
|
headers: config.headers,
|
|
4796
|
-
apiKey: config.apiKey,
|
|
5690
|
+
apiKey: config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
|
|
4797
5691
|
authHeader: config.authHeader,
|
|
4798
5692
|
compat: config.compat,
|
|
4799
5693
|
requestTransform: config.requestTransform,
|
|
@@ -4808,6 +5702,11 @@ export class ModelRegistry {
|
|
|
4808
5702
|
if (m.provider !== providerName) return m;
|
|
4809
5703
|
return this.#applyRuntimeProviderOverride(m, transportOverride);
|
|
4810
5704
|
});
|
|
5705
|
+
if (config.authHeader === false) {
|
|
5706
|
+
for (const model of this.#models) {
|
|
5707
|
+
if (model.provider === providerName) this.#applyEffectiveAuthHeader(model, undefined);
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
4811
5710
|
this.#rebuildCanonicalIndex();
|
|
4812
5711
|
this.#rebuildProviderActivity();
|
|
4813
5712
|
}
|