@rexvane/piweb 0.3.20 → 0.3.22

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 (317) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -4
  3. package/.next/build-manifest.json +5 -5
  4. package/.next/next-minimal-server.js.nft.json +1 -1
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +7 -7
  7. package/.next/react-loadable-manifest.json +12 -12
  8. package/.next/required-server-files.js +18 -2
  9. package/.next/required-server-files.json +18 -2
  10. package/.next/routes-manifest.json +12 -0
  11. package/.next/server/app/_global-error/page.js +2 -2
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_global-error.html +1 -1
  15. package/.next/server/app/_global-error.meta +8 -0
  16. package/.next/server/app/_global-error.rsc +15 -11
  17. package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
  18. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
  20. package/.next/server/app/_not-found/page.js +1 -1
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.meta +8 -0
  25. package/.next/server/app/_not-found.rsc +17 -13
  26. package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
  27. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
  28. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
  29. package/.next/server/app/api/agent/[id]/events/route.js +3 -3
  30. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/[id]/route.js +1 -1
  32. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  34. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  35. package/.next/server/app/api/agent/new/route.js +1 -1
  36. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  37. package/.next/server/app/api/config/route.js +1 -1
  38. package/.next/server/app/api/config/route.js.nft.json +1 -1
  39. package/.next/server/app/api/files/route.js +1 -1
  40. package/.next/server/app/api/files/route.js.nft.json +1 -1
  41. package/.next/server/app/api/fs/route.js +1 -1
  42. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  43. package/.next/server/app/api/git/route.js +4 -4
  44. package/.next/server/app/api/git/route.js.nft.json +1 -1
  45. package/.next/server/app/api/github/route.js +5 -0
  46. package/.next/server/app/api/github/route.js.nft.json +1 -0
  47. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
  48. package/.next/server/app/api/growth/route.js +1 -1
  49. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  50. package/.next/server/app/api/health/route.js +1 -1
  51. package/.next/server/app/api/health/route.js.nft.json +1 -1
  52. package/.next/server/app/api/models/defaults/route.js +1 -0
  53. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
  54. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
  55. package/.next/server/app/api/models/route.js +1 -1
  56. package/.next/server/app/api/models/route.js.nft.json +1 -1
  57. package/.next/server/app/api/pi-settings/route.js +1 -1
  58. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  59. package/.next/server/app/api/plugins/route.js +1 -1
  60. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  61. package/.next/server/app/api/prompts/route.js +1 -1
  62. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  63. package/.next/server/app/api/remote/route.js +3 -3
  64. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  65. package/.next/server/app/api/security/route.js +1 -1
  66. package/.next/server/app/api/security/route.js.nft.json +1 -1
  67. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  68. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  69. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  70. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  71. package/.next/server/app/api/sessions/import/route.js +2 -2
  72. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  73. package/.next/server/app/api/sessions/route.js +1 -1
  74. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  75. package/.next/server/app/api/skills/route.js +2 -2
  76. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  77. package/.next/server/app/api/update/route.js +2 -2
  78. package/.next/server/app/api/update/route.js.nft.json +1 -1
  79. package/.next/server/app/api/version/route.js +1 -1
  80. package/.next/server/app/api/version/route.js.nft.json +1 -1
  81. package/.next/server/app/api/web-auth/route.js +1 -1
  82. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  83. package/.next/server/app/api/workspaces/route.js +1 -1
  84. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  85. package/.next/server/app/icon.svg/route.js +1 -1
  86. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  87. package/.next/server/app/icon.svg.meta +1 -1
  88. package/.next/server/app/index.html +1 -1
  89. package/.next/server/app/index.meta +8 -0
  90. package/.next/server/app/index.rsc +19 -13
  91. package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
  92. package/.next/server/app/index.segments/_full.segment.rsc +19 -13
  93. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  94. package/.next/server/app/login/page.js +1 -1
  95. package/.next/server/app/login/page.js.nft.json +1 -1
  96. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/login.html +1 -1
  98. package/.next/server/app/login.meta +8 -0
  99. package/.next/server/app/login.rsc +23 -17
  100. package/.next/server/app/login.segments/_full.segment.rsc +23 -17
  101. package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
  102. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
  103. package/.next/server/app/page.js +1 -1
  104. package/.next/server/app/page.js.nft.json +1 -1
  105. package/.next/server/app/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app-paths-manifest.json +6 -4
  107. package/.next/server/chunks/229.js +1 -1
  108. package/.next/server/chunks/421.js +1 -0
  109. package/.next/server/chunks/430.js +215 -31
  110. package/.next/server/chunks/496.js +1 -1
  111. package/.next/server/chunks/538.js +1 -1
  112. package/.next/server/chunks/662.js +2 -0
  113. package/.next/server/chunks/686.js +1 -1
  114. package/.next/server/chunks/807.js +1 -1
  115. package/.next/server/chunks/813.js +23 -1
  116. package/.next/server/chunks/920.js +2 -2
  117. package/.next/server/chunks/924.js +1 -1
  118. package/.next/server/chunks/970.js +305 -0
  119. package/.next/server/instrumentation.js +1 -1
  120. package/.next/server/instrumentation.js.nft.json +1 -1
  121. package/.next/server/middleware-build-manifest.js +1 -1
  122. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  123. package/.next/server/middleware.js +185 -23
  124. package/.next/server/middleware.js.nft.json +1 -1
  125. package/.next/server/pages/404.html +1 -1
  126. package/.next/server/pages/500.html +1 -1
  127. package/.next/server/preview-props.json +5 -0
  128. package/.next/server/server-reference-manifest.json +1 -1
  129. package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
  130. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  131. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  132. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
  133. package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
  134. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  135. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  136. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  137. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  138. package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
  139. package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
  140. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
  141. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
  142. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
  143. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
  144. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
  145. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
  146. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
  147. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
  148. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
  149. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
  150. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
  151. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
  152. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
  153. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
  154. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
  155. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
  156. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
  157. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
  158. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
  159. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
  160. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
  161. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
  162. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
  163. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
  164. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
  165. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
  166. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
  167. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
  168. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
  169. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  170. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  171. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
  172. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
  173. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
  174. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
  175. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
  176. package/.next/static/css/6671d0d1589fdc0e.css +1 -0
  177. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  178. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  179. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  180. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  181. package/.next/types/app/api/agent/new/route.ts +3 -1
  182. package/.next/types/app/api/config/route.ts +3 -1
  183. package/.next/types/app/api/files/route.ts +3 -1
  184. package/.next/types/app/api/fs/route.ts +3 -1
  185. package/.next/types/app/api/git/route.ts +3 -1
  186. package/.next/types/app/api/github/route.ts +355 -0
  187. package/.next/types/app/api/growth/route.ts +3 -1
  188. package/.next/types/app/api/health/route.ts +3 -1
  189. package/.next/types/app/api/models/defaults/route.ts +355 -0
  190. package/.next/types/app/api/models/route.ts +3 -1
  191. package/.next/types/app/api/pi-settings/route.ts +3 -1
  192. package/.next/types/app/api/plugins/route.ts +3 -1
  193. package/.next/types/app/api/prompts/route.ts +3 -1
  194. package/.next/types/app/api/remote/route.ts +3 -1
  195. package/.next/types/app/api/security/route.ts +3 -1
  196. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  197. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  198. package/.next/types/app/api/sessions/import/route.ts +3 -1
  199. package/.next/types/app/api/sessions/route.ts +3 -1
  200. package/.next/types/app/api/skills/route.ts +3 -1
  201. package/.next/types/app/api/update/route.ts +3 -1
  202. package/.next/types/app/api/version/route.ts +3 -1
  203. package/.next/types/app/api/web-auth/route.ts +3 -1
  204. package/.next/types/app/api/workspaces/route.ts +3 -1
  205. package/.next/types/app/layout.ts +4 -1
  206. package/.next/types/app/login/page.ts +4 -1
  207. package/.next/types/app/page.ts +4 -1
  208. package/.next/types/cache-life.d.ts +11 -43
  209. package/.next/types/routes.d.ts +3 -1
  210. package/.next/types/validator.ts +76 -2
  211. package/README.md +14 -5
  212. package/next.config.ts +4 -2
  213. package/package.json +2 -2
  214. package/scripts/release.mjs +1 -1
  215. package/src/app/api/agent/[id]/events/route.ts +42 -13
  216. package/src/app/api/files/route.ts +5 -2
  217. package/src/app/api/github/route.ts +26 -0
  218. package/src/app/api/models/defaults/route.ts +18 -0
  219. package/src/app/api/models/route.ts +24 -1
  220. package/src/app/api/update/route.ts +18 -2
  221. package/src/app/globals.css +275 -0
  222. package/src/components/AppShell.tsx +89 -33
  223. package/src/components/ChatInput.tsx +245 -63
  224. package/src/components/ChatWindow.tsx +123 -108
  225. package/src/components/ContextMeter.tsx +204 -123
  226. package/src/components/DrawerPanel.tsx +19 -26
  227. package/src/components/ExtensionUI.tsx +8 -2
  228. package/src/components/GitPanel.tsx +110 -40
  229. package/src/components/MarkdownBlock.tsx +333 -37
  230. package/src/components/ProjectPanel.tsx +55 -30
  231. package/src/components/PromptPanel.tsx +2 -4
  232. package/src/components/ProviderSetupModal.module.css +20 -0
  233. package/src/components/ProviderSetupModal.tsx +134 -29
  234. package/src/components/SettingsPanel.tsx +365 -80
  235. package/src/components/TrajectoryView.tsx +4 -6
  236. package/src/components/chat/chat-scroll.ts +18 -0
  237. package/src/components/composer/ComposerImages.tsx +306 -0
  238. package/src/components/update/CopyCommand.tsx +34 -0
  239. package/src/components/update/UpdateNotice.tsx +130 -0
  240. package/src/components/update/update-store.ts +155 -0
  241. package/src/hooks/useGrowth.ts +3 -0
  242. package/src/hooks/usePiWeb.ts +54 -27
  243. package/src/hooks/useWorkspaceFiles.ts +93 -0
  244. package/src/i18n.tsx +182 -6
  245. package/src/instrumentation-node.ts +3 -0
  246. package/src/lib/agent-manager.ts +193 -119
  247. package/src/lib/chat/event-stream.ts +113 -0
  248. package/src/lib/chat/stream-settings.ts +192 -0
  249. package/src/lib/files-service.ts +100 -1
  250. package/src/lib/format.ts +56 -0
  251. package/src/lib/git-service.ts +45 -13
  252. package/src/lib/github-identity.ts +245 -0
  253. package/src/lib/growth-service.ts +6 -2
  254. package/src/lib/hero-model-memory.ts +28 -0
  255. package/src/lib/model-defaults.ts +105 -0
  256. package/src/lib/model-draft.ts +257 -9
  257. package/src/lib/model-endpoint.ts +23 -0
  258. package/src/lib/model-files.ts +33 -0
  259. package/src/lib/models-service.ts +240 -78
  260. package/src/lib/pi.ts +27 -2
  261. package/src/lib/process-format.ts +3 -2
  262. package/src/lib/session/metrics.ts +31 -0
  263. package/src/lib/thinking-level.ts +39 -0
  264. package/src/lib/trajectory.ts +17 -3
  265. package/src/lib/types.ts +19 -1
  266. package/src/lib/update/startup-check.ts +117 -0
  267. package/src/lib/update-service.ts +23 -3
  268. package/src/lib/workspace-index.ts +270 -0
  269. package/.next/server/chunks/131.js +0 -1
  270. package/.next/server/chunks/243.js +0 -1
  271. package/.next/server/chunks/741.js +0 -165
  272. package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +0 -1
  273. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  274. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  275. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  276. package/.next/static/chunks/4686-b1e9fa876d0ea671.js +0 -1
  277. package/.next/static/chunks/4778.e4c9ed093a413bc4.js +0 -1
  278. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  279. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  280. package/.next/static/chunks/839.d3fe337a9117c325.js +0 -1
  281. package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
  282. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  283. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  284. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  285. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  286. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  287. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  288. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  289. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  290. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  291. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  292. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  293. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  294. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  295. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  296. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  297. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  298. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  299. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  300. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  301. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  302. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  303. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  304. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  305. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  306. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  307. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  308. package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
  309. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  310. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  311. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  312. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  313. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  314. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  315. package/.next/static/chunks/webpack-3614a8337843456b.js +0 -1
  316. package/.next/static/css/7391c42e1ae617f9.css +0 -1
  317. /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
