@gajae-code/coding-agent 0.15.5 → 0.16.0

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 (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. 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);
@@ -1091,6 +1192,7 @@ type CustomModelOverlay = {
1091
1192
  wireModelId?: string;
1092
1193
  requestTransform?: ModelRequestTransform;
1093
1194
  cacheRetention?: CacheRetention;
1195
+ transport?: Model<Api>["transport"];
1094
1196
  isOAuth?: boolean;
1095
1197
  };
1096
1198
 
@@ -1100,7 +1202,36 @@ function mergeCustomModelHeaders(
1100
1202
  authHeader: boolean | undefined,
1101
1203
  apiKeyConfig: string | undefined,
1102
1204
  ): Record<string, string> | undefined {
1103
- return mergeAuthHeader({ ...providerHeaders, ...modelHeaders }, authHeader, apiKeyConfig);
1205
+ const merged = mergeCaseInsensitiveHeaders(providerHeaders, modelHeaders);
1206
+ return mergeAuthHeader(merged, authHeader, apiKeyConfig);
1207
+ }
1208
+
1209
+ function mergeCaseInsensitiveHeaders(
1210
+ baseHeaders: Record<string, string> | undefined,
1211
+ overrideHeaders: Record<string, string> | undefined,
1212
+ ): (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined {
1213
+ const merged: Record<string, string> = {};
1214
+ for (const [key, value] of Object.entries(baseHeaders ?? {})) {
1215
+ if (key.toLowerCase() === "authorization") deleteHeaderCaseInsensitive(merged, "Authorization");
1216
+ merged[key] = value;
1217
+ }
1218
+ for (const [key, value] of Object.entries(overrideHeaders ?? {})) {
1219
+ if (key.toLowerCase() === "authorization") deleteHeaderCaseInsensitive(merged, "Authorization");
1220
+ merged[key] = value;
1221
+ }
1222
+ const baseGenerated = (baseHeaders as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined)?.[
1223
+ GENERATED_AUTH_HEADER
1224
+ ];
1225
+ const overrideGenerated = (
1226
+ overrideHeaders as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined
1227
+ )?.[GENERATED_AUTH_HEADER];
1228
+ const survivingGenerated =
1229
+ headerValue(overrideHeaders, "Authorization") !== undefined ? overrideGenerated : baseGenerated;
1230
+ if (survivingGenerated !== undefined && headerValue(merged, "Authorization") === survivingGenerated) {
1231
+ (merged as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[GENERATED_AUTH_HEADER] =
1232
+ survivingGenerated;
1233
+ }
1234
+ return Object.keys(merged).length > 0 ? merged : undefined;
1104
1235
  }
1105
1236
 
1106
1237
  function mergeAuthHeader(
@@ -1109,11 +1240,41 @@ function mergeAuthHeader(
1109
1240
  apiKeyConfig: string | undefined,
1110
1241
  ): Record<string, string> | undefined {
1111
1242
  const nextHeaders = headers && Object.keys(headers).length > 0 ? { ...headers } : undefined;
1112
- if (!authHeader || !apiKeyConfig) {
1243
+ if (!authHeader || !apiKeyConfig || headerValue(nextHeaders, "Authorization") !== undefined) {
1113
1244
  return nextHeaders;
1114
1245
  }
1115
- const resolvedKey = resolveApiKeyConfig(apiKeyConfig);
1116
- return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
1246
+ return apiKeyConfig
1247
+ ? ({
1248
+ ...nextHeaders,
1249
+ Authorization: `Bearer ${apiKeyConfig}`,
1250
+ [GENERATED_AUTH_HEADER]: `Bearer ${apiKeyConfig}`,
1251
+ } as Record<string, string>)
1252
+ : nextHeaders;
1253
+ }
1254
+
1255
+ function headerValue(headers: Record<string, string> | undefined, name: string): string | undefined {
1256
+ const lowerName = name.toLowerCase();
1257
+ const key = Object.keys(headers ?? {}).find(candidate => candidate.toLowerCase() === lowerName);
1258
+ return key === undefined ? undefined : headers?.[key];
1259
+ }
1260
+
1261
+ function deleteHeaderCaseInsensitive(headers: Record<string, string>, name: string): void {
1262
+ const lowerName = name.toLowerCase();
1263
+ for (const key of Object.keys(headers)) {
1264
+ if (key.toLowerCase() === lowerName) delete headers[key];
1265
+ }
1266
+ }
1267
+
1268
+ function ownsOnlyGeneratedAuthorization(headers: Record<string, string>, generated: string | undefined): boolean {
1269
+ if (generated === undefined) return false;
1270
+ const entries = Object.entries(headers).filter(([key]) => key.toLowerCase() === "authorization");
1271
+ return entries.length === 1 && entries[0]![1] === generated;
1272
+ }
1273
+
1274
+ function deleteAuthorizationValue(headers: Record<string, string>, value: string): void {
1275
+ for (const key of Object.keys(headers)) {
1276
+ if (key.toLowerCase() === "authorization" && headers[key] === value) delete headers[key];
1277
+ }
1117
1278
  }
1118
1279
 
1119
1280
  /**
@@ -1143,6 +1304,7 @@ function buildCustomModelOverlay(
1143
1304
  providerRequestTransform: ModelRequestTransform | undefined,
1144
1305
  providerAuth: ProviderAuthMode | undefined,
1145
1306
  providerCacheRetention: CacheRetention | undefined,
1307
+ providerTransport: Model<Api>["transport"] | undefined,
1146
1308
  modelDef: CustomModelDefinitionLike,
1147
1309
  ): CustomModelOverlay | undefined {
1148
1310
  const api = modelDef.api ?? providerApi;
@@ -1171,6 +1333,7 @@ function buildCustomModelOverlay(
1171
1333
  contextPromotionTarget: modelDef.contextPromotionTarget,
1172
1334
  premiumMultiplier: modelDef.premiumMultiplier,
1173
1335
  cacheRetention: modelDef.cacheRetention ?? providerCacheRetention,
1336
+ transport: providerTransport,
1174
1337
  isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
1175
1338
  };
1176
1339
  }
@@ -1306,6 +1469,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
1306
1469
  wireModelId: resolvedModel.wireModelId,
1307
1470
  requestTransform: resolvedModel.requestTransform,
1308
1471
  cacheRetention: resolvedModel.cacheRetention ?? reference?.cacheRetention,
1472
+ transport: resolvedModel.transport,
1309
1473
  premiumMultiplier: resolvedModel.premiumMultiplier,
1310
1474
  isOAuth: resolvedModel.isOAuth,
1311
1475
  } as Model<Api>);
@@ -1366,6 +1530,8 @@ interface ConfiguredDiscoveryResult {
1366
1530
  clearPublishedModelIds?: readonly string[];
1367
1531
  }
1368
1532
 
1533
+ type ProviderRefreshFence = { providerId: string; generation: number } | { generations: ReadonlyMap<string, number> };
1534
+
1369
1535
  /**
1370
1536
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
1371
1537
  */
@@ -1384,6 +1550,7 @@ export class ModelRegistry {
1384
1550
  #sessionCanonicalVariants = new Map<string, string>();
1385
1551
  #customProviderApiKeys: Map<string, string> = new Map();
1386
1552
  #customProviderApiKeyEnvNames: Map<string, string> = new Map();
1553
+ #customProviderAuthHeaders: Map<string, boolean> = new Map();
1387
1554
  #providerWebSearchModes: Map<string, WebSearchMode> = new Map();
1388
1555
  #keylessProviders: Set<string> = new Set();
1389
1556
  #optionalAuthProviders: Set<string> = new Set();
@@ -1420,21 +1587,42 @@ export class ModelRegistry {
1420
1587
  #configError: ConfigError | undefined = undefined;
1421
1588
  #modelsConfigFile: ConfigFile<ModelsConfig>;
1422
1589
  #settings: Pick<Settings, "get" | "getGlobal">;
1590
+ readonly #authStorageConfigOwner: object = {};
1591
+ #disposeAuthStorageFallbackResolver: (() => void) | undefined;
1423
1592
  #lastStaticLoadMtime: number | null = null;
1424
1593
  #lastStaticLoadEnvironmentFingerprint: string | undefined;
1594
+ #lastModelPresetRegistryFingerprint: string | undefined;
1595
+ #loadedModelPresetRegistryManifestSha256: string | undefined;
1596
+ #modelPresetRegistryAgentDir: string;
1597
+ #modelPresetRegistryDependencies: Omit<ModelPresetRegistryDependencies, "agentDir">;
1598
+ #cancelModelPresetRegistryRefresh: (() => Promise<void>) | undefined;
1599
+ #disposePromise: Promise<void> | undefined;
1600
+ #unsubscribeAuthGeneration: (() => void) | undefined;
1425
1601
  #staticModelsLoaded = false;
1426
1602
  #lastDisabledProviderKey: string | undefined;
1427
1603
  #registeredProviderSources: Set<string> = new Set();
1428
1604
  #cacheDbPath?: string;
1429
1605
  #suppressedSelectors: Map<string, number> = new Map();
1430
1606
  #backgroundRefresh?: Promise<void>;
1607
+ #catalogMutationTail: Promise<void> = Promise.resolve();
1608
+ #pendingCatalogMutations = 0;
1609
+ #catalogRefreshGeneration = 0;
1610
+ #providerRefreshGenerations = new Map<string, number>();
1611
+ #disposed = false;
1431
1612
  // Runtime extension model overlays — persist across refresh() cycles so that
1432
1613
  // models registered by extensions survive the model selector's offline reload.
1433
1614
  #runtimeModelOverlays: CustomModelOverlay[] = [];
1434
1615
  #runtimeProviderApiKeys: Map<string, string> = new Map();
1616
+ #runtimeProviderResolvedApiKeys: Map<string, string> = new Map();
1617
+ #runtimeProviderCredentialInstalled: Set<string> = new Set();
1618
+ #runtimeProviderApiKeyEnvNames: Map<string, string> = new Map();
1435
1619
  #runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
1620
+ #runtimeProviderAuthHeaders: Map<string, boolean> = new Map();
1621
+ #generatedAuthHeaderProviders: Set<string> = new Set();
1622
+ #generatedAuthHeaders: WeakMap<Model<Api>, { authorization?: string; apiKey?: string }> = new WeakMap();
1436
1623
  #runtimeProvidersBySource: Map<string, Set<string>> = new Map();
1437
1624
  #runtimeProviderSourceByName: Map<string, string> = new Map();
1625
+ #registryModelKeys: Set<string> = new Set();
1438
1626
  #rebuildPending: boolean = false;
1439
1627
  #rebuildSuspended: number = 0;
1440
1628
  #configuredApiKeyEnvNames: Set<string> = new Set();
@@ -1448,18 +1636,77 @@ export class ModelRegistry {
1448
1636
  readonly authStorage: AuthStorage,
1449
1637
  modelsPath?: string,
1450
1638
  registrySettings?: Pick<Settings, "get" | "getGlobal">,
1639
+ modelPresetRegistryDependencies: ModelPresetRegistryDependencies = {},
1451
1640
  ) {
1452
1641
  this.#settings = registrySettings ?? settings;
1642
+ const configuredAgentDir = path.resolve(modelPresetRegistryDependencies.agentDir ?? getAgentDir());
1453
1643
  this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
1644
+ this.#modelPresetRegistryAgentDir = modelPresetRegistryDependencies.agentDir
1645
+ ? configuredAgentDir
1646
+ : modelsPath && path.isAbsolute(modelsPath)
1647
+ ? path.dirname(modelsPath)
1648
+ : configuredAgentDir;
1649
+ const { agentDir: _agentDir, ...registryDependencies } = modelPresetRegistryDependencies;
1650
+ this.#modelPresetRegistryDependencies = registryDependencies;
1454
1651
  this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
1455
1652
  // Set up fallback resolver for custom provider API keys
1456
- this.authStorage.setFallbackResolver(provider => {
1653
+ this.#disposeAuthStorageFallbackResolver = this.authStorage.setFallbackResolver(provider => {
1457
1654
  const keyConfig = this.#customProviderApiKeys.get(provider);
1458
1655
  return keyConfig;
1459
- });
1460
- this.authStorage.onGenerationChanged(() => this.#invalidateAvailableModels());
1656
+ }, this.#authStorageConfigOwner);
1657
+ this.#unsubscribeAuthGeneration = this.authStorage.onGenerationChanged(() => this.#invalidateAvailableModels());
1461
1658
  // Load models synchronously in constructor
1462
1659
  this.#loadModels();
1660
+ this.#cancelModelPresetRegistryRefresh = refreshModelPresetRegistryInBackground(
1661
+ {
1662
+ ...this.#modelPresetRegistryDependencies,
1663
+ agentDir: this.#modelPresetRegistryAgentDir,
1664
+ knownManifestSha256: this.#loadedModelPresetRegistryManifestSha256,
1665
+ },
1666
+ () => {
1667
+ void this.#enqueueCatalogMutation(() => {
1668
+ if (this.#disposed) return;
1669
+ this.#reloadStaticModels();
1670
+ this.#modelBindingsApplier.apply();
1671
+ this.#notifyCatalogChanged();
1672
+ }).catch(() => undefined);
1673
+ },
1674
+ );
1675
+ }
1676
+
1677
+ dispose(): Promise<void> {
1678
+ if (this.#disposePromise) return this.#disposePromise;
1679
+ this.#disposed = true;
1680
+ this.#catalogRefreshGeneration++;
1681
+ const awaitRefreshDisposal = this.#cancelModelPresetRegistryRefresh?.() ?? Promise.resolve();
1682
+ this.#cancelModelPresetRegistryRefresh = undefined;
1683
+ this.#unsubscribeAuthGeneration?.();
1684
+ this.#unsubscribeAuthGeneration = undefined;
1685
+ this.authStorage.clearConfigApiKeys(this.#authStorageConfigOwner);
1686
+ this.#disposeAuthStorageFallbackResolver?.();
1687
+ this.#disposeAuthStorageFallbackResolver = undefined;
1688
+ this.#catalogChangeListeners.clear();
1689
+ this.#disposePromise = awaitRefreshDisposal;
1690
+ return this.#disposePromise;
1691
+ }
1692
+
1693
+ #enqueueCatalogMutation(operation: () => void | Promise<void>): Promise<void> {
1694
+ this.#pendingCatalogMutations++;
1695
+ let run: Promise<void>;
1696
+ if (this.#pendingCatalogMutations === 1) {
1697
+ try {
1698
+ run = Promise.resolve(operation());
1699
+ } catch (error) {
1700
+ run = Promise.reject(error);
1701
+ }
1702
+ } else {
1703
+ run = this.#catalogMutationTail.then(operation);
1704
+ }
1705
+ const completion = run.finally(() => {
1706
+ this.#pendingCatalogMutations--;
1707
+ });
1708
+ this.#catalogMutationTail = completion.catch(() => undefined);
1709
+ return completion;
1463
1710
  }
1464
1711
 
1465
1712
  onCatalogChanged(listener: () => void): () => void {
@@ -1471,22 +1718,34 @@ export class ModelRegistry {
1471
1718
 
1472
1719
  /** Replace the read-only settings snapshot used by profile-scoped resolution. */
1473
1720
  setScopedSettings(settingsReader: Pick<Settings, "get" | "getGlobal">): void {
1721
+ this.#catalogRefreshGeneration++;
1474
1722
  this.#settings = settingsReader;
1723
+ this.#staticModelsLoaded = false;
1724
+ this.#reloadStaticModels();
1725
+ this.#rebuildCanonicalIndex();
1475
1726
  }
1476
1727
 
1477
1728
  /**
1478
- * Reload models from disk (built-in + custom from models.json).
1729
+ * Reload models from disk (embedded + accepted registry + custom from models.yml).
1479
1730
  */
1480
1731
  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
- }
1732
+ if (this.#disposed) return;
1733
+ await this.#enqueueCatalogMutation(async () => {
1734
+ if (this.#disposed) return;
1735
+ const refreshGeneration = ++this.#catalogRefreshGeneration;
1736
+ const providerRefreshFence: ProviderRefreshFence = {
1737
+ generations: new Map(this.#providerRefreshGenerations),
1738
+ };
1739
+ this.#suspendRebuild();
1740
+ try {
1741
+ this.#reloadStaticModels();
1742
+ this.#suppressedSelectors.clear();
1743
+ await this.#refreshRuntimeDiscoveries(strategy, undefined, refreshGeneration, providerRefreshFence);
1744
+ if (refreshGeneration === this.#catalogRefreshGeneration) this.#modelBindingsApplier.apply();
1745
+ } finally {
1746
+ this.#resumeRebuild();
1747
+ }
1748
+ });
1490
1749
  }
1491
1750
 
1492
1751
  refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void {
@@ -1508,19 +1767,34 @@ export class ModelRegistry {
1508
1767
  }
1509
1768
 
1510
1769
  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);
1770
+ if (this.#disposed) return;
1771
+ const providerRefreshGeneration = (this.#providerRefreshGenerations.get(providerId) ?? 0) + 1;
1772
+ this.#providerRefreshGenerations.set(providerId, providerRefreshGeneration);
1773
+ await this.#enqueueCatalogMutation(async () => {
1774
+ if (this.#disposed) return;
1775
+ const refreshGeneration = this.#catalogRefreshGeneration;
1776
+ this.#suspendRebuild();
1777
+ try {
1778
+ this.#reloadStaticModels();
1779
+ for (const selector of this.#suppressedSelectors.keys()) {
1780
+ if (selector.startsWith(`${providerId}/`)) {
1781
+ this.#suppressedSelectors.delete(selector);
1782
+ }
1783
+ }
1784
+ await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]), refreshGeneration, {
1785
+ providerId,
1786
+ generation: providerRefreshGeneration,
1787
+ });
1788
+ if (
1789
+ refreshGeneration === this.#catalogRefreshGeneration &&
1790
+ this.#providerRefreshGenerations.get(providerId) === providerRefreshGeneration
1791
+ ) {
1792
+ this.#modelBindingsApplier.apply();
1517
1793
  }
1794
+ } finally {
1795
+ this.#resumeRebuild();
1518
1796
  }
1519
- await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
1520
- this.#modelBindingsApplier.apply();
1521
- } finally {
1522
- this.#resumeRebuild();
1523
- }
1797
+ });
1524
1798
  }
1525
1799
 
1526
1800
  admitCachedProviderForStoredLiteralCredential(providerId: string, selector: AuthCredentialSelector): boolean {
@@ -1564,7 +1838,11 @@ export class ModelRegistry {
1564
1838
  this.#discoveryManager.providerIds().has(providerId) ||
1565
1839
  PROVIDER_DESCRIPTORS.some(descriptor => descriptor.providerId === providerId);
1566
1840
  if (!supportsDiscovery) return undefined;
1567
- const authEvidence = this.authStorage.getStoredLiteralApiKeyEvidenceGeneration(providerId, selector);
1841
+ const authEvidence = this.authStorage.getStoredLiteralApiKeyEvidenceGeneration(
1842
+ providerId,
1843
+ selector,
1844
+ this.#authStorageConfigOwner,
1845
+ );
1568
1846
  if (!authEvidence) return undefined;
1569
1847
  return [
1570
1848
  ...this.#loadCachedStandardProviderModels(providerId, authEvidence),
@@ -1608,11 +1886,22 @@ export class ModelRegistry {
1608
1886
  const currentMtime = this.#modelsConfigFile.getMtimeMs();
1609
1887
  const disabledProviderKey = [...getDisabledProviderIdsFromSettings(this.#settings)].sort().join("\u0000");
1610
1888
  const environmentFingerprint = this.#getStaticLoadEnvironmentFingerprint();
1889
+ const acceptedPresets = loadAcceptedModelPresetProfiles(
1890
+ this.#modelPresetRegistryAgentDir,
1891
+ this.#modelPresetRegistryDependencies,
1892
+ );
1893
+ const modelPresetRegistryFingerprint = JSON.stringify({
1894
+ revision: acceptedPresets.revision,
1895
+ manifestSha256: acceptedPresets.manifestSha256,
1896
+ disabled: acceptedPresets.disabled,
1897
+ error: acceptedPresets.error,
1898
+ });
1611
1899
  if (
1612
1900
  this.#staticModelsLoaded &&
1613
1901
  currentMtime === this.#lastStaticLoadMtime &&
1614
1902
  disabledProviderKey === this.#lastDisabledProviderKey &&
1615
- environmentFingerprint === this.#lastStaticLoadEnvironmentFingerprint
1903
+ environmentFingerprint === this.#lastStaticLoadEnvironmentFingerprint &&
1904
+ modelPresetRegistryFingerprint === this.#lastModelPresetRegistryFingerprint
1616
1905
  ) {
1617
1906
  // models.json and settings-derived implicit provider state are unchanged.
1618
1907
  return;
@@ -1620,6 +1909,7 @@ export class ModelRegistry {
1620
1909
  this.#modelsConfigFile.invalidate();
1621
1910
  this.#customProviderApiKeys.clear();
1622
1911
  this.#customProviderApiKeyEnvNames.clear();
1912
+ this.#customProviderAuthHeaders.clear();
1623
1913
  this.#providerWebSearchModes.clear();
1624
1914
  this.#keylessProviders.clear();
1625
1915
  this.#optionalAuthProviders.clear();
@@ -1631,7 +1921,7 @@ export class ModelRegistry {
1631
1921
  // Drop config-sourced apiKeys from AuthStorage before reload; entries
1632
1922
  // removed from models.yml must actually disappear from the resolver, not
1633
1923
  // linger from the previous parse. The post-load setters below repopulate.
1634
- this.authStorage.clearConfigApiKeys();
1924
+ this.authStorage.clearConfigApiKeys(this.#authStorageConfigOwner);
1635
1925
  // Runtime provider keys are reapplied after #loadModels so they retain
1636
1926
  // registration-time precedence over colliding static provider keys.
1637
1927
  this.#providerOverrides.clear();
@@ -1641,10 +1931,70 @@ export class ModelRegistry {
1641
1931
  this.#configError = undefined;
1642
1932
  this.#loadModels();
1643
1933
  for (const [provider, apiKeyConfig] of this.#runtimeProviderApiKeys) {
1644
- const resolved = resolveApiKeyConfig(apiKeyConfig);
1934
+ const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
1935
+ ? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
1936
+ : resolveApiKeyConfig(apiKeyConfig);
1937
+ if (!resolved) {
1938
+ this.#runtimeProviderCredentialInstalled.delete(provider);
1939
+ const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
1940
+ if (authHeader === true) {
1941
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
1942
+ if (overlay.provider !== provider) return overlay;
1943
+ const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
1944
+ [GENERATED_AUTH_HEADER]?: string;
1945
+ };
1946
+ const generated = headers[GENERATED_AUTH_HEADER];
1947
+ if (typeof generated === "string" && headers.Authorization === generated)
1948
+ delete headers.Authorization;
1949
+ delete headers[GENERATED_AUTH_HEADER];
1950
+ return { ...overlay, headers };
1951
+ });
1952
+ }
1953
+ const override = this.#runtimeProviderOverrides.get(provider);
1954
+ if (override) this.#runtimeProviderOverrides.set(provider, { ...override, apiKey: "" });
1955
+ continue;
1956
+ }
1957
+ this.#customProviderApiKeys.set(provider, resolved);
1958
+ this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
1959
+ this.#runtimeProviderCredentialInstalled.add(provider);
1960
+ this.authStorage.setConfigApiKey(provider, resolved, { owner: this.#authStorageConfigOwner });
1961
+ const override = this.#runtimeProviderOverrides.get(provider);
1962
+ if (override) this.#runtimeProviderOverrides.set(provider, { ...override, apiKey: resolved });
1963
+ const authHeader = this.#runtimeProviderAuthHeaders.get(provider);
1964
+ if (authHeader === true) {
1965
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
1966
+ if (overlay.provider !== provider) return overlay;
1967
+ const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
1968
+ [GENERATED_AUTH_HEADER]?: string;
1969
+ };
1970
+ const generated = headers[GENERATED_AUTH_HEADER];
1971
+ const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
1972
+ const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated);
1973
+ if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
1974
+ if (resolved && (ownsAuthorization || !hadAuthorization))
1975
+ return {
1976
+ ...overlay,
1977
+ headers: {
1978
+ ...headers,
1979
+ Authorization: `Bearer ${resolved}`,
1980
+ [GENERATED_AUTH_HEADER]: `Bearer ${resolved}`,
1981
+ },
1982
+ };
1983
+ delete headers[GENERATED_AUTH_HEADER];
1984
+ return { ...overlay, headers };
1985
+ });
1986
+ }
1987
+ }
1988
+ this.#loadModels();
1989
+ for (const [provider, apiKeyConfig] of this.#runtimeProviderApiKeys) {
1990
+ const resolved = this.#runtimeProviderApiKeyEnvNames.has(provider)
1991
+ ? $rotatingCredentialEnv(this.#runtimeProviderApiKeyEnvNames.get(provider)!)
1992
+ : resolveApiKeyConfig(apiKeyConfig);
1645
1993
  if (!resolved) continue;
1646
1994
  this.#customProviderApiKeys.set(provider, resolved);
1647
- this.authStorage.setConfigApiKey(provider, resolved);
1995
+ this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
1996
+ this.#runtimeProviderCredentialInstalled.add(provider);
1997
+ this.authStorage.setConfigApiKey(provider, resolved, { owner: this.#authStorageConfigOwner });
1648
1998
  }
1649
1999
  this.#lastDisabledProviderKey = disabledProviderKey;
1650
2000
  }
@@ -1670,7 +2020,6 @@ export class ModelRegistry {
1670
2020
  profiles,
1671
2021
  error: configError,
1672
2022
  } = this.#loadCustomModels();
1673
- this.#configError = configError;
1674
2023
  this.#keylessProviders = keylessProviders;
1675
2024
  this.#discoveryManager.setProviders(discoverableProviders);
1676
2025
  this.#configuredProviderIds = new Set(configuredProviders);
@@ -1681,25 +2030,59 @@ export class ModelRegistry {
1681
2030
  this.#codexContextWindowOverrides = this.#collectCodexContextWindowOverrides();
1682
2031
  this.#equivalenceConfig = equivalence;
1683
2032
  this.#modelBindingsApplier.setBindings(modelBindings);
1684
- this.#modelProfiles = mergeModelProfiles(profiles);
2033
+ const acceptedPresets = loadAcceptedModelPresetProfiles(
2034
+ this.#modelPresetRegistryAgentDir,
2035
+ this.#modelPresetRegistryDependencies,
2036
+ );
2037
+ const acceptedRegistryError = acceptedPresets.error
2038
+ ? new ConfigError("model-preset-registry", undefined, {
2039
+ err: new Error(acceptedPresets.error),
2040
+ stage: "Registry",
2041
+ })
2042
+ : undefined;
2043
+ this.#configError = configError ?? acceptedRegistryError;
1685
2044
 
1686
2045
  this.#addImplicitDiscoverableProviders(configuredProviders);
1687
2046
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
2047
+ const registryModels = this.#applyHardcodedModelPolicies(acceptedPresets.presets);
2048
+ const acceptedRegistryModelKeys = new Set(registryModels.map(model => `${model.provider}\u0000${model.id}`));
1688
2049
  const cachedStandardModels = this.#applyHardcodedModelPolicies(this.#loadCachedStandardProviderModels());
1689
2050
  const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
1690
- const resolvedDefaults = this.#mergeResolvedModels(
2051
+ const resolvedProviderCatalog = this.#mergeResolvedModels(
1691
2052
  this.#mergeResolvedModels(builtInModels, cachedStandardModels),
1692
2053
  cachedDiscoveries,
1693
2054
  );
2055
+ const resolvedDefaults = this.#mergeRegistryModelMetadata(resolvedProviderCatalog, registryModels, overrides);
2056
+ const materializedRegistryModelKeys = new Set(
2057
+ resolvedDefaults
2058
+ .map(model => `${model.provider}\u0000${model.id}`)
2059
+ .filter(key => acceptedRegistryModelKeys.has(key)),
2060
+ );
2061
+ this.#registryModelKeys = materializedRegistryModelKeys;
1694
2062
  const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
1695
2063
  // Merge runtime extension models so they survive refresh() cycles
1696
2064
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1697
2065
  const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
1698
2066
  this.#models = this.#finalizeModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
2067
+ this.#modelProfiles = mergeModelProfiles(
2068
+ profiles,
2069
+ filterMaterializedRegistryProfiles(
2070
+ acceptedPresets.profiles,
2071
+ this.#models,
2072
+ new Set(acceptedPresets.dynamicProviders),
2073
+ ),
2074
+ );
1699
2075
  this.#rebuildProviderActivity();
1700
2076
  this.#rebuildCanonicalIndex();
1701
2077
  this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
1702
2078
  this.#lastStaticLoadEnvironmentFingerprint = this.#getStaticLoadEnvironmentFingerprint();
2079
+ this.#lastModelPresetRegistryFingerprint = JSON.stringify({
2080
+ revision: acceptedPresets.revision,
2081
+ manifestSha256: acceptedPresets.manifestSha256,
2082
+ disabled: acceptedPresets.disabled,
2083
+ error: acceptedPresets.error,
2084
+ });
2085
+ this.#loadedModelPresetRegistryManifestSha256 = acceptedPresets.manifestSha256;
1703
2086
  this.#staticModelsLoaded = true;
1704
2087
  }
1705
2088
 
@@ -1716,6 +2099,10 @@ export class ModelRegistry {
1716
2099
  }
1717
2100
  for (const overlay of [...this.#customModelOverlays, ...this.#runtimeModelOverlays])
1718
2101
  addStaticModel(overlay.provider, overlay.id);
2102
+ for (const model of this.#models) {
2103
+ if (this.#registryModelKeys.has(`${model.provider}\u0000${model.id}`))
2104
+ addStaticModel(model.provider, model.id);
2105
+ }
1719
2106
 
1720
2107
  const runtimeProviderIds = new Set(this.#runtimeProviderSourceByName.keys());
1721
2108
  const providerIds = new Set<string>([
@@ -1794,6 +2181,86 @@ export class ModelRegistry {
1794
2181
  return merged;
1795
2182
  }
1796
2183
 
2184
+ #mergeRegistryModelMetadata(
2185
+ baseModels: Model<Api>[],
2186
+ registryModels: Model<Api>[],
2187
+ providerOverrides: ReadonlyMap<string, ProviderOverride>,
2188
+ ): Model<Api>[] {
2189
+ const merged = [...baseModels];
2190
+ const indexByKey = new Map(merged.map((model, index) => [`${model.provider}\u0000${model.id}`, index]));
2191
+ for (const registryModel of registryModels) {
2192
+ const key = `${registryModel.provider}\u0000${registryModel.id}`;
2193
+ const existingIndex = indexByKey.get(key);
2194
+ const explicitTransport = this.#resolveProviderOverride(registryModel.provider, providerOverrides);
2195
+ if (existingIndex === undefined) {
2196
+ const transportTemplates = merged.filter(
2197
+ model => model.provider === registryModel.provider && model.api === registryModel.api,
2198
+ );
2199
+ const transportTemplate = transportTemplates[0];
2200
+ const explicitTransportMatches =
2201
+ explicitTransport?.api === registryModel.api && explicitTransport.baseUrl !== undefined;
2202
+ const templatesAgree =
2203
+ transportTemplate !== undefined &&
2204
+ transportTemplates.every(template => hasMatchingRegistryTransport(transportTemplate, template));
2205
+ if (!explicitTransportMatches && transportTemplates.length > 0 && !templatesAgree) continue;
2206
+ if (!transportTemplate && !explicitTransportMatches) continue;
2207
+ const transportSource = explicitTransportMatches ? explicitTransport : transportTemplate!;
2208
+ const inheritedCompat = explicitTransportMatches
2209
+ ? explicitTransport?.compat
2210
+ : commonRegistryTransportCompat(transportTemplates);
2211
+ const hydratedRegistryModel = {
2212
+ ...registryModel,
2213
+ baseUrl: transportSource.baseUrl!,
2214
+ headers: transportSource.headers,
2215
+ transport: transportSource.transport,
2216
+ requestTransform: transportSource.requestTransform,
2217
+ cacheRetention: transportSource.cacheRetention,
2218
+ isOAuth: transportSource.isOAuth,
2219
+ compat:
2220
+ inheritedCompat || registryModel.compat || explicitTransport?.compat
2221
+ ? { ...inheritedCompat, ...registryModel.compat, ...explicitTransport?.compat }
2222
+ : undefined,
2223
+ };
2224
+ merged.push(
2225
+ explicitTransportMatches
2226
+ ? this.#applyProviderTransportOverride(hydratedRegistryModel, explicitTransport!)
2227
+ : hydratedRegistryModel,
2228
+ );
2229
+ indexByKey.set(key, merged.length - 1);
2230
+ continue;
2231
+ }
2232
+ const existing = merged[existingIndex]!;
2233
+ const effectiveApi = explicitTransport?.api ?? existing.api;
2234
+ const registryApiMatchesEffectiveTransport = registryModel.api === effectiveApi;
2235
+ const registryMetadata = registryApiMatchesEffectiveTransport
2236
+ ? registryModel
2237
+ : registryModelMetadataWithoutApiSpecificFields(registryModel);
2238
+ merged[existingIndex] = {
2239
+ ...existing,
2240
+ ...registryMetadata,
2241
+ api: explicitTransport?.api ?? existing.api,
2242
+ baseUrl: existing.baseUrl,
2243
+ headers: existing.headers,
2244
+ transport: existing.transport,
2245
+ requestTransform: existing.requestTransform,
2246
+ cacheRetention: existing.cacheRetention,
2247
+ isOAuth: explicitTransport?.isOAuth ?? existing.isOAuth,
2248
+ wireModelId: existing.wireModelId,
2249
+ compat:
2250
+ existing.compat ||
2251
+ (registryApiMatchesEffectiveTransport ? registryModel.compat : undefined) ||
2252
+ explicitTransport?.compat
2253
+ ? {
2254
+ ...existing.compat,
2255
+ ...(registryApiMatchesEffectiveTransport ? registryModel.compat : undefined),
2256
+ ...explicitTransport?.compat,
2257
+ }
2258
+ : undefined,
2259
+ };
2260
+ }
2261
+ return merged;
2262
+ }
2263
+
1797
2264
  /** Merge custom models with built-in, replacing by provider+id match */
1798
2265
  #mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
1799
2266
  const merged = [...builtInModels];
@@ -1828,6 +2295,7 @@ export class ModelRegistry {
1828
2295
  // re-merge bundled transport metadata here.
1829
2296
  headers: customModel.headers,
1830
2297
  compat: customModel.compat,
2298
+ transport: customModel.transport,
1831
2299
  contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
1832
2300
  wireModelId: customModel.wireModelId,
1833
2301
  requestTransform: customModel.requestTransform,
@@ -2104,6 +2572,11 @@ export class ModelRegistry {
2104
2572
  const configuredProviders = new Set(Object.keys(value.providers ?? {}));
2105
2573
 
2106
2574
  for (const [providerName, providerConfig] of providerEntries) {
2575
+ const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
2576
+ const isOAuth = resolveCustomModelIsOAuth(
2577
+ (providerConfig.api as Api | undefined) ?? "openai-completions",
2578
+ providerConfig.auth as ProviderAuthMode | undefined,
2579
+ );
2107
2580
  if (providerConfig.apiKeyEnv) {
2108
2581
  this.#configuredApiKeyEnvNames.add(providerConfig.apiKeyEnv);
2109
2582
  }
@@ -2113,12 +2586,16 @@ export class ModelRegistry {
2113
2586
  if (providerConfig.openaiCompat?.apiKey)
2114
2587
  this.#configuredApiKeyEnvNames.add(providerConfig.openaiCompat.apiKey);
2115
2588
  if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
2116
- const providerApiKeyConfig = providerConfig.apiKey ?? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
2589
+ const providerApiKeyConfig = providerConfig.apiKey
2590
+ ? resolveApiKeyConfig(providerConfig.apiKey)
2591
+ : resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
2117
2592
  const localOpenAICompat = providerConfig.openaiCompat;
2118
2593
  const rotatingApiKeyEnv = providerConfig.apiKey
2119
2594
  ? undefined
2120
2595
  : (providerConfig.apiKeyEnv ?? (localOpenAICompat?.apiKey ? undefined : localOpenAICompat?.apiKeyEnv));
2121
2596
  if (rotatingApiKeyEnv) this.#customProviderApiKeyEnvNames.set(providerName, rotatingApiKeyEnv);
2597
+ if (providerConfig.authHeader !== undefined)
2598
+ this.#customProviderAuthHeaders.set(providerName, providerConfig.authHeader);
2122
2599
  const localOpenAICompatApiKeyConfig = localOpenAICompat
2123
2600
  ? localOpenAICompat.apiKey
2124
2601
  ? resolveApiKeyConfig(localOpenAICompat.apiKey)
@@ -2132,8 +2609,10 @@ export class ModelRegistry {
2132
2609
  ? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
2133
2610
  : undefined;
2134
2611
  overrides.set(providerName, {
2612
+ api: "openai-completions",
2135
2613
  baseUrl: localOpenAICompatBaseUrl,
2136
2614
  apiKey: localOpenAICompatApiKeyConfig,
2615
+ isOAuth,
2137
2616
  compat: {
2138
2617
  supportsStore: false,
2139
2618
  supportsDeveloperRole: false,
@@ -2156,6 +2635,7 @@ export class ModelRegistry {
2156
2635
  this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
2157
2636
  this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey, {
2158
2637
  envSourced: !localOpenAICompat.apiKey,
2638
+ owner: this.#authStorageConfigOwner,
2159
2639
  });
2160
2640
  } else {
2161
2641
  keylessProviders.add(providerName);
@@ -2177,6 +2657,7 @@ export class ModelRegistry {
2177
2657
  ) {
2178
2658
  const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
2179
2659
  overrides.set(providerName, {
2660
+ api: providerConfig.api as Api | undefined,
2180
2661
  baseUrl: providerConfig.baseUrl,
2181
2662
  headers: providerConfig.headers,
2182
2663
  apiKey: providerApiKeyConfig,
@@ -2185,10 +2666,10 @@ export class ModelRegistry {
2185
2666
  transport: providerConfig.transport,
2186
2667
  requestTransform: providerConfig.requestTransform,
2187
2668
  cacheRetention: providerConfig.cacheRetention,
2669
+ isOAuth,
2188
2670
  });
2189
2671
  }
2190
2672
 
2191
- const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
2192
2673
  if (authMode === "none") {
2193
2674
  keylessProviders.add(providerName);
2194
2675
  }
@@ -2219,7 +2700,10 @@ export class ModelRegistry {
2219
2700
  : undefined;
2220
2701
  if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
2221
2702
  if (resolved) {
2222
- this.authStorage.setConfigApiKey(providerName, resolved, { envSourced: !providerConfig.apiKey });
2703
+ this.authStorage.setConfigApiKey(providerName, resolved, {
2704
+ envSourced: !providerConfig.apiKey,
2705
+ owner: this.#authStorageConfigOwner,
2706
+ });
2223
2707
  }
2224
2708
  }
2225
2709
 
@@ -2275,7 +2759,7 @@ export class ModelRegistry {
2275
2759
  );
2276
2760
  }
2277
2761
  const current = loaded.value ?? this.#modelsConfigFile.createDefault();
2278
- if (mergeModelProfiles(current.profiles).has(normalizedName)) {
2762
+ if (current.profiles?.[normalizedName] !== undefined) {
2279
2763
  throw new Error(`Custom model profile already exists: ${normalizedName}. Choose a unique preset id.`);
2280
2764
  }
2281
2765
  const next: ModelsConfig = {
@@ -2401,6 +2885,8 @@ export class ModelRegistry {
2401
2885
  async #refreshRuntimeDiscoveries(
2402
2886
  strategy: ModelRefreshStrategy,
2403
2887
  providerFilter?: ReadonlySet<string>,
2888
+ refreshGeneration = this.#catalogRefreshGeneration,
2889
+ providerRefresh?: ProviderRefreshFence,
2404
2890
  ): Promise<void> {
2405
2891
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
2406
2892
  const selectedDiscoverableProviders = (
@@ -2412,12 +2898,22 @@ export class ModelRegistry {
2412
2898
  selectedDiscoverableProviders.length === 0
2413
2899
  ? Promise.resolve([] as ConfiguredDiscoveryResult[])
2414
2900
  : Promise.all(
2415
- selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
2901
+ selectedDiscoverableProviders.map(provider =>
2902
+ this.#discoverProviderModels(provider, strategy, providerRefresh),
2903
+ ),
2416
2904
  );
2417
2905
  const [configuredDiscoveryResults, builtInDiscovered] = await Promise.all([
2418
2906
  configuredDiscoveriesPromise,
2419
- this.#discoverBuiltInProviderModels(strategy, providerFilter),
2907
+ this.#discoverBuiltInProviderModels(strategy, providerFilter, providerRefresh),
2420
2908
  ]);
2909
+ if (
2910
+ refreshGeneration !== this.#catalogRefreshGeneration ||
2911
+ (providerRefresh !== undefined &&
2912
+ "providerId" in providerRefresh &&
2913
+ this.#providerRefreshGenerations.get(providerRefresh.providerId) !== providerRefresh.generation)
2914
+ ) {
2915
+ return;
2916
+ }
2421
2917
  const currentConfiguredDiscoveryResults = configuredDiscoveryResults.map(result => {
2422
2918
  const providerConfig = selectedDiscoverableProviders.find(provider => provider.provider === result.provider);
2423
2919
  const current =
@@ -2584,6 +3080,7 @@ export class ModelRegistry {
2584
3080
  async #discoverProviderModels(
2585
3081
  providerConfig: DiscoveryProviderConfig,
2586
3082
  strategy: ModelRefreshStrategy,
3083
+ providerRefresh?: ProviderRefreshFence,
2587
3084
  ): Promise<ConfiguredDiscoveryResult> {
2588
3085
  const provider = providerConfig.provider;
2589
3086
  const preflightEpoch = this.#optionalAuthPreflightEpoch;
@@ -2598,7 +3095,8 @@ export class ModelRegistry {
2598
3095
  let preflightCompleted = false;
2599
3096
  const optionalAuth = this.#optionalAuthProviders.has(provider);
2600
3097
  const shouldPreflightAuth = optionalAuth
2601
- ? this.authStorage.has(provider) || this.authStorage.hasAuth(provider)
3098
+ ? this.authStorage.has(provider) ||
3099
+ this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
2602
3100
  : !this.#isCredentiallessProvider(provider);
2603
3101
  let preflightAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
2604
3102
  let preflightOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
@@ -2639,7 +3137,11 @@ export class ModelRegistry {
2639
3137
  preflightCompleted = true;
2640
3138
  if (!preflightFailed && optionalAuth && isCurrentPreflight()) {
2641
3139
  this.#providerEvidenceApiKeys.set(provider, apiKey);
2642
- const authGeneration = this.authStorage.getProviderEvidenceGeneration(provider, apiKey);
3140
+ const authGeneration = this.authStorage.getProviderEvidenceGeneration(
3141
+ provider,
3142
+ apiKey,
3143
+ this.#authStorageConfigOwner,
3144
+ );
2643
3145
  if (apiKey === undefined) this.#credentiallessAuthFallbackProviders.set(provider, authGeneration);
2644
3146
  else this.#credentiallessAuthFallbackProviders.delete(provider);
2645
3147
  }
@@ -2688,6 +3190,14 @@ export class ModelRegistry {
2688
3190
  const isCurrentEndpoint = () =>
2689
3191
  endpoint ===
2690
3192
  this.#normalizeDiscoveryEvidenceEndpoint(this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "");
3193
+ const isCurrentProviderRefresh = () =>
3194
+ providerRefresh === undefined ||
3195
+ ("providerId" in providerRefresh
3196
+ ? strategy !== "online-if-uncached" ||
3197
+ (providerRefresh.providerId === provider &&
3198
+ this.#providerRefreshGenerations.get(provider) === providerRefresh.generation)
3199
+ : (this.#providerRefreshGenerations.get(provider) ?? 0) ===
3200
+ (providerRefresh.generations.get(provider) ?? 0));
2691
3201
  const evidence = this.#configuredDiscoveryEvidence.get(provider);
2692
3202
  const cacheDynamicModelProvenance = fingerprintConfiguredDiscoveryRequestShape(
2693
3203
  effectiveProviderConfig,
@@ -2736,7 +3246,7 @@ export class ModelRegistry {
2736
3246
  ),
2737
3247
  getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
2738
3248
  cacheDynamicModelProvenance: cacheLookupProvenance,
2739
- canPublishCache: isCurrentEndpoint,
3249
+ canPublishCache: () => isCurrentEndpoint() && isCurrentProviderRefresh(),
2740
3250
  });
2741
3251
  const authGeneration =
2742
3252
  mergeInput.authGeneration ??
@@ -2792,7 +3302,7 @@ export class ModelRegistry {
2792
3302
  return {
2793
3303
  ...providerConfig,
2794
3304
  baseUrl: effectiveBaseUrl,
2795
- headers: override?.headers ? { ...providerConfig.headers, ...override.headers } : providerConfig.headers,
3305
+ headers: mergeCaseInsensitiveHeaders(providerConfig.headers, override?.headers),
2796
3306
  compat: override?.compat ? mergeCompat(providerConfig.compat, override.compat) : providerConfig.compat,
2797
3307
  requestTransform: mergeRequestTransform(providerConfig.requestTransform, override?.requestTransform),
2798
3308
  cacheRetention: override?.cacheRetention ?? providerConfig.cacheRetention,
@@ -2822,6 +3332,7 @@ export class ModelRegistry {
2822
3332
  async #discoverBuiltInProviderModels(
2823
3333
  strategy: ModelRefreshStrategy,
2824
3334
  providerFilter?: ReadonlySet<string>,
3335
+ providerRefresh?: ProviderRefreshFence,
2825
3336
  ): Promise<Model<Api>[]> {
2826
3337
  // Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
2827
3338
  const configuredDiscoveryProviders = new Set(this.#discoveryManager.providers.map(p => p.provider));
@@ -2839,7 +3350,7 @@ export class ModelRegistry {
2839
3350
  return [];
2840
3351
  }
2841
3352
  const discoveries = await Promise.all(
2842
- managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy)),
3353
+ managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy, providerRefresh)),
2843
3354
  );
2844
3355
  return discoveries.flat();
2845
3356
  }
@@ -2874,7 +3385,12 @@ export class ModelRegistry {
2874
3385
  providerId: "openai-codex",
2875
3386
  resolveKey: value => value,
2876
3387
  createOptions: accessToken => {
2877
- const accountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", accessToken);
3388
+ const accountId = resolveOAuthAccountIdForAccessToken(
3389
+ this.authStorage,
3390
+ "openai-codex",
3391
+ accessToken,
3392
+ this.#authStorageConfigOwner,
3393
+ );
2878
3394
  return openaiCodexModelManagerOptions({
2879
3395
  accessToken,
2880
3396
  accountId,
@@ -2967,6 +3483,7 @@ export class ModelRegistry {
2967
3483
  async #discoverWithModelManager(
2968
3484
  { options, authGeneration, apiKey, endpoint, endpointContainsUserinfo }: ModelManagerDiscoveryOptions,
2969
3485
  strategy: ModelRefreshStrategy,
3486
+ providerRefresh?: ProviderRefreshFence,
2970
3487
  ): Promise<Model<Api>[]> {
2971
3488
  const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
2972
3489
  this.#descriptorDiscoveryGenerations.set(options.providerId, generation);
@@ -2987,18 +3504,34 @@ export class ModelRegistry {
2987
3504
  // stale fallback, and online-if-uncached reuse all fail closed.
2988
3505
  const cacheDynamicModelProvenance =
2989
3506
  reusableCacheProvenance ?? `gajae:non-cacheable-endpoint:${crypto.randomUUID()}`;
2990
- const isCurrentDiscovery = () =>
3507
+ const isCurrentDiscoveryContext = () =>
2991
3508
  (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
2992
3509
  this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
2993
3510
  (endpoint ===
2994
3511
  this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(options.providerId) ?? "") ||
2995
3512
  (canUseCredentialDerivedXiaomiEndpoint && credentialDerivedEndpoint !== undefined));
3513
+ const isCurrentProviderRefresh = () =>
3514
+ providerRefresh === undefined ||
3515
+ ("providerId" in providerRefresh
3516
+ ? providerRefresh.providerId === options.providerId &&
3517
+ this.#providerRefreshGenerations.get(options.providerId) === providerRefresh.generation
3518
+ : (this.#providerRefreshGenerations.get(options.providerId) ?? 0) ===
3519
+ (providerRefresh.generations.get(options.providerId) ?? 0));
3520
+ const isCurrentDiscovery = () =>
3521
+ isCurrentDiscoveryContext() &&
3522
+ (providerRefresh === undefined || "generations" in providerRefresh || isCurrentProviderRefresh());
3523
+ const canPublishCache = () =>
3524
+ isCurrentDiscoveryContext() &&
3525
+ (providerRefresh === undefined ||
3526
+ ("providerId" in providerRefresh
3527
+ ? strategy !== "online-if-uncached" || isCurrentDiscovery()
3528
+ : isCurrentProviderRefresh()));
2996
3529
  try {
2997
3530
  const manager = createModelManager({
2998
3531
  ...options,
2999
3532
  cacheDbPath: this.#cacheDbPath,
3000
3533
  cacheDynamicModelProvenance,
3001
- canPublishCache: isCurrentDiscovery,
3534
+ canPublishCache,
3002
3535
  ...(options.fetchDynamicModels
3003
3536
  ? {
3004
3537
  fetchDynamicModels: async () => {
@@ -3159,8 +3692,15 @@ export class ModelRegistry {
3159
3692
  discoveryApiKey ??
3160
3693
  (this.#isCredentiallessProvider(providerConfig.provider)
3161
3694
  ? kNoAuth
3162
- : await this.authStorage.getApiKey(providerConfig.provider));
3163
- if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
3695
+ : await this.authStorage.getApiKey(providerConfig.provider, undefined, {
3696
+ owner: this.#authStorageConfigOwner,
3697
+ }));
3698
+ if (
3699
+ apiKey &&
3700
+ apiKey !== DEFAULT_LOCAL_TOKEN &&
3701
+ apiKey !== kNoAuth &&
3702
+ headerValue(headers, "Authorization") === undefined
3703
+ ) {
3164
3704
  headers.Authorization = `Bearer ${apiKey}`;
3165
3705
  }
3166
3706
  const response = await fetch(tagsUrl, {
@@ -3239,8 +3779,15 @@ export class ModelRegistry {
3239
3779
  discoveryApiKey ??
3240
3780
  (this.#isCredentiallessProvider(providerConfig.provider)
3241
3781
  ? kNoAuth
3242
- : await this.authStorage.getApiKey(providerConfig.provider));
3243
- if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
3782
+ : await this.authStorage.getApiKey(providerConfig.provider, undefined, {
3783
+ owner: this.#authStorageConfigOwner,
3784
+ }));
3785
+ if (
3786
+ apiKey &&
3787
+ apiKey !== DEFAULT_LOCAL_TOKEN &&
3788
+ apiKey !== kNoAuth &&
3789
+ headerValue(requestHeaders, "Authorization") === undefined
3790
+ ) {
3244
3791
  requestHeaders.Authorization = `Bearer ${apiKey}`;
3245
3792
  }
3246
3793
 
@@ -3361,12 +3908,16 @@ export class ModelRegistry {
3361
3908
  discoveryApiKey ??
3362
3909
  (this.#isCredentiallessProvider(providerConfig.provider)
3363
3910
  ? kNoAuth
3364
- : await this.authStorage.getApiKey(providerConfig.provider, undefined, { baseUrl }));
3911
+ : await this.authStorage.getApiKey(providerConfig.provider, undefined, {
3912
+ baseUrl,
3913
+ owner: this.#authStorageConfigOwner,
3914
+ }));
3365
3915
  if (
3366
3916
  apiKey &&
3367
3917
  apiKey !== DEFAULT_LOCAL_TOKEN &&
3368
3918
  !isVllmNoAuthToken(providerConfig.provider, apiKey) &&
3369
- apiKey !== kNoAuth
3919
+ apiKey !== kNoAuth &&
3920
+ headerValue(requestHeaders, "Authorization") === undefined
3370
3921
  ) {
3371
3922
  requestHeaders.Authorization = `Bearer ${apiKey}`;
3372
3923
  }
@@ -3508,7 +4059,11 @@ export class ModelRegistry {
3508
4059
  try {
3509
4060
  fallbackMatchesCurrentEvidence =
3510
4061
  fallbackEvidenceGeneration ===
3511
- this.authStorage.getProviderEvidenceGeneration(provider, this.#providerEvidenceApiKeys.get(provider));
4062
+ this.authStorage.getProviderEvidenceGeneration(
4063
+ provider,
4064
+ this.#providerEvidenceApiKeys.get(provider),
4065
+ this.#authStorageConfigOwner,
4066
+ );
3512
4067
  } catch {
3513
4068
  // AuthStorage may be unavailable while a registry is being torn down.
3514
4069
  }
@@ -3516,7 +4071,8 @@ export class ModelRegistry {
3516
4071
  return (
3517
4072
  this.#keylessProviders.has(provider) &&
3518
4073
  (!this.#optionalAuthProviders.has(provider) ||
3519
- (!this.authStorage.hasAuth(provider) && !this.authStorage.has(provider)) ||
4074
+ (!this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner }) &&
4075
+ !this.authStorage.has(provider)) ||
3520
4076
  fallbackMatchesCurrentEvidence)
3521
4077
  );
3522
4078
  }
@@ -3527,6 +4083,7 @@ export class ModelRegistry {
3527
4083
  return this.authStorage.getProviderEvidenceGeneration(
3528
4084
  provider,
3529
4085
  resolvedApiKey ?? this.#providerEvidenceApiKeys.get(provider),
4086
+ this.#authStorageConfigOwner,
3530
4087
  );
3531
4088
  }
3532
4089
  #normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
@@ -3564,7 +4121,7 @@ export class ModelRegistry {
3564
4121
 
3565
4122
  #resolveProviderOverride(
3566
4123
  provider: string,
3567
- overrides: Map<string, ProviderOverride> = this.#providerOverrides,
4124
+ overrides: ReadonlyMap<string, ProviderOverride> = this.#providerOverrides,
3568
4125
  ): ProviderOverride | undefined {
3569
4126
  const explicitOverride = overrides.get(provider);
3570
4127
  if (explicitOverride?.baseUrl) {
@@ -3591,24 +4148,19 @@ export class ModelRegistry {
3591
4148
 
3592
4149
  #mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
3593
4150
  return {
4151
+ api: override.api ?? baseOverride?.api,
3594
4152
  baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
3595
4153
  apiKey: override.apiKey ?? baseOverride?.apiKey,
3596
4154
  authHeader: override.authHeader ?? baseOverride?.authHeader,
3597
- headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
4155
+ headers: mergeCaseInsensitiveHeaders(baseOverride?.headers, override.headers),
3598
4156
  compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
3599
4157
  transport: override.transport ?? baseOverride?.transport,
3600
4158
  requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
3601
4159
  cacheRetention: override.cacheRetention ?? baseOverride?.cacheRetention,
4160
+ isOAuth: override.isOAuth ?? baseOverride?.isOAuth,
3602
4161
  };
3603
4162
  }
3604
- #applyProviderTransportOverride<
3605
- T extends {
3606
- baseUrl?: string;
3607
- headers?: Record<string, string>;
3608
- compat?: Model<Api>["compat"];
3609
- cacheRetention?: CacheRetention;
3610
- },
3611
- >(
4163
+ #applyProviderTransportOverride<T extends Model<Api>>(
3612
4164
  entry: T,
3613
4165
  override: Pick<
3614
4166
  ProviderOverride,
@@ -3622,12 +4174,39 @@ export class ModelRegistry {
3622
4174
  | "cacheRetention"
3623
4175
  >,
3624
4176
  ): T {
3625
- const headers = mergeAuthHeader(
3626
- override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
3627
- override.authHeader,
3628
- override.apiKey,
3629
- );
3630
- return {
4177
+ const overrideHeaders = (
4178
+ override.headers ? { ...entry.headers, ...override.headers } : { ...entry.headers }
4179
+ ) as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string };
4180
+ const explicitAuthKey = override.headers
4181
+ ? Object.keys(override.headers).find(key => key.toLowerCase() === "authorization")
4182
+ : undefined;
4183
+ if (explicitAuthKey !== undefined) {
4184
+ const explicitAuthValue = override.headers![explicitAuthKey]!;
4185
+ deleteHeaderCaseInsensitive(overrideHeaders, "Authorization");
4186
+ overrideHeaders[explicitAuthKey] = explicitAuthValue;
4187
+ delete (overrideHeaders as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4188
+ GENERATED_AUTH_HEADER
4189
+ ];
4190
+ }
4191
+ const existingMarker = (overrideHeaders as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4192
+ GENERATED_AUTH_HEADER
4193
+ ];
4194
+ const managesAuth = override.authHeader !== undefined || override.apiKey !== undefined;
4195
+ const canInject =
4196
+ managesAuth &&
4197
+ override.authHeader === true &&
4198
+ Boolean(override.apiKey) &&
4199
+ (headerValue(overrideHeaders, "Authorization") === undefined ||
4200
+ ownsOnlyGeneratedAuthorization(overrideHeaders, existingMarker));
4201
+ if (canInject) {
4202
+ deleteHeaderCaseInsensitive(overrideHeaders, "Authorization");
4203
+ delete overrideHeaders[GENERATED_AUTH_HEADER];
4204
+ } else if (managesAuth && existingMarker !== undefined) {
4205
+ deleteAuthorizationValue(overrideHeaders, existingMarker);
4206
+ delete overrideHeaders[GENERATED_AUTH_HEADER];
4207
+ }
4208
+ const headers = mergeAuthHeader(overrideHeaders, canInject, canInject ? override.apiKey : undefined);
4209
+ const result = {
3631
4210
  ...entry,
3632
4211
  compat: mergeProviderCompat(entry.compat, override.compat),
3633
4212
  baseUrl: override.baseUrl ?? entry.baseUrl,
@@ -3641,6 +4220,21 @@ export class ModelRegistry {
3641
4220
  ),
3642
4221
  cacheRetention: entry.cacheRetention ?? override.cacheRetention,
3643
4222
  };
4223
+ if (!canInject && explicitAuthKey !== undefined) {
4224
+ delete (result.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4225
+ GENERATED_AUTH_HEADER
4226
+ ];
4227
+ }
4228
+ const generatedHeader = (
4229
+ result.headers as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined
4230
+ )?.[GENERATED_AUTH_HEADER];
4231
+ if (generatedHeader !== undefined && result.headers?.Authorization === generatedHeader) {
4232
+ this.#generatedAuthHeaders.set(result, { authorization: result.headers.Authorization });
4233
+ } else if (explicitAuthKey === undefined) {
4234
+ const generated = this.#generatedAuthHeaders.get(entry);
4235
+ if (generated) this.#generatedAuthHeaders.set(result, generated);
4236
+ }
4237
+ return result;
3644
4238
  }
3645
4239
  #applyRuntimeProviderOverride(model: Model<Api>, override: ProviderOverride): Model<Api> {
3646
4240
  const withTransportOverride = this.#applyProviderTransportOverride(
@@ -3653,26 +4247,53 @@ export class ModelRegistry {
3653
4247
  ? withTransportOverride
3654
4248
  : { ...withTransportOverride, ...(sanitizedBaseUrl === undefined ? {} : { baseUrl: sanitizedBaseUrl }) };
3655
4249
  const modelCompat = this.#modelOverrides.get(model.provider.toLowerCase())?.get(model.id.toLowerCase())?.compat;
3656
- return enrichModelThinking(
4250
+ const result = enrichModelThinking(
3657
4251
  modelCompat
3658
4252
  ? { ...sanitizedTransport, compat: mergeCompat(sanitizedTransport.compat, modelCompat) }
3659
4253
  : sanitizedTransport,
3660
4254
  );
4255
+ const generated = this.#generatedAuthHeaders.get(sanitizedTransport);
4256
+ if (generated) this.#generatedAuthHeaders.set(result, generated);
4257
+ return result;
3661
4258
  }
3662
4259
  #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
3663
4260
  if (this.#runtimeProviderOverrides.size === 0) return models;
3664
4261
  return models.map(model => {
3665
4262
  const override = this.#runtimeProviderOverrides.get(model.provider);
3666
4263
  if (!override) return model;
3667
- return this.#applyRuntimeProviderOverride(model, override);
4264
+ const runtimeCredentialRevoked =
4265
+ this.#runtimeProviderApiKeys.has(model.provider) &&
4266
+ !this.#runtimeProviderCredentialInstalled.has(model.provider);
4267
+ return this.#applyRuntimeProviderOverride(
4268
+ model,
4269
+ runtimeCredentialRevoked ? { ...override, apiKey: undefined, authHeader: undefined } : override,
4270
+ );
3668
4271
  });
3669
4272
  }
3670
4273
  #finalizeModels(models: Model<Api>[]): Model<Api>[] {
3671
- return applyFinalCodexGpt56ContextCap(
3672
- models.map(model => enrichModelThinking({ ...this.#restoreDeclaredThinking(model) })),
3673
- undefined,
3674
- this.#codexContextWindowOverrides,
3675
- );
4274
+ const finalized = models.map(model => enrichModelThinking({ ...this.#restoreDeclaredThinking(model) }));
4275
+ const result = applyFinalCodexGpt56ContextCap(finalized, undefined, this.#codexContextWindowOverrides);
4276
+ for (let index = 0; index < result.length; index++) {
4277
+ if (
4278
+ (result[index]?.headers as (Record<string, string> & { [GENERATED_AUTH_HEADER]?: string }) | undefined)?.[
4279
+ GENERATED_AUTH_HEADER
4280
+ ]
4281
+ ) {
4282
+ const authorization = result[index]?.headers?.Authorization;
4283
+ const marker = (result[index]?.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4284
+ GENERATED_AUTH_HEADER
4285
+ ];
4286
+ if (authorization && marker === authorization)
4287
+ this.#generatedAuthHeaders.set(result[index]!, { authorization });
4288
+ else
4289
+ delete (result[index]?.headers as Record<string, string> & { [GENERATED_AUTH_HEADER]?: string })[
4290
+ GENERATED_AUTH_HEADER
4291
+ ];
4292
+ }
4293
+ const generated = this.#generatedAuthHeaders.get(models[index]!);
4294
+ if (generated) this.#generatedAuthHeaders.set(result[index]!, generated);
4295
+ }
4296
+ return result;
3676
4297
  }
3677
4298
  #restoreDeclaredThinking(model: Model<Api>): Model<Api> {
3678
4299
  const overrideThinking = this.#modelOverrides
@@ -3811,7 +4432,10 @@ export class ModelRegistry {
3811
4432
  : undefined;
3812
4433
  if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
3813
4434
  if (resolved) {
3814
- this.authStorage.setConfigApiKey(providerName, resolved, { envSourced: !providerConfig.apiKey });
4435
+ this.authStorage.setConfigApiKey(providerName, resolved, {
4436
+ envSourced: !providerConfig.apiKey,
4437
+ owner: this.#authStorageConfigOwner,
4438
+ });
3815
4439
  }
3816
4440
  }
3817
4441
  for (const modelDef of modelDefs) {
@@ -3833,6 +4457,7 @@ export class ModelRegistry {
3833
4457
  providerConfig.requestTransform,
3834
4458
  (providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
3835
4459
  providerConfig.cacheRetention,
4460
+ providerConfig.transport,
3836
4461
  modelDef as CustomModelDefinitionLike,
3837
4462
  );
3838
4463
  if (!model) continue;
@@ -3843,8 +4468,8 @@ export class ModelRegistry {
3843
4468
  }
3844
4469
 
3845
4470
  /**
3846
- * Get all models (built-in + custom).
3847
- * If models.json had errors, returns only built-in models.
4471
+ * Get all models (embedded + accepted registry + custom).
4472
+ * If models.yml had errors, returns embedded plus accepted registry models.
3848
4473
  */
3849
4474
  getAll(): Model<Api>[] {
3850
4475
  return this.#models;
@@ -3859,7 +4484,8 @@ export class ModelRegistry {
3859
4484
  const disabled = disabledProviders ?? getDisabledProviderIdsFromSettings(this.#settings);
3860
4485
  return (
3861
4486
  !disabled.has(model.provider) &&
3862
- (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
4487
+ (this.#keylessProviders.has(model.provider) ||
4488
+ this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner }))
3863
4489
  );
3864
4490
  }
3865
4491
  /** Per-query precomputed variant filter inputs; plan fields are authoritative when provided. */
@@ -3913,7 +4539,9 @@ export class ModelRegistry {
3913
4539
  * 6. Unknown (no credential surface)
3914
4540
  */
3915
4541
  #effectiveProviderAuth(provider: string, sessionId?: string): EffectiveProviderAuth {
3916
- const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId);
4542
+ const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId, {
4543
+ owner: this.#authStorageConfigOwner,
4544
+ });
3917
4545
  if (credentialType === "api_key") return "key";
3918
4546
  if (credentialType === "oauth") return "oauth";
3919
4547
  if (this.#keylessProviders.has(provider)) return "keyless";
@@ -3962,6 +4590,22 @@ export class ModelRegistry {
3962
4590
  autoroutingProviderOrder(): readonly string[] {
3963
4591
  return projectCatalogProviderOrder(getConfiguredProviderOrderFromSettings(), this.#models);
3964
4592
  }
4593
+
4594
+ /**
4595
+ * Provider priority for automatic model selection. Explicit provider order
4596
+ * wins first, followed by omitted OAuth providers, then other providers;
4597
+ * catalog order breaks ties within each policy band.
4598
+ */
4599
+ automaticProviderOrder(sessionId?: string): readonly string[] {
4600
+ const policy = this.#buildProviderSelectionPolicy(sessionId);
4601
+ return [...policy.orderedProviders()].sort((left, right) => {
4602
+ const rankDifference = policy.rank(left) - policy.rank(right);
4603
+ return rankDifference !== 0
4604
+ ? rankDifference
4605
+ : policy.providerCatalogIndex(left) - policy.providerCatalogIndex(right);
4606
+ });
4607
+ }
4608
+
3965
4609
  #providerRankMap(policy: ProviderSelectionPolicy): Map<string, number> {
3966
4610
  const providerRank = new Map<string, number>();
3967
4611
  for (const provider of policy.orderedProviders()) {
@@ -4263,9 +4907,10 @@ export class ModelRegistry {
4263
4907
  * This is a fast check that doesn't refresh OAuth tokens.
4264
4908
  */
4265
4909
  getAvailable(): Model<Api>[] {
4910
+ this.#synchronizeEnvironmentCredentials();
4266
4911
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
4267
4912
  const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
4268
- const envFingerprint = envAvailabilityFingerprint();
4913
+ const envFingerprint = envAvailabilityFingerprint(this.#configuredApiKeyEnvNames);
4269
4914
  if (
4270
4915
  this.#availableModelsCache &&
4271
4916
  this.#availableModelsDisabledProviders === disabledProviderKey &&
@@ -4279,6 +4924,19 @@ export class ModelRegistry {
4279
4924
  return this.#availableModelsCache;
4280
4925
  }
4281
4926
 
4927
+ #synchronizeEnvironmentCredentials(): void {
4928
+ // Runtime registrations own a provider while their credential is present.
4929
+ // Refresh them first so a missing runtime env key can hand ownership back to
4930
+ // the static config before the static apiKeyEnv pass runs.
4931
+ for (const provider of this.#runtimeProviderApiKeyEnvNames.keys()) {
4932
+ this.#refreshRotatingConfigApiKey(provider);
4933
+ }
4934
+ for (const provider of this.#customProviderApiKeyEnvNames.keys()) {
4935
+ if (this.#runtimeProviderCredentialInstalled.has(provider)) continue;
4936
+ this.#refreshRotatingConfigApiKey(provider, true);
4937
+ }
4938
+ }
4939
+
4282
4940
  /**
4283
4941
  * Get authenticated models, excluding bundled entries that a fresh provider
4284
4942
  * catalog has positively shown to be unavailable. Bundled entries remain
@@ -4348,7 +5006,10 @@ export class ModelRegistry {
4348
5006
 
4349
5007
  #activeConnectionKind(model: Model<Api>): ActiveProviderDescriptor["connectionKind"] | undefined {
4350
5008
  const evidence = this.#providerActivity.get(model.provider);
4351
- if (!this.#isCredentiallessProvider(model.provider) && this.authStorage.hasUsableAuth(model.provider)) {
5009
+ if (
5010
+ !this.#isCredentiallessProvider(model.provider) &&
5011
+ this.authStorage.hasUsableAuth(model.provider, { owner: this.#authStorageConfigOwner })
5012
+ ) {
4352
5013
  if (!evidence) return undefined;
4353
5014
  const discoveryOnly =
4354
5015
  !evidence.staticConfigured &&
@@ -4423,14 +5084,20 @@ export class ModelRegistry {
4423
5084
  * as providers with stored credentials. See issue #993.
4424
5085
  */
4425
5086
  hasConfiguredAuth(model: Model<Api>): boolean {
4426
- return this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider);
5087
+ return (
5088
+ this.#keylessProviders.has(model.provider) ||
5089
+ this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner })
5090
+ );
4427
5091
  }
4428
5092
 
4429
5093
  /**
4430
5094
  * Check whether auth is configured for a provider.
4431
5095
  */
4432
5096
  hasConfiguredProviderAuth(provider: string): boolean {
4433
- return this.#keylessProviders.has(provider) || this.authStorage.hasAuth(provider);
5097
+ return (
5098
+ this.#keylessProviders.has(provider) ||
5099
+ this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
5100
+ );
4434
5101
  }
4435
5102
 
4436
5103
  isCredentiallessProvider(provider: string): boolean {
@@ -4465,11 +5132,24 @@ export class ModelRegistry {
4465
5132
  );
4466
5133
  }
4467
5134
 
5135
+ /** Opaque owner token for registry-scoped AuthStorage reads. */
5136
+ getAuthStorageOwner(): object {
5137
+ return this.#authStorageConfigOwner;
5138
+ }
5139
+
4468
5140
  getProviderWebSearchMode(provider: string): WebSearchMode | undefined {
4469
5141
  return this.#providerWebSearchModes.get(provider);
4470
5142
  }
4471
5143
 
4472
5144
  getActiveSearchModelContext(model: Model<Api>): ActiveSearchModelContext {
5145
+ const provider = model.provider.toLowerCase();
5146
+ const ownerAuthOverride =
5147
+ this.authStorage.hasConfigApiKey(model.provider, this.#authStorageConfigOwner) ||
5148
+ this.#providerOverrides.has(provider) ||
5149
+ this.#runtimeProviderOverrides.has(provider) ||
5150
+ this.#modelOverrides.get(provider)?.has(model.id.toLowerCase()) === true ||
5151
+ this.#customProviderAuthHeaders.has(provider) ||
5152
+ this.#runtimeProviderAuthHeaders.has(provider);
4473
5153
  return {
4474
5154
  provider: model.provider,
4475
5155
  modelId: model.id,
@@ -4477,6 +5157,15 @@ export class ModelRegistry {
4477
5157
  api: model.api,
4478
5158
  baseUrl: model.baseUrl,
4479
5159
  headers: model.headers,
5160
+ ownerAuthOverride,
5161
+ resolveCredentials: async ({ sessionId, signal }) => {
5162
+ // Resolve against the current registry model so rotating apiKeyEnv
5163
+ // credentials and generated Authorization headers are synchronized
5164
+ // immediately before native search sends its request.
5165
+ const currentModel = this.find(model.provider, model.id) ?? model;
5166
+ const apiKey = await this.getApiKey(currentModel, sessionId, { signal });
5167
+ return { apiKey, headers: currentModel.headers };
5168
+ },
4480
5169
  webSearch: this.getProviderWebSearchMode(model.provider),
4481
5170
  };
4482
5171
  }
@@ -4507,15 +5196,18 @@ export class ModelRegistry {
4507
5196
  } = {},
4508
5197
  ): Promise<string | undefined> {
4509
5198
  this.#refreshRotatingConfigApiKey(model.provider);
4510
- return this.#getApiKeyOrNoAuth(model.provider, () =>
5199
+ const apiKey = await this.#getApiKeyOrNoAuth(model.provider, () =>
4511
5200
  this.authStorage.getApiKey(model.provider, sessionId, {
4512
5201
  baseUrl: model.baseUrl,
4513
5202
  modelId: model.id,
4514
5203
  credentialSelector: options.credentialSelector,
4515
5204
  preferredCredentialSelector: options.preferredCredentialSelector,
5205
+ owner: this.#authStorageConfigOwner,
4516
5206
  signal: options.signal,
4517
5207
  }),
4518
5208
  );
5209
+ this.#applyEffectiveAuthHeader(model, apiKey);
5210
+ return apiKey;
4519
5211
  }
4520
5212
 
4521
5213
  /**
@@ -4532,28 +5224,143 @@ export class ModelRegistry {
4532
5224
  } = {},
4533
5225
  ): Promise<string | undefined> {
4534
5226
  this.#refreshRotatingConfigApiKey(provider);
4535
- return this.#getApiKeyOrNoAuth(provider, () =>
5227
+ const apiKey = await this.#getApiKeyOrNoAuth(provider, () =>
4536
5228
  this.authStorage.getApiKey(provider, sessionId, {
4537
5229
  baseUrl,
4538
5230
  credentialSelector: options.credentialSelector,
4539
5231
  preferredCredentialSelector: options.preferredCredentialSelector,
5232
+ owner: this.#authStorageConfigOwner,
4540
5233
  signal: options.signal,
4541
5234
  }),
4542
5235
  );
5236
+ if (
5237
+ this.#runtimeProviderAuthHeaders.get(provider) === true ||
5238
+ this.#customProviderAuthHeaders.get(provider) === true
5239
+ ) {
5240
+ for (const model of this.#models) {
5241
+ if (model.provider === provider) this.#applyEffectiveAuthHeader(model, apiKey);
5242
+ }
5243
+ }
5244
+ return apiKey;
4543
5245
  }
4544
5246
 
4545
- #refreshRotatingConfigApiKey(provider: string): void {
4546
- const envName = this.#customProviderApiKeyEnvNames.get(provider);
5247
+ #refreshRotatingConfigApiKey(provider: string, forceStatic = false): void {
5248
+ const runtimeOwned = !forceStatic && this.#runtimeProviderApiKeys.has(provider);
5249
+ const envName = runtimeOwned
5250
+ ? this.#runtimeProviderApiKeyEnvNames.get(provider)
5251
+ : this.#customProviderApiKeyEnvNames.get(provider);
4547
5252
  if (!envName) return;
4548
5253
  const resolved = $rotatingCredentialEnv(envName);
4549
- if (resolved === this.#customProviderApiKeys.get(provider)) return;
5254
+ const previous = runtimeOwned
5255
+ ? this.#runtimeProviderResolvedApiKeys.get(provider)
5256
+ : this.#customProviderApiKeys.get(provider);
5257
+ if (resolved === previous && (!runtimeOwned || this.#runtimeProviderCredentialInstalled.has(provider))) return;
4550
5258
  if (resolved === undefined) {
5259
+ if (runtimeOwned) {
5260
+ if (!this.#runtimeProviderCredentialInstalled.has(provider)) return;
5261
+ this.#runtimeProviderCredentialInstalled.delete(provider);
5262
+ this.#staticModelsLoaded = false;
5263
+ this.#reloadStaticModels();
5264
+ return;
5265
+ }
4551
5266
  this.#customProviderApiKeys.delete(provider);
4552
- this.authStorage.removeConfigApiKey(provider);
5267
+ this.authStorage.removeConfigApiKey(provider, this.#authStorageConfigOwner);
5268
+ } else {
5269
+ this.#customProviderApiKeys.set(provider, resolved);
5270
+ if (runtimeOwned) {
5271
+ this.#runtimeProviderResolvedApiKeys.set(provider, resolved);
5272
+ this.#runtimeProviderCredentialInstalled.add(provider);
5273
+ }
5274
+ if (runtimeOwned) {
5275
+ this.authStorage.setConfigApiKey(provider, resolved, { owner: this.#authStorageConfigOwner });
5276
+ } else {
5277
+ this.authStorage.setConfigApiKey(provider, resolved, {
5278
+ envSourced: true,
5279
+ owner: this.#authStorageConfigOwner,
5280
+ });
5281
+ }
5282
+ }
5283
+ const authHeader =
5284
+ this.#runtimeProviderAuthHeaders.get(provider) ?? this.#customProviderAuthHeaders.get(provider);
5285
+ const runtimeOverride = this.#runtimeProviderOverrides.get(provider);
5286
+ if (runtimeOverride) this.#runtimeProviderOverrides.set(provider, { ...runtimeOverride, apiKey: resolved ?? "" });
5287
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
5288
+ if (overlay.provider !== provider) return overlay;
5289
+ const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
5290
+ [GENERATED_AUTH_HEADER]?: string;
5291
+ };
5292
+ const generated = headers[GENERATED_AUTH_HEADER];
5293
+ const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
5294
+ const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated);
5295
+ if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
5296
+ else if (typeof generated === "string") deleteAuthorizationValue(headers, generated);
5297
+ delete headers[GENERATED_AUTH_HEADER];
5298
+ if (authHeader === true && resolved && (ownsAuthorization || !hadAuthorization)) {
5299
+ headers.Authorization = `Bearer ${resolved}`;
5300
+ headers[GENERATED_AUTH_HEADER] = `Bearer ${resolved}`;
5301
+ }
5302
+ return { ...overlay, headers };
5303
+ });
5304
+ if (authHeader === true) this.#generatedAuthHeaderProviders.add(provider);
5305
+ if (authHeader !== true && !this.#generatedAuthHeaderProviders.has(provider)) return;
5306
+ for (const model of this.#models) {
5307
+ if (model.provider !== provider) continue;
5308
+ const headers = { ...(model.headers ?? {}) } as Record<string, string> & {
5309
+ [GENERATED_AUTH_HEADER]?: string;
5310
+ };
5311
+ const generated =
5312
+ this.#generatedAuthHeaders.get(model) ??
5313
+ (typeof headers[GENERATED_AUTH_HEADER] === "string"
5314
+ ? { authorization: headers[GENERATED_AUTH_HEADER] }
5315
+ : undefined);
5316
+ const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
5317
+ const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated?.authorization);
5318
+ if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
5319
+ else if (generated?.authorization) deleteAuthorizationValue(headers, generated.authorization);
5320
+ if (generated?.apiKey === headers["X-Api-Key"]) delete headers["X-Api-Key"];
5321
+ delete headers[GENERATED_AUTH_HEADER];
5322
+ model.headers =
5323
+ authHeader === true && resolved && (ownsAuthorization || !hadAuthorization)
5324
+ ? { ...headers, Authorization: `Bearer ${resolved}`, [GENERATED_AUTH_HEADER]: `Bearer ${resolved}` }
5325
+ : headers;
5326
+ if (authHeader === true && resolved && (ownsAuthorization || !hadAuthorization))
5327
+ this.#generatedAuthHeaders.set(model, { authorization: model.headers.Authorization });
5328
+ }
5329
+ this.#rebuildCanonicalIndex();
5330
+ }
5331
+
5332
+ #applyEffectiveAuthHeader(model: Model<Api>, apiKey: string | undefined): void {
5333
+ const runtimeCredentialRevoked =
5334
+ this.#runtimeProviderApiKeys.has(model.provider) &&
5335
+ !this.#runtimeProviderCredentialInstalled.has(model.provider);
5336
+ const authHeader = runtimeCredentialRevoked
5337
+ ? this.#customProviderAuthHeaders.get(model.provider)
5338
+ : (this.#runtimeProviderAuthHeaders.get(model.provider) ??
5339
+ this.#customProviderAuthHeaders.get(model.provider));
5340
+ if (authHeader === true) this.#generatedAuthHeaderProviders.add(model.provider);
5341
+ const headers = { ...(model.headers ?? {}) } as Record<string, string> & {
5342
+ [GENERATED_AUTH_HEADER]?: string;
5343
+ };
5344
+ const generated =
5345
+ this.#generatedAuthHeaders.get(model) ??
5346
+ (typeof headers[GENERATED_AUTH_HEADER] === "string"
5347
+ ? { authorization: headers[GENERATED_AUTH_HEADER] }
5348
+ : undefined);
5349
+ const hadAuthorization = headerValue(headers, "Authorization") !== undefined;
5350
+ const ownsAuthorization = ownsOnlyGeneratedAuthorization(headers, generated?.authorization);
5351
+ if (authHeader !== true && generated === undefined && !this.#generatedAuthHeaderProviders.has(model.provider))
4553
5352
  return;
5353
+ if (ownsAuthorization) deleteHeaderCaseInsensitive(headers, "Authorization");
5354
+ else if (generated?.authorization) deleteAuthorizationValue(headers, generated.authorization);
5355
+ if (generated?.apiKey === headers["X-Api-Key"]) delete headers["X-Api-Key"];
5356
+ delete headers[GENERATED_AUTH_HEADER];
5357
+ model.headers =
5358
+ authHeader === true && apiKey && apiKey !== kNoAuth && (ownsAuthorization || !hadAuthorization)
5359
+ ? { ...headers, Authorization: `Bearer ${apiKey}`, [GENERATED_AUTH_HEADER]: `Bearer ${apiKey}` }
5360
+ : headers;
5361
+ if (authHeader === true && apiKey && apiKey !== kNoAuth && (ownsAuthorization || !hadAuthorization)) {
5362
+ this.#generatedAuthHeaders.set(model, { authorization: model.headers.Authorization });
4554
5363
  }
4555
- this.#customProviderApiKeys.set(provider, resolved);
4556
- this.authStorage.setConfigApiKey(provider, resolved, { envSourced: true });
4557
5364
  }
4558
5365
 
4559
5366
  async #peekApiKeyForProvider(
@@ -4569,16 +5376,21 @@ export class ModelRegistry {
4569
5376
  return kNoAuth;
4570
5377
  }
4571
5378
  try {
4572
- this.authStorage.getProviderEvidenceGeneration(provider);
5379
+ this.authStorage.getProviderEvidenceGeneration(provider, undefined, this.#authStorageConfigOwner);
4573
5380
  } catch {
4574
5381
  return undefined;
4575
5382
  }
4576
5383
  if (options.refreshOAuth && this.authStorage.hasOAuth(provider)) {
4577
- return this.authStorage.getApiKey(provider, undefined, { baseUrl: options.baseUrl });
5384
+ return this.authStorage.getApiKey(provider, undefined, {
5385
+ baseUrl: options.baseUrl,
5386
+ owner: this.#authStorageConfigOwner,
5387
+ });
4578
5388
  }
4579
5389
  return options.ignoreCredentiallessFallback
4580
- ? this.authStorage.peekApiKey(provider)
4581
- : this.#getApiKeyOrNoAuth(provider, () => this.authStorage.peekApiKey(provider));
5390
+ ? this.authStorage.peekApiKey(provider, { owner: this.#authStorageConfigOwner })
5391
+ : this.#getApiKeyOrNoAuth(provider, () =>
5392
+ this.authStorage.peekApiKey(provider, { owner: this.#authStorageConfigOwner }),
5393
+ );
4582
5394
  }
4583
5395
 
4584
5396
  /**
@@ -4604,9 +5416,13 @@ export class ModelRegistry {
4604
5416
 
4605
5417
  #clearRuntimeProviderState(providerName: string): void {
4606
5418
  this.#runtimeProviderApiKeys.delete(providerName);
5419
+ this.#runtimeProviderResolvedApiKeys.delete(providerName);
5420
+ this.#runtimeProviderCredentialInstalled.delete(providerName);
5421
+ this.#runtimeProviderApiKeyEnvNames.delete(providerName);
4607
5422
  this.#runtimeProviderOverrides.delete(providerName);
5423
+ this.#runtimeProviderAuthHeaders.delete(providerName);
4608
5424
  this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
4609
- this.authStorage.removeConfigApiKey(providerName);
5425
+ this.authStorage.removeConfigApiKey(providerName, this.#authStorageConfigOwner);
4610
5426
  this.#clearDescriptorDiscoveryEvidence(providerName);
4611
5427
  }
4612
5428
 
@@ -4692,7 +5508,6 @@ export class ModelRegistry {
4692
5508
  sourceId,
4693
5509
  });
4694
5510
  }
4695
-
4696
5511
  let sourceHandoff = false;
4697
5512
  if (sourceId) {
4698
5513
  this.#registeredProviderSources.add(sourceId);
@@ -4715,14 +5530,89 @@ export class ModelRegistry {
4715
5530
  this.#lastStaticLoadMtime = null;
4716
5531
  this.#staticModelsLoaded = false;
4717
5532
  this.#reloadStaticModels();
5533
+ if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
5534
+ }
5535
+ if (config.authHeader !== undefined) {
5536
+ if (config.authHeader === false) {
5537
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
5538
+ if (overlay.provider !== providerName) return overlay;
5539
+ const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
5540
+ [GENERATED_AUTH_HEADER]?: string;
5541
+ };
5542
+ const generated = headers[GENERATED_AUTH_HEADER];
5543
+ if (typeof generated === "string" && headers.Authorization === generated) delete headers.Authorization;
5544
+ delete headers[GENERATED_AUTH_HEADER];
5545
+ return { ...overlay, headers };
5546
+ });
5547
+ }
5548
+ this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
5549
+ if (config.authHeader === true) this.#generatedAuthHeaderProviders.add(providerName);
4718
5550
  }
4719
5551
 
4720
5552
  if (config.apiKey) {
4721
- this.#customProviderApiKeys.set(providerName, config.apiKey);
5553
+ const resolved = resolveApiKeyConfig(config.apiKey);
5554
+ if (!resolved) return;
5555
+ if (Bun.env[config.apiKey] !== undefined) this.#runtimeProviderApiKeyEnvNames.set(providerName, config.apiKey);
5556
+ else this.#runtimeProviderApiKeyEnvNames.delete(providerName);
5557
+ this.#customProviderApiKeys.set(providerName, resolved);
4722
5558
  // Persist runtime API keys so they survive #reloadStaticModels() cycles
4723
5559
  this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
4724
- const resolved = resolveApiKeyConfig(config.apiKey);
4725
- if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
5560
+ this.#runtimeProviderResolvedApiKeys.set(providerName, resolved);
5561
+ this.#runtimeProviderCredentialInstalled.add(providerName);
5562
+ if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
5563
+ this.authStorage.setConfigApiKey(providerName, resolved, { owner: this.#authStorageConfigOwner });
5564
+ }
5565
+ if (config.oauth && !config.apiKey && this.#runtimeProviderApiKeys.has(providerName)) {
5566
+ const previousApiKey = this.#runtimeProviderResolvedApiKeys.get(providerName);
5567
+ this.#runtimeProviderApiKeys.delete(providerName);
5568
+ this.#runtimeProviderResolvedApiKeys.delete(providerName);
5569
+ this.#runtimeProviderCredentialInstalled.delete(providerName);
5570
+ this.#runtimeProviderApiKeyEnvNames.delete(providerName);
5571
+ this.#runtimeProviderAuthHeaders.delete(providerName);
5572
+ this.#customProviderApiKeys.delete(providerName);
5573
+ this.authStorage.removeConfigApiKey(providerName, this.#authStorageConfigOwner);
5574
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.map(overlay => {
5575
+ if (overlay.provider !== providerName) return overlay;
5576
+ const headers = { ...(overlay.headers ?? {}) } as Record<string, string> & {
5577
+ [GENERATED_AUTH_HEADER]?: string;
5578
+ };
5579
+ const generated = headers[GENERATED_AUTH_HEADER];
5580
+ for (const key of Object.keys(headers)) {
5581
+ if (key.toLowerCase() !== "authorization") continue;
5582
+ const value = headers[key];
5583
+ if (
5584
+ (typeof generated === "string" && value === generated) ||
5585
+ (previousApiKey !== undefined && value === `Bearer ${previousApiKey}`) ||
5586
+ (previousApiKey === undefined && value.startsWith("Bearer "))
5587
+ )
5588
+ delete headers[key];
5589
+ }
5590
+ delete headers[GENERATED_AUTH_HEADER];
5591
+ return { ...overlay, headers };
5592
+ });
5593
+ const previousOverride = this.#runtimeProviderOverrides.get(providerName);
5594
+ if (previousOverride) {
5595
+ const headers = { ...(previousOverride.headers ?? {}) };
5596
+ for (const key of Object.keys(headers)) {
5597
+ if (key.toLowerCase() !== "authorization") continue;
5598
+ const value = headers[key];
5599
+ if (
5600
+ (previousApiKey !== undefined && value === `Bearer ${previousApiKey}`) ||
5601
+ (previousApiKey === undefined && value.startsWith("Bearer "))
5602
+ )
5603
+ delete headers[key];
5604
+ }
5605
+ this.#runtimeProviderOverrides.set(providerName, {
5606
+ ...previousOverride,
5607
+ apiKey: "",
5608
+ authHeader: undefined,
5609
+ isOAuth: true,
5610
+ headers,
5611
+ });
5612
+ }
5613
+ this.#staticModelsLoaded = false;
5614
+ this.#reloadStaticModels();
5615
+ if (config.authHeader !== undefined) this.#runtimeProviderAuthHeaders.set(providerName, config.authHeader);
4726
5616
  }
4727
5617
 
4728
5618
  if (config.models && config.models.length > 0) {
@@ -4734,12 +5624,13 @@ export class ModelRegistry {
4734
5624
  config.baseUrl!,
4735
5625
  config.api,
4736
5626
  config.headers,
4737
- config.apiKey,
5627
+ config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
4738
5628
  config.authHeader,
4739
5629
  config.compat,
4740
5630
  config.requestTransform,
4741
5631
  undefined,
4742
5632
  undefined,
5633
+ config.transport,
4743
5634
  modelDef as CustomModelDefinitionLike,
4744
5635
  );
4745
5636
  if (!overlay) {
@@ -4756,6 +5647,29 @@ export class ModelRegistry {
4756
5647
  for (const overlay of newOverlays) {
4757
5648
  nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
4758
5649
  }
5650
+ if (
5651
+ config.baseUrl ||
5652
+ config.headers ||
5653
+ config.apiKey ||
5654
+ config.authHeader !== undefined ||
5655
+ config.compat !== undefined ||
5656
+ config.requestTransform !== undefined ||
5657
+ config.transport !== undefined
5658
+ ) {
5659
+ this.#runtimeProviderOverrides.set(
5660
+ providerName,
5661
+ this.#mergeProviderOverride(this.#runtimeProviderOverrides.get(providerName), {
5662
+ api: config.api,
5663
+ baseUrl: this.#runtimeProviderOverrides.get(providerName)?.baseUrl ?? config.baseUrl,
5664
+ headers: config.headers,
5665
+ apiKey: config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
5666
+ authHeader: config.authHeader,
5667
+ compat: config.compat,
5668
+ requestTransform: config.requestTransform,
5669
+ transport: config.transport,
5670
+ }),
5671
+ );
5672
+ }
4759
5673
  const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
4760
5674
  const withRuntimeTransportOverride = runtimeTransportOverride
4761
5675
  ? nextModels.map(model => {
@@ -4766,7 +5680,9 @@ export class ModelRegistry {
4766
5680
  const withModelOverrides = this.#applyProviderModelOverrides(providerName, withRuntimeTransportOverride);
4767
5681
 
4768
5682
  if (config.oauth?.modifyModels) {
4769
- const credential = this.authStorage.getOAuthCredential(providerName);
5683
+ const credential = this.authStorage.getOAuthCredential(providerName, undefined, {
5684
+ owner: this.#authStorageConfigOwner,
5685
+ });
4770
5686
  if (credential) {
4771
5687
  this.#models = this.#finalizeModels(config.oauth.modifyModels(withModelOverrides, credential));
4772
5688
  this.#rebuildCanonicalIndex();
@@ -4791,9 +5707,10 @@ export class ModelRegistry {
4791
5707
  config.transport !== undefined
4792
5708
  ) {
4793
5709
  const transportOverride = {
5710
+ api: config.api,
4794
5711
  baseUrl: config.baseUrl,
4795
5712
  headers: config.headers,
4796
- apiKey: config.apiKey,
5713
+ apiKey: config.apiKey ? resolveApiKeyConfig(config.apiKey) : undefined,
4797
5714
  authHeader: config.authHeader,
4798
5715
  compat: config.compat,
4799
5716
  requestTransform: config.requestTransform,
@@ -4808,6 +5725,11 @@ export class ModelRegistry {
4808
5725
  if (m.provider !== providerName) return m;
4809
5726
  return this.#applyRuntimeProviderOverride(m, transportOverride);
4810
5727
  });
5728
+ if (config.authHeader === false) {
5729
+ for (const model of this.#models) {
5730
+ if (model.provider === providerName) this.#applyEffectiveAuthHeader(model, undefined);
5731
+ }
5732
+ }
4811
5733
  this.#rebuildCanonicalIndex();
4812
5734
  this.#rebuildProviderActivity();
4813
5735
  }