@circuitwall/jarela 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +5 -5
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +4 -4
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +14 -2
  13. package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/v1/access/[identity]/route.js +7 -0
  19. package/.next/standalone/.next/server/app/api/v1/access/route.js +7 -0
  20. package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +7 -0
  21. package/.next/standalone/.next/server/app/api/v1/agents/[id]/display-filters/route.js +17 -5
  22. package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +7 -0
  23. package/.next/standalone/.next/server/app/api/v1/agents/[id]/thread/route.js +17 -5
  24. package/.next/standalone/.next/server/app/api/v1/agents/route.js +7 -0
  25. package/.next/standalone/.next/server/app/api/v1/allowed-sites/[hostname]/route.js +7 -0
  26. package/.next/standalone/.next/server/app/api/v1/allowed-sites/route.js +7 -0
  27. package/.next/standalone/.next/server/app/api/v1/artifacts/lifecycle/route.js +7 -0
  28. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +26 -32
  29. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/[remote_jid]/route.js +18 -7
  31. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/route.js +18 -7
  32. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +26 -32
  33. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +27 -33
  35. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +26 -32
  37. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/[route_id]/route.js +28 -12
  39. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/route.js +28 -12
  40. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +26 -32
  41. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js +9 -2
  43. package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/v1/bridges/route.js +19 -7
  45. package/.next/standalone/.next/server/app/api/v1/bridges/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/v1/dashboard/metrics/route.js +17 -5
  47. package/.next/standalone/.next/server/app/api/v1/dashboard/pricing/route.js +7 -0
  48. package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/v1/documents/search/route.js +7 -0
  50. package/.next/standalone/.next/server/app/api/v1/documents/settings/route.js +7 -0
  51. package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
  53. package/.next/standalone/.next/server/app/api/v1/env-sync/allowlist/route.js +7 -0
  54. package/.next/standalone/.next/server/app/api/v1/env-sync/route.js +7 -0
  55. package/.next/standalone/.next/server/app/api/v1/events/route.js +26 -32
  56. package/.next/standalone/.next/server/app/api/v1/events/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/v1/extension/browser/activate/route.js +7 -0
  59. package/.next/standalone/.next/server/app/api/v1/extension/browser/foreground/route.js +7 -0
  60. package/.next/standalone/.next/server/app/api/v1/extension/browser/history/route.js +7 -0
  61. package/.next/standalone/.next/server/app/api/v1/extension/browser/poll/route.js +7 -0
  62. package/.next/standalone/.next/server/app/api/v1/extension/browser/progress/route.js +7 -0
  63. package/.next/standalone/.next/server/app/api/v1/extension/browser/result/route.js +7 -0
  64. package/.next/standalone/.next/server/app/api/v1/extension/browser/retry/route.js +7 -0
  65. package/.next/standalone/.next/server/app/api/v1/extension/browser/status/route.js +7 -0
  66. package/.next/standalone/.next/server/app/api/v1/extension/browser/tabs/route.js +7 -0
  67. package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
  68. package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
  69. package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
  70. package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
  71. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
  72. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
  73. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
  74. package/.next/standalone/.next/server/app/api/v1/harnesses/[id]/route.js +7 -0
  75. package/.next/standalone/.next/server/app/api/v1/harnesses/default/route.js +7 -0
  76. package/.next/standalone/.next/server/app/api/v1/harnesses/route.js +7 -0
  77. package/.next/standalone/.next/server/app/api/v1/health/route.js +17 -5
  78. package/.next/standalone/.next/server/app/api/v1/integrations/[name]/route.js +7 -0
  79. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/callback/route.js +7 -0
  80. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/start/route.js +7 -0
  81. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/status/route.js +7 -0
  82. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/callback/route.js +7 -0
  83. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/start/route.js +7 -0
  84. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/status/route.js +7 -0
  85. package/.next/standalone/.next/server/app/api/v1/integrations/route.js +7 -0
  86. package/.next/standalone/.next/server/app/api/v1/lifecycle/adoption/route.js +17 -5
  87. package/.next/standalone/.next/server/app/api/v1/maps/embed/route.js +7 -0
  88. package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +17 -5
  89. package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +17 -5
  90. package/.next/standalone/.next/server/app/api/v1/memory/[namespace]/[key]/route.js +7 -0
  91. package/.next/standalone/.next/server/app/api/v1/memory/policy/route.js +7 -0
  92. package/.next/standalone/.next/server/app/api/v1/memory/route.js +7 -0
  93. package/.next/standalone/.next/server/app/api/v1/memory/search/route.js +7 -0
  94. package/.next/standalone/.next/server/app/api/v1/models/[name]/compact-threads/route.js +7 -0
  95. package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +7 -0
  96. package/.next/standalone/.next/server/app/api/v1/models/route.js +7 -0
  97. package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
  98. package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
  99. package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
  100. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
  101. package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +27 -12
  102. package/.next/standalone/.next/server/app/api/v1/profile/location/route.js +7 -0
  103. package/.next/standalone/.next/server/app/api/v1/profile/route.js +7 -0
  104. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +585 -58
  105. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +7 -0
  106. package/.next/standalone/.next/server/app/api/v1/providers/github-copilot/auth/route.js +7 -0
  107. package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -0
  108. package/.next/standalone/.next/server/app/api/v1/proxy-config/route.js +7 -0
  109. package/.next/standalone/.next/server/app/api/v1/redaction/init/route.js +7 -0
  110. package/.next/standalone/.next/server/app/api/v1/redaction/route.js +7 -0
  111. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/route.js +17 -5
  112. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
  113. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/route.js +7 -0
  114. package/.next/standalone/.next/server/app/api/v1/security/idle-timeout/route.js +7 -0
  115. package/.next/standalone/.next/server/app/api/v1/security/pin/route.js +7 -0
  116. package/.next/standalone/.next/server/app/api/v1/security/state/route.js +7 -0
  117. package/.next/standalone/.next/server/app/api/v1/security/unlock/route.js +7 -0
  118. package/.next/standalone/.next/server/app/api/v1/security/verify-pin/route.js +7 -0
  119. package/.next/standalone/.next/server/app/api/v1/skills/[id]/route.js +7 -0
  120. package/.next/standalone/.next/server/app/api/v1/skills/repos/[id]/route.js +7 -0
  121. package/.next/standalone/.next/server/app/api/v1/skills/repos/route.js +7 -0
  122. package/.next/standalone/.next/server/app/api/v1/skills/route.js +7 -0
  123. package/.next/standalone/.next/server/app/api/v1/tasks/[agent_id]/route.js +7 -0
  124. package/.next/standalone/.next/server/app/api/v1/tasks/route.js +7 -0
  125. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/activity/route.js +7 -0
  126. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/context-pin/route.js +12 -6
  127. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +125 -26
  128. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -2
  129. package/.next/standalone/.next/server/app/api/v1/threads/route.js +17 -5
  130. package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
  131. package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
  132. package/.next/standalone/.next/server/app/api/v1/voice/transcribe/route.js +17 -5
  133. package/.next/standalone/.next/server/app/api/v1/voice/tts/route.js +17 -5
  134. package/.next/standalone/.next/server/app/api/v1/watchers/[id]/route.js +17 -5
  135. package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
  136. package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
  137. package/.next/standalone/.next/server/app/page.js +27323 -27034
  138. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  139. package/.next/standalone/.next/server/app/setup/page.js +8 -0
  140. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/app-paths-manifest.json +5 -5
  142. package/.next/standalone/.next/server/chunks/1388.js +106 -36
  143. package/.next/standalone/.next/server/chunks/2068.js +47 -10
  144. package/.next/standalone/.next/server/chunks/210.js +6 -6
  145. package/.next/standalone/.next/server/chunks/2447.js +10 -5
  146. package/.next/standalone/.next/server/chunks/2653.js +80 -1
  147. package/.next/standalone/.next/server/chunks/3042.js +10 -8
  148. package/.next/standalone/.next/server/chunks/{4540.js → 309.js} +104 -38
  149. package/.next/standalone/.next/server/chunks/{2350.js → 3272.js} +104 -37
  150. package/.next/standalone/.next/server/chunks/3524.js +150 -67
  151. package/.next/standalone/.next/server/chunks/3566.js +1 -1
  152. package/.next/standalone/.next/server/chunks/4189.js +1 -1
  153. package/.next/standalone/.next/server/chunks/4596.js +28 -4
  154. package/.next/standalone/.next/server/chunks/46.js +91 -31
  155. package/.next/standalone/.next/server/chunks/466.js +53 -7
  156. package/.next/standalone/.next/server/chunks/4955.js +30 -3
  157. package/.next/standalone/.next/server/chunks/5585.js +28 -4
  158. package/.next/standalone/.next/server/chunks/5908.js +63 -42
  159. package/.next/standalone/.next/server/chunks/6142.js +10 -2
  160. package/.next/standalone/.next/server/chunks/8461.js +10 -8
  161. package/.next/standalone/.next/server/chunks/9032.js +5 -5
  162. package/.next/standalone/.next/server/chunks/9164.js +487 -177
  163. package/.next/standalone/.next/server/chunks/9304.js +51 -12
  164. package/.next/standalone/.next/server/chunks/{423.js → 9590.js} +84 -24
  165. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  166. package/.next/standalone/.next/server/middleware.js +89 -21
  167. package/.next/standalone/.next/server/pages/404.html +14 -2
  168. package/.next/standalone/.next/server/pages/500.html +1 -1
  169. package/.next/standalone/.next/server/preview-props.json +3 -3
  170. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  171. package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
  172. package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
  173. package/.next/standalone/.next/static/chunks/app/{page-213a623b37226452.js → page-2ef8d75d40eb4311.js} +10643 -10295
  174. package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
  175. package/.next/standalone/docs/adoption/changesets.json +2805 -0
  176. package/.next/standalone/package.json +1 -1
  177. package/CHANGELOG.md +52 -0
  178. package/api/client.models.test.ts +10 -0
  179. package/api/client.startup.test.ts +129 -0
  180. package/api/client.ts +51 -9
  181. package/api/thread-metadata.test.ts +72 -0
  182. package/api/types.ts +6 -3
  183. package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
  184. package/app/api/v1/providers/[provider]/models/route.ts +259 -40
  185. package/app/api/v1/threads/[thread_id]/route.ts +26 -23
  186. package/app/api/v1/threads/[thread_id]/run/route.ts +0 -1
  187. package/app/globals.css +119 -29
  188. package/app/layout.tsx +12 -0
  189. package/components/agents/AgentsPanel.tsx +5 -13
  190. package/components/chat/ChatView.tsx +9 -18
  191. package/components/chat/InputBar.tsx +1 -1
  192. package/components/chat/MessageBubble.tsx +13 -1
  193. package/components/chat/MessageList.test.tsx +45 -4
  194. package/components/chat/MessageList.tsx +123 -89
  195. package/components/chat/ToolList.tsx +88 -84
  196. package/components/chat/useThreadActivity.test.tsx +2 -2
  197. package/components/chat/useThreadActivity.ts +2 -2
  198. package/components/chat/useThreadData.ts +1 -30
  199. package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
  200. package/components/chat/useThreadMetadataPolling.ts +50 -0
  201. package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
  202. package/components/chat/useUserProfileAndAgent.ts +13 -2
  203. package/components/dashboard/DashboardPanel.tsx +1 -1
  204. package/components/dashboard/Series.tsx +34 -21
  205. package/components/dashboard/SummaryCards.tsx +1 -1
  206. package/components/integrations/NetworkSection.tsx +2 -2
  207. package/components/layout/AppShell.tsx +22 -7
  208. package/components/layout/MenuPanel.tsx +14 -24
  209. package/components/profile/ProfilePanel.tsx +2 -2
  210. package/components/settings/AppearancePanel.tsx +70 -3
  211. package/components/settings/SettingsPanel.tsx +2 -2
  212. package/components/tools/ToolsPanel.tsx +2 -7
  213. package/components/ui/AppearancePreferences.test.tsx +68 -0
  214. package/components/ui/BootScreen.test.tsx +44 -0
  215. package/components/ui/BootScreen.tsx +24 -11
  216. package/components/ui/NotificationStatus.test.tsx +2 -2
  217. package/components/ui/NotificationStatus.tsx +5 -5
  218. package/components/ui/SubTabBar.tsx +2 -3
  219. package/contexts/ThemeContext.tsx +84 -1
  220. package/hooks/useAgentSession.test.ts +12 -1
  221. package/hooks/useAgentSession.ts +6 -4
  222. package/hooks/useUrlSync.test.ts +13 -0
  223. package/hooks/useUrlSync.ts +0 -4
  224. package/lib/agents/citation-checker.test.ts +40 -2
  225. package/lib/agents/citation-checker.ts +56 -11
  226. package/lib/agents/context-boundary.ts +5 -6
  227. package/lib/agents/instruction-edits.test.ts +120 -0
  228. package/lib/agents/instruction-edits.ts +65 -42
  229. package/lib/agents/prepare/request.ts +0 -2
  230. package/lib/agents/prepare/system-prompt.test.ts +10 -0
  231. package/lib/agents/prepare/system-prompt.ts +8 -2
  232. package/lib/agents/proposals.test.ts +76 -0
  233. package/lib/agents/proposals.ts +13 -5
  234. package/lib/agents/run-thread.auto-boundary.test.ts +125 -5
  235. package/lib/agents/run-thread.ts +24 -9
  236. package/lib/agents/sqlite-checkpoint-saver.test.ts +91 -0
  237. package/lib/agents/warm-summary-background.ts +22 -2
  238. package/lib/agents/warm-topics.test.ts +155 -1
  239. package/lib/agents/warm-topics.ts +27 -7
  240. package/lib/bridges/dispatcher.ts +1 -0
  241. package/lib/bridges/message-role.test.ts +81 -0
  242. package/lib/bridges/message-role.ts +23 -10
  243. package/lib/bridges/types.ts +6 -0
  244. package/lib/bridges/whatsapp-link.test.ts +32 -0
  245. package/lib/bridges/whatsapp-link.ts +21 -0
  246. package/lib/bridges/whatsapp.inbound.test.ts +239 -0
  247. package/lib/bridges/whatsapp.sender.test.ts +80 -1
  248. package/lib/bridges/whatsapp.ts +76 -38
  249. package/lib/db/migrations.test.ts +61 -0
  250. package/lib/db/migrations.ts +13 -0
  251. package/lib/embeddings/performance.test.ts +114 -0
  252. package/lib/pricing/snapshot.test.ts +70 -0
  253. package/lib/pricing/snapshot.ts +10 -2
  254. package/lib/proxy/dispatcher.test.ts +119 -1
  255. package/lib/proxy/dispatcher.ts +22 -9
  256. package/lib/stores/agent-configs.ts +10 -5
  257. package/lib/stores/bridges.test.ts +81 -0
  258. package/lib/stores/bridges.ts +9 -8
  259. package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
  260. package/lib/stores/dashboard-metrics.ts +30 -4
  261. package/lib/stores/message-embedding-jobs.test.ts +27 -0
  262. package/lib/stores/message-embedding-jobs.ts +5 -3
  263. package/lib/stores/message-usage.test.ts +21 -1
  264. package/lib/stores/pricing-cache.test.ts +31 -0
  265. package/lib/stores/pricing.ts +3 -0
  266. package/lib/stores/proxy-config.test.ts +120 -0
  267. package/lib/stores/proxy-config.ts +22 -4
  268. package/lib/stores/threads.ts +12 -8
  269. package/lib/tools/communications/calendar.test.ts +43 -2
  270. package/lib/tools/communications/calendar.ts +2 -0
  271. package/lib/tools/communications/gmail.test.ts +51 -1
  272. package/lib/tools/communications/gmail.ts +44 -2
  273. package/lib/tools/communications/outlook-calendar.ts +2 -0
  274. package/lib/tools/communications/outlook.test.ts +33 -2
  275. package/lib/tools/communications/outlook.ts +2 -0
  276. package/lib/tools/filesystem/edit-match.test.ts +133 -0
  277. package/lib/tools/filesystem/edit-match.ts +7 -2
  278. package/lib/tools/packages/default-packages.test.ts +21 -0
  279. package/lib/tools/packages/optional-package.test.ts +72 -0
  280. package/lib/tools/packages/optional-package.ts +33 -4
  281. package/lib/tools/support/result-refs.test.ts +131 -0
  282. package/lib/tools/support/result-refs.ts +41 -9
  283. package/lib/ui/navigate.test.ts +13 -0
  284. package/lib/ui/navigate.ts +10 -2
  285. package/lib/utils/source-link.test.ts +51 -0
  286. package/lib/utils/source-link.ts +45 -0
  287. package/package.json +1 -1
  288. package/.next/standalone/.next/static/css/312542a5725ba0d5.css +0 -5
  289. /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
  290. /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
