wyxrouter 0.4.71

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 (763) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/LICENSE +21 -0
  3. package/README.md +96 -0
  4. package/README.zh-CN.md +1311 -0
  5. package/assets/pixel-router-2d.png +0 -0
  6. package/cli/LICENSE +42 -0
  7. package/cli/README.md +125 -0
  8. package/cli/app/package.json +58 -0
  9. package/cli/cli.js +806 -0
  10. package/cli/package.json +48 -0
  11. package/i18n/README.ja-JP.md +1209 -0
  12. package/i18n/README.ru.md +1311 -0
  13. package/i18n/README.vi.md +1310 -0
  14. package/i18n/README.zh-CN.md +1306 -0
  15. package/images/9router.png +0 -0
  16. package/jsconfig.json +12 -0
  17. package/next.config.mjs +71 -0
  18. package/open-sse/config/appConstants.js +203 -0
  19. package/open-sse/config/codexInstructions.js +119 -0
  20. package/open-sse/config/constants.js +4 -0
  21. package/open-sse/config/defaultThinkingSignature.js +12 -0
  22. package/open-sse/config/errorConfig.js +85 -0
  23. package/open-sse/config/googleTtsLanguages.js +62 -0
  24. package/open-sse/config/kiroConstants.js +322 -0
  25. package/open-sse/config/models.js +13 -0
  26. package/open-sse/config/ollamaModels.js +19 -0
  27. package/open-sse/config/providerModels.js +944 -0
  28. package/open-sse/config/providers.js +458 -0
  29. package/open-sse/config/runtimeConfig.js +72 -0
  30. package/open-sse/config/ttsModels.js +129 -0
  31. package/open-sse/executors/antigravity.js +504 -0
  32. package/open-sse/executors/azure.js +57 -0
  33. package/open-sse/executors/base.js +185 -0
  34. package/open-sse/executors/codex.js +469 -0
  35. package/open-sse/executors/commandcode.js +88 -0
  36. package/open-sse/executors/cursor.js +795 -0
  37. package/open-sse/executors/default.js +497 -0
  38. package/open-sse/executors/gemini-cli.js +89 -0
  39. package/open-sse/executors/github.js +379 -0
  40. package/open-sse/executors/grok-web.js +345 -0
  41. package/open-sse/executors/iflow.js +108 -0
  42. package/open-sse/executors/index.js +75 -0
  43. package/open-sse/executors/kiro.js +508 -0
  44. package/open-sse/executors/ollama-local.js +14 -0
  45. package/open-sse/executors/opencode-go.js +41 -0
  46. package/open-sse/executors/opencode.js +32 -0
  47. package/open-sse/executors/perplexity-web.js +507 -0
  48. package/open-sse/executors/qoder.js +450 -0
  49. package/open-sse/executors/qwen.js +129 -0
  50. package/open-sse/executors/vertex.js +131 -0
  51. package/open-sse/executors/xiaomi-tokenplan.js +19 -0
  52. package/open-sse/handlers/chatCore/nonStreamingHandler.js +230 -0
  53. package/open-sse/handlers/chatCore/requestDetail.js +102 -0
  54. package/open-sse/handlers/chatCore/sseToJsonHandler.js +231 -0
  55. package/open-sse/handlers/chatCore/streamingHandler.js +103 -0
  56. package/open-sse/handlers/chatCore.js +287 -0
  57. package/open-sse/handlers/embeddingProviders/_base.js +4 -0
  58. package/open-sse/handlers/embeddingProviders/gemini.js +54 -0
  59. package/open-sse/handlers/embeddingProviders/index.js +23 -0
  60. package/open-sse/handlers/embeddingProviders/openai.js +39 -0
  61. package/open-sse/handlers/embeddingProviders/openaiCompatNode.js +13 -0
  62. package/open-sse/handlers/embeddingsCore.js +126 -0
  63. package/open-sse/handlers/fetch/index.js +237 -0
  64. package/open-sse/handlers/imageGenerationCore.js +189 -0
  65. package/open-sse/handlers/imageProviders/_base.js +31 -0
  66. package/open-sse/handlers/imageProviders/blackForestLabs.js +43 -0
  67. package/open-sse/handlers/imageProviders/cloudflareAi.js +178 -0
  68. package/open-sse/handlers/imageProviders/codex.js +198 -0
  69. package/open-sse/handlers/imageProviders/comfyui.js +8 -0
  70. package/open-sse/handlers/imageProviders/falAi.js +41 -0
  71. package/open-sse/handlers/imageProviders/gemini.js +25 -0
  72. package/open-sse/handlers/imageProviders/huggingface.js +22 -0
  73. package/open-sse/handlers/imageProviders/index.js +40 -0
  74. package/open-sse/handlers/imageProviders/nanobanana.js +58 -0
  75. package/open-sse/handlers/imageProviders/openai.js +40 -0
  76. package/open-sse/handlers/imageProviders/runwayml.js +47 -0
  77. package/open-sse/handlers/imageProviders/sdwebui.js +17 -0
  78. package/open-sse/handlers/imageProviders/stabilityAi.js +34 -0
  79. package/open-sse/handlers/responsesHandler.js +103 -0
  80. package/open-sse/handlers/search/callers.js +371 -0
  81. package/open-sse/handlers/search/chatSearch.js +409 -0
  82. package/open-sse/handlers/search/index.js +201 -0
  83. package/open-sse/handlers/search/normalizers.js +223 -0
  84. package/open-sse/handlers/sttCore.js +194 -0
  85. package/open-sse/handlers/ttsCore.js +74 -0
  86. package/open-sse/handlers/ttsProviders/_base.js +39 -0
  87. package/open-sse/handlers/ttsProviders/edgeTts.js +89 -0
  88. package/open-sse/handlers/ttsProviders/elevenlabs.js +48 -0
  89. package/open-sse/handlers/ttsProviders/gemini.js +117 -0
  90. package/open-sse/handlers/ttsProviders/genericFormats.js +169 -0
  91. package/open-sse/handlers/ttsProviders/googleTts.js +54 -0
  92. package/open-sse/handlers/ttsProviders/index.js +50 -0
  93. package/open-sse/handlers/ttsProviders/localDevice.js +87 -0
  94. package/open-sse/handlers/ttsProviders/minimax.js +59 -0
  95. package/open-sse/handlers/ttsProviders/openai.js +30 -0
  96. package/open-sse/handlers/ttsProviders/openrouter.js +70 -0
  97. package/open-sse/index.js +82 -0
  98. package/open-sse/rtk/applyFilter.js +15 -0
  99. package/open-sse/rtk/autodetect.js +111 -0
  100. package/open-sse/rtk/caveman.js +100 -0
  101. package/open-sse/rtk/cavemanPrompts.js +78 -0
  102. package/open-sse/rtk/constants.js +55 -0
  103. package/open-sse/rtk/filters/buildOutput.js +127 -0
  104. package/open-sse/rtk/filters/dedupLog.js +44 -0
  105. package/open-sse/rtk/filters/find.js +49 -0
  106. package/open-sse/rtk/filters/gitDiff.js +92 -0
  107. package/open-sse/rtk/filters/gitStatus.js +117 -0
  108. package/open-sse/rtk/filters/grep.js +48 -0
  109. package/open-sse/rtk/filters/ls.js +79 -0
  110. package/open-sse/rtk/filters/readNumbered.js +27 -0
  111. package/open-sse/rtk/filters/searchList.js +52 -0
  112. package/open-sse/rtk/filters/smartTruncate.js +15 -0
  113. package/open-sse/rtk/filters/tree.js +32 -0
  114. package/open-sse/rtk/index.js +155 -0
  115. package/open-sse/rtk/registry.js +38 -0
  116. package/open-sse/services/accountFallback.js +238 -0
  117. package/open-sse/services/combo.js +198 -0
  118. package/open-sse/services/compact.js +71 -0
  119. package/open-sse/services/kiroModels.js +332 -0
  120. package/open-sse/services/model.js +261 -0
  121. package/open-sse/services/oauthCredentialManager.js +151 -0
  122. package/open-sse/services/projectId.js +306 -0
  123. package/open-sse/services/provider.js +356 -0
  124. package/open-sse/services/qoderModels.js +214 -0
  125. package/open-sse/services/tokenRefresh.js +939 -0
  126. package/open-sse/services/usage.js +1496 -0
  127. package/open-sse/transformer/responsesTransformer.js +439 -0
  128. package/open-sse/transformer/streamToJsonConverter.js +103 -0
  129. package/open-sse/translator/formats.js +36 -0
  130. package/open-sse/translator/helpers/claudeHelper.js +216 -0
  131. package/open-sse/translator/helpers/geminiHelper.js +372 -0
  132. package/open-sse/translator/helpers/imageHelper.js +34 -0
  133. package/open-sse/translator/helpers/maxTokensHelper.js +27 -0
  134. package/open-sse/translator/helpers/openaiHelper.js +130 -0
  135. package/open-sse/translator/helpers/responsesApiHelper.js +139 -0
  136. package/open-sse/translator/helpers/toolCallHelper.js +148 -0
  137. package/open-sse/translator/index.js +251 -0
  138. package/open-sse/translator/request/antigravity-to-openai.js +229 -0
  139. package/open-sse/translator/request/claude-to-openai.js +232 -0
  140. package/open-sse/translator/request/gemini-to-openai.js +147 -0
  141. package/open-sse/translator/request/openai-responses.js +318 -0
  142. package/open-sse/translator/request/openai-to-claude.js +401 -0
  143. package/open-sse/translator/request/openai-to-commandcode.js +170 -0
  144. package/open-sse/translator/request/openai-to-cursor.js +183 -0
  145. package/open-sse/translator/request/openai-to-gemini.js +470 -0
  146. package/open-sse/translator/request/openai-to-kiro.js +629 -0
  147. package/open-sse/translator/request/openai-to-kiro.old.js +278 -0
  148. package/open-sse/translator/request/openai-to-ollama.js +192 -0
  149. package/open-sse/translator/request/openai-to-vertex.js +42 -0
  150. package/open-sse/translator/response/claude-to-openai.js +206 -0
  151. package/open-sse/translator/response/commandcode-to-openai.js +197 -0
  152. package/open-sse/translator/response/cursor-to-openai.js +30 -0
  153. package/open-sse/translator/response/gemini-to-openai.js +245 -0
  154. package/open-sse/translator/response/kiro-to-openai.js +195 -0
  155. package/open-sse/translator/response/ollama-to-openai.js +152 -0
  156. package/open-sse/translator/response/openai-responses.js +590 -0
  157. package/open-sse/translator/response/openai-to-antigravity.js +122 -0
  158. package/open-sse/translator/response/openai-to-claude.js +266 -0
  159. package/open-sse/utils/bypassHandler.js +298 -0
  160. package/open-sse/utils/claudeCloaking.js +155 -0
  161. package/open-sse/utils/claudeHeaderCache.js +70 -0
  162. package/open-sse/utils/clientDetector.js +63 -0
  163. package/open-sse/utils/cursorChecksum.js +149 -0
  164. package/open-sse/utils/cursorProtobuf.js +904 -0
  165. package/open-sse/utils/debugLog.js +14 -0
  166. package/open-sse/utils/error.js +147 -0
  167. package/open-sse/utils/ollamaTransform.js +85 -0
  168. package/open-sse/utils/proxyFetch.js +368 -0
  169. package/open-sse/utils/reasoningContentInjector.js +79 -0
  170. package/open-sse/utils/requestLogger.js +260 -0
  171. package/open-sse/utils/responsesStreamHelpers.js +49 -0
  172. package/open-sse/utils/sessionManager.js +82 -0
  173. package/open-sse/utils/stream.js +462 -0
  174. package/open-sse/utils/streamHandler.js +250 -0
  175. package/open-sse/utils/streamHelpers.js +122 -0
  176. package/open-sse/utils/toolDeduper.js +49 -0
  177. package/open-sse/utils/usageTracking.js +347 -0
  178. package/package.json +100 -0
  179. package/postcss.config.mjs +12 -0
  180. package/public/favicon.svg +11 -0
  181. package/public/file.svg +1 -0
  182. package/public/globe.svg +1 -0
  183. package/public/i18n/literals/ar.json +195 -0
  184. package/public/i18n/literals/bn.json +195 -0
  185. package/public/i18n/literals/cs.json +195 -0
  186. package/public/i18n/literals/da.json +195 -0
  187. package/public/i18n/literals/de.json +195 -0
  188. package/public/i18n/literals/el.json +195 -0
  189. package/public/i18n/literals/es.json +195 -0
  190. package/public/i18n/literals/fi.json +195 -0
  191. package/public/i18n/literals/fr.json +195 -0
  192. package/public/i18n/literals/he.json +195 -0
  193. package/public/i18n/literals/hi.json +195 -0
  194. package/public/i18n/literals/hu.json +195 -0
  195. package/public/i18n/literals/id.json +195 -0
  196. package/public/i18n/literals/it.json +195 -0
  197. package/public/i18n/literals/ja.json +195 -0
  198. package/public/i18n/literals/ko.json +195 -0
  199. package/public/i18n/literals/nl.json +195 -0
  200. package/public/i18n/literals/no.json +195 -0
  201. package/public/i18n/literals/pl.json +195 -0
  202. package/public/i18n/literals/pt-BR.json +195 -0
  203. package/public/i18n/literals/pt-PT.json +195 -0
  204. package/public/i18n/literals/ro.json +195 -0
  205. package/public/i18n/literals/ru.json +195 -0
  206. package/public/i18n/literals/sv.json +195 -0
  207. package/public/i18n/literals/th.json +195 -0
  208. package/public/i18n/literals/tl.json +195 -0
  209. package/public/i18n/literals/tr.json +195 -0
  210. package/public/i18n/literals/uk.json +195 -0
  211. package/public/i18n/literals/ur.json +195 -0
  212. package/public/i18n/literals/vi.json +195 -0
  213. package/public/i18n/literals/zh-CN.json +772 -0
  214. package/public/i18n/literals/zh-TW.json +195 -0
  215. package/public/icons/discord.svg +4 -0
  216. package/public/icons/icon-192.svg +4 -0
  217. package/public/icons/icon-512.svg +4 -0
  218. package/public/next.svg +1 -0
  219. package/public/providers/alicode-intl.png +0 -0
  220. package/public/providers/alicode.png +0 -0
  221. package/public/providers/amp.png +0 -0
  222. package/public/providers/anthropic-m.png +0 -0
  223. package/public/providers/anthropic.png +0 -0
  224. package/public/providers/antigravity.png +0 -0
  225. package/public/providers/assemblyai.png +0 -0
  226. package/public/providers/aws-polly.png +0 -0
  227. package/public/providers/azure.png +0 -0
  228. package/public/providers/black-forest-labs.png +0 -0
  229. package/public/providers/blackbox.png +0 -0
  230. package/public/providers/brave-search.png +0 -0
  231. package/public/providers/byteplus.png +0 -0
  232. package/public/providers/cartesia.png +0 -0
  233. package/public/providers/cerebras.png +0 -0
  234. package/public/providers/chutes.png +0 -0
  235. package/public/providers/claude.png +0 -0
  236. package/public/providers/cline.png +0 -0
  237. package/public/providers/cloudflare-ai.png +0 -0
  238. package/public/providers/codebuddy.svg +37 -0
  239. package/public/providers/codex.png +0 -0
  240. package/public/providers/cohere.png +0 -0
  241. package/public/providers/comfyui.png +0 -0
  242. package/public/providers/commandcode.png +0 -0
  243. package/public/providers/continue.png +0 -0
  244. package/public/providers/copilot.png +0 -0
  245. package/public/providers/coqui.png +0 -0
  246. package/public/providers/cursor.png +0 -0
  247. package/public/providers/deepgram.png +0 -0
  248. package/public/providers/deepseek-tui.png +0 -0
  249. package/public/providers/deepseek.png +0 -0
  250. package/public/providers/droid.png +0 -0
  251. package/public/providers/edge-tts.png +0 -0
  252. package/public/providers/elevenlabs.png +0 -0
  253. package/public/providers/exa.png +0 -0
  254. package/public/providers/fal-ai.png +0 -0
  255. package/public/providers/firecrawl.png +0 -0
  256. package/public/providers/fireworks.png +0 -0
  257. package/public/providers/gemini-cli.png +0 -0
  258. package/public/providers/gemini.png +0 -0
  259. package/public/providers/github.png +0 -0
  260. package/public/providers/glm-cn.png +0 -0
  261. package/public/providers/glm.png +0 -0
  262. package/public/providers/google-pse.png +0 -0
  263. package/public/providers/google-tts.png +0 -0
  264. package/public/providers/grok-web.png +0 -0
  265. package/public/providers/groq.png +0 -0
  266. package/public/providers/hermes.png +0 -0
  267. package/public/providers/huggingface.png +0 -0
  268. package/public/providers/hyperbolic.png +0 -0
  269. package/public/providers/iflow.png +0 -0
  270. package/public/providers/inworld.png +0 -0
  271. package/public/providers/jcode.png +0 -0
  272. package/public/providers/jina-ai.png +0 -0
  273. package/public/providers/jina-reader.png +0 -0
  274. package/public/providers/kilocode.png +0 -0
  275. package/public/providers/kimi-coding.png +0 -0
  276. package/public/providers/kimi.png +0 -0
  277. package/public/providers/kiro.png +0 -0
  278. package/public/providers/linkup.png +0 -0
  279. package/public/providers/local-device.png +0 -0
  280. package/public/providers/minimax-cn.png +0 -0
  281. package/public/providers/minimax.png +0 -0
  282. package/public/providers/mistral.png +0 -0
  283. package/public/providers/nanobanana.png +0 -0
  284. package/public/providers/nebius.png +0 -0
  285. package/public/providers/nvidia.png +0 -0
  286. package/public/providers/oai-cc.png +0 -0
  287. package/public/providers/oai-r.png +0 -0
  288. package/public/providers/ollama-local.png +0 -0
  289. package/public/providers/ollama.png +0 -0
  290. package/public/providers/openai.png +0 -0
  291. package/public/providers/openclaw.png +0 -0
  292. package/public/providers/opencode-go.png +0 -0
  293. package/public/providers/opencode.png +0 -0
  294. package/public/providers/openrouter.png +0 -0
  295. package/public/providers/perplexity-web.png +0 -0
  296. package/public/providers/perplexity.png +0 -0
  297. package/public/providers/playht.png +0 -0
  298. package/public/providers/qoder.png +0 -0
  299. package/public/providers/qwen.png +0 -0
  300. package/public/providers/recraft.png +0 -0
  301. package/public/providers/roo.png +0 -0
  302. package/public/providers/runwayml.png +0 -0
  303. package/public/providers/sdwebui.png +0 -0
  304. package/public/providers/searchapi.png +0 -0
  305. package/public/providers/searxng.png +0 -0
  306. package/public/providers/serper.png +0 -0
  307. package/public/providers/siliconflow.png +0 -0
  308. package/public/providers/stability-ai.png +0 -0
  309. package/public/providers/tavily.png +0 -0
  310. package/public/providers/together.png +0 -0
  311. package/public/providers/topaz.png +0 -0
  312. package/public/providers/tortoise.png +0 -0
  313. package/public/providers/vertex-partner.png +0 -0
  314. package/public/providers/vertex.png +0 -0
  315. package/public/providers/volcengine-ark.png +0 -0
  316. package/public/providers/voyage-ai.png +0 -0
  317. package/public/providers/xai.png +0 -0
  318. package/public/providers/xiaomi-mimo.png +0 -0
  319. package/public/providers/xiaomi-tokenplan.png +0 -0
  320. package/public/providers/youcom.png +0 -0
  321. package/public/sw.js +22 -0
  322. package/public/vercel.svg +1 -0
  323. package/public/window.svg +1 -0
  324. package/scripts/compact-request-details.mjs +71 -0
  325. package/scripts/import-codex-gptjson.mjs +342 -0
  326. package/scripts/start-standalone.mjs +25 -0
  327. package/scripts/translate-readme.js +201 -0
  328. package/src/app/(dashboard)/dashboard/automation/page.js +294 -0
  329. package/src/app/(dashboard)/dashboard/basic-chat/BasicChatPageClient.js +967 -0
  330. package/src/app/(dashboard)/dashboard/basic-chat/page.js +5 -0
  331. package/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js +66 -0
  332. package/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js +173 -0
  333. package/src/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js +11 -0
  334. package/src/app/(dashboard)/dashboard/cli-tools/components/AntigravityToolCard.js +481 -0
  335. package/src/app/(dashboard)/dashboard/cli-tools/components/ApiKeySelect.js +66 -0
  336. package/src/app/(dashboard)/dashboard/cli-tools/components/BaseUrlSelect.js +174 -0
  337. package/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +390 -0
  338. package/src/app/(dashboard)/dashboard/cli-tools/components/ClineToolCard.js +301 -0
  339. package/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js +458 -0
  340. package/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js +323 -0
  341. package/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js +640 -0
  342. package/src/app/(dashboard)/dashboard/cli-tools/components/DeepSeekTuiToolCard.js +338 -0
  343. package/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js +271 -0
  344. package/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js +410 -0
  345. package/src/app/(dashboard)/dashboard/cli-tools/components/EndpointPresetControl.js +128 -0
  346. package/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js +317 -0
  347. package/src/app/(dashboard)/dashboard/cli-tools/components/JcodeToolCard.js +380 -0
  348. package/src/app/(dashboard)/dashboard/cli-tools/components/KiloToolCard.js +275 -0
  349. package/src/app/(dashboard)/dashboard/cli-tools/components/MitmLinkCard.js +40 -0
  350. package/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +329 -0
  351. package/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js +318 -0
  352. package/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js +388 -0
  353. package/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js +500 -0
  354. package/src/app/(dashboard)/dashboard/cli-tools/components/ToolSummaryCard.js +39 -0
  355. package/src/app/(dashboard)/dashboard/cli-tools/components/cliEndpointMatch.js +13 -0
  356. package/src/app/(dashboard)/dashboard/cli-tools/components/index.js +19 -0
  357. package/src/app/(dashboard)/dashboard/cli-tools/page.js +7 -0
  358. package/src/app/(dashboard)/dashboard/combos/page.js +612 -0
  359. package/src/app/(dashboard)/dashboard/console-log/ConsoleLogClient.js +91 -0
  360. package/src/app/(dashboard)/dashboard/console-log/page.js +8 -0
  361. package/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +1555 -0
  362. package/src/app/(dashboard)/dashboard/endpoint/page.js +7 -0
  363. package/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js +1903 -0
  364. package/src/app/(dashboard)/dashboard/media-providers/[kind]/page.js +289 -0
  365. package/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js +410 -0
  366. package/src/app/(dashboard)/dashboard/media-providers/web/page.js +209 -0
  367. package/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js +117 -0
  368. package/src/app/(dashboard)/dashboard/mitm/page.js +5 -0
  369. package/src/app/(dashboard)/dashboard/page.js +7 -0
  370. package/src/app/(dashboard)/dashboard/profile/page.js +1140 -0
  371. package/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js +389 -0
  372. package/src/app/(dashboard)/dashboard/providers/[id]/AddCustomModelModal.js +125 -0
  373. package/src/app/(dashboard)/dashboard/providers/[id]/CompatibleModelsSection.js +250 -0
  374. package/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js +299 -0
  375. package/src/app/(dashboard)/dashboard/providers/[id]/CooldownTimer.js +42 -0
  376. package/src/app/(dashboard)/dashboard/providers/[id]/EditCompatibleNodeModal.js +161 -0
  377. package/src/app/(dashboard)/dashboard/providers/[id]/ModelRow.js +95 -0
  378. package/src/app/(dashboard)/dashboard/providers/[id]/PassthroughModelsSection.js +183 -0
  379. package/src/app/(dashboard)/dashboard/providers/[id]/page.js +2053 -0
  380. package/src/app/(dashboard)/dashboard/providers/[id]/page.new.js +1724 -0
  381. package/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js +497 -0
  382. package/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js +185 -0
  383. package/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js +294 -0
  384. package/src/app/(dashboard)/dashboard/providers/new/page.js +220 -0
  385. package/src/app/(dashboard)/dashboard/providers/page.js +1365 -0
  386. package/src/app/(dashboard)/dashboard/proxy-pools/page.js +1092 -0
  387. package/src/app/(dashboard)/dashboard/quota/page.js +11 -0
  388. package/src/app/(dashboard)/dashboard/skills/page.js +112 -0
  389. package/src/app/(dashboard)/dashboard/translator/page.js +303 -0
  390. package/src/app/(dashboard)/dashboard/usage/components/OverviewCards.js +35 -0
  391. package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js +185 -0
  392. package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js +127 -0
  393. package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js +259 -0
  394. package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +1394 -0
  395. package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +244 -0
  396. package/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js +327 -0
  397. package/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js +433 -0
  398. package/src/app/(dashboard)/dashboard/usage/components/UsageChart.js +141 -0
  399. package/src/app/(dashboard)/dashboard/usage/components/UsageTable.js +247 -0
  400. package/src/app/(dashboard)/dashboard/usage/page.js +75 -0
  401. package/src/app/(dashboard)/layout.js +6 -0
  402. package/src/app/api/auth/login/route.js +76 -0
  403. package/src/app/api/auth/logout/route.js +12 -0
  404. package/src/app/api/auth/oidc/callback/route.js +87 -0
  405. package/src/app/api/auth/oidc/start/route.js +52 -0
  406. package/src/app/api/auth/oidc/test/route.js +84 -0
  407. package/src/app/api/auth/status/route.js +45 -0
  408. package/src/app/api/cli-tools/all-statuses/route.js +46 -0
  409. package/src/app/api/cli-tools/antigravity-mitm/alias/route.js +53 -0
  410. package/src/app/api/cli-tools/antigravity-mitm/route.js +202 -0
  411. package/src/app/api/cli-tools/claude-settings/route.js +203 -0
  412. package/src/app/api/cli-tools/cline-settings/route.js +133 -0
  413. package/src/app/api/cli-tools/codex-gateway/accounts/route.js +16 -0
  414. package/src/app/api/cli-tools/codex-settings/route.js +239 -0
  415. package/src/app/api/cli-tools/copilot-settings/route.js +148 -0
  416. package/src/app/api/cli-tools/cowork-mcp-registry/route.js +77 -0
  417. package/src/app/api/cli-tools/cowork-mcp-tools/route.js +95 -0
  418. package/src/app/api/cli-tools/cowork-settings/route.js +412 -0
  419. package/src/app/api/cli-tools/deepseek-tui-settings/route.js +164 -0
  420. package/src/app/api/cli-tools/droid-settings/route.js +213 -0
  421. package/src/app/api/cli-tools/hermes-settings/route.js +175 -0
  422. package/src/app/api/cli-tools/jcode-settings/route.js +216 -0
  423. package/src/app/api/cli-tools/kilo-settings/route.js +131 -0
  424. package/src/app/api/cli-tools/openclaw-settings/route.js +292 -0
  425. package/src/app/api/cli-tools/opencode-settings/route.js +259 -0
  426. package/src/app/api/combos/[id]/route.js +81 -0
  427. package/src/app/api/combos/route.js +48 -0
  428. package/src/app/api/health/route.js +15 -0
  429. package/src/app/api/init/route.js +4 -0
  430. package/src/app/api/keys/[id]/route.js +58 -0
  431. package/src/app/api/keys/route.js +42 -0
  432. package/src/app/api/locale/route.js +30 -0
  433. package/src/app/api/mcp/[plugin]/message/route.js +21 -0
  434. package/src/app/api/mcp/[plugin]/sse/route.js +37 -0
  435. package/src/app/api/media-providers/tts/deepgram/voices/route.js +65 -0
  436. package/src/app/api/media-providers/tts/elevenlabs/voices/route.js +71 -0
  437. package/src/app/api/media-providers/tts/inworld/voices/route.js +61 -0
  438. package/src/app/api/media-providers/tts/minimax/voices/route.js +113 -0
  439. package/src/app/api/media-providers/tts/voices/route.js +99 -0
  440. package/src/app/api/models/alias/route.js +53 -0
  441. package/src/app/api/models/availability/route.js +103 -0
  442. package/src/app/api/models/custom/route.js +48 -0
  443. package/src/app/api/models/disabled/route.js +50 -0
  444. package/src/app/api/models/route.js +64 -0
  445. package/src/app/api/models/test/ping.js +191 -0
  446. package/src/app/api/models/test/route.js +14 -0
  447. package/src/app/api/oauth/[provider]/[action]/route.js +343 -0
  448. package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/cancel/route.js +19 -0
  449. package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/manual/[workerId]/route.js +30 -0
  450. package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/route.js +23 -0
  451. package/src/app/api/oauth/codebuddy/bulk-import/latest/route.js +25 -0
  452. package/src/app/api/oauth/codebuddy/bulk-import/route.js +49 -0
  453. package/src/app/api/oauth/codebuddy/quota-cookie/route.js +133 -0
  454. package/src/app/api/oauth/codex/import-token/route.js +96 -0
  455. package/src/app/api/oauth/cursor/auto-import/route.js +258 -0
  456. package/src/app/api/oauth/cursor/import/route.js +100 -0
  457. package/src/app/api/oauth/gitlab/pat/route.js +62 -0
  458. package/src/app/api/oauth/iflow/cookie/route.js +137 -0
  459. package/src/app/api/oauth/kiro/auto-import/route.js +85 -0
  460. package/src/app/api/oauth/kiro/bulk-import/[jobId]/cancel/route.js +18 -0
  461. package/src/app/api/oauth/kiro/bulk-import/[jobId]/manual/[workerId]/route.js +29 -0
  462. package/src/app/api/oauth/kiro/bulk-import/[jobId]/route.js +22 -0
  463. package/src/app/api/oauth/kiro/bulk-import/latest/route.js +25 -0
  464. package/src/app/api/oauth/kiro/bulk-import/route.js +49 -0
  465. package/src/app/api/oauth/kiro/import/route.js +110 -0
  466. package/src/app/api/oauth/kiro/social-authorize/route.js +27 -0
  467. package/src/app/api/oauth/kiro/social-exchange/route.js +41 -0
  468. package/src/app/api/pricing/route.js +134 -0
  469. package/src/app/api/provider-nodes/[id]/route.js +101 -0
  470. package/src/app/api/provider-nodes/route.js +104 -0
  471. package/src/app/api/provider-nodes/validate/route.js +201 -0
  472. package/src/app/api/providers/[id]/models/route.js +526 -0
  473. package/src/app/api/providers/[id]/route.js +189 -0
  474. package/src/app/api/providers/[id]/test/route.js +23 -0
  475. package/src/app/api/providers/[id]/test/testUtils.js +714 -0
  476. package/src/app/api/providers/[id]/test-models/route.js +66 -0
  477. package/src/app/api/providers/client/route.js +126 -0
  478. package/src/app/api/providers/kilo/free-models/route.js +55 -0
  479. package/src/app/api/providers/route.js +206 -0
  480. package/src/app/api/providers/suggested-models/filters.js +20 -0
  481. package/src/app/api/providers/suggested-models/route.js +32 -0
  482. package/src/app/api/providers/test-batch/route.js +131 -0
  483. package/src/app/api/providers/validate/route.js +637 -0
  484. package/src/app/api/proxy-pools/[id]/route.js +123 -0
  485. package/src/app/api/proxy-pools/[id]/test/route.js +70 -0
  486. package/src/app/api/proxy-pools/cloudflare-deploy/route.js +145 -0
  487. package/src/app/api/proxy-pools/deno-deploy/route.js +175 -0
  488. package/src/app/api/proxy-pools/route.js +93 -0
  489. package/src/app/api/proxy-pools/vercel-deploy/route.js +142 -0
  490. package/src/app/api/settings/database/route.js +36 -0
  491. package/src/app/api/settings/proxy-test/route.js +23 -0
  492. package/src/app/api/settings/require-login/route.js +15 -0
  493. package/src/app/api/settings/route.js +100 -0
  494. package/src/app/api/shutdown/route.js +24 -0
  495. package/src/app/api/tags/route.js +18 -0
  496. package/src/app/api/translator/console-logs/route.js +24 -0
  497. package/src/app/api/translator/console-logs/stream/route.js +79 -0
  498. package/src/app/api/translator/load/route.js +45 -0
  499. package/src/app/api/translator/save/route.js +44 -0
  500. package/src/app/api/translator/send/route.js +94 -0
  501. package/src/app/api/translator/translate/route.js +90 -0
  502. package/src/app/api/tunnel/disable/route.js +12 -0
  503. package/src/app/api/tunnel/enable/route.js +16 -0
  504. package/src/app/api/tunnel/status/route.js +13 -0
  505. package/src/app/api/tunnel/tailscale-check/route.js +50 -0
  506. package/src/app/api/tunnel/tailscale-disable/route.js +12 -0
  507. package/src/app/api/tunnel/tailscale-enable/route.js +12 -0
  508. package/src/app/api/tunnel/tailscale-install/route.js +72 -0
  509. package/src/app/api/usage/[connectionId]/route.js +188 -0
  510. package/src/app/api/usage/chart/route.js +21 -0
  511. package/src/app/api/usage/history/route.js +12 -0
  512. package/src/app/api/usage/logs/route.js +12 -0
  513. package/src/app/api/usage/providers/route.js +42 -0
  514. package/src/app/api/usage/request-details/route.js +57 -0
  515. package/src/app/api/usage/request-logs/route.js +13 -0
  516. package/src/app/api/usage/stats/route.js +23 -0
  517. package/src/app/api/usage/stream/route.js +79 -0
  518. package/src/app/api/v1/api/chat/route.js +37 -0
  519. package/src/app/api/v1/audio/speech/route.js +16 -0
  520. package/src/app/api/v1/audio/transcriptions/route.js +19 -0
  521. package/src/app/api/v1/audio/voices/route.js +68 -0
  522. package/src/app/api/v1/chat/completions/route.js +35 -0
  523. package/src/app/api/v1/embeddings/route.js +21 -0
  524. package/src/app/api/v1/images/generations/route.js +16 -0
  525. package/src/app/api/v1/messages/count_tokens/route.js +52 -0
  526. package/src/app/api/v1/messages/route.js +36 -0
  527. package/src/app/api/v1/models/[kind]/route.js +55 -0
  528. package/src/app/api/v1/models/info/route.js +110 -0
  529. package/src/app/api/v1/models/route.js +451 -0
  530. package/src/app/api/v1/responses/compact/route.js +37 -0
  531. package/src/app/api/v1/responses/route.js +30 -0
  532. package/src/app/api/v1/route.js +1 -0
  533. package/src/app/api/v1/search/route.js +21 -0
  534. package/src/app/api/v1/web/fetch/route.js +21 -0
  535. package/src/app/api/v1beta/models/[...path]/route.js +328 -0
  536. package/src/app/api/v1beta/models/route.js +44 -0
  537. package/src/app/api/version/route.js +45 -0
  538. package/src/app/api/version/shutdown/route.js +15 -0
  539. package/src/app/api/version/update/route.js +21 -0
  540. package/src/app/callback/page.js +148 -0
  541. package/src/app/dashboard/settings/pricing/page.js +173 -0
  542. package/src/app/favicon.ico +0 -0
  543. package/src/app/globals.css +496 -0
  544. package/src/app/landing/components/AnimatedBackground.js +57 -0
  545. package/src/app/landing/components/Features.js +133 -0
  546. package/src/app/landing/components/FlowAnimation.js +175 -0
  547. package/src/app/landing/components/Footer.js +61 -0
  548. package/src/app/landing/components/GetStarted.js +97 -0
  549. package/src/app/landing/components/HeroSection.js +47 -0
  550. package/src/app/landing/components/HowItWorks.js +66 -0
  551. package/src/app/landing/components/Navigation.js +72 -0
  552. package/src/app/landing/page.js +106 -0
  553. package/src/app/layout.js +49 -0
  554. package/src/app/login/page.js +197 -0
  555. package/src/app/manifest.js +30 -0
  556. package/src/app/page.js +5 -0
  557. package/src/dashboardGuard.js +242 -0
  558. package/src/i18n/RuntimeI18nProvider.js +27 -0
  559. package/src/i18n/config.js +146 -0
  560. package/src/i18n/runtime.js +162 -0
  561. package/src/lib/appUpdater.js +200 -0
  562. package/src/lib/auth/dashboardSession.js +68 -0
  563. package/src/lib/auth/loginLimiter.js +52 -0
  564. package/src/lib/auth/oidc.js +234 -0
  565. package/src/lib/consoleLogBuffer.js +79 -0
  566. package/src/lib/dataDir.js +29 -0
  567. package/src/lib/db/adapters/betterSqliteAdapter.js +55 -0
  568. package/src/lib/db/adapters/bunSqliteAdapter.js +63 -0
  569. package/src/lib/db/adapters/nodeSqliteAdapter.js +84 -0
  570. package/src/lib/db/adapters/sqljsAdapter.js +115 -0
  571. package/src/lib/db/backup.js +35 -0
  572. package/src/lib/db/driver.js +85 -0
  573. package/src/lib/db/helpers/jsonCol.js +9 -0
  574. package/src/lib/db/helpers/kvStore.js +39 -0
  575. package/src/lib/db/helpers/metaStore.js +22 -0
  576. package/src/lib/db/index.js +171 -0
  577. package/src/lib/db/migrate.js +286 -0
  578. package/src/lib/db/migrations/001-initial.js +14 -0
  579. package/src/lib/db/migrations/index.js +10 -0
  580. package/src/lib/db/paths.js +18 -0
  581. package/src/lib/db/repos/aliasRepo.js +62 -0
  582. package/src/lib/db/repos/apiKeysRepo.js +75 -0
  583. package/src/lib/db/repos/combosRepo.js +73 -0
  584. package/src/lib/db/repos/connectionsRepo.js +226 -0
  585. package/src/lib/db/repos/disabledModelsRepo.js +56 -0
  586. package/src/lib/db/repos/nodesRepo.js +95 -0
  587. package/src/lib/db/repos/pricingRepo.js +108 -0
  588. package/src/lib/db/repos/proxyPoolsRepo.js +103 -0
  589. package/src/lib/db/repos/requestDetailsRepo.js +259 -0
  590. package/src/lib/db/repos/settingsRepo.js +104 -0
  591. package/src/lib/db/repos/usageRepo.js +731 -0
  592. package/src/lib/db/schema.js +157 -0
  593. package/src/lib/db/version.js +21 -0
  594. package/src/lib/disabledModelsDb.js +4 -0
  595. package/src/lib/localDb.js +21 -0
  596. package/src/lib/mcp/stdioSseBridge.js +198 -0
  597. package/src/lib/mitmAliasCache.js +46 -0
  598. package/src/lib/network/connectionProxy.js +160 -0
  599. package/src/lib/network/initOutboundProxy.js +25 -0
  600. package/src/lib/network/outboundProxy.js +68 -0
  601. package/src/lib/network/proxyTest.js +91 -0
  602. package/src/lib/oauth/constants/oauth.js +284 -0
  603. package/src/lib/oauth/constants/xai.js +61 -0
  604. package/src/lib/oauth/providers.js +1506 -0
  605. package/src/lib/oauth/services/antigravity.js +321 -0
  606. package/src/lib/oauth/services/claude.js +136 -0
  607. package/src/lib/oauth/services/codebuddyBulkImportManager.js +454 -0
  608. package/src/lib/oauth/services/codex.js +144 -0
  609. package/src/lib/oauth/services/cursor.js +179 -0
  610. package/src/lib/oauth/services/gemini.js +240 -0
  611. package/src/lib/oauth/services/github.js +225 -0
  612. package/src/lib/oauth/services/iflow.js +202 -0
  613. package/src/lib/oauth/services/index.js +17 -0
  614. package/src/lib/oauth/services/kiro.js +334 -0
  615. package/src/lib/oauth/services/kiroBulkImportManager.js +778 -0
  616. package/src/lib/oauth/services/kiroConnections.js +93 -0
  617. package/src/lib/oauth/services/kiroGoogleAutomation.js +1136 -0
  618. package/src/lib/oauth/services/oauth.js +157 -0
  619. package/src/lib/oauth/services/openai.js +123 -0
  620. package/src/lib/oauth/services/qoder.js +216 -0
  621. package/src/lib/oauth/services/qwen.js +170 -0
  622. package/src/lib/oauth/services/xai.js +238 -0
  623. package/src/lib/oauth/utils/banner.js +63 -0
  624. package/src/lib/oauth/utils/pkce.js +39 -0
  625. package/src/lib/oauth/utils/server.js +415 -0
  626. package/src/lib/oauth/utils/ui.js +48 -0
  627. package/src/lib/providerNormalization.js +45 -0
  628. package/src/lib/qoder/constants.js +64 -0
  629. package/src/lib/qoder/cosy.js +175 -0
  630. package/src/lib/qoder/encoding.js +55 -0
  631. package/src/lib/requestDetailsDb.js +4 -0
  632. package/src/lib/tunnel/cloudflare/cloudflared.js +449 -0
  633. package/src/lib/tunnel/cloudflare/config.js +9 -0
  634. package/src/lib/tunnel/cloudflare/healthCheck.js +29 -0
  635. package/src/lib/tunnel/cloudflare/manager.js +151 -0
  636. package/src/lib/tunnel/cloudflare/pid.js +23 -0
  637. package/src/lib/tunnel/index.js +48 -0
  638. package/src/lib/tunnel/shared/dnsResolver.js +17 -0
  639. package/src/lib/tunnel/shared/internetCheck.js +26 -0
  640. package/src/lib/tunnel/shared/state.js +41 -0
  641. package/src/lib/tunnel/shared/watchdogConfig.js +8 -0
  642. package/src/lib/tunnel/tailscale/config.js +7 -0
  643. package/src/lib/tunnel/tailscale/healthCheck.js +29 -0
  644. package/src/lib/tunnel/tailscale/manager.js +129 -0
  645. package/src/lib/tunnel/tailscale/tailscale.js +790 -0
  646. package/src/lib/updater/updater.js +235 -0
  647. package/src/lib/usage/fetcher.js +208 -0
  648. package/src/lib/usageDb.js +7 -0
  649. package/src/mitm/antigravityIdeVersion.js +50 -0
  650. package/src/mitm/cert/generate.js +32 -0
  651. package/src/mitm/cert/install.js +269 -0
  652. package/src/mitm/cert/rootCA.js +173 -0
  653. package/src/mitm/config.js +87 -0
  654. package/src/mitm/dbReader.js +22 -0
  655. package/src/mitm/dns/dnsConfig.js +266 -0
  656. package/src/mitm/handlers/antigravity.js +33 -0
  657. package/src/mitm/handlers/base.js +226 -0
  658. package/src/mitm/handlers/copilot.js +35 -0
  659. package/src/mitm/handlers/cursor.js +15 -0
  660. package/src/mitm/handlers/kiro.js +526 -0
  661. package/src/mitm/logger.js +106 -0
  662. package/src/mitm/manager.js +851 -0
  663. package/src/mitm/paths.js +32 -0
  664. package/src/mitm/server.js +435 -0
  665. package/src/mitm/winElevated.js +81 -0
  666. package/src/models/index.js +38 -0
  667. package/src/proxy.js +5 -0
  668. package/src/shared/components/AddCustomEmbeddingModal.js +183 -0
  669. package/src/shared/components/Avatar.js +88 -0
  670. package/src/shared/components/Badge.js +54 -0
  671. package/src/shared/components/BulkAccountAutomationModal.js +508 -0
  672. package/src/shared/components/Button.js +56 -0
  673. package/src/shared/components/Card.js +116 -0
  674. package/src/shared/components/ChangelogModal.js +97 -0
  675. package/src/shared/components/CodeBuddyQuotaCookieModal.js +109 -0
  676. package/src/shared/components/ComboFormModal.js +176 -0
  677. package/src/shared/components/CursorAuthModal.js +212 -0
  678. package/src/shared/components/DonateModal.js +136 -0
  679. package/src/shared/components/Drawer.js +82 -0
  680. package/src/shared/components/EditConnectionModal.js +286 -0
  681. package/src/shared/components/Footer.js +132 -0
  682. package/src/shared/components/GitLabAuthModal.js +194 -0
  683. package/src/shared/components/Header.js +380 -0
  684. package/src/shared/components/HeaderLanguage.js +46 -0
  685. package/src/shared/components/HeaderMenu.js +126 -0
  686. package/src/shared/components/IFlowCookieModal.js +132 -0
  687. package/src/shared/components/Input.js +65 -0
  688. package/src/shared/components/KiroAuthModal.js +1171 -0
  689. package/src/shared/components/KiroOAuthWrapper.js +149 -0
  690. package/src/shared/components/KiroSocialOAuthModal.js +205 -0
  691. package/src/shared/components/LanguageSwitcher.js +190 -0
  692. package/src/shared/components/Loading.js +75 -0
  693. package/src/shared/components/ManualConfigModal.js +44 -0
  694. package/src/shared/components/McpMarketplaceModal.js +255 -0
  695. package/src/shared/components/Modal.js +146 -0
  696. package/src/shared/components/ModelSelectModal.js +537 -0
  697. package/src/shared/components/NineRemoteButton.js +23 -0
  698. package/src/shared/components/NineRemotePromoModal.js +99 -0
  699. package/src/shared/components/NoAuthProxyCard.js +86 -0
  700. package/src/shared/components/OAuthModal.js +682 -0
  701. package/src/shared/components/Pagination.js +150 -0
  702. package/src/shared/components/PricingModal.js +208 -0
  703. package/src/shared/components/ProviderIcon.js +63 -0
  704. package/src/shared/components/ProviderInfoCard.js +82 -0
  705. package/src/shared/components/RequestLogger.js +121 -0
  706. package/src/shared/components/SegmentedControl.js +48 -0
  707. package/src/shared/components/Select.js +67 -0
  708. package/src/shared/components/Sidebar.js +441 -0
  709. package/src/shared/components/ThemeProvider.js +15 -0
  710. package/src/shared/components/ThemeToggle.js +42 -0
  711. package/src/shared/components/Toggle.js +69 -0
  712. package/src/shared/components/Tooltip.js +25 -0
  713. package/src/shared/components/UsageStats.js +505 -0
  714. package/src/shared/components/index.js +46 -0
  715. package/src/shared/components/layouts/AuthLayout.js +29 -0
  716. package/src/shared/components/layouts/DashboardLayout.js +104 -0
  717. package/src/shared/components/layouts/index.js +4 -0
  718. package/src/shared/constants/cliTools.js +397 -0
  719. package/src/shared/constants/colors.js +77 -0
  720. package/src/shared/constants/config.js +99 -0
  721. package/src/shared/constants/coworkPlugins.js +75 -0
  722. package/src/shared/constants/index.js +4 -0
  723. package/src/shared/constants/locales.js +36 -0
  724. package/src/shared/constants/mitmToolHosts.js +12 -0
  725. package/src/shared/constants/models.js +38 -0
  726. package/src/shared/constants/pricing.js +303 -0
  727. package/src/shared/constants/providers.js +289 -0
  728. package/src/shared/constants/skills.js +78 -0
  729. package/src/shared/constants/ttsProviders.js +138 -0
  730. package/src/shared/hooks/index.js +2 -0
  731. package/src/shared/hooks/useCopyToClipboard.js +43 -0
  732. package/src/shared/hooks/useTheme.js +60 -0
  733. package/src/shared/services/bootstrap.js +12 -0
  734. package/src/shared/services/initializeApp.js +268 -0
  735. package/src/shared/utils/api.js +93 -0
  736. package/src/shared/utils/apiKey.js +98 -0
  737. package/src/shared/utils/clineAuth.js +37 -0
  738. package/src/shared/utils/cn.js +11 -0
  739. package/src/shared/utils/connectionStatus.js +162 -0
  740. package/src/shared/utils/index.js +40 -0
  741. package/src/shared/utils/machine.js +6 -0
  742. package/src/shared/utils/machineId.js +66 -0
  743. package/src/shared/utils/providerModelsFetcher.js +30 -0
  744. package/src/sse/handlers/chat.js +261 -0
  745. package/src/sse/handlers/embeddings.js +141 -0
  746. package/src/sse/handlers/fetch.js +213 -0
  747. package/src/sse/handlers/imageGeneration.js +142 -0
  748. package/src/sse/handlers/search.js +206 -0
  749. package/src/sse/handlers/stt.js +88 -0
  750. package/src/sse/handlers/tts.js +114 -0
  751. package/src/sse/services/auth.js +346 -0
  752. package/src/sse/services/codexGateway.js +215 -0
  753. package/src/sse/services/model.js +99 -0
  754. package/src/sse/services/tokenRefresh.js +319 -0
  755. package/src/sse/utils/logger.js +75 -0
  756. package/src/store/headerSearchStore.js +19 -0
  757. package/src/store/index.js +6 -0
  758. package/src/store/notificationStore.js +45 -0
  759. package/src/store/providerStore.js +55 -0
  760. package/src/store/settingsStore.js +51 -0
  761. package/src/store/themeStore.js +54 -0
  762. package/src/store/userStore.js +20 -0
  763. package/start.sh +4 -0
