@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.
Files changed (155) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +2 -0
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +96 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  22. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  23. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  25. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/main.d.ts +3 -1
  28. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  29. package/dist/types/modes/jobs-observer.d.ts +36 -0
  30. package/dist/types/sdk/broker/broker.d.ts +3 -0
  31. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  32. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  33. package/dist/types/sdk/client/discovery.d.ts +1 -0
  34. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  35. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  36. package/dist/types/sdk/host/session-runtime.d.ts +15 -1
  37. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  38. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  39. package/dist/types/sdk/router/session-router.d.ts +9 -0
  40. package/dist/types/session/account-inventory.d.ts +1 -0
  41. package/dist/types/session/agent-session.d.ts +19 -7
  42. package/dist/types/session/fold-coordinator.d.ts +201 -0
  43. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  44. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  45. package/dist/types/telemetry/control.d.ts +11 -0
  46. package/dist/types/telemetry/events.d.ts +20 -0
  47. package/dist/types/telemetry/index.d.ts +3 -0
  48. package/dist/types/telemetry/transport.d.ts +22 -0
  49. package/dist/types/tools/bash-interactive.d.ts +23 -0
  50. package/dist/types/tools/index.d.ts +6 -5
  51. package/dist/types/web/search/providers/base.d.ts +9 -4
  52. package/dist/types/web/search/providers/xai.d.ts +2 -1
  53. package/dist/types/web/search/types.d.ts +14 -0
  54. package/package.json +10 -7
  55. package/scripts/generate-hotkeys-docs.ts +4 -0
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  57. package/src/async/job-manager.ts +671 -39
  58. package/src/cli/auth-broker-cli.ts +40 -12
  59. package/src/cli/auth-gateway-cli.ts +273 -97
  60. package/src/cli/config-cli.ts +2 -2
  61. package/src/cli/fast-help.ts +3 -0
  62. package/src/cli/model-presets-cli.ts +100 -0
  63. package/src/cli/update-cli.ts +40 -5
  64. package/src/cli.ts +6 -2
  65. package/src/commands/auth-gateway.ts +12 -8
  66. package/src/commands/gc.ts +19 -0
  67. package/src/commands/model-presets.ts +24 -0
  68. package/src/config/autorouting-tier-map.ts +32 -0
  69. package/src/config/file-lock-gc.ts +9 -4
  70. package/src/config/file-lock.ts +775 -59
  71. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  72. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  73. package/src/config/keybindings.ts +6 -2
  74. package/src/config/model-preset-registry.ts +2799 -0
  75. package/src/config/model-profile-activation.ts +115 -28
  76. package/src/config/model-profile-contract.ts +4 -1
  77. package/src/config/model-profiles.ts +51 -9
  78. package/src/config/model-registry.ts +1021 -122
  79. package/src/config/settings-schema.ts +11 -0
  80. package/src/config/settings.ts +6 -1
  81. package/src/coordinator-mcp/model-preset.ts +32 -17
  82. package/src/coordinator-mcp/projection-scan.ts +571 -0
  83. package/src/coordinator-mcp/server.ts +19 -24
  84. package/src/coordinator-mcp/session-reaper.ts +9 -3
  85. package/src/extensibility/extensions/loader.ts +0 -1
  86. package/src/extensibility/extensions/types.ts +2 -3
  87. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  88. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  89. package/src/gjc-runtime/gc-render.ts +29 -0
  90. package/src/gjc-runtime/gc-runtime.ts +174 -11
  91. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  92. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  93. package/src/index.ts +1 -0
  94. package/src/internal-urls/docs-index.generated.ts +9 -8
  95. package/src/lsp/config.ts +4 -3
  96. package/src/lsp/path-trust.ts +12 -6
  97. package/src/main.ts +9 -2
  98. package/src/modes/acp/acp-agent.ts +11 -5
  99. package/src/modes/components/jobs-overlay-model.ts +99 -16
  100. package/src/modes/components/model-selector.ts +158 -18
  101. package/src/modes/components/status-line/segments.ts +6 -1
  102. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  103. package/src/modes/controllers/input-controller.ts +23 -6
  104. package/src/modes/interactive-mode.ts +14 -1
  105. package/src/modes/jobs-observer.ts +149 -16
  106. package/src/modes/runtime-init.ts +1 -1
  107. package/src/modes/utils/injected-user-submission.ts +15 -1
  108. package/src/prompts/tools/bash.md +2 -2
  109. package/src/prompts/tools/fold-receipt.md +3 -0
  110. package/src/prompts/tools/job.md +1 -1
  111. package/src/sdk/acp/adapter.ts +17 -2
  112. package/src/sdk/broker/broker.ts +48 -21
  113. package/src/sdk/broker/lifecycle.ts +32 -6
  114. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  115. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  116. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  117. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  118. package/src/sdk/bus/slack-conversation.ts +5 -1
  119. package/src/sdk/cli/session-cli.ts +12 -1
  120. package/src/sdk/client/discovery.ts +3 -1
  121. package/src/sdk/host/control/dispatch.ts +3 -0
  122. package/src/sdk/host/model-pin.ts +185 -25
  123. package/src/sdk/host/sdk-run-capability.ts +1 -0
  124. package/src/sdk/host/session-runtime.test.ts +371 -37
  125. package/src/sdk/host/session-runtime.ts +364 -52
  126. package/src/sdk/prompt-deadline-manager.ts +16 -1
  127. package/src/sdk/protocol/adapter-validation.ts +27 -0
  128. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  129. package/src/sdk/protocol/operation-registry.ts +4 -1
  130. package/src/sdk/router/session-router.ts +228 -96
  131. package/src/sdk/session.ts +83 -12
  132. package/src/session/account-inventory.ts +27 -11
  133. package/src/session/agent-session.ts +678 -103
  134. package/src/session/fold-coordinator.ts +433 -0
  135. package/src/session/sdk-run-capability-internal.ts +15 -0
  136. package/src/session/sdk-run-capability.ts +2 -0
  137. package/src/session/session-manager.ts +5 -4
  138. package/src/setup/provider-presets.json +1 -1
  139. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  140. package/src/task/executor.ts +19 -3
  141. package/src/telemetry/control.ts +26 -0
  142. package/src/telemetry/events.ts +142 -0
  143. package/src/telemetry/index.ts +3 -0
  144. package/src/telemetry/transport.ts +87 -0
  145. package/src/tools/bash-interactive.ts +182 -85
  146. package/src/tools/bash.ts +772 -67
  147. package/src/tools/index.ts +7 -5
  148. package/src/tools/tool-catalog.generated.ts +1 -1
  149. package/src/web/search/provider.ts +120 -11
  150. package/src/web/search/providers/anthropic.ts +89 -17
  151. package/src/web/search/providers/base.ts +9 -4
  152. package/src/web/search/providers/gemini.ts +87 -15
  153. package/src/web/search/providers/openai-compatible.ts +23 -8
  154. package/src/web/search/providers/xai.ts +98 -12
  155. 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 { type ConfigError, ConfigFile } from "./config-file";
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
- return Object.entries(process.env)
139
- .filter(
140
- ([name]) =>
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
- .sort(([left], [right]) => left.localeCompare(right))
145
- .map(([name, value]) => `${name}=${value ?? ""}`)
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 ? { ...existing.headers, ...model.headers } : model.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: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.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
- return mergeAuthHeader({ ...providerHeaders, ...modelHeaders }, authHeader, apiKeyConfig);
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
- const resolvedKey = resolveApiKeyConfig(apiKeyConfig);
1116
- return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
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 (built-in + custom from models.json).
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.#suspendRebuild();
1482
- try {
1483
- this.#reloadStaticModels();
1484
- this.#suppressedSelectors.clear();
1485
- await this.#refreshRuntimeDiscoveries(strategy);
1486
- this.#modelBindingsApplier.apply();
1487
- } finally {
1488
- this.#resumeRebuild();
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.#suspendRebuild();
1512
- try {
1513
- this.#reloadStaticModels();
1514
- for (const selector of this.#suppressedSelectors.keys()) {
1515
- if (selector.startsWith(`${providerId}/`)) {
1516
- this.#suppressedSelectors.delete(selector);
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
- await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
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(providerId, selector);
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 = resolveApiKeyConfig(apiKeyConfig);
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.authStorage.setConfigApiKey(provider, resolved);
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
- this.#modelProfiles = mergeModelProfiles(profiles);
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 resolvedDefaults = this.#mergeResolvedModels(
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 ?? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
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, { envSourced: !providerConfig.apiKey });
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 (mergeModelProfiles(current.profiles).has(normalizedName)) {
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 => this.#discoverProviderModels(provider, strategy)),
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) || this.authStorage.hasAuth(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(provider, apiKey);
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: override?.headers ? { ...providerConfig.headers, ...override.headers } : providerConfig.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(this.authStorage, "openai-codex", accessToken);
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 isCurrentDiscovery = () =>
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: isCurrentDiscovery,
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
- if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
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
- if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
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, { baseUrl }));
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(provider, this.#providerEvidenceApiKeys.get(provider));
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) && !this.authStorage.has(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: Map<string, ProviderOverride> = this.#providerOverrides,
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: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.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 headers = mergeAuthHeader(
3626
- override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
3627
- override.authHeader,
3628
- override.apiKey,
3629
- );
3630
- return {
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
- return enrichModelThinking(
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
- return this.#applyRuntimeProviderOverride(model, override);
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
- return applyFinalCodexGpt56ContextCap(
3672
- models.map(model => enrichModelThinking({ ...this.#restoreDeclaredThinking(model) })),
3673
- undefined,
3674
- this.#codexContextWindowOverrides,
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, { envSourced: !providerConfig.apiKey });
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 (built-in + custom).
3847
- * If models.json had errors, returns only built-in 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) || this.authStorage.hasAuth(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 (!this.#isCredentiallessProvider(model.provider) && this.authStorage.hasUsableAuth(model.provider)) {
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 this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider);
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 this.#keylessProviders.has(provider) || this.authStorage.hasAuth(provider);
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
- return this.#getApiKeyOrNoAuth(model.provider, () =>
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
- return this.#getApiKeyOrNoAuth(provider, () =>
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 envName = this.#customProviderApiKeyEnvNames.get(provider);
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
- if (resolved === this.#customProviderApiKeys.get(provider)) return;
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, { baseUrl: options.baseUrl });
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, () => this.authStorage.peekApiKey(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
- this.#customProviderApiKeys.set(providerName, config.apiKey);
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
- const resolved = resolveApiKeyConfig(config.apiKey);
4725
- if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
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
  }