@@ -8,13 +8,16 @@ const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-provider-models-"));
8
8
  process.env.JARELA_DB_DIR = tmpRoot;
9
9
 
10
10
  const { upsertModelConfig, deleteModelConfig } = await import("@/lib/stores/model-config");
11
- const { GET } = await import("./route");
11
+ const { GET, POST } = await import("./route");
12
12
 
13
- const params = { params: Promise.resolve({ provider: "github-copilot" }) };
14
- const request = () => new NextRequest("http://localhost/api/v1/providers/github-copilot/models?fresh=1");
13
+ const routeParams = (provider: string) => ({ params: Promise.resolve({ provider }) });
14
+ const request = (provider: string, fresh = true) =>
15
+ new NextRequest(`http://localhost/api/v1/providers/${provider}/models${fresh ? "?fresh=1" : ""}`);
15
16
 
16
17
  afterEach(() => {
17
- deleteModelConfig("copilot-config");
18
+ for (const name of ["copilot-config", "anthropic-config", "gemini-config", "deepseek-config"]) {
19
+ deleteModelConfig(name);
20
+ }
18
21
  vi.unstubAllEnvs();
19
22
  vi.unstubAllGlobals();
20
23
  });
@@ -29,7 +32,7 @@ describe("provider model catalogs", () => {
29
32
  api_key: "github_pat_synthetic",
30
33
  }, true);
