@geminilight/mindos 0.6.63 → 0.6.65

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 (268) hide show
  1. package/_standalone/.mindos-build-version +1 -1
  2. package/_standalone/.next/BUILD_ID +1 -1
  3. package/_standalone/.next/app-path-routes-manifest.json +21 -21
  4. package/_standalone/.next/build-manifest.json +2 -2
  5. package/_standalone/.next/cache/.previewinfo +1 -1
  6. package/_standalone/.next/cache/.rscinfo +1 -1
  7. package/_standalone/.next/cache/config.json +3 -3
  8. package/_standalone/.next/prerender-manifest.json +3 -3
  9. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  10. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/_standalone/.next/server/app/_global-error.html +2 -2
  12. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  13. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  20. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  23. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  25. package/_standalone/.next/server/app/agents/page.js +1 -1
  26. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  27. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  28. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  29. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  38. package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
  39. package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
  40. package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
  41. package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
  42. package/_standalone/.next/server/app/api/ask/route.js +48 -42
  43. package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
  44. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  45. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  46. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  47. package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
  48. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  49. package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
  50. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  51. package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
  52. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  53. package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
  54. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  55. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  56. package/_standalone/.next/server/app/api/file/import/route.js +1 -1
  57. package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
  58. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  59. package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
  60. package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
  61. package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
  62. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  63. package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  64. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  65. package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  66. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  67. package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
  68. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  69. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
  71. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
  73. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  74. package/_standalone/.next/server/app/api/mcp/agents/route.js +1 -1
  75. package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
  76. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  78. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  79. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  80. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  81. package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
  82. package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
  83. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  84. package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
  85. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  86. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  87. package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  88. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  89. package/_standalone/.next/server/app/api/settings/list-models/route.js +1 -1
  90. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  91. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  92. package/_standalone/.next/server/app/api/settings/route.js +1 -1
  93. package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  94. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/api/settings/test-key/route.js +1 -1
  96. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  97. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  98. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  99. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  100. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/api/setup/route.js +1 -1
  102. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  103. package/_standalone/.next/server/app/api/skills/route.js +1 -1
  104. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  105. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  106. package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
  107. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  108. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  109. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  110. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  111. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  112. package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
  113. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  114. package/_standalone/.next/server/app/changes/page.js +1 -1
  115. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  116. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  117. package/_standalone/.next/server/app/echo/[segment]/page.js +2 -2
  118. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  119. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  120. package/_standalone/.next/server/app/echo/page.js +1 -1
  121. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  122. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  123. package/_standalone/.next/server/app/explore/page.js +1 -1
  124. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  125. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  126. package/_standalone/.next/server/app/help/page.js +1 -1
  127. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  128. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  129. package/_standalone/.next/server/app/inbox/history/page.js +1 -1
  130. package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
  131. package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
  132. package/_standalone/.next/server/app/login/page.js +1 -1
  133. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  134. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  135. package/_standalone/.next/server/app/page.js +1 -1
  136. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  137. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  138. package/_standalone/.next/server/app/setup/page.js +2 -2
  139. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  140. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  141. package/_standalone/.next/server/app/trash/page.js +3 -3
  142. package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
  143. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  144. package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
  145. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  146. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  147. package/_standalone/.next/server/app/wiki/page.js +1 -1
  148. package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
  149. package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
  150. package/_standalone/.next/server/app-paths-manifest.json +21 -21
  151. package/_standalone/.next/server/chunks/122.js +222 -0
  152. package/_standalone/.next/server/chunks/3113.js +52 -0
  153. package/_standalone/.next/server/chunks/6539.js +1 -1
  154. package/_standalone/.next/server/chunks/8388.js +2 -2
  155. package/_standalone/.next/server/chunks/953.js +3 -3
  156. package/_standalone/.next/server/chunks/9787.js +2 -0
  157. package/_standalone/.next/server/pages/500.html +2 -2
  158. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  159. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  160. package/_standalone/.next/static/chunks/1001-99da82ec8d8c136f.js +1 -0
  161. package/_standalone/.next/static/chunks/5149-4d828886dda479fa.js +1 -0
  162. package/_standalone/.next/static/chunks/{5581-82e5db227f8e9393.js → 5581-c671163a2fe1b312.js} +2 -2
  163. package/_standalone/.next/static/chunks/6636-53238eff89503f03.js +6 -0
  164. package/_standalone/.next/static/chunks/6757-1c1a89720fdda8f0.js +1 -0
  165. package/_standalone/.next/static/chunks/7129-20e9d2463a9da646.js +1 -0
  166. package/_standalone/.next/static/chunks/{3674-be69a8b858ceacdd.js → 7294-cac25d97869afadc.js} +1 -1
  167. package/_standalone/.next/static/chunks/8225-21e5cebc3731ddf0.js +1 -0
  168. package/_standalone/.next/static/chunks/8520-b51810e66293ceb8.js +22 -0
  169. package/_standalone/.next/static/chunks/9207-dc9c31b351a2ed78.js +1 -0
  170. package/_standalone/.next/static/chunks/app/agents/[agentKey]/{page-b0dabe793500383d.js → page-2f5cf97e03dc1cc9.js} +1 -1
  171. package/_standalone/.next/static/chunks/app/agents/{page-1f1ac330c8177cf6.js → page-50eac58d511dcc6e.js} +1 -1
  172. package/_standalone/.next/static/chunks/app/echo/[segment]/page-2a00f4686adf3885.js +11 -0
  173. package/_standalone/.next/static/chunks/app/{layout-50a6b1164ee98ab9.js → layout-2cb7a6602d2e5d5f.js} +62 -58
  174. package/_standalone/.next/static/chunks/app/{page-73802bd31d7f6c9f.js → page-5ab911b2226f6ff7.js} +1 -1
  175. package/_standalone/.next/static/chunks/app/setup/page-907b7c57fad2292b.js +1 -0
  176. package/_standalone/.next/static/chunks/app/trash/page-11a511b065ea84c2.js +1 -0
  177. package/_standalone/.next/static/chunks/app/view/[...path]/{page-808f39963bf04715.js → page-26e47dd4c533a58c.js} +2 -2
  178. package/_standalone/.next/static/css/67e7918f5ed7d147.css +1 -0
  179. package/_standalone/.next/trace +65 -65
  180. package/_standalone/__tests__/api/ask-attachments.test.ts +194 -0
  181. package/_standalone/__tests__/api/settings.test.ts +16 -12
  182. package/_standalone/__tests__/api/setup.test.ts +11 -9
  183. package/_standalone/__tests__/api/test-key.test.ts +0 -10
  184. package/_standalone/__tests__/components/UpdateToast.test.ts +344 -0
  185. package/_standalone/__tests__/core/context.test.ts +48 -426
  186. package/_standalone/__tests__/lib/pi-skills.test.ts +4 -4
  187. package/_standalone/__tests__/lib/settings-ai-client.test.ts +32 -12
  188. package/_standalone/__tests__/setup.ts +5 -5
  189. package/_standalone/components/ask/AskContent.tsx +70 -40
  190. package/_standalone/components/ask/AskHeader.tsx +8 -1
  191. package/_standalone/components/ask/MessageList.tsx +37 -3
  192. package/_standalone/components/ask/ProviderModelCapsule.tsx +51 -129
  193. package/_standalone/components/settings/AiTab.tsx +270 -347
  194. package/_standalone/components/settings/CustomProviderFields.tsx +121 -0
  195. package/_standalone/components/settings/CustomProvidersCard.tsx +2 -2
  196. package/_standalone/components/settings/KnowledgeTab.tsx +6 -20
  197. package/_standalone/components/settings/McpAgentInstall.tsx +7 -2
  198. package/_standalone/components/settings/Primitives.tsx +48 -104
  199. package/_standalone/components/settings/ProviderModal.tsx +38 -221
  200. package/_standalone/components/settings/SettingsContent.tsx +5 -12
  201. package/_standalone/components/settings/TestButton.tsx +64 -0
  202. package/_standalone/components/settings/types.ts +3 -12
  203. package/_standalone/components/settings/useCustomProviderForm.ts +132 -0
  204. package/_standalone/components/setup/StepAI.tsx +3 -3
  205. package/_standalone/components/shared/ModelInput.tsx +18 -4
  206. package/_standalone/components/shared/ProviderSelect.tsx +126 -134
  207. package/_standalone/hooks/useAskChat.ts +97 -13
  208. package/_standalone/hooks/useAskPanel.ts +17 -1
  209. package/_standalone/lib/settings-ai-client.ts +17 -8
  210. package/_standalone/tsconfig.tsbuildinfo +1 -1
  211. package/app/app/api/ask/route.ts +124 -44
  212. package/app/app/api/mcp/agents/route.ts +3 -3
  213. package/app/app/api/settings/list-models/route.ts +15 -26
  214. package/app/app/api/settings/route.ts +14 -59
  215. package/app/app/api/settings/test-key/route.ts +47 -12
  216. package/app/app/api/setup/route.ts +36 -18
  217. package/app/app/api/skills/route.ts +1 -1
  218. package/app/app/layout.tsx +5 -3
  219. package/app/components/HomeContent.tsx +11 -0
  220. package/app/components/UpdateToast.tsx +255 -0
  221. package/app/components/ask/AskContent.tsx +70 -40
  222. package/app/components/ask/AskHeader.tsx +8 -1
  223. package/app/components/ask/MessageList.tsx +37 -3
  224. package/app/components/ask/ProviderModelCapsule.tsx +51 -129
  225. package/app/components/settings/AiTab.tsx +270 -347
  226. package/app/components/settings/CustomProviderFields.tsx +121 -0
  227. package/app/components/settings/CustomProvidersCard.tsx +2 -2
  228. package/app/components/settings/KnowledgeTab.tsx +6 -20
  229. package/app/components/settings/McpAgentInstall.tsx +7 -2
  230. package/app/components/settings/Primitives.tsx +48 -104
  231. package/app/components/settings/ProviderModal.tsx +38 -221
  232. package/app/components/settings/SettingsContent.tsx +5 -12
  233. package/app/components/settings/TestButton.tsx +64 -0
  234. package/app/components/settings/types.ts +3 -12
  235. package/app/components/settings/useCustomProviderForm.ts +132 -0
  236. package/app/components/setup/StepAI.tsx +3 -3
  237. package/app/components/shared/ModelInput.tsx +18 -4
  238. package/app/components/shared/ProviderSelect.tsx +126 -134
  239. package/app/hooks/useAskChat.ts +97 -13
  240. package/app/hooks/useAskPanel.ts +17 -1
  241. package/app/lib/agent/context.ts +65 -0
  242. package/app/lib/agent/providers.ts +25 -0
  243. package/app/lib/agent/tools.ts +1 -1
  244. package/app/lib/custom-endpoints.ts +129 -29
  245. package/app/lib/i18n/modules/settings.ts +20 -0
  246. package/app/lib/pi-integration/skills.ts +16 -4
  247. package/app/lib/settings-ai-client.ts +17 -8
  248. package/app/lib/settings.ts +64 -90
  249. package/app/lib/types.ts +4 -0
  250. package/package.json +1 -1
  251. package/_standalone/.next/server/chunks/530.js +0 -218
  252. package/_standalone/.next/server/chunks/9007.js +0 -2
  253. package/_standalone/.next/server/chunks/9137.js +0 -52
  254. package/_standalone/.next/static/chunks/1309-373ade1b40aea186.js +0 -1
  255. package/_standalone/.next/static/chunks/3165-9189a38fd9ebf6f2.js +0 -1
  256. package/_standalone/.next/static/chunks/4587-5d06728133fff222.js +0 -1
  257. package/_standalone/.next/static/chunks/6261-5ce86db54b19ae46.js +0 -1
  258. package/_standalone/.next/static/chunks/6636-9bbc90fb3b8731fe.js +0 -6
  259. package/_standalone/.next/static/chunks/7637-904b0a381dc3ec02.js +0 -1
  260. package/_standalone/.next/static/chunks/8520-84e607f33c409f91.js +0 -22
  261. package/_standalone/.next/static/chunks/9207-9a4a1a1ede4f8e6e.js +0 -1
  262. package/_standalone/.next/static/chunks/app/echo/[segment]/page-bc5e104eb7ae6327.js +0 -11
  263. package/_standalone/.next/static/chunks/app/setup/page-79acb0baf38184c6.js +0 -1
  264. package/_standalone/.next/static/chunks/app/trash/page-d040db56863da504.js +0 -1
  265. package/_standalone/.next/static/css/1287672978833d07.css +0 -1
  266. package/_standalone/lib/agent/context.ts +0 -403
  267. /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_buildManifest.js +0 -0
  268. /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_ssgManifest.js +0 -0
