@gajae-code/coding-agent 0.18.12 → 0.19.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 (305) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/dist/types/capability/rule.d.ts +7 -2
  3. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  4. package/dist/types/cli/args.d.ts +7 -2
  5. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  6. package/dist/types/cli/customize-doctor.d.ts +4 -3
  7. package/dist/types/cli/public-command-errors.d.ts +1 -0
  8. package/dist/types/cli/root-flags.d.ts +0 -12
  9. package/dist/types/cli/update-cli.d.ts +7 -2
  10. package/dist/types/cli-main.d.ts +0 -6
  11. package/dist/types/commands/launch.d.ts +0 -6
  12. package/dist/types/commands/migrate.d.ts +1 -1
  13. package/dist/types/commands/sdk.d.ts +8 -0
  14. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  15. package/dist/types/config/autorouting-tier-map.d.ts +23 -1
  16. package/dist/types/config/config-hot-reload.d.ts +5 -0
  17. package/dist/types/config/file-lock.d.ts +44 -2
  18. package/dist/types/config/model-profile-activation.d.ts +19 -5
  19. package/dist/types/config/model-registry.d.ts +16 -3
  20. package/dist/types/config/model-resolver.d.ts +5 -4
  21. package/dist/types/config/settings-schema.d.ts +11 -1
  22. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  23. package/dist/types/customization/import.d.ts +2 -0
  24. package/dist/types/customization/types.d.ts +1 -1
  25. package/dist/types/dap/session.d.ts +11 -2
  26. package/dist/types/discovery/helpers.d.ts +7 -2
  27. package/dist/types/edit/index.d.ts +5 -0
  28. package/dist/types/edit/modes/patch.d.ts +9 -0
  29. package/dist/types/eval/py/runtime.d.ts +9 -0
  30. package/dist/types/export/ttsr.d.ts +19 -0
  31. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  32. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  33. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  34. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  36. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  37. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  38. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  39. package/dist/types/hindsight/content.d.ts +5 -6
  40. package/dist/types/internal-urls/types.d.ts +2 -0
  41. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  42. package/dist/types/main.d.ts +23 -18
  43. package/dist/types/migrate/action-planner.d.ts +10 -1
  44. package/dist/types/migrate/adapters/codex.d.ts +1 -1
  45. package/dist/types/migrate/adapters/index.d.ts +35 -3
  46. package/dist/types/migrate/hook-module.d.ts +23 -0
  47. package/dist/types/migrate/report.d.ts +2 -1
  48. package/dist/types/migrate/types.d.ts +69 -2
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +43 -0
  51. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  52. package/dist/types/modes/utils/host-status.d.ts +15 -11
  53. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  54. package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
  55. package/dist/types/sdk/broker/broker.d.ts +22 -1
  56. package/dist/types/sdk/broker/ensure.d.ts +5 -2
  57. package/dist/types/sdk/broker/hop.d.ts +9 -0
  58. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  59. package/dist/types/sdk/broker/session-index.d.ts +2 -0
  60. package/dist/types/sdk/broker/transport.d.ts +2 -0
  61. package/dist/types/sdk/bus/index.d.ts +21 -0
  62. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  63. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  64. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  65. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  66. package/dist/types/sdk/host/events.d.ts +4 -0
  67. package/dist/types/sdk/host/host.d.ts +2 -1
  68. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  69. package/dist/types/sdk/host/session-runtime.d.ts +8 -8
  70. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  71. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  72. package/dist/types/sdk/prompt-status.d.ts +11 -4
  73. package/dist/types/session/agent-session.d.ts +50 -8
  74. package/dist/types/session/session-manager.d.ts +16 -0
  75. package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
  76. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  77. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  78. package/dist/types/system-prompt.d.ts +3 -2
  79. package/dist/types/task/role-body.d.ts +11 -0
  80. package/dist/types/tools/awaiting-input.d.ts +28 -0
  81. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
  82. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  83. package/dist/types/tools/bash.d.ts +7 -0
  84. package/dist/types/tools/descriptors.d.ts +13 -2
  85. package/dist/types/tools/fetch.d.ts +6 -0
  86. package/dist/types/tools/irc.d.ts +5 -3
  87. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  88. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +2 -0
  90. package/dist/types/utils/file-mentions.d.ts +5 -0
  91. package/dist/types/utils/image-limits.d.ts +7 -0
  92. package/dist/types/utils/image-loading.d.ts +0 -1
  93. package/dist/types/utils/open.d.ts +0 -1
  94. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  95. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  96. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  97. package/dist/types/utils/title-generator.d.ts +13 -3
  98. package/dist/types/web/search/providers/codex.d.ts +6 -7
  99. package/package.json +9 -8
  100. package/scripts/acp-conformance-agent.ts +12 -2
  101. package/scripts/generate-sdk-operation-inventory.ts +16 -0
  102. package/src/capability/fs.ts +31 -3
  103. package/src/capability/rule.ts +11 -2
  104. package/src/cli/append-system-prompt.ts +12 -0
  105. package/src/cli/args.ts +11 -7
  106. package/src/cli/customize-doctor-precedence.ts +31 -0
  107. package/src/cli/customize-doctor.ts +49 -39
  108. package/src/cli/fast-help.ts +0 -2
  109. package/src/cli/migrate-cli.ts +14 -3
  110. package/src/cli/public-command-errors.ts +2 -0
  111. package/src/cli/root-flags.ts +0 -2
  112. package/src/cli/update-cli.ts +247 -6
  113. package/src/commands/harness.ts +25 -15
  114. package/src/commands/migrate.ts +3 -3
  115. package/src/commands/sdk.ts +81 -11
  116. package/src/commit/agentic/agent.ts +6 -6
  117. package/src/commit/analysis/conventional.ts +14 -5
  118. package/src/commit/map-reduce/index.ts +7 -7
  119. package/src/commit/pipeline.ts +12 -12
  120. package/src/config/autorouting-tier-map.ts +20 -1
  121. package/src/config/config-file.ts +106 -3
  122. package/src/config/config-hot-reload.ts +8 -0
  123. package/src/config/file-lock.ts +1431 -304
  124. package/src/config/keybindings.ts +154 -7
  125. package/src/config/model-profile-activation.ts +177 -39
  126. package/src/config/model-registry.ts +380 -55
  127. package/src/config/model-resolver.ts +49 -21
  128. package/src/config/settings-schema.ts +13 -1
  129. package/src/config/settings.ts +2 -0
  130. package/src/coordinator-mcp/server.ts +37 -2
  131. package/src/crash/sanitize.ts +3 -1
  132. package/src/customization/import.ts +276 -7
  133. package/src/customization/inventory.ts +24 -8
  134. package/src/customization/types.ts +1 -0
  135. package/src/dap/client.ts +3 -2
  136. package/src/dap/session.ts +161 -36
  137. package/src/debug/crash-diagnostics.ts +87 -5
  138. package/src/debug/report-bundle.ts +71 -2
  139. package/src/discovery/builtin.ts +4 -3
  140. package/src/discovery/helpers.ts +24 -14
  141. package/src/edit/index.ts +191 -5
  142. package/src/edit/modes/patch.ts +27 -0
  143. package/src/eval/py/runtime.ts +44 -4
  144. package/src/exec/non-interactive-env.ts +0 -1
  145. package/src/export/html/index.ts +60 -20
  146. package/src/export/ttsr.ts +176 -1
  147. package/src/extensibility/extensions/loader.ts +2 -0
  148. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  149. package/src/extensibility/extensions/types.ts +5 -1
  150. package/src/extensibility/hooks/loader.ts +10 -2
  151. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  152. package/src/extensibility/slash-commands.ts +5 -0
  153. package/src/gjc-runtime/gc-runtime.ts +4 -1
  154. package/src/gjc-runtime/launch-tmux.ts +23 -13
  155. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  156. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  157. package/src/gjc-runtime/state-writer.ts +109 -5
  158. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  159. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  160. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  161. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  162. package/src/harness-control-plane/finalize.ts +11 -0
  163. package/src/harness-control-plane/receipt-spool.ts +105 -28
  164. package/src/harness-control-plane/storage.ts +124 -6
  165. package/src/hindsight/content.ts +116 -16
  166. package/src/hindsight/mental-models.ts +22 -10
  167. package/src/hindsight/state.ts +7 -4
  168. package/src/hooks/native-skill-hook.ts +3 -1
  169. package/src/internal-urls/artifact-protocol.ts +3 -1
  170. package/src/internal-urls/docs-index.generated.ts +14 -14
  171. package/src/internal-urls/local-protocol.ts +28 -3
  172. package/src/internal-urls/types.ts +2 -0
  173. package/src/lsp/client.ts +2 -1
  174. package/src/lsp/clients/biome-client.ts +2 -0
  175. package/src/lsp/clients/swiftlint-client.ts +2 -0
  176. package/src/main.ts +330 -92
  177. package/src/memories/index.ts +25 -2
  178. package/src/migrate/action-planner.ts +517 -2
  179. package/src/migrate/adapters/claude-code.ts +141 -3
  180. package/src/migrate/adapters/codex.ts +57 -5
  181. package/src/migrate/adapters/index.ts +240 -5
  182. package/src/migrate/adapters/opencode.ts +4 -0
  183. package/src/migrate/executor.ts +141 -3
  184. package/src/migrate/hook-module.ts +193 -0
  185. package/src/migrate/report.ts +45 -1
  186. package/src/migrate/types.ts +73 -3
  187. package/src/modes/acp/acp-agent.ts +10 -1
  188. package/src/modes/components/agent-dashboard.ts +20 -4
  189. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  190. package/src/modes/components/customization/import-wizard.ts +29 -3
  191. package/src/modes/components/model-selector.ts +157 -103
  192. package/src/modes/components/welcome.ts +3 -1
  193. package/src/modes/controllers/command-controller.ts +271 -16
  194. package/src/modes/controllers/event-controller.ts +1 -1
  195. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  196. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  197. package/src/modes/controllers/input-controller.ts +2 -0
  198. package/src/modes/controllers/plan-mode-controller.ts +49 -4
  199. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  200. package/src/modes/controllers/selector-controller.ts +14 -1
  201. package/src/modes/interactive-mode.ts +55 -4
  202. package/src/modes/prompt-suggestion-controller.ts +1 -1
  203. package/src/modes/runtime-init.ts +5 -1
  204. package/src/modes/utils/host-status.ts +21 -10
  205. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  206. package/src/prompts/system/irc-incoming.md +3 -1
  207. package/src/prompts/tools/irc.md +1 -1
  208. package/src/runtime-mcp/manager.ts +8 -3
  209. package/src/runtime-mcp/project-host-values.ts +12 -8
  210. package/src/runtime-mcp/tool-bridge.ts +68 -4
  211. package/src/runtime-mcp/transports/stdio.ts +2 -1
  212. package/src/sdk/broker/broker-exit.ts +2 -0
  213. package/src/sdk/broker/broker.ts +143 -4
  214. package/src/sdk/broker/ensure.ts +74 -6
  215. package/src/sdk/broker/hop.ts +13 -1
  216. package/src/sdk/broker/lifecycle-ledger.ts +3 -0
  217. package/src/sdk/broker/lifecycle.ts +102 -6
  218. package/src/sdk/broker/session-index.ts +29 -4
  219. package/src/sdk/broker/transport.ts +17 -1
  220. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  221. package/src/sdk/bus/index.ts +408 -14
  222. package/src/sdk/bus/session-control.ts +2 -0
  223. package/src/sdk/bus/slack-daemon.ts +105 -9
  224. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  225. package/src/sdk/bus/telegram-daemon.ts +10 -2
  226. package/src/sdk/bus/threaded-render.ts +20 -1
  227. package/src/sdk/cli/session-cli.ts +2 -0
  228. package/src/sdk/host/control/dispatch.ts +13 -1
  229. package/src/sdk/host/control/operations.ts +10 -1
  230. package/src/sdk/host/events.ts +67 -4
  231. package/src/sdk/host/host.ts +8 -2
  232. package/src/sdk/host/prompt-image-upload.ts +438 -0
  233. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  234. package/src/sdk/host/session-runtime.test.ts +1537 -24
  235. package/src/sdk/host/session-runtime.ts +332 -36
  236. package/src/sdk/host/surface-policy.ts +4 -0
  237. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  238. package/src/sdk/host/websocket-transport.ts +19 -7
  239. package/src/sdk/lifecycle/service.ts +14 -1
  240. package/src/sdk/prompt-status.ts +6 -4
  241. package/src/sdk/protocol/adapter-validation.ts +28 -1
  242. package/src/sdk/protocol/operation-inventory.generated.json +160 -0
  243. package/src/sdk/protocol/operation-registry.ts +33 -3
  244. package/src/sdk/session.ts +123 -36
  245. package/src/session/agent-session.ts +1614 -329
  246. package/src/session/history-storage.ts +182 -12
  247. package/src/session/internal/managed-session-scope.ts +47 -18
  248. package/src/session/internal/managed-session-storage.ts +71 -1
  249. package/src/session/session-manager.ts +90 -5
  250. package/src/session-import/redact.ts +53 -2
  251. package/src/skill-state/workflow-mutation-guard.ts +1498 -18
  252. package/src/slash-commands/builtin-registry.ts +6 -3
  253. package/src/slash-commands/helpers/mcp.ts +11 -3
  254. package/src/ssh/ssh-executor.ts +9 -2
  255. package/src/stt/recorder.ts +26 -9
  256. package/src/system-prompt.ts +61 -16
  257. package/src/task/executor.ts +12 -8
  258. package/src/task/role-body.ts +84 -0
  259. package/src/task/token-log-mode.test.ts +162 -0
  260. package/src/task/token-log.ts +91 -2
  261. package/src/tools/archive-reader.ts +650 -37
  262. package/src/tools/ask.ts +1 -0
  263. package/src/tools/ast-grep.ts +7 -2
  264. package/src/tools/auto-generated-guard.ts +12 -3
  265. package/src/tools/awaiting-input.ts +114 -0
  266. package/src/tools/bash-allowed-prefixes.ts +61 -1
  267. package/src/tools/bash-skill-urls.ts +58 -5
  268. package/src/tools/bash.ts +99 -26
  269. package/src/tools/browser/tab-worker.ts +39 -13
  270. package/src/tools/browser.ts +17 -7
  271. package/src/tools/cron.ts +10 -1
  272. package/src/tools/debug.ts +9 -2
  273. package/src/tools/descriptors.test.ts +4 -4
  274. package/src/tools/descriptors.ts +16 -4
  275. package/src/tools/eval.ts +15 -0
  276. package/src/tools/fetch.ts +90 -2
  277. package/src/tools/gh.ts +27 -12
  278. package/src/tools/image-gen.ts +20 -6
  279. package/src/tools/irc.ts +5 -3
  280. package/src/tools/monitor.ts +9 -1
  281. package/src/tools/output-meta.ts +51 -3
  282. package/src/tools/read.ts +9 -2
  283. package/src/tools/recipe/runner.ts +23 -12
  284. package/src/tools/render-utils.ts +2 -2
  285. package/src/tools/search.ts +3 -2
  286. package/src/tools/ssh.ts +274 -8
  287. package/src/tools/subagent.ts +10 -4
  288. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  289. package/src/tools/tool-catalog.generated.ts +1 -1
  290. package/src/tools/ultragoal-ask-guard.ts +13 -1
  291. package/src/tools/write.ts +7 -0
  292. package/src/tui/tree-list.ts +15 -1
  293. package/src/utils/commit-message-generator.ts +3 -2
  294. package/src/utils/external-editor.ts +21 -7
  295. package/src/utils/file-mentions.ts +46 -6
  296. package/src/utils/git.ts +38 -7
  297. package/src/utils/image-limits.ts +8 -0
  298. package/src/utils/image-loading.ts +1 -1
  299. package/src/utils/open.ts +73 -4
  300. package/src/utils/pasted-image-loading.ts +9 -8
  301. package/src/utils/prompt-suggestion.ts +5 -4
  302. package/src/utils/shell-snapshot.ts +9 -1
  303. package/src/utils/title-generator.ts +65 -20
  304. package/src/web/search/index.ts +2 -1
  305. package/src/web/search/providers/codex.ts +29 -32