31
34
 
32
- const response = await GET(request(), params);
35
+ const response = await GET(request("github-copilot"), routeParams("github-copilot"));
33
36
 
34
37
  expect(response.status).toBe(400);
35
38
  await expect(response.json()).resolves.toMatchObject({
@@ -43,11 +46,218 @@ describe("provider model catalogs", () => {
43
46
  const fetch = vi.fn();
44
47
  vi.stubGlobal("fetch", fetch);
45
48
 
46
- const response = await GET(request(), params);
49
+ const response = await GET(request("github-copilot"), routeParams("github-copilot"));
47
50
 
48
51
  expect(response.status).toBe(200);
49
52
  const models = await response.json() as Array<{ id: string }>;
50
53
  expect(models.length).toBeGreaterThan(0);
51
54
  expect(fetch).not.toHaveBeenCalled();
52
55
  });
56
+
57
+ it("loads the Anthropic catalog and follows its pagination cursor", async () => {
58
+ upsertModelConfig("anthropic-config", "anthropic", "claude-sonnet-4-6", {
59
+ api_key: "synthetic-anthropic-key",
60
+ }, true);
61
+ const fetch = vi.fn()
62
+ .mockResolvedValueOnce(new Response(JSON.stringify({
63
+ data: [{
64
+ id: "claude-first",
65
+ max_input_tokens: 500000,
66
+ max_tokens: 64000,
67
+ capabilities: {
68
+ image_input: { supported: true },
69
+ structured_outputs: { supported: true },
70
+ },
71
+ }],
72
+ has_more: true,
73
+ last_id: "claude-first",
74
+ })))
75
+ .mockResolvedValueOnce(new Response(JSON.stringify({
76
+ data: [{ id: "claude-second" }],
77
+ has_more: false,
78
+ last_id: "claude-second",
79
+ })));
80
+ vi.stubGlobal("fetch", fetch);
81
+
82
+ const response = await GET(request("anthropic"), routeParams("anthropic"));
83
+
84
+ expect(response.status).toBe(200);
85
+ await expect(response.json()).resolves.toMatchObject([
86
+ {
87
+ id: "claude-first",
88
+ hosted_on: "anthropic",
89
+ context_length: 500000,
90
+ max_output_tokens: 64000,
91
+ capabilities: { vision: true, files: false, json_mode: true, web_search: false },
92
+ },
93
+ { id: "claude-second", hosted_on: "anthropic", capabilities: { vision: false, files: false, web_search: false } },
94
+ ]);
95
+ expect(fetch).toHaveBeenCalledTimes(2);
96
+ expect(String(fetch.mock.calls[0][0])).toBe("https://api.anthropic.com/v1/models?limit=1000");
97
+ expect(String(fetch.mock.calls[1][0])).toContain("after_id=claude-first");
98
+ expect(fetch.mock.calls[0][1]?.headers).toMatchObject({
99
+ "x-api-key": "synthetic-anthropic-key",
100
+ "anthropic-version": "2023-06-01",
101
+ });
102
+ });
103
+
104
+ it("loads Gemini generation models with live limits and skips embedding-only models", async () => {
105
+ upsertModelConfig("gemini-config", "gemini", "gemini-2.5-flash", {
106
+ api_key: "synthetic-gemini-key",
107
+ }, true);
108
+ const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({
109
+ models: [
110
+ {
111
+ name: "models/gemini-current",
112
+ inputTokenLimit: 131072,
113
+ outputTokenLimit: 16384,
114
+ supportedGenerationMethods: ["generateContent"],
115
+ },
116
+ {
117
+ name: "models/text-embedding-current",
118
+ supportedGenerationMethods: ["embedContent"],
119
+ },
120
+ ],
121
+ })));
122
+ vi.stubGlobal("fetch", fetch);
123
+
124
+ const response = await GET(request("gemini"), routeParams("gemini"));
125
+
126
+ expect(response.status).toBe(200);
127
+ await expect(response.json()).resolves.toMatchObject([
128
+ {
129
+ id: "gemini-current",
130
+ context_length: 131072,
131
+ max_output_tokens: 16384,
132
+ capabilities: { vision: false, files: false, audio: false, web_search: false },
133
+ },
134
+ ]);
135
+ expect(fetch.mock.calls[0][1]?.headers).toMatchObject({
136
+ "x-goog-api-key": "synthetic-gemini-key",
137
+ });
138
+ });
139
+
140
+ it("uses a configured Gemini native base URL for catalog discovery", async () => {
141
+ upsertModelConfig("gemini-config", "gemini", "gemini-2.5-flash", {
142
+ api_key: "synthetic-gemini-key",
143
+ gemini_native_base_url: "https://gemini-proxy.example/v1beta",
144
+ }, true);
145
+ const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ models: [] })));
146
+ vi.stubGlobal("fetch", fetch);
147
+
148
+ const response = await GET(request("gemini"), routeParams("gemini"));
149
+
150
+ expect(response.status).toBe(200);
151
+ expect(String(fetch.mock.calls[0][0])).toBe("https://gemini-proxy.example/v1beta/models?pageSize=1000");
152
+ expect(fetch.mock.calls[0][1]?.headers).toMatchObject({
153
+ "x-goog-api-key": "synthetic-gemini-key",
154
+ });
155
+ });
156
+
157
+ it("loads DeepSeek models with live limits and modality capabilities", async () => {
158
+ upsertModelConfig("deepseek-config", "deepseek", "deepseek-flash", {
159
+ api_key: "synthetic-deepseek-key",
160
+ }, true);
161
+ const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({
162
+ data: [{
163
+ id: "deepseek-new",
164
+ context_window: 900000,
165
+ max_output_tokens: 120000,
166
+ input_modalities: ["text", "image"],
167
+ }],
168
+ })));
169
+ vi.stubGlobal("fetch", fetch);
170
+
171
+ const response = await GET(request("deepseek"), routeParams("deepseek"));
172
+
173
+ expect(response.status).toBe(200);
174
+ await expect(response.json()).resolves.toMatchObject([
175
+ {
176
+ id: "deepseek-new",
177
+ context_length: 900000,
178
+ max_output_tokens: 120000,
179
+ capabilities: { vision: true, files: false },
180
+ },
181
+ ]);
182
+ expect(String(fetch.mock.calls[0][0])).toBe("https://api.deepseek.com/models");
183
+ expect(fetch.mock.calls[0][1]?.headers).toMatchObject({
184
+ Authorization: "Bearer synthetic-deepseek-key",
185
+ });
186
+ });
187
+
188
+ it("uses unsaved credentials and base URLs for OpenAI catalog discovery", async () => {
189
+ const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({
190
+ data: [{ id: "gpt-live-catalog-model" }],
191
+ })));
192
+ vi.stubGlobal("fetch", fetch);
193
+ const req = new NextRequest("http://localhost/api/v1/providers/openai/models", {
194
+ method: "POST",
195
+ headers: { "Content-Type": "application/json" },
196
+ body: JSON.stringify({
197
+ params: {
198
+ api_key: "synthetic-openai-key",
199
+ base_url: "https://openai.example/v1",
200
+ },
201
+ }),
202
+ });
203
+
204
+ const response = await POST(req, routeParams("openai"));
205
+
206
+ expect(response.status).toBe(200);
207
+ await expect(response.json()).resolves.toMatchObject([
208
+ { id: "gpt-live-catalog-model" },
209
+ ]);
210
+ expect(String(fetch.mock.calls[0][0])).toBe("https://openai.example/v1/models");
211
+ expect(fetch.mock.calls[0][1]?.headers).toMatchObject({
212
+ Authorization: "Bearer synthetic-openai-key",
213
+ });
214
+ });
215
+
216
+ it("rejects malformed catalog request bodies", async () => {
217
+ const fetch = vi.fn();
218
+ vi.stubGlobal("fetch", fetch);
219
+ const req = new NextRequest("http://localhost/api/v1/providers/openai/models", {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/json" },
222
+ body: "{",
223
+ });
224
+
225
+ const response = await POST(req, routeParams("openai"));
226
+
227
+ expect(response.status).toBe(400);
228
+ expect(fetch).not.toHaveBeenCalled();
229
+ });
230
+
231
+ it("invalidates the cached fallback after credentialed catalog discovery", async () => {
232
+ vi.stubEnv("ANTHROPIC_API_KEY", "");
233
+ const fetch = vi.fn().mockImplementation(() => new Response(JSON.stringify({
234
+ data: [{ id: "claude-live-after-save" }],
235
+ has_more: false,
236
+ last_id: "claude-live-after-save",
237
+ })));
238
+ vi.stubGlobal("fetch", fetch);
239
+
240
+ const fallbackResponse = await GET(request("anthropic"), routeParams("anthropic"));
241
+ expect(fallbackResponse.status).toBe(200);
242
+ expect(fetch).not.toHaveBeenCalled();
243
+
244
+ const postRequest = new NextRequest("http://localhost/api/v1/providers/anthropic/models", {
245
+ method: "POST",
246
+ headers: { "Content-Type": "application/json" },
247
+ body: JSON.stringify({ params: { api_key: "synthetic-anthropic-key" } }),
248
+ });
249
+ const liveResponse = await POST(postRequest, routeParams("anthropic"));
250
+ expect(liveResponse.status).toBe(200);
251
+
252
+ upsertModelConfig("anthropic-config", "anthropic", "claude-live-after-save", {
253
+ api_key: "synthetic-anthropic-key",
254
+ }, true);
255
+ const nextResponse = await GET(request("anthropic", false), routeParams("anthropic"));
256
+
257
+ expect(nextResponse.status).toBe(200);
258
+ await expect(nextResponse.json()).resolves.toMatchObject([
259
+ { id: "claude-live-after-save" },
260
+ ]);
261
+ expect(fetch).toHaveBeenCalledTimes(2);
262
+ });
53
263
  });