@@ -24,6 +24,7 @@ import { isProviderId, type ProviderId, toPiProvider } from '@/lib/agent/provide
24
24
  import { getRequestScopedTools, getOrganizeTools, getChatTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
25
25
  import { isCustomProviderId, findCustomProvider } from '@/lib/custom-endpoints';
26
26
  import { AGENT_SYSTEM_PROMPT, ORGANIZE_SYSTEM_PROMPT, CHAT_SYSTEM_PROMPT } from '@/lib/agent/prompt';
27
+ import { estimateStringTokens, getOllamaContextWindow } from '@/lib/agent/context';
27
28
  import type { AskModeApi } from '@/lib/types';
28
29
  import { toAgentMessages } from '@/lib/agent/to-agent-messages';
29
30
  import { logAgentOp } from '@/lib/agent/log';
@@ -39,6 +40,48 @@ import type { Message as FrontendMessage } from '@/lib/types';
39
40
 
40
41
  const MAX_DIR_FILES = 30;
41
42
 
43
+ /**
44
+ * Load attached and current files into context parts for the system prompt.
45
+ * Returns the context parts array and a list of file paths that failed to load.
46
+ * Deduplicates files and logs failures with the given mode label.
47
+ */
48
+ function loadAttachedFileContext(
49
+ attachedFiles: string[] | undefined,
50
+ currentFile: string | undefined,
51
+ mode: string,
52
+ ): { contextParts: string[]; failedFiles: string[] } {
53
+ const contextParts: string[] = [];
54
+ const failedFiles: string[] = [];
55
+ const seen = new Set<string>();
56
+
57
+ if (Array.isArray(attachedFiles) && attachedFiles.length > 0) {
58
+ for (const filePath of attachedFiles) {
59
+ if (seen.has(filePath)) continue;
60
+ seen.add(filePath);
61
+ try {
62
+ const content = truncate(getFileContent(filePath));
63
+ contextParts.push(`## Attached: ${filePath}\n\n${content}`);
64
+ } catch (err) {
65
+ console.warn(`[ask] ${mode}: failed to read attached file "${filePath}":`, err instanceof Error ? err.message : err);
66
+ failedFiles.push(filePath);
67
+ }
68
+ }
69
+ }
70
+
71
+ if (currentFile && !seen.has(currentFile)) {
72
+ seen.add(currentFile);
73
+ try {
74
+ const content = truncate(getFileContent(currentFile));
75
+ contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
76
+ } catch (err) {
77
+ console.warn(`[ask] ${mode}: failed to read currentFile "${currentFile}":`, err instanceof Error ? err.message : err);
78
+ failedFiles.push(currentFile);
79
+ }
80
+ }
81
+
82
+ return { contextParts, failedFiles };
83
+ }
84
+
42
85
  /** Expand attachedFiles entries: directory paths (trailing /) become individual file paths. */
43
86
  function expandAttachedFiles(raw: string[]): string[] {
44
87
  const result: string[] = [];
@@ -722,6 +765,11 @@ export async function POST(req: NextRequest) {
722
765
  : body.mode === 'chat' ? 'chat'
723
766
  : 'agent';
724
767
 
768
+ // Diagnostic: log attached files so silent failures are visible
769
+ if (Array.isArray(attachedFiles) && attachedFiles.length > 0) {
770
+ console.log(`[ask] mode=${askMode} attachedFiles=${JSON.stringify(attachedFiles)} currentFile=${currentFile ?? 'none'}`);
771
+ }
772
+
725
773
  // Read agent config from settings
726
774
  const serverSettings = readSettings();
727
775
  const agentConfig = serverSettings.agent ?? {};
@@ -758,7 +806,7 @@ export async function POST(req: NextRequest) {
758
806
  let systemPrompt: string;
759
807
 
760
808
  if (askMode === 'organize') {
761
- // Organize mode: minimal prompt — only KB structure + uploaded files
809
+ // Organize mode: minimal prompt — only KB structure + attached/uploaded files
762
810
  const promptParts: string[] = [ORGANIZE_SYSTEM_PROMPT];
763
811
 
764
812
  promptParts.push(`---\n\nmind_root=${getMindRoot()}`);
@@ -769,6 +817,15 @@ export async function POST(req: NextRequest) {
769
817
  promptParts.push(`---\n\n## Knowledge Base Structure\n\n${bootstrapIndex.content}`);
770
818
  }
771
819
 
820
+ // Include attached KB files (@ mentions) — same pattern as chat/agent modes
821
+ const { contextParts, failedFiles } = loadAttachedFileContext(attachedFiles, currentFile, 'organize');
822
+ if (contextParts.length > 0) {
823
+ promptParts.push(`---\n\nThe user is currently viewing these files:\n\n${contextParts.join('\n\n---\n\n')}`);
824
+ }
825
+ if (failedFiles.length > 0) {
826
+ promptParts.push(`---\n\n⚠️ The following attached files could not be read: ${failedFiles.join(', ')}. Inform the user that these files were not loaded.`);
827
+ }
828
+
772
829
  if (uploadedParts.length > 0) {
773
830
  promptParts.push(
774
831
  `---\n\n## ⚠️ USER-UPLOADED FILES\n\n` +
@@ -794,28 +851,13 @@ export async function POST(req: NextRequest) {
794
851
  const now = new Date();
795
852
  promptParts.push(`---\n\n## Current Time Context\n- Current UTC Time: ${now.toISOString()}\n- System Local Time: ${new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeStyle: 'long' }).format(now)}`);
796
853
 
797
- const contextParts: string[] = [];
798
- const seen = new Set<string>();
799
- if (Array.isArray(attachedFiles) && attachedFiles.length > 0) {
800
- for (const filePath of attachedFiles!) {
801
- if (seen.has(filePath)) continue;
802
- seen.add(filePath);
803
- try {
804
- const content = truncate(getFileContent(filePath));
805
- contextParts.push(`## Attached: ${filePath}\n\n${content}`);
806
- } catch { /* ignore missing files */ }
807
- }
808
- }
809
- if (currentFile && !seen.has(currentFile)) {
810
- seen.add(currentFile);
811
- try {
812
- const content = truncate(getFileContent(currentFile));
813
- contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
814
- } catch { /* ignore */ }
815
- }
854
+ const { contextParts, failedFiles } = loadAttachedFileContext(attachedFiles, currentFile, 'chat');
816
855
  if (contextParts.length > 0) {
817
856
  promptParts.push(`---\n\nThe user is currently viewing these files:\n\n${contextParts.join('\n\n---\n\n')}`);
818
857
  }
858
+ if (failedFiles.length > 0) {
859
+ promptParts.push(`---\n\n⚠️ The following attached files could not be read: ${failedFiles.join(', ')}. Inform the user that these files were not loaded.`);
860
+ }
819
861
 
820
862
  if (uploadedParts.length > 0) {
821
863
  promptParts.push(
@@ -927,28 +969,7 @@ export async function POST(req: NextRequest) {
927
969
  if (bootstrap.target_config_json?.ok) initContextBlocks.push(`## bootstrap_target_config_json\n\n${bootstrap.target_config_json.content}`);
928
970
 
929
971
  // Build initial context from attached/current files
930
- const contextParts: string[] = [];
931
- const seen = new Set<string>();
932
- const hasAttached = Array.isArray(attachedFiles) && attachedFiles.length > 0;
933
-
934
- if (hasAttached) {
935
- for (const filePath of attachedFiles!) {
936
- if (seen.has(filePath)) continue;
937
- seen.add(filePath);
938
- try {
939
- const content = truncate(getFileContent(filePath));
940
- contextParts.push(`## Attached: ${filePath}\n\n${content}`);
941
- } catch { /* ignore missing files */ }
942
- }
943
- }
944
-
945
- if (currentFile && !seen.has(currentFile)) {
946
- seen.add(currentFile);
947
- try {
948
- const content = truncate(getFileContent(currentFile));
949
- contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
950
- } catch { /* ignore */ }
951
- }
972
+ const { contextParts, failedFiles } = loadAttachedFileContext(attachedFiles, currentFile, 'agent');
952
973
 
953
974
  const now = new Date();
954
975
  const timeContext = `## Current Time Context
@@ -973,6 +994,9 @@ export async function POST(req: NextRequest) {
973
994
  if (contextParts.length > 0) {
974
995
  promptParts.push(`---\n\nThe user is currently viewing these files:\n\n${contextParts.join('\n\n---\n\n')}`);
975
996
  }
997
+ if (failedFiles.length > 0) {
998
+ promptParts.push(`---\n\n⚠️ The following attached files could not be read: ${failedFiles.join(', ')}. Inform the user that these files were not loaded.`);
999
+ }
976
1000
 
977
1001
  if (uploadedParts.length > 0) {
978
1002
  promptParts.push(
@@ -987,6 +1011,9 @@ export async function POST(req: NextRequest) {
987
1011
  systemPrompt = promptParts.join('\n\n');
988
1012
  }
989
1013
 
1014
+ // Log system prompt size for diagnosing context truncation issues (e.g. Ollama)
1015
+ console.log(`[ask] mode=${askMode} systemPrompt=${systemPrompt.length} chars (~${Math.ceil(systemPrompt.length / 4)} tokens)`);
1016
+
990
1017
  try {
991
1018
  let provOverride: ProviderId | undefined;
992
1019
  let customProviderConfig: { apiKey: string; model: string; baseUrl: string } | undefined;
@@ -995,11 +1022,11 @@ export async function POST(req: NextRequest) {
995
1022
  if (body.providerOverride) {
996
1023
  if (isCustomProviderId(body.providerOverride)) {
997
1024
  const settings = readSettings();
998
- const customProvider = findCustomProvider(settings.customProviders ?? [], body.providerOverride);
1025
+ const customProvider = findCustomProvider(settings.ai.providers ?? [], body.providerOverride);
999
1026
  if (!customProvider) {
1000
1027
  return apiError(ErrorCodes.INVALID_REQUEST, 'Custom provider not found', 400);
1001
1028
  }
1002
- provOverride = customProvider.baseProviderId;
1029
+ provOverride = customProvider.protocol;
1003
1030
  customProviderConfig = {
1004
1031
  apiKey: customProvider.apiKey,
1005
1032
  model: customProvider.model,
@@ -1022,6 +1049,59 @@ export async function POST(req: NextRequest) {
1022
1049
  hasImages: hasImages(messages),
1023
1050
  });
1024
1051
 
1052
+ // ── Ollama context window guard ──
1053
+ // Ollama silently truncates input that exceeds the model's actual context window.
1054
+ // Detect this and compact the system prompt to prevent attached files from being dropped.
1055
+ if (provider === 'ollama') {
1056
+ const ollamaBase = baseUrl || 'http://localhost:11434/v1';
1057
+ const actualCtx = await getOllamaContextWindow(ollamaBase, modelName);
1058
+ const promptTokens = estimateStringTokens(systemPrompt);
1059
+ // Reserve ~30% of context for conversation history + model output
1060
+ const maxPromptTokens = actualCtx ? Math.floor(actualCtx * 0.7) : undefined;
1061
+
1062
+ if (actualCtx) {
1063
+ console.log(`[ask] Ollama model="${modelName}" context=${actualCtx} promptTokens=${promptTokens} maxPromptTokens=${maxPromptTokens}`);
1064
+ }
1065
+
1066
+ if (maxPromptTokens && promptTokens > maxPromptTokens) {
1067
+ console.warn(`[ask] Ollama context overflow: prompt ${promptTokens} tokens > ${maxPromptTokens} max (${actualCtx} ctx). Compacting...`);
1068
+ // Compact by progressively stripping lower-priority sections from system prompt.
1069
+ // Priority order (keep first, strip last):
1070
+ // 1. Core system prompt (AGENT/CHAT/ORGANIZE base) — must keep
1071
+ // 2. Attached/current file content — user explicitly requested these
1072
+ // 3. KB structure (README.md) — important for navigation
1073
+ // 4. Time context — low priority
1074
+ // 5. SKILL.md + write-supplement — largest sections, can be stripped
1075
+ // 6. bootstrap INSTRUCTION/CONFIG — can be stripped for local models
1076
+
1077
+ // Strategy: strip sections between "---" delimiters from the end,
1078
+ // but preserve sections containing "Attached:" or "Current file:" or "USER-UPLOADED"
1079
+ const sections = systemPrompt.split('\n\n---\n\n');
1080
+ const preserved: string[] = [];
1081
+ let currentTokens = 0;
1082
+
1083
+ for (const section of sections) {
1084
+ const sectionTokens = estimateStringTokens(section);
1085
+ const isAttachment = section.includes('## Attached:') || section.includes('## Current file:') || section.includes('USER-UPLOADED');
1086
+ const isCore = preserved.length === 0; // first section = base system prompt
1087
+
1088
+ if (isCore || isAttachment) {
1089
+ // Always keep core prompt and user attachments
1090
+ preserved.push(section);
1091
+ currentTokens += sectionTokens;
1092
+ } else if (currentTokens + sectionTokens <= maxPromptTokens) {
1093
+ preserved.push(section);
1094
+ currentTokens += sectionTokens;
1095
+ } else {
1096
+ console.log(`[ask] Ollama compact: stripping section (${sectionTokens} tokens): ${section.slice(0, 80)}...`);
1097
+ }
1098
+ }
1099
+
1100
+ systemPrompt = preserved.join('\n\n---\n\n');
1101
+ console.log(`[ask] Ollama compacted: ${promptTokens} → ${estimateStringTokens(systemPrompt)} tokens`);
1102
+ }
1103
+ }
1104
+
1025
1105
  // Convert frontend messages to AgentMessage[]
1026
1106
  const agentMessages = toAgentMessages(messages);
1027
1107
 
@@ -20,7 +20,7 @@ import { readSettings } from '@/lib/settings';
20
20
  import { scanSkillDirs } from '@/lib/pi-integration/skills';
21
21
  import { getMindRoot } from '@/lib/fs';
22
22
 
23
- function enrichMindOsAgent(agent: Record<string, unknown>) {
23
+ async function enrichMindOsAgent(agent: Record<string, unknown>) {
24
24
  agent.present = true;
25
25
  agent.installed = true;
26
26
  agent.scope = 'builtin';
@@ -35,7 +35,7 @@ function enrichMindOsAgent(agent: Record<string, unknown>) {
35
35
 
36
36
  try {
37
37
  const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
38
- const skills = scanSkillDirs({ projectRoot, mindRoot: getMindRoot() });
38
+ const skills = await scanSkillDirs({ projectRoot, mindRoot: getMindRoot() });
39
39
  const enabledSkills = skills.filter(s => s.enabled);
40
40
  agent.installedSkillNames = enabledSkills.map(s => s.name);
41
41
  agent.installedSkillCount = enabledSkills.length;
@@ -217,7 +217,7 @@ export async function GET() {
217
217
  });
218
218
 
219
219
  const mindos = agents.find(a => a.key === 'mindos');
220
- if (mindos) enrichMindOsAgent(mindos as unknown as Record<string, unknown>);
220
+ if (mindos) await enrichMindOsAgent(mindos as unknown as Record<string, unknown>);
221
221
 
222
222
  // Runtime verification: for agents marked as installed with HTTP endpoint,
223
223
  // verify endpoint is reachable (1s timeout to avoid blocking)
@@ -1,45 +1,34 @@
1
1
  export const dynamic = 'force-dynamic';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { getModels as piGetModels } from '@mariozechner/pi-ai';
4
- import { effectiveAiConfig } from '@/lib/settings';
4
+ import { effectiveAiConfig, readSettings } from '@/lib/settings';
5
5
  import { type ProviderId, isProviderId, PROVIDER_PRESETS, toPiProvider, getDefaultBaseUrl } from '@/lib/agent/providers';
6
- import { isCustomProviderId, parseCustomProviders } from '@/lib/custom-endpoints';
6
+ import { isProviderEntryId, findProvider } from '@/lib/custom-endpoints';
7
7
 
8
8
  const TIMEOUT = 10_000;
9
9
 
10
10
  export async function POST(req: NextRequest) {
11
11
  try {
12
12
  const body = await req.json();
13
- const { provider, customProviderId, apiKey, baseUrl } = body as {
13
+ const { provider, apiKey, baseUrl } = body as {
14
14
  provider?: string;
15
- customProviderId?: string;
16
15
  apiKey?: string;
17
16
  baseUrl?: string;
18
17
  };
19
18
 
20
- // Handle custom provider
21
- if (customProviderId) {
22
- if (!isCustomProviderId(customProviderId)) {
23
- return NextResponse.json({ ok: false, error: 'Invalid custom provider ID' }, { status: 400 });
19
+ // Handle provider entry ID (p_*) — look up from unified providers list
20
+ if (provider && isProviderEntryId(provider)) {
21
+ const settings = readSettings();
22
+ const entry = findProvider(settings.ai.providers, provider);
23
+ if (!entry) {
24
+ return NextResponse.json({ ok: false, error: 'Provider not found' }, { status: 404 });
24
25
  }
25
-
26
- // Fetch custom provider from settings
27
- const settings = await fetch(new URL('/api/settings', req.url), {
28
- headers: req.headers,
29
- }).then(r => r.json() as Promise<any>);
30
-
31
- const customProviders = parseCustomProviders(settings.customProviders);
32
- const cp = customProviders.find(p => p.id === customProviderId);
33
- if (!cp) {
34
- return NextResponse.json({ ok: false, error: 'Custom provider not found' }, { status: 404 });
35
- }
36
-
37
- // Use the custom provider's base provider type to fetch models
26
+
38
27
  const ctrl = new AbortController();
39
28
  const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
40
-
29
+
41
30
  try {
42
- const models = await fetchModels(cp.baseProviderId as ProviderId, cp.apiKey, cp.baseUrl, ctrl.signal);
31
+ const models = await fetchModels(entry.protocol, apiKey || entry.apiKey, baseUrl || entry.baseUrl, ctrl.signal);
43
32
  return NextResponse.json({ ok: true, models });
44
33
  } catch (e: unknown) {
45
34
  if (e instanceof Error && e.name === 'AbortError') {
@@ -51,7 +40,7 @@ export async function POST(req: NextRequest) {
51
40
  }
52
41
  }
53
42
 
54
- // Handle built-in provider
43
+ // Handle built-in protocol ID (openai, anthropic, etc.)
55
44
  if (!provider || !isProviderId(provider)) {
56
45
  return NextResponse.json({ ok: false, error: 'Invalid provider' }, { status: 400 });
57
46
  }
@@ -64,9 +53,9 @@ export async function POST(req: NextRequest) {
64
53
  return NextResponse.json({ ok: true, models });
65
54
  }
66
55
 
67
- const cfg = effectiveAiConfig(provider as ProviderId);
56
+ const cfg = effectiveAiConfig();
68
57
  let resolvedKey = apiKey || '';
69
- if (!resolvedKey || resolvedKey === '***set***') {
58
+ if (!resolvedKey) {
70
59
  resolvedKey = cfg.apiKey;
71
60
  }
72
61
 
@@ -2,8 +2,8 @@ export const dynamic = 'force-dynamic';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { readSettings, writeSettings, ServerSettings } from '@/lib/settings';
4
4
  import { invalidateCache } from '@/lib/fs';
5
- import { PROVIDER_PRESETS, ALL_PROVIDER_IDS, getApiKeyEnvVar, getApiKeyFromEnv } from '@/lib/agent/providers';
6
- import { maskCustomProviderKey, parseCustomProviders, type CustomProvider } from '@/lib/custom-endpoints';
5
+ import { ALL_PROVIDER_IDS, getApiKeyEnvVar, getApiKeyFromEnv } from '@/lib/agent/providers';
6
+ import { parseProviders } from '@/lib/custom-endpoints';
7
7
 
8
8
  function maskToken(token: string | undefined): string {
9
9
  if (!token) return '';
@@ -35,32 +35,19 @@ export async function GET() {
35
35
  }
36
36
  }
37
37
 
38
- // Mask API keys for all configured providers
39
- const maskedProviders: Record<string, { apiKey: string; model: string; baseUrl?: string }> = {};
40
- for (const [id, cfg] of Object.entries(settings.ai.providers)) {
41
- if (!cfg) continue;
42
- maskedProviders[id] = {
43
- apiKey: cfg.apiKey ? '***set***' : '',
44
- model: cfg.model ?? '',
45
- ...(cfg.baseUrl !== undefined ? { baseUrl: cfg.baseUrl } : {}),
46
- };
47
- }
48
-
49
- const masked = {
38
+ return NextResponse.json({
50
39
  ai: {
51
- provider: settings.ai.provider,
52
- providers: maskedProviders,
40
+ activeProvider: settings.ai.activeProvider,
41
+ providers: settings.ai.providers,
53
42
  },
54
43
  mindRoot: settings.mindRoot,
55
- webPassword: settings.webPassword ? '***set***' : '',
44
+ webPassword: settings.webPassword ?? '',
56
45
  authToken: maskToken(settings.authToken),
57
46
  mcpPort: settings.mcpPort ?? 8781,
58
47
  agent: settings.agent ?? {},
59
48
  envOverrides,
60
49
  envValues,
61
- customProviders: (settings.customProviders ?? []).map(maskCustomProviderKey),
62
- };
63
- return NextResponse.json(masked);
50
+ });
64
51
  }
65
52
 
66
53
  export async function POST(req: NextRequest) {
@@ -68,28 +55,14 @@ export async function POST(req: NextRequest) {
68
55
  const body = await req.json() as Partial<ServerSettings>;
69
56
  const current = readSettings();
70
57
 
71
- // Merge providers dynamically, preserving masked keys ('***set***' = keep existing)
72
- const mergedProviders = { ...current.ai.providers };
73
- if (body.ai?.providers) {
74
- for (const [id, incoming] of Object.entries(body.ai.providers)) {
75
- if (!incoming) continue;
76
- const cur = mergedProviders[id as keyof typeof mergedProviders] ?? { apiKey: '', model: '' };
77
- mergedProviders[id as keyof typeof mergedProviders] = {
78
- ...cur,
79
- ...incoming,
80
- apiKey: incoming.apiKey === '***set***'
81
- ? (cur.apiKey ?? '')
82
- : (incoming.apiKey ?? cur.apiKey ?? ''),
83
- model: incoming.model ?? cur.model ?? '',
84
- };
85
- }
58
+ // Resolve AI config
59
+ const resolvedAi = { ...current.ai };
60
+ if (body.ai) {
61
+ if (body.ai.activeProvider !== undefined) resolvedAi.activeProvider = body.ai.activeProvider;
62
+ if (body.ai.providers !== undefined) resolvedAi.providers = parseProviders(body.ai.providers);
86
63
  }
87
64
 
88
- // Resolve webPassword: '***set***' means keep existing, '' means clear, anything else = new value
89
- const incomingWebPassword = body.webPassword;
90
- const resolvedWebPassword = incomingWebPassword === '***set***'
91
- ? current.webPassword
92
- : (incomingWebPassword ?? current.webPassword);
65
+ const resolvedWebPassword = body.webPassword ?? current.webPassword;
93
66
 
94
67
  // authToken is read-only via POST (use /api/settings/reset-token to regenerate)
95
68
  // but allow clearing it by passing empty string
@@ -110,25 +83,8 @@ export async function POST(req: NextRequest) {
110
83
  }
111
84
  }
112
85
 
113
- // Handle customProviders: merge with existing, preserving masked keys
114
- let resolvedCustomProviders = current.customProviders ?? [];
115
- if (body.customProviders !== undefined) {
116
- const incoming = parseCustomProviders(body.customProviders);
117
- resolvedCustomProviders = incoming.map(cp => {
118
- // If API key is masked, keep existing key
119
- if (cp.apiKey === '***set***') {
120
- const existing = (current.customProviders ?? []).find(e => e.id === cp.id);
121
- return { ...cp, apiKey: existing?.apiKey ?? '' };
122
- }
123
- return cp;
124
- });
125
- }
126
-
127
86
  const next: ServerSettings = {
128
- ai: {
129
- provider: body.ai?.provider ?? current.ai.provider,
130
- providers: mergedProviders,
131
- },
87
+ ai: resolvedAi,
132
88
  mindRoot: body.mindRoot ?? current.mindRoot,
133
89
  agent: body.agent ?? current.agent,
134
90
  webPassword: resolvedWebPassword,
@@ -137,7 +93,6 @@ export async function POST(req: NextRequest) {
137
93
  mcpPort: typeof body.mcpPort === 'number' ? body.mcpPort : current.mcpPort,
138
94
  startMode: body.startMode ?? current.startMode,
139
95
  connectionMode: resolvedConnectionMode,
140
- customProviders: resolvedCustomProviders,
141
96
  };
142
97
 
143
98
  writeSettings(next);
@@ -4,7 +4,7 @@ import { complete } from '@mariozechner/pi-ai';
4
4
  import { effectiveAiConfig, readBaseUrlCompat, writeSettings, readSettings } from '@/lib/settings';
5
5
  import { getModelConfig } from '@/lib/agent/model';
6
6
  import { type ProviderId, isProviderId } from '@/lib/agent/providers';
7
- import { isCustomProviderId, findCustomProvider } from '@/lib/custom-endpoints';
7
+ import { isProviderEntryId, findProvider } from '@/lib/custom-endpoints';
8
8
 
9
9
  const TIMEOUT = 15_000;
10
10
 
@@ -40,26 +40,60 @@ function classifyPiAiError(err: unknown): { code: ErrorCode; error: string } {
40
40
  export async function POST(req: NextRequest) {
41
41
  try {
42
42
  const body = await req.json();
43
- const { provider, apiKey, model, baseUrl } = body as {
43
+ const { provider, apiKey, model, baseUrl, baseProviderId } = body as {
44
44
  provider?: string;
45
45
  apiKey?: string;
46
46
  model?: string;
47
47
  baseUrl?: string;
48
+ /** When set, run an inline test using only the supplied params (no settings fallback). */
49
+ baseProviderId?: string;
48
50
  };
49
51
 
50
- // Support custom provider IDs (cp_*)
51
- if (provider && isCustomProviderId(provider)) {
52
+ // Inline test for unsaved custom providers uses only the supplied params.
53
+ if (baseProviderId && isProviderId(baseProviderId)) {
54
+ if (!apiKey) {
55
+ return NextResponse.json({ ok: false, code: 'auth_error', error: 'No API key configured' });
56
+ }
57
+ if (!model) {
58
+ return NextResponse.json({ ok: false, code: 'unknown', error: 'Model is required' }, { status: 400 });
59
+ }
60
+ const start = Date.now();
61
+ const ctrl = new AbortController();
62
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
63
+ try {
64
+ const { model: piModel } = getModelConfig({
65
+ provider: baseProviderId as ProviderId,
66
+ apiKey,
67
+ model,
68
+ baseUrl: baseUrl || undefined,
69
+ });
70
+ await complete(piModel, {
71
+ messages: [{ role: 'user', content: 'hi', timestamp: Date.now() }],
72
+ }, {
73
+ apiKey,
74
+ signal: ctrl.signal,
75
+ });
76
+ return NextResponse.json({ ok: true, latency: Date.now() - start });
77
+ } catch (e) {
78
+ return NextResponse.json({ ok: false, ...classifyPiAiError(e) });
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ // Support provider entry IDs (p_*) — look up from unified providers list
85
+ if (provider && isProviderEntryId(provider)) {
52
86
  const settings = readSettings();
53
- const cp = findCustomProvider(settings.customProviders ?? [], provider);
54
- if (!cp) {
87
+ const entry = findProvider(settings.ai.providers, provider);
88
+ if (!entry) {
55
89
  return NextResponse.json(
56
- { ok: false, code: 'unknown', error: 'Custom provider not found' },
90
+ { ok: false, code: 'unknown', error: 'Provider not found' },
57
91
  { status: 400 },
58
92
  );
59
93
  }
60
- const resolvedKey = (apiKey && apiKey !== '***set***') ? apiKey : cp.apiKey;
61
- const resolvedModel = model || cp.model;
62
- const resolvedBaseUrl = baseUrl || cp.baseUrl;
94
+ const resolvedKey = apiKey || entry.apiKey;
95
+ const resolvedModel = model || entry.model;
96
+ const resolvedBaseUrl = baseUrl || entry.baseUrl;
63
97
  if (!resolvedKey) {
64
98
  return NextResponse.json({ ok: false, code: 'auth_error', error: 'No API key configured' });
65
99
  }
@@ -68,7 +102,7 @@ export async function POST(req: NextRequest) {
68
102
  const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
69
103
  try {
70
104
  const { model: piModel } = getModelConfig({
71
- provider: cp.baseProviderId,
105
+ provider: entry.protocol,
72
106
  apiKey: resolvedKey,
73
107
  model: resolvedModel || undefined,
74
108
  baseUrl: resolvedBaseUrl || undefined,
@@ -87,6 +121,7 @@ export async function POST(req: NextRequest) {
87
121
  }
88
122
  }
89
123
 
124
+ // Legacy: support raw protocol IDs (openai, anthropic, etc.)
90
125
  if (!provider || !isProviderId(provider)) {
91
126
  return NextResponse.json(
92
127
  { ok: false, code: 'unknown', error: 'Invalid provider' },
@@ -96,7 +131,7 @@ export async function POST(req: NextRequest) {
96
131
 
97
132
  const cfg = effectiveAiConfig(provider as ProviderId);
98
133
  let resolvedKey = apiKey || '';
99
- if (!resolvedKey || resolvedKey === '***set***') {
134
+ if (!resolvedKey) {
100
135
  resolvedKey = cfg.apiKey;
101
136
  }
102
137
 
@@ -6,6 +6,7 @@ import { readSettings, writeSettings, ServerSettings } from '@/lib/settings';
6
6
  import { applyTemplate } from '@/lib/template';
7
7
  import { expandSetupPathHome } from './path-utils';
8
8
  import { type ProviderId, isProviderId, PROVIDER_PRESETS } from '@/lib/agent/providers';
9
+ import { type Provider, generateProviderId, findProvider } from '@/lib/custom-endpoints';
9
10
 
10
11
  function maskApiKey(key: string): string {
11
12
  if (!key || key.length < 6) return key ? '***' : '';
@@ -20,15 +21,14 @@ export async function GET() {
20
21
  const defaultMindRoot = s.mindRoot || [home, 'MindOS', 'mind'].join(sep);
21
22
 
22
23
  // Build providerConfigs for frontend (masked keys)
23
- const providerConfigs: Record<string, { model: string; baseUrl?: string; apiKeyMask: string }> = {};
24
- for (const [id, cfg] of Object.entries(s.ai.providers)) {
25
- if (!cfg) continue;
26
- providerConfigs[id] = {
27
- model: cfg.model,
28
- baseUrl: cfg.baseUrl,
29
- apiKeyMask: maskApiKey(cfg.apiKey),
30
- };
31
- }
24
+ const providerConfigs = s.ai.providers.map(p => ({
25
+ id: p.id,
26
+ name: p.name,
27
+ protocol: p.protocol,
28
+ model: p.model,
29
+ baseUrl: p.baseUrl,
30
+ apiKeyMask: maskApiKey(p.apiKey),
31
+ }));
32
32
 
33
33
  return NextResponse.json({
34
34
  mindRoot: defaultMindRoot,
@@ -38,7 +38,7 @@ export async function GET() {
38
38
  mcpPort: s.mcpPort ?? 8781,
39
39
  authToken: s.authToken ?? '',
40
40
  webPassword: s.webPassword ?? '',
41
- provider: s.ai.provider,
41
+ activeProvider: s.ai.activeProvider,
42
42
  providerConfigs,
43
43
  guideState: s.guideState ?? null,
44
44
  });
@@ -104,23 +104,41 @@ export async function POST(req: NextRequest) {
104
104
  // configured key with blank just because the user didn't re-enter it.
105
105
  let mergedAi = current.ai;
106
106
  if (ai) {
107
- const newProvider = ai.provider && isProviderId(ai.provider) ? ai.provider : current.ai.provider;
108
- const mergedProviders = { ...current.ai.providers };
107
+ const mergedProviders = [...current.ai.providers];
109
108
 
110
109
  // Merge each provider's config from the incoming payload
111
110
  if (ai.providers && typeof ai.providers === 'object') {
112
111
  for (const [id, inCfg] of Object.entries(ai.providers as Record<string, any>)) {
113
112
  if (!isProviderId(id) || !inCfg) continue;
114
- const existing = mergedProviders[id] ?? { apiKey: '', model: PROVIDER_PRESETS[id].defaultModel };
115
- mergedProviders[id] = {
116
- apiKey: inCfg.apiKey || existing.apiKey,
117
- model: inCfg.model || existing.model,
118
- ...(inCfg.baseUrl !== undefined ? { baseUrl: inCfg.baseUrl } : existing.baseUrl ? { baseUrl: existing.baseUrl } : {}),
113
+ const preset = PROVIDER_PRESETS[id];
114
+ const existingIdx = mergedProviders.findIndex(p => p.protocol === id);
115
+ const existing = existingIdx >= 0 ? mergedProviders[existingIdx] : null;
116
+
117
+ const merged: Provider = {
118
+ id: existing?.id ?? generateProviderId(),
119
+ name: existing?.name ?? preset?.name ?? id,
120
+ protocol: id,
121
+ apiKey: inCfg.apiKey || existing?.apiKey || '',
122
+ model: inCfg.model || existing?.model || preset?.defaultModel || '',
123
+ baseUrl: inCfg.baseUrl !== undefined ? (inCfg.baseUrl || '') : (existing?.baseUrl ?? ''),
119
124
  };
125
+
126
+ if (existingIdx >= 0) {
127
+ mergedProviders[existingIdx] = merged;
128
+ } else {
129
+ mergedProviders.push(merged);
130
+ }
120
131
  }
121
132
  }
122
133
 
123
- mergedAi = { provider: newProvider, providers: mergedProviders };
134
+ // Determine active provider
135
+ let newActiveProvider = current.ai.activeProvider;
136
+ if (ai.provider && isProviderId(ai.provider)) {
137
+ const match = mergedProviders.find(p => p.protocol === ai.provider);
138
+ if (match) newActiveProvider = match.id;
139
+ }
140
+
141
+ mergedAi = { activeProvider: newActiveProvider, providers: mergedProviders };
124
142
  }
125
143
 
126
144
  const disabledSkills = body.template === 'zh' ? ['mindos'] : ['mindos-zh'];