@astralyn/pi 0.85.1 → 0.85.3

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 (316) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/bundle/chunks/anthropic-messages-IHRRITCV.js +6 -0
  3. package/dist/bundle/chunks/anthropic.js +75 -0
  4. package/dist/bundle/chunks/azure-openai-responses-QXBIDYXO.js +2 -0
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +78 -0
  6. package/dist/bundle/chunks/chunk-37ATLPRE.js +2 -0
  7. package/dist/bundle/chunks/chunk-4L3WN2XY.js +2 -0
  8. package/dist/bundle/chunks/chunk-5B3S5DVL.js +4 -0
  9. package/dist/bundle/chunks/chunk-5FDOJRDS.js +2 -0
  10. package/dist/bundle/chunks/chunk-6CYCI3EJ.js +12 -0
  11. package/dist/bundle/chunks/chunk-7LO5Z4WE.js +2 -0
  12. package/dist/bundle/chunks/chunk-APKJ4JGA.js +2 -0
  13. package/dist/bundle/chunks/chunk-EDBLK2JQ.js +2 -0
  14. package/dist/bundle/chunks/chunk-FEZ5RM2B.js +2 -0
  15. package/dist/bundle/chunks/chunk-JEIV47WR.js +2 -0
  16. package/dist/bundle/chunks/chunk-JSJPY26N.js +2 -0
  17. package/dist/bundle/chunks/chunk-KI6LC764.js +42 -0
  18. package/dist/bundle/chunks/chunk-LB4P77SH.js +1688 -0
  19. package/dist/bundle/chunks/chunk-NUHFSC37.js +25 -0
  20. package/dist/bundle/chunks/chunk-O36ZMA3U.js +2 -0
  21. package/dist/bundle/chunks/chunk-OWESLY5T.js +11 -0
  22. package/dist/bundle/chunks/chunk-RRBI7OUG.js +50 -0
  23. package/dist/bundle/chunks/chunk-S2DJQQI2.js +2 -0
  24. package/dist/bundle/chunks/chunk-UP6RLTHQ.js +2 -0
  25. package/dist/bundle/chunks/chunk-UUSTLCVC.js +2 -0
  26. package/dist/bundle/chunks/chunk-WWBSZG5V.js +2 -0
  27. package/dist/bundle/chunks/chunk-YZJWMPUI.js +2 -0
  28. package/dist/bundle/chunks/github-copilot.js +2 -0
  29. package/dist/bundle/chunks/google-generative-ai-NBOR3IQR.js +2 -0
  30. package/dist/bundle/chunks/google-vertex-VTHX3SUB.js +2 -0
  31. package/dist/bundle/chunks/https-proxy-agent-2VXB7436.js +13 -0
  32. package/dist/bundle/chunks/image-resize-worker.js +2 -0
  33. package/dist/bundle/chunks/kimi-coding.js +2 -0
  34. package/dist/bundle/chunks/mistral-conversations-NO4ANAA5.js +5 -0
  35. package/dist/bundle/chunks/multipart-parser-SQ6GDJL7.js +3 -0
  36. package/dist/bundle/chunks/node-7L5OTM6S.js +2 -0
  37. package/dist/bundle/chunks/node-G4XOIDPH.js +16 -0
  38. package/dist/bundle/chunks/openai-codex-responses-5S3T3HMA.js +10 -0
  39. package/dist/bundle/chunks/openai-codex.js +75 -0
  40. package/dist/bundle/chunks/openai-completions-CXRRTRWE.js +7 -0
  41. package/dist/bundle/chunks/openai-responses-CV3MH6VP.js +2 -0
  42. package/dist/bundle/chunks/openrouter-images-6XDE2CEW.js +2 -0
  43. package/dist/bundle/chunks/openrouter.js +75 -0
  44. package/dist/bundle/chunks/pi-messages-PE6B42TG.js +8 -0
  45. package/dist/bundle/chunks/radius.js +75 -0
  46. package/dist/bundle/chunks/src-UK77SYZE.js +4 -0
  47. package/dist/bundle/chunks/xai.js +2 -0
  48. package/dist/bundle/cli.js +3 -0
  49. package/dist/bundle/index.js +2 -0
  50. package/dist/bundle/rpc-entry.js +3 -0
  51. package/dist/cli.js +5 -2469
  52. package/dist/core/agent-session.d.ts.map +1 -1
  53. package/dist/core/agent-session.js +2 -6
  54. package/dist/core/agent-session.js.map +1 -1
  55. package/dist/core/background/history.d.ts.map +1 -1
  56. package/dist/core/background/history.js +6 -0
  57. package/dist/core/background/history.js.map +1 -1
  58. package/dist/core/background/presentation.d.ts.map +1 -1
  59. package/dist/core/background/presentation.js +41 -4
  60. package/dist/core/background/presentation.js.map +1 -1
  61. package/dist/core/background/service.d.ts +2 -2
  62. package/dist/core/background/service.d.ts.map +1 -1
  63. package/dist/core/background/service.js +38 -15
  64. package/dist/core/background/service.js.map +1 -1
  65. package/dist/core/background/types.d.ts +9 -0
  66. package/dist/core/background/types.d.ts.map +1 -1
  67. package/dist/core/background/types.js +4 -0
  68. package/dist/core/background/types.js.map +1 -1
  69. package/dist/core/compaction/compaction.d.ts +5 -12
  70. package/dist/core/compaction/compaction.d.ts.map +1 -1
  71. package/dist/core/compaction/compaction.js +11 -19
  72. package/dist/core/compaction/compaction.js.map +1 -1
  73. package/dist/core/compaction/settings.d.ts +27 -0
  74. package/dist/core/compaction/settings.d.ts.map +1 -0
  75. package/dist/core/compaction/settings.js +40 -0
  76. package/dist/core/compaction/settings.js.map +1 -0
  77. package/dist/core/extensions/index.d.ts +1 -1
  78. package/dist/core/extensions/index.d.ts.map +1 -1
  79. package/dist/core/extensions/index.js.map +1 -1
  80. package/dist/core/extensions/runner.d.ts +0 -1
  81. package/dist/core/extensions/runner.d.ts.map +1 -1
  82. package/dist/core/extensions/runner.js +0 -6
  83. package/dist/core/extensions/runner.js.map +1 -1
  84. package/dist/core/extensions/types.d.ts +0 -11
  85. package/dist/core/extensions/types.d.ts.map +1 -1
  86. package/dist/core/extensions/types.js.map +1 -1
  87. package/dist/core/keybindings.d.ts +15 -0
  88. package/dist/core/keybindings.d.ts.map +1 -1
  89. package/dist/core/keybindings.js +12 -0
  90. package/dist/core/keybindings.js.map +1 -1
  91. package/dist/core/model-config.d.ts +32 -10
  92. package/dist/core/model-config.d.ts.map +1 -1
  93. package/dist/core/model-config.js +38 -1
  94. package/dist/core/model-config.js.map +1 -1
  95. package/dist/core/model-runtime.d.ts +2 -0
  96. package/dist/core/model-runtime.d.ts.map +1 -1
  97. package/dist/core/model-runtime.js +4 -0
  98. package/dist/core/model-runtime.js.map +1 -1
  99. package/dist/core/settings-manager.d.ts +4 -7
  100. package/dist/core/settings-manager.d.ts.map +1 -1
  101. package/dist/core/settings-manager.js +7 -6
  102. package/dist/core/settings-manager.js.map +1 -1
  103. package/dist/core/tools/bash.d.ts +3 -9
  104. package/dist/core/tools/bash.d.ts.map +1 -1
  105. package/dist/core/tools/bash.js +3 -8
  106. package/dist/core/tools/bash.js.map +1 -1
  107. package/dist/core/tools/shell-execution.d.ts.map +1 -1
  108. package/dist/core/tools/shell-execution.js +3 -1
  109. package/dist/core/tools/shell-execution.js.map +1 -1
  110. package/dist/extensions/background/manager.d.ts +8 -5
  111. package/dist/extensions/background/manager.d.ts.map +1 -1
  112. package/dist/extensions/background/manager.js +115 -39
  113. package/dist/extensions/background/manager.js.map +1 -1
  114. package/dist/extensions/background/task-view.d.ts +5 -0
  115. package/dist/extensions/background/task-view.d.ts.map +1 -1
  116. package/dist/extensions/background/task-view.js +4 -0
  117. package/dist/extensions/background/task-view.js.map +1 -1
  118. package/dist/extensions/btw/controller.d.ts +1 -1
  119. package/dist/extensions/btw/controller.d.ts.map +1 -1
  120. package/dist/extensions/btw/controller.js +4 -4
  121. package/dist/extensions/btw/controller.js.map +1 -1
  122. package/dist/extensions/btw/index.js +3 -3
  123. package/dist/extensions/btw/index.js.map +1 -1
  124. package/dist/extensions/index.js +2 -2
  125. package/dist/extensions/index.js.map +1 -1
  126. package/dist/extensions/provider/catalog.d.ts +57 -0
  127. package/dist/extensions/provider/catalog.d.ts.map +1 -0
  128. package/dist/extensions/provider/catalog.js +181 -0
  129. package/dist/extensions/provider/catalog.js.map +1 -0
  130. package/dist/extensions/provider/compat-fields.d.ts +25 -0
  131. package/dist/extensions/provider/compat-fields.d.ts.map +1 -0
  132. package/dist/extensions/provider/compat-fields.js +148 -0
  133. package/dist/extensions/provider/compat-fields.js.map +1 -0
  134. package/dist/extensions/provider/configuration.d.ts +8 -0
  135. package/dist/extensions/provider/configuration.d.ts.map +1 -0
  136. package/dist/extensions/provider/configuration.js +21 -0
  137. package/dist/extensions/provider/configuration.js.map +1 -0
  138. package/dist/extensions/provider/connection.d.ts +14 -0
  139. package/dist/extensions/provider/connection.d.ts.map +1 -0
  140. package/dist/extensions/provider/connection.js +86 -0
  141. package/dist/extensions/provider/connection.js.map +1 -0
  142. package/dist/extensions/provider/constants.d.ts +38 -0
  143. package/dist/extensions/provider/constants.d.ts.map +1 -0
  144. package/dist/extensions/provider/constants.js +75 -0
  145. package/dist/extensions/provider/constants.js.map +1 -0
  146. package/dist/extensions/provider/index.d.ts +4 -0
  147. package/dist/extensions/provider/index.d.ts.map +1 -0
  148. package/dist/extensions/provider/index.js +65 -0
  149. package/dist/extensions/provider/index.js.map +1 -0
  150. package/dist/extensions/provider/probe.d.ts +34 -0
  151. package/dist/extensions/provider/probe.d.ts.map +1 -0
  152. package/dist/extensions/provider/probe.js +178 -0
  153. package/dist/extensions/provider/probe.js.map +1 -0
  154. package/dist/extensions/provider/refresh.d.ts +30 -0
  155. package/dist/extensions/provider/refresh.d.ts.map +1 -0
  156. package/dist/extensions/provider/refresh.js +66 -0
  157. package/dist/extensions/provider/refresh.js.map +1 -0
  158. package/dist/extensions/provider/store.d.ts +131 -0
  159. package/dist/extensions/provider/store.d.ts.map +1 -0
  160. package/dist/extensions/provider/store.js +503 -0
  161. package/dist/extensions/provider/store.js.map +1 -0
  162. package/dist/extensions/provider/ui/app.d.ts +25 -0
  163. package/dist/extensions/provider/ui/app.d.ts.map +1 -0
  164. package/dist/extensions/provider/ui/app.js +153 -0
  165. package/dist/extensions/provider/ui/app.js.map +1 -0
  166. package/dist/extensions/provider/ui/builtin-data.d.ts +53 -0
  167. package/dist/extensions/provider/ui/builtin-data.d.ts.map +1 -0
  168. package/dist/extensions/provider/ui/builtin-data.js +315 -0
  169. package/dist/extensions/provider/ui/builtin-data.js.map +1 -0
  170. package/dist/extensions/provider/ui/compat.d.ts +57 -0
  171. package/dist/extensions/provider/ui/compat.d.ts.map +1 -0
  172. package/dist/extensions/provider/ui/compat.js +427 -0
  173. package/dist/extensions/provider/ui/compat.js.map +1 -0
  174. package/dist/extensions/provider/ui/dialogs.d.ts +30 -0
  175. package/dist/extensions/provider/ui/dialogs.d.ts.map +1 -0
  176. package/dist/extensions/provider/ui/dialogs.js +89 -0
  177. package/dist/extensions/provider/ui/dialogs.js.map +1 -0
  178. package/dist/extensions/provider/ui/dictionary.d.ts +29 -0
  179. package/dist/extensions/provider/ui/dictionary.d.ts.map +1 -0
  180. package/dist/extensions/provider/ui/dictionary.js +385 -0
  181. package/dist/extensions/provider/ui/dictionary.js.map +1 -0
  182. package/dist/extensions/provider/ui/editor.d.ts +91 -0
  183. package/dist/extensions/provider/ui/editor.d.ts.map +1 -0
  184. package/dist/extensions/provider/ui/editor.js +667 -0
  185. package/dist/extensions/provider/ui/editor.js.map +1 -0
  186. package/dist/extensions/provider/ui/fetch-models.d.ts +39 -0
  187. package/dist/extensions/provider/ui/fetch-models.d.ts.map +1 -0
  188. package/dist/extensions/provider/ui/fetch-models.js +320 -0
  189. package/dist/extensions/provider/ui/fetch-models.js.map +1 -0
  190. package/dist/extensions/provider/ui/model-fields.d.ts +43 -0
  191. package/dist/extensions/provider/ui/model-fields.d.ts.map +1 -0
  192. package/dist/extensions/provider/ui/model-fields.js +441 -0
  193. package/dist/extensions/provider/ui/model-fields.js.map +1 -0
  194. package/dist/extensions/provider/ui/model-options.d.ts +93 -0
  195. package/dist/extensions/provider/ui/model-options.d.ts.map +1 -0
  196. package/dist/extensions/provider/ui/model-options.js +529 -0
  197. package/dist/extensions/provider/ui/model-options.js.map +1 -0
  198. package/dist/extensions/provider/ui/pane.d.ts +81 -0
  199. package/dist/extensions/provider/ui/pane.d.ts.map +1 -0
  200. package/dist/extensions/provider/ui/pane.js +3 -0
  201. package/dist/extensions/provider/ui/pane.js.map +1 -0
  202. package/dist/extensions/provider/ui/provider-fields.d.ts +60 -0
  203. package/dist/extensions/provider/ui/provider-fields.d.ts.map +1 -0
  204. package/dist/extensions/provider/ui/provider-fields.js +326 -0
  205. package/dist/extensions/provider/ui/provider-fields.js.map +1 -0
  206. package/dist/extensions/provider/ui/provider-list.d.ts +34 -0
  207. package/dist/extensions/provider/ui/provider-list.d.ts.map +1 -0
  208. package/dist/extensions/provider/ui/provider-list.js +162 -0
  209. package/dist/extensions/provider/ui/provider-list.js.map +1 -0
  210. package/dist/extensions/provider/ui/session.d.ts +20 -0
  211. package/dist/extensions/provider/ui/session.d.ts.map +1 -0
  212. package/dist/extensions/provider/ui/session.js +52 -0
  213. package/dist/extensions/provider/ui/session.js.map +1 -0
  214. package/dist/extensions/provider/ui/thinking-map.d.ts +20 -0
  215. package/dist/extensions/provider/ui/thinking-map.d.ts.map +1 -0
  216. package/dist/extensions/provider/ui/thinking-map.js +166 -0
  217. package/dist/extensions/provider/ui/thinking-map.js.map +1 -0
  218. package/dist/extensions/provider/ui/value-row.d.ts +88 -0
  219. package/dist/extensions/provider/ui/value-row.d.ts.map +1 -0
  220. package/dist/extensions/provider/ui/value-row.js +158 -0
  221. package/dist/extensions/provider/ui/value-row.js.map +1 -0
  222. package/dist/extensions/statusline/index.js +2 -2
  223. package/dist/extensions/statusline/index.js.map +1 -1
  224. package/dist/extensions/subagent/index.d.ts.map +1 -1
  225. package/dist/extensions/subagent/index.js +4 -2
  226. package/dist/extensions/subagent/index.js.map +1 -1
  227. package/dist/extensions/subagent/resolve.d.ts +5 -0
  228. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  229. package/dist/extensions/subagent/resolve.js +15 -7
  230. package/dist/extensions/subagent/resolve.js.map +1 -1
  231. package/dist/extensions/subagent/schema.d.ts +2 -0
  232. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  233. package/dist/extensions/subagent/schema.js +1 -0
  234. package/dist/extensions/subagent/schema.js.map +1 -1
  235. package/dist/index.d.ts +1 -1
  236. package/dist/index.d.ts.map +1 -1
  237. package/dist/index.js.map +1 -1
  238. package/dist/modes/interactive/components/tool-execution.d.ts +10 -0
  239. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  240. package/dist/modes/interactive/components/tool-execution.js +20 -2
  241. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  242. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  243. package/dist/modes/interactive/interactive-mode.js +6 -1
  244. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  245. package/dist/rpc-entry.js +10 -2469
  246. package/docs/bundled/README.md +1 -1
  247. package/docs/bundled/extensions/background.md +5 -5
  248. package/docs/bundled/extensions/provider.md +117 -0
  249. package/docs/bundled/extensions/statusline.md +1 -1
  250. package/docs/bundled/extensions/subagent.md +7 -3
  251. package/docs/compaction.md +16 -6
  252. package/docs/development.md +1 -1
  253. package/docs/docs.json +2 -2
  254. package/docs/extensions.md +0 -13
  255. package/docs/keybindings.md +3 -0
  256. package/docs/settings.md +11 -7
  257. package/npm-shrinkwrap.json +4 -4
  258. package/package.json +6 -5
  259. package/dist/extensions/router/constants.d.ts +0 -23
  260. package/dist/extensions/router/constants.d.ts.map +0 -1
  261. package/dist/extensions/router/constants.js +0 -30
  262. package/dist/extensions/router/constants.js.map +0 -1
  263. package/dist/extensions/router/dialog-host.d.ts +0 -60
  264. package/dist/extensions/router/dialog-host.d.ts.map +0 -1
  265. package/dist/extensions/router/dialog-host.js +0 -196
  266. package/dist/extensions/router/dialog-host.js.map +0 -1
  267. package/dist/extensions/router/dialog.d.ts +0 -45
  268. package/dist/extensions/router/dialog.d.ts.map +0 -1
  269. package/dist/extensions/router/dialog.js +0 -378
  270. package/dist/extensions/router/dialog.js.map +0 -1
  271. package/dist/extensions/router/identity.d.ts +0 -15
  272. package/dist/extensions/router/identity.d.ts.map +0 -1
  273. package/dist/extensions/router/identity.js +0 -173
  274. package/dist/extensions/router/identity.js.map +0 -1
  275. package/dist/extensions/router/index.d.ts +0 -15
  276. package/dist/extensions/router/index.d.ts.map +0 -1
  277. package/dist/extensions/router/index.js +0 -70
  278. package/dist/extensions/router/index.js.map +0 -1
  279. package/dist/extensions/router/presets.d.ts +0 -51
  280. package/dist/extensions/router/presets.d.ts.map +0 -1
  281. package/dist/extensions/router/presets.js +0 -108
  282. package/dist/extensions/router/presets.js.map +0 -1
  283. package/dist/extensions/router/probe.d.ts +0 -26
  284. package/dist/extensions/router/probe.d.ts.map +0 -1
  285. package/dist/extensions/router/probe.js +0 -216
  286. package/dist/extensions/router/probe.js.map +0 -1
  287. package/dist/extensions/router/register.d.ts +0 -12
  288. package/dist/extensions/router/register.d.ts.map +0 -1
  289. package/dist/extensions/router/register.js +0 -90
  290. package/dist/extensions/router/register.js.map +0 -1
  291. package/dist/extensions/router/state.d.ts +0 -20
  292. package/dist/extensions/router/state.d.ts.map +0 -1
  293. package/dist/extensions/router/state.js +0 -169
  294. package/dist/extensions/router/state.js.map +0 -1
  295. package/dist/extensions/router/store.d.ts +0 -12
  296. package/dist/extensions/router/store.d.ts.map +0 -1
  297. package/dist/extensions/router/store.js +0 -266
  298. package/dist/extensions/router/store.js.map +0 -1
  299. package/dist/extensions/router/stream.d.ts +0 -12
  300. package/dist/extensions/router/stream.d.ts.map +0 -1
  301. package/dist/extensions/router/stream.js +0 -233
  302. package/dist/extensions/router/stream.js.map +0 -1
  303. package/dist/extensions/router/transport.d.ts +0 -9
  304. package/dist/extensions/router/transport.d.ts.map +0 -1
  305. package/dist/extensions/router/transport.js +0 -81
  306. package/dist/extensions/router/transport.js.map +0 -1
  307. package/dist/extensions/router/types.d.ts +0 -39
  308. package/dist/extensions/router/types.d.ts.map +0 -1
  309. package/dist/extensions/router/types.js +0 -2
  310. package/dist/extensions/router/types.js.map +0 -1
  311. package/dist/extensions/router/ui.d.ts +0 -26
  312. package/dist/extensions/router/ui.d.ts.map +0 -1
  313. package/dist/extensions/router/ui.js +0 -897
  314. package/dist/extensions/router/ui.js.map +0 -1
  315. package/dist/image-resize-worker.js +0 -2
  316. package/docs/bundled/extensions/router.md +0 -197
