@gajae-code/coding-agent 0.15.4 → 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +46 -2
  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 +27 -5
  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 +99 -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 +4 -4
  22. package/dist/types/extensibility/shared-events.d.ts +2 -0
  23. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  25. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  26. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/main.d.ts +3 -1
  29. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  30. package/dist/types/modes/jobs-observer.d.ts +36 -0
  31. package/dist/types/sdk/broker/broker.d.ts +3 -0
  32. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  33. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  34. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  35. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  36. package/dist/types/sdk/client/discovery.d.ts +1 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  39. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  40. package/dist/types/sdk/host/session-runtime.d.ts +19 -1
  41. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  42. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  43. package/dist/types/sdk/router/session-router.d.ts +9 -0
  44. package/dist/types/session/account-inventory.d.ts +1 -0
  45. package/dist/types/session/agent-session.d.ts +19 -5
  46. package/dist/types/session/fold-coordinator.d.ts +201 -0
  47. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  48. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  49. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  50. package/dist/types/telemetry/control.d.ts +11 -0
  51. package/dist/types/telemetry/events.d.ts +20 -0
  52. package/dist/types/telemetry/index.d.ts +3 -0
  53. package/dist/types/telemetry/transport.d.ts +22 -0
  54. package/dist/types/tools/bash-interactive.d.ts +23 -0
  55. package/dist/types/tools/index.d.ts +6 -5
  56. package/dist/types/tools/path-utils.d.ts +5 -6
  57. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  58. package/dist/types/web/search/providers/base.d.ts +9 -4
  59. package/dist/types/web/search/providers/xai.d.ts +2 -1
  60. package/dist/types/web/search/types.d.ts +14 -0
  61. package/package.json +10 -7
  62. package/scripts/generate-hotkeys-docs.ts +4 -0
  63. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  64. package/src/async/job-manager.ts +671 -39
  65. package/src/cli/auth-broker-cli.ts +40 -12
  66. package/src/cli/auth-gateway-cli.ts +273 -97
  67. package/src/cli/config-cli.ts +2 -2
  68. package/src/cli/fast-help.ts +3 -0
  69. package/src/cli/model-presets-cli.ts +100 -0
  70. package/src/cli/update-cli.ts +142 -6
  71. package/src/cli.ts +6 -2
  72. package/src/commands/auth-gateway.ts +12 -8
  73. package/src/commands/gc.ts +19 -0
  74. package/src/commands/model-presets.ts +24 -0
  75. package/src/commands/sdk.ts +5 -0
  76. package/src/config/autorouting-tier-map.ts +33 -0
  77. package/src/config/file-lock-gc.ts +9 -4
  78. package/src/config/file-lock.ts +775 -59
  79. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  80. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  81. package/src/config/keybindings.ts +6 -2
  82. package/src/config/model-preset-registry.ts +2799 -0
  83. package/src/config/model-profile-activation.ts +115 -28
  84. package/src/config/model-profile-contract.ts +4 -1
  85. package/src/config/model-profiles.ts +56 -14
  86. package/src/config/model-registry.ts +1028 -123
  87. package/src/config/settings-schema.ts +11 -0
  88. package/src/config/settings.ts +6 -1
  89. package/src/coordinator-mcp/model-preset.ts +32 -17
  90. package/src/coordinator-mcp/policy.ts +1 -1
  91. package/src/coordinator-mcp/projection-scan.ts +571 -0
  92. package/src/coordinator-mcp/server.ts +19 -24
  93. package/src/coordinator-mcp/session-reaper.ts +9 -3
  94. package/src/extensibility/extensions/loader.ts +0 -1
  95. package/src/extensibility/extensions/types.ts +4 -2
  96. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  97. package/src/extensibility/shared-events.ts +2 -0
  98. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  99. package/src/gjc-runtime/gc-render.ts +29 -0
  100. package/src/gjc-runtime/gc-runtime.ts +174 -11
  101. package/src/gjc-runtime/launch-worktree.ts +44 -7
  102. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  103. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  104. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  105. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  106. package/src/index.ts +1 -0
  107. package/src/internal-urls/docs-index.generated.ts +14 -13
  108. package/src/lsp/config.ts +4 -3
  109. package/src/lsp/path-trust.ts +12 -6
  110. package/src/main.ts +9 -2
  111. package/src/modes/acp/acp-agent.ts +11 -5
  112. package/src/modes/components/jobs-overlay-model.ts +99 -16
  113. package/src/modes/components/model-selector.ts +158 -18
  114. package/src/modes/components/status-line/segments.ts +6 -1
  115. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  116. package/src/modes/controllers/input-controller.ts +23 -6
  117. package/src/modes/interactive-mode.ts +14 -1
  118. package/src/modes/jobs-observer.ts +149 -16
  119. package/src/modes/prompt-action-autocomplete.ts +26 -47
  120. package/src/modes/runtime-init.ts +1 -1
  121. package/src/modes/utils/injected-user-submission.ts +15 -1
  122. package/src/prompts/tools/bash.md +2 -2
  123. package/src/prompts/tools/fold-receipt.md +3 -0
  124. package/src/prompts/tools/job.md +1 -1
  125. package/src/sdk/acp/adapter.ts +17 -2
  126. package/src/sdk/broker/broker.ts +48 -21
  127. package/src/sdk/broker/lifecycle.ts +32 -6
  128. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  129. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  130. package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
  131. package/src/sdk/bus/prompt-reconciliation.ts +33 -6
  132. package/src/sdk/bus/slack-conversation.ts +5 -1
  133. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  134. package/src/sdk/cli/session-cli.ts +161 -5
  135. package/src/sdk/client/discovery.ts +3 -1
  136. package/src/sdk/host/control/dispatch.ts +3 -0
  137. package/src/sdk/host/host.ts +7 -0
  138. package/src/sdk/host/model-pin.ts +185 -25
  139. package/src/sdk/host/sdk-run-capability.ts +1 -0
  140. package/src/sdk/host/session-runtime.test.ts +1240 -107
  141. package/src/sdk/host/session-runtime.ts +1029 -115
  142. package/src/sdk/prompt-deadline-manager.ts +16 -1
  143. package/src/sdk/protocol/adapter-validation.ts +27 -0
  144. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  145. package/src/sdk/protocol/operation-registry.ts +4 -1
  146. package/src/sdk/router/session-router.ts +297 -111
  147. package/src/sdk/session.ts +83 -12
  148. package/src/session/account-inventory.ts +27 -11
  149. package/src/session/agent-session.ts +887 -95
  150. package/src/session/fold-coordinator.ts +433 -0
  151. package/src/session/internal/managed-session-storage.ts +64 -26
  152. package/src/session/sdk-run-capability-internal.ts +15 -0
  153. package/src/session/sdk-run-capability.ts +2 -0
  154. package/src/session/session-manager.ts +29 -10
  155. package/src/setup/provider-presets.json +1 -1
  156. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  157. package/src/task/executor.ts +19 -3
  158. package/src/telemetry/control.ts +26 -0
  159. package/src/telemetry/events.ts +142 -0
  160. package/src/telemetry/index.ts +3 -0
  161. package/src/telemetry/transport.ts +87 -0
  162. package/src/tools/bash-interactive.ts +182 -85
  163. package/src/tools/bash.ts +786 -67
  164. package/src/tools/index.ts +7 -5
  165. package/src/tools/path-utils.ts +77 -29
  166. package/src/tools/tmux-self-injection-guard.ts +359 -0
  167. package/src/tools/tool-catalog.generated.ts +1 -1
  168. package/src/web/search/provider.ts +120 -11
  169. package/src/web/search/providers/anthropic.ts +89 -17
  170. package/src/web/search/providers/base.ts +9 -4
  171. package/src/web/search/providers/gemini.ts +87 -15
  172. package/src/web/search/providers/openai-compatible.ts +23 -8
  173. package/src/web/search/providers/xai.ts +98 -12
  174. 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,