@@ -12,9 +12,10 @@ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
12
12
  import { getNodeValue, parseTree, printParseErrorCode, type ParseError } from "jsonc-parser";
13
13
  import { filterModelsByPiScope } from "./models/model-scope";
14
14
  import { invalidateJsonFilesNamed, readJsonObjectSafe } from "./agent-config";
15
- import { getAgentDir, getModelRuntime, refreshModelRuntime } from "./pi";
15
+ import { getAgentDir, getModelRuntime, modelRuntimeVersion, refreshModelRuntime, refreshModelRuntimeIfFilesChanged } from "./pi";
16
16
  import { getEnabledModelPatterns } from "./pi-settings";
17
- import { reloadSessionsForCwd } from "./agent-manager";
17
+ import { refreshSessionModels, reloadSessionsForCwd } from "./agent-manager";
18
+ import { withContextWindow } from "./model-draft";
18
19
  import {
19
20
  getSupportedThinkingLevels, InMemoryCredentialStore, InMemoryModelsStore,
20
21
  type AuthInteraction, type AuthPrompt, type AuthType,
@@ -43,6 +44,7 @@ export interface ProviderView {
43
44
  baseUrl?: string;
44
45
  apiKeyLabel?: string;
45
46
  authTypes: Array<"api_key" | "oauth">;
47
+ /** 用到的协议,用的模型最多的在前:apis[0] 是这家的默认协议(新加模型的 api、探测目录时的鉴权方式) */
46
48
  apis: string[];
47
49
  builtIn: boolean;
48
50
  authConfigured: boolean;
@@ -60,13 +62,57 @@ export interface ProviderView {
60
62
  export interface DiscoveredModel {
61
63
  id: string;
62
64
  name?: string;
65
+ /** 端点自报的上下文窗口(各家字段名不同,见 catalogCapabilities) */
66
+ contextWindow?: number;
67
+ /** 端点自报的单次输出上限 */
68
+ maxTokens?: number;
69
+ /** 端点自报的输入模态里有 image */
70
+ vision?: boolean;
63
71
  }
64
72
 
65
- let builtInProviderIdsPromise: Promise<Set<string>> | null = null;
73
+ function positiveInt(value: unknown): number | undefined {
74
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
75
+ }
76
+
77
+ /**
78
+ * 从模型目录条目里挑出能力字段。各家命名不同,能给就读、读不到就留空(不猜,缺的由体检卡片提醒用户填):
79
+ * - 上下文:`context_window`(本机中转)/ `context_length`(OpenRouter)/ `max_input_tokens`;
80
+ * - 输出上限:`max_output_tokens` / `max_completion_tokens` / `top_provider.max_completion_tokens`;
81
+ * - 视觉:`modalities.input`(本机中转)/ `architecture.input_modalities`(OpenRouter / llama.cpp)。
82
+ */
83
+ export function catalogCapabilities(value: Record<string, unknown>): Pick<DiscoveredModel, "contextWindow" | "maxTokens" | "vision"> {
84
+ const architecture = isRecord(value.architecture) ? value.architecture : undefined;
85
+ const modalities = value.modalities ?? architecture?.input_modalities ?? value.input_modalities;
86
+ const inputList = Array.isArray(modalities)
87
+ ? modalities
88
+ : isRecord(modalities) && Array.isArray(modalities.input)
89
+ ? modalities.input
90
+ : undefined;
91
+ const topProvider = isRecord(value.top_provider) ? value.top_provider : undefined;
92
+ const contextWindow = positiveInt(value.context_window)
93
+ ?? positiveInt(value.context_length)
94
+ ?? positiveInt(value.max_input_tokens)
95
+ ?? positiveInt(architecture?.context_length);
96
+ const maxTokens = positiveInt(value.max_output_tokens)
97
+ ?? positiveInt(value.max_completion_tokens)
98
+ ?? positiveInt(topProvider?.max_completion_tokens);
99
+ const vision = inputList
100
+ ? inputList.some((item) => typeof item === "string" && item.toLowerCase() === "image")
101
+ : undefined;
102
+ return {
103
+ ...(contextWindow === undefined ? {} : { contextWindow }),
104
+ ...(maxTokens === undefined ? {} : { maxTokens }),
105
+ ...(vision === undefined ? {} : { vision }),
106
+ };
107
+ }
108
+
109
+ let builtInCatalogPromise: Promise<Map<string, Map<string, number>>> | null = null;
66
110
 
67
111
  /** 目录短缓存:页面挂载与设置关闭都会拉一次;认证探测(每个供应商一次网络往返)不必秒级重复 */
68
112
  let listCache: { at: number; value: Promise<{ providers: ProviderView[]; models: ModelView[] }> } | null = null;
69
113
  const LIST_TTL_MS = 5_000;
114
+ /** 缓存是按哪一代共享运行时算的:运行时刷新过(手改文件、/reload)就整体作废 */
115
+ let listedGeneration = -1;
70
116
 
71
117
  /**
72
118
  * 认证探测结果单独缓存:目录只活 5 秒,但「这个 provider 能不能用」在一次会话里几乎不变。
@@ -88,31 +134,55 @@ export function modelListStamp(): string {
88
134
  return listCache ? String(listCache.at) : "none";
89
135
  }
90
136
 
91
- /** Read the unmodified SDK catalog through its public modelsPath:null option. */
92
- function getBuiltInProviderIds(): Promise<Set<string>> {
93
- if (!builtInProviderIdsPromise) {
94
- builtInProviderIdsPromise = ModelRuntime.create({
137
+ /** Read the unmodified SDK catalog through its public modelsPath:null option: provider ID → model ID → built-in context window. */
138
+ function getBuiltInCatalog(): Promise<Map<string, Map<string, number>>> {
139
+ if (!builtInCatalogPromise) {
140
+ builtInCatalogPromise = ModelRuntime.create({
95
141
  modelsPath: null, refreshOnCreate: false, allowModelNetwork: false,
96
142
  credentials: new InMemoryCredentialStore(), modelsStore: new InMemoryModelsStore(),
97
143
  })
98
- .then((runtime) => new Set(runtime.getProviders().map((provider) => provider.id)))
144
+ .then((runtime) => new Map(runtime.getProviders().map((provider) => [
145
+ provider.id,
146
+ new Map(runtime.getModels(provider.id).map((model) => [model.id, Number((model as { contextWindow?: unknown }).contextWindow) || 0])),
147
+ ])))
99
148
  .catch((error) => {
100
- builtInProviderIdsPromise = null;
149
+ builtInCatalogPromise = null;
101
150
  throw error;
102
151
  });
103
152
  }
104
- return builtInProviderIdsPromise;
153
+ return builtInCatalogPromise;
105
154
  }
106
155
 
107
- function envKeyOf(providerId: string): string | null {
108
- void providerId;
109
- return null;
156
+ /**
157
+ * 内置 provider 的目录模型 ID(provider → 排好序的 ID),给设置页的模型表单分流:与目录同 ID 的行写
158
+ * modelOverrides(只覆盖填写的字段),新 ID 才写 models[]——同 ID 的 models[] 条目会被 SDK 按默认值整条重建。
159
+ * 目录 = SDK 静态目录 ∪ 当前目录里不是 models.json `models[]` 定义出来的模型(远程目录缓存、OAuth / 扩展补的)。
160
+ * content 是 readCustomProviders 读到的那份,models 是同一时刻的完整目录。
161
+ */
162
+ export async function builtinCatalogModelIds(content: string, models: ReadonlyArray<{ provider: string; id: string }>): Promise<Record<string, string[]>> {
163
+ const catalog = await getBuiltInCatalog();
164
+ const defined = new Map<string, Set<string>>();
165
+ for (const [providerId, provider] of Object.entries(parseCustomProviders(content).providers)) {
166
+ const rows = Array.isArray(provider.models) ? provider.models : [];
167
+ defined.set(providerId, new Set(rows.flatMap((row) => (isRecord(row) && typeof row.id === "string" ? [row.id] : []))));
168
+ }
169
+ const ids = new Map([...catalog].map(([providerId, modelIds]) => [providerId, new Set(modelIds.keys())]));
170
+ for (const model of models) {
171
+ if (!defined.get(model.provider)?.has(model.id)) ids.get(model.provider)?.add(model.id);
172
+ }
173
+ return Object.fromEntries([...ids].map(([providerId, modelIds]) => [providerId, [...modelIds].sort()]));
110
174
  }
111
175
 
112
- export function listModels(): Promise<{
176
+ export async function listModels(): Promise<{
113
177
  providers: ProviderView[];
114
178
  models: ModelView[];
115
179
  }> {
180
+ // 手改过 models.json / auth.json 时先刷新共享目录(只做两次 stat),再看缓存是不是还对得上
181
+ await refreshModelRuntimeIfFilesChanged();
182
+ if (listedGeneration !== modelRuntimeVersion()) {
183
+ listedGeneration = modelRuntimeVersion();
184
+ invalidateModelList();
185
+ }
116
186
  if (listCache && Date.now() - listCache.at < LIST_TTL_MS) return listCache.value;
117
187
  const value = listModelsUncached().catch((error) => {
118
188
  listCache = null;
@@ -136,7 +206,20 @@ export async function listModelsForPiWeb(): Promise<{ providers: ProviderView[];
136
206
  };
137
207
  }
138
208
 
139
- /** auth.json 各条目的凭证类型(与 storedCredentialFor 同样的两层数据结构);读取失败不影响目录 */
209
+ /**
210
+ * provider 用到的协议,按用它的模型数从多到少排(同数按字母序)。apis[0] 当这家的默认协议用:
211
+ * 以前取字母序第一个,多协议的 provider 会挑到少数派(比如大部分模型走 responses 却给新模型配上 completions)。
212
+ */
213
+ export function apisByUsage(models: ReadonlyArray<{ api?: unknown }>): string[] {
214
+ const counts = new Map<string, number>();
215
+ for (const model of models) {
216
+ const api = String(model.api ?? "");
217
+ if (api) counts.set(api, (counts.get(api) ?? 0) + 1);
218
+ }
219
+ return [...counts].sort(([a, x], [b, y]) => y - x || (a < b ? -1 : a > b ? 1 : 0)).map(([api]) => api);
220
+ }
221
+
222
+ /** auth.json 各条目的凭证类型(顶层和 providers 两层都认);读取失败不影响目录 */
140
223
  async function readStoredAuthTypes(): Promise<Map<string, "api_key" | "oauth">> {
141
224
  const out = new Map<string, "api_key" | "oauth">();
142
225
  try {
@@ -161,7 +244,7 @@ async function listModelsUncached(): Promise<{
161
244
  models: ModelView[];
162
245
  }> {
163
246
  const rt = await getModelRuntime();
164
- const builtInProviderIds = await getBuiltInProviderIds();
247
+ const builtInCatalog = await getBuiltInCatalog();
165
248
  const providers = rt.getProviders() as any[];
166
249
  const storedAuthTypes = await readStoredAuthTypes();
167
250
  const out: ProviderView[] = [];
@@ -214,8 +297,8 @@ async function listModelsUncached(): Promise<{
214
297
  baseUrl: typeof p.baseUrl === "string" ? p.baseUrl : undefined,
215
298
  apiKeyLabel: typeof p.auth?.apiKey?.name === "string" ? p.auth.apiKey.name : undefined,
216
299
  authTypes,
217
- apis: [...new Set(list.map((mm) => String(mm.api ?? "")).filter(Boolean))].sort(),
218
- builtIn: builtInProviderIds.has(pid),
300
+ apis: apisByUsage(list),
301
+ builtIn: builtInCatalog.has(pid),
219
302
  authConfigured: auth?.configured === true,
220
303
  authSource: auth?.source,
221
304
  authReady,
@@ -265,28 +348,46 @@ export async function removeApiKey(providerId: string): Promise<void> {
265
348
  invalidateModelList();
266
349
  }
267
350
 
268
- /** 已存凭证回退:编辑表单的 key 框为空(「留空保持不变」)时,从这里取。oauth=true 表示来自 OAuth 令牌(部分端点不适用)。 */
269
- async function storedCredentialFor(providerId: string): Promise<{ key: string; oauth: boolean; accountId?: string } | undefined> {
270
- // 自定义 provider / 内置覆盖层:models.json providers[id].apiKey。
271
- // 损坏/权限失败不能静默回退到别的凭证;JSONC 与编辑器使用相同解析规则。
272
- const custom = await readCustomProvidersSnapshot(modelsJsonPath());
273
- if (custom.content !== undefined) {
274
- const key = parseCustomProviders(custom.content).providers[providerId]?.apiKey;
275
- if (typeof key === "string" && key.trim()) return { key: key.trim(), oauth: false };
276
- }
277
- // 内置 provider:auth.json 条目 —— API key 直接用;OAuth 条目的 access 就是
278
- // SDK 发请求时的 Bearer key(如 xai 的 toAuth 返回 { apiKey: credential.access })
351
+ interface ConfiguredCredential {
352
+ key: string;
353
+ /** 来自 OAuth 令牌(部分端点只认它,部分端点不认它) */
354
+ oauth: boolean;
355
+ /** openai-codex:ChatGPT 账号 ID,用量端点要带 */
356
+ accountId?: string;
357
+ /** 认证结果自带的地址(如 Copilot 企业版),也算这家已配置的地址 */
358
+ baseUrl?: string;
359
+ }
360
+
361
+ /**
362
+ * 该 provider 当前生效的凭证,和 SDK 发请求时同一套解析(ModelRuntime.getAuth):运行时 key → auth.json
363
+ * (OAuth 快过期会先刷新并写回)→ models.json 的 apiKey($ENV / ${ENV} / !command 照样解析)→ 环境变量。
364
+ * 编辑表单的 key 框留空(「留空保持不变」)时的模型目录探测、各家用量探测都从这里取。
365
+ * 拿不到或解析失败都返回 undefined:报错里可能带着命令原文,不往外带。
366
+ */
367
+ async function configuredCredentialFor(providerId: string): Promise<ConfiguredCredential | undefined> {
279
368
  try {
280
- const parsed = JSON.parse(await fs.readFile(path.join(getAgentDir(), "auth.json"), "utf8")) as Record<string, any>;
281
- const entry = parsed?.[providerId] ?? parsed?.providers?.[providerId];
282
- const key = entry?.apiKey ?? entry?.api_key ?? entry?.key;
283
- if (typeof key === "string" && key.trim()) return { key: key.trim(), oauth: false };
284
- if (entry?.type === "oauth" && typeof entry?.access === "string" && entry.access.trim()) {
285
- const accountId = typeof entry.accountId === "string" && entry.accountId.trim() ? entry.accountId.trim() : undefined;
286
- return { key: entry.access.trim(), oauth: true, ...(accountId === undefined ? {} : { accountId }) };
287
- }
369
+ const rt = await getModelRuntime();
370
+ const resolved = await rt.getAuth(providerId, { signal: AbortSignal.timeout(20_000) });
371
+ const key = (resolved?.auth.apiKey ?? bearerToken(resolved?.auth.headers))?.trim();
372
+ if (!resolved || !key) return undefined;
373
+ const oauth = (await rt.checkAuth(providerId).catch(() => undefined))?.type === "oauth";
374
+ // SDK 发 Codex 请求时也是从令牌里取账号 ID
375
+ const accountId = oauth && providerId === "openai-codex" ? openaiAuthClaims(key)?.chatgpt_account_id : undefined;
376
+ return {
377
+ key,
378
+ oauth,
379
+ ...(typeof accountId === "string" && accountId.trim() ? { accountId: accountId.trim() } : {}),
380
+ ...(resolved.auth.baseUrl ? { baseUrl: resolved.auth.baseUrl } : {}),
381
+ };
288
382
  } catch {
289
- /* 同上 */
383
+ return undefined;
384
+ }
385
+ }
386
+
387
+ /** 只给请求头的认证结果(如 kimi-coding 的 OAuth):从 Authorization: Bearer 里取令牌 */
388
+ function bearerToken(headers: Record<string, string | null> | undefined): string | undefined {
389
+ for (const [name, value] of Object.entries(headers ?? {})) {
390
+ if (name.toLowerCase() === "authorization" && value) return /^Bearer\s+(\S+)/i.exec(value.trim())?.[1];
290
391
  }
291
392
  return undefined;
292
393
  }
@@ -424,18 +525,18 @@ export async function discoverModels(input: { baseUrl: string; api?: string; api
424
525
  if (base.username || base.password) throw new Error("API 地址不能包含用户名或密码");
425
526
  const endpoint = modelsEndpoint(base);
426
527
  const headers: Record<string, string> = { Accept: "application/json" };
427
- // 表单 key 为空时回退到该 provider 的已存凭证(编辑场景:占位符「留空保持不变」)
428
- const credential = input.apiKey?.trim()
528
+ // 表单 key 为空时回退到该 provider 已配置的凭证(编辑场景:占位符「留空保持不变」)
529
+ const credential: ConfiguredCredential | undefined = input.apiKey?.trim()
429
530
  ? { key: input.apiKey.trim(), oauth: false }
430
531
  : input.providerId
431
- ? await storedCredentialFor(input.providerId)
532
+ ? await configuredCredentialFor(input.providerId)
432
533
  : undefined;
433
534
  if (credential && !input.apiKey?.trim()) {
434
535
  const rt = await getModelRuntime();
435
536
  const provider = rt.getProviders().find((p) => p.id === input.providerId);
436
- const configuredUrls = [provider?.baseUrl, ...(provider ? rt.getModels(provider.id).map((model) => model.baseUrl) : [])];
537
+ const configuredUrls = [provider?.baseUrl, credential.baseUrl, ...(provider ? rt.getModels(provider.id).map((model) => model.baseUrl) : [])];
437
538
  if (!isTrustedCredentialEndpoint(endpoint, configuredUrls)) {
438
- throw new Error("已存凭证只能用于该 Provider 已配置的 API 地址;新地址请显式填写密钥");
539
+ throw new Error("已配置的凭证只能用于该 Provider 已配置的 API 地址;新地址请显式填写密钥");
439
540
  }
440
541
  }
441
542
  const apiKey = credential?.key;
@@ -484,7 +585,7 @@ export async function discoverModels(input: { baseUrl: string; api?: string; api
484
585
  const id = typeof value.id === "string" ? value.id.trim() : "";
485
586
  if (!id) return null;
486
587
  const name = typeof value.name === "string" && value.name.trim() ? value.name.trim() : undefined;
487
- return { id, ...(name ? { name } : {}) };
588
+ return { id, ...(name ? { name } : {}), ...catalogCapabilities(value) };
488
589
  })
489
590
  .filter((model): model is DiscoveredModel => Boolean(model))
490
591
  .filter((model, index, all) => all.findIndex((item) => item.id === model.id) === index)
@@ -586,17 +687,22 @@ function isoOrUndefined(value: unknown): string | undefined {
586
687
  return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : undefined;
587
688
  }
588
689
 
589
- /** 从 access token 的 JWT 本地读套餐声明:端点不可用时界面仍显示套餐。 */
590
- export function planTypeFromAccessToken(accessToken: string): string | undefined {
690
+ /** ChatGPT access token(JWT)里的 OpenAI 声明:套餐、账号 ID 都在这里 */
691
+ function openaiAuthClaims(accessToken: string): Record<string, any> | undefined {
591
692
  try {
592
- const payload = JSON.parse(Buffer.from(accessToken.split(".")[1] ?? "", "base64").toString("utf8")) as Record<string, any>;
593
- const value = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type;
594
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
693
+ const payload = JSON.parse(Buffer.from(accessToken.split(".")[1] ?? "", "base64").toString("utf8")) as unknown;
694
+ return asRecord(asRecord(payload)?.["https://api.openai.com/auth"]);
595
695
  } catch {
596
696
  return undefined;
597
697
  }
598
698
  }
599
699
 
700
+ /** 从 access token 的 JWT 本地读套餐声明:端点不可用时界面仍显示套餐。 */
701
+ export function planTypeFromAccessToken(accessToken: string): string | undefined {
702
+ const value = openaiAuthClaims(accessToken)?.chatgpt_plan_type;
703
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
704
+ }
705
+
600
706
  /**
601
707
  * /wham/usage 载荷里只取界面需要的部分。窗口组合随套餐不同:常见是 5 小时窗 +
602
708
  * 7 天窗,有的套餐只有 7 天窗,Go 这类只有 30 天窗(secondary_window 直接是
@@ -633,7 +739,7 @@ export function parseCodexUsagePayload(value: unknown): Omit<QuotaUsage, "kind"
633
739
  }
634
740
 
635
741
  async function probeCodexUsage(): Promise<QuotaUsage | null> {
636
- const credential = await storedCredentialFor("openai-codex");
742
+ const credential = await configuredCredentialFor("openai-codex");
637
743
  if (!credential) return null;
638
744
  // JWT 里的套餐声明本地可得,端点失败时用它兜底,界面不至于整块空白
639
745
  const fallbackPlan = planTypeFromAccessToken(credential.key);
@@ -684,7 +790,7 @@ export function parseAnthropicUsagePayload(value: unknown): Omit<QuotaUsage, "ki
684
790
  }
685
791
 
686
792
  async function probeAnthropicUsage(): Promise<QuotaUsage | null> {
687
- const credential = await storedCredentialFor("anthropic");
793
+ const credential = await configuredCredentialFor("anthropic");
688
794
  // 该端点只认订阅 OAuth 令牌;API key 走的是按量计费,没有这组窗口
689
795
  if (!credential?.oauth) return null;
690
796
  const payload = await fetchUsageJson(ANTHROPIC_USAGE_ENDPOINT, {
@@ -757,7 +863,7 @@ const MOONSHOT_HOSTS: Record<string, { base: string; currency: string }> = {
757
863
  };
758
864
 
759
865
  async function probeDeepSeekBalance(): Promise<BalanceUsage | null> {
760
- const credential = await storedCredentialFor("deepseek");
866
+ const credential = await configuredCredentialFor("deepseek");
761
867
  if (!credential) return null;
762
868
  const payload = await fetchUsageJson("https://api.deepseek.com/user/balance", {
763
869
  headers: { Authorization: `Bearer ${credential.key}` },
@@ -767,7 +873,7 @@ async function probeDeepSeekBalance(): Promise<BalanceUsage | null> {
767
873
  }
768
874
 
769
875
  async function probeOpenRouterBalance(): Promise<BalanceUsage | null> {
770
- const credential = await storedCredentialFor("openrouter");
876
+ const credential = await configuredCredentialFor("openrouter");
771
877
  if (!credential) return null;
772
878
  const payload = await fetchUsageJson("https://openrouter.ai/api/v1/key", {
773
879
  headers: { Authorization: `Bearer ${credential.key}` },
@@ -777,7 +883,7 @@ async function probeOpenRouterBalance(): Promise<BalanceUsage | null> {
777
883
  }
778
884
 
779
885
  async function probeMoonshotBalance(providerId: "moonshotai" | "moonshotai-cn"): Promise<BalanceUsage | null> {
780
- const credential = await storedCredentialFor(providerId);
886
+ const credential = await configuredCredentialFor(providerId);
781
887
  if (!credential) return null;
782
888
  const host = MOONSHOT_HOSTS[providerId];
783
889
  const payload = await fetchUsageJson(`${host.base}/v1/users/me/balance`, {
@@ -894,7 +1000,7 @@ export function parseOpencodeUsagePayload(value: unknown): Omit<QuotaUsage, "kin
894
1000
  }
895
1001
 
896
1002
  async function probeKimiCodingUsage(): Promise<QuotaUsage | null> {
897
- const credential = await storedCredentialFor("kimi-coding");
1003
+ const credential = await configuredCredentialFor("kimi-coding");
898
1004
  if (!credential) return null;
899
1005
  const payload = await fetchUsageJson("https://api.kimi.com/coding/v1/usages", {
900
1006
  headers: { Authorization: `Bearer ${credential.key}`, Accept: "application/json" },
@@ -905,7 +1011,7 @@ async function probeKimiCodingUsage(): Promise<QuotaUsage | null> {
905
1011
  }
906
1012
 
907
1013
  async function probeCopilotUsage(): Promise<QuotaUsage | null> {
908
- const credential = await storedCredentialFor("github-copilot");
1014
+ const credential = await configuredCredentialFor("github-copilot");
909
1015
  if (!credential?.oauth) return null;
910
1016
  // 凭据本身就是 Copilot 令牌(tid=...),直接可用于这个内部端点
911
1017
  const payload = await fetchUsageJson("https://api.github.com/copilot_internal/user", {
@@ -917,7 +1023,7 @@ async function probeCopilotUsage(): Promise<QuotaUsage | null> {
917
1023
  }
918
1024
 
919
1025
  async function probeVercelGatewayBalance(): Promise<BalanceUsage | null> {
920
- const credential = await storedCredentialFor("vercel-ai-gateway");
1026
+ const credential = await configuredCredentialFor("vercel-ai-gateway");
921
1027
  if (!credential) return null;
922
1028
  const payload = await fetchUsageJson("https://ai-gateway.vercel.sh/v1/credits", {
923
1029
  headers: { Authorization: `Bearer ${credential.key}` },
@@ -927,7 +1033,7 @@ async function probeVercelGatewayBalance(): Promise<BalanceUsage | null> {
927
1033
  }
928
1034
 
929
1035
  async function probeOpencodeUsage(): Promise<QuotaUsage | null> {
930
- const credential = await storedCredentialFor("opencode-go");
1036
+ const credential = await configuredCredentialFor("opencode-go");
931
1037
  if (!credential) return null;
932
1038
  const payload = await fetchUsageJson("https://opencode.ai/zen/go/v1/usage", {
933
1039
  headers: { Authorization: `Bearer ${credential.key}`, Accept: "application/json" },
@@ -938,7 +1044,7 @@ async function probeOpencodeUsage(): Promise<QuotaUsage | null> {
938
1044
  }
939
1045
 
940
1046
  async function probeXaiUsage(): Promise<XaiUsage | null> {
941
- const credential = await storedCredentialFor("xai");
1047
+ const credential = await configuredCredentialFor("xai");
942
1048
  if (!credential) return null;
943
1049
  const key = credential.key;
944
1050
  let model = "";
@@ -1306,6 +1412,38 @@ async function validateStagedCustomProviders(file: string): Promise<void> {
1306
1412
  if (error) throw new CustomProvidersValidationError(error);
1307
1413
  }
1308
1414
 
1415
+ /**
1416
+ * 校验后原子替换 models.json(调用方已持两把锁):现有文件先过一遍 SDK 校验(坏文件不能当空文件覆盖),
1417
+ * 新内容校验通过、且期间没被别的程序改过才落盘。
1418
+ */
1419
+ async function replaceCustomProvidersFile(
1420
+ file: string,
1421
+ previous: { content: string | undefined; revision: string },
1422
+ next: CustomProvidersFile,
1423
+ prepare?: (old: CustomProvidersFile) => void,
1424
+ ): Promise<void> {
1425
+ const old = parseCustomProviders(previous.content ?? '{"providers":{}}');
1426
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
1427
+ try {
1428
+ if (previous.content !== undefined) {
1429
+ await fs.writeFile(temporary, JSON.stringify(old, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
1430
+ // A damaged existing config is never treated as an empty/missing file.
1431
+ await validateStagedCustomProviders(temporary);
1432
+ }
1433
+ prepare?.(old);
1434
+ validateTokenLimits(next);
1435
+ await fs.writeFile(temporary, JSON.stringify(next, null, 2), {
1436
+ encoding: "utf8", flag: previous.content === undefined ? "wx" : "w", mode: 0o600,
1437
+ });
1438
+ await validateStagedCustomProviders(temporary);
1439
+ // Also detect non-cooperating writers that changed the file during validation.
1440
+ if ((await readCustomProvidersSnapshot(file)).revision !== previous.revision) throw new CustomProvidersConflictError();
1441
+ await fs.rename(temporary, file);
1442
+ } finally {
1443
+ await fs.rm(temporary, { force: true }).catch(() => undefined);
1444
+ }
1445
+ }
1446
+
1309
1447
  export async function writeCustomProviders(content: string, revision?: string): Promise<void> {
1310
1448
  const file = modelsJsonPath();
1311
1449
  await withSettingsWriteLock(file, () => withExternalSettingsLock(file, async () => {
@@ -1313,28 +1451,11 @@ export async function writeCustomProviders(content: string, revision?: string):
1313
1451
  // Compare before parsing or restoring secrets; stale clients must reload, not overwrite.
1314
1452
  if (revision !== undefined && revision !== previous.revision) throw new CustomProvidersConflictError();
1315
1453
  const parsed = parseCustomProviders(content);
1316
- const old = parseCustomProviders(previous.content ?? '{"providers":{}}');
1317
- const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
1318
- try {
1319
- if (previous.content !== undefined) {
1320
- await fs.writeFile(temporary, JSON.stringify(old, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
1321
- // A damaged existing config is never treated as an empty/missing file.
1322
- await validateStagedCustomProviders(temporary);
1323
- }
1454
+ await replaceCustomProvidersFile(file, previous, parsed, (old) => {
1324
1455
  mergeCustomProviderApiKeys(parsed, old);
1325
1456
  // 本地网关无 key 也得给占位值,否则 SDK 判定「未配置」→ 发消息报 No API key
1326
1457
  applyLocalPlaceholderKeys(parsed);
1327
- validateTokenLimits(parsed);
1328
- await fs.writeFile(temporary, JSON.stringify(parsed, null, 2), {
1329
- encoding: "utf8", flag: previous.content === undefined ? "wx" : "w", mode: 0o600,
1330
- });
1331
- await validateStagedCustomProviders(temporary);
1332
- // Also detect non-cooperating writers that changed the file during validation.
1333
- if ((await readCustomProvidersSnapshot(file)).revision !== previous.revision) throw new CustomProvidersConflictError();
1334
- await fs.rename(temporary, file);
1335
- } finally {
1336
- await fs.rm(temporary, { force: true }).catch(() => undefined);
1337
- }
1458
+ });
1338
1459
  }));
1339
1460
  await refreshModelRuntime();
1340
1461
  invalidateModelList();
@@ -1342,3 +1463,44 @@ export async function writeCustomProviders(content: string, revision?: string):
1342
1463
  // 把当前模型对象换成新配置(改了 baseUrl / contextWindow 等字段时生效)
1343
1464
  await reloadSessionsForCwd();
1344
1465
  }
1466
+
1467
+ export interface ContextWindowInfo {
1468
+ /** 当前生效的值(已套用 models.json) */
1469
+ contextWindow: number;
1470
+ /** pi 内置目录里的原值;不在内置目录里的模型(自定义、远程目录、扩展补的)为 null */
1471
+ builtin: number | null;
1472
+ }
1473
+
1474
+ async function contextWindowOf(providerId: string, modelId: string): Promise<ContextWindowInfo> {
1475
+ const model = (await getModelRuntime()).getModel(providerId, modelId) as { contextWindow?: unknown } | undefined;
1476
+ if (!model) throw new CustomProvidersValidationError(`找不到模型:${providerId}/${modelId}`);
1477
+ const builtin = (await getBuiltInCatalog()).get(providerId)?.get(modelId);
1478
+ return { contextWindow: Number(model.contextWindow) || 0, builtin: builtin || null };
1479
+ }
1480
+
1481
+ /** 上下文圆环「修改窗口」用:当前值,加内置目录原值给用户对照(只对照,不替用户决定) */
1482
+ export async function contextWindowInfo(providerId: string, modelId: string): Promise<ContextWindowInfo> {
1483
+ await refreshModelRuntimeIfFilesChanged();
1484
+ return contextWindowOf(providerId, modelId);
1485
+ }
1486
+
1487
+ /**
1488
+ * 只改一个模型的上下文窗口(写在 pi 实际取值的那一层,见 withContextWindow),与设置页共用校验和原子替换。
1489
+ * 不重载会话资源:刷新共享目录后,只给正在用这个模型的会话换上新模型对象并推一次用量,圆环当场更新。
1490
+ */
1491
+ export async function setModelContextWindow(providerId: string, modelId: string, contextWindow: number): Promise<ContextWindowInfo> {
1492
+ if (!Number.isSafeInteger(contextWindow) || contextWindow <= 0) throw new CustomProvidersValidationError("contextWindow 必须是正整数");
1493
+ // 只给目录里有的模型写:未知 provider / ID 不落进配置
1494
+ await contextWindowOf(providerId, modelId);
1495
+ const file = modelsJsonPath();
1496
+ await withSettingsWriteLock(file, () => withExternalSettingsLock(file, async () => {
1497
+ const previous = await readCustomProvidersSnapshot(file);
1498
+ const parsed = parseCustomProviders(previous.content ?? '{"providers":{}}');
1499
+ const providers = withContextWindow(parsed.providers, providerId, modelId, contextWindow) as CustomProvidersFile["providers"];
1500
+ await replaceCustomProvidersFile(file, previous, { ...parsed, providers });
1501
+ }));
1502
+ await refreshModelRuntime();
1503
+ invalidateModelList();
1504
+ refreshSessionModels(providerId, modelId);
1505
+ return contextWindowOf(providerId, modelId);
1506
+ }
package/src/lib/pi.ts CHANGED
@@ -23,6 +23,8 @@ import {
23
23
  loadProjectContextFiles,
24
24
  } from "@earendil-works/pi-coding-agent";
25
25
 
26
+ import { modelFilesFingerprint, reloadStoredCredentials } from "./model-files";
27
+
26
28
  export { createAgentSession, getAgentDir, loadProjectContextFiles, SessionManager };
27
29
 
28
30
  // Child tools inherit these defaults; Node itself already decodes source and JSON as UTF-8.
@@ -30,9 +32,15 @@ process.env.PYTHONUTF8 ??= "1";
30
32
  process.env.PYTHONIOENCODING ??= "utf-8";
31
33
 
32
34
  let modelRuntimePromise: Promise<PiModelRuntime> | null = null;
35
+ /** 共享运行时每创建一次、每刷新一次就加一:模型列表缓存据此作废(见 models-service) */
36
+ let modelRuntimeGeneration = 0;
37
+ /** 上一次加载目录时 models.json / auth.json 的指纹 */
38
+ let modelFilesSeen: string | null = null;
33
39
 
34
40
  export function getModelRuntime(): Promise<PiModelRuntime> {
35
41
  if (!modelRuntimePromise) {
42
+ modelFilesSeen = modelFilesFingerprint(getAgentDir());
43
+ modelRuntimeGeneration += 1;
36
44
  modelRuntimePromise = ModelRuntime.create().catch((err) => {
37
45
  modelRuntimePromise = null;
38
46
  throw err;
@@ -41,22 +49,39 @@ export function getModelRuntime(): Promise<PiModelRuntime> {
41
49
  return modelRuntimePromise;
42
50
  }
43
51
 
52
+ export function modelRuntimeVersion(): number {
53
+ return modelRuntimeGeneration;
54
+ }
55
+
44
56
  /**
45
57
  * models.json 落盘后调用:就地刷新共享的 ModelRuntime,让目录与认证立即生效。
46
58
  * 不能换一个新实例 —— 活跃会话、扩展注册的 provider 都挂在旧实例上,换掉后会话里
47
59
  * 选新 provider 的模型会报 "No API key",扩展 provider 也会从目录里消失。
48
- * 只读本地配置不联网;刷新失败才退回整体重建。
60
+ * 只读本地配置不联网;刷新失败才退回整体重建。手改的 auth.json 也在这里重读(SDK 的存储会缓存它)。
49
61
  */
50
62
  export async function refreshModelRuntime(): Promise<void> {
51
63
  const pending = modelRuntimePromise;
52
64
  if (!pending) return;
65
+ // 先记指纹再刷新:刷新期间又改的文件,下一次检查还能发现
66
+ modelFilesSeen = modelFilesFingerprint(getAgentDir());
67
+ modelRuntimeGeneration += 1;
53
68
  try {
54
- await (await pending).refresh({ allowNetwork: false });
69
+ const runtime = await pending;
70
+ reloadStoredCredentials(runtime);
71
+ await runtime.refresh({ allowNetwork: false });
55
72
  } catch {
56
73
  if (modelRuntimePromise === pending) modelRuntimePromise = null;
57
74
  }
58
75
  }
59
76
 
77
+ /** 读目录的入口调用:models.json / auth.json 的指纹变了才刷新(两次 stat,不读内容)。返回是否刷新过。 */
78
+ export async function refreshModelRuntimeIfFilesChanged(): Promise<boolean> {
79
+ if (!modelRuntimePromise) return false;
80
+ if (modelFilesFingerprint(getAgentDir()) === modelFilesSeen) return false;
81
+ await refreshModelRuntime();
82
+ return true;
83
+ }
84
+
60
85
  const settingsCache = new Map<string, SettingsManager>();
61
86
  /** 缓存键 → 原始目录。键在 Windows 上是小写的,不能拿它当目录交给 SDK 或算资源指纹 */
62
87
  const settingsCwds = new Map<string, string>();
@@ -86,11 +86,12 @@ export function classifyMessageChars(
86
86
  if (content.type === "text") {
87
87
  const chars = content.text?.length ?? 0;
88
88
  if (message.role === "user") out.user += chars;
89
- else out.agentText += chars;
89
+ else if (message.role === "toolResult") out.toolOutput += chars;
90
+ else if (message.role !== "system") out.agentText += chars;
90
91
  } else if (content.type === "thinking") {
91
92
  out.agentThinking += content.thinking?.length ?? 0;
92
93
  } else if (content.type === "toolCall") {
93
- out.agentToolCall += JSON.stringify(content.arguments ?? "").length;
94
+ out.agentToolCall += ((content as { name?: string }).name?.length ?? 0) + JSON.stringify(content.arguments ?? "").length;
94
95
  } else if (content.type === "toolResult") {
95
96
  out.toolOutput += (content as { text?: string }).text?.length ?? 0;
96
97
  }
@@ -0,0 +1,31 @@
1
+ import { calculateContextTokens, estimateTokens, type SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import type { WebSnapshot } from "../types";
3
+
4
+ /** 冷会话沿用 SDK 的投影口径:API usage + 尾部估算;压缩 / 编辑后不信旧 usage。 */
5
+ export function projectedContextUsage(
6
+ projection: ReturnType<SessionManager["buildSessionProjection"]>,
7
+ branch: ReturnType<SessionManager["getBranch"]>,
8
+ contextWindow: number,
9
+ ): WebSnapshot["contextUsage"] {
10
+ if (contextWindow <= 0) return null;
11
+ let lastUsage: { tokens: number; messageIndex: number; branchIndex: number } | undefined;
12
+ let messageIndex = 0;
13
+ for (const entry of projection.entries) {
14
+ for (const message of entry.messages) {
15
+ if (message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted" && message.usage) {
16
+ const tokens = calculateContextTokens(message.usage);
17
+ if (tokens > 0) lastUsage = { tokens, messageIndex, branchIndex: branch.findIndex((raw) => raw.id === entry.sourceEntry.id) };
18
+ }
19
+ messageIndex++;
20
+ }
21
+ }
22
+ const compactionIndex = branch.findLastIndex((entry) => entry.type === "compaction");
23
+ if (compactionIndex >= 0 && (!lastUsage || lastUsage.branchIndex <= compactionIndex)) {
24
+ return { tokens: null, contextWindow, percent: null };
25
+ }
26
+ const invalidatingIndex = branch.findLastIndex((entry) => entry.type === "compaction" || entry.type === "context_edit");
27
+ const tokens = lastUsage && lastUsage.branchIndex > invalidatingIndex
28
+ ? lastUsage.tokens + projection.messages.slice(lastUsage.messageIndex + 1).reduce((sum, message) => sum + estimateTokens(message), 0)
29
+ : projection.messages.reduce((sum, message) => sum + estimateTokens(message), 0);
30
+ return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
31
+ }