@gajae-code/coding-agent 0.14.2 → 0.15.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 (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -1,3 +1,4 @@
1
+ import * as crypto from "node:crypto";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import {
@@ -88,6 +89,7 @@ import {
88
89
  createProviderSelectionPolicy,
89
90
  type EffectiveProviderAuth,
90
91
  type ProviderSelectionPolicy,
92
+ projectCatalogProviderOrder,
91
93
  } from "./provider-selection-policy";
92
94
  import { type Settings, settings } from "./settings";
93
95
 
@@ -683,6 +685,78 @@ interface DiscoveryProviderConfig {
683
685
  optional?: boolean;
684
686
  }
685
687
 
688
+ const REUSABLE_DISCOVERY_HEADER_NAMES = new Set([
689
+ "accept",
690
+ "content-type",
691
+ "openai-organization",
692
+ "openai-project",
693
+ "x-organization-id",
694
+ "x-project-id",
695
+ "x-routing-id",
696
+ "x-tenant-id",
697
+ "x-workspace-id",
698
+ ]);
699
+
700
+ function isSensitiveDiscoveryHeader(name: string): boolean {
701
+ // Persist values only for a small, audited set of request-shaping headers.
702
+ // Unknown headers fail closed because their semantics cannot be inferred from
703
+ // their names (for example, CF-Access-Jwt-Assertion carries a credential).
704
+ return !REUSABLE_DISCOVERY_HEADER_NAMES.has(name.toLowerCase());
705
+ }
706
+
707
+ /**
708
+ * Credential-safe fingerprint of the effective configured-discovery request
709
+ * identity: non-secret credential evidence, normalized endpoint, effective
710
+ * request headers (config plus runtime overrides, lowercased and sorted), and
711
+ * the semantic request-shape fields (discovery type, provider api, per-prefix
712
+ * api routing, models.dev catalog key). A
713
+ * published cache row is only trusted while this fingerprint is unchanged, so
714
+ * a tenant/project header change (or any other request-shape change)
715
+ * invalidates the authoritative catalog instead of silently serving the old
716
+ * context's models. Configurations containing secret-bearing headers are not
717
+ * fingerprinted or persisted at all because an unsalted digest would permit
718
+ * offline guessing of low-entropy values.
719
+ */
720
+ function fingerprintConfiguredDiscoveryRequestShape(
721
+ providerConfig: DiscoveryProviderConfig,
722
+ authEvidence: string,
723
+ endpoint: string,
724
+ ): string | undefined {
725
+ const sortEntries = (record: Record<string, string> | undefined) =>
726
+ Object.entries(record ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
727
+ try {
728
+ const parsed = new URL(providerConfig.baseUrl ?? "");
729
+ if (parsed.username || parsed.password || parsed.search) return undefined;
730
+ } catch {
731
+ return undefined;
732
+ }
733
+ if (Object.keys(providerConfig.headers ?? {}).some(isSensitiveDiscoveryHeader)) return undefined;
734
+ const context = JSON.stringify({
735
+ authEvidence,
736
+ endpoint,
737
+ headers: sortEntries(providerConfig.headers).map(([name, value]) => [name.toLowerCase(), value]),
738
+ discoveryType: providerConfig.discovery.type,
739
+ api: providerConfig.api,
740
+ apiByModelPrefix: sortEntries(providerConfig.discovery.apiByModelPrefix),
741
+ modelsDevProvider: providerConfig.discovery.modelsDevProvider ?? "",
742
+ });
743
+ return crypto.createHash("sha256").update("gajae:model-discovery-provenance\0").update(context).digest("hex");
744
+ }
745
+
746
+ function fingerprintDescriptorDiscoveryProvenance(authEvidence: string, endpoint: string): string | undefined {
747
+ try {
748
+ const parsed = new URL(endpoint);
749
+ if (parsed.username || parsed.password || parsed.search) return undefined;
750
+ } catch {
751
+ return undefined;
752
+ }
753
+ return crypto
754
+ .createHash("sha256")
755
+ .update("gajae:model-discovery-provenance\0")
756
+ .update(JSON.stringify({ authEvidence, endpoint }))
757
+ .digest("hex");
758
+ }
759
+
686
760
  export interface CanonicalModelQueryOptions {
687
761
  availableOnly?: boolean;
688
762
  candidates?: readonly Model<Api>[];
@@ -692,6 +766,12 @@ export interface CanonicalModelQueryOptions {
692
766
  credentialSessionId?: string;
693
767
  }
694
768
 
769
+ /** One canonical record with its winning variant resolved, from a batch query. */
770
+ export interface CanonicalModelSelection {
771
+ record: CanonicalModelRecord;
772
+ model: Model<Api> | undefined;
773
+ }
774
+
695
775
  /** Result of loading custom models from models.json */
696
776
  interface CustomModelsResult {
697
777
  models?: CustomModelOverlay[];
@@ -1207,17 +1287,17 @@ function normalizeSuppressedSelector(selector: string): string {
1207
1287
  return `${parsed.provider}/${parsed.id}`;
1208
1288
  }
1209
1289
 
1210
- function getDisabledProviderIdsFromSettings(): Set<string> {
1290
+ function getDisabledProviderIdsFromSettings(settingsReader: Pick<Settings, "get"> = settings): Set<string> {
1211
1291
  try {
1212
- return new Set(settings.get("disabledProviders"));
1292
+ return new Set(settingsReader.get("disabledProviders"));
1213
1293
  } catch {
1214
1294
  return new Set();
1215
1295
  }
1216
1296
  }
1217
1297
 
1218
- function getConfiguredProviderOrderFromSettings(): string[] {
1298
+ function getConfiguredProviderOrderFromSettings(settingsReader: Pick<Settings, "getGlobal"> = settings): string[] {
1219
1299
  try {
1220
- const configured = settings.getGlobal("modelProviderOrder");
1300
+ const configured = settingsReader.getGlobal("modelProviderOrder");
1221
1301
  return Array.isArray(configured) ? configured.filter((value): value is string => typeof value === "string") : [];
1222
1302
  } catch {
1223
1303
  return [];
@@ -1240,6 +1320,18 @@ interface ModelManagerDiscoveryOptions {
1240
1320
  authGeneration: string;
1241
1321
  apiKey: string | undefined;
1242
1322
  endpoint: string;
1323
+ endpointContainsUserinfo: boolean;
1324
+ }
1325
+
1326
+ interface ConfiguredDiscoveryResult {
1327
+ provider: string;
1328
+ current: boolean;
1329
+ models: Model<Api>[];
1330
+ authGeneration: string;
1331
+ configurationGeneration: number;
1332
+ endpoint: string;
1333
+ fetched: boolean;
1334
+ clearPublishedModelIds?: readonly string[];
1243
1335
  }
1244
1336
 
1245
1337
  /**
@@ -1295,6 +1387,7 @@ export class ModelRegistry {
1295
1387
  #modelProfiles: Map<string, ModelProfileDefinition> = mergeModelProfiles();
1296
1388
  #configError: ConfigError | undefined = undefined;
1297
1389
  #modelsConfigFile: ConfigFile<ModelsConfig>;
1390
+ #settings: Pick<Settings, "get" | "getGlobal">;
1298
1391
  #lastStaticLoadMtime: number | null = null;
1299
1392
  #lastStaticLoadEnvironmentFingerprint: string | undefined;
1300
1393
  #staticModelsLoaded = false;
@@ -1322,7 +1415,9 @@ export class ModelRegistry {
1322
1415
  constructor(
1323
1416
  readonly authStorage: AuthStorage,
1324
1417
  modelsPath?: string,
1418
+ registrySettings?: Pick<Settings, "get" | "getGlobal">,
1325
1419
  ) {
1420
+ this.#settings = registrySettings ?? settings;
1326
1421
  this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
1327
1422
  this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
1328
1423
  // Set up fallback resolver for custom provider API keys
@@ -1342,6 +1437,11 @@ export class ModelRegistry {
1342
1437
  };
1343
1438
  }
1344
1439
 
1440
+ /** Replace the read-only settings snapshot used by profile-scoped resolution. */
1441
+ setScopedSettings(settingsReader: Pick<Settings, "get" | "getGlobal">): void {
1442
+ this.#settings = settingsReader;
1443
+ }
1444
+
1345
1445
  /**
1346
1446
  * Reload models from disk (built-in + custom from models.json).
1347
1447
  */
@@ -1412,7 +1512,7 @@ export class ModelRegistry {
1412
1512
 
1413
1513
  #reloadStaticModels(): void {
1414
1514
  const currentMtime = this.#modelsConfigFile.getMtimeMs();
1415
- const disabledProviderKey = [...getDisabledProviderIdsFromSettings()].sort().join("\u0000");
1515
+ const disabledProviderKey = [...getDisabledProviderIdsFromSettings(this.#settings)].sort().join("\u0000");
1416
1516
  const environmentFingerprint = this.#getStaticLoadEnvironmentFingerprint();
1417
1517
  if (
1418
1518
  this.#staticModelsLoaded &&
@@ -1658,7 +1758,16 @@ export class ModelRegistry {
1658
1758
  continue;
1659
1759
  }
1660
1760
  const cache = readModelCache<Api>(descriptor.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1661
- if (!cache) {
1761
+ const expectedProvenance = fingerprintDescriptorDiscoveryProvenance(
1762
+ this.#getProviderEvidenceGeneration(descriptor.providerId),
1763
+ this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? ""),
1764
+ );
1765
+ if (
1766
+ expectedProvenance === undefined ||
1767
+ !cache ||
1768
+ cache.dynamicModelIds === undefined ||
1769
+ cache.dynamicModelProvenance !== expectedProvenance
1770
+ ) {
1662
1771
  continue;
1663
1772
  }
1664
1773
  const models = cache.models.map(model =>
@@ -1678,12 +1787,33 @@ export class ModelRegistry {
1678
1787
  #loadCachedDiscoverableModels(): Model<Api>[] {
1679
1788
  const cachedModels: Model<Api>[] = [];
1680
1789
  for (const providerConfig of this.#discoveryManager.providers) {
1681
- const models = this.#discoveryManager.loadCached(providerConfig, this.#cacheDbPath);
1790
+ let expectedProvenance: string | undefined;
1791
+ try {
1792
+ const effectiveConfig = this.#effectiveDiscoveryProviderConfig(providerConfig);
1793
+ expectedProvenance = fingerprintConfiguredDiscoveryRequestShape(
1794
+ effectiveConfig,
1795
+ this.#getProviderEvidenceGeneration(effectiveConfig.provider),
1796
+ this.#normalizeDiscoveryEvidenceEndpoint(effectiveConfig.baseUrl ?? ""),
1797
+ );
1798
+ } catch {
1799
+ // A context that cannot be fully derived cannot vouch for a cache row.
1800
+ expectedProvenance = undefined;
1801
+ }
1802
+ const models = this.#discoveryManager.loadCached(providerConfig, this.#cacheDbPath, expectedProvenance);
1803
+ // Cache rows persist sanitized transport metadata (no headers), so a
1804
+ // rebooted registry re-derives the provider transport override from the
1805
+ // same source the live publish path uses — mirroring the cached
1806
+ // descriptor path — instead of serving header-less models until the
1807
+ // next successful online discovery.
1808
+ const providerOverride = this.#resolveProviderOverride(providerConfig.provider);
1809
+ const withTransport = providerOverride
1810
+ ? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
1811
+ : models;
1682
1812
  const normalized = this.#applyProviderModelOverrides(
1683
1813
  providerConfig.provider,
1684
1814
  this.#normalizeDiscoverableModels(
1685
1815
  providerConfig,
1686
- this.#applyProviderCompat(providerConfig.compat, [...models]),
1816
+ this.#applyProviderCompat(providerConfig.compat, [...withTransport]),
1687
1817
  ),
1688
1818
  );
1689
1819
  applyGeneratedModelPolicies(normalized);
@@ -1724,26 +1854,52 @@ export class ModelRegistry {
1724
1854
  };
1725
1855
  });
1726
1856
  }
1727
- #sanitizeDiscoverableModelsForCache(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
1728
- return providerConfig.discovery.type === "openai-models-list" ||
1729
- providerConfig.discovery.type === "lm-studio" ||
1730
- providerConfig.discovery.type === "omlx"
1731
- ? this.#stripModelBaseUrlQueries(models)
1732
- : models;
1857
+ #sanitizeDiscoverableModelsForCache(_providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
1858
+ return this.#stripModelBaseUrlQueries(models).map(model => ({
1859
+ ...model,
1860
+ headers: undefined,
1861
+ }));
1733
1862
  }
1734
1863
  #stripModelBaseUrlQueries(models: readonly Model<Api>[]): Model<Api>[] {
1735
- return models.map(model => (model.baseUrl ? { ...model, baseUrl: stripUrlQuery(model.baseUrl) } : model));
1864
+ return models.map(model => {
1865
+ if (!model.baseUrl) return model;
1866
+ try {
1867
+ const parsed = new URL(model.baseUrl);
1868
+ parsed.username = "";
1869
+ parsed.password = "";
1870
+ parsed.search = "";
1871
+ parsed.hash = "";
1872
+ return { ...model, baseUrl: parsed.toString().replace(/\/$/, "") };
1873
+ } catch {
1874
+ const { baseUrl: _baseUrl, ...withoutBaseUrl } = model;
1875
+ return withoutBaseUrl as Model<Api>;
1876
+ }
1877
+ });
1878
+ }
1879
+ #stripUrlUserinfo(url: string | undefined): string | undefined {
1880
+ if (!url) return url;
1881
+ try {
1882
+ const parsed = new URL(url);
1883
+ if (!parsed.username && !parsed.password) return url;
1884
+ parsed.username = "";
1885
+ parsed.password = "";
1886
+ return parsed.toString();
1887
+ } catch {
1888
+ return undefined;
1889
+ }
1736
1890
  }
1737
1891
  #restoreLiveDiscoveryBaseUrl(modelBaseUrl: string | undefined, liveBaseUrl: string | undefined): string | undefined {
1738
- if (!modelBaseUrl || !liveBaseUrl?.includes("?")) return modelBaseUrl;
1739
- return this.#normalizeDiscoveryEvidenceEndpoint(stripUrlQuery(modelBaseUrl)) ===
1892
+ if (!modelBaseUrl || !liveBaseUrl) return this.#stripUrlUserinfo(modelBaseUrl);
1893
+ const restored =
1894
+ this.#normalizeDiscoveryEvidenceEndpoint(stripUrlQuery(modelBaseUrl)) !==
1740
1895
  this.#normalizeDiscoveryEvidenceEndpoint(stripUrlQuery(liveBaseUrl))
1741
- ? liveBaseUrl
1742
- : modelBaseUrl;
1896
+ ? modelBaseUrl
1897
+ : liveBaseUrl;
1898
+ return this.#stripUrlUserinfo(restored);
1743
1899
  }
1744
1900
 
1745
1901
  #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
1746
- const disabledProviders = getDisabledProviderIdsFromSettings();
1902
+ const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
1747
1903
  if (!configuredProviders.has("ollama") && !disabledProviders.has("ollama")) {
1748
1904
  this.#discoveryManager.addProvider({
1749
1905
  provider: "ollama",
@@ -2125,7 +2281,7 @@ export class ModelRegistry {
2125
2281
  strategy: ModelRefreshStrategy,
2126
2282
  providerFilter?: ReadonlySet<string>,
2127
2283
  ): Promise<void> {
2128
- const disabledProviders = getDisabledProviderIdsFromSettings();
2284
+ const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
2129
2285
  const selectedDiscoverableProviders = (
2130
2286
  providerFilter
2131
2287
  ? this.#discoveryManager.providers.filter(provider => providerFilter.has(provider.provider))
@@ -2133,17 +2289,7 @@ export class ModelRegistry {
2133
2289
  ).filter(provider => !disabledProviders.has(provider.provider));
2134
2290
  const configuredDiscoveriesPromise =
2135
2291
  selectedDiscoverableProviders.length === 0
2136
- ? Promise.resolve(
2137
- [] as Array<{
2138
- provider: string;
2139
- current: boolean;
2140
- models: Model<Api>[];
2141
- authGeneration: string;
2142
- configurationGeneration: number;
2143
- endpoint: string;
2144
- fetched: boolean;
2145
- }>,
2146
- )
2292
+ ? Promise.resolve([] as ConfiguredDiscoveryResult[])
2147
2293
  : Promise.all(
2148
2294
  selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
2149
2295
  );
@@ -2242,6 +2388,11 @@ export class ModelRegistry {
2242
2388
  const configuredDiscoveries = new Map(currentConfiguredDiscoveryResults.map(result => [result.provider, result]));
2243
2389
  const configuredDiscovered = currentConfiguredDiscoveryResults.flatMap(result => result.models);
2244
2390
  const discovered = [...configuredDiscovered, ...currentBuiltInDiscovered];
2391
+ const clearPublishedModelIds = new Map(
2392
+ currentConfiguredDiscoveryResults
2393
+ .filter(result => result.current && result.clearPublishedModelIds !== undefined)
2394
+ .map(result => [result.provider, new Set(result.clearPublishedModelIds ?? [])]),
2395
+ );
2245
2396
  for (const provider of selectedDiscoverableProviders) {
2246
2397
  const evidence = configuredDiscoveryEvidence.get(provider.provider);
2247
2398
  const discovery = configuredDiscoveries.get(provider.provider);
@@ -2272,7 +2423,19 @@ export class ModelRegistry {
2272
2423
  }
2273
2424
  }
2274
2425
  this.#rebuildProviderActivity();
2426
+ if (clearPublishedModelIds.size > 0) {
2427
+ this.#models = this.#models.filter(model => {
2428
+ const ids = clearPublishedModelIds.get(model.provider);
2429
+ return (
2430
+ !ids?.has(model.id) ||
2431
+ (this.#providerActivity.get(model.provider)?.staticModelIds.has(model.id) ?? false)
2432
+ );
2433
+ });
2434
+ }
2275
2435
  if (discovered.length === 0) {
2436
+ if (clearPublishedModelIds.size > 0) {
2437
+ this.#rebuildCanonicalIndex();
2438
+ }
2276
2439
  return;
2277
2440
  }
2278
2441
  const discoveredModels = this.#applyHardcodedModelPolicies(
@@ -2300,15 +2463,7 @@ export class ModelRegistry {
2300
2463
  async #discoverProviderModels(
2301
2464
  providerConfig: DiscoveryProviderConfig,
2302
2465
  strategy: ModelRefreshStrategy,
2303
- ): Promise<{
2304
- provider: string;
2305
- current: boolean;
2306
- models: Model<Api>[];
2307
- authGeneration: string;
2308
- configurationGeneration: number;
2309
- endpoint: string;
2310
- fetched: boolean;
2311
- }> {
2466
+ ): Promise<ConfiguredDiscoveryResult> {
2312
2467
  const provider = providerConfig.provider;
2313
2468
  const preflightEpoch = this.#optionalAuthPreflightEpoch;
2314
2469
  const preflightGeneration = (this.#optionalAuthPreflightGenerations.get(provider) ?? 0) + 1;
@@ -2394,10 +2549,32 @@ export class ModelRegistry {
2394
2549
  endpoint ===
2395
2550
  this.#normalizeDiscoveryEvidenceEndpoint(this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "");
2396
2551
  const evidence = this.#configuredDiscoveryEvidence.get(provider);
2552
+ const cacheDynamicModelProvenance = fingerprintConfiguredDiscoveryRequestShape(
2553
+ effectiveProviderConfig,
2554
+ authGenerationBeforeDiscovery,
2555
+ endpoint,
2556
+ );
2557
+ if (cacheDynamicModelProvenance === undefined && strategy === "offline") {
2558
+ const previouslyPublishedModelIds = [...(evidence?.modelIds ?? [])];
2559
+ this.#configuredDiscoveryEvidence.delete(provider);
2560
+ return {
2561
+ provider: effectiveProviderConfig.provider,
2562
+ current: true,
2563
+ models: [],
2564
+ authGeneration: authGenerationBeforeDiscovery,
2565
+ configurationGeneration: preflightAuthConfigurationGeneration,
2566
+ endpoint,
2567
+ fetched: false,
2568
+ clearPublishedModelIds: previouslyPublishedModelIds,
2569
+ };
2570
+ }
2571
+ const cacheLookupProvenance =
2572
+ cacheDynamicModelProvenance ?? `gajae:non-cacheable-configured:${crypto.randomUUID()}`;
2397
2573
  const refreshStrategy =
2398
2574
  strategy === "online-if-uncached" &&
2399
- evidence !== undefined &&
2400
- (evidence.authGeneration !== authGenerationBeforeDiscovery || evidence.endpoint !== endpoint)
2575
+ (cacheDynamicModelProvenance === undefined ||
2576
+ (evidence !== undefined &&
2577
+ (evidence.authGeneration !== authGenerationBeforeDiscovery || evidence.endpoint !== endpoint)))
2401
2578
  ? "online"
2402
2579
  : strategy;
2403
2580
  const mergeInput = await this.#discoveryManager.discover(effectiveProviderConfig, refreshStrategy, {
@@ -2418,7 +2595,8 @@ export class ModelRegistry {
2418
2595
  await this.#discoverModelsByProviderType(provider, apiKey),
2419
2596
  ),
2420
2597
  getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
2421
- canPublishCache: () => isCurrentEndpoint(),
2598
+ cacheDynamicModelProvenance: cacheLookupProvenance,
2599
+ canPublishCache: isCurrentEndpoint,
2422
2600
  });
2423
2601
  const authGeneration =
2424
2602
  mergeInput.authGeneration ??
@@ -2555,7 +2733,7 @@ export class ModelRegistry {
2555
2733
  },
2556
2734
  },
2557
2735
  ];
2558
- const disabledProviders = getDisabledProviderIdsFromSettings();
2736
+ const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
2559
2737
  const standardProviderDescriptors = PROVIDER_DESCRIPTORS.filter(
2560
2738
  descriptor => !disabledProviders.has(descriptor.providerId) && !excludedProviderIds.has(descriptor.providerId),
2561
2739
  );
@@ -2603,6 +2781,7 @@ export class ModelRegistry {
2603
2781
  authGeneration,
2604
2782
  apiKey,
2605
2783
  endpoint: this.#normalizeDiscoveryEvidenceEndpoint(baseUrl ?? ""),
2784
+ endpointContainsUserinfo: this.#urlContainsUserinfo(baseUrl ?? ""),
2606
2785
  });
2607
2786
  }
2608
2787
  }
@@ -2617,13 +2796,13 @@ export class ModelRegistry {
2617
2796
  isAuthenticated(key)
2618
2797
  ) {
2619
2798
  const managerOptions = descriptor.createOptions(key);
2799
+ const baseUrl = this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? "";
2620
2800
  options.push({
2621
2801
  options: managerOptions,
2622
2802
  authGeneration,
2623
2803
  apiKey: apiKeyValue,
2624
- endpoint: this.#normalizeDiscoveryEvidenceEndpoint(
2625
- this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? "",
2626
- ),
2804
+ endpoint: this.#normalizeDiscoveryEvidenceEndpoint(baseUrl),
2805
+ endpointContainsUserinfo: this.#urlContainsUserinfo(baseUrl),
2627
2806
  });
2628
2807
  }
2629
2808
  }
@@ -2631,7 +2810,7 @@ export class ModelRegistry {
2631
2810
  }
2632
2811
 
2633
2812
  async #discoverWithModelManager(
2634
- { options, authGeneration, apiKey, endpoint }: ModelManagerDiscoveryOptions,
2813
+ { options, authGeneration, apiKey, endpoint, endpointContainsUserinfo }: ModelManagerDiscoveryOptions,
2635
2814
  strategy: ModelRefreshStrategy,
2636
2815
  ): Promise<Model<Api>[]> {
2637
2816
  const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
@@ -2643,6 +2822,16 @@ export class ModelRegistry {
2643
2822
  this.#providerOverrides.get("xiaomi")?.baseUrl === undefined &&
2644
2823
  resolveProviderBaseUrlFromEnv("xiaomi") === undefined;
2645
2824
  let credentialDerivedEndpoint: string | undefined;
2825
+ const reusableCacheProvenance = endpointContainsUserinfo
2826
+ ? undefined
2827
+ : fingerprintDescriptorDiscoveryProvenance(authGeneration, endpoint);
2828
+ if (reusableCacheProvenance === undefined && strategy === "offline") return [];
2829
+ // Signed/userinfo endpoints may still publish a sanitized best-effort row,
2830
+ // but each refresh gets a new opaque nonce. The nonce contains no endpoint
2831
+ // secret and can never match a previous invocation, so constructor loads,
2832
+ // stale fallback, and online-if-uncached reuse all fail closed.
2833
+ const cacheDynamicModelProvenance =
2834
+ reusableCacheProvenance ?? `gajae:non-cacheable-endpoint:${crypto.randomUUID()}`;
2646
2835
  const isCurrentDiscovery = () =>
2647
2836
  (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
2648
2837
  this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
@@ -2653,7 +2842,7 @@ export class ModelRegistry {
2653
2842
  const manager = createModelManager({
2654
2843
  ...options,
2655
2844
  cacheDbPath: this.#cacheDbPath,
2656
- cacheDynamicModelProvenance: Bun.hash(`${authGeneration}\u0000${endpoint}`).toString(36),
2845
+ cacheDynamicModelProvenance,
2657
2846
  canPublishCache: isCurrentDiscovery,
2658
2847
  ...(options.fetchDynamicModels
2659
2848
  ? {
@@ -3136,6 +3325,14 @@ export class ModelRegistry {
3136
3325
  return endpoint.replace(/\/+$/g, "");
3137
3326
  }
3138
3327
  }
3328
+ #urlContainsUserinfo(endpoint: string): boolean {
3329
+ try {
3330
+ const parsed = new URL(endpoint);
3331
+ return Boolean(parsed.username || parsed.password);
3332
+ } catch {
3333
+ return false;
3334
+ }
3335
+ }
3139
3336
  #isCredentiallessProvider(provider: string): boolean {
3140
3337
  let fallbackMatchesCurrentEvidence = false;
3141
3338
  const fallbackEvidenceGeneration = this.#credentiallessAuthFallbackProviders.get(provider);
@@ -3171,7 +3368,8 @@ export class ModelRegistry {
3171
3368
  const parsed = new URL(raw);
3172
3369
  const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
3173
3370
  parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
3174
- return `${parsed.protocol}//${parsed.host}${parsed.pathname}${parsed.search}`;
3371
+ parsed.hash = "";
3372
+ return parsed.toString().replace(/\/$/, "");
3175
3373
  } catch {
3176
3374
  return raw;
3177
3375
  }
@@ -3278,10 +3476,15 @@ export class ModelRegistry {
3278
3476
  }
3279
3477
  #applyRuntimeProviderOverride(model: Model<Api>, override: ProviderOverride): Model<Api> {
3280
3478
  const withTransportOverride = this.#applyProviderTransportOverride(model, override);
3479
+ const sanitizedBaseUrl = this.#stripUrlUserinfo(withTransportOverride.baseUrl);
3480
+ const sanitizedTransport: Model<Api> =
3481
+ sanitizedBaseUrl === withTransportOverride.baseUrl
3482
+ ? withTransportOverride
3483
+ : { ...withTransportOverride, ...(sanitizedBaseUrl === undefined ? {} : { baseUrl: sanitizedBaseUrl }) };
3281
3484
  const modelCompat = this.#modelOverrides.get(model.provider.toLowerCase())?.get(model.id.toLowerCase())?.compat;
3282
3485
  return modelCompat
3283
- ? { ...withTransportOverride, compat: mergeCompat(withTransportOverride.compat, modelCompat) }
3284
- : withTransportOverride;
3486
+ ? { ...sanitizedTransport, compat: mergeCompat(sanitizedTransport.compat, modelCompat) }
3487
+ : sanitizedTransport;
3285
3488
  }
3286
3489
  #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
3287
3490
  if (this.#runtimeProviderOverrides.size === 0) return models;
@@ -3459,21 +3662,43 @@ export class ModelRegistry {
3459
3662
  return [...this.#configuredProviderIds];
3460
3663
  }
3461
3664
 
3462
- #isModelAvailable(model: Model<Api>, disabledProviders = getDisabledProviderIdsFromSettings()): boolean {
3665
+ #isModelAvailable(model: Model<Api>, disabledProviders?: ReadonlySet<string>): boolean {
3666
+ const disabled = disabledProviders ?? getDisabledProviderIdsFromSettings(this.#settings);
3463
3667
  return (
3464
- !disabledProviders.has(model.provider) &&
3668
+ !disabled.has(model.provider) &&
3465
3669
  (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
3466
3670
  );
3467
3671
  }
3672
+ /** Per-query precomputed variant filter inputs; plan fields are authoritative when provided. */
3673
+ #buildCanonicalVariantFilterPlan(options: CanonicalModelQueryOptions | undefined): {
3674
+ candidateKeys: Set<string> | undefined;
3675
+ disabledProviders: ReadonlySet<string> | undefined;
3676
+ } {
3677
+ return {
3678
+ candidateKeys: options?.candidates
3679
+ ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
3680
+ : undefined,
3681
+ disabledProviders: options?.availableOnly ? getDisabledProviderIdsFromSettings(this.#settings) : undefined,
3682
+ };
3683
+ }
3468
3684
 
3469
3685
  #filterCanonicalVariants(
3470
3686
  record: CanonicalModelRecord,
3471
3687
  options: CanonicalModelQueryOptions | undefined,
3688
+ plan?: { candidateKeys: Set<string> | undefined; disabledProviders: ReadonlySet<string> | undefined },
3472
3689
  ): CanonicalModelVariant[] {
3473
- const candidateKeys = options?.candidates
3474
- ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
3475
- : undefined;
3476
- const disabledProviders = options?.availableOnly ? getDisabledProviderIdsFromSettings() : undefined;
3690
+ const candidateKeys =
3691
+ plan !== undefined
3692
+ ? plan.candidateKeys
3693
+ : options?.candidates
3694
+ ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
3695
+ : undefined;
3696
+ const disabledProviders =
3697
+ plan !== undefined
3698
+ ? plan.disabledProviders
3699
+ : options?.availableOnly
3700
+ ? getDisabledProviderIdsFromSettings(this.#settings)
3701
+ : undefined;
3477
3702
  return record.variants.filter(variant => {
3478
3703
  if (candidateKeys && !candidateKeys.has(variant.selector)) return false;
3479
3704
  if (options?.availableOnly && !this.#isModelAvailable(variant.model, disabledProviders)) return false;
@@ -3520,13 +3745,30 @@ export class ModelRegistry {
3520
3745
  effectiveAuth.set(providerKey, this.#effectiveProviderAuth(model.provider, sessionId));
3521
3746
  }
3522
3747
  return createProviderSelectionPolicy({
3523
- explicitProviderOrder: getConfiguredProviderOrderFromSettings(),
3748
+ explicitProviderOrder: getConfiguredProviderOrderFromSettings(this.#settings),
3524
3749
  effectiveAuth,
3525
3750
  catalogProviders,
3526
3751
  catalogModels,
3527
3752
  });
3528
3753
  }
3529
3754
 
3755
+ /**
3756
+ * Deterministic provider priority for autorouting tier generation: configured
3757
+ * `modelProviderOrder` first, then first-wins catalog order.
3758
+ *
3759
+ * Deliberately takes no session and never touches `authStorage`. It bypasses
3760
+ * `#buildProviderSelectionPolicy` entirely so no `effectiveAuth` map is even
3761
+ * assembled — auth-independence is structural here, not a convention. Ranking
3762
+ * that *is* auth-aware stays private to the policy.
3763
+ *
3764
+ * Providers absent from the catalog are dropped so a dead declaration cannot
3765
+ * pollute the generated setup's `declarationFingerprint`. Returned ids use the
3766
+ * catalog's original spelling because the generator matches provider prefixes
3767
+ * with case-sensitive exact strings.
3768
+ */
3769
+ autoroutingProviderOrder(): readonly string[] {
3770
+ return projectCatalogProviderOrder(getConfiguredProviderOrderFromSettings(), this.#models);
3771
+ }
3530
3772
  #providerRankMap(policy: ProviderSelectionPolicy): Map<string, number> {
3531
3773
  const providerRank = new Map<string, number>();
3532
3774
  for (const provider of policy.orderedProviders()) {
@@ -3572,7 +3814,13 @@ export class ModelRegistry {
3572
3814
  #resolveCanonicalVariant(
3573
3815
  variants: readonly CanonicalModelVariant[],
3574
3816
  sessionId?: string,
3575
- options: { providerRankFirst?: boolean; exactnessKey?: string; credentialSessionId?: string } = {},
3817
+ options: {
3818
+ providerRankFirst?: boolean;
3819
+ exactnessKey?: string;
3820
+ credentialSessionId?: string;
3821
+ providerRank?: Map<string, number>;
3822
+ modelOrder?: Map<string, number>;
3823
+ } = {},
3576
3824
  ): CanonicalModelVariant | undefined {
3577
3825
  if (variants.length === 0) return undefined;
3578
3826
  const normalizedSessionId = sessionId?.trim();
@@ -3585,9 +3833,10 @@ export class ModelRegistry {
3585
3833
  return stickyVariant;
3586
3834
  }
3587
3835
  if (normalizedSessionId && stickySelector) this.#sessionCanonicalVariants.delete(normalizedSessionId);
3588
- const policy = this.#buildProviderSelectionPolicy(options.credentialSessionId ?? normalizedSessionId);
3589
- const providerRank = this.#providerRankMap(policy);
3590
- const modelOrder = this.#catalogModelOrder();
3836
+ const providerRank =
3837
+ options.providerRank ??
3838
+ this.#providerRankMap(this.#buildProviderSelectionPolicy(options.credentialSessionId ?? normalizedSessionId));
3839
+ const modelOrder = options.modelOrder ?? this.#catalogModelOrder();
3591
3840
  const sourceRank: Record<CanonicalModelVariant["source"], number> = {
3592
3841
  override: 1,
3593
3842
  bundled: 1,
@@ -3609,9 +3858,10 @@ export class ModelRegistry {
3609
3858
  }
3610
3859
 
3611
3860
  getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
3861
+ const filterPlan = this.#buildCanonicalVariantFilterPlan(options);
3612
3862
  const records: CanonicalModelRecord[] = [];
3613
3863
  for (const record of this.#canonicalIndex.records) {
3614
- const variants = this.#filterCanonicalVariants(record, options);
3864
+ const variants = this.#filterCanonicalVariants(record, options, filterPlan);
3615
3865
  if (variants.length === 0) {
3616
3866
  continue;
3617
3867
  }
@@ -3624,6 +3874,36 @@ export class ModelRegistry {
3624
3874
  return records;
3625
3875
  }
3626
3876
 
3877
+ /**
3878
+ * Batch form of {@link resolveCanonicalModel} over every canonical record:
3879
+ * one candidate-key set, one provider policy, and one catalog order for the
3880
+ * whole query instead of per record. `model` is `undefined` only when a
3881
+ * record has surviving variants but none can win resolution.
3882
+ */
3883
+ getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[] {
3884
+ const filterPlan = this.#buildCanonicalVariantFilterPlan(options);
3885
+ const providerRank = this.#providerRankMap(
3886
+ this.#buildProviderSelectionPolicy(options?.credentialSessionId ?? options?.sessionId?.trim()),
3887
+ );
3888
+ const modelOrder = this.#catalogModelOrder();
3889
+ const selections: CanonicalModelSelection[] = [];
3890
+ for (const record of this.#canonicalIndex.records) {
3891
+ const variants = this.#filterCanonicalVariants(record, options, filterPlan);
3892
+ if (variants.length === 0) {
3893
+ continue;
3894
+ }
3895
+ const resolved = this.#resolveCanonicalVariant(variants, options?.sessionId, {
3896
+ providerRankFirst: true,
3897
+ credentialSessionId: options?.credentialSessionId,
3898
+ providerRank,
3899
+ modelOrder,
3900
+ });
3901
+ if (resolved && options?.sessionId) this.#rememberCanonicalVariant(options.sessionId, resolved.selector);
3902
+ selections.push({ record: { id: record.id, name: record.name, variants }, model: resolved?.model });
3903
+ }
3904
+ return selections;
3905
+ }
3906
+
3627
3907
  getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
3628
3908
  const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
3629
3909
  if (!record) {
@@ -3790,7 +4070,7 @@ export class ModelRegistry {
3790
4070
  * This is a fast check that doesn't refresh OAuth tokens.
3791
4071
  */
3792
4072
  getAvailable(): Model<Api>[] {
3793
- const disabledProviders = getDisabledProviderIdsFromSettings();
4073
+ const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
3794
4074
  const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
3795
4075
  const envFingerprint = envAvailabilityFingerprint();
3796
4076
  if (
@@ -3926,7 +4206,7 @@ export class ModelRegistry {
3926
4206
  getActiveProviders(): ActiveProviderDescriptor[] {
3927
4207
  try {
3928
4208
  const descriptors: ActiveProviderDescriptor[] = [];
3929
- const disabledProviders = getDisabledProviderIdsFromSettings();
4209
+ const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
3930
4210
  const available = this.#models.filter(model => this.#isModelAvailable(model, disabledProviders));
3931
4211
  for (const model of available) {
3932
4212
  const connectionKind = this.#activeConnectionKind(model);
@@ -3963,7 +4243,7 @@ export class ModelRegistry {
3963
4243
  }
3964
4244
 
3965
4245
  getDiscoverableProviders(): string[] {
3966
- const disabledProviders = getDisabledProviderIdsFromSettings();
4246
+ const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
3967
4247
  return this.#discoveryManager.providers
3968
4248
  .filter(provider => !disabledProviders.has(provider.provider))
3969
4249
  .map(provider => provider.provider);