@@ -0,0 +1,1724 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useMemo } from "react";
4
+ import PropTypes from "prop-types";
5
+ import { useParams, useRouter } from "next/navigation";
6
+ import Link from "next/link";
7
+ import Image from "next/image";
8
+ import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, Toggle, Select } from "@/shared/components";
9
+ import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
10
+ import { getModelsByProviderId } from "@/shared/constants/models";
11
+ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
12
+
13
+ export default function ProviderDetailPage() {
14
+ const params = useParams();
15
+ const router = useRouter();
16
+ const providerId = params.id;
17
+ const [connections, setConnections] = useState([]);
18
+ const [loading, setLoading] = useState(true);
19
+ const [providerNode, setProviderNode] = useState(null);
20
+ const [showOAuthModal, setShowOAuthModal] = useState(false);
21
+ const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
22
+ const [showEditModal, setShowEditModal] = useState(false);
23
+ const [showEditNodeModal, setShowEditNodeModal] = useState(false);
24
+ const [selectedConnection, setSelectedConnection] = useState(null);
25
+ const [modelAliases, setModelAliases] = useState({});
26
+ const [remoteModels, setRemoteModels] = useState([]);
27
+ const [loadingRemoteModels, setLoadingRemoteModels] = useState(false);
28
+ const [selectedModelIds, setSelectedModelIds] = useState([]);
29
+ const [savingSelectedModels, setSavingSelectedModels] = useState(false);
30
+ const [modelSearchQuery, setModelSearchQuery] = useState("");
31
+ const [showSelectedOnly, setShowSelectedOnly] = useState(false);
32
+ const [headerImgError, setHeaderImgError] = useState(false);
33
+ const { copied, copy } = useCopyToClipboard();
34
+
35
+ const providerInfo = providerNode
36
+ ? {
37
+ id: providerNode.id,
38
+ name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"),
39
+ color: providerNode.type === "anthropic-compatible" ? "#D97757" : "#10A37F",
40
+ textIcon: providerNode.type === "anthropic-compatible" ? "AC" : "OC",
41
+ apiType: providerNode.apiType,
42
+ baseUrl: providerNode.baseUrl,
43
+ type: providerNode.type,
44
+ }
45
+ : (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId]);
46
+ const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId];
47
+ const models = useMemo(() => getModelsByProviderId(providerId), [providerId]);
48
+ const providerAlias = getProviderAlias(providerId);
49
+
50
+ const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
51
+ const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
52
+ const isCompatible = isOpenAICompatible || isAnthropicCompatible;
53
+
54
+ const providerStorageAlias = isCompatible ? providerId : providerAlias;
55
+ const providerDisplayAlias = isCompatible
56
+ ? (providerNode?.prefix || providerId)
57
+ : providerAlias;
58
+ const activeConnection = connections.find((conn) => conn.isActive !== false) || null;
59
+ const allProviderModels = models.length > 0 ? models : remoteModels;
60
+ const allProviderModelIds = useMemo(
61
+ () => allProviderModels.map((model) => model.id),
62
+ [allProviderModels]
63
+ );
64
+ const savedEnabledModels = useMemo(() => {
65
+ const enabled = activeConnection?.providerSpecificData?.enabledModels;
66
+ return Array.isArray(enabled)
67
+ ? enabled.filter((modelId) => allProviderModelIds.includes(modelId))
68
+ : [];
69
+ }, [activeConnection?.providerSpecificData?.enabledModels, allProviderModelIds]);
70
+ const savedEnabledModelsKey = useMemo(
71
+ () => savedEnabledModels.join("|"),
72
+ [savedEnabledModels]
73
+ );
74
+
75
+ // Define callbacks BEFORE the useEffect that uses them
76
+ const fetchAliases = useCallback(async () => {
77
+ try {
78
+ const res = await fetch("/api/models/alias");
79
+ const data = await res.json();
80
+ if (res.ok) {
81
+ setModelAliases(data.aliases || {});
82
+ }
83
+ } catch (error) {
84
+ console.log("Error fetching aliases:", error);
85
+ }
86
+ }, []);
87
+
88
+ const fetchConnections = useCallback(async () => {
89
+ try {
90
+ const [connectionsRes, nodesRes] = await Promise.all([
91
+ fetch("/api/providers", { cache: "no-store" }),
92
+ fetch("/api/provider-nodes", { cache: "no-store" }),
93
+ ]);
94
+ const connectionsData = await connectionsRes.json();
95
+ const nodesData = await nodesRes.json();
96
+ if (connectionsRes.ok) {
97
+ const filtered = (connectionsData.connections || []).filter(c => c.provider === providerId);
98
+ setConnections(filtered);
99
+ }
100
+ if (nodesRes.ok) {
101
+ let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null;
102
+
103
+ // Newly created compatible nodes can be briefly unavailable on one worker.
104
+ // Retry a few times before showing "Provider not found".
105
+ if (!node && isCompatible) {
106
+ for (let attempt = 0; attempt < 3; attempt += 1) {
107
+ await new Promise((resolve) => setTimeout(resolve, 150));
108
+ const retryRes = await fetch("/api/provider-nodes", { cache: "no-store" });
109
+ if (!retryRes.ok) continue;
110
+ const retryData = await retryRes.json();
111
+ node = (retryData.nodes || []).find((entry) => entry.id === providerId) || null;
112
+ if (node) break;
113
+ }
114
+ }
115
+
116
+ setProviderNode(node);
117
+ }
118
+ } catch (error) {
119
+ console.log("Error fetching connections:", error);
120
+ } finally {
121
+ setLoading(false);
122
+ }
123
+ }, [providerId, isCompatible]);
124
+
125
+ const handleUpdateNode = async (formData) => {
126
+ try {
127
+ const res = await fetch(`/api/provider-nodes/${providerId}`, {
128
+ method: "PUT",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify(formData),
131
+ });
132
+ const data = await res.json();
133
+ if (res.ok) {
134
+ setProviderNode(data.node);
135
+ await fetchConnections();
136
+ setShowEditNodeModal(false);
137
+ }
138
+ } catch (error) {
139
+ console.log("Error updating provider node:", error);
140
+ }
141
+ };
142
+
143
+ const handleToggleModelSelected = (modelId) => {
144
+ setSelectedModelIds((prev) => (
145
+ prev.includes(modelId)
146
+ ? prev.filter((id) => id !== modelId)
147
+ : [...prev, modelId]
148
+ ));
149
+ };
150
+
151
+ const handleSaveSelectedModels = async () => {
152
+ if (!activeConnection || savingSelectedModels) return;
153
+ setSavingSelectedModels(true);
154
+ try {
155
+ const res = await fetch(`/api/providers/${activeConnection.id}`, {
156
+ method: "PUT",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: JSON.stringify({
159
+ providerSpecificData: {
160
+ enabledModels: selectedModelIds,
161
+ },
162
+ }),
163
+ });
164
+
165
+ if (!res.ok) {
166
+ const data = await res.json();
167
+ alert(data.error || "Failed to save selected models");
168
+ return;
169
+ }
170
+
171
+ await fetchConnections();
172
+ } catch (error) {
173
+ console.log("Error saving selected models:", error);
174
+ alert("Failed to save selected models");
175
+ } finally {
176
+ setSavingSelectedModels(false);
177
+ }
178
+ };
179
+
180
+ useEffect(() => {
181
+ fetchConnections();
182
+ fetchAliases();
183
+ }, [fetchConnections, fetchAliases]);
184
+
185
+ useEffect(() => {
186
+ const nextSelectedModelIds = (isCompatible || providerInfo?.passthroughModels)
187
+ ? []
188
+ : savedEnabledModels;
189
+
190
+ setSelectedModelIds((prev) => {
191
+ if (
192
+ prev.length === nextSelectedModelIds.length
193
+ && prev.every((modelId, index) => modelId === nextSelectedModelIds[index])
194
+ ) {
195
+ return prev;
196
+ }
197
+ return nextSelectedModelIds;
198
+ });
199
+ }, [
200
+ isCompatible,
201
+ providerInfo?.passthroughModels,
202
+ activeConnection?.id,
203
+ savedEnabledModels
204
+ ]);
205
+
206
+ const fetchRemoteModels = useCallback(async () => {
207
+ if (isCompatible || providerInfo?.passthroughModels || models.length > 0) {
208
+ setRemoteModels([]);
209
+ return;
210
+ }
211
+
212
+ if (!activeConnection) {
213
+ setRemoteModels([]);
214
+ return;
215
+ }
216
+
217
+ setLoadingRemoteModels(true);
218
+ try {
219
+ const res = await fetch(`/api/providers/${activeConnection.id}/models`);
220
+ const data = await res.json();
221
+ if (!res.ok) {
222
+ setRemoteModels([]);
223
+ return;
224
+ }
225
+
226
+ const parsed = (data.models || [])
227
+ .map((item) => {
228
+ if (typeof item === "string") return { id: item, name: item };
229
+ const modelId = item?.id || item?.name || item?.model;
230
+ if (!modelId) return null;
231
+ return { id: modelId, name: item?.name || modelId };
232
+ })
233
+ .filter(Boolean);
234
+
235
+ const deduped = Array.from(
236
+ new Map(parsed.map((item) => [item.id, item])).values()
237
+ );
238
+
239
+ setRemoteModels(deduped);
240
+ } catch (error) {
241
+ console.log("Error fetching remote models:", error);
242
+ setRemoteModels([]);
243
+ } finally {
244
+ setLoadingRemoteModels(false);
245
+ }
246
+ }, [activeConnection, isCompatible, models.length, providerInfo?.passthroughModels]);
247
+
248
+ useEffect(() => {
249
+ fetchRemoteModels();
250
+ }, [fetchRemoteModels]);
251
+
252
+ const handleSetAlias = async (modelId, alias, providerAliasOverride = providerAlias) => {
253
+ const fullModel = `${providerAliasOverride}/${modelId}`;
254
+ try {
255
+ const res = await fetch("/api/models/alias", {
256
+ method: "PUT",
257
+ headers: { "Content-Type": "application/json" },
258
+ body: JSON.stringify({ model: fullModel, alias }),
259
+ });
260
+ if (res.ok) {
261
+ await fetchAliases();
262
+ } else {
263
+ const data = await res.json();
264
+ alert(data.error || "Failed to set alias");
265
+ }
266
+ } catch (error) {
267
+ console.log("Error setting alias:", error);
268
+ }
269
+ };
270
+
271
+ const handleDeleteAlias = async (alias) => {
272
+ try {
273
+ const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, {
274
+ method: "DELETE",
275
+ });
276
+ if (res.ok) {
277
+ await fetchAliases();
278
+ }
279
+ } catch (error) {
280
+ console.log("Error deleting alias:", error);
281
+ }
282
+ };
283
+
284
+ const handleDelete = async (id) => {
285
+ if (!confirm("Delete this connection?")) return;
286
+ try {
287
+ const res = await fetch(`/api/providers/${id}`, { method: "DELETE" });
288
+ if (res.ok) {
289
+ setConnections(connections.filter(c => c.id !== id));
290
+ }
291
+ } catch (error) {
292
+ console.log("Error deleting connection:", error);
293
+ }
294
+ };
295
+
296
+ const handleOAuthSuccess = () => {
297
+ fetchConnections();
298
+ setShowOAuthModal(false);
299
+ };
300
+
301
+ const handleSaveApiKey = async (formData) => {
302
+ try {
303
+ const res = await fetch("/api/providers", {
304
+ method: "POST",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify({ provider: providerId, ...formData }),
307
+ });
308
+ if (res.ok) {
309
+ await fetchConnections();
310
+ setShowAddApiKeyModal(false);
311
+ }
312
+ } catch (error) {
313
+ console.log("Error saving connection:", error);
314
+ }
315
+ };
316
+
317
+ const handleUpdateConnection = async (formData) => {
318
+ try {
319
+ const res = await fetch(`/api/providers/${selectedConnection.id}`, {
320
+ method: "PUT",
321
+ headers: { "Content-Type": "application/json" },
322
+ body: JSON.stringify(formData),
323
+ });
324
+ if (res.ok) {
325
+ await fetchConnections();
326
+ setShowEditModal(false);
327
+ }
328
+ } catch (error) {
329
+ console.log("Error updating connection:", error);
330
+ }
331
+ };
332
+
333
+ const handleUpdateConnectionStatus = async (id, isActive) => {
334
+ try {
335
+ const res = await fetch(`/api/providers/${id}`, {
336
+ method: "PUT",
337
+ headers: { "Content-Type": "application/json" },
338
+ body: JSON.stringify({ isActive }),
339
+ });
340
+ if (res.ok) {
341
+ setConnections(prev => prev.map(c => c.id === id ? { ...c, isActive } : c));
342
+ }
343
+ } catch (error) {
344
+ console.log("Error updating connection status:", error);
345
+ }
346
+ };
347
+
348
+ const handleSwapPriority = async (conn1, conn2) => {
349
+ if (!conn1 || !conn2) return;
350
+ try {
351
+ // If they have the same priority, we need to ensure the one moving up
352
+ // gets a lower value than the one moving down.
353
+ // We use a small offset which the backend re-indexing will fix.
354
+ let p1 = conn2.priority;
355
+ let p2 = conn1.priority;
356
+
357
+ if (p1 === p2) {
358
+ // If moving conn1 "up" (index decreases)
359
+ const isConn1MovingUp = connections.indexOf(conn1) > connections.indexOf(conn2);
360
+ if (isConn1MovingUp) {
361
+ p1 = conn2.priority - 0.5;
362
+ } else {
363
+ p1 = conn2.priority + 0.5;
364
+ }
365
+ }
366
+
367
+ await Promise.all([
368
+ fetch(`/api/providers/${conn1.id}`, {
369
+ method: "PUT",
370
+ headers: { "Content-Type": "application/json" },
371
+ body: JSON.stringify({ priority: p1 }),
372
+ }),
373
+ fetch(`/api/providers/${conn2.id}`, {
374
+ method: "PUT",
375
+ headers: { "Content-Type": "application/json" },
376
+ body: JSON.stringify({ priority: p2 }),
377
+ }),
378
+ ]);
379
+ await fetchConnections();
380
+ } catch (error) {
381
+ console.log("Error swapping priority:", error);
382
+ }
383
+ };
384
+
385
+ const renderModelsSection = () => {
386
+ if (isCompatible) {
387
+ return (
388
+ <CompatibleModelsSection
389
+ providerStorageAlias={providerStorageAlias}
390
+ providerDisplayAlias={providerDisplayAlias}
391
+ modelAliases={modelAliases}
392
+ copied={copied}
393
+ onCopy={copy}
394
+ onSetAlias={handleSetAlias}
395
+ onDeleteAlias={handleDeleteAlias}
396
+ connections={connections}
397
+ isAnthropic={isAnthropicCompatible}
398
+ />
399
+ );
400
+ }
401
+ if (providerInfo.passthroughModels) {
402
+ return (
403
+ <PassthroughModelsSection
404
+ providerAlias={providerAlias}
405
+ modelAliases={modelAliases}
406
+ copied={copied}
407
+ onCopy={copy}
408
+ onSetAlias={handleSetAlias}
409
+ onDeleteAlias={handleDeleteAlias}
410
+ />
411
+ );
412
+ }
413
+
414
+ const availableModels = allProviderModels;
415
+ if (availableModels.length === 0) {
416
+ if (loadingRemoteModels) {
417
+ return <p className="text-sm text-text-muted">Loading models from provider...</p>;
418
+ }
419
+ return <p className="text-sm text-text-muted">No models configured</p>;
420
+ }
421
+
422
+ const selectedSet = new Set(selectedModelIds);
423
+ const filteredBySelection = showSelectedOnly
424
+ ? availableModels.filter((model) => selectedSet.has(model.id))
425
+ : availableModels;
426
+ const query = modelSearchQuery.trim().toLowerCase();
427
+ const visibleModels = query
428
+ ? filteredBySelection.filter((model) =>
429
+ model.id.toLowerCase().includes(query) ||
430
+ (model.name || "").toLowerCase().includes(query)
431
+ )
432
+ : filteredBySelection;
433
+ const hasSelectionChanges =
434
+ savedEnabledModels.length !== selectedModelIds.length ||
435
+ savedEnabledModels.some((modelId) => !selectedSet.has(modelId));
436
+
437
+ return (
438
+ <div className="flex flex-col gap-3">
439
+ <div className="flex flex-wrap items-center gap-2">
440
+ <div className="relative flex-1 min-w-55">
441
+ <Input
442
+ value={modelSearchQuery}
443
+ onChange={(e) => setModelSearchQuery(e.target.value)}
444
+ placeholder="Search model id"
445
+ className="pr-8"
446
+ />
447
+ {modelSearchQuery && (
448
+ <button
449
+ type="button"
450
+ onClick={() => setModelSearchQuery("")}
451
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary"
452
+ title="Clear search"
453
+ >
454
+ <span className="material-symbols-outlined text-[16px]">close</span>
455
+ </button>
456
+ )}
457
+ </div>
458
+ <Button
459
+ size="sm"
460
+ variant="secondary"
461
+ onClick={() => setSelectedModelIds(allProviderModelIds)}
462
+ disabled={allProviderModels.length === 0}
463
+ >
464
+ Select all
465
+ </Button>
466
+ <Button
467
+ size="sm"
468
+ variant="secondary"
469
+ onClick={() => setSelectedModelIds([])}
470
+ >
471
+ Unselect all
472
+ </Button>
473
+ <Button
474
+ size="sm"
475
+ onClick={handleSaveSelectedModels}
476
+ disabled={!activeConnection || savingSelectedModels || !hasSelectionChanges}
477
+ >
478
+ {savingSelectedModels ? "Saving..." : "Save selection"}
479
+ </Button>
480
+ <div className="flex items-center gap-1 pl-2">
481
+ <span className="text-xs text-text-muted">Selected only</span>
482
+ <Toggle
483
+ size="sm"
484
+ checked={showSelectedOnly}
485
+ onChange={setShowSelectedOnly}
486
+ title="Show only selected models"
487
+ />
488
+ </div>
489
+ </div>
490
+
491
+ <p className="text-xs text-text-muted">
492
+ {selectedModelIds.length > 0
493
+ ? `${selectedModelIds.length} selected`
494
+ : "All models enabled"}
495
+ </p>
496
+
497
+ {visibleModels.length === 0 ? (
498
+ <p className="text-sm text-text-muted">No models match your filter.</p>
499
+ ) : (
500
+ <div className="flex flex-wrap gap-3">
501
+ {visibleModels.map((model) => {
502
+ const fullModel = `${providerStorageAlias}/${model.id}`;
503
+ const oldFormatModel = `${providerId}/${model.id}`;
504
+ const existingAlias = Object.entries(modelAliases).find(
505
+ ([, m]) => m === fullModel || m === oldFormatModel
506
+ )?.[0];
507
+ return (
508
+ <ModelRow
509
+ key={model.id}
510
+ model={model}
511
+ fullModel={`${providerDisplayAlias}/${model.id}`}
512
+ alias={existingAlias}
513
+ copied={copied}
514
+ onCopy={copy}
515
+ selected={selectedSet.has(model.id)}
516
+ onToggleSelect={() => handleToggleModelSelected(model.id)}
517
+ onSetAlias={(alias) => handleSetAlias(model.id, alias, providerStorageAlias)}
518
+ onDeleteAlias={() => handleDeleteAlias(existingAlias)}
519
+ />
520
+ );
521
+ })}
522
+ </div>
523
+ )}
524
+ </div>
525
+ );
526
+ };
527
+
528
+ if (loading) {
529
+ return (
530
+ <div className="flex flex-col gap-8">
531
+ <CardSkeleton />
532
+ <CardSkeleton />
533
+ </div>
534
+ );
535
+ }
536
+
537
+ if (!providerInfo) {
538
+ return (
539
+ <div className="text-center py-20">
540
+ <p className="text-text-muted">Provider not found</p>
541
+ <Link href="/dashboard/providers" className="text-primary mt-4 inline-block">
542
+ Back to Providers
543
+ </Link>
544
+ </div>
545
+ );
546
+ }
547
+
548
+ // Determine icon path: OpenAI Compatible providers use specialized icons
549
+ const getHeaderIconPath = () => {
550
+ if (isOpenAICompatible && providerInfo.apiType) {
551
+ return providerInfo.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png";
552
+ }
553
+ if (isAnthropicCompatible) {
554
+ return "/providers/anthropic-m.png";
555
+ }
556
+ return `/providers/${providerInfo.id}.png`;
557
+ };
558
+
559
+ return (
560
+ <div className="flex flex-col gap-8">
561
+ {/* Header */}
562
+ <div>
563
+ <Link
564
+ href="/dashboard/providers"
565
+ className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary transition-colors mb-4"
566
+ >
567
+ <span className="material-symbols-outlined text-lg">arrow_back</span>
568
+ Back to Providers
569
+ </Link>
570
+ <div className="flex items-center gap-4">
571
+ <div
572
+ className="rounded-lg flex items-center justify-center"
573
+ style={{ backgroundColor: `${providerInfo.color}15` }}
574
+ >
575
+ {headerImgError ? (
576
+ <span className="text-sm font-bold" style={{ color: providerInfo.color }}>
577
+ {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()}
578
+ </span>
579
+ ) : (
580
+ <Image
581
+ src={getHeaderIconPath()}
582
+ alt={providerInfo.name}
583
+ width={48}
584
+ height={48}
585
+ className="object-contain rounded-lg max-w-[48px] max-h-[48px]"
586
+ sizes="48px"
587
+ onError={() => setHeaderImgError(true)}
588
+ />
589
+ )}
590
+ </div>
591
+ <div>
592
+ <h1 className="text-3xl font-semibold tracking-tight">{providerInfo.name}</h1>
593
+ <p className="text-text-muted">
594
+ {connections.length} connection{connections.length === 1 ? "" : "s"}
595
+ </p>
596
+ </div>
597
+ </div>
598
+ </div>
599
+
600
+ {isCompatible && providerNode && (
601
+ <Card>
602
+ <div className="flex items-center justify-between mb-4">
603
+ <div>
604
+ <h2 className="text-lg font-semibold">{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}</h2>
605
+ <p className="text-sm text-text-muted">
606
+ {isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
607
+ {isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")}
608
+ </p>
609
+ </div>
610
+ <div className="flex items-center gap-2">
611
+ <Button
612
+ size="sm"
613
+ icon="add"
614
+ onClick={() => setShowAddApiKeyModal(true)}
615
+ disabled={connections.length > 0}
616
+ >
617
+ Add
618
+ </Button>
619
+ <Button
620
+ size="sm"
621
+ variant="secondary"
622
+ icon="edit"
623
+ onClick={() => setShowEditNodeModal(true)}
624
+ >
625
+ Edit
626
+ </Button>
627
+ <Button
628
+ size="sm"
629
+ variant="secondary"
630
+ icon="delete"
631
+ onClick={async () => {
632
+ if (!confirm(`Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`)) return;
633
+ try {
634
+ const res = await fetch(`/api/provider-nodes/${providerId}`, { method: "DELETE" });
635
+ if (res.ok) {
636
+ router.push("/dashboard/providers");
637
+ }
638
+ } catch (error) {
639
+ console.log("Error deleting provider node:", error);
640
+ }
641
+ }}
642
+ >
643
+ Delete
644
+ </Button>
645
+ </div>
646
+ </div>
647
+ {connections.length > 0 && (
648
+ <p className="text-sm text-text-muted">
649
+ Only one connection is allowed per compatible node. Add another node if you need more connections.
650
+ </p>
651
+ )}
652
+ </Card>
653
+ )}
654
+
655
+ {/* Connections */}
656
+ <Card>
657
+ <div className="flex items-center justify-between mb-4">
658
+ <h2 className="text-lg font-semibold">Connections</h2>
659
+ {!isCompatible && (
660
+ <Button
661
+ size="sm"
662
+ icon="add"
663
+ onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}
664
+ >
665
+ Add
666
+ </Button>
667
+ )}
668
+ </div>
669
+
670
+ {connections.length === 0 ? (
671
+ <div className="text-center py-12">
672
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
673
+ <span className="material-symbols-outlined text-[32px]">{isOAuth ? "lock" : "key"}</span>
674
+ </div>
675
+ <p className="text-text-main font-medium mb-1">No connections yet</p>
676
+ <p className="text-sm text-text-muted mb-4">Add your first connection to get started</p>
677
+ {!isCompatible && (
678
+ <Button icon="add" onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}>
679
+ Add Connection
680
+ </Button>
681
+ )}
682
+ </div>
683
+ ) : (
684
+ <div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
685
+ {connections
686
+ .sort((a, b) => (a.priority || 0) - (b.priority || 0))
687
+ .map((conn, index) => (
688
+ <ConnectionRow
689
+ key={conn.id}
690
+ connection={conn}
691
+ isOAuth={isOAuth}
692
+ isFirst={index === 0}
693
+ isLast={index === connections.length - 1}
694
+ onMoveUp={() => handleSwapPriority(conn, connections[index - 1])}
695
+ onMoveDown={() => handleSwapPriority(conn, connections[index + 1])}
696
+ onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)}
697
+ onEdit={() => {
698
+ setSelectedConnection(conn);
699
+ setShowEditModal(true);
700
+ }}
701
+ onDelete={() => handleDelete(conn.id)}
702
+ />
703
+ ))}
704
+ </div>
705
+ )}
706
+ </Card>
707
+
708
+ {/* Models */}
709
+ <Card>
710
+ <h2 className="text-lg font-semibold mb-4">
711
+ {providerInfo.passthroughModels ? "Model Aliases" : "Available Models"}
712
+ </h2>
713
+ {renderModelsSection()}
714
+
715
+ </Card>
716
+
717
+ {/* Modals */}
718
+ {providerId === "kiro" ? (
719
+ <KiroOAuthWrapper
720
+ isOpen={showOAuthModal}
721
+ providerInfo={providerInfo}
722
+ onSuccess={handleOAuthSuccess}
723
+ onClose={() => setShowOAuthModal(false)}
724
+ />
725
+ ) : providerId === "cursor" ? (
726
+ <CursorAuthModal
727
+ isOpen={showOAuthModal}
728
+ onSuccess={handleOAuthSuccess}
729
+ onClose={() => setShowOAuthModal(false)}
730
+ />
731
+ ) : (
732
+ <OAuthModal
733
+ isOpen={showOAuthModal}
734
+ provider={providerId}
735
+ providerInfo={providerInfo}
736
+ onSuccess={handleOAuthSuccess}
737
+ onClose={() => setShowOAuthModal(false)}
738
+ />
739
+ )}
740
+ <AddApiKeyModal
741
+ isOpen={showAddApiKeyModal}
742
+ provider={providerId}
743
+ providerName={providerInfo.name}
744
+ isCompatible={isCompatible}
745
+ isAnthropic={isAnthropicCompatible}
746
+ onSave={handleSaveApiKey}
747
+ onClose={() => setShowAddApiKeyModal(false)}
748
+ />
749
+ <EditConnectionModal
750
+ isOpen={showEditModal}
751
+ connection={selectedConnection}
752
+ onSave={handleUpdateConnection}
753
+ onClose={() => setShowEditModal(false)}
754
+ />
755
+ {isCompatible && (
756
+ <EditCompatibleNodeModal
757
+ isOpen={showEditNodeModal}
758
+ node={providerNode}
759
+ onSave={handleUpdateNode}
760
+ onClose={() => setShowEditNodeModal(false)}
761
+ isAnthropic={isAnthropicCompatible}
762
+ />
763
+ )}
764
+ </div>
765
+ );
766
+ }
767
+
768
+ function ModelRow({ model, fullModel, alias, selected, onToggleSelect, copied, onCopy }) {
769
+ return (
770
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-border hover:bg-sidebar/50">
771
+ <button
772
+ type="button"
773
+ onClick={onToggleSelect}
774
+ className={`p-0.5 rounded ${selected ? "text-primary" : "text-text-muted hover:text-primary"}`}
775
+ title={selected ? "Deselect model" : "Select model"}
776
+ >
777
+ <span className="material-symbols-outlined text-[18px]">
778
+ {selected ? "check_box" : "check_box_outline_blank"}
779
+ </span>
780
+ </button>
781
+ <span className="material-symbols-outlined text-base text-text-muted">smart_toy</span>
782
+ <code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
783
+ <button
784
+ onClick={() => onCopy(fullModel, `model-${model.id}`)}
785
+ className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
786
+ title="Copy model"
787
+ >
788
+ <span className="material-symbols-outlined text-sm">
789
+ {copied === `model-${model.id}` ? "check" : "content_copy"}
790
+ </span>
791
+ </button>
792
+ </div>
793
+ );
794
+ }
795
+
796
+ ModelRow.propTypes = {
797
+ model: PropTypes.shape({
798
+ id: PropTypes.string.isRequired,
799
+ }).isRequired,
800
+ fullModel: PropTypes.string.isRequired,
801
+ alias: PropTypes.string,
802
+ selected: PropTypes.bool,
803
+ onToggleSelect: PropTypes.func,
804
+ copied: PropTypes.string,
805
+ onCopy: PropTypes.func.isRequired,
806
+ };
807
+
808
+ function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) {
809
+ const [newModel, setNewModel] = useState("");
810
+ const [adding, setAdding] = useState(false);
811
+
812
+ // Filter aliases for this provider - models are persisted via alias
813
+ const providerAliases = Object.entries(modelAliases).filter(
814
+ ([, model]) => model.startsWith(`${providerAlias}/`)
815
+ );
816
+
817
+ const allModels = providerAliases.map(([alias, fullModel]) => ({
818
+ modelId: fullModel.replace(`${providerAlias}/`, ""),
819
+ fullModel,
820
+ alias,
821
+ }));
822
+
823
+ // Generate default alias from modelId (last part after /)
824
+ const generateDefaultAlias = (modelId) => {
825
+ const parts = modelId.split("/");
826
+ return parts[parts.length - 1];
827
+ };
828
+
829
+ const handleAdd = async () => {
830
+ if (!newModel.trim() || adding) return;
831
+ const modelId = newModel.trim();
832
+ const defaultAlias = generateDefaultAlias(modelId);
833
+
834
+ // Check if alias already exists
835
+ if (modelAliases[defaultAlias]) {
836
+ alert(`Alias "${defaultAlias}" already exists. Please use a different model or edit existing alias.`);
837
+ return;
838
+ }
839
+
840
+ setAdding(true);
841
+ try {
842
+ await onSetAlias(modelId, defaultAlias);
843
+ setNewModel("");
844
+ } catch (error) {
845
+ console.log("Error adding model:", error);
846
+ } finally {
847
+ setAdding(false);
848
+ }
849
+ };
850
+
851
+ return (
852
+ <div className="flex flex-col gap-4">
853
+ <p className="text-sm text-text-muted">
854
+ OpenRouter supports any model. Add models and create aliases for quick access.
855
+ </p>
856
+
857
+ {/* Add new model */}
858
+ <div className="flex items-end gap-2">
859
+ <div className="flex-1">
860
+ <label htmlFor="new-model-input" className="text-xs text-text-muted mb-1 block">Model ID (from OpenRouter)</label>
861
+ <input
862
+ id="new-model-input"
863
+ type="text"
864
+ value={newModel}
865
+ onChange={(e) => setNewModel(e.target.value)}
866
+ onKeyDown={(e) => e.key === "Enter" && handleAdd()}
867
+ placeholder="anthropic/claude-3-opus"
868
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
869
+ />
870
+ </div>
871
+ <Button size="sm" icon="add" onClick={handleAdd} disabled={!newModel.trim() || adding}>
872
+ {adding ? "Adding..." : "Add"}
873
+ </Button>
874
+ </div>
875
+
876
+ {/* Models list */}
877
+ {allModels.length > 0 && (
878
+ <div className="flex flex-col gap-3">
879
+ {allModels.map(({ modelId, fullModel, alias }) => (
880
+ <PassthroughModelRow
881
+ key={fullModel}
882
+ modelId={modelId}
883
+ fullModel={fullModel}
884
+ copied={copied}
885
+ onCopy={onCopy}
886
+ onDeleteAlias={() => onDeleteAlias(alias)}
887
+ />
888
+ ))}
889
+ </div>
890
+ )}
891
+ </div>
892
+ );
893
+ }
894
+
895
+ PassthroughModelsSection.propTypes = {
896
+ providerAlias: PropTypes.string.isRequired,
897
+ modelAliases: PropTypes.object.isRequired,
898
+ copied: PropTypes.string,
899
+ onCopy: PropTypes.func.isRequired,
900
+ onSetAlias: PropTypes.func.isRequired,
901
+ onDeleteAlias: PropTypes.func.isRequired,
902
+ };
903
+
904
+ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias }) {
905
+ return (
906
+ <div className="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-sidebar/50">
907
+ <span className="material-symbols-outlined text-base text-text-muted">smart_toy</span>
908
+
909
+ <div className="flex-1 min-w-0">
910
+ <p className="text-sm font-medium truncate">{modelId}</p>
911
+
912
+ <div className="flex items-center gap-1 mt-1">
913
+ <code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
914
+ <button
915
+ onClick={() => onCopy(fullModel, `model-${modelId}`)}
916
+ className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
917
+ title="Copy model"
918
+ >
919
+ <span className="material-symbols-outlined text-sm">
920
+ {copied === `model-${modelId}` ? "check" : "content_copy"}
921
+ </span>
922
+ </button>
923
+ </div>
924
+ </div>
925
+
926
+ {/* Delete button */}
927
+ <button
928
+ onClick={onDeleteAlias}
929
+ className="p-1 hover:bg-red-50 rounded text-red-500"
930
+ title="Remove model"
931
+ >
932
+ <span className="material-symbols-outlined text-sm">delete</span>
933
+ </button>
934
+ </div>
935
+ );
936
+ }
937
+
938
+ PassthroughModelRow.propTypes = {
939
+ modelId: PropTypes.string.isRequired,
940
+ fullModel: PropTypes.string.isRequired,
941
+ copied: PropTypes.string,
942
+ onCopy: PropTypes.func.isRequired,
943
+ onDeleteAlias: PropTypes.func.isRequired,
944
+ };
945
+
946
+ function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) {
947
+ const [newModel, setNewModel] = useState("");
948
+ const [adding, setAdding] = useState(false);
949
+ const [importing, setImporting] = useState(false);
950
+
951
+ const providerAliases = Object.entries(modelAliases).filter(
952
+ ([, model]) => model.startsWith(`${providerStorageAlias}/`)
953
+ );
954
+
955
+ const allModels = providerAliases.map(([alias, fullModel]) => ({
956
+ modelId: fullModel.replace(`${providerStorageAlias}/`, ""),
957
+ fullModel,
958
+ alias,
959
+ }));
960
+
961
+ const generateDefaultAlias = (modelId) => {
962
+ const parts = modelId.split("/");
963
+ return parts[parts.length - 1];
964
+ };
965
+
966
+ const resolveAlias = (modelId) => {
967
+ const baseAlias = generateDefaultAlias(modelId);
968
+ if (!modelAliases[baseAlias]) return baseAlias;
969
+ const prefixedAlias = `${providerDisplayAlias}-${baseAlias}`;
970
+ if (!modelAliases[prefixedAlias]) return prefixedAlias;
971
+ return null;
972
+ };
973
+
974
+ const handleAdd = async () => {
975
+ if (!newModel.trim() || adding) return;
976
+ const modelId = newModel.trim();
977
+ const resolvedAlias = resolveAlias(modelId);
978
+ if (!resolvedAlias) {
979
+ alert("All suggested aliases already exist. Please choose a different model or remove conflicting aliases.");
980
+ return;
981
+ }
982
+
983
+ setAdding(true);
984
+ try {
985
+ await onSetAlias(modelId, resolvedAlias, providerStorageAlias);
986
+ setNewModel("");
987
+ } catch (error) {
988
+ console.log("Error adding model:", error);
989
+ } finally {
990
+ setAdding(false);
991
+ }
992
+ };
993
+
994
+ const handleImport = async () => {
995
+ if (importing) return;
996
+ const activeConnection = connections.find((conn) => conn.isActive !== false);
997
+ if (!activeConnection) return;
998
+
999
+ setImporting(true);
1000
+ try {
1001
+ const res = await fetch(`/api/providers/${activeConnection.id}/models`);
1002
+ const data = await res.json();
1003
+ if (!res.ok) {
1004
+ alert(data.error || "Failed to import models");
1005
+ return;
1006
+ }
1007
+ const models = data.models || [];
1008
+ if (models.length === 0) {
1009
+ alert("No models returned from /models.");
1010
+ return;
1011
+ }
1012
+ let importedCount = 0;
1013
+ for (const model of models) {
1014
+ const modelId = model.id || model.name || model.model;
1015
+ if (!modelId) continue;
1016
+ const resolvedAlias = resolveAlias(modelId);
1017
+ if (!resolvedAlias) continue;
1018
+ await onSetAlias(modelId, resolvedAlias, providerStorageAlias);
1019
+ importedCount += 1;
1020
+ }
1021
+ if (importedCount === 0) {
1022
+ alert("No new models were added.");
1023
+ }
1024
+ } catch (error) {
1025
+ console.log("Error importing models:", error);
1026
+ } finally {
1027
+ setImporting(false);
1028
+ }
1029
+ };
1030
+
1031
+ const canImport = connections.some((conn) => conn.isActive !== false);
1032
+
1033
+ return (
1034
+ <div className="flex flex-col gap-4">
1035
+ <p className="text-sm text-text-muted">
1036
+ Add {isAnthropic ? "Anthropic" : "OpenAI"}-compatible models manually or import them from the /models endpoint.
1037
+ </p>
1038
+
1039
+ <div className="flex items-end gap-2 flex-wrap">
1040
+ <div className="flex-1 min-w-[240px]">
1041
+ <label htmlFor="new-compatible-model-input" className="text-xs text-text-muted mb-1 block">Model ID</label>
1042
+ <input
1043
+ id="new-compatible-model-input"
1044
+ type="text"
1045
+ value={newModel}
1046
+ onChange={(e) => setNewModel(e.target.value)}
1047
+ onKeyDown={(e) => e.key === "Enter" && handleAdd()}
1048
+ placeholder={isAnthropic ? "claude-3-opus-20240229" : "gpt-4o"}
1049
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
1050
+ />
1051
+ </div>
1052
+ <Button size="sm" icon="add" onClick={handleAdd} disabled={!newModel.trim() || adding}>
1053
+ {adding ? "Adding..." : "Add"}
1054
+ </Button>
1055
+ <Button size="sm" variant="secondary" icon="download" onClick={handleImport} disabled={!canImport || importing}>
1056
+ {importing ? "Importing..." : "Import from /models"}
1057
+ </Button>
1058
+ </div>
1059
+
1060
+ {!canImport && (
1061
+ <p className="text-xs text-text-muted">
1062
+ Add a connection to enable importing models.
1063
+ </p>
1064
+ )}
1065
+
1066
+ {allModels.length > 0 && (
1067
+ <div className="flex flex-col gap-3">
1068
+ {allModels.map(({ modelId, fullModel, alias }) => (
1069
+ <PassthroughModelRow
1070
+ key={fullModel}
1071
+ modelId={modelId}
1072
+ fullModel={`${providerDisplayAlias}/${modelId}`}
1073
+ copied={copied}
1074
+ onCopy={onCopy}
1075
+ onDeleteAlias={() => onDeleteAlias(alias)}
1076
+ />
1077
+ ))}
1078
+ </div>
1079
+ )}
1080
+ </div>
1081
+ );
1082
+ }
1083
+
1084
+ CompatibleModelsSection.propTypes = {
1085
+ providerStorageAlias: PropTypes.string.isRequired,
1086
+ providerDisplayAlias: PropTypes.string.isRequired,
1087
+ modelAliases: PropTypes.object.isRequired,
1088
+ copied: PropTypes.string,
1089
+ onCopy: PropTypes.func.isRequired,
1090
+ onSetAlias: PropTypes.func.isRequired,
1091
+ onDeleteAlias: PropTypes.func.isRequired,
1092
+ connections: PropTypes.arrayOf(PropTypes.shape({
1093
+ id: PropTypes.string,
1094
+ isActive: PropTypes.bool,
1095
+ })).isRequired,
1096
+ isAnthropic: PropTypes.bool,
1097
+ };
1098
+
1099
+ function CooldownTimer({ until }) {
1100
+ const [remaining, setRemaining] = useState("");
1101
+
1102
+ useEffect(() => {
1103
+ const updateRemaining = () => {
1104
+ const diff = new Date(until).getTime() - Date.now();
1105
+ if (diff <= 0) {
1106
+ setRemaining("");
1107
+ return;
1108
+ }
1109
+ const secs = Math.floor(diff / 1000);
1110
+ if (secs < 60) {
1111
+ setRemaining(`${secs}s`);
1112
+ } else if (secs < 3600) {
1113
+ setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`);
1114
+ } else {
1115
+ const hrs = Math.floor(secs / 3600);
1116
+ const mins = Math.floor((secs % 3600) / 60);
1117
+ setRemaining(`${hrs}h ${mins}m`);
1118
+ }
1119
+ };
1120
+
1121
+ updateRemaining();
1122
+ const interval = setInterval(updateRemaining, 1000);
1123
+ return () => clearInterval(interval);
1124
+ }, [until]);
1125
+
1126
+ if (!remaining) return null;
1127
+
1128
+ return (
1129
+ <span className="text-xs text-orange-500 font-mono">
1130
+ ⏱ {remaining}
1131
+ </span>
1132
+ );
1133
+ }
1134
+
1135
+ CooldownTimer.propTypes = {
1136
+ until: PropTypes.string.isRequired,
1137
+ };
1138
+
1139
+ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onEdit, onDelete }) {
1140
+ const displayName = isOAuth
1141
+ ? connection.name || connection.email || connection.displayName || "OAuth Account"
1142
+ : connection.name;
1143
+
1144
+ // Use useState + useEffect for impure Date.now() to avoid calling during render
1145
+ const [isCooldown, setIsCooldown] = useState(false);
1146
+
1147
+ const modelLockUntil = Object.entries(connection)
1148
+ .filter(([k]) => k.startsWith("modelLock_"))
1149
+ .map(([, v]) => v)
1150
+ .filter(v => v && new Date(v).getTime() > Date.now())
1151
+ .sort()[0] || null;
1152
+
1153
+ useEffect(() => {
1154
+ const checkCooldown = () => {
1155
+ const until = Object.entries(connection)
1156
+ .filter(([k]) => k.startsWith("modelLock_"))
1157
+ .map(([, v]) => v)
1158
+ .filter(v => v && new Date(v).getTime() > Date.now())
1159
+ .sort()[0] || null;
1160
+ setIsCooldown(!!until);
1161
+ };
1162
+
1163
+ checkCooldown();
1164
+ const interval = modelLockUntil ? setInterval(checkCooldown, 1000) : null;
1165
+ return () => {
1166
+ if (interval) clearInterval(interval);
1167
+ };
1168
+ }, [modelLockUntil]);
1169
+
1170
+ // Determine effective status (override unavailable if cooldown expired)
1171
+ const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown)
1172
+ ? "active" // Cooldown expired → treat as active
1173
+ : connection.testStatus;
1174
+
1175
+ const getStatusVariant = () => {
1176
+ if (connection.isActive === false) return "default";
1177
+ if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
1178
+ if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
1179
+ return "default";
1180
+ };
1181
+
1182
+ return (
1183
+ <div className={`group flex items-center justify-between p-3 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
1184
+ <div className="flex items-center gap-3 flex-1 min-w-0">
1185
+ {/* Priority arrows */}
1186
+ <div className="flex flex-col">
1187
+ <button
1188
+ onClick={onMoveUp}
1189
+ disabled={isFirst}
1190
+ className={`p-0.5 rounded ${isFirst ? "text-text-muted/30 cursor-not-allowed" : "hover:bg-sidebar text-text-muted hover:text-primary"}`}
1191
+ >
1192
+ <span className="material-symbols-outlined text-sm">keyboard_arrow_up</span>
1193
+ </button>
1194
+ <button
1195
+ onClick={onMoveDown}
1196
+ disabled={isLast}
1197
+ className={`p-0.5 rounded ${isLast ? "text-text-muted/30 cursor-not-allowed" : "hover:bg-sidebar text-text-muted hover:text-primary"}`}
1198
+ >
1199
+ <span className="material-symbols-outlined text-sm">keyboard_arrow_down</span>
1200
+ </button>
1201
+ </div>
1202
+ <span className="material-symbols-outlined text-base text-text-muted">
1203
+ {isOAuth ? "lock" : "key"}
1204
+ </span>
1205
+ <div className="flex-1 min-w-0">
1206
+ <p className="text-sm font-medium truncate">{displayName}</p>
1207
+ <div className="flex items-center gap-2 mt-1">
1208
+ <Badge variant={getStatusVariant()} size="sm" dot>
1209
+ {connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
1210
+ </Badge>
1211
+ {isCooldown && connection.isActive !== false && <CooldownTimer until={modelLockUntil} />}
1212
+ {connection.lastError && connection.isActive !== false && (
1213
+ <span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>
1214
+ {connection.lastError}
1215
+ </span>
1216
+ )}
1217
+ <span className="text-xs text-text-muted">#{connection.priority}</span>
1218
+ {connection.globalPriority && (
1219
+ <span className="text-xs text-text-muted">Auto: {connection.globalPriority}</span>
1220
+ )}
1221
+ </div>
1222
+ </div>
1223
+ </div>
1224
+ <div className="flex items-center gap-2">
1225
+ <Toggle
1226
+ size="sm"
1227
+ checked={connection.isActive ?? true}
1228
+ onChange={onToggleActive}
1229
+ title={(connection.isActive ?? true) ? "Disable connection" : "Enable connection"}
1230
+ />
1231
+ <div className="flex gap-1 ml-1 opacity-0 group-hover:opacity-100 transition-opacity">
1232
+ <button onClick={onEdit} className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary">
1233
+ <span className="material-symbols-outlined text-[18px]">edit</span>
1234
+ </button>
1235
+ <button onClick={onDelete} className="p-2 hover:bg-red-500/10 rounded text-red-500">
1236
+ <span className="material-symbols-outlined text-[18px]">delete</span>
1237
+ </button>
1238
+ </div>
1239
+ </div>
1240
+ </div>
1241
+ );
1242
+ }
1243
+
1244
+ ConnectionRow.propTypes = {
1245
+ connection: PropTypes.shape({
1246
+ id: PropTypes.string,
1247
+ name: PropTypes.string,
1248
+ email: PropTypes.string,
1249
+ displayName: PropTypes.string,
1250
+ modelLockUntil: PropTypes.string,
1251
+ testStatus: PropTypes.string,
1252
+ isActive: PropTypes.bool,
1253
+ lastError: PropTypes.string,
1254
+ priority: PropTypes.number,
1255
+ globalPriority: PropTypes.number,
1256
+ }).isRequired,
1257
+ isOAuth: PropTypes.bool.isRequired,
1258
+ isFirst: PropTypes.bool.isRequired,
1259
+ isLast: PropTypes.bool.isRequired,
1260
+ onMoveUp: PropTypes.func.isRequired,
1261
+ onMoveDown: PropTypes.func.isRequired,
1262
+ onToggleActive: PropTypes.func.isRequired,
1263
+ onEdit: PropTypes.func.isRequired,
1264
+ onDelete: PropTypes.func.isRequired,
1265
+ };
1266
+
1267
+ function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, onSave, onClose }) {
1268
+ const [formData, setFormData] = useState({
1269
+ name: "",
1270
+ apiKey: "",
1271
+ priority: 1,
1272
+ });
1273
+ const [validating, setValidating] = useState(false);
1274
+ const [validationResult, setValidationResult] = useState(null);
1275
+ const [saving, setSaving] = useState(false);
1276
+
1277
+ const handleValidate = async () => {
1278
+ setValidating(true);
1279
+ try {
1280
+ const res = await fetch("/api/providers/validate", {
1281
+ method: "POST",
1282
+ headers: { "Content-Type": "application/json" },
1283
+ body: JSON.stringify({ provider, apiKey: formData.apiKey }),
1284
+ });
1285
+ const data = await res.json();
1286
+ setValidationResult(data.valid ? "success" : "failed");
1287
+ } catch {
1288
+ setValidationResult("failed");
1289
+ } finally {
1290
+ setValidating(false);
1291
+ }
1292
+ };
1293
+
1294
+ const handleSubmit = async () => {
1295
+ if (!provider || !formData.apiKey) return;
1296
+
1297
+ setSaving(true);
1298
+ try {
1299
+ let isValid = false;
1300
+ try {
1301
+ setValidating(true);
1302
+ setValidationResult(null);
1303
+ const res = await fetch("/api/providers/validate", {
1304
+ method: "POST",
1305
+ headers: { "Content-Type": "application/json" },
1306
+ body: JSON.stringify({ provider, apiKey: formData.apiKey }),
1307
+ });
1308
+ const data = await res.json();
1309
+ isValid = !!data.valid;
1310
+ setValidationResult(isValid ? "success" : "failed");
1311
+ } catch {
1312
+ setValidationResult("failed");
1313
+ } finally {
1314
+ setValidating(false);
1315
+ }
1316
+
1317
+ await onSave({
1318
+ name: formData.name,
1319
+ apiKey: formData.apiKey,
1320
+ priority: formData.priority,
1321
+ testStatus: isValid ? "active" : "unknown",
1322
+ });
1323
+ } finally {
1324
+ setSaving(false);
1325
+ }
1326
+ };
1327
+
1328
+ if (!provider) return null;
1329
+
1330
+ return (
1331
+ <Modal isOpen={isOpen} title={`Add ${providerName || provider} API Key`} onClose={onClose}>
1332
+ <div className="flex flex-col gap-4">
1333
+ <Input
1334
+ label="Name"
1335
+ value={formData.name}
1336
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
1337
+ placeholder="Production Key"
1338
+ />
1339
+ <div className="flex gap-2">
1340
+ <Input
1341
+ label="API Key"
1342
+ type="password"
1343
+ value={formData.apiKey}
1344
+ onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
1345
+ className="flex-1"
1346
+ />
1347
+ <div className="pt-6">
1348
+ <Button onClick={handleValidate} disabled={!formData.apiKey || validating || saving} variant="secondary">
1349
+ {validating ? "Checking..." : "Check"}
1350
+ </Button>
1351
+ </div>
1352
+ </div>
1353
+ {validationResult && (
1354
+ <Badge variant={validationResult === "success" ? "success" : "error"}>
1355
+ {validationResult === "success" ? "Valid" : "Invalid"}
1356
+ </Badge>
1357
+ )}
1358
+ {isCompatible && (
1359
+ <p className="text-xs text-text-muted">
1360
+ {isAnthropic
1361
+ ? `Validation checks ${providerName || "Anthropic Compatible"} by verifying the API key.`
1362
+ : `Validation checks ${providerName || "OpenAI Compatible"} via /models on your base URL.`
1363
+ }
1364
+ </p>
1365
+ )}
1366
+ <Input
1367
+ label="Priority"
1368
+ type="number"
1369
+ value={formData.priority}
1370
+ onChange={(e) => setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })}
1371
+ />
1372
+ <div className="flex gap-2">
1373
+ <Button onClick={handleSubmit} fullWidth disabled={!formData.name || !formData.apiKey || saving}>
1374
+ {saving ? "Saving..." : "Save"}
1375
+ </Button>
1376
+ <Button onClick={onClose} variant="ghost" fullWidth>
1377
+ Cancel
1378
+ </Button>
1379
+ </div>
1380
+ </div>
1381
+ </Modal>
1382
+ );
1383
+ }
1384
+
1385
+ AddApiKeyModal.propTypes = {
1386
+ isOpen: PropTypes.bool.isRequired,
1387
+ provider: PropTypes.string,
1388
+ providerName: PropTypes.string,
1389
+ isCompatible: PropTypes.bool,
1390
+ isAnthropic: PropTypes.bool,
1391
+ onSave: PropTypes.func.isRequired,
1392
+ onClose: PropTypes.func.isRequired,
1393
+ };
1394
+
1395
+ function EditConnectionModal({ isOpen, connection, onSave, onClose }) {
1396
+ const [formData, setFormData] = useState({
1397
+ name: "",
1398
+ priority: 1,
1399
+ apiKey: "",
1400
+ });
1401
+ const [testing, setTesting] = useState(false);
1402
+ const [testResult, setTestResult] = useState(null);
1403
+ const [validating, setValidating] = useState(false);
1404
+ const [validationResult, setValidationResult] = useState(null);
1405
+ const [saving, setSaving] = useState(false);
1406
+
1407
+ useEffect(() => {
1408
+ if (connection) {
1409
+ setFormData({
1410
+ name: connection.name || "",
1411
+ priority: connection.priority || 1,
1412
+ apiKey: "",
1413
+ });
1414
+ setTestResult(null);
1415
+ setValidationResult(null);
1416
+ }
1417
+ }, [connection]);
1418
+
1419
+ const handleTest = async () => {
1420
+ if (!connection?.provider) return;
1421
+ setTesting(true);
1422
+ setTestResult(null);
1423
+ try {
1424
+ const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" });
1425
+ const data = await res.json();
1426
+ setTestResult(data.valid ? "success" : "failed");
1427
+ } catch {
1428
+ setTestResult("failed");
1429
+ } finally {
1430
+ setTesting(false);
1431
+ }
1432
+ };
1433
+
1434
+ const handleValidate = async () => {
1435
+ if (!connection?.provider || !formData.apiKey) return;
1436
+ setValidating(true);
1437
+ setValidationResult(null);
1438
+ try {
1439
+ const res = await fetch("/api/providers/validate", {
1440
+ method: "POST",
1441
+ headers: { "Content-Type": "application/json" },
1442
+ body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }),
1443
+ });
1444
+ const data = await res.json();
1445
+ setValidationResult(data.valid ? "success" : "failed");
1446
+ } catch {
1447
+ setValidationResult("failed");
1448
+ } finally {
1449
+ setValidating(false);
1450
+ }
1451
+ };
1452
+
1453
+ const handleSubmit = async () => {
1454
+ setSaving(true);
1455
+ try {
1456
+ const updates = { name: formData.name, priority: formData.priority };
1457
+ if (!isOAuth && formData.apiKey) {
1458
+ updates.apiKey = formData.apiKey;
1459
+ let isValid = validationResult === "success";
1460
+ if (!isValid) {
1461
+ try {
1462
+ setValidating(true);
1463
+ setValidationResult(null);
1464
+ const res = await fetch("/api/providers/validate", {
1465
+ method: "POST",
1466
+ headers: { "Content-Type": "application/json" },
1467
+ body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }),
1468
+ });
1469
+ const data = await res.json();
1470
+ isValid = !!data.valid;
1471
+ setValidationResult(isValid ? "success" : "failed");
1472
+ } catch {
1473
+ setValidationResult("failed");
1474
+ } finally {
1475
+ setValidating(false);
1476
+ }
1477
+ }
1478
+ if (isValid) {
1479
+ updates.testStatus = "active";
1480
+ updates.lastError = null;
1481
+ updates.lastErrorAt = null;
1482
+ }
1483
+ }
1484
+ await onSave(updates);
1485
+ } finally {
1486
+ setSaving(false);
1487
+ }
1488
+ };
1489
+
1490
+ if (!connection) return null;
1491
+
1492
+ const isOAuth = connection.authType === "oauth";
1493
+ const isCompatible = isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider);
1494
+
1495
+ return (
1496
+ <Modal isOpen={isOpen} title="Edit Connection" onClose={onClose}>
1497
+ <div className="flex flex-col gap-4">
1498
+ <Input
1499
+ label="Name"
1500
+ value={formData.name}
1501
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
1502
+ placeholder={isOAuth ? "Account name" : "Production Key"}
1503
+ />
1504
+ {isOAuth && connection.email && (
1505
+ <div className="bg-sidebar/50 p-3 rounded-lg">
1506
+ <p className="text-sm text-text-muted mb-1">Email</p>
1507
+ <p className="font-medium">{connection.email}</p>
1508
+ </div>
1509
+ )}
1510
+ <Input
1511
+ label="Priority"
1512
+ type="number"
1513
+ value={formData.priority}
1514
+ onChange={(e) => setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })}
1515
+ />
1516
+ {!isOAuth && (
1517
+ <>
1518
+ <div className="flex gap-2">
1519
+ <Input
1520
+ label="API Key"
1521
+ type="password"
1522
+ value={formData.apiKey}
1523
+ onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
1524
+ placeholder="Enter new API key"
1525
+ hint="Leave blank to keep the current API key."
1526
+ className="flex-1"
1527
+ />
1528
+ <div className="pt-6">
1529
+ <Button onClick={handleValidate} disabled={!formData.apiKey || validating || saving} variant="secondary">
1530
+ {validating ? "Checking..." : "Check"}
1531
+ </Button>
1532
+ </div>
1533
+ </div>
1534
+ {validationResult && (
1535
+ <Badge variant={validationResult === "success" ? "success" : "error"}>
1536
+ {validationResult === "success" ? "Valid" : "Invalid"}
1537
+ </Badge>
1538
+ )}
1539
+ </>
1540
+ )}
1541
+
1542
+ {/* Test Connection */}
1543
+ {!isCompatible && (
1544
+ <div className="flex items-center gap-3">
1545
+ <Button onClick={handleTest} variant="secondary" disabled={testing}>
1546
+ {testing ? "Testing..." : "Test Connection"}
1547
+ </Button>
1548
+ {testResult && (
1549
+ <Badge variant={testResult === "success" ? "success" : "error"}>
1550
+ {testResult === "success" ? "Valid" : "Failed"}
1551
+ </Badge>
1552
+ )}
1553
+ </div>
1554
+ )}
1555
+
1556
+ <div className="flex gap-2">
1557
+ <Button onClick={handleSubmit} fullWidth disabled={saving}>{saving ? "Saving..." : "Save"}</Button>
1558
+ <Button onClick={onClose} variant="ghost" fullWidth>Cancel</Button>
1559
+ </div>
1560
+ </div>
1561
+ </Modal>
1562
+ );
1563
+ }
1564
+
1565
+ EditConnectionModal.propTypes = {
1566
+ isOpen: PropTypes.bool.isRequired,
1567
+ connection: PropTypes.shape({
1568
+ id: PropTypes.string,
1569
+ name: PropTypes.string,
1570
+ email: PropTypes.string,
1571
+ priority: PropTypes.number,
1572
+ authType: PropTypes.string,
1573
+ provider: PropTypes.string,
1574
+ }),
1575
+ onSave: PropTypes.func.isRequired,
1576
+ onClose: PropTypes.func.isRequired,
1577
+ };
1578
+
1579
+ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) {
1580
+ const [formData, setFormData] = useState({
1581
+ name: "",
1582
+ prefix: "",
1583
+ apiType: "chat",
1584
+ baseUrl: "https://api.openai.com/v1",
1585
+ });
1586
+ const [saving, setSaving] = useState(false);
1587
+ const [checkKey, setCheckKey] = useState("");
1588
+ const [validating, setValidating] = useState(false);
1589
+ const [validationResult, setValidationResult] = useState(null);
1590
+
1591
+ useEffect(() => {
1592
+ if (node) {
1593
+ setFormData({
1594
+ name: node.name || "",
1595
+ prefix: node.prefix || "",
1596
+ apiType: node.apiType || "chat",
1597
+ baseUrl: node.baseUrl || (isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"),
1598
+ });
1599
+ }
1600
+ }, [node, isAnthropic]);
1601
+
1602
+ const apiTypeOptions = [
1603
+ { value: "chat", label: "Chat Completions" },
1604
+ { value: "responses", label: "Responses API" },
1605
+ ];
1606
+
1607
+ const handleSubmit = async () => {
1608
+ if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
1609
+ setSaving(true);
1610
+ try {
1611
+ const payload = {
1612
+ name: formData.name,
1613
+ prefix: formData.prefix,
1614
+ baseUrl: formData.baseUrl,
1615
+ };
1616
+ if (!isAnthropic) {
1617
+ payload.apiType = formData.apiType;
1618
+ }
1619
+ await onSave(payload);
1620
+ } finally {
1621
+ setSaving(false);
1622
+ }
1623
+ };
1624
+
1625
+ const handleValidate = async () => {
1626
+ setValidating(true);
1627
+ try {
1628
+ const res = await fetch("/api/provider-nodes/validate", {
1629
+ method: "POST",
1630
+ headers: { "Content-Type": "application/json" },
1631
+ body: JSON.stringify({
1632
+ baseUrl: formData.baseUrl,
1633
+ apiKey: checkKey,
1634
+ type: isAnthropic ? "anthropic-compatible" : "openai-compatible"
1635
+ }),
1636
+ });
1637
+ const data = await res.json();
1638
+ setValidationResult(data.valid ? "success" : "failed");
1639
+ } catch {
1640
+ setValidationResult("failed");
1641
+ } finally {
1642
+ setValidating(false);
1643
+ }
1644
+ };
1645
+
1646
+ if (!node) return null;
1647
+
1648
+ return (
1649
+ <Modal isOpen={isOpen} title={`Edit ${isAnthropic ? "Anthropic" : "OpenAI"} Compatible`} onClose={onClose}>
1650
+ <div className="flex flex-col gap-4">
1651
+ <Input
1652
+ label="Name"
1653
+ value={formData.name}
1654
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
1655
+ placeholder={`${isAnthropic ? "Anthropic" : "OpenAI"} Compatible (Prod)`}
1656
+ hint="Required. A friendly label for this node."
1657
+ />
1658
+ <Input
1659
+ label="Prefix"
1660
+ value={formData.prefix}
1661
+ onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
1662
+ placeholder={isAnthropic ? "ac-prod" : "oc-prod"}
1663
+ hint="Required. Used as the provider prefix for model IDs."
1664
+ />
1665
+ {!isAnthropic && (
1666
+ <Select
1667
+ label="API Type"
1668
+ options={apiTypeOptions}
1669
+ value={formData.apiType}
1670
+ onChange={(e) => setFormData({ ...formData, apiType: e.target.value })}
1671
+ />
1672
+ )}
1673
+ <Input
1674
+ label="Base URL"
1675
+ value={formData.baseUrl}
1676
+ onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
1677
+ placeholder={isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"}
1678
+ hint={`Use the base URL (ending in /v1) for your ${isAnthropic ? "Anthropic" : "OpenAI"}-compatible API.`}
1679
+ />
1680
+ <div className="flex gap-2">
1681
+ <Input
1682
+ label="API Key (for Check)"
1683
+ type="password"
1684
+ value={checkKey}
1685
+ onChange={(e) => setCheckKey(e.target.value)}
1686
+ className="flex-1"
1687
+ />
1688
+ <div className="pt-6">
1689
+ <Button onClick={handleValidate} disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary">
1690
+ {validating ? "Checking..." : "Check"}
1691
+ </Button>
1692
+ </div>
1693
+ </div>
1694
+ {validationResult && (
1695
+ <Badge variant={validationResult === "success" ? "success" : "error"}>
1696
+ {validationResult === "success" ? "Valid" : "Invalid"}
1697
+ </Badge>
1698
+ )}
1699
+ <div className="flex gap-2">
1700
+ <Button onClick={handleSubmit} fullWidth disabled={!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || saving}>
1701
+ {saving ? "Saving..." : "Save"}
1702
+ </Button>
1703
+ <Button onClick={onClose} variant="ghost" fullWidth>
1704
+ Cancel
1705
+ </Button>
1706
+ </div>
1707
+ </div>
1708
+ </Modal>
1709
+ );
1710
+ }
1711
+
1712
+ EditCompatibleNodeModal.propTypes = {
1713
+ isOpen: PropTypes.bool.isRequired,
1714
+ node: PropTypes.shape({
1715
+ id: PropTypes.string,
1716
+ name: PropTypes.string,
1717
+ prefix: PropTypes.string,
1718
+ apiType: PropTypes.string,
1719
+ baseUrl: PropTypes.string,
1720
+ }),
1721
+ onSave: PropTypes.func.isRequired,
1722
+ onClose: PropTypes.func.isRequired,
1723
+ isAnthropic: PropTypes.bool,
1724
+ };