@@ -1,7 +1,10 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
2
3
  import { getProvider } from "@/lib/providers";
4
+ import { modelCapabilities } from "@/lib/providers/capabilities";
3
5
  import { listModelConfigs, getModelParams } from "@/lib/stores/model-config";
4
6
  import type { ProviderCatalogModel, ProviderParams } from "@/lib/providers/types";
7
+ import { validateBody } from "@/lib/api/responses";
5
8
  import {
6
9
  getKnownContextLength,
7
10
  getKnownMaxOutputTokens,
@@ -26,9 +29,12 @@ export interface CatalogModel {
26
29
 
27
30
  type Params = { params: Promise<{ provider: string }> };
28
31
 
29
- // Live provider catalogs (OpenAI, GitHub Copilot) hit external APIs on every
30
- // call; static ones still cost JSON construction. Cache per-provider for a
31
- // short TTL so flipping panels / model browsers doesn't re-hit upstream.
32
+ const CatalogBody = z.object({
33
+ params: z.record(z.string(), z.unknown()).optional(),
34
+ });
35
+
36
+ // Live provider catalogs hit external APIs. Cache per-provider briefly so
37
+ // opening the model browser repeatedly doesn't re-hit upstream.
32
38
  const PROVIDER_CATALOG_TTL_MS = 10 * 60 * 1000;
33
39
  type CachedCatalog = { data: CatalogModel[]; fetchedAt: number };
34
40
  const catalogCache = new Map<string, CachedCatalog>();
@@ -74,10 +80,11 @@ function catalogErrorResponse(err: unknown): NextResponse {
74
80
  // these responses since they're keyed on user-supplied secrets.
75
81
  export async function POST(req: NextRequest, { params }: Params) {
76
82
  const { provider } = await params;
77
- let body: { params?: ProviderParams } = {};
78
- try { body = await req.json(); } catch { /* empty body is fine */ }
83
+ const body = await validateBody(req, CatalogBody);
84
+ if (body instanceof NextResponse) return body;
79
85
  try {
80
86
  const models = await fetchCatalog(provider, body.params);
87
+ catalogCache.delete(provider);
81
88
  return NextResponse.json(models, { headers: { "Cache-Control": "no-store" } });
82
89
  } catch (err) {
83
90
  return catalogErrorResponse(err);
@@ -86,15 +93,70 @@ export async function POST(req: NextRequest, { params }: Params) {
86
93
 
87
94
  async function fetchCatalog(provider: string, overrides?: ProviderParams): Promise<CatalogModel[]> {
88
95
  switch (provider) {
89
- case "openai": return fetchOpenAICatalog();
96
+ case "openai": return fetchOpenAICatalog(overrides);
90
97
  case "github-copilot": return fetchGitHubCopilotCatalog();
91
- case "anthropic": return anthropicKnownModels();
92
- case "gemini": return geminiKnownModels();
93
- case "deepseek": return deepseekKnownModels();
98
+ case "anthropic": return fetchAnthropicCatalog(overrides);
99
+ case "gemini": return fetchGeminiCatalog(overrides);
100
+ case "deepseek": return fetchDeepseekCatalog(overrides);
94
101
  default: return fetchExternalCatalog(provider, overrides);
95
102
  }
96
103
  }
97
104
 
105
+ const PROVIDER_API_KEY_ENV: Record<string, string> = {
106
+ anthropic: "ANTHROPIC_API_KEY",
107
+ openai: "OPENAI_API_KEY",
108
+ gemini: "GEMINI_API_KEY",
109
+ deepseek: "DEEPSEEK_API_KEY",
110
+ };
111
+
112
+ function catalogParamsCandidates(provider: string, overrides?: ProviderParams): ProviderParams[] {
113
+ const envKey = process.env[PROVIDER_API_KEY_ENV[provider] ?? ""];
114
+ const configs = listModelConfigs().filter((cfg) => cfg.provider === provider);
115
+ const candidates = configs.map((cfg) => {
116
+ const params = { ...getModelParams(cfg), ...(overrides ?? {}) };
117
+ if (!params.api_key && envKey) params.api_key = envKey;
118
+ return params;
119
+ });
120
+
121
+ if (overrides) {
122
+ const params = { ...overrides };
123
+ if (!params.api_key && envKey) params.api_key = envKey;
124
+ candidates.push(params);
125
+ }
126
+ if (candidates.length === 0 && envKey) candidates.push({ api_key: envKey });
127
+ if (candidates.length === 0) candidates.push({});
128
+ return candidates;
129
+ }
130
+
131
+ async function fetchConfiguredCatalog(
132
+ provider: string,
133
+ overrides: ProviderParams | undefined,
134
+ fetcher: (params: ProviderParams, apiKey: string) => Promise<CatalogModel[]>,
135
+ fallback: () => CatalogModel[],
136
+ ): Promise<CatalogModel[]> {
137
+ const candidates = catalogParamsCandidates(provider, overrides)
138
+ .filter((params): params is ProviderParams & { api_key: string } =>
139
+ typeof params.api_key === "string" && params.api_key.length > 0,
140
+ );
141
+ if (candidates.length === 0) return fallback();
142
+
143
+ let lastErr: unknown;
144
+ for (const params of candidates) {
145
+ try {
146
+ return await fetcher(params, params.api_key);
147
+ } catch (err) {
148
+ lastErr = err;
149
+ }
150
+ }
151
+ throw lastErr ?? new Error(`${provider} catalog request failed`);
152
+ }
153
+
154
+ async function requestCatalogJson<T>(provider: string, url: URL, init: RequestInit): Promise<T> {
155
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15_000) });
156
+ if (!res.ok) throw new Error(`${provider} catalog error: ${res.status}`);
157
+ return res.json() as Promise<T>;
158
+ }
159
+
98
160
  async function fetchExternalCatalog(providerName: string, overrides?: ProviderParams): Promise<CatalogModel[]> {
99
161
  let provider;
100
162
  try {
@@ -132,40 +194,197 @@ async function fetchExternalCatalog(providerName: string, overrides?: ProviderPa
132
194
 
133
195
  // ── OpenAI ────────────────────────────────────────────────────────────────────
134
196
 
135
- async function fetchOpenAICatalog(): Promise<CatalogModel[]> {
136
- const cfg = listModelConfigs().find((c) => c.provider === "openai");
137
- if (!cfg) throw new Error("No OpenAI model config found.");
138
- const params = getModelParams(cfg);
139
- const apiKey = (params.api_key as string | undefined) ?? process.env.OPENAI_API_KEY;
140
- if (!apiKey) throw new Error("OpenAI: no api_key configured.");
141
-
142
- const res = await fetch("https://api.openai.com/v1/models", {
143
- headers: { Authorization: `Bearer ${apiKey}` },
197
+ async function fetchOpenAICatalog(overrides?: ProviderParams): Promise<CatalogModel[]> {
198
+ return fetchConfiguredCatalog("openai", overrides, async (params, apiKey) => {
199
+ const url = endpointUrl(params.base_url, "https://api.openai.com/v1", "/models");
200
+ const data = await requestCatalogJson<{ data?: Array<{ id: string }> }>("OpenAI", url, {
201
+ headers: {
202
+ ...params.extra_headers,
203
+ Authorization: "Bearer " + apiKey,
204
+ },
205
+ });
206
+ const chatPrefixes = ["gpt-", "o1", "o3", "o4", "chatgpt-"];
207
+ return (data.data ?? [])
208
+ .filter((m) => chatPrefixes.some((p) => m.id.startsWith(p)))
209
+ .map((m): CatalogModel => ({
210
+ id: m.id,
211
+ context_length: getKnownContextLength("openai", m.id),
212
+ max_output_tokens: getKnownMaxOutputTokens("openai", m.id),
213
+ hosted_on: "azure/openai",
214
+ capabilities: {
215
+ vision: m.id.includes("vision") || m.id.startsWith("gpt-4"),
216
+ tools: true,
217
+ streaming: true,
218
+ json_mode: true,
219
+ web_search: false,
220
+ audio: m.id.includes("audio") || m.id.startsWith("gpt-4o") || m.id.startsWith("gpt-5"),
221
+ files: m.id.startsWith("gpt-4") || m.id.startsWith("gpt-5") || m.id.startsWith("o"),
222
+ },
223
+ }))
224
+ .sort((a, b) => a.id.localeCompare(b.id));
225
+ }, () => {
226
+ throw new Error("OpenAI: no api_key configured.");
144
227
  });
145
- if (!res.ok) throw new Error(`OpenAI catalog error: ${res.status}`);
146
- const data = await res.json() as { data: Array<{ id: string }> };
147
-
148
- const chatPrefixes = ["gpt-", "o1", "o3", "o4", "chatgpt-"];
149
- const chatModels = data.data
150
- .filter((m) => chatPrefixes.some((p) => m.id.startsWith(p)))
151
- .map((m): CatalogModel => ({
152
- id: m.id,
153
- context_length: getKnownContextLength("openai", m.id),
154
- max_output_tokens: getKnownMaxOutputTokens("openai", m.id),
155
- hosted_on: "azure/openai",
156
- capabilities: {
157
- vision: m.id.includes("vision") || m.id.startsWith("gpt-4"),
158
- tools: true,
159
- streaming: true,
160
- json_mode: true,
161
- web_search: false,
162
- audio: m.id.includes("audio") || m.id.startsWith("gpt-4o") || m.id.startsWith("gpt-5"),
163
- files: m.id.startsWith("gpt-4") || m.id.startsWith("gpt-5") || m.id.startsWith("o"),
228
+ }
229
+
230
+ async function fetchAnthropicCatalog(overrides?: ProviderParams): Promise<CatalogModel[]> {
231
+ return fetchConfiguredCatalog("anthropic", overrides, async (params, apiKey) => {
232
+ const url = endpointUrl(params.base_url, "https://api.anthropic.com", "/v1/models");
233
+ url.searchParams.set("limit", "1000");
234
+ const models: Array<{
235
+ id: string;
236
+ max_input_tokens?: number | null;
237
+ max_tokens?: number | null;
238
+ capabilities?: {
239
+ image_input?: { supported?: boolean };
240
+ pdf_input?: { supported?: boolean };
241
+ structured_outputs?: { supported?: boolean };
242
+ server_tools?: { web_search?: { supported?: boolean } };
243
+ };
244
+ }> = [];
245
+ let afterId: string | undefined;
246
+
247
+ do {
248
+ const pageUrl = new URL(url);
249
+ if (afterId) pageUrl.searchParams.set("after_id", afterId);
250
+ const page = await requestCatalogJson<{
251
+ data?: typeof models;
252
+ has_more?: boolean;
253
+ last_id?: string | null;
254
+ }>("Anthropic", pageUrl, {
255
+ headers: {
256
+ ...params.extra_headers,
257
+ "x-api-key": apiKey,
258
+ "anthropic-version": "2023-06-01",
259
+ },
260
+ });
261
+ models.push(...(page.data ?? []));
262
+ if (page.has_more && !page.last_id) throw new Error("Anthropic catalog response is missing its next-page cursor");
263
+ if (page.has_more && page.last_id === afterId) throw new Error("Anthropic catalog pagination did not advance");
264
+ afterId = page.has_more ? page.last_id ?? undefined : undefined;
265
+ } while (afterId);
266
+
267
+ return models.map((model): CatalogModel => {
268
+ const known = modelCapabilities("anthropic", model.id);
269
+ const vision = model.capabilities?.image_input?.supported ?? known.vision;
270
+ return {
271
+ id: model.id,
272
+ context_length: model.max_input_tokens ?? getKnownContextLength("anthropic", model.id),
273
+ max_output_tokens: model.max_tokens ?? getKnownMaxOutputTokens("anthropic", model.id),
274
+ hosted_on: "anthropic",
275
+ capabilities: {
276
+ vision,
277
+ tools: known.tools,
278
+ streaming: known.streaming,
279
+ json_mode: model.capabilities?.structured_outputs?.supported ?? known.json_mode,
280
+ web_search: model.capabilities?.server_tools?.web_search?.supported ?? known.web_search,
281
+ audio: known.audio,
282
+ files: model.capabilities?.pdf_input?.supported ?? known.files,
283
+ },
284
+ };
285
+ });
286
+ }, anthropicKnownModels);
287
+ }
288
+
289
+ async function fetchGeminiCatalog(overrides?: ProviderParams): Promise<CatalogModel[]> {
290
+ return fetchConfiguredCatalog("gemini", overrides, async (params, apiKey) => {
291
+ const url = endpointUrl(
292
+ params.gemini_native_base_url,
293
+ "https://generativelanguage.googleapis.com/v1beta",
294
+ "/models",
295
+ );
296
+ url.searchParams.set("pageSize", "1000");
297
+ const models: Array<{
298
+ name: string;
299
+ inputTokenLimit?: number;
300
+ outputTokenLimit?: number;
301
+ supportedGenerationMethods?: string[];
302
+ }> = [];
303
+ let pageToken: string | undefined;
304
+
305
+ do {
306
+ const pageUrl = new URL(url);
307
+ if (pageToken) pageUrl.searchParams.set("pageToken", pageToken);
308
+ const page = await requestCatalogJson<{
309
+ models?: typeof models;
310
+ nextPageToken?: string;
311
+ }>("Gemini", pageUrl, { headers: { "x-goog-api-key": apiKey } });
312
+ models.push(...(page.models ?? []));
313
+ if (page.nextPageToken && page.nextPageToken === pageToken) {
314
+ throw new Error("Gemini catalog pagination did not advance");
315
+ }
316
+ pageToken = page.nextPageToken;
317
+ } while (pageToken);
318
+
319
+ return models
320
+ .map((model): CatalogModel | null => {
321
+ const id = model.name.replace(/^models\//, "");
322
+ if (!id.startsWith("gemini-") ||
323
+ !model.supportedGenerationMethods?.includes("generateContent")) return null;
324
+ return {
325
+ id,
326
+ context_length: model.inputTokenLimit ?? getKnownContextLength("gemini", id),
327
+ max_output_tokens: model.outputTokenLimit ?? getKnownMaxOutputTokens("gemini", id),
328
+ hosted_on: "google",
329
+ capabilities: modelCapabilities("gemini", id),
330
+ };
331
+ })
332
+ .filter((model): model is CatalogModel => model !== null)
333
+ .sort((a, b) => a.id.localeCompare(b.id));
334
+ }, geminiKnownModels);
335
+ }
336
+
337
+ async function fetchDeepseekCatalog(overrides?: ProviderParams): Promise<CatalogModel[]> {
338
+ return fetchConfiguredCatalog("deepseek", overrides, async (params, apiKey) => {
339
+ const url = endpointUrl(params.base_url, "https://api.deepseek.com", "/models");
340
+ const data = await requestCatalogJson<{
341
+ data?: Array<{
342
+ id: string;
343
+ context_window?: number;
344
+ max_output_tokens?: number;
345
+ input_modalities?: string[];
346
+ }>;
347
+ }>("DeepSeek", url, {
348
+ headers: {
349
+ ...params.extra_headers,
350
+ Authorization: "Bearer " + apiKey,
164
351
  },
165
- }))
166
- .sort((a, b) => a.id.localeCompare(b.id));
352
+ });
353
+ return (data.data ?? []).map((model): CatalogModel => {
354
+ const known = modelCapabilities("deepseek", model.id);
355
+ const vision = model.input_modalities?.includes("image") ?? known.vision;
356
+ return {
357
+ id: model.id,
358
+ context_length: model.context_window ?? getKnownContextLength("deepseek", model.id),
359
+ max_output_tokens: model.max_output_tokens ?? getKnownMaxOutputTokens("deepseek", model.id),
360
+ hosted_on: "deepseek",
361
+ capabilities: {
362
+ ...known,
363
+ vision,
364
+ audio: model.input_modalities?.includes("audio") ?? known.audio,
365
+ },
366
+ };
367
+ });
368
+ }, deepseekKnownModels);
369
+ }
167
370
 
168
- return chatModels;
371
+ function endpointUrl(baseUrl: unknown, defaultBaseUrl: string, path: string): URL {
372
+ const base = typeof baseUrl === "string" && baseUrl.trim() ? baseUrl.trim() : defaultBaseUrl;
373
+ let url: URL;
374
+ try {
375
+ url = new URL(base);
376
+ } catch {
377
+ throw new Error("Provider base URL must be a valid absolute URL");
378
+ }
379
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
380
+ throw new Error("Provider base URL must use HTTP or HTTPS");
381
+ }
382
+ const normalizedPath = url.pathname.replace(/\/+$/, "");
383
+ const suffix = path === "/v1/models" && normalizedPath.endsWith("/v1") ? "/models" : path;
384
+ url.pathname = `${normalizedPath}${suffix}` || "/";
385
+ url.search = "";
386
+ url.hash = "";
387
+ return url;
169
388
  }
170
389
 
171
390
  // ── GitHub Copilot ────────────────────────────────────────────────────────────