@@ -1,897 +0,0 @@
1
- /**
2
- * /router interactive flows: list, add, edit, fetch, thinking map.
3
- *
4
- * Relay edits are live: confirmed inputs and every toggle persist immediately.
5
- * Searchable pickers follow Pi's native /model interaction pattern.
6
- */
7
- import { BorderedLoader } from "../../modes/interactive/components/bordered-loader.js";
8
- import { getModelSelectorSearchText } from "../../modes/interactive/model-search.js";
9
- import { formatError, isValidRelayId, NO_UI_WARNING, ROUTER_THINKING_LEVELS, truncate } from "./constants.js";
10
- import { createModelChecklist, createSearchableSelector, createThinkingMapEditor, } from "./dialog.js";
11
- import { createNativeRouterDialogs, RouterTuiSession, RouterTuiSessionClosedError, } from "./dialog-host.js";
12
- import { createDefaultModelConfig, displayModelLabel, resolveModelConfig, resolveRouterThinkingMap, summarizeThinkingMap, } from "./presets.js";
13
- import { probeRelayModels } from "./probe.js";
14
- import { applyRouterFile, initializeRouterState, registerOneRelay, unregisterOneRelay } from "./register.js";
15
- import { loadRouterFile, removeRelay, upsertRelay } from "./store.js";
16
- /**
17
- * Serialize live relay mutations without forcing a model-registry refresh for
18
- * every checkbox press. Provider registration follows each snapshot; the
19
- * registry is refreshed once when the editor closes.
20
- */
21
- class RelayAutoSaver {
22
- constructor(ctx, pi, relay) {
23
- this.chain = Promise.resolve();
24
- this.needsRefresh = false;
25
- this.ctx = ctx;
26
- this.pi = pi;
27
- this.relay = relay;
28
- }
29
- save() {
30
- const snapshot = structuredClone(this.relay);
31
- this.needsRefresh = true;
32
- this.chain = this.chain
33
- .catch(() => { })
34
- .then(async () => {
35
- await upsertRelay(snapshot);
36
- registerOneRelay(this.pi, snapshot);
37
- this.error = undefined;
38
- })
39
- .catch((error) => {
40
- this.error = error;
41
- });
42
- }
43
- async flush() {
44
- await this.chain;
45
- if (this.error !== undefined) {
46
- const error = this.error;
47
- this.error = undefined;
48
- throw error;
49
- }
50
- if (this.needsRefresh) {
51
- this.needsRefresh = false;
52
- await this.ctx.modelRegistry.refresh();
53
- }
54
- }
55
- }
56
- /** Abort the catalog request if its internal page or the whole Router session is disposed. */
57
- class RouterProbeLoader extends BorderedLoader {
58
- constructor(tui, theme, message) {
59
- super(tui, theme, message, { cancellable: true });
60
- this.disposeController = new AbortController();
61
- this.probeSignal = AbortSignal.any([this.signal, this.disposeController.signal]);
62
- }
63
- dispose() {
64
- this.disposeController.abort();
65
- super.dispose();
66
- }
67
- }
68
- export function isCurrentRouterModel(current, relayId, modelId) {
69
- return current?.provider === relayId && current.id === modelId;
70
- }
71
- export async function runRouterCommand(args, ctx, pi) {
72
- if (!ctx.hasUI) {
73
- ctx.ui.notify(NO_UI_WARNING, "warning");
74
- return;
75
- }
76
- // Initialize before any provider closure can capture request state, including first /router reload.
77
- await initializeRouterState(pi);
78
- const trimmed = args.trim().toLowerCase();
79
- if (trimmed === "reload") {
80
- const file = await loadRouterFile();
81
- applyRouterFile(pi, file);
82
- await ctx.modelRegistry.refresh();
83
- ctx.ui.notify(`Reloaded ${file.relays.length} relay(s).`, "info");
84
- return;
85
- }
86
- if (ctx.mode === "tui") {
87
- await runRouterTuiCommand(args, ctx, pi);
88
- return;
89
- }
90
- await runRouterDialogCommand(args, ctx, pi, createNativeRouterDialogs(ctx));
91
- }
92
- async function runRouterTuiCommand(args, ctx, pi) {
93
- let failure;
94
- let closedDuringTransition = false;
95
- await ctx.ui.custom((tui, theme, keybindings, done) => {
96
- const session = new RouterTuiSession(tui, theme, keybindings, () => {
97
- closedDuringTransition = true;
98
- done();
99
- });
100
- void runRouterDialogCommand(args, ctx, pi, session).then(() => {
101
- if (!closedDuringTransition)
102
- done();
103
- }, (error) => {
104
- if (closedDuringTransition) {
105
- if (!(error instanceof RouterTuiSessionClosedError)) {
106
- try {
107
- ctx.ui.notify(formatError(error), "error");
108
- }
109
- catch {
110
- // The command context may have gone stale after the modal closed.
111
- }
112
- }
113
- return;
114
- }
115
- failure = { error };
116
- done();
117
- });
118
- return session;
119
- });
120
- if (failure)
121
- throw failure.error;
122
- }
123
- async function runRouterDialogCommand(args, ctx, pi, dialogs) {
124
- const trimmed = args.trim().toLowerCase();
125
- if (!trimmed || trimmed === "list") {
126
- await openMainMenu(ctx, pi, dialogs);
127
- return;
128
- }
129
- if (trimmed === "add") {
130
- await addRelayFlow(ctx, pi, dialogs);
131
- return;
132
- }
133
- // Treat as relay id open.
134
- const file = await loadRouterFile();
135
- const exact = file.relays.find((relay) => relay.id.toLowerCase() === trimmed);
136
- if (exact) {
137
- await editRelayFlow(ctx, pi, dialogs, exact);
138
- return;
139
- }
140
- await openMainMenu(ctx, pi, dialogs, args.trim());
141
- }
142
- async function openMainMenu(ctx, pi, dialogs, initialQuery) {
143
- let cursor;
144
- let query = initialQuery;
145
- while (true) {
146
- const file = await loadRouterFile();
147
- // Relays first (primary content), actions at the bottom.
148
- const items = [
149
- ...file.relays.map((relay) => ({
150
- value: `relay:${relay.id}`,
151
- label: relay.id,
152
- description: `${relay.models.length} model${relay.models.length === 1 ? "" : "s"} · ${truncate(relay.baseUrl, 40)}`,
153
- searchText: `${relay.id} ${relay.baseUrl}`,
154
- })),
155
- {
156
- value: "action:add",
157
- label: "+ Add relay",
158
- description: "Name · base URL · API key · fetch models",
159
- searchText: "add create new relay gateway",
160
- },
161
- {
162
- value: "action:reload",
163
- label: "Reload from disk",
164
- description: "Re-register providers from router.json",
165
- searchText: "reload refresh",
166
- },
167
- ];
168
- const selected = dialogs.kind === "tui"
169
- ? await dialogs.show(createSearchableSelector({
170
- title: "API relays",
171
- subtitle: file.relays.length === 0
172
- ? "No relays yet — add a base URL + API key."
173
- : `${file.relays.length} relay${file.relays.length === 1 ? "" : "s"} · Enter opens · Esc closes`,
174
- items,
175
- initialValue: cursor,
176
- initialQuery: query,
177
- maxVisible: 10,
178
- }))
179
- : await selectRouterItem(dialogs, "API relays", items.map((item) => ({ value: item.value, label: item.label, description: item.description })));
180
- query = undefined;
181
- if (selected === undefined)
182
- return;
183
- cursor = selected;
184
- if (selected === "action:add") {
185
- const id = await addRelayFlow(ctx, pi, dialogs);
186
- if (id)
187
- cursor = `relay:${id}`;
188
- continue;
189
- }
190
- if (selected === "action:reload") {
191
- const latest = await loadRouterFile();
192
- applyRouterFile(pi, latest);
193
- await ctx.modelRegistry.refresh();
194
- ctx.ui.notify(`Reloaded ${latest.relays.length} relay(s).`, "info");
195
- continue;
196
- }
197
- if (selected.startsWith("relay:")) {
198
- const id = selected.slice("relay:".length);
199
- const latest = await loadRouterFile();
200
- const relay = latest.relays.find((entry) => entry.id === id);
201
- if (!relay) {
202
- ctx.ui.notify(`Relay "${id}" not found.`, "warning");
203
- continue;
204
- }
205
- await editRelayFlow(ctx, pi, dialogs, relay);
206
- }
207
- }
208
- }
209
- async function addRelayFlow(ctx, pi, dialogs) {
210
- const file = await loadRouterFile();
211
- const existing = new Set(file.relays.map((relay) => relay.id));
212
- const id = await promptText(ctx, dialogs, "New relay · name (provider id)", "my-relay", (value) => {
213
- const trimmed = value.trim();
214
- if (!trimmed)
215
- return "Name is required.";
216
- if (!isValidRelayId(trimmed))
217
- return "Name cannot be empty or contain '/'.";
218
- if (existing.has(trimmed))
219
- return `Relay "${trimmed}" already exists.`;
220
- return undefined;
221
- });
222
- if (id === undefined)
223
- return undefined;
224
- const baseUrl = await promptText(ctx, dialogs, `New relay · ${id.trim()} · base URL`, "https://relay.example/v1", (value) => {
225
- const trimmed = value.trim();
226
- if (!trimmed)
227
- return "Base URL is required.";
228
- try {
229
- const url = new URL(trimmed);
230
- if (url.protocol !== "http:" && url.protocol !== "https:")
231
- return "Use http or https.";
232
- if (url.username || url.password || url.hash)
233
- return "Do not embed credentials or a fragment in the URL.";
234
- }
235
- catch {
236
- return "Invalid URL.";
237
- }
238
- return undefined;
239
- });
240
- if (baseUrl === undefined)
241
- return undefined;
242
- const apiKey = await promptText(ctx, dialogs, `New relay · ${id.trim()} · API key`, "sk-… or $RELAY_KEY", (value) => {
243
- if (!value.trim())
244
- return "API key is required.";
245
- return undefined;
246
- });
247
- if (apiKey === undefined)
248
- return undefined;
249
- const relay = {
250
- id: id.trim(),
251
- baseUrl: baseUrl.trim().replace(/\/+$/, ""),
252
- apiKey: apiKey.trim(),
253
- models: [],
254
- };
255
- // Connection fields are committed before the network step so cancelling or
256
- // failing catalog discovery never loses the relay the user just entered.
257
- await persistRelay(ctx, pi, relay);
258
- await fetchAndSelectModels(ctx, pi, dialogs, relay);
259
- if (relay.models.length === 0) {
260
- ctx.ui.notify(`Relay "${relay.id}" has no models yet.`, "warning");
261
- }
262
- return relay.id;
263
- }
264
- /**
265
- * Relay editor — flat menu. Every mutation writes router.json immediately.
266
- * Hierarchy: list → relay → (models list | connection field | fetch | remove).
267
- */
268
- async function editRelayFlow(ctx, pi, dialogs, initial) {
269
- const relay = structuredClone(initial);
270
- while (true) {
271
- const modelSummary = relay.models.length === 0
272
- ? "none yet — fetch catalog"
273
- : relay.models.length <= 3
274
- ? relay.models.map((m) => displayModelLabel(m)).join(", ")
275
- : `${relay.models.length} models · ${relay.models
276
- .slice(0, 2)
277
- .map((m) => displayModelLabel(m))
278
- .join(", ")}…`;
279
- const choice = await selectRouterItem(dialogs, `Relay · ${relay.id}`, [
280
- {
281
- value: "models",
282
- label: "Models",
283
- description: modelSummary,
284
- },
285
- { value: "baseUrl", label: "Base URL", description: relay.baseUrl },
286
- { value: "apiKey", label: "API key", description: maskKey(relay.apiKey) },
287
- { value: "catalog", label: "Catalog format", description: relay.catalog ?? "openai" },
288
- { value: "providerName", label: "Provider display name", description: relay.name ?? relay.id },
289
- { value: "remove", label: "Remove relay", description: "Delete from router.json" },
290
- { value: "back", label: "Back", description: "Return to relays" },
291
- ]);
292
- if (choice === undefined || choice === "back")
293
- return;
294
- if (choice === "catalog") {
295
- const next = await selectRouterItem(dialogs, "Catalog format", [
296
- { value: "openai", label: "OpenAI", description: "GET /models · data[].id" },
297
- { value: "codex", label: "Codex", description: "GET /models?client_version=… · models[].slug" },
298
- ]);
299
- if (next) {
300
- relay.catalog = next;
301
- await persistRelay(ctx, pi, relay);
302
- }
303
- continue;
304
- }
305
- if (choice === "providerName") {
306
- const next = await dialogs.input("Provider display name · empty uses id", relay.name ?? "");
307
- if (next !== undefined) {
308
- if (next.trim())
309
- relay.name = next.trim();
310
- else
311
- delete relay.name;
312
- await persistRelay(ctx, pi, relay);
313
- }
314
- continue;
315
- }
316
- if (choice === "baseUrl") {
317
- const next = await promptText(ctx, dialogs, `Relay · ${relay.id} · base URL`, relay.baseUrl, (value) => {
318
- try {
319
- const url = new URL(value.trim());
320
- if (url.protocol !== "http:" && url.protocol !== "https:")
321
- return "Use http or https.";
322
- if (url.username || url.password || url.hash)
323
- return "Do not embed credentials or a fragment in the URL.";
324
- }
325
- catch {
326
- return "Invalid URL.";
327
- }
328
- return undefined;
329
- });
330
- if (next === undefined)
331
- continue;
332
- const normalized = next.trim().replace(/\/+$/, "");
333
- if (normalized === relay.baseUrl)
334
- continue;
335
- relay.baseUrl = normalized;
336
- await persistRelay(ctx, pi, relay);
337
- continue;
338
- }
339
- if (choice === "apiKey") {
340
- const next = await promptText(ctx, dialogs, `Relay · ${relay.id} · API key`, "Enter a new key · blank keeps the current value");
341
- if (next === undefined)
342
- continue;
343
- const trimmed = next.trim();
344
- if (!trimmed || trimmed === relay.apiKey)
345
- continue;
346
- relay.apiKey = trimmed;
347
- await persistRelay(ctx, pi, relay);
348
- continue;
349
- }
350
- if (choice === "models") {
351
- // Always open the models screen (fetch lives only here). Empty list still
352
- // shows Fetch so the path is Relay → Models → Fetch, not a top-level shortcut.
353
- await manageModelsFlow(ctx, pi, dialogs, relay);
354
- continue;
355
- }
356
- if (choice === "remove") {
357
- if (ctx.model?.provider === relay.id) {
358
- ctx.ui.notify(`Switch away from relay "${relay.id}" before removing it.`, "warning");
359
- continue;
360
- }
361
- const ok = await dialogs.confirm(`Remove relay "${relay.id}"?`, "Models will disappear from /model.");
362
- if (!ok)
363
- continue;
364
- await removeRelay(relay.id);
365
- unregisterOneRelay(pi, relay.id);
366
- await ctx.modelRegistry.refresh();
367
- ctx.ui.notify(`Removed relay "${relay.id}".`, "info");
368
- return;
369
- }
370
- }
371
- }
372
- /**
373
- * Models screen: searchable configured models plus fetch/manual-add actions.
374
- * Selecting a model opens its editor (name / thinking / remove).
375
- */
376
- async function manageModelsFlow(ctx, pi, dialogs, relay) {
377
- let cursor;
378
- while (true) {
379
- const items = [
380
- ...relay.models.map((model) => {
381
- const resolved = resolveModelConfig(model);
382
- const label = displayModelLabel(resolved);
383
- const thinking = summarizeThinkingMap(resolved.thinkingLevelMap);
384
- const current = ctx.model?.provider === relay.id && ctx.model.id === model.id ? " ✓" : "";
385
- return {
386
- value: `model:${model.id}`,
387
- label: `${model.id}${current}`,
388
- description: label !== model.id ? `${label} · ${thinking}` : thinking,
389
- searchText: `${getModelSelectorSearchText({ id: model.id, provider: relay.id, name: resolved.name })} ${thinking}`,
390
- };
391
- }),
392
- {
393
- value: "action:fetch",
394
- label: "Fetch catalog",
395
- description: relay.models.length === 0
396
- ? "GET /models · select models"
397
- : "Refresh from server · keeps selected customizations",
398
- searchText: "fetch catalog refresh models server",
399
- },
400
- {
401
- value: "action:manual",
402
- label: "Add models manually",
403
- description: "Comma- or newline-separated model ids",
404
- searchText: "add manual custom model ids",
405
- },
406
- { value: "action:back", label: "Back", description: "Return to relay", searchText: "back return" },
407
- ];
408
- const currentValue = `model:${ctx.model?.id}`;
409
- const initialValue = cursor ?? (ctx.model?.provider === relay.id ? currentValue : undefined);
410
- const choice = await selectRouterItem(dialogs, `Relay · ${relay.id} · models`, items, { initialValue });
411
- if (choice === undefined || choice === "action:back")
412
- return;
413
- cursor = choice;
414
- if (choice === "action:fetch") {
415
- await fetchAndSelectModels(ctx, pi, dialogs, relay);
416
- continue;
417
- }
418
- if (choice === "action:manual") {
419
- const additions = await manualModelEntry(ctx, dialogs);
420
- if (!additions)
421
- continue;
422
- relay.models = mergeAddedModels(relay.models, additions);
423
- await persistRelay(ctx, pi, relay);
424
- continue;
425
- }
426
- if (choice.startsWith("model:")) {
427
- const modelId = choice.slice("model:".length);
428
- const model = relay.models.find((entry) => entry.id === modelId);
429
- if (!model)
430
- continue;
431
- await editModelFlow(ctx, pi, dialogs, relay, model);
432
- }
433
- }
434
- }
435
- /** Single-model editor: display name + thinking. Every confirmed change is live. */
436
- async function editModelFlow(ctx, pi, dialogs, relay, model) {
437
- while (true) {
438
- const resolved = resolveModelConfig(model);
439
- const nameDesc = resolved.name ? resolved.name : "(empty · /model shows id)";
440
- const action = await selectRouterItem(dialogs, `Relay · ${relay.id} · ${model.id}`, [
441
- { value: "name", label: "Display name", description: nameDesc },
442
- {
443
- value: "thinking",
444
- label: "Thinking levels",
445
- description: summarizeThinkingMap(resolved.thinkingLevelMap),
446
- },
447
- { value: "contextWindow", label: "Context window", description: String(resolved.contextWindow) },
448
- {
449
- value: "maxTokens",
450
- label: "Output tokens (local metadata, not wire cap)",
451
- description: String(resolved.maxTokens),
452
- },
453
- { value: "reasoning", label: "Reasoning", description: resolved.reasoning ? "Enabled" : "Disabled" },
454
- {
455
- value: "image",
456
- label: "Image input",
457
- description: resolved.input.includes("image") ? "Enabled" : "Disabled",
458
- },
459
- { value: "codex", label: "Codex request settings", description: "Summary · verbosity · parallel tools" },
460
- { value: "remove", label: "Remove model", description: "Delete this model from the relay" },
461
- { value: "back", label: "Back", description: "Return to models" },
462
- ]);
463
- if (action === undefined || action === "back")
464
- return;
465
- if (action === "contextWindow" || action === "maxTokens") {
466
- const next = await promptText(ctx, dialogs, action === "maxTokens" ? "Output tokens · local metadata, not wire cap" : "Context window", String(resolved[action]), (value) => {
467
- const number = Number(value);
468
- if (!Number.isSafeInteger(number) || number <= 0)
469
- return "Use a safe positive integer.";
470
- if (action === "maxTokens" ? number > resolved.contextWindow : number < resolved.maxTokens)
471
- return "Output tokens must not exceed context window.";
472
- return undefined;
473
- });
474
- if (next !== undefined) {
475
- model[action] = Number(next);
476
- await persistRelay(ctx, pi, relay);
477
- }
478
- continue;
479
- }
480
- if (action === "reasoning" || action === "image") {
481
- if (action === "reasoning")
482
- model.reasoning = !resolved.reasoning;
483
- else
484
- model.input = resolved.input.includes("image") ? ["text"] : ["text", "image"];
485
- await persistRelay(ctx, pi, relay);
486
- continue;
487
- }
488
- if (action === "codex") {
489
- await editCodexSettings(dialogs, model, () => persistRelay(ctx, pi, relay));
490
- continue;
491
- }
492
- if (action === "name") {
493
- const next = await promptText(ctx, dialogs, `Display name · ${model.id} (empty = show id)`, resolved.name ?? "");
494
- if (next === undefined)
495
- continue;
496
- const trimmed = next.trim();
497
- const nextName = !trimmed || trimmed === model.id ? undefined : trimmed;
498
- const prevName = model.name?.trim() && model.name.trim() !== model.id ? model.name.trim() : undefined;
499
- if (nextName === prevName)
500
- continue;
501
- if (nextName)
502
- model.name = nextName;
503
- else
504
- delete model.name;
505
- await persistRelay(ctx, pi, relay);
506
- continue;
507
- }
508
- if (action === "thinking") {
509
- const saver = new RelayAutoSaver(ctx, pi, relay);
510
- if (dialogs.kind === "tui") {
511
- await dialogs.show(createThinkingMapEditor({
512
- title: `Thinking · ${relay.id} / ${model.id}`,
513
- map: resolved.thinkingLevelMap,
514
- levels: ROUTER_THINKING_LEVELS,
515
- onChange: (nextMap) => {
516
- model.thinkingLevelMap = nextMap;
517
- saver.save();
518
- },
519
- }));
520
- }
521
- else {
522
- await editThinkingMapNative(dialogs, resolved.thinkingLevelMap, (nextMap) => {
523
- model.thinkingLevelMap = nextMap;
524
- saver.save();
525
- });
526
- }
527
- await saver.flush();
528
- continue;
529
- }
530
- if (action === "remove") {
531
- if (isCurrentRouterModel(ctx.model, relay.id, model.id)) {
532
- ctx.ui.notify(`Switch away from model "${model.id}" before removing it.`, "warning");
533
- continue;
534
- }
535
- const ok = await dialogs.confirm(`Remove model "${model.id}"?`, "Its display name and thinking settings will be removed.");
536
- if (!ok)
537
- continue;
538
- relay.models = relay.models.filter((entry) => entry.id !== model.id);
539
- await persistRelay(ctx, pi, relay);
540
- ctx.ui.notify(`Removed model "${model.id}".`, "info");
541
- return;
542
- }
543
- }
544
- }
545
- async function editCodexSettings(dialogs, model, save) {
546
- while (true) {
547
- const field = await selectRouterItem(dialogs, "Codex request settings", [
548
- { value: "reasoningSummary", label: "Reasoning summary", description: String(model.codex?.reasoningSummary) },
549
- { value: "verbosity", label: "Verbosity", description: String(model.codex?.verbosity) },
550
- {
551
- value: "parallelToolCalls",
552
- label: "Parallel tool calls",
553
- description: String(model.codex?.parallelToolCalls),
554
- },
555
- ]);
556
- if (!field)
557
- return;
558
- const values = field === "reasoningSummary"
559
- ? ["auto", "concise", "detailed"]
560
- : field === "verbosity"
561
- ? ["low", "medium", "high"]
562
- : ["true", "false"];
563
- const choice = await selectRouterItem(dialogs, field, [
564
- { value: "inherit", label: "Inherit" },
565
- ...(field === "parallelToolCalls" ? [] : [{ value: "omit", label: "Omit (null)" }]),
566
- ...values.map((value) => ({ value, label: value })),
567
- ]);
568
- if (!choice)
569
- continue;
570
- model.codex ??= {};
571
- const codex = model.codex;
572
- if (choice === "inherit")
573
- delete codex[field];
574
- else if (field === "parallelToolCalls")
575
- codex.parallelToolCalls = choice === "true";
576
- else if (field === "reasoningSummary")
577
- codex.reasoningSummary = choice === "omit" ? null : choice;
578
- else
579
- codex.verbosity = choice === "omit" ? null : choice;
580
- await save();
581
- }
582
- }
583
- async function editThinkingMapNative(dialogs, map, onChange) {
584
- const working = resolveRouterThinkingMap(map);
585
- while (true) {
586
- const choice = await selectRouterItem(dialogs, "Edit thinking level", [
587
- ...ROUTER_THINKING_LEVELS.map((level) => ({
588
- value: level,
589
- label: level,
590
- description: working[level] === null ? "hidden" : working[level] === undefined ? "default" : String(working[level]),
591
- })),
592
- { value: "back", label: "Back", description: "Changes are live" },
593
- ]);
594
- if (choice === undefined || choice === "back")
595
- return;
596
- if (!ROUTER_THINKING_LEVELS.includes(choice))
597
- continue;
598
- const level = choice;
599
- const mode = await selectRouterItem(dialogs, `${level} · mapping`, [
600
- { value: "inherit", label: "Inherit", description: "Pi default" },
601
- { value: "target", label: "String target", description: "Provider effort" },
602
- { value: "hidden", label: "Hidden", description: "null" },
603
- ]);
604
- if (mode === undefined)
605
- continue;
606
- if (mode === "inherit")
607
- delete working[level];
608
- else if (mode === "hidden")
609
- working[level] = null;
610
- else {
611
- const target = await dialogs.input(`${level} · provider effort`, working[level] ?? level);
612
- if (!target?.trim())
613
- continue;
614
- working[level] = target.trim();
615
- }
616
- onChange({ ...working });
617
- }
618
- }
619
- async function fetchAndSelectModels(ctx, pi, dialogs, relay) {
620
- const connection = await resolveProbeConnection(relay, ctx);
621
- if (connection.error) {
622
- ctx.ui.notify(connection.error, "warning");
623
- await recoverWithManualModels(ctx, pi, dialogs, relay);
624
- return;
625
- }
626
- let result;
627
- while (true) {
628
- const probeOptions = {
629
- baseUrl: relay.baseUrl,
630
- apiKey: connection.apiKey,
631
- headers: connection.headers,
632
- catalog: relay.catalog,
633
- };
634
- if (dialogs.kind === "tui") {
635
- result = await dialogs.show((tui, theme, _kb, done) => {
636
- const loader = new RouterProbeLoader(tui, theme, `Fetching models · ${relay.id}`);
637
- let settled = false;
638
- const finish = (value) => {
639
- if (settled)
640
- return;
641
- settled = true;
642
- loader.dispose();
643
- done(value);
644
- };
645
- loader.onAbort = () => finish(undefined);
646
- void probeRelayModels({ ...probeOptions, signal: loader.probeSignal })
647
- .then(finish)
648
- .catch((error) => finish({ ok: false, error: formatError(error) }));
649
- return loader;
650
- });
651
- }
652
- else {
653
- ctx.ui.notify(`Fetching models from ${relay.baseUrl}…`, "info");
654
- result = await probeRelayModels(probeOptions);
655
- }
656
- if (result === undefined)
657
- return;
658
- if (result.ok && result.models.length === 0) {
659
- ctx.ui.notify("Server returned an empty model list.", "warning");
660
- const recovery = await selectRouterItem(dialogs, `Catalog empty · ${relay.id}`, [
661
- { value: "retry", label: "Retry", description: "Fetch /models again" },
662
- { value: "manual", label: "Add models manually", description: "Enter model ids without a catalog" },
663
- { value: "back", label: "Back", description: "Return to relay settings" },
664
- ]);
665
- if (recovery === "retry")
666
- continue;
667
- if (recovery === "manual")
668
- await recoverWithManualModels(ctx, pi, dialogs, relay);
669
- return;
670
- }
671
- if (result.ok)
672
- break;
673
- ctx.ui.notify(`Fetch failed: ${result.error}`, "error");
674
- const recovery = await selectRouterItem(dialogs, `Catalog unavailable · ${relay.id}`, [
675
- { value: "retry", label: "Retry", description: "Fetch /models again" },
676
- { value: "manual", label: "Add models manually", description: "Enter model ids without a catalog" },
677
- { value: "back", label: "Back", description: "Return to relay settings" },
678
- ]);
679
- if (recovery === "retry")
680
- continue;
681
- if (recovery === "manual")
682
- await recoverWithManualModels(ctx, pi, dialogs, relay);
683
- return;
684
- }
685
- if (!result || !result.ok)
686
- return;
687
- if (result.truncated)
688
- ctx.ui.notify("Catalog truncated to 2,000 models.", "warning");
689
- const discovered = new Map(result.models.map((model) => [model.id, model]));
690
- const activeModelId = ctx.model?.provider === relay.id ? ctx.model.id : undefined;
691
- const catalog = mergeCatalogWithConfigured(relay.models, result.models, activeModelId);
692
- const initiallySelected = new Set(relay.models.map((model) => model.id));
693
- if (activeModelId)
694
- initiallySelected.add(activeModelId);
695
- const currentModelId = activeModelId;
696
- const protectedIds = currentModelId ? new Set([currentModelId]) : undefined;
697
- const preserved = new Map(relay.models.map((model) => [model.id, structuredClone(model)]));
698
- if (activeModelId && !preserved.has(activeModelId)) {
699
- preserved.set(activeModelId, createDefaultModelConfig(activeModelId));
700
- }
701
- const saver = new RelayAutoSaver(ctx, pi, relay);
702
- const applySelection = (selectedIds) => {
703
- if (currentModelId && !selectedIds.includes(currentModelId)) {
704
- ctx.ui.notify(`Switch away from model "${currentModelId}" before disabling it.`, "warning");
705
- return;
706
- }
707
- relay.models = selectedIds.map((id) => {
708
- const previous = preserved.get(id);
709
- if (previous)
710
- return structuredClone(previous);
711
- const next = modelFromCatalog(discovered.get(id) ?? { id });
712
- preserved.set(id, next);
713
- return structuredClone(next);
714
- });
715
- saver.save();
716
- };
717
- if (dialogs.kind === "tui") {
718
- const choice = await dialogs.show(createModelChecklist({
719
- title: `Select models · ${relay.id}`,
720
- subtitle: "Space toggles immediately · Enter/Esc returns",
721
- models: catalog,
722
- initiallySelected,
723
- protectedIds,
724
- onChange: applySelection,
725
- onProtectedToggle: (id) => ctx.ui.notify(`Switch away from model "${id}" before disabling it.`, "warning"),
726
- }));
727
- if (choice.kind === "close")
728
- applySelectionIfChanged(relay, choice.selectedIds, applySelection);
729
- }
730
- else {
731
- const ok = await dialogs.confirm(`Import ${result.models.length} models?`, "Non-TUI mode imports the full catalog.");
732
- if (ok)
733
- applySelection([...new Set([...initiallySelected, ...result.models.map((model) => model.id)])]);
734
- }
735
- await saver.flush();
736
- if (relay.models.length === 0)
737
- ctx.ui.notify(`No models enabled for "${relay.id}".`, "warning");
738
- }
739
- export function modelFromCatalog(entry) {
740
- const model = { ...createDefaultModelConfig(entry.id, entry.name), ...entry.metadata };
741
- model.maxTokens = Math.min(model.maxTokens, model.contextWindow);
742
- return model;
743
- }
744
- async function resolveProbeConnection(relay, ctx) {
745
- try {
746
- // Registered relays use Pi's canonical credential precedence and dynamic resolution.
747
- // A new, empty relay is not registered yet: use the bounded literal/env fallback below.
748
- const resolved = relay.models.length > 0 ? await ctx.modelRegistry.getProviderAuth?.(relay.id) : undefined;
749
- const key = resolved?.auth.apiKey ? { value: resolved.auth.apiKey } : resolveProbeApiKey(relay.apiKey);
750
- if (key.error)
751
- return { error: key.error };
752
- const headers = { ...resolved?.auth.headers };
753
- for (const [name, raw] of Object.entries(relay.headers ?? {})) {
754
- if (Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase()))
755
- continue;
756
- const value = resolveProbeApiKey(raw);
757
- if (value.error)
758
- return {
759
- error: `Cannot resolve catalog header ${name}. Configure a model first for Pi's dynamic auth resolution.`,
760
- };
761
- headers[name] = value.value ?? "";
762
- }
763
- return { apiKey: key.value, headers };
764
- }
765
- catch {
766
- return {
767
- error: "Cannot resolve relay credentials for catalog discovery. Check provider authentication and configured values.",
768
- };
769
- }
770
- }
771
- async function manualModelEntry(ctx, dialogs) {
772
- const value = await dialogs.input("Model IDs (comma-separated)", "gpt-5.6-sol, gpt-5.6-luna");
773
- if (value === undefined)
774
- return undefined;
775
- const ids = [
776
- ...new Set(value
777
- .split(/[,\r\n]+/)
778
- .map((id) => id.trim())
779
- .filter(Boolean)),
780
- ];
781
- if (ids.length === 0) {
782
- ctx.ui.notify("Enter at least one model id.", "warning");
783
- return undefined;
784
- }
785
- return ids.map((id) => createDefaultModelConfig(id));
786
- }
787
- async function recoverWithManualModels(ctx, pi, dialogs, relay) {
788
- const additions = await manualModelEntry(ctx, dialogs);
789
- if (!additions)
790
- return;
791
- relay.models = mergeAddedModels(relay.models, additions);
792
- await persistRelay(ctx, pi, relay);
793
- }
794
- function mergeAddedModels(previous, additions) {
795
- const result = previous.map((model) => structuredClone(model));
796
- const ids = new Set(result.map((model) => model.id));
797
- for (const model of additions) {
798
- if (ids.has(model.id))
799
- continue;
800
- ids.add(model.id);
801
- result.push(model);
802
- }
803
- return result;
804
- }
805
- export function mergeCatalogWithConfigured(configured, catalog, activeModelId) {
806
- const items = new Map();
807
- const configuredById = new Map(configured.map((model) => [model.id, model]));
808
- for (const model of catalog) {
809
- const previous = configuredById.get(model.id);
810
- items.set(model.id, {
811
- id: model.id,
812
- name: previous?.name ?? model.name,
813
- });
814
- }
815
- for (const model of configured) {
816
- if (!items.has(model.id)) {
817
- items.set(model.id, {
818
- id: model.id,
819
- name: model.name ?? model.id,
820
- unavailable: true,
821
- });
822
- }
823
- }
824
- if (activeModelId && !items.has(activeModelId)) {
825
- items.set(activeModelId, { id: activeModelId, name: activeModelId, unavailable: true });
826
- }
827
- return [...items.values()].sort((left, right) => left.id.localeCompare(right.id));
828
- }
829
- function applySelectionIfChanged(relay, selectedIds, apply) {
830
- const current = new Set(relay.models.map((model) => model.id));
831
- if (current.size === selectedIds.length && selectedIds.every((id) => current.has(id)))
832
- return;
833
- apply([...selectedIds]);
834
- }
835
- async function persistRelay(ctx, pi, relay) {
836
- await initializeRouterState(pi);
837
- await upsertRelay(relay);
838
- registerOneRelay(pi, relay);
839
- await ctx.modelRegistry.refresh();
840
- }
841
- async function promptText(ctx, dialogs, title, placeholder, validate) {
842
- while (true) {
843
- const value = await dialogs.input(title, placeholder);
844
- if (value === undefined)
845
- return undefined;
846
- const error = validate?.(value);
847
- if (error) {
848
- ctx.ui.notify(error, "warning");
849
- continue;
850
- }
851
- return value;
852
- }
853
- }
854
- async function selectRouterItem(dialogs, title, items, opts) {
855
- if (dialogs.kind === "tui") {
856
- return dialogs.show(createSearchableSelector({
857
- title,
858
- items,
859
- initialValue: opts?.initialValue,
860
- initialQuery: opts?.initialQuery,
861
- maxVisible: opts?.maxVisible ?? Math.min(10, Math.max(1, items.length)),
862
- }));
863
- }
864
- const labels = items.map((item) => (item.description ? `${item.label} — ${item.description}` : item.label));
865
- const selected = await dialogs.select(title, labels);
866
- if (selected === undefined)
867
- return undefined;
868
- return items[labels.indexOf(selected)]?.value;
869
- }
870
- function maskKey(key) {
871
- if (key.startsWith("$") || key.startsWith("!"))
872
- return key;
873
- if (key.length <= 8)
874
- return "••••";
875
- return `${key.slice(0, 4)}…${key.slice(-4)}`;
876
- }
877
- /** Resolve only keys that can be safely used by the catalog probe. */
878
- export function resolveProbeApiKey(apiKey) {
879
- const trimmed = apiKey.trim();
880
- if (!trimmed)
881
- return {};
882
- if (trimmed.startsWith("!")) {
883
- return { error: "This API key uses a !command and cannot be resolved for catalog probing." };
884
- }
885
- const envOnly = trimmed.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
886
- const braced = trimmed.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
887
- const envName = envOnly?.[1] ?? braced?.[1];
888
- if (envName) {
889
- const value = process.env[envName]?.trim();
890
- return value ? { value } : { error: `Environment variable $${envName} is not set.` };
891
- }
892
- if (trimmed.includes("$")) {
893
- return { error: "This API key contains interpolation and cannot be resolved for catalog probing." };
894
- }
895
- return { value: trimmed };
896
- }
897
- //# sourceMappingURL=ui.js.map