@@ -45,6 +45,7 @@ import {
45
45
  } from "@gajae-code/ai/core";
46
46
  import { fetchModelsDevPayload } from "@gajae-code/ai/provider-models/openai-compat";
47
47
  import { isDirectXaiReasoningEffortModel } from "@gajae-code/ai/providers/openai-completions-compat";
48
+ import { getAntigravityDiscoveryUserAgent } from "@gajae-code/ai/utils/discovery/antigravity";
48
49
  import {
49
50
  detectDiscoveredApiFamily,
50
51
  isSafeCatalogModelId,
@@ -1115,7 +1116,8 @@ function filterMaterializedRegistryProfiles(
1115
1116
  type SpecialProviderDescriptor = {
1116
1117
  providerId: string;
1117
1118
  resolveKey: (value: string | undefined) => string | undefined;
1118
- createOptions: (key: string) => ModelManagerOptions<Api>;
1119
+ resolveProjectId?: (key: string, rawValue: string | undefined) => string | undefined;
1120
+ createOptions: (key: string, projectId?: string) => ModelManagerOptions<Api>;
1119
1121
  };
1120
1122
 
1121
1123
  const PROVIDER_BASE_URL_ENV_ALIASES: Record<string, readonly string[]> = {
@@ -1297,17 +1299,33 @@ function fingerprintConfiguredDiscoveryRequestShape(
1297
1299
  return crypto.createHash("sha256").update("gajae:model-discovery-provenance\0").update(context).digest("hex");
1298
1300
  }
1299
1301
 
1300
- function fingerprintDescriptorDiscoveryProvenance(authEvidence: string, endpoint: string): string | undefined {
1302
+ function fingerprintDescriptorDiscoveryProvenance(
1303
+ providerId: string,
1304
+ authEvidence: string,
1305
+ endpoint: string,
1306
+ projectId?: string,
1307
+ ): string | undefined {
1301
1308
  try {
1302
1309
  const parsed = new URL(endpoint);
1303
1310
  if (parsed.username || parsed.password || parsed.search) return undefined;
1304
1311
  } catch {
1305
1312
  return undefined;
1306
1313
  }
1314
+ const discoveryIdentity =
1315
+ providerId === "google-antigravity" || providerId === "google-gemini-cli"
1316
+ ? getAntigravityDiscoveryUserAgent()
1317
+ : undefined;
1307
1318
  return crypto
1308
1319
  .createHash("sha256")
1309
1320
  .update("gajae:model-discovery-provenance\0")
1310
- .update(JSON.stringify({ authEvidence, endpoint }))
1321
+ .update(
1322
+ JSON.stringify({
1323
+ authEvidence,
1324
+ endpoint,
1325
+ ...(projectId !== undefined ? { projectId } : {}),
1326
+ ...(discoveryIdentity !== undefined ? { discoveryIdentity } : {}),
1327
+ }),
1328
+ )
1311
1329
  .digest("hex");
1312
1330
  }
1313
1331
 
@@ -1444,6 +1462,48 @@ function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: stri
1444
1462
  return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
1445
1463
  }
1446
1464
 
1465
+ /** Internal pure parser shared by discovery; exported for regression coverage. */
1466
+ export function extractGoogleOAuthProjectId(value: string | undefined, accessToken: string): string | undefined {
1467
+ if (!value) return undefined;
1468
+ try {
1469
+ const parsed = JSON.parse(value) as { token?: unknown; projectId?: unknown; project_id?: unknown };
1470
+ if (typeof parsed.token !== "string" || parsed.token.trim() !== accessToken) return undefined;
1471
+ const projectId = typeof parsed.projectId === "string" ? parsed.projectId : parsed.project_id;
1472
+ return typeof projectId === "string" ? projectId.trim() || undefined : undefined;
1473
+ } catch {
1474
+ return undefined;
1475
+ }
1476
+ }
1477
+
1478
+ function resolveGoogleOAuthProjectId(
1479
+ authStorage: AuthStorage,
1480
+ provider: string,
1481
+ accessToken: string,
1482
+ rawValue?: string,
1483
+ sessionId?: string,
1484
+ owner?: object,
1485
+ ): string | undefined {
1486
+ const configuredProjectId = extractGoogleOAuthProjectId(rawValue, accessToken);
1487
+ if (authStorage.getEffectiveCredentialType(provider, sessionId, owner ? { owner } : undefined) !== "oauth") {
1488
+ return configuredProjectId;
1489
+ }
1490
+ const selector = authStorage.resolveEffectiveCredentialSelector(provider, sessionId);
1491
+ if (selector) {
1492
+ const selectedCredential = authStorage.getOAuthCredential(provider, sessionId, owner ? { owner } : undefined);
1493
+ if (selectedCredential?.access !== accessToken) return configuredProjectId;
1494
+ return selectedCredential.projectId?.trim() || configuredProjectId;
1495
+ }
1496
+ const matchingCredentials = getOAuthCredentialsForProvider(authStorage, provider).filter(
1497
+ credential => credential.access === accessToken,
1498
+ );
1499
+ if (matchingCredentials.length === 0) return configuredProjectId;
1500
+ const projectId = matchingCredentials[0]?.projectId?.trim();
1501
+ if (matchingCredentials.some(credential => (credential.projectId?.trim() ?? "") !== (projectId ?? ""))) {
1502
+ return configuredProjectId;
1503
+ }
1504
+ return projectId || configuredProjectId;
1505
+ }
1506
+
1447
1507
  function resolveOAuthAccountIdForAccessToken(
1448
1508
  authStorage: AuthStorage,
1449
1509
  provider: string,
@@ -2010,6 +2070,12 @@ interface ModelManagerDiscoveryOptions {
2010
2070
  apiKey: string | undefined;
2011
2071
  endpoint: string;
2012
2072
  endpointContainsUserinfo: boolean;
2073
+ projectId?: string;
2074
+ isCurrentContext?: () => boolean;
2075
+ }
2076
+
2077
+ interface GoogleSessionDiscoveryCatalog {
2078
+ models: Model<Api>[];
2013
2079
  }
2014
2080
 
2015
2081
  interface ConfiguredDiscoveryResult {
@@ -2232,9 +2298,11 @@ export class ModelRegistry {
2232
2298
  bySelector: new Map(),
2233
2299
  aliases: new Map(),
2234
2300
  };
2235
- #availableModelsCache: Model<Api>[] | undefined;
2236
- #availableModelsDisabledProviders: string | undefined;
2237
- #availableModelsEnvFingerprint: string | undefined;
2301
+ #availableModelsCache = new Map<
2302
+ string,
2303
+ { models: Model<Api>[]; disabledProviders: string; envFingerprint: string }
2304
+ >();
2305
+ #googleSessionDiscoveryCatalogs = new Map<string, Map<string, GoogleSessionDiscoveryCatalog>>();
2238
2306
  #sessionCanonicalVariants = new Map<string, string>();
2239
2307
  #customProviderApiKeys: Map<string, string> = new Map();
2240
2308
  #customProviderApiKeyEnvNames: Map<string, string> = new Map();
@@ -2254,6 +2322,7 @@ export class ModelRegistry {
2254
2322
  modelIds: ReadonlySet<string>;
2255
2323
  authGeneration: string;
2256
2324
  endpoint: string;
2325
+ projectId?: string;
2257
2326
  }
2258
2327
  >();
2259
2328
  #descriptorDiscoveryGenerations = new Map<string, number>();
@@ -2295,7 +2364,7 @@ export class ModelRegistry {
2295
2364
  #cacheDbPath?: string;
2296
2365
  #suppressedSelectors: Map<string, number> = new Map();
2297
2366
  #selectorCircuits: Map<string, SelectorCircuit> = new Map();
2298
- #backgroundRefresh?: Promise<void>;
2367
+ #backgroundRefreshes: Map<string, Promise<void>> = new Map();
2299
2368
  #catalogMutationTail: Promise<void> = Promise.resolve();
2300
2369
  #pendingCatalogMutations = 0;
2301
2370
  #catalogRefreshGeneration = 0;
@@ -2392,6 +2461,12 @@ export class ModelRegistry {
2392
2461
 
2393
2462
  #copyRuntimeStateFrom(source: ModelRegistry): void {
2394
2463
  this.#sessionCanonicalVariants = new Map(source.#sessionCanonicalVariants);
2464
+ this.#googleSessionDiscoveryCatalogs = new Map(
2465
+ [...source.#googleSessionDiscoveryCatalogs].map(([provider, catalogs]) => [
2466
+ provider,
2467
+ new Map([...catalogs].map(([identity, catalog]) => [identity, { models: [...catalog.models] }])),
2468
+ ]),
2469
+ );
2395
2470
  this.#suppressedSelectors = new Map(source.#suppressedSelectors);
2396
2471
  this.#selectorCircuits = new Map(
2397
2472
  [...source.#selectorCircuits].map(([selector, circuit]) => [selector, structuredClone(circuit)]),
@@ -2471,13 +2546,9 @@ export class ModelRegistry {
2471
2546
  other.#availableModelsCache,
2472
2547
  this.#availableModelsCache,
2473
2548
  ];
2474
- [this.#availableModelsDisabledProviders, other.#availableModelsDisabledProviders] = [
2475
- other.#availableModelsDisabledProviders,
2476
- this.#availableModelsDisabledProviders,
2477
- ];
2478
- [this.#availableModelsEnvFingerprint, other.#availableModelsEnvFingerprint] = [
2479
- other.#availableModelsEnvFingerprint,
2480
- this.#availableModelsEnvFingerprint,
2549
+ [this.#googleSessionDiscoveryCatalogs, other.#googleSessionDiscoveryCatalogs] = [
2550
+ other.#googleSessionDiscoveryCatalogs,
2551
+ this.#googleSessionDiscoveryCatalogs,
2481
2552
  ];
2482
2553
  [this.#customProviderApiKeys, other.#customProviderApiKeys] = [
2483
2554
  other.#customProviderApiKeys,
@@ -2626,7 +2697,7 @@ export class ModelRegistry {
2626
2697
  other.#modelBindingsApplier.setBindings(previousBindings);
2627
2698
  this.#modelBindingsApplier.apply();
2628
2699
  this.#modelsConfigFile.invalidate();
2629
- this.#availableModelsCache = undefined;
2700
+ this.#invalidateAvailableModels();
2630
2701
  }
2631
2702
 
2632
2703
  #reconcileOwnedConfigApiKeys(
@@ -2792,6 +2863,7 @@ export class ModelRegistry {
2792
2863
  for (const provider of changedProviders) {
2793
2864
  copyProviderEntry(registry.#configuredDiscoveryEvidence, this.#configuredDiscoveryEvidence, provider);
2794
2865
  copyProviderEntry(registry.#descriptorDiscoveryEvidence, this.#descriptorDiscoveryEvidence, provider);
2866
+ registry.#googleSessionDiscoveryCatalogs.delete(provider);
2795
2867
  copyProviderEntry(registry.#descriptorDiscoveryGenerations, this.#descriptorDiscoveryGenerations, provider);
2796
2868
  copyProviderEntry(registry.#providerEvidenceApiKeys, this.#providerEvidenceApiKeys, provider);
2797
2869
  copyProviderEntry(
@@ -3088,9 +3160,12 @@ export class ModelRegistry {
3088
3160
  }
3089
3161
 
3090
3162
  refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached", credentialSessionId?: string): void {
3091
- if (this.#backgroundRefresh) {
3092
- return;
3093
- }
3163
+ // Each credential context has an isolated discovery catalog. Coalescing every
3164
+ // request behind one global promise discarded B's refresh when A was pending.
3165
+ // Equivalent contexts still coalesce; distinct ones queue through the catalog
3166
+ // mutation tail so their overlays remain independently populated.
3167
+ const contextKey = `${strategy}\u0000${credentialSessionId ?? ""}`;
3168
+ if (this.#backgroundRefreshes.has(contextKey)) return;
3094
3169
  const refreshPromise = this.refresh(strategy, credentialSessionId)
3095
3170
  .catch(error => {
3096
3171
  logger.warn("background model refresh failed", {
@@ -3098,11 +3173,11 @@ export class ModelRegistry {
3098
3173
  });
3099
3174
  })
3100
3175
  .finally(() => {
3101
- if (this.#backgroundRefresh === refreshPromise) {
3102
- this.#backgroundRefresh = undefined;
3176
+ if (this.#backgroundRefreshes.get(contextKey) === refreshPromise) {
3177
+ this.#backgroundRefreshes.delete(contextKey);
3103
3178
  }
3104
3179
  });
3105
- this.#backgroundRefresh = refreshPromise;
3180
+ this.#backgroundRefreshes.set(contextKey, refreshPromise);
3106
3181
  }
3107
3182
 
3108
3183
  async refreshProvider(
@@ -3698,6 +3773,7 @@ export class ModelRegistry {
3698
3773
  }
3699
3774
  const cache = readModelCache<Api>(descriptor.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
3700
3775
  const expectedProvenance = fingerprintDescriptorDiscoveryProvenance(
3776
+ descriptor.providerId,
3701
3777
  authEvidence ?? this.#getProviderEvidenceGeneration(descriptor.providerId),
3702
3778
  this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? ""),
3703
3779
  );
@@ -4367,6 +4443,12 @@ export class ModelRegistry {
4367
4443
  credentialSessionId?: string,
4368
4444
  ): Promise<void> {
4369
4445
  const profileAvailabilityEvidenceBefore = this.#profileAvailabilityEvidenceFingerprint();
4446
+ const googleDescriptorGenerationsBefore = new Map(
4447
+ (["google-antigravity", "google-gemini-cli"] as const).map(provider => [
4448
+ provider,
4449
+ this.#descriptorDiscoveryGenerations.get(provider) ?? 0,
4450
+ ]),
4451
+ );
4370
4452
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
4371
4453
  const selectedDiscoverableProviders = (
4372
4454
  providerFilter
@@ -4518,6 +4600,38 @@ export class ModelRegistry {
4518
4600
  return false;
4519
4601
  }
4520
4602
  });
4603
+ let sessionCatalogChanged = false;
4604
+ for (const provider of ["google-antigravity", "google-gemini-cli"] as const) {
4605
+ if (
4606
+ this.authStorage.getEffectiveCredentialType(provider, credentialSessionId, {
4607
+ owner: this.#authStorageConfigOwner,
4608
+ }) !== "oauth"
4609
+ ) {
4610
+ continue;
4611
+ }
4612
+ if (
4613
+ (this.#descriptorDiscoveryGenerations.get(provider) ?? 0) ===
4614
+ googleDescriptorGenerationsBefore.get(provider)
4615
+ ) {
4616
+ continue;
4617
+ }
4618
+ const evidence = this.#descriptorDiscoveryEvidence.get(provider);
4619
+ if (!evidence) continue;
4620
+ const identity = this.#googleDescriptorContextIdentity(
4621
+ provider,
4622
+ evidence.authGeneration,
4623
+ evidence.endpoint,
4624
+ evidence.projectId,
4625
+ );
4626
+ if (!identity || identity !== this.#googleSessionDescriptorIdentity(provider, credentialSessionId)) continue;
4627
+ const catalogs = this.#googleSessionDiscoveryCatalogs.get(provider) ?? new Map();
4628
+ const models = currentBuiltInDiscovered.filter(model => model.provider === provider);
4629
+ const previous = catalogs.get(identity);
4630
+ if (models.length === 0 && evidence.fresh === false && previous !== undefined) continue;
4631
+ if (!isDeepStrictEqual(previous?.models, models)) sessionCatalogChanged = true;
4632
+ catalogs.set(identity, { models });
4633
+ this.#googleSessionDiscoveryCatalogs.set(provider, catalogs);
4634
+ }
4521
4635
  const configuredDiscoveryEvidence = new Map(
4522
4636
  currentConfiguredDiscoveryResults
4523
4637
  .filter(result => result.current)
@@ -4585,6 +4699,10 @@ export class ModelRegistry {
4585
4699
  );
4586
4700
  });
4587
4701
  }
4702
+ if (sessionCatalogChanged) {
4703
+ this.#invalidateAvailableModels();
4704
+ this.#notifyCatalogChanged();
4705
+ }
4588
4706
  if (discovered.length === 0) {
4589
4707
  if (
4590
4708
  clearPublishedModelIds.size > 0 ||
@@ -4597,6 +4715,53 @@ export class ModelRegistry {
4597
4715
  this.#mergeDiscoveredModels(discovered);
4598
4716
  }
4599
4717
 
4718
+ #googleDescriptorContextIdentity(
4719
+ provider: "google-antigravity" | "google-gemini-cli",
4720
+ authGeneration: string,
4721
+ endpoint: string,
4722
+ projectId?: string,
4723
+ ): string | undefined {
4724
+ return fingerprintDescriptorDiscoveryProvenance(provider, authGeneration, endpoint, projectId);
4725
+ }
4726
+
4727
+ #googleSessionDescriptorIdentity(
4728
+ provider: "google-antigravity" | "google-gemini-cli",
4729
+ credentialSessionId?: string,
4730
+ ): string | undefined {
4731
+ if (
4732
+ this.authStorage.getEffectiveCredentialType(provider, credentialSessionId, {
4733
+ owner: this.#authStorageConfigOwner,
4734
+ }) !== "oauth"
4735
+ ) {
4736
+ return undefined;
4737
+ }
4738
+ const credential = this.authStorage.getOAuthCredential(provider, credentialSessionId, {
4739
+ owner: this.#authStorageConfigOwner,
4740
+ });
4741
+ if (!credential) return undefined;
4742
+ const endpoint = this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "");
4743
+ try {
4744
+ return this.#googleDescriptorContextIdentity(
4745
+ provider,
4746
+ this.#getProviderEvidenceGeneration(provider, credential.access),
4747
+ endpoint,
4748
+ credential.projectId?.trim() || undefined,
4749
+ );
4750
+ } catch {
4751
+ return undefined;
4752
+ }
4753
+ }
4754
+
4755
+ #getGoogleSessionDiscoveryModels(credentialSessionId?: string): Model<Api>[] {
4756
+ const models: Model<Api>[] = [];
4757
+ for (const provider of ["google-antigravity", "google-gemini-cli"] as const) {
4758
+ const identity = this.#googleSessionDescriptorIdentity(provider, credentialSessionId);
4759
+ if (!identity) continue;
4760
+ models.push(...(this.#googleSessionDiscoveryCatalogs.get(provider)?.get(identity)?.models ?? []));
4761
+ }
4762
+ return models;
4763
+ }
4764
+
4600
4765
  #profileAvailabilityEvidenceFingerprint(): string {
4601
4766
  return JSON.stringify({
4602
4767
  descriptor: [...this.#descriptorDiscoveryEvidence.entries()]
@@ -4606,6 +4771,7 @@ export class ModelRegistry {
4606
4771
  evidence.fresh,
4607
4772
  evidence.authGeneration,
4608
4773
  evidence.endpoint,
4774
+ evidence.projectId,
4609
4775
  [...evidence.modelIds].sort(),
4610
4776
  ]),
4611
4777
  configured: [...this.#configuredDiscoveryEvidence.entries()]
@@ -4619,23 +4785,37 @@ export class ModelRegistry {
4619
4785
  });
4620
4786
  }
4621
4787
 
4788
+ #removeDescriptorDiscoveredModels(provider: string, modelIds: ReadonlySet<string>): boolean {
4789
+ const staticModelIds = this.#providerActivity.get(provider)?.staticModelIds;
4790
+ const previousLength = this.#models.length;
4791
+ this.#models = this.#models.filter(model => {
4792
+ if (model.provider !== provider || !modelIds.has(model.id)) return true;
4793
+ return staticModelIds?.has(model.id) === true;
4794
+ });
4795
+ return this.#models.length !== previousLength;
4796
+ }
4797
+
4622
4798
  #mergeDiscoveredModels(discovered: readonly Model<Api>[]): void {
4799
+ this.#models = this.#resolveDiscoveredModelCatalog(this.#models, discovered);
4800
+ this.#rebuildCanonicalIndex();
4801
+ }
4802
+
4803
+ #resolveDiscoveredModelCatalog(baseModels: readonly Model<Api>[], discovered: readonly Model<Api>[]): Model<Api>[] {
4623
4804
  const discoveredModels = this.#applyHardcodedModelPolicies(
4624
4805
  discovered.map(model =>
4625
4806
  mergeDiscoveredModel(
4626
4807
  model,
4627
- this.find(model.provider, model.id),
4808
+ baseModels.find(candidate => candidate.provider === model.provider && candidate.id === model.id),
4628
4809
  this.#resolveProviderOverride(model.provider),
4629
4810
  ),
4630
4811
  ),
4631
4812
  );
4632
- const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
4813
+ const resolved = this.#mergeResolvedModels([...baseModels], discoveredModels);
4633
4814
  const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
4634
4815
  // Merge runtime extension models so they survive online discovery completion
4635
4816
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
4636
4817
  const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
4637
- this.#models = this.#finalizeModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
4638
- this.#rebuildCanonicalIndex();
4818
+ return this.#finalizeModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
4639
4819
  }
4640
4820
 
4641
4821
  async #discoverProviderModels(
@@ -4966,19 +5146,39 @@ export class ModelRegistry {
4966
5146
  {
4967
5147
  providerId: "google-antigravity",
4968
5148
  resolveKey: extractGoogleOAuthToken,
4969
- createOptions: oauthToken =>
5149
+ resolveProjectId: (oauthToken, rawValue) =>
5150
+ resolveGoogleOAuthProjectId(
5151
+ this.authStorage,
5152
+ "google-antigravity",
5153
+ oauthToken,
5154
+ rawValue,
5155
+ credentialSessionId,
5156
+ this.#authStorageConfigOwner,
5157
+ ),
5158
+ createOptions: (oauthToken, projectId) =>
4970
5159
  googleAntigravityModelManagerOptions({
4971
5160
  oauthToken,
4972
5161
  endpoint: this.getProviderBaseUrl("google-antigravity"),
5162
+ project: projectId,
4973
5163
  }),
4974
5164
  },
4975
5165
  {
4976
5166
  providerId: "google-gemini-cli",
4977
5167
  resolveKey: extractGoogleOAuthToken,
4978
- createOptions: oauthToken =>
5168
+ resolveProjectId: (oauthToken, rawValue) =>
5169
+ resolveGoogleOAuthProjectId(
5170
+ this.authStorage,
5171
+ "google-gemini-cli",
5172
+ oauthToken,
5173
+ rawValue,
5174
+ credentialSessionId,
5175
+ this.#authStorageConfigOwner,
5176
+ ),
5177
+ createOptions: (oauthToken, projectId) =>
4979
5178
  googleGeminiCliModelManagerOptions({
4980
5179
  oauthToken,
4981
5180
  endpoint: this.getProviderBaseUrl("google-gemini-cli"),
5181
+ project: projectId,
4982
5182
  }),
4983
5183
  },
4984
5184
  {
@@ -5087,7 +5287,11 @@ export class ModelRegistry {
5087
5287
  authGeneration === this.#getProviderEvidenceGeneration(descriptor.providerId, apiKeyValue) &&
5088
5288
  isAuthenticated(key)
5089
5289
  ) {
5090
- const managerOptions = descriptor.createOptions(key);
5290
+ const projectId = descriptor.resolveProjectId?.(key, apiKeyValue);
5291
+ const isCurrentContext = descriptor.resolveProjectId
5292
+ ? () => descriptor.resolveProjectId?.(key, apiKeyValue) === projectId
5293
+ : undefined;
5294
+ const managerOptions = descriptor.createOptions(key, projectId);
5091
5295
  const baseUrl = this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? "";
5092
5296
  options.push({
5093
5297
  options: managerOptions,
@@ -5095,6 +5299,8 @@ export class ModelRegistry {
5095
5299
  apiKey: apiKeyValue,
5096
5300
  endpoint: this.#normalizeDiscoveryEvidenceEndpoint(baseUrl),
5097
5301
  endpointContainsUserinfo: this.#urlContainsUserinfo(baseUrl),
5302
+ projectId,
5303
+ ...(isCurrentContext ? { isCurrentContext } : {}),
5098
5304
  });
5099
5305
  }
5100
5306
  }
@@ -5102,12 +5308,28 @@ export class ModelRegistry {
5102
5308
  }
5103
5309
 
5104
5310
  async #discoverWithModelManager(
5105
- { options, authGeneration, apiKey, endpoint, endpointContainsUserinfo }: ModelManagerDiscoveryOptions,
5311
+ {
5312
+ options,
5313
+ authGeneration,
5314
+ apiKey,
5315
+ endpoint,
5316
+ endpointContainsUserinfo,
5317
+ projectId,
5318
+ isCurrentContext,
5319
+ }: ModelManagerDiscoveryOptions,
5106
5320
  strategy: ModelRefreshStrategy,
5107
5321
  providerRefresh?: ProviderRefreshFence,
5108
5322
  ): Promise<Model<Api>[]> {
5109
5323
  const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
5110
5324
  this.#descriptorDiscoveryGenerations.set(options.providerId, generation);
5325
+ const previousEvidence = this.#descriptorDiscoveryEvidence.get(options.providerId);
5326
+ const discoveryContextChanged =
5327
+ previousEvidence !== undefined &&
5328
+ (previousEvidence.authGeneration !== authGeneration ||
5329
+ previousEvidence.endpoint !== endpoint ||
5330
+ previousEvidence.projectId !== projectId);
5331
+ const isGoogleProjectScopedProvider =
5332
+ options.providerId === "google-antigravity" || options.providerId === "google-gemini-cli";
5111
5333
  const canUseCredentialDerivedXiaomiEndpoint =
5112
5334
  options.providerId === "xiaomi" &&
5113
5335
  apiKey?.startsWith("tp-") === true &&
@@ -5117,8 +5339,7 @@ export class ModelRegistry {
5117
5339
  let credentialDerivedEndpoint: string | undefined;
5118
5340
  const reusableCacheProvenance = endpointContainsUserinfo
5119
5341
  ? undefined
5120
- : fingerprintDescriptorDiscoveryProvenance(authGeneration, endpoint);
5121
- if (reusableCacheProvenance === undefined && strategy === "offline") return [];
5342
+ : fingerprintDescriptorDiscoveryProvenance(options.providerId, authGeneration, endpoint, projectId);
5122
5343
  // Signed/userinfo endpoints may still publish a sanitized best-effort row,
5123
5344
  // but each refresh gets a new opaque nonce. The nonce contains no endpoint
5124
5345
  // secret and can never match a previous invocation, so constructor loads,
@@ -5128,6 +5349,7 @@ export class ModelRegistry {
5128
5349
  const isCurrentDiscoveryContext = () =>
5129
5350
  (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
5130
5351
  this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
5352
+ (isCurrentContext?.() ?? true) &&
5131
5353
  (endpoint ===
5132
5354
  this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(options.providerId) ?? "") ||
5133
5355
  (canUseCredentialDerivedXiaomiEndpoint && credentialDerivedEndpoint !== undefined));
@@ -5148,6 +5370,40 @@ export class ModelRegistry {
5148
5370
  ? strategy !== "online-if-uncached" || isCurrentDiscovery()
5149
5371
  : isCurrentProviderRefresh()));
5150
5372
  try {
5373
+ if (!isCurrentDiscovery()) return [];
5374
+ if (discoveryContextChanged && previousEvidence) {
5375
+ const removedPreviousModels = this.#removeDescriptorDiscoveredModels(
5376
+ options.providerId,
5377
+ previousEvidence.modelIds,
5378
+ );
5379
+ if (removedPreviousModels) {
5380
+ this.#rebuildProviderActivity();
5381
+ this.#rebuildCanonicalIndex();
5382
+ }
5383
+ }
5384
+ if (strategy === "offline") {
5385
+ const cache =
5386
+ reusableCacheProvenance !== undefined && isGoogleProjectScopedProvider
5387
+ ? readModelCache<Api>(options.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath)
5388
+ : null;
5389
+ const hasCurrentProjectCache =
5390
+ cache?.dynamicModelIds !== undefined && cache.dynamicModelProvenance === reusableCacheProvenance;
5391
+ if (reusableCacheProvenance === undefined || (isGoogleProjectScopedProvider && !hasCurrentProjectCache)) {
5392
+ if (discoveryContextChanged) {
5393
+ this.#providerEvidenceApiKeys.set(options.providerId, apiKey);
5394
+ this.#descriptorDiscoveryEvidence.set(options.providerId, {
5395
+ fresh: false,
5396
+ modelIds: new Set(),
5397
+ authGeneration,
5398
+ endpoint,
5399
+ projectId,
5400
+ });
5401
+ this.#rebuildProviderActivity();
5402
+ this.#rebuildCanonicalIndex();
5403
+ }
5404
+ return [];
5405
+ }
5406
+ }
5151
5407
  const manager = createModelManager({
5152
5408
  ...options,
5153
5409
  cacheDbPath: this.#cacheDbPath,
@@ -5179,7 +5435,9 @@ export class ModelRegistry {
5179
5435
  const evidence = this.#descriptorDiscoveryEvidence.get(options.providerId);
5180
5436
  const refreshStrategy =
5181
5437
  strategy === "online-if-uncached" &&
5182
- (evidence?.authGeneration !== authGeneration || evidence.endpoint !== endpoint)
5438
+ (evidence?.authGeneration !== authGeneration ||
5439
+ evidence.endpoint !== endpoint ||
5440
+ evidence.projectId !== projectId)
5183
5441
  ? "online"
5184
5442
  : strategy;
5185
5443
  const result = await manager.refresh(refreshStrategy);
@@ -5194,7 +5452,7 @@ export class ModelRegistry {
5194
5452
  const preservesExistingDescriptorEvidence =
5195
5453
  (result.dynamicModelIds === undefined && !result.fetched && !result.stale) ||
5196
5454
  (result.stale && result.cacheFresh && result.cacheAuthoritative && evidence?.fresh === true) ||
5197
- (strategy === "offline" && result.dynamicModelIds === undefined);
5455
+ (strategy === "offline" && result.dynamicModelIds === undefined && !discoveryContextChanged);
5198
5456
  if (
5199
5457
  isCurrentDiscovery() &&
5200
5458
  !preservesExistingDescriptorEvidence &&
@@ -5202,13 +5460,15 @@ export class ModelRegistry {
5202
5460
  result.dynamicModelIds !== undefined ||
5203
5461
  result.stale ||
5204
5462
  this.#descriptorDiscoveryEvidence.get(options.providerId)?.authGeneration !== authGeneration ||
5205
- this.#descriptorDiscoveryEvidence.get(options.providerId)?.endpoint !== endpoint)
5463
+ this.#descriptorDiscoveryEvidence.get(options.providerId)?.endpoint !== endpoint ||
5464
+ this.#descriptorDiscoveryEvidence.get(options.providerId)?.projectId !== projectId)
5206
5465
  ) {
5207
5466
  this.#descriptorDiscoveryEvidence.set(options.providerId, {
5208
5467
  fresh: result.fetched,
5209
5468
  modelIds: new Set(models.map(model => model.id)),
5210
5469
  authGeneration,
5211
5470
  endpoint: this.#normalizeDiscoveryEvidenceEndpoint(models[0]?.baseUrl ?? endpoint),
5471
+ projectId,
5212
5472
  });
5213
5473
  }
5214
5474
  if (!isCurrentDiscovery()) {
@@ -5234,6 +5494,7 @@ export class ModelRegistry {
5234
5494
  modelIds: new Set(),
5235
5495
  authGeneration,
5236
5496
  endpoint,
5497
+ projectId,
5237
5498
  });
5238
5499
  }
5239
5500
  logger.warn("model discovery failed for provider", {
@@ -6213,9 +6474,7 @@ export class ModelRegistry {
6213
6474
  }
6214
6475
 
6215
6476
  #invalidateAvailableModels(): void {
6216
- this.#availableModelsCache = undefined;
6217
- this.#availableModelsDisabledProviders = undefined;
6218
- this.#availableModelsEnvFingerprint = undefined;
6477
+ this.#availableModelsCache.clear();
6219
6478
  }
6220
6479
 
6221
6480
  #notifyCatalogChanged(): void {
@@ -6290,10 +6549,11 @@ export class ModelRegistry {
6290
6549
 
6291
6550
  /**
6292
6551
  * Get all models (embedded + accepted registry + custom).
6552
+ * Supplying a credential session selects only that session's Google OAuth discovery catalog.
6293
6553
  * If models.yml had errors, returns embedded plus accepted registry models.
6294
6554
  */
6295
- getAll(): Model<Api>[] {
6296
- return this.#models;
6555
+ getAll(credentialSessionId?: string): Model<Api>[] {
6556
+ return credentialSessionId === undefined ? this.#models : this.#getCredentialScopedCatalog(credentialSessionId);
6297
6557
  }
6298
6558
 
6299
6559
  /** Provider ids declared in models.yml, including override-only providers. */
@@ -6381,12 +6641,16 @@ export class ModelRegistry {
6381
6641
  );
6382
6642
  }
6383
6643
 
6384
- #isModelAvailable(model: Model<Api>, disabledProviders?: ReadonlySet<string>): boolean {
6644
+ #isModelAvailable(
6645
+ model: Model<Api>,
6646
+ disabledProviders?: ReadonlySet<string>,
6647
+ credentialSessionId?: string,
6648
+ ): boolean {
6385
6649
  const disabled = disabledProviders ?? getDisabledProviderIdsFromSettings(this.#settings);
6386
6650
  return (
6387
6651
  !disabled.has(model.provider) &&
6388
6652
  (this.#keylessProviders.has(model.provider) ||
6389
- this.authStorage.hasAuth(model.provider, undefined, { owner: this.#authStorageConfigOwner }))
6653
+ this.authStorage.hasAuth(model.provider, credentialSessionId, { owner: this.#authStorageConfigOwner }))
6390
6654
  );
6391
6655
  }
6392
6656
  /** Per-query precomputed variant filter inputs; plan fields are authoritative when provided. */
@@ -6821,23 +7085,54 @@ export class ModelRegistry {
6821
7085
  * is authoritative for visibility: live discovery only enriches entries and
6822
7086
  * adds unknown ids, and never removes a bundled model. A provider that cannot
6823
7087
  * serve a model reports its own error when the model is used.
7088
+ * Without a credential session, project-scoped Google OAuth discoveries are omitted.
6824
7089
  */
6825
- getAvailable(): Model<Api>[] {
7090
+ getAvailable(credentialSessionId?: string): Model<Api>[] {
6826
7091
  this.#synchronizeEnvironmentCredentials();
6827
7092
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
6828
7093
  const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
6829
7094
  const envFingerprint = envAvailabilityFingerprint(this.#configuredApiKeyEnvNames);
6830
- if (
6831
- this.#availableModelsCache &&
6832
- this.#availableModelsDisabledProviders === disabledProviderKey &&
6833
- this.#availableModelsEnvFingerprint === envFingerprint
6834
- ) {
6835
- return this.#availableModelsCache;
6836
- }
6837
- this.#availableModelsCache = this.#models.filter(model => this.#isModelAvailable(model, disabledProviders));
6838
- this.#availableModelsDisabledProviders = disabledProviderKey;
6839
- this.#availableModelsEnvFingerprint = envFingerprint;
6840
- return this.#availableModelsCache;
7095
+ const cacheKey = credentialSessionId ?? "";
7096
+ const cached = this.#availableModelsCache.get(cacheKey);
7097
+ if (cached?.disabledProviders === disabledProviderKey && cached.envFingerprint === envFingerprint) {
7098
+ return cached.models;
7099
+ }
7100
+ const catalog = this.#getCredentialScopedCatalog(credentialSessionId);
7101
+ const models = catalog.filter(model => this.#isModelAvailable(model, disabledProviders, credentialSessionId));
7102
+ this.#availableModelsCache.set(cacheKey, {
7103
+ models,
7104
+ disabledProviders: disabledProviderKey,
7105
+ envFingerprint,
7106
+ });
7107
+ return models;
7108
+ }
7109
+
7110
+ #getCredentialScopedCatalog(credentialSessionId?: string): Model<Api>[] {
7111
+ const sessionScopedProviders = new Set(
7112
+ (["google-antigravity", "google-gemini-cli"] as const).filter(
7113
+ provider =>
7114
+ this.authStorage.getEffectiveCredentialType(provider, credentialSessionId, {
7115
+ owner: this.#authStorageConfigOwner,
7116
+ }) === "oauth",
7117
+ ),
7118
+ );
7119
+ const commonModels = this.#models.filter(model => {
7120
+ if (model.provider !== "google-antigravity" && model.provider !== "google-gemini-cli") {
7121
+ return true;
7122
+ }
7123
+ if (!sessionScopedProviders.has(model.provider) || this.#discoveryManager.providerIds().has(model.provider))
7124
+ return true;
7125
+ return this.#providerActivity.get(model.provider)?.staticModelIds.has(model.id) === true;
7126
+ });
7127
+ // Commands such as `--list-models` read without a credential session. The
7128
+ // effective (default) credential still has a descriptor identity, so its
7129
+ // discovery overlay must be visible here instead of being dropped.
7130
+ const scopedDiscovered = this.#getGoogleSessionDiscoveryModels(credentialSessionId);
7131
+ const catalog =
7132
+ scopedDiscovered.length > 0
7133
+ ? this.#resolveDiscoveredModelCatalog(commonModels, scopedDiscovered)
7134
+ : commonModels;
7135
+ return catalog;
6841
7136
  }
6842
7137
 
6843
7138
  #synchronizeEnvironmentCredentials(): void {
@@ -7023,8 +7318,9 @@ export class ModelRegistry {
7023
7318
  /**
7024
7319
  * Find a model by provider and ID.
7025
7320
  */
7026
- find(provider: string, modelId: string): Model<Api> | undefined {
7027
- return resolveProviderModelReference(provider, modelId, this.#models);
7321
+ find(provider: string, modelId: string, credentialSessionId?: string): Model<Api> | undefined {
7322
+ const models = credentialSessionId === undefined ? this.#models : this.getAll(credentialSessionId);
7323
+ return resolveProviderModelReference(provider, modelId, models);
7028
7324
  }
7029
7325
 
7030
7326
  /**
@@ -7115,6 +7411,35 @@ export class ModelRegistry {
7115
7411
  return apiKey;
7116
7412
  }
7117
7413
 
7414
+ /** Get a model API key together with the stored row that resolved it, when any. */
7415
+ async getApiKeyWithCredentialRowId(
7416
+ model: Model<Api>,
7417
+ sessionId?: string,
7418
+ options: {
7419
+ credentialSelector?: AuthCredentialSelector;
7420
+ preferredCredentialSelector?: AuthCredentialSelector;
7421
+ signal?: AbortSignal;
7422
+ } = {},
7423
+ ): Promise<{ apiKey: string | undefined; credentialRowId: number | undefined }> {
7424
+ this.#refreshRotatingConfigApiKey(model.provider);
7425
+ let credentialRowId: number | undefined;
7426
+ const apiKey = await this.#getApiKeyOrNoAuth(model.provider, async () => {
7427
+ const resolved = await this.authStorage.getApiKeyWithCredentialRowId(model.provider, sessionId, {
7428
+ baseUrl: model.baseUrl,
7429
+ modelId: model.id,
7430
+ credentialSelector: options.credentialSelector,
7431
+ preferredCredentialSelector: options.preferredCredentialSelector,
7432
+ owner: this.#authStorageConfigOwner,
7433
+ signal: options.signal,
7434
+ });
7435
+ credentialRowId = isVllmNoAuthToken(model.provider, resolved.apiKey) ? undefined : resolved.credentialRowId;
7436
+ return resolved.apiKey;
7437
+ });
7438
+ if (apiKey === kNoAuth) credentialRowId = undefined;
7439
+ this.#applyEffectiveAuthHeader(model, apiKey);
7440
+ return { apiKey, credentialRowId };
7441
+ }
7442
+
7118
7443
  /**
7119
7444
  * Get API key for a provider (e.g., "openai").
7120
7445
  */