@@ -2154,7 +2628,10 @@ export class ModelRegistry {
2154
2628
  });
2155
2629
  if (localCompatResolvedKey) {
2156
2630
  this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
2157
- this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey);
2631
+ this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey, {
2632
+ envSourced: !localOpenAICompat.apiKey,
2633
+ owner: this.#authStorageConfigOwner,
2634
+ });
2158
2635
  } else {
2159
2636
  keylessProviders.add(providerName);
2160
2637
  this.#optionalAuthProviders.add(providerName);
@@ -2175,6 +2652,7 @@ export class ModelRegistry {
2175
2652
  ) {
2176
2653
  const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
2177
2654
  overrides.set(providerName, {
2655
+ api: providerConfig.api as Api | undefined,
2178
2656
  baseUrl: providerConfig.baseUrl,
2179
2657
  headers: providerConfig.headers,
2180
2658
  apiKey: providerApiKeyConfig,
@@ -2183,10 +2661,10 @@ export class ModelRegistry {
2183
2661
  transport: providerConfig.transport,
2184
2662
  requestTransform: providerConfig.requestTransform,
2185
2663
  cacheRetention: providerConfig.cacheRetention,
2664
+ isOAuth,
2186
2665
  });
2187
2666
  }
2188
2667
 
2189
- const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
2190
2668
  if (authMode === "none") {
2191
2669
  keylessProviders.add(providerName);
2192
2670
  }
@@ -2216,7 +2694,12 @@ export class ModelRegistry {
2216
2694
  ? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
2217
2695
  : undefined;
2218
2696
  if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
2219
- if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
2697
+ if (resolved) {
2698
+ this.authStorage.setConfigApiKey(providerName, resolved, {
2699
+ envSourced: !providerConfig.apiKey,
2700
+ owner: this.#authStorageConfigOwner,
2701
+ });
2702
+ }
2220
2703
  }
2221
2704
 
2222
2705
  // Parse per-model overrides
@@ -2271,7 +2754,7 @@ export class ModelRegistry {
2271
2754
  );
2272
2755
  }
2273
2756
  const current = loaded.value ?? this.#modelsConfigFile.createDefault();
2274
- if (mergeModelProfiles(current.profiles).has(normalizedName)) {
2757
+ if (current.profiles?.[normalizedName] !== undefined) {
2275
2758
  throw new Error(`Custom model profile already exists: ${normalizedName}. Choose a unique preset id.`);
2276
2759
  }
2277
2760
  const next: ModelsConfig = {
@@ -2397,6 +2880,8 @@ export class ModelRegistry {
2397
2880
  async #refreshRuntimeDiscoveries(
2398
2881
  strategy: ModelRefreshStrategy,
2399
2882
  providerFilter?: ReadonlySet<string>,
2883
+ refreshGeneration = this.#catalogRefreshGeneration,
2884
+ providerRefresh?: ProviderRefreshFence,
2400
2885
  ): Promise<void> {
2401
2886
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
2402
2887
  const selectedDiscoverableProviders = (
@@ -2408,12 +2893,22 @@ export class ModelRegistry {
2408
2893
  selectedDiscoverableProviders.length === 0
2409
2894
  ? Promise.resolve([] as ConfiguredDiscoveryResult[])
2410
2895
  : Promise.all(
2411
- selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
2896
+ selectedDiscoverableProviders.map(provider =>
2897
+ this.#discoverProviderModels(provider, strategy, providerRefresh),
2898
+ ),
2412
2899
  );
2413
2900
  const [configuredDiscoveryResults, builtInDiscovered] = await Promise.all([
2414
2901
  configuredDiscoveriesPromise,
2415
- this.#discoverBuiltInProviderModels(strategy, providerFilter),
2902
+ this.#discoverBuiltInProviderModels(strategy, providerFilter, providerRefresh),
2416
2903
  ]);
2904
+ if (
2905
+ refreshGeneration !== this.#catalogRefreshGeneration ||
2906
+ (providerRefresh !== undefined &&
2907
+ "providerId" in providerRefresh &&
2908
+ this.#providerRefreshGenerations.get(providerRefresh.providerId) !== providerRefresh.generation)
2909
+ ) {
2910
+ return;
2911
+ }
2417
2912
  const currentConfiguredDiscoveryResults = configuredDiscoveryResults.map(result => {
2418
2913
  const providerConfig = selectedDiscoverableProviders.find(provider => provider.provider === result.provider);
2419
2914
  const current =
@@ -2580,6 +3075,7 @@ export class ModelRegistry {
2580
3075
  async #discoverProviderModels(
2581
3076
  providerConfig: DiscoveryProviderConfig,
2582
3077
  strategy: ModelRefreshStrategy,
3078
+ providerRefresh?: ProviderRefreshFence,
2583
3079
  ): Promise<ConfiguredDiscoveryResult> {
2584
3080
  const provider = providerConfig.provider;
2585
3081
  const preflightEpoch = this.#optionalAuthPreflightEpoch;
@@ -2594,7 +3090,8 @@ export class ModelRegistry {
2594
3090
  let preflightCompleted = false;
2595
3091
  const optionalAuth = this.#optionalAuthProviders.has(provider);
2596
3092
  const shouldPreflightAuth = optionalAuth
2597
- ? this.authStorage.has(provider) || this.authStorage.hasAuth(provider)
3093
+ ? this.authStorage.has(provider) ||
3094
+ this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
2598
3095
  : !this.#isCredentiallessProvider(provider);
2599
3096
  let preflightAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
2600
3097
  let preflightOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
@@ -2635,7 +3132,11 @@ export class ModelRegistry {
2635
3132
  preflightCompleted = true;
2636
3133
  if (!preflightFailed && optionalAuth && isCurrentPreflight()) {
2637
3134
  this.#providerEvidenceApiKeys.set(provider, apiKey);
2638
- const authGeneration = this.authStorage.getProviderEvidenceGeneration(provider, apiKey);
3135
+ const authGeneration = this.authStorage.getProviderEvidenceGeneration(
3136
+ provider,
3137
+ apiKey,
3138
+ this.#authStorageConfigOwner,
3139
+ );
2639
3140
  if (apiKey === undefined) this.#credentiallessAuthFallbackProviders.set(provider, authGeneration);
2640
3141
  else this.#credentiallessAuthFallbackProviders.delete(provider);
2641
3142
  }
@@ -2684,6 +3185,14 @@ export class ModelRegistry {
2684
3185
  const isCurrentEndpoint = () =>
2685
3186
  endpoint ===
2686
3187
  this.#normalizeDiscoveryEvidenceEndpoint(this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "");
3188
+ const isCurrentProviderRefresh = () =>
3189
+ providerRefresh === undefined ||
3190
+ ("providerId" in providerRefresh
3191
+ ? strategy !== "online-if-uncached" ||
3192
+ (providerRefresh.providerId === provider &&
3193
+ this.#providerRefreshGenerations.get(provider) === providerRefresh.generation)
3194
+ : (this.#providerRefreshGenerations.get(provider) ?? 0) ===
3195
+ (providerRefresh.generations.get(provider) ?? 0));
2687
3196
  const evidence = this.#configuredDiscoveryEvidence.get(provider);
2688
3197
  const cacheDynamicModelProvenance = fingerprintConfiguredDiscoveryRequestShape(
2689
3198
  effectiveProviderConfig,
@@ -2732,7 +3241,7 @@ export class ModelRegistry {
2732
3241
  ),
2733
3242
  getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
2734
3243
  cacheDynamicModelProvenance: cacheLookupProvenance,
2735
- canPublishCache: isCurrentEndpoint,
3244
+ canPublishCache: () => isCurrentEndpoint() && isCurrentProviderRefresh(),
2736
3245
  });
2737
3246
  const authGeneration =
2738
3247
  mergeInput.authGeneration ??
@@ -2788,7 +3297,7 @@ export class ModelRegistry {
2788
3297
  return {
2789
3298
  ...providerConfig,
2790
3299
  baseUrl: effectiveBaseUrl,
2791
- headers: override?.headers ? { ...providerConfig.headers, ...override.headers } : providerConfig.headers,
3300
+ headers: mergeCaseInsensitiveHeaders(providerConfig.headers, override?.headers),
2792
3301
  compat: override?.compat ? mergeCompat(providerConfig.compat, override.compat) : providerConfig.compat,
2793
3302
  requestTransform: mergeRequestTransform(providerConfig.requestTransform, override?.requestTransform),
2794
3303
  cacheRetention: override?.cacheRetention ?? providerConfig.cacheRetention,
@@ -2818,6 +3327,7 @@ export class ModelRegistry {
2818
3327
  async #discoverBuiltInProviderModels(
2819
3328
  strategy: ModelRefreshStrategy,
2820
3329
  providerFilter?: ReadonlySet<string>,
3330
+ providerRefresh?: ProviderRefreshFence,
2821
3331
  ): Promise<Model<Api>[]> {
2822
3332
  // Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
2823
3333
  const configuredDiscoveryProviders = new Set(this.#discoveryManager.providers.map(p => p.provider));
@@ -2835,7 +3345,7 @@ export class ModelRegistry {
2835
3345
  return [];
2836
3346
  }
2837
3347
  const discoveries = await Promise.all(
2838
- managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy)),
3348
+ managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy, providerRefresh)),
2839
3349
  );
2840
3350
  return discoveries.flat();
2841
3351
  }
@@ -2870,7 +3380,12 @@ export class ModelRegistry {
2870
3380
  providerId: "openai-codex",
2871
3381
  resolveKey: value => value,
2872
3382
  createOptions: accessToken => {
2873
- const accountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", accessToken);
3383
+ const accountId = resolveOAuthAccountIdForAccessToken(
3384
+ this.authStorage,
3385
+ "openai-codex",
3386
+ accessToken,
3387
+ this.#authStorageConfigOwner,
3388
+ );
2874
3389
  return openaiCodexModelManagerOptions({
2875
3390
  accessToken,
2876
3391
  accountId,
@@ -2963,6 +3478,7 @@ export class ModelRegistry {
2963
3478
  async #discoverWithModelManager(
2964
3479
  { options, authGeneration, apiKey, endpoint, endpointContainsUserinfo }: ModelManagerDiscoveryOptions,
2965
3480
  strategy: ModelRefreshStrategy,
3481
+ providerRefresh?: ProviderRefreshFence,
2966
3482
  ): Promise<Model<Api>[]> {
2967
3483
  const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
2968
3484
  this.#descriptorDiscoveryGenerations.set(options.providerId, generation);
@@ -2983,18 +3499,34 @@ export class ModelRegistry {
2983
3499
  // stale fallback, and online-if-uncached reuse all fail closed.
2984
3500
  const cacheDynamicModelProvenance =
2985
3501
  reusableCacheProvenance ?? `gajae:non-cacheable-endpoint:${crypto.randomUUID()}`;
2986
- const isCurrentDiscovery = () =>
3502
+ const isCurrentDiscoveryContext = () =>
2987
3503
  (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
2988
3504
  this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
2989
3505
  (endpoint ===
2990
3506
  this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(options.providerId) ?? "") ||
2991
3507
  (canUseCredentialDerivedXiaomiEndpoint && credentialDerivedEndpoint !== undefined));
3508
+ const isCurrentProviderRefresh = () =>
3509
+ providerRefresh === undefined ||
3510
+ ("providerId" in providerRefresh
3511
+ ? providerRefresh.providerId === options.providerId &&
3512
+ this.#providerRefreshGenerations.get(options.providerId) === providerRefresh.generation
3513
+ : (this.#providerRefreshGenerations.get(options.providerId) ?? 0) ===
3514
+ (providerRefresh.generations.get(options.providerId) ?? 0));
3515
+ const isCurrentDiscovery = () =>
3516
+ isCurrentDiscoveryContext() &&
3517
+ (providerRefresh === undefined || "generations" in providerRefresh || isCurrentProviderRefresh());
3518
+ const canPublishCache = () =>
3519
+ isCurrentDiscoveryContext() &&
3520
+ (providerRefresh === undefined ||
3521
+ ("providerId" in providerRefresh
3522
+ ? strategy !== "online-if-uncached" || isCurrentDiscovery()
3523
+ : isCurrentProviderRefresh()));
2992
3524
  try {
2993
3525
  const manager = createModelManager({
2994
3526
  ...options,
2995
3527
  cacheDbPath: this.#cacheDbPath,
2996
3528
  cacheDynamicModelProvenance,
2997
- canPublishCache: isCurrentDiscovery,
3529
+ canPublishCache,
2998
3530
  ...(options.fetchDynamicModels
2999
3531
  ? {
3000
3532
  fetchDynamicModels: async () => {
@@ -3155,8 +3687,15 @@ export class ModelRegistry {
3155
3687
  discoveryApiKey ??
3156
3688
  (this.#isCredentiallessProvider(providerConfig.provider)
3157
3689
  ? kNoAuth
3158
- : await this.authStorage.getApiKey(providerConfig.provider));
3159
- 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
+ ) {
3160
3699
  headers.Authorization = `Bearer ${apiKey}`;
3161
3700
  }
3162
3701
  const response = await fetch(tagsUrl, {
@@ -3235,8 +3774,15 @@ export class ModelRegistry {
3235
3774
  discoveryApiKey ??
3236
3775
  (this.#isCredentiallessProvider(providerConfig.provider)
3237
3776
  ? kNoAuth
3238
- : await this.authStorage.getApiKey(providerConfig.provider));
3239
- 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
+ ) {
3240
3786
  requestHeaders.Authorization = `Bearer ${apiKey}`;
3241
3787
  }
3242
3788
 
@@ -3357,12 +3903,16 @@ export class ModelRegistry {
3357
3903
  discoveryApiKey ??
3358
3904
  (this.#isCredentiallessProvider(providerConfig.provider)
3359
3905
  ? kNoAuth
3360
- : await this.authStorage.getApiKey(providerConfig.provider, undefined, { baseUrl }));
3906
+ : await this.authStorage.getApiKey(providerConfig.provider, undefined, {
3907
+ baseUrl,
3908
+ owner: this.#authStorageConfigOwner,
3909
+ }));
3361
3910
  if (
3362
3911
  apiKey &&
3363
3912
  apiKey !== DEFAULT_LOCAL_TOKEN &&
3364
3913
  !isVllmNoAuthToken(providerConfig.provider, apiKey) &&
3365
- apiKey !== kNoAuth
3914
+ apiKey !== kNoAuth &&
3915
+ headerValue(requestHeaders, "Authorization") === undefined
3366
3916
  ) {
3367
3917
  requestHeaders.Authorization = `Bearer ${apiKey}`;
3368
3918
  }
@@ -3504,7 +4054,11 @@ export class ModelRegistry {
3504
4054
  try {
3505
4055
  fallbackMatchesCurrentEvidence =
3506
4056
  fallbackEvidenceGeneration ===
3507
- this.authStorage.getProviderEvidenceGeneration(provider, this.#providerEvidenceApiKeys.get(provider));
4057
+ this.authStorage.getProviderEvidenceGeneration(
4058
+ provider,
4059
+ this.#providerEvidenceApiKeys.get(provider),
4060
+ this.#authStorageConfigOwner,
4061
+ );
3508
4062
  } catch {
3509
4063
  // AuthStorage may be unavailable while a registry is being torn down.
3510
4064
  }
@@ -3512,7 +4066,8 @@ export class ModelRegistry {
3512
4066
  return (
3513
4067
  this.#keylessProviders.has(provider) &&
3514
4068
  (!this.#optionalAuthProviders.has(provider) ||
3515
- (!this.authStorage.hasAuth(provider) && !this.authStorage.has(provider)) ||
4069
+ (!this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner }) &&
4070
+ !this.authStorage.has(provider)) ||
3516
4071
  fallbackMatchesCurrentEvidence)
3517
4072
  );
3518
4073
  }
@@ -3523,6 +4078,7 @@ export class ModelRegistry {
3523
4078
  return this.authStorage.getProviderEvidenceGeneration(
3524
4079
  provider,
3525
4080
  resolvedApiKey ?? this.#providerEvidenceApiKeys.get(provider),
4081
+ this.#authStorageConfigOwner,
3526
4082
  );
3527
4083
  }
3528
4084
  #normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
@@ -3560,7 +4116,7 @@ export class ModelRegistry {
3560
4116
 
3561
4117
  #resolveProviderOverride(
3562
4118
  provider: string,
3563
- overrides: Map<string, ProviderOverride> = this.#providerOverrides,
4119
+ overrides: ReadonlyMap<string, ProviderOverride> = this.#providerOverrides,
3564
4120
  ): ProviderOverride | undefined {
3565
4121
  const explicitOverride = overrides.get(provider);
3566
4122
  if (explicitOverride?.baseUrl) {
@@ -3587,24 +4143,19 @@ export class ModelRegistry {
3587
4143
 
3588
4144
  #mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
3589
4145
  return {
4146
+ api: override.api ?? baseOverride?.api,
3590
4147
  baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
3591
4148
  apiKey: override.apiKey ?? baseOverride?.apiKey,
3592
4149
  authHeader: override.authHeader ?? baseOverride?.authHeader,
3593
- headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
4150
+ headers: mergeCaseInsensitiveHeaders(baseOverride?.headers, override.headers),
3594
4151
  compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
3595
4152
  transport: override.transport ?? baseOverride?.transport,
3596
4153
  requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
3597
4154
  cacheRetention: override.cacheRetention ?? baseOverride?.cacheRetention,
4155
+ isOAuth: override.isOAuth ?? baseOverride?.isOAuth,
3598
4156
  };
3599
4157
  }
3600
- #applyProviderTransportOverride<
3601
- T extends {
3602
- baseUrl?: string;
3603
- headers?: Record<string, string>;
3604
- compat?: Model<Api>["compat"];
3605
- cacheRetention?: CacheRetention;
3606
- },
3607
- >(
4158
+ #applyProviderTransportOverride<T extends Model<Api>>(
3608
4159
  entry: T,
3609
4160
  override: Pick<
3610
4161
  ProviderOverride,
@@ -3618,12 +4169,39 @@ export class ModelRegistry {
3618
4169
  | "cacheRetention"
3619
4170
  >,
3620
4171
  ): T {
3621
- const headers = mergeAuthHeader(
3622
- override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
3623
- override.authHeader,
3624
- override.apiKey,
3625
- );
3626
- 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 = {
3627
4205
  ...entry,
3628
4206
  compat: mergeProviderCompat(entry.compat, override.compat),
3629
4207
  baseUrl: override.baseUrl ?? entry.baseUrl,
@@ -3637,6 +4215,21 @@ export class ModelRegistry {
3637
4215
  ),
3638
4216
  cacheRetention: entry.cacheRetention ?? override.cacheRetention,
3639
4217
  };
4218
+ if (!canInject && explicitAuthKey !== undefined) {
4219
+ delete (result.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4220
+ GENERATED_AUTH_HEADER
4221
+ ];
4222
+ }
4223
+ const generatedHeader = (
4224
+ result.headers as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined
4225
+ )?.[GENERATED_AUTH_HEADER];
4226
+ if (generatedHeader !== undefined && result.headers?.Authorization === generatedHeader) {
4227
+ this.#generatedAuthHeaders.set(result, { authorization: result.headers.Authorization });
4228
+ } else if (explicitAuthKey === undefined) {
4229
+ const generated = this.#generatedAuthHeaders.get(entry);
4230
+ if (generated) this.#generatedAuthHeaders.set(result, generated);
4231
+ }
4232
+ return result;
3640
4233
  }
3641
4234
  #applyRuntimeProviderOverride(model: Model<Api>, override: ProviderOverride): Model<Api> {
3642
4235
  const withTransportOverride = this.#applyProviderTransportOverride(
@@ -3649,26 +4242,53 @@ export class ModelRegistry {
3649
4242
  ? withTransportOverride
3650
4243
  : { ...withTransportOverride, ...(sanitizedBaseUrl === undefined ? {} : { baseUrl: sanitizedBaseUrl }) };
3651
4244
  const modelCompat = this.#modelOverrides.get(model.provider.toLowerCase())?.get(model.id.toLowerCase())?.compat;
3652
- return enrichModelThinking(
4245
+ const result = enrichModelThinking(
3653
4246
  modelCompat
3654
4247
  ? { ...sanitizedTransport, compat: mergeCompat(sanitizedTransport.compat, modelCompat) }
3655
4248
  : sanitizedTransport,
3656
4249
  );
4250
+ const generated = this.#generatedAuthHeaders.get(sanitizedTransport);
4251
+ if (generated) this.#generatedAuthHeaders.set(result, generated);
4252
+ return result;
3657
4253
  }
3658
4254
  #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
3659
4255
  if (this.#runtimeProviderOverrides.size === 0) return models;
3660
4256
  return models.map(model => {
3661
4257
  const override = this.#runtimeProviderOverrides.get(model.provider);
3662
4258
  if (!override) return model;
3663
- 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
+ );
3664
4266
  });
3665
4267
  }
3666
4268
  #finalizeModels(models: Model<Api>[]): Model<Api>[] {
3667
- return applyFinalCodexGpt56ContextCap(
3668
- models.map(model => enrichModelThinking({ ...this.#restoreDeclaredThinking(model) })),
3669
- undefined,
3670
- this.#codexContextWindowOverrides,
3671
- );
4269
+ const finalized = models.map(model => enrichModelThinking({ ...this.#restoreDeclaredThinking(model) }));
4270
+ const result = applyFinalCodexGpt56ContextCap(finalized, undefined, this.#codexContextWindowOverrides);
4271
+ for (let index = 0; index < result.length; index++) {
4272
+ if (
4273
+ (result[index]?.headers as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined)?.[
4274
+ GENERATED_AUTH_HEADER
4275
+ ]
4276
+ ) {
4277
+ const authorization = result[index]?.headers?.Authorization;
4278
+ const marker = (result[index]?.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4279
+ GENERATED_AUTH_HEADER
4280
+ ];
4281
+ if (authorization && marker === authorization)
4282
+ this.#generatedAuthHeaders.set(result[index]!, { authorization });
4283
+ else
4284
+ delete (result[index]?.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4285
+ GENERATED_AUTH_HEADER
4286
+ ];
4287
+ }
4288
+ const generated = this.#generatedAuthHeaders.get(models[index]!);
4289
+ if (generated) this.#generatedAuthHeaders.set(result[index]!, generated);
4290
+ }
4291
+ return result;
3672
4292
  }
3673
4293
  #restoreDeclaredThinking(model: Model<Api>): Model<Api> {
3674
4294
  const overrideThinking = this.#modelOverrides
@@ -3806,7 +4426,12 @@ export class ModelRegistry {
3806
4426
  ? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
3807
4427
  : undefined;
3808
4428
  if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
3809
- if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
4429
+ if (resolved) {
4430
+ this.authStorage.setConfigApiKey(providerName, resolved, {
4431
+ envSourced: !providerConfig.apiKey,
4432
+ owner: this.#authStorageConfigOwner,
4433
+ });
4434
+ }
3810
4435
  }
3811
4436
  for (const modelDef of modelDefs) {
3812
4437
  const providerCompat = providerConfig.disableStrictTools
@@ -3837,8 +4462,8 @@ export class ModelRegistry {
3837
4462
  }
3838
4463
 
3839
4464
  /**
3840
- * Get all models (built-in + custom).
3841
- * 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.
3842
4467
  */
3843
4468
  getAll(): Model<Api>[] {
3844
4469
  return this.#models;
@@ -3853,7 +4478,8 @@ export class ModelRegistry {
3853
4478
  const disabled = disabledProviders ?? getDisabledProviderIdsFromSettings(this.#settings);
3854
4479
  return (
3855
4480
  !disabled.has(model.provider) &&
3856
- (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
4481
+ (this.#keylessProviders.has(model.provider) ||
4482
+ this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner }))
3857
4483
  );
3858
4484
  }
3859
4485
  /** Per-query precomputed variant filter inputs; plan fields are authoritative when provided. */
@@ -3907,7 +4533,9 @@ export class ModelRegistry {
3907
4533
  * 6. Unknown (no credential surface)
3908
4534
  */
3909
4535
  #effectiveProviderAuth(provider: string, sessionId?: string): EffectiveProviderAuth {
3910
- const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId);
4536
+ const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId, {
4537
+ owner: this.#authStorageConfigOwner,
4538
+ });
3911
4539
  if (credentialType === "api_key") return "key";
3912
4540
  if (credentialType === "oauth") return "oauth";
3913
4541
  if (this.#keylessProviders.has(provider)) return "keyless";
@@ -4257,9 +4885,10 @@ export class ModelRegistry {
4257
4885
  * This is a fast check that doesn't refresh OAuth tokens.
4258
4886
  */
4259
4887
  getAvailable(): Model<Api>[] {
4888
+ this.#synchronizeEnvironmentCredentials();
4260
4889
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
4261
4890
  const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
4262
- const envFingerprint = envAvailabilityFingerprint();
4891
+ const envFingerprint = envAvailabilityFingerprint(this.#configuredApiKeyEnvNames);
4263
4892
  if (
4264
4893
  this.#availableModelsCache &&
4265
4894
  this.#availableModelsDisabledProviders === disabledProviderKey &&
@@ -4273,6 +4902,19 @@ export class ModelRegistry {
4273
4902
  return this.#availableModelsCache;
4274
4903
  }
4275
4904
 
4905
+ #synchronizeEnvironmentCredentials(): void {
4906
+ // Runtime registrations own a provider while their credential is present.
4907
+ // Refresh them first so a missing runtime env key can hand ownership back to
4908
+ // the static config before the static apiKeyEnv pass runs.
4909
+ for (const provider of this.#runtimeProviderApiKeyEnvNames.keys()) {
4910
+ this.#refreshRotatingConfigApiKey(provider);
4911
+ }
4912
+ for (const provider of this.#customProviderApiKeyEnvNames.keys()) {
4913
+ if (this.#runtimeProviderCredentialInstalled.has(provider)) continue;
4914
+ this.#refreshRotatingConfigApiKey(provider, true);
4915
+ }
4916
+ }
4917
+
4276
4918
  /**
4277
4919
  * Get authenticated models, excluding bundled entries that a fresh provider
4278
4920
  * catalog has positively shown to be unavailable. Bundled entries remain
@@ -4342,7 +4984,10 @@ export class ModelRegistry {
4342
4984
 
4343
4985
  #activeConnectionKind(model: Model<Api>): ActiveProviderDescriptor["connectionKind"] | undefined {
4344
4986
  const evidence = this.#providerActivity.get(model.provider);
4345
- if (!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
+ ) {
4346
4991
  if (!evidence) return undefined;
4347
4992
  const discoveryOnly =
4348
4993
  !evidence.staticConfigured &&
@@ -4417,14 +5062,20 @@ export class ModelRegistry {
4417
5062
  * as providers with stored credentials. See issue #993.
4418
5063
  */
4419
5064
  hasConfiguredAuth(model: Model<Api>): boolean {
4420
- return 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
+ );
4421
5069
  }
4422
5070
 
4423
5071
  /**
4424
5072
  * Check whether auth is configured for a provider.
4425
5073
  */
4426
5074
  hasConfiguredProviderAuth(provider: string): boolean {
4427
- return 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
+ );
4428
5079
  }
4429
5080
 
4430
5081
  isCredentiallessProvider(provider: string): boolean {
@@ -4459,11 +5110,24 @@ export class ModelRegistry {
4459
5110
  );
4460
5111
  }
4461
5112
 
5113
+ /** Opaque owner token for registry-scoped AuthStorage reads. */
5114
+ getAuthStorageOwner(): object {
5115
+ return this.#authStorageConfigOwner;
5116
+ }
5117
+
4462
5118
  getProviderWebSearchMode(provider: string): WebSearchMode | undefined {
4463
5119
  return this.#providerWebSearchModes.get(provider);
4464
5120
  }
4465
5121
 
4466
5122
  getActiveSearchModelContext(model: Model<Api>): ActiveSearchModelContext {
5123
+ const provider = model.provider.toLowerCase();
5124
+ const ownerAuthOverride =
5125
+ this.authStorage.hasConfigApiKey(model.provider, this.#authStorageConfigOwner) ||
5126
+ this.#providerOverrides.has(provider) ||
5127
+ this.#runtimeProviderOverrides.has(provider) ||
5128
+ this.#modelOverrides.get(provider)?.has(model.id.toLowerCase()) === true ||
5129
+ this.#customProviderAuthHeaders.has(provider) ||
5130
+ this.#runtimeProviderAuthHeaders.has(provider);
4467
5131
  return {
4468
5132
  provider: model.provider,
4469
5133
  modelId: model.id,
@@ -4471,6 +5135,15 @@ export class ModelRegistry {
4471
5135
  api: model.api,
4472
5136
  baseUrl: model.baseUrl,
4473
5137
  headers: model.headers,
5138
+ ownerAuthOverride,
5139
+ resolveCredentials: async ({ sessionId, signal }) => {
5140
+ // Resolve against the current registry model so rotating apiKeyEnv
5141
+ // credentials and generated Authorization headers are synchronized
5142
+ // immediately before native search sends its request.
5143
+ const currentModel = this.find(model.provider, model.id) ?? model;
5144
+ const apiKey = await this.getApiKey(currentModel, sessionId, { signal });
5145
+ return { apiKey, headers: currentModel.headers };
5146
+ },
4474
5147
  webSearch: this.getProviderWebSearchMode(model.provider),
4475
5148
  };
4476
5149
  }
@@ -4501,15 +5174,18 @@ export class ModelRegistry {
4501
5174
  } = {},
4502
5175
  ): Promise<string | undefined> {
4503
5176
  this.#refreshRotatingConfigApiKey(model.provider);
4504
- return this.#getApiKeyOrNoAuth(model.provider, () =>
5177
+ const apiKey = await this.#getApiKeyOrNoAuth(model.provider, () =>
4505
5178
  this.authStorage.getApiKey(model.provider, sessionId, {
4506
5179
  baseUrl: model.baseUrl,
4507
5180
  modelId: model.id,
4508
5181
  credentialSelector: options.credentialSelector,
4509
5182
  preferredCredentialSelector: options.preferredCredentialSelector,
5183
+ owner: this.#authStorageConfigOwner,
4510
5184
  signal: options.signal,
4511
5185
  }),
4512
5186
  );
5187
+ this.#applyEffectiveAuthHeader(model, apiKey);
5188
+ return apiKey;
4513
5189
  }
4514
5190
 
4515
5191
  /**
@@ -4526,28 +5202,143 @@ export class ModelRegistry {
4526
5202
  } = {},
4527
5203
  ): Promise<string | undefined> {
4528
5204
  this.#refreshRotatingConfigApiKey(provider);
4529
- return this.#getApiKeyOrNoAuth(provider, () =>
5205
+ const apiKey = await this.#getApiKeyOrNoAuth(provider, () =>
4530
5206
  this.authStorage.getApiKey(provider, sessionId, {
4531
5207
  baseUrl,
4532
5208
  credentialSelector: options.credentialSelector,
4533
5209
  preferredCredentialSelector: options.preferredCredentialSelector,
5210
+ owner: this.#authStorageConfigOwner,
4534
5211
  signal: options.signal,
4535
5212
  }),
4536
5213
  );
5214
+ if (
5215
+ this.#runtimeProviderAuthHeaders.get(provider) === true ||
5216
+ this.#customProviderAuthHeaders.get(provider) === true
5217
+ ) {
5218
+ for (const model of this.#models) {
5219
+ if (model.provider === provider) this.#applyEffectiveAuthHeader(model, apiKey);
5220
+ }
5221
+ }
5222
+ return apiKey;
4537
5223
  }
4538
5224
 
4539
- #refreshRotatingConfigApiKey(provider: string): void {
4540
- const 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);
4541
5230
  if (!envName) return;
4542
5231
  const resolved = $rotatingCredentialEnv(envName);
4543
- 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;
4544
5236
  if (resolved === undefined) {
5237
+ if (runtimeOwned) {
5238
+ if (!this.#runtimeProviderCredentialInstalled.has(provider)) return;
5239
+ this.#runtimeProviderCredentialInstalled.delete(provider);
5240
+ this.#staticModelsLoaded = false;
5241
+ this.#reloadStaticModels();
5242
+ return;
5243
+ }
4545
5244
  this.#customProviderApiKeys.delete(provider);
4546
- this.authStorage.removeConfigApiKey(provider);
5245
+ this.authStorage.removeConfigApiKey(provider, this.#authStorageConfigOwner);
5246
+ } else {
5247
+ this.#customProviderApiKeys.set(provider, resolved);
5248
+ if (runtimeOwned) {
5249
+ this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
5250
+ this.#runtimeProviderCredentialInstalled.add(provider);
5251
+ }
5252
+ if (runtimeOwned) {
5253
+ this.authStorage.setConfigApiKey(provider, resolved, { owner: this.#authStorageConfigOwner });
5254
+ } else {
5255
+ this.authStorage.setConfigApiKey(provider, resolved, {
5256
+ envSourced: true,
5257
+ owner: this.#authStorageConfigOwner,
5258
+ });
5259
+ }
5260
+ }
5261
+ const authHeader =
5262
+ this.#runtimeProviderAuthHeaders.get(provider) ?? this.#customProviderAuthHeaders.get(provider);
5263
+ const runtimeOverride = this.#runtimeProviderOverrides.get(provider);
5264
+ if (runtimeOverride) this.#runtimeProviderOverrides.set(provider, { ...runtimeOverride, apiKey: resolved ?? "" });
5265
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
5266
+ if (overlay.provider !== provider) return overlay;
5267
+ const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
5268
+ [GENERATED_AUTH_HEADER]?: string;
5269
+ };
5270
+ const generated = headers[GENERATED_AUTH_HEADER];
5271
+ const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
5272
+ const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated);
5273
+ if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
5274
+ else if (typeof generated === "string") deleteAuthorizationValue(headers, generated);
5275
+ delete headers[GENERATED_AUTH_HEADER];
5276
+ if (authHeader === true && resolved && (ownsAuthorization || !hadAuthorization)) {
5277
+ headers.Authorization = `Bearer ${resolved}`;
5278
+ headers[GENERATED_AUTH_HEADER] = `Bearer ${resolved}`;
5279
+ }
5280
+ return { ...overlay, headers };
5281
+ });
5282
+ if (authHeader === true) this.#generatedAuthHeaderProviders.add(provider);
5283
+ if (authHeader !== true && !this.#generatedAuthHeaderProviders.has(provider)) return;
5284
+ for (const model of this.#models) {
5285
+ if (model.provider !== provider) continue;
5286
+ const headers = { ...(model.headers ?? {}) } as Record<string, string> & {
5287
+ [GENERATED_AUTH_HEADER]?: string;
5288
+ };
5289
+ const generated =
5290
+ this.#generatedAuthHeaders.get(model) ??
5291
+ (typeof headers[GENERATED_AUTH_HEADER] === "string"
5292
+ ? { authorization: headers[GENERATED_AUTH_HEADER] }
5293
+ : undefined);
5294
+ const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
5295
+ const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated?.authorization);
5296
+ if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
5297
+ else if (generated?.authorization) deleteAuthorizationValue(headers, generated.authorization);
5298
+ if (generated?.apiKey === headers["X-Api-Key"]) delete headers["X-Api-Key"];
5299
+ delete headers[GENERATED_AUTH_HEADER];
5300
+ model.headers =
5301
+ authHeader === true && resolved && (ownsAuthorization || !hadAuthorization)
5302
+ ? { ...headers, Authorization: `Bearer ${resolved}`, [GENERATED_AUTH_HEADER]: `Bearer ${resolved}` }
5303
+ : headers;
5304
+ if (authHeader === true && resolved && (ownsAuthorization || !hadAuthorization))
5305
+ this.#generatedAuthHeaders.set(model, { authorization: model.headers.Authorization });
5306
+ }
5307
+ this.#rebuildCanonicalIndex();
5308
+ }
5309
+
5310
+ #applyEffectiveAuthHeader(model: Model<Api>, apiKey: string | undefined): void {
5311
+ const runtimeCredentialRevoked =
5312
+ this.#runtimeProviderApiKeys.has(model.provider) &&
5313
+ !this.#runtimeProviderCredentialInstalled.has(model.provider);
5314
+ const authHeader = runtimeCredentialRevoked
5315
+ ? this.#customProviderAuthHeaders.get(model.provider)
5316
+ : (this.#runtimeProviderAuthHeaders.get(model.provider) ??
5317
+ this.#customProviderAuthHeaders.get(model.provider));
5318
+ if (authHeader === true) this.#generatedAuthHeaderProviders.add(model.provider);
5319
+ const headers = { ...(model.headers ?? {}) } as Record<string, string> & {
5320
+ [GENERATED_AUTH_HEADER]?: string;
5321
+ };
5322
+ const generated =
5323
+ this.#generatedAuthHeaders.get(model) ??
5324
+ (typeof headers[GENERATED_AUTH_HEADER] === "string"
5325
+ ? { authorization: headers[GENERATED_AUTH_HEADER] }
5326
+ : undefined);
5327
+ const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
5328
+ const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated?.authorization);
5329
+ if (authHeader !== true && generated === undefined && !this.#generatedAuthHeaderProviders.has(model.provider))
4547
5330
  return;
5331
+ if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
5332
+ else if (generated?.authorization) deleteAuthorizationValue(headers, generated.authorization);
5333
+ if (generated?.apiKey === headers["X-Api-Key"]) delete headers["X-Api-Key"];
5334
+ delete headers[GENERATED_AUTH_HEADER];
5335
+ model.headers =
5336
+ authHeader === true && apiKey && apiKey !== kNoAuth && (ownsAuthorization || !hadAuthorization)
5337
+ ? { ...headers, Authorization: `Bearer ${apiKey}`, [GENERATED_AUTH_HEADER]: `Bearer ${apiKey}` }
5338
+ : headers;
5339
+ if (authHeader === true && apiKey && apiKey !== kNoAuth && (ownsAuthorization || !hadAuthorization)) {
5340
+ this.#generatedAuthHeaders.set(model, { authorization: model.headers.Authorization });
4548
5341
  }
4549
- this.#customProviderApiKeys.set(provider, resolved);
4550
- this.authStorage.setConfigApiKey(provider, resolved);
4551
5342
  }
4552
5343
 
4553
5344
  async #peekApiKeyForProvider(
@@ -4563,16 +5354,21 @@ export class ModelRegistry {
4563
5354
  return kNoAuth;
4564
5355
  }
4565
5356
  try {
4566
- this.authStorage.getProviderEvidenceGeneration(provider);
5357
+ this.authStorage.getProviderEvidenceGeneration(provider, undefined, this.#authStorageConfigOwner);
4567
5358
  } catch {
4568
5359
  return undefined;
4569
5360
  }
4570
5361
  if (options.refreshOAuth && this.authStorage.hasOAuth(provider)) {
4571
- return this.authStorage.getApiKey(provider, undefined, { baseUrl: options.baseUrl });
5362
+ return this.authStorage.getApiKey(provider, undefined, {
5363
+ baseUrl: options.baseUrl,
5364
+ owner: this.#authStorageConfigOwner,
5365
+ });
4572
5366
  }
4573
5367
  return options.ignoreCredentiallessFallback
4574
- ? this.authStorage.peekApiKey(provider)
4575
- : this.#getApiKeyOrNoAuth(provider, () => 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
+ );
4576
5372
  }
4577
5373
 
4578
5374
  /**
@@ -4598,9 +5394,13 @@ export class ModelRegistry {
4598
5394
 
4599
5395
  #clearRuntimeProviderState(providerName: string): void {
4600
5396
  this.#runtimeProviderApiKeys.delete(providerName);
5397
+ this.#runtimeProviderResolvedApiKeys.delete(providerName);
5398
+ this.#runtimeProviderCredentialInstalled.delete(providerName);
5399
+ this.#runtimeProviderApiKeyEnvNames.delete(providerName);
4601
5400
  this.#runtimeProviderOverrides.delete(providerName);
5401
+ this.#runtimeProviderAuthHeaders.delete(providerName);
4602
5402
  this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
4603
- this.authStorage.removeConfigApiKey(providerName);
5403
+ this.authStorage.removeConfigApiKey(providerName, this.#authStorageConfigOwner);
4604
5404
  this.#clearDescriptorDiscoveryEvidence(providerName);
4605
5405
  }
4606
5406
 
@@ -4686,7 +5486,6 @@ export class ModelRegistry {
4686
5486
  sourceId,
4687
5487
  });
4688
5488
  }
4689
-
4690
5489
  let sourceHandoff = false;
4691
5490
  if (sourceId) {
4692
5491
  this.#registeredProviderSources.add(sourceId);
@@ -4709,14 +5508,89 @@ export class ModelRegistry {
4709
5508
  this.#lastStaticLoadMtime = null;
4710
5509
  this.#staticModelsLoaded = false;
4711
5510
  this.#reloadStaticModels();
5511
+ if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
5512
+ }
5513
+ if (config.authHeader !== undefined) {
5514
+ if (config.authHeader === false) {
5515
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
5516
+ if (overlay.provider !== providerName) return overlay;
5517
+ const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
5518
+ [GENERATED_AUTH_HEADER]?: string;
5519
+ };
5520
+ const generated = headers[GENERATED_AUTH_HEADER];
5521
+ if (typeof generated === "string" && headers.Authorization === generated) delete headers.Authorization;
5522
+ delete headers[GENERATED_AUTH_HEADER];
5523
+ return { ...overlay, headers };
5524
+ });
5525
+ }
5526
+ this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
5527
+ if (config.authHeader === true) this.#generatedAuthHeaderProviders.add(providerName);
4712
5528
  }
4713
5529
 
4714
5530
  if (config.apiKey) {
4715
- 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);
4716
5536
  // Persist runtime API keys so they survive #reloadStaticModels() cycles
4717
5537
  this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
4718
- const resolved = resolveApiKeyConfig(config.apiKey);
4719
- 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);
4720
5594
  }
4721
5595
 
4722
5596
  if (config.models && config.models.length > 0) {
@@ -4728,7 +5602,7 @@ export class ModelRegistry {
4728
5602
  config.baseUrl!,
4729
5603
  config.api,
4730
5604
  config.headers,
4731
- config.apiKey,
5605
+ config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
4732
5606
  config.authHeader,
4733
5607
  config.compat,
4734
5608
  config.requestTransform,
@@ -4750,6 +5624,29 @@ export class ModelRegistry {
4750
5624
  for (const overlay of newOverlays) {
4751
5625
  nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
4752
5626
  }
5627
+ if (
5628
+ config.baseUrl ||
5629
+ config.headers ||
5630
+ config.apiKey ||
5631
+ config.authHeader !== undefined ||
5632
+ config.compat !== undefined ||
5633
+ config.requestTransform !== undefined ||
5634
+ config.transport !== undefined
5635
+ ) {
5636
+ this.#runtimeProviderOverrides.set(
5637
+ providerName,
5638
+ this.#mergeProviderOverride(this.#runtimeProviderOverrides.get(providerName), {
5639
+ api: config.api,
5640
+ baseUrl: this.#runtimeProviderOverrides.get(providerName)?.baseUrl ?? config.baseUrl,
5641
+ headers: config.headers,
5642
+ apiKey: config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
5643
+ authHeader: config.authHeader,
5644
+ compat: config.compat,
5645
+ requestTransform: config.requestTransform,
5646
+ transport: config.transport,
5647
+ }),
5648
+ );
5649
+ }
4753
5650
  const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
4754
5651
  const withRuntimeTransportOverride = runtimeTransportOverride
4755
5652
  ? nextModels.map(model => {
@@ -4760,7 +5657,9 @@ export class ModelRegistry {
4760
5657
  const withModelOverrides = this.#applyProviderModelOverrides(providerName, withRuntimeTransportOverride);
4761
5658
 
4762
5659
  if (config.oauth?.modifyModels) {
4763
- const credential = this.authStorage.getOAuthCredential(providerName);
5660
+ const credential = this.authStorage.getOAuthCredential(providerName, undefined, {
5661
+ owner: this.#authStorageConfigOwner,
5662
+ });
4764
5663
  if (credential) {
4765
5664
  this.#models = this.#finalizeModels(config.oauth.modifyModels(withModelOverrides, credential));
4766
5665
  this.#rebuildCanonicalIndex();
@@ -4785,9 +5684,10 @@ export class ModelRegistry {
4785
5684
  config.transport !== undefined
4786
5685
  ) {
4787
5686
  const transportOverride = {
5687
+ api: config.api,
4788
5688
  baseUrl: config.baseUrl,
4789
5689
  headers: config.headers,
4790
- apiKey: config.apiKey,
5690
+ apiKey: config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
4791
5691
  authHeader: config.authHeader,
4792
5692
  compat: config.compat,
4793
5693
  requestTransform: config.requestTransform,
@@ -4802,6 +5702,11 @@ export class ModelRegistry {
4802
5702
  if (m.provider !== providerName) return m;
4803
5703
  return this.#applyRuntimeProviderOverride(m, transportOverride);
4804
5704
  });
5705
+ if (config.authHeader === false) {
5706
+ for (const model of this.#models) {
5707
+ if (model.provider === providerName) this.#applyEffectiveAuthHeader(model, undefined);
5708
+ }
5709
+ }
4805
5710
  this.#rebuildCanonicalIndex();
4806
5711
  this.#rebuildProviderActivity();
4807
5712
  }