@open-mercato/cezar 0.9.2 → 0.10.0-pr892.1298

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 (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -38
  3. package/dist/agent-config/account-identity.d.ts +44 -0
  4. package/dist/agent-config/account-identity.js +128 -0
  5. package/dist/agent-config/account-identity.js.map +1 -0
  6. package/dist/agent-config/catalog.d.ts +8 -0
  7. package/dist/agent-config/catalog.js +18 -0
  8. package/dist/agent-config/catalog.js.map +1 -1
  9. package/dist/agent-config/model-settings/claude.d.ts +2 -0
  10. package/dist/agent-config/model-settings/claude.js +11 -0
  11. package/dist/agent-config/model-settings/claude.js.map +1 -0
  12. package/dist/agent-config/model-settings/codex.d.ts +2 -0
  13. package/dist/agent-config/model-settings/codex.js +16 -0
  14. package/dist/agent-config/model-settings/codex.js.map +1 -0
  15. package/dist/agent-config/model-settings/opencode.d.ts +2 -0
  16. package/dist/agent-config/model-settings/opencode.js +8 -0
  17. package/dist/agent-config/model-settings/opencode.js.map +1 -0
  18. package/dist/agent-config/model-settings/pi.d.ts +12 -0
  19. package/dist/agent-config/model-settings/pi.js +18 -0
  20. package/dist/agent-config/model-settings/pi.js.map +1 -0
  21. package/dist/agent-config/model-settings/shared.d.ts +9 -0
  22. package/dist/agent-config/model-settings/shared.js +93 -0
  23. package/dist/agent-config/model-settings/shared.js.map +1 -0
  24. package/dist/agent-config/model-settings/types.d.ts +9 -0
  25. package/dist/agent-config/model-settings/types.js +2 -0
  26. package/dist/agent-config/model-settings/types.js.map +1 -0
  27. package/dist/agent-config/models.d.ts +9 -0
  28. package/dist/agent-config/models.js +32 -0
  29. package/dist/agent-config/models.js.map +1 -0
  30. package/dist/agent-config/service.js +4 -4
  31. package/dist/agent-config/service.js.map +1 -1
  32. package/dist/automations/types.d.ts +4 -0
  33. package/dist/automations/types.js +2 -1
  34. package/dist/automations/types.js.map +1 -1
  35. package/dist/config.d.ts +10 -1
  36. package/dist/config.js +48 -5
  37. package/dist/config.js.map +1 -1
  38. package/dist/contract/agent-config.d.ts +2 -0
  39. package/dist/contract/agent-profiles.d.ts +347 -0
  40. package/dist/contract/automations.d.ts +24 -0
  41. package/dist/contract/events.d.ts +59 -0
  42. package/dist/contract/github.d.ts +88 -0
  43. package/dist/contract/health.d.ts +7 -1
  44. package/dist/contract/index.d.ts +1 -0
  45. package/dist/contract/index.js +1005 -555
  46. package/dist/contract/projects.d.ts +33 -5
  47. package/dist/contract/runs.d.ts +247 -0
  48. package/dist/contract/skills.d.ts +7 -0
  49. package/dist/contract/workflows.d.ts +6 -0
  50. package/dist/contract/workspace.d.ts +137 -2
  51. package/dist/core/agent-env.js +41 -16
  52. package/dist/core/agent-env.js.map +1 -1
  53. package/dist/core/agent-model-policy.d.ts +10 -0
  54. package/dist/core/agent-model-policy.js +36 -0
  55. package/dist/core/agent-model-policy.js.map +1 -0
  56. package/dist/core/agent-profiles.d.ts +62 -0
  57. package/dist/core/agent-profiles.js +90 -0
  58. package/dist/core/agent-profiles.js.map +1 -0
  59. package/dist/core/agent-runner.d.ts +37 -6
  60. package/dist/core/agent-runner.js +35 -3
  61. package/dist/core/agent-runner.js.map +1 -1
  62. package/dist/core/backend-detect.d.ts +5 -5
  63. package/dist/core/backend-detect.js +44 -7
  64. package/dist/core/backend-detect.js.map +1 -1
  65. package/dist/core/claude-cli-runner.js +10 -5
  66. package/dist/core/claude-cli-runner.js.map +1 -1
  67. package/dist/core/codex-app-server-runner.js +13 -3
  68. package/dist/core/codex-app-server-runner.js.map +1 -1
  69. package/dist/core/codex-app-server-transport.js +7 -2
  70. package/dist/core/codex-app-server-transport.js.map +1 -1
  71. package/dist/core/model-identity.d.ts +23 -11
  72. package/dist/core/model-identity.js +41 -24
  73. package/dist/core/model-identity.js.map +1 -1
  74. package/dist/core/model-presets.d.ts +8 -2
  75. package/dist/core/model-presets.js +51 -8
  76. package/dist/core/model-presets.js.map +1 -1
  77. package/dist/core/opencode-model-catalog.d.ts +32 -0
  78. package/dist/core/opencode-model-catalog.js +153 -0
  79. package/dist/core/opencode-model-catalog.js.map +1 -0
  80. package/dist/core/opencode-server-runner.d.ts +2 -0
  81. package/dist/core/opencode-server-runner.js +42 -11
  82. package/dist/core/opencode-server-runner.js.map +1 -1
  83. package/dist/core/pi-runner.d.ts +24 -0
  84. package/dist/core/pi-runner.js +349 -0
  85. package/dist/core/pi-runner.js.map +1 -0
  86. package/dist/core/pi-ui-mapper.d.ts +34 -0
  87. package/dist/core/pi-ui-mapper.js +270 -0
  88. package/dist/core/pi-ui-mapper.js.map +1 -0
  89. package/dist/core/provider-auth.d.ts +95 -3
  90. package/dist/core/provider-auth.js +234 -15
  91. package/dist/core/provider-auth.js.map +1 -1
  92. package/dist/core/provider-availability.d.ts +1 -1
  93. package/dist/core/provider-availability.js +2 -1
  94. package/dist/core/provider-availability.js.map +1 -1
  95. package/dist/core/runner-factory.js +3 -0
  96. package/dist/core/runner-factory.js.map +1 -1
  97. package/dist/core/shell-env.d.ts +28 -0
  98. package/dist/core/shell-env.js +56 -0
  99. package/dist/core/shell-env.js.map +1 -0
  100. package/dist/core/ui-events.d.ts +1 -1
  101. package/dist/core/usage-limit.d.ts +42 -0
  102. package/dist/core/usage-limit.js +209 -0
  103. package/dist/core/usage-limit.js.map +1 -0
  104. package/dist/git-diff-base.d.ts +51 -26
  105. package/dist/git-diff-base.js +163 -37
  106. package/dist/git-diff-base.js.map +1 -1
  107. package/dist/git-worktree.d.ts +15 -6
  108. package/dist/git-worktree.js +10 -3
  109. package/dist/git-worktree.js.map +1 -1
  110. package/dist/index.js +3 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/paths.d.ts +53 -5
  113. package/dist/paths.js +63 -9
  114. package/dist/paths.js.map +1 -1
  115. package/dist/planner.js +6 -0
  116. package/dist/planner.js.map +1 -1
  117. package/dist/release/snapshot.d.ts +13 -2
  118. package/dist/release/snapshot.js +25 -1
  119. package/dist/release/snapshot.js.map +1 -1
  120. package/dist/runs/agent-tmpdir.d.ts +41 -0
  121. package/dist/runs/agent-tmpdir.js +183 -0
  122. package/dist/runs/agent-tmpdir.js.map +1 -0
  123. package/dist/runs/auto-name.js +6 -1
  124. package/dist/runs/auto-name.js.map +1 -1
  125. package/dist/runs/event-history.d.ts +50 -0
  126. package/dist/runs/event-history.js +613 -0
  127. package/dist/runs/event-history.js.map +1 -0
  128. package/dist/runs/run-index.d.ts +19 -0
  129. package/dist/runs/run-index.js +43 -0
  130. package/dist/runs/run-index.js.map +1 -0
  131. package/dist/runs/store.d.ts +78 -19
  132. package/dist/runs/store.js +175 -43
  133. package/dist/runs/store.js.map +1 -1
  134. package/dist/server/capabilities.d.ts +15 -1
  135. package/dist/server/capabilities.js +16 -1
  136. package/dist/server/capabilities.js.map +1 -1
  137. package/dist/server/forge/github.d.ts +169 -0
  138. package/dist/server/forge/github.js +511 -0
  139. package/dist/server/forge/github.js.map +1 -1
  140. package/dist/server/forge/index.d.ts +9 -0
  141. package/dist/server/forge/index.js +14 -0
  142. package/dist/server/forge/index.js.map +1 -1
  143. package/dist/server/git-changes.d.ts +7 -5
  144. package/dist/server/git-changes.js +15 -10
  145. package/dist/server/git-changes.js.map +1 -1
  146. package/dist/server/github.d.ts +2 -2
  147. package/dist/server/github.js +1 -1
  148. package/dist/server/github.js.map +1 -1
  149. package/dist/server/open-in-app.d.ts +2 -1
  150. package/dist/server/open-in-app.js +3 -1
  151. package/dist/server/open-in-app.js.map +1 -1
  152. package/dist/server/open-in-terminal.d.ts +46 -1
  153. package/dist/server/open-in-terminal.js +72 -13
  154. package/dist/server/open-in-terminal.js.map +1 -1
  155. package/dist/server/provider-action-gate.js +3 -1
  156. package/dist/server/provider-action-gate.js.map +1 -1
  157. package/dist/server/server.d.ts +2206 -434
  158. package/dist/server/server.js +1219 -136
  159. package/dist/server/server.js.map +1 -1
  160. package/dist/server-install/engine.js +5 -18
  161. package/dist/server-install/engine.js.map +1 -1
  162. package/dist/server-install/steps.js +6 -2
  163. package/dist/server-install/steps.js.map +1 -1
  164. package/dist/skills.js +4 -0
  165. package/dist/skills.js.map +1 -1
  166. package/dist/workflows/run.d.ts +229 -9
  167. package/dist/workflows/run.js +877 -147
  168. package/dist/workflows/run.js.map +1 -1
  169. package/dist/workflows/types.d.ts +3 -0
  170. package/dist/workflows/types.js +11 -2
  171. package/dist/workflows/types.js.map +1 -1
  172. package/dist/workspace/agent-accounts.d.ts +153 -0
  173. package/dist/workspace/agent-accounts.js +304 -0
  174. package/dist/workspace/agent-accounts.js.map +1 -0
  175. package/dist/workspace/agent-profiles.d.ts +78 -0
  176. package/dist/workspace/agent-profiles.js +115 -0
  177. package/dist/workspace/agent-profiles.js.map +1 -0
  178. package/dist/workspace/config.d.ts +27 -1
  179. package/dist/workspace/config.js +83 -2
  180. package/dist/workspace/config.js.map +1 -1
  181. package/dist/workspace/projects-cli.js +54 -4
  182. package/dist/workspace/projects-cli.js.map +1 -1
  183. package/dist/workspace/projects.d.ts +19 -1
  184. package/dist/workspace/projects.js +34 -1
  185. package/dist/workspace/projects.js.map +1 -1
  186. package/dist/workspace/semaphore.d.ts +46 -1
  187. package/dist/workspace/semaphore.js +41 -4
  188. package/dist/workspace/semaphore.js.map +1 -1
  189. package/dist/workspace/ui-state.d.ts +3 -2
  190. package/dist/workspace/ui-state.js +3 -2
  191. package/dist/workspace/ui-state.js.map +1 -1
  192. package/package.json +3 -3
  193. package/scripts/mock-claude.mjs +20 -0
  194. package/scripts/mock-pi-rpc.mjs +83 -0
  195. package/web/dist/assets/{alert-dialog-ghffK6g9.js → alert-dialog-Byp42_IG.js} +1 -1
  196. package/web/dist/assets/arrow-down-CGDbDYKD.js +1 -0
  197. package/web/dist/assets/arrow-left-ESCjr1Hq.js +1 -0
  198. package/web/dist/assets/centered-state-LfAW-igm.js +43 -0
  199. package/web/dist/assets/chevron-right-Du-P1IUd.js +1 -0
  200. package/web/dist/assets/{collapsible-B0JVzd0B.js → collapsible-CBiiqDJM.js} +1 -1
  201. package/web/dist/assets/{commit-list-wm6DPHNs.js → commit-list-6PPve7PE.js} +1 -1
  202. package/web/dist/assets/{compare-variants-DbdP4Y5T.js → compare-variants-BDnp6mXJ.js} +1 -1
  203. package/web/dist/assets/{diff-Bizc96qe.js → diff-_jK0OWpF.js} +2 -2
  204. package/web/dist/assets/{diff-stat-Dyfg3ltF.js → diff-stat-CIrTu4Ym.js} +1 -1
  205. package/web/dist/assets/{diff-view-CG8ek6tC.js → diff-view-COA_5IyY.js} +1 -1
  206. package/web/dist/assets/dropdown-menu-BofYpFFw.js +1 -0
  207. package/web/dist/assets/editable-title-_J0zFDl-.js +1 -0
  208. package/web/dist/assets/{ellipsis-vertical-DpGTRqC2.js → ellipsis-vertical-BeMR-l62.js} +1 -1
  209. package/web/dist/assets/{file-DlUGpUlb.js → file-DY996Pee.js} +1 -1
  210. package/web/dist/assets/{folder-BAY6Vddy.js → folder-NO3Ec-TY.js} +1 -1
  211. package/web/dist/assets/git-toolbar-BqJeUWcD.js +1 -0
  212. package/web/dist/assets/github-C96HPV9E.js +1 -0
  213. package/web/dist/assets/{image-preview-BWGk5fPX.js → image-preview-D9cCDfKQ.js} +1 -1
  214. package/web/dist/assets/index-Bx-grqa9.css +2 -0
  215. package/web/dist/assets/index-DoVvGDfW.js +7 -0
  216. package/web/dist/assets/{markdown-BGBOGCA_.js → markdown-DDzk11zj.js} +1 -1
  217. package/web/dist/assets/new-task-form-BNLLgC6P.js +1 -0
  218. package/web/dist/assets/pill-CVM1EmV4.js +1 -0
  219. package/web/dist/assets/{project-router-Cx8NPoqn.js → project-router-LRCtP48W.js} +1 -1
  220. package/web/dist/assets/prompt-templates-DFOvYgtW.js +15 -0
  221. package/web/dist/assets/{refresh-cw-BoGdAykg.js → refresh-cw-D1ys6QY2.js} +1 -1
  222. package/web/dist/assets/repo-git-CzUnsgWY.js +1 -0
  223. package/web/dist/assets/{run-diff-EcOkf3ti.js → run-diff-BlnqwfSS.js} +2 -2
  224. package/web/dist/assets/run-header-Bn7cN0Z1.js +1 -0
  225. package/web/dist/assets/{search-x-X3EpVhgs.js → search-x-D6FSpxId.js} +1 -1
  226. package/web/dist/assets/{skill-empty-hint-CZLALx6l.js → skill-empty-hint-iwpCcqgI.js} +1 -1
  227. package/web/dist/assets/skills-CezbOaol.js +1 -0
  228. package/web/dist/assets/{sparkles-Dt4q7pbG.js → sparkles-MrQrgCI9.js} +1 -1
  229. package/web/dist/assets/{square-terminal-hwBL8XsC.js → square-terminal-TTH64lO7.js} +1 -1
  230. package/web/dist/assets/tab-link-FYksz037.js +1 -0
  231. package/web/dist/assets/task-changes-ClWlnzBL.js +1 -0
  232. package/web/dist/assets/{task-commits-BTTA5k5T.js → task-commits-Crbrl86b.js} +1 -1
  233. package/web/dist/assets/{task-files-Da8_4C9g.js → task-files-uSCWyD0-.js} +2 -2
  234. package/web/dist/assets/task-thread-DPmamBA1.js +9 -0
  235. package/web/dist/assets/{textarea-CUdokhkv.js → textarea-B9Ahj4E4.js} +1 -1
  236. package/web/dist/assets/thread-loading-BH_ypQoy.js +1 -0
  237. package/web/dist/assets/{trash-2-DhVIdWv8.js → trash-2-D8S3V3fu.js} +1 -1
  238. package/web/dist/assets/{triangle-alert-XYT0DGCI.js → triangle-alert-bB7p9n5E.js} +1 -1
  239. package/web/dist/assets/{upload-BKjN59-1.js → upload-Bd5oRLPV.js} +1 -1
  240. package/web/dist/assets/{use-desktop-Bf5LS_P4.js → use-desktop-BNvbRBeZ.js} +1 -1
  241. package/web/dist/assets/{use-submit-shortcut-CzVyoAMd.js → use-submit-shortcut-B-xhu4Bf.js} +1 -1
  242. package/web/dist/assets/utils-BhpbVMwN.js +64 -0
  243. package/web/dist/assets/{workflows-o2QU_pvU.js → workflows-RmfbM9Vy.js} +3 -3
  244. package/web/dist/assets/{zoomable-image-7WyUiaVD.js → zoomable-image-BCW6ZBV0.js} +1 -1
  245. package/web/dist/index.html +24 -23
  246. package/web/dist/assets/arrow-down-Cz8qb71e.js +0 -1
  247. package/web/dist/assets/arrow-left-wcYKQXI_.js +0 -1
  248. package/web/dist/assets/centered-state-ZtnFHA-n.js +0 -43
  249. package/web/dist/assets/chevron-right-G4caF2QV.js +0 -1
  250. package/web/dist/assets/circle-check--KvBxXWp.js +0 -1
  251. package/web/dist/assets/circle-x-C3EFZ3xf.js +0 -1
  252. package/web/dist/assets/dropdown-menu-C1Dlf35P.js +0 -1
  253. package/web/dist/assets/editable-title-5rC_QRBZ.js +0 -1
  254. package/web/dist/assets/git-pull-request-agOG0iGM.js +0 -1
  255. package/web/dist/assets/git-toolbar-DGiNMmtY.js +0 -1
  256. package/web/dist/assets/github-Vp7lxYGQ.js +0 -1
  257. package/web/dist/assets/index-DSxyJpuK.css +0 -2
  258. package/web/dist/assets/index-DiawI6RX.js +0 -6
  259. package/web/dist/assets/new-task-form-BpAw_Dks.js +0 -1
  260. package/web/dist/assets/pill-Dvs-P0xy.js +0 -1
  261. package/web/dist/assets/prompt-templates-CSBYBR1I.js +0 -15
  262. package/web/dist/assets/repo-git-PmX-BNMo.js +0 -1
  263. package/web/dist/assets/run-header-BGiv6_qX.js +0 -1
  264. package/web/dist/assets/skills-CC91ouGH.js +0 -1
  265. package/web/dist/assets/tab-link-DIlR5T07.js +0 -1
  266. package/web/dist/assets/task-changes-XkHDxzQA.js +0 -1
  267. package/web/dist/assets/task-thread-DoNXUsHr.js +0 -9
  268. package/web/dist/assets/thread-loading-B8q8ukXJ.js +0 -1
  269. package/web/dist/assets/utils-DsLuRXwV.js +0 -64
@@ -40,12 +40,13 @@ export declare class ModelIdentityError extends Error {
40
40
  constructor(message: string);
41
41
  }
42
42
  /**
43
- * Providers cezar names today. Informational and extensible — an unknown
44
- * provider in an explicit `provider/model` string still passes through, so
45
- * future models and new backends are never silently rejected.
43
+ * No provider allowlist lives here on purpose (#548). `resolveModelIdentity`
44
+ * lets an unknown provider in an explicit `provider/model` string pass through,
45
+ * so future models and new backends are never silently rejected — an exported
46
+ * roster of "providers cezar names today" would gate nothing, and an unused
47
+ * public export reads as a contract someone must keep current. The per-backend
48
+ * knowledge that IS load-bearing lives in `BACKEND_MODEL_MAP` below.
46
49
  */
47
- export declare const KNOWN_PROVIDERS: readonly ['anthropic', 'openai', 'google', 'openrouter'];
48
- export type KnownProvider = (typeof KNOWN_PROVIDERS)[number];
49
50
  interface BackendModelMap {
50
51
  /**
51
52
  * The provider a bare (provider-less) model id belongs to for this backend.
@@ -54,6 +55,10 @@ interface BackendModelMap {
54
55
  * It also selects the wire form in {@link toBackendModel}.
55
56
  */
56
57
  readonly defaultProvider?: string;
58
+ /** Whether explicit foreign provider/model ids are valid backend-native ids. */
59
+ readonly allowExplicitProvider?: boolean;
60
+ /** Whether the backend wire model keeps the explicit provider prefix. */
61
+ readonly wireProviderQualified?: boolean;
57
62
  /**
58
63
  * Bare model id → provider override, for the rare backend whose bare ids
59
64
  * span providers. Checked before `defaultProvider`; empty for today's set.
@@ -63,7 +68,11 @@ interface BackendModelMap {
63
68
  /**
64
69
  * Per-backend mapping table — the one place #387 extends for the `pi` runner.
65
70
  * `claude-cli` is the legacy id kept so old run records normalise identically
66
- * to `claude`.
71
+ * to `claude`. That is now true end to end: `storedRunnerSchema`
72
+ * (`runs/store.ts`) parses the legacy spelling out of `runs.json` and folds it
73
+ * to `claude` (#547), so the entry below is what a record carrying it maps
74
+ * through on the way in — not, as the comment used to imply, a mapping for a
75
+ * value the store could never load.
67
76
  */
68
77
  export declare const BACKEND_MODEL_MAP: Readonly<Record<AgentBackend, BackendModelMap>>;
69
78
  /** Serialize a canonical identity to its `provider/model` string form. */
@@ -84,14 +93,15 @@ export declare function parseModelIdentity(raw: string | undefined | null): Mode
84
93
  * explicit choice, not a silent fallback);
85
94
  * - an explicit `provider/model` → that identity, for a multi-provider backend
86
95
  * or when the provider is the single-provider backend's own;
87
- * - an explicit `provider/model` naming a FOREIGN provider on a single-provider
88
- * backend → throws {@link ModelIdentityError} (it would be dropped on the wire
89
- * yet persisted, asserting a provider that never ran — #405 review M2);
96
+ * - an explicit `provider/model` naming a FOREIGN provider is preserved when
97
+ * the backend supports custom/gateway model ids;
90
98
  * - a bare id → the backend's default provider;
91
99
  * - a bare id on a backend with no default provider → throws
92
100
  * {@link ModelIdentityError}.
93
101
  */
94
- export declare function resolveModelIdentity(backend: AgentBackend, raw: string | undefined): ModelIdentity | undefined;
102
+ export declare function resolveModelIdentity(backend: AgentBackend, raw: string | undefined, options?: {
103
+ configuredProvider?: string;
104
+ }): ModelIdentity | undefined;
95
105
  /**
96
106
  * The model string to hand `backend`'s CLI/API for a canonical identity — the
97
107
  * inverse of {@link resolveModelIdentity}. Single-provider backends
@@ -106,7 +116,9 @@ export declare function toBackendModel(backend: AgentBackend, id: ModelIdentity)
106
116
  * `undefined` for empty/auto. Throws {@link ModelIdentityError} on an
107
117
  * unresolvable model — the single fail-loud gate the run wiring calls.
108
118
  */
109
- export declare function normalizeModelForBackend(backend: AgentBackend, raw: string | undefined): {
119
+ export declare function normalizeModelForBackend(backend: AgentBackend, raw: string | undefined, options?: {
120
+ configuredProvider?: string;
121
+ }): {
110
122
  backendModel: string;
111
123
  identity: ModelIdentity;
112
124
  } | undefined;
@@ -10,24 +10,31 @@ export class ModelIdentityError extends Error {
10
10
  this.name = 'ModelIdentityError';
11
11
  }
12
12
  }
13
- /**
14
- * Providers cezar names today. Informational and extensible — an unknown
15
- * provider in an explicit `provider/model` string still passes through, so
16
- * future models and new backends are never silently rejected.
17
- */
18
- export const KNOWN_PROVIDERS = ['anthropic', 'openai', 'google', 'openrouter'];
19
13
  /**
20
14
  * Per-backend mapping table — the one place #387 extends for the `pi` runner.
21
15
  * `claude-cli` is the legacy id kept so old run records normalise identically
22
- * to `claude`.
16
+ * to `claude`. That is now true end to end: `storedRunnerSchema`
17
+ * (`runs/store.ts`) parses the legacy spelling out of `runs.json` and folds it
18
+ * to `claude` (#547), so the entry below is what a record carrying it maps
19
+ * through on the way in — not, as the comment used to imply, a mapping for a
20
+ * value the store could never load.
23
21
  */
24
22
  export const BACKEND_MODEL_MAP = {
25
- claude: { defaultProvider: 'anthropic' },
26
- 'claude-cli': { defaultProvider: 'anthropic' },
23
+ // Claude Code supports custom/gateway model ids such as DeepSeek via an
24
+ // Anthropic-compatible endpoint, so preserve an explicit provider/model.
25
+ claude: { defaultProvider: 'anthropic', allowExplicitProvider: true, wireProviderQualified: true },
26
+ 'claude-cli': { defaultProvider: 'anthropic', allowExplicitProvider: true, wireProviderQualified: true },
27
+ // Codex selects the provider from model_provider in config.toml. A foreign
28
+ // explicit prefix is accepted only when the caller proves it matches that
29
+ // effective setting; the provider is then stripped from the wire model id.
27
30
  codex: { defaultProvider: 'openai' },
28
31
  // opencode selects across providers, so a bare model is ambiguous: reject it
29
32
  // loudly rather than let the server pick a default the user never asked for.
30
33
  opencode: {},
34
+ // pi selects across providers with the same `provider/model` convention as
35
+ // opencode (#387) — no default provider, so a bare model is rejected loudly
36
+ // and `toBackendModel` hands pi the full `provider/model` on its `--model`.
37
+ pi: {},
31
38
  };
32
39
  const SLASH = '/';
33
40
  /** Serialize a canonical identity to its `provider/model` string form. */
@@ -59,31 +66,34 @@ export function parseModelIdentity(raw) {
59
66
  * explicit choice, not a silent fallback);
60
67
  * - an explicit `provider/model` → that identity, for a multi-provider backend
61
68
  * or when the provider is the single-provider backend's own;
62
- * - an explicit `provider/model` naming a FOREIGN provider on a single-provider
63
- * backend → throws {@link ModelIdentityError} (it would be dropped on the wire
64
- * yet persisted, asserting a provider that never ran — #405 review M2);
69
+ * - an explicit `provider/model` naming a FOREIGN provider is preserved when
70
+ * the backend supports custom/gateway model ids;
65
71
  * - a bare id → the backend's default provider;
66
72
  * - a bare id on a backend with no default provider → throws
67
73
  * {@link ModelIdentityError}.
68
74
  */
69
- export function resolveModelIdentity(backend, raw) {
75
+ export function resolveModelIdentity(backend, raw, options = {}) {
70
76
  if (!raw || !raw.trim())
71
77
  return undefined;
72
78
  const map = BACKEND_MODEL_MAP[backend] ?? {};
79
+ const configuredProvider = options.configuredProvider?.trim().toLowerCase() || undefined;
80
+ const effectiveProvider = configuredProvider ?? map.defaultProvider;
73
81
  const explicit = parseModelIdentity(raw);
74
82
  if (explicit) {
75
- // A single-provider backend (claude/codex) can only serve its own provider. An explicit
76
- // foreign provider would be silently dropped on the wire — `toBackendModel` hands the CLI
77
- // the bare model id — while the record persists the foreign provider, asserting one that
78
- // never served the run (#405 review M2). Reject it, the same fail-loud contract a bare id
79
- // gets on a multi-provider backend.
80
- if (map.defaultProvider !== undefined && explicit.provider !== map.defaultProvider) {
81
- throw new ModelIdentityError(`provider "${explicit.provider}" can't be served by the ${backend} runner (serves ${map.defaultProvider}) — use "${map.defaultProvider}/${explicit.model}" or a bare model id`);
83
+ // Most single-provider backends need their bare wire form, but Claude Code
84
+ // also accepts explicit gateway/custom provider ids. Keep rejecting foreign
85
+ // providers for backends that do not advertise that capability.
86
+ if (map.defaultProvider !== undefined &&
87
+ explicit.provider !== effectiveProvider &&
88
+ !map.allowExplicitProvider) {
89
+ throw new ModelIdentityError(configuredProvider
90
+ ? `provider "${explicit.provider}" doesn't match the ${backend} runner's configured provider "${configuredProvider}" — use "${configuredProvider}/${explicit.model}" or a bare model id`
91
+ : `provider "${explicit.provider}" can't be verified for the ${backend} runner (serves ${map.defaultProvider}) — use "${map.defaultProvider}/${explicit.model}" or a bare model id`);
82
92
  }
83
93
  return explicit;
84
94
  }
85
95
  const model = raw.trim();
86
- const provider = map.providerByModel?.[model] ?? map.defaultProvider;
96
+ const provider = map.providerByModel?.[model] ?? effectiveProvider;
87
97
  if (!provider) {
88
98
  throw new ModelIdentityError(`model "${model}" is ambiguous for the ${backend} runner — name the provider as "provider/model" (e.g. anthropic/${model})`);
89
99
  }
@@ -98,7 +108,14 @@ export function resolveModelIdentity(backend, raw) {
98
108
  */
99
109
  export function toBackendModel(backend, id) {
100
110
  const map = BACKEND_MODEL_MAP[backend] ?? {};
101
- return map.defaultProvider === undefined ? formatModelIdentity(id) : id.model;
111
+ if (map.defaultProvider === undefined)
112
+ return formatModelIdentity(id);
113
+ if (map.allowExplicitProvider &&
114
+ map.wireProviderQualified &&
115
+ id.provider !== map.defaultProvider) {
116
+ return formatModelIdentity(id);
117
+ }
118
+ return id.model;
102
119
  }
103
120
  /**
104
121
  * Normalise a raw model string for `backend` in one step: the backend-native
@@ -106,8 +123,8 @@ export function toBackendModel(backend, id) {
106
123
  * `undefined` for empty/auto. Throws {@link ModelIdentityError} on an
107
124
  * unresolvable model — the single fail-loud gate the run wiring calls.
108
125
  */
109
- export function normalizeModelForBackend(backend, raw) {
110
- const identity = resolveModelIdentity(backend, raw);
126
+ export function normalizeModelForBackend(backend, raw, options = {}) {
127
+ const identity = resolveModelIdentity(backend, raw, options);
111
128
  if (!identity)
112
129
  return undefined;
113
130
  return { backendModel: toBackendModel(backend, identity), identity };
@@ -1 +1 @@
1
- {"version":3,"file":"model-identity.js","sourceRoot":"","sources":["../../src/core/model-identity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model-identity.js","sourceRoot":"","sources":["../../src/core/model-identity.ts"],"names":[],"mappings":"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"}
@@ -8,8 +8,14 @@ import type { RunnerId } from './agent-runner.ts';
8
8
  * and config presets must keep working), so the only thing ever rejected is a model that is
9
9
  * recognizably ANOTHER runner's preset — the corruption a client/server resolution mismatch
10
10
  * can produce (#401 review). Unknown ids never conflict (fail-open).
11
+ *
12
+ * OpenCode lists nothing here on purpose (#794): its models are discovered from the host
13
+ * (`opencode-model-catalog.ts`), so any hard-coded list would be one release away from naming
14
+ * models the user's provider does not have. Its half of the guard is the structural check in
15
+ * {@link modelConflictsWithRunner} instead, which needs no vendor knowledge at all.
11
16
  */
12
17
  export declare const KNOWN_PRESETS_BY_RUNNER: Record<RunnerId, readonly string[]>;
13
- /** True when `model` is recognizably a preset of a runner OTHER than `runner` (and not also
14
- * one of `runner`'s own presets). `''`/unknown/custom ids never conflict. */
18
+ /** True when `model` is recognizably a preset of a runner OTHER than `runner` (and not also one
19
+ * of `runner`'s own presets), or when its provider prefix is one this runner is known not to
20
+ * serve. `''`/unknown/custom ids never conflict. */
15
21
  export declare function modelConflictsWithRunner(model: string, runner: RunnerId): boolean;
@@ -1,3 +1,12 @@
1
+ import { BACKEND_MODEL_MAP, parseModelIdentity } from './model-identity.js';
2
+ /**
3
+ * The vendor prefixes this module treats as evidence of a mis-paired model. Deliberately local
4
+ * rather than an export of `model-identity.ts` (#548/#831): nothing else may gate on it, because
5
+ * `resolveModelIdentity` lets an unknown provider through on purpose. It is a heuristic for
6
+ * {@link namesAnotherKnownProvider} alone, and being short is a feature — every entry costs a
7
+ * false refusal if cezar is ever wrong about who serves it.
8
+ */
9
+ const KNOWN_PROVIDERS = ['anthropic', 'openai', 'google', 'openrouter'];
1
10
  /**
2
11
  * The model-preset ids each runner's picker offers — the ids of the web composer's
3
12
  * `MODELS_BY_RUNNER` (packages/web/src/routes/new-task-form.ts), hand-mirrored the same way the
@@ -7,6 +16,11 @@
7
16
  * and config presets must keep working), so the only thing ever rejected is a model that is
8
17
  * recognizably ANOTHER runner's preset — the corruption a client/server resolution mismatch
9
18
  * can produce (#401 review). Unknown ids never conflict (fail-open).
19
+ *
20
+ * OpenCode lists nothing here on purpose (#794): its models are discovered from the host
21
+ * (`opencode-model-catalog.ts`), so any hard-coded list would be one release away from naming
22
+ * models the user's provider does not have. Its half of the guard is the structural check in
23
+ * {@link modelConflictsWithRunner} instead, which needs no vendor knowledge at all.
10
24
  */
11
25
  export const KNOWN_PRESETS_BY_RUNNER = {
12
26
  claude: [
@@ -19,20 +33,49 @@ export const KNOWN_PRESETS_BY_RUNNER = {
19
33
  'claude-haiku-4-5',
20
34
  ],
21
35
  codex: ['gpt-5.1-codex', 'gpt-5.1-codex-mini', 'gpt-5-codex'],
22
- opencode: [
23
- 'anthropic/claude-opus-4-8',
24
- 'anthropic/claude-sonnet-5',
25
- 'openai/gpt-5.1',
26
- 'openai/gpt-5.1-codex',
27
- ],
36
+ opencode: [],
37
+ // pi lists nothing for the same reason OpenCode does (#794), plus one of its own: it picks a
38
+ // model with the canonical `provider/model` convention and has no default provider, so the
39
+ // structural check in {@link modelConflictsWithRunner} already guards it without vendor
40
+ // knowledge. Listing pi's composer presets here would actively break OpenCode — the ids
41
+ // overlap (`anthropic/claude-sonnet-5`), and a shared id present in pi's list but absent from
42
+ // OpenCode's empty one would read as "another runner's preset" and be refused.
43
+ pi: [],
28
44
  };
29
- /** True when `model` is recognizably a preset of a runner OTHER than `runner` (and not also
30
- * one of `runner`'s own presets). `''`/unknown/custom ids never conflict. */
45
+ /**
46
+ * True when the `provider/` prefix on `model` names a provider cezar knows that this
47
+ * single-provider runner does not serve — `anthropic/…` handed to codex.
48
+ *
49
+ * Deliberately narrower than the run-time gate in `model-identity.ts`: this check is
50
+ * synchronous and therefore cannot read the runner's `configuredProvider`, so an UNFAMILIAR
51
+ * prefix (`my-org/custom-tune`) is left alone — it may well be the user's own gateway, which
52
+ * the run-time gate accepts once it has read the config. Only a prefix naming another known
53
+ * vendor is evidence enough to refuse the pairing up front.
54
+ */
55
+ function namesAnotherKnownProvider(model, runner) {
56
+ const map = BACKEND_MODEL_MAP[runner];
57
+ // Nothing to contradict: OpenCode spans providers (no default of its own), and Claude Code
58
+ // accepts explicit gateway/custom provider ids by design.
59
+ if (map?.defaultProvider === undefined || map.allowExplicitProvider)
60
+ return false;
61
+ const explicit = parseModelIdentity(model);
62
+ if (!explicit || explicit.provider === map.defaultProvider)
63
+ return false;
64
+ return KNOWN_PROVIDERS.includes(explicit.provider);
65
+ }
66
+ /** True when `model` is recognizably a preset of a runner OTHER than `runner` (and not also one
67
+ * of `runner`'s own presets), or when its provider prefix is one this runner is known not to
68
+ * serve. `''`/unknown/custom ids never conflict. */
31
69
  export function modelConflictsWithRunner(model, runner) {
32
70
  if (!model)
33
71
  return false;
34
72
  if (KNOWN_PRESETS_BY_RUNNER[runner]?.includes(model))
35
73
  return false;
74
+ // The provider half of the guard needs no vendor model knowledge, so — unlike the preset
75
+ // lists — it keeps holding for models that do not exist yet. That is what lets OpenCode's
76
+ // entry above be empty (#794) without losing the protection it used to provide.
77
+ if (namesAnotherKnownProvider(model, runner))
78
+ return true;
36
79
  return Object.entries(KNOWN_PRESETS_BY_RUNNER).some(([other, presets]) => other !== runner && presets.includes(model));
37
80
  }
38
81
  //# sourceMappingURL=model-presets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-presets.js","sourceRoot":"","sources":["../../src/core/model-presets.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAwC;IAC1E,MAAM,EAAE;QACN,MAAM;QACN,QAAQ;QACR,OAAO;QACP,gBAAgB;QAChB,iBAAiB;QACjB,iBAAiB;QACjB,kBAAkB;KACnB;IACD,KAAK,EAAE,CAAC,eAAe,EAAE,oBAAoB,EAAE,aAAa,CAAC;IAC7D,QAAQ,EAAE;QACR,2BAA2B;QAC3B,2BAA2B;QAC3B,gBAAgB;QAChB,sBAAsB;KACvB;CACF,CAAC;AAEF;8EAC8E;AAC9E,MAAM,UAAU,wBAAwB,CAAC,KAAa,EAAE,MAAgB;IACtE,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,uBAAuB,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnE,OAAO,MAAM,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC,IAAI,CACjD,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAClE,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"model-presets.js","sourceRoot":"","sources":["../../src/core/model-presets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE5E;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,CAAC,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAU,CAAC;AAEjF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAwC;IAC1E,MAAM,EAAE;QACN,MAAM;QACN,QAAQ;QACR,OAAO;QACP,gBAAgB;QAChB,iBAAiB;QACjB,iBAAiB;QACjB,kBAAkB;KACnB;IACD,KAAK,EAAE,CAAC,eAAe,EAAE,oBAAoB,EAAE,aAAa,CAAC;IAC7D,QAAQ,EAAE,EAAE;IACZ,6FAA6F;IAC7F,2FAA2F;IAC3F,wFAAwF;IACxF,wFAAwF;IACxF,8FAA8F;IAC9F,+EAA+E;IAC/E,EAAE,EAAE,EAAE;CACP,CAAC;AAEF;;;;;;;;;GASG;AACH,SAAS,yBAAyB,CAAC,KAAa,EAAE,MAAgB;IAChE,MAAM,GAAG,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACtC,2FAA2F;IAC3F,0DAA0D;IAC1D,IAAI,GAAG,EAAE,eAAe,KAAK,SAAS,IAAI,GAAG,CAAC,qBAAqB;QAAE,OAAO,KAAK,CAAC;IAClF,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC3C,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,KAAK,GAAG,CAAC,eAAe;QAAE,OAAO,KAAK,CAAC;IACzE,OAAQ,eAAqC,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5E,CAAC;AAED;;qDAEqD;AACrD,MAAM,UAAU,wBAAwB,CAAC,KAAa,EAAE,MAAgB;IACtE,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,uBAAuB,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnE,yFAAyF;IACzF,0FAA0F;IAC1F,gFAAgF;IAChF,IAAI,yBAAyB,CAAC,KAAK,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,OAAO,MAAM,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC,IAAI,CACjD,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAClE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import type { ModelOption } from './runner-model-catalog.ts';
3
+ export interface OpencodeModelDiscoveryOptions {
4
+ cwd: string;
5
+ bin?: string;
6
+ timeoutMs?: number;
7
+ spawn?: (bin: string, args: readonly string[], cwd: string) => ChildProcessWithoutNullStreams;
8
+ }
9
+ /** Grace between the probe's SIGTERM and the SIGKILL that follows it. */
10
+ export declare const KILL_GRACE_MS = 2000;
11
+ /** The host binary, resolved exactly like `OpencodeServerRunner` and the backend probe. */
12
+ export declare function resolveOpencodeExecutable(bin?: string): string;
13
+ /**
14
+ * Discover the models the host's own OpenCode installation offers, by asking it: `opencode
15
+ * models` lists every `provider/model` id the configured providers expose, which is the same
16
+ * list OpenCode's own picker routes to.
17
+ *
18
+ * Best-effort by contract — `RunnerModelCatalog` turns any throw here into a cached or
19
+ * `unavailable` answer, and `auto` stays selectable either way. No config is read or written,
20
+ * no session is started; the child is short-lived and bounded by a deadline, a stdout cap and
21
+ * a model cap.
22
+ */
23
+ export declare function discoverOpencodeModels(options: OpencodeModelDiscoveryOptions): Promise<ModelOption[]>;
24
+ /**
25
+ * Turn `opencode models` output into picker options, preserving OpenCode's own order.
26
+ *
27
+ * Empty output is a legitimate answer (no provider configured yet) and yields no models, so the
28
+ * picker shows `auto` alone. Output that contains lines but NO recognizable id is treated as a
29
+ * failure instead: the CLI said something we cannot read, and reporting "unavailable" is more
30
+ * honest than an empty catalog that looks like "you have no models".
31
+ */
32
+ export declare function parseOpencodeModels(stdout: string): ModelOption[];
@@ -0,0 +1,153 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { trackChildExit } from './agent-runner.js';
3
+ import { buildChildEnv } from './agent-env.js';
4
+ const DEFAULT_DISCOVERY_TIMEOUT_MS = 10_000;
5
+ /** Grace between the probe's SIGTERM and the SIGKILL that follows it. */
6
+ export const KILL_GRACE_MS = 2_000;
7
+ const MAX_MODELS = 500;
8
+ /** Defensive cap on what we buffer from a misbehaving child (characters of stdout). */
9
+ const MAX_OUTPUT_CHARS = 512 * 1_024;
10
+ /**
11
+ * One `provider/model` id per line — the shape `opencode models` prints (#794). Deliberately
12
+ * strict: anything else (a banner, a prompt, a stack trace, a bare model name that OpenCode
13
+ * could not route) is NOT a model id, and silently turning it into a picker entry would
14
+ * recreate the very defect this replaces.
15
+ */
16
+ const MODEL_LINE_RE = /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:/-]*$/i;
17
+ /** SGR/CSI sequences, in case a future CLI colorizes even a piped stdout. */
18
+ const ANSI_RE = /\u001B\[[0-9;]*[A-Za-z]/g;
19
+ /** The host binary, resolved exactly like `OpencodeServerRunner` and the backend probe. */
20
+ export function resolveOpencodeExecutable(bin) {
21
+ return bin ?? process.env.CEZ_OPENCODE_BIN ?? 'opencode';
22
+ }
23
+ /**
24
+ * Discover the models the host's own OpenCode installation offers, by asking it: `opencode
25
+ * models` lists every `provider/model` id the configured providers expose, which is the same
26
+ * list OpenCode's own picker routes to.
27
+ *
28
+ * Best-effort by contract — `RunnerModelCatalog` turns any throw here into a cached or
29
+ * `unavailable` answer, and `auto` stays selectable either way. No config is read or written,
30
+ * no session is started; the child is short-lived and bounded by a deadline, a stdout cap and
31
+ * a model cap.
32
+ */
33
+ export async function discoverOpencodeModels(options) {
34
+ const bin = resolveOpencodeExecutable(options.bin);
35
+ const args = ['models'];
36
+ const child = (options.spawn ?? spawnOpencode)(bin, args, options.cwd);
37
+ const kill = teardown(child);
38
+ const timeoutMs = options.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
39
+ let timeout;
40
+ try {
41
+ return await new Promise((resolve, reject) => {
42
+ let stdout = '';
43
+ let overflowed = false;
44
+ const fail = (message) => {
45
+ reject(new Error(message));
46
+ kill();
47
+ };
48
+ timeout = setTimeout(() => fail('OpenCode model discovery timed out'), timeoutMs);
49
+ timeout.unref?.();
50
+ child.stdout.setEncoding('utf8');
51
+ child.stdout.on('data', (chunk) => {
52
+ if (overflowed)
53
+ return;
54
+ stdout += chunk;
55
+ if (stdout.length > MAX_OUTPUT_CHARS) {
56
+ overflowed = true;
57
+ fail('OpenCode model discovery exceeded the output limit');
58
+ }
59
+ });
60
+ // Drained but ignored: OpenCode prints provider warnings here, and an unread pipe would
61
+ // eventually stall the child.
62
+ child.stderr.resume();
63
+ child.once('error', () => fail('OpenCode model discovery child failed'));
64
+ child.once('close', (code) => {
65
+ if (overflowed)
66
+ return;
67
+ if (code !== 0) {
68
+ fail(`OpenCode model discovery child exited (${code ?? 'unknown'})`);
69
+ return;
70
+ }
71
+ try {
72
+ resolve(parseOpencodeModels(stdout));
73
+ }
74
+ catch (error) {
75
+ reject(error instanceof Error ? error : new Error(String(error)));
76
+ }
77
+ });
78
+ });
79
+ }
80
+ finally {
81
+ if (timeout)
82
+ clearTimeout(timeout);
83
+ kill();
84
+ }
85
+ }
86
+ /**
87
+ * Turn `opencode models` output into picker options, preserving OpenCode's own order.
88
+ *
89
+ * Empty output is a legitimate answer (no provider configured yet) and yields no models, so the
90
+ * picker shows `auto` alone. Output that contains lines but NO recognizable id is treated as a
91
+ * failure instead: the CLI said something we cannot read, and reporting "unavailable" is more
92
+ * honest than an empty catalog that looks like "you have no models".
93
+ */
94
+ export function parseOpencodeModels(stdout) {
95
+ const lines = stdout
96
+ .replace(ANSI_RE, '')
97
+ .split('\n')
98
+ .map((line) => line.trim())
99
+ .filter((line) => line.length > 0);
100
+ const models = [];
101
+ const ids = new Set();
102
+ for (const line of lines) {
103
+ if (!MODEL_LINE_RE.test(line) || ids.has(line))
104
+ continue;
105
+ if (models.length >= MAX_MODELS)
106
+ throw new Error('OpenCode model discovery exceeded the size limit');
107
+ ids.add(line);
108
+ const provider = line.slice(0, line.indexOf('/'));
109
+ models.push({ id: line, label: line, description: `via ${provider}` });
110
+ }
111
+ if (models.length === 0 && lines.length > 0) {
112
+ throw new Error('OpenCode model discovery returned unrecognized output');
113
+ }
114
+ return models;
115
+ }
116
+ function spawnOpencode(bin, args, cwd) {
117
+ const child = nodeSpawn(bin, [...args], {
118
+ cwd,
119
+ env: buildChildEnv({ backend: 'opencode' }),
120
+ });
121
+ // Nothing is ever written to it, and an open stdin is what makes a CLI that expects a TTY
122
+ // sit and wait instead of printing its list.
123
+ child.stdin.end();
124
+ return child;
125
+ }
126
+ /**
127
+ * Returns the probe's teardown: SIGTERM now, SIGKILL once the grace window elapses.
128
+ *
129
+ * Both steps gate on the child's *real* termination, never on `child.killed` — Node flips that
130
+ * flag the moment a signal is delivered, so the old `exitCode === null && !child.killed` test
131
+ * went false the instant our own SIGTERM landed. A probe that installs a SIGTERM handler then
132
+ * survived every teardown, leaking one process per cache refresh (#858, the shape #841 fixed for
133
+ * the Claude probe in `a67b327d`).
134
+ *
135
+ * Called from both the failure path and the `finally`, so the escalation is armed at most once.
136
+ */
137
+ function teardown(child) {
138
+ const hasExited = trackChildExit(child);
139
+ let signalled = false;
140
+ return () => {
141
+ if (signalled || hasExited())
142
+ return;
143
+ signalled = true;
144
+ child.kill('SIGTERM');
145
+ const escalation = setTimeout(() => {
146
+ if (hasExited())
147
+ return;
148
+ child.kill('SIGKILL');
149
+ }, KILL_GRACE_MS);
150
+ escalation.unref?.();
151
+ };
152
+ }
153
+ //# sourceMappingURL=opencode-model-catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-model-catalog.js","sourceRoot":"","sources":["../../src/core/opencode-model-catalog.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,8 @@ export interface OpencodeRunnerOptions {
6
6
  /** Wall-clock timeout for a run (ms); per-spec `timeoutMs` still wins. */
7
7
  timeoutMs?: number;
8
8
  }
9
+ /** Grace between the teardown SIGTERM and the SIGKILL that follows it. */
10
+ export declare const KILL_GRACE_MS = 4000;
9
11
  /**
10
12
  * `AgentRunner` over `opencode serve` — a headless HTTP server (the same one
11
13
  * the opencode TUI talks to) with an SSE event stream. One server per session,
@@ -1,11 +1,13 @@
1
1
  import { spawn as nodeSpawn } from 'node:child_process';
2
- import { prependSystemPrompt } from './agent-runner.js';
2
+ import { prependSystemPrompt, trackChildExit } from './agent-runner.js';
3
3
  import { buildChildEnv } from './agent-env.js';
4
4
  import { AUTO_END_DELAY_MS, DEFAULT_RUN_TIMEOUT_MS } from './claude-cli-runner.js';
5
5
  import { parseModelIdentity } from './model-identity.js';
6
6
  import { V1TextCoalescer } from './v1-text-coalescer.js';
7
7
  import { createOpencodeUiState, mapOpencodeEvent, opencodeSessionStarted, opencodeTurnStarted, } from './opencode-ui-mapper.js';
8
8
  const SERVER_START_TIMEOUT_MS = 30_000;
9
+ /** Grace between the teardown SIGTERM and the SIGKILL that follows it. */
10
+ export const KILL_GRACE_MS = 4_000;
9
11
  /**
10
12
  * `AgentRunner` over `opencode serve` — a headless HTTP server (the same one
11
13
  * the opencode TUI talks to) with an SSE event stream. One server per session,
@@ -47,6 +49,9 @@ class OpencodeSession {
47
49
  opts;
48
50
  result;
49
51
  child;
52
+ /** "Has the server actually terminated?" — never `child.killed`, which only
53
+ * reports delivery and would disarm the escalation (#844/#858). */
54
+ hasExited;
50
55
  serverOpen = true;
51
56
  baseUrl;
52
57
  sessionId;
@@ -81,6 +86,8 @@ class OpencodeSession {
81
86
  autoEndTimer;
82
87
  spawnFailed = null;
83
88
  timedOut = false;
89
+ /** One teardown per session — see `terminate()`. */
90
+ signalled = false;
84
91
  constructor(bin, timeoutMs, spec, onEvent, opts) {
85
92
  this.bin = bin;
86
93
  this.spec = spec;
@@ -97,6 +104,7 @@ class OpencodeSession {
97
104
  catch (err) {
98
105
  throw wrapSpawnError(err, bin);
99
106
  }
107
+ this.hasExited = trackChildExit(this.child);
100
108
  this.child.on('error', (err) => {
101
109
  this.spawnFailed = wrapSpawnError(err, bin);
102
110
  });
@@ -142,8 +150,7 @@ class OpencodeSession {
142
150
  clearTimeout(this.autoEndTimer);
143
151
  this.sse.abort();
144
152
  this.serverOpen = false;
145
- if (!this.child.killed)
146
- this.child.kill('SIGTERM');
153
+ this.terminate();
147
154
  }
148
155
  await this.exited;
149
156
  if (this.spawnFailed)
@@ -196,12 +203,7 @@ class OpencodeSession {
196
203
  return;
197
204
  this.serverOpen = false;
198
205
  this.sse.abort();
199
- if (!this.child.killed)
200
- this.child.kill('SIGTERM');
201
- setTimeout(() => {
202
- if (this.child.exitCode == null && !this.child.killed)
203
- this.child.kill('SIGKILL');
204
- }, 4_000).unref?.();
206
+ this.terminate();
205
207
  }
206
208
  interrupt() {
207
209
  this.serverOpen = false;
@@ -209,8 +211,37 @@ class OpencodeSession {
209
211
  void this.http('POST', `/session/${this.sessionId}/abort`, undefined).catch(() => undefined);
210
212
  }
211
213
  this.sse.abort();
212
- if (!this.child.killed)
213
- this.child.kill('SIGTERM');
214
+ this.terminate();
215
+ }
216
+ /**
217
+ * The one place either signal is sent: SIGTERM now, SIGKILL once the grace
218
+ * window elapses.
219
+ *
220
+ * Both steps gate on `hasExited()`, never on `child.killed` — the latter
221
+ * flips the moment SIGTERM is *delivered*, so the old nested
222
+ * `exitCode == null && !killed` guard disarmed the escalation for exactly the
223
+ * server it was written for: one that installs its own SIGTERM handler stayed
224
+ * alive with `killed = true` and `exitCode === null`, outliving the whole
225
+ * window (#858, the same defect #844 fixed for the other two backends). Every
226
+ * caller here is followed by `await this.exited`, so a server that survived
227
+ * SIGTERM did not just leak — it hung the session's result forever.
228
+ *
229
+ * One teardown per session: all three call sites can run for the same session
230
+ * (`interrupt()` on the deadline, then the result promise's `finally`), and
231
+ * once SIGTERM is out with SIGKILL armed there is nothing a second pass adds.
232
+ * The old `!child.killed` test deduplicated this as a side effect of being
233
+ * wrong; `signalled` keeps that property on purpose.
234
+ */
235
+ terminate() {
236
+ if (this.signalled || this.hasExited())
237
+ return;
238
+ this.signalled = true;
239
+ this.child.kill('SIGTERM');
240
+ setTimeout(() => {
241
+ if (this.hasExited())
242
+ return;
243
+ this.child.kill('SIGKILL');
244
+ }, KILL_GRACE_MS).unref?.();
214
245
  }
215
246
  // ---- server lifecycle ---------------------------------------------------
216
247
  waitForServerUrl(fallbackPort) {