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,1365 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import PropTypes from "prop-types";
5
+ import {
6
+ Card,
7
+ CardSkeleton,
8
+ Badge,
9
+ Button,
10
+ Input,
11
+ Modal,
12
+ Select,
13
+ Toggle,
14
+ Pagination,
15
+ } from "@/shared/components";
16
+ import ProviderIcon from "@/shared/components/ProviderIcon";
17
+ import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
18
+ import {
19
+ FREE_PROVIDERS,
20
+ FREE_TIER_PROVIDERS,
21
+ WEB_COOKIE_PROVIDERS,
22
+ OPENAI_COMPATIBLE_PREFIX,
23
+ ANTHROPIC_COMPATIBLE_PREFIX,
24
+ } from "@/shared/constants/providers";
25
+ import Link from "next/link";
26
+ import { getErrorCode, getRelativeTime } from "@/shared/utils";
27
+ import { useNotificationStore } from "@/store/notificationStore";
28
+ import { useHeaderSearchStore } from "@/store/headerSearchStore";
29
+ import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge";
30
+
31
+ function getStatusDisplay(connected, error, errorCode) {
32
+ const parts = [];
33
+ if (connected > 0) {
34
+ parts.push(
35
+ <Badge key="connected" variant="success" size="sm" dot>
36
+ {connected} Connected
37
+ </Badge>,
38
+ );
39
+ }
40
+ if (error > 0) {
41
+ const errText = errorCode
42
+ ? `${error} Error (${errorCode})`
43
+ : `${error} Error`;
44
+ parts.push(
45
+ <Badge key="error" variant="error" size="sm" dot>
46
+ {errText}
47
+ </Badge>,
48
+ );
49
+ }
50
+ if (parts.length === 0) {
51
+ return <span className="text-text-muted">No connections</span>;
52
+ }
53
+ return parts;
54
+ }
55
+
56
+ function getConnectionErrorTag(connection) {
57
+ if (!connection) return null;
58
+
59
+ const explicitType = connection.lastErrorType;
60
+ if (explicitType === "runtime_error") return "RUNTIME";
61
+ if (
62
+ explicitType === "upstream_auth_error" ||
63
+ explicitType === "auth_missing" ||
64
+ explicitType === "token_refresh_failed" ||
65
+ explicitType === "token_expired"
66
+ )
67
+ return "AUTH";
68
+ if (explicitType === "upstream_rate_limited") return "429";
69
+ if (explicitType === "upstream_unavailable") return "5XX";
70
+ if (explicitType === "network_error") return "NET";
71
+
72
+ const numericCode = Number(connection.errorCode);
73
+ if (Number.isFinite(numericCode) && numericCode >= 400)
74
+ return String(numericCode);
75
+
76
+ const fromMessage = getErrorCode(connection.lastError);
77
+ if (fromMessage === "401" || fromMessage === "403") return "AUTH";
78
+ if (fromMessage && fromMessage !== "ERR") return fromMessage;
79
+
80
+ const msg = (connection.lastError || "").toLowerCase();
81
+ if (
82
+ msg.includes("runtime") ||
83
+ msg.includes("not runnable") ||
84
+ msg.includes("not installed")
85
+ )
86
+ return "RUNTIME";
87
+ if (
88
+ msg.includes("invalid api key") ||
89
+ msg.includes("token invalid") ||
90
+ msg.includes("revoked") ||
91
+ msg.includes("unauthorized")
92
+ )
93
+ return "AUTH";
94
+
95
+ return "ERR";
96
+ }
97
+
98
+ const PROVIDERS_DEFAULT_PAGE_SIZE = 20;
99
+ const PROVIDERS_FETCH_TIMEOUT_MS = 8000;
100
+
101
+ async function fetchJsonWithTimeout(url) {
102
+ const controller = new AbortController();
103
+ const timeout = window.setTimeout(() => controller.abort(), PROVIDERS_FETCH_TIMEOUT_MS);
104
+
105
+ try {
106
+ const response = await fetch(url, {
107
+ cache: "no-store",
108
+ signal: controller.signal,
109
+ });
110
+ let data = null;
111
+ try {
112
+ data = await response.json();
113
+ } catch {
114
+ data = null;
115
+ }
116
+ return { response, data };
117
+ } finally {
118
+ window.clearTimeout(timeout);
119
+ }
120
+ }
121
+
122
+ export default function ProvidersPage() {
123
+ const [connections, setConnections] = useState([]);
124
+ const [providerNodes, setProviderNodes] = useState([]);
125
+ const [loading, setLoading] = useState(true);
126
+ const [loadError, setLoadError] = useState("");
127
+ const [apikeyPage, setApikeyPage] = useState(1);
128
+ const [apikeyPageSize, setApikeyPageSize] = useState(PROVIDERS_DEFAULT_PAGE_SIZE);
129
+ const [showAddCompatibleModal, setShowAddCompatibleModal] = useState(false);
130
+ const [showAddAnthropicCompatibleModal, setShowAddAnthropicCompatibleModal] =
131
+ useState(false);
132
+ const [testingMode, setTestingMode] = useState(null);
133
+ const [testResults, setTestResults] = useState(null);
134
+ const notify = useNotificationStore();
135
+ const searchQuery = useHeaderSearchStore((s) => s.query);
136
+ const registerSearch = useHeaderSearchStore((s) => s.register);
137
+ const unregisterSearch = useHeaderSearchStore((s) => s.unregister);
138
+
139
+ useEffect(() => {
140
+ registerSearch("Search providers...");
141
+ return () => unregisterSearch();
142
+ }, [registerSearch, unregisterSearch]);
143
+
144
+ useEffect(() => {
145
+ setApikeyPage(1);
146
+ }, [searchQuery, apikeyPageSize]);
147
+
148
+ const matchSearch = (name) =>
149
+ !searchQuery.trim() ||
150
+ name.toLowerCase().includes(searchQuery.trim().toLowerCase());
151
+
152
+ const sortByPriority = (entries, authType) =>
153
+ [...entries].sort(([ka, a], [kb, b]) => {
154
+ const sa = getProviderStats(ka, authType);
155
+ const sb = getProviderStats(kb, authType);
156
+ const ca = sa.connected > 0 ? 1 : 0;
157
+ const cb = sb.connected > 0 ? 1 : 0;
158
+ if (ca !== cb) return cb - ca;
159
+ return (a.name || "").localeCompare(b.name || "");
160
+ });
161
+
162
+ const sortItemsByPriority = (items, authType) =>
163
+ [...items].sort((a, b) => {
164
+ const sa = getProviderStats(a.id, authType);
165
+ const sb = getProviderStats(b.id, authType);
166
+ const ca = sa.connected > 0 ? 1 : 0;
167
+ const cb = sb.connected > 0 ? 1 : 0;
168
+ if (ca !== cb) return cb - ca;
169
+ return (a.name || "").localeCompare(b.name || "");
170
+ });
171
+
172
+ useEffect(() => {
173
+ const fetchData = async () => {
174
+ try {
175
+ setLoadError("");
176
+ const [connectionsResult, nodesResult] = await Promise.allSettled([
177
+ fetchJsonWithTimeout("/api/providers"),
178
+ fetchJsonWithTimeout("/api/provider-nodes"),
179
+ ]);
180
+
181
+ if (connectionsResult.status === "fulfilled" && connectionsResult.value.response.ok) {
182
+ setConnections(connectionsResult.value.data?.connections || []);
183
+ } else {
184
+ setConnections([]);
185
+ setLoadError("Provider connection stats could not be loaded. The provider catalog is still available.");
186
+ }
187
+
188
+ if (nodesResult.status === "fulfilled" && nodesResult.value.response.ok) {
189
+ setProviderNodes(nodesResult.value.data?.nodes || []);
190
+ } else {
191
+ setProviderNodes([]);
192
+ setLoadError((current) => current || "Custom provider data could not be loaded. The provider catalog is still available.");
193
+ }
194
+ } catch (error) {
195
+ console.log("Error fetching data:", error);
196
+ setLoadError("Provider data could not be loaded. The provider catalog is still available.");
197
+ } finally {
198
+ setLoading(false);
199
+ }
200
+ };
201
+ fetchData();
202
+ }, []);
203
+
204
+ const getProviderStats = (providerId, authType) => {
205
+ const providerConnections = connections.filter(
206
+ (c) => c.provider === providerId && c.authType === authType,
207
+ );
208
+
209
+ const getEffectiveStatus = (conn) => {
210
+ const isCooldown = Object.entries(conn).some(
211
+ ([k, v]) =>
212
+ k.startsWith("modelLock_") && v && new Date(v).getTime() > Date.now(),
213
+ );
214
+ return conn.testStatus === "unavailable" && !isCooldown
215
+ ? "active"
216
+ : conn.testStatus;
217
+ };
218
+
219
+ const connected = providerConnections.filter((c) => {
220
+ const status = getEffectiveStatus(c);
221
+ return status === "active" || status === "success";
222
+ }).length;
223
+
224
+ const errorConns = providerConnections.filter((c) => {
225
+ const status = getEffectiveStatus(c);
226
+ return (
227
+ status === "error" || status === "expired" || status === "unavailable"
228
+ );
229
+ });
230
+
231
+ const error = errorConns.length;
232
+ const total = providerConnections.length;
233
+ const allDisabled =
234
+ total > 0 && providerConnections.every((c) => c.isActive === false);
235
+
236
+ const latestError = errorConns.sort(
237
+ (a, b) => new Date(b.lastErrorAt || 0) - new Date(a.lastErrorAt || 0),
238
+ )[0];
239
+ const errorCode = latestError ? getConnectionErrorTag(latestError) : null;
240
+ const errorTime = latestError?.lastErrorAt
241
+ ? getRelativeTime(latestError.lastErrorAt)
242
+ : null;
243
+
244
+ return { connected, error, total, errorCode, errorTime, allDisabled };
245
+ };
246
+
247
+ // Toggle all connections for a provider on/off
248
+ const handleToggleProvider = async (providerId, authType, newActive) => {
249
+ const providerConns = connections.filter(
250
+ (c) => c.provider === providerId && c.authType === authType,
251
+ );
252
+ setConnections((prev) =>
253
+ prev.map((c) =>
254
+ c.provider === providerId && c.authType === authType
255
+ ? { ...c, isActive: newActive }
256
+ : c,
257
+ ),
258
+ );
259
+ await Promise.allSettled(
260
+ providerConns.map((c) =>
261
+ fetch(`/api/providers/${c.id}`, {
262
+ method: "PUT",
263
+ headers: { "Content-Type": "application/json" },
264
+ body: JSON.stringify({ isActive: newActive }),
265
+ }),
266
+ ),
267
+ );
268
+ };
269
+
270
+ const handleBatchTest = async (mode, providerId = null) => {
271
+ if (testingMode) return;
272
+ setTestingMode(mode === "provider" ? providerId : mode);
273
+ setTestResults(null);
274
+ try {
275
+ const res = await fetch("/api/providers/test-batch", {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({ mode, providerId }),
279
+ });
280
+ const data = await res.json();
281
+ setTestResults(data);
282
+ if (data.summary) {
283
+ const { passed, failed, total } = data.summary;
284
+ if (failed === 0) notify.success(`All ${total} tests passed`);
285
+ else notify.warning(`${passed}/${total} passed, ${failed} failed`);
286
+ }
287
+ } catch (error) {
288
+ setTestResults({ error: "Test request failed" });
289
+ notify.error("Provider test failed");
290
+ } finally {
291
+ setTestingMode(null);
292
+ }
293
+ };
294
+
295
+ const compatibleProviders = providerNodes
296
+ .filter((node) => node.type === "openai-compatible")
297
+ .map((node) => ({
298
+ id: node.id,
299
+ name: node.name || "OpenAI Compatible",
300
+ color: "#10A37F",
301
+ textIcon: "OC",
302
+ apiType: node.apiType,
303
+ }))
304
+ .filter((p) => matchSearch(p.name));
305
+
306
+ const anthropicCompatibleProviders = providerNodes
307
+ .filter((node) => node.type === "anthropic-compatible")
308
+ .map((node) => ({
309
+ id: node.id,
310
+ name: node.name || "Anthropic Compatible",
311
+ color: "#D97757",
312
+ textIcon: "AC",
313
+ }))
314
+ .filter((p) => matchSearch(p.name));
315
+
316
+ const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter(
317
+ ([, info]) => !info.hidden && matchSearch(info.name),
318
+ );
319
+ const freeEntries = Object.entries(FREE_PROVIDERS).filter(
320
+ ([, info]) => !info.hidden && matchSearch(info.name),
321
+ );
322
+ const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS).filter(
323
+ ([, info]) => !info.hidden && matchSearch(info.name),
324
+ );
325
+ const apikeyEntries = sortByPriority(
326
+ Object.entries(APIKEY_PROVIDERS).filter(
327
+ ([, info]) =>
328
+ !info.hidden &&
329
+ (info.serviceKinds ?? ["llm"]).includes("llm") &&
330
+ matchSearch(info.name),
331
+ ),
332
+ "apikey",
333
+ );
334
+ const totalApikeyPages = Math.max(1, Math.ceil(apikeyEntries.length / apikeyPageSize));
335
+ const activeApikeyPage = Math.min(apikeyPage, totalApikeyPages);
336
+ const visibleApikeyEntries = apikeyEntries.slice(
337
+ (activeApikeyPage - 1) * apikeyPageSize,
338
+ activeApikeyPage * apikeyPageSize
339
+ );
340
+
341
+ if (loading) {
342
+ return (
343
+ <div className="flex flex-col gap-8">
344
+ <CardSkeleton />
345
+ <CardSkeleton />
346
+ </div>
347
+ );
348
+ }
349
+
350
+ const hasAnyResult =
351
+ oauthEntries.length > 0 ||
352
+ freeEntries.length > 0 ||
353
+ freeTierEntries.length > 0 ||
354
+ apikeyEntries.length > 0 ||
355
+ compatibleProviders.length > 0 ||
356
+ anthropicCompatibleProviders.length > 0;
357
+
358
+ return (
359
+ <div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
360
+ {loadError && (
361
+ <div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-300">
362
+ {loadError}
363
+ </div>
364
+ )}
365
+
366
+ {!hasAnyResult && (
367
+ <div className="text-center py-8 border border-dashed border-border rounded-xl">
368
+ <span className="material-symbols-outlined text-[32px] text-text-muted mb-2">
369
+ search_off
370
+ </span>
371
+ <p className="text-text-muted text-sm">No providers match your search</p>
372
+ </div>
373
+ )}
374
+
375
+ {/* Custom Providers (OpenAI/Anthropic Compatible) — dynamic */}
376
+ <div className="flex flex-col gap-4">
377
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
378
+ <h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
379
+ Custom Providers (OpenAI/Anthropic Compatible){" "}
380
+ </h2>
381
+ <div className="grid grid-cols-1 gap-2 sm:flex sm:w-auto">
382
+ <Button
383
+ size="sm"
384
+ icon="add"
385
+ onClick={() => setShowAddAnthropicCompatibleModal(true)}
386
+ className="w-full sm:w-auto"
387
+ >
388
+ Add Anthropic Compatible
389
+ </Button>
390
+ <Button
391
+ size="sm"
392
+ variant="secondary"
393
+ icon="add"
394
+ onClick={() => setShowAddCompatibleModal(true)}
395
+ className="w-full !bg-white !text-black hover:!bg-gray-100 sm:w-auto"
396
+ >
397
+ Add OpenAI Compatible
398
+ </Button>
399
+ </div>
400
+ </div>
401
+ {compatibleProviders.length === 0 &&
402
+ anthropicCompatibleProviders.length === 0 ? (
403
+ <div className="flex items-center justify-center gap-2 py-2 border border-dashed border-border rounded-xl text-text-muted text-sm">
404
+ <span className="material-symbols-outlined text-[18px]">extension</span>
405
+ <span>No custom providers — use buttons above to add OpenAI/Anthropic compatible endpoints</span>
406
+ </div>
407
+ ) : (
408
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
409
+ {[...compatibleProviders, ...anthropicCompatibleProviders].map(
410
+ (info) => (
411
+ <ApiKeyProviderCard
412
+ key={info.id}
413
+ providerId={info.id}
414
+ provider={info}
415
+ stats={getProviderStats(info.id, "apikey")}
416
+ authType="compatible"
417
+ onToggle={(active) =>
418
+ handleToggleProvider(info.id, "apikey", active)
419
+ }
420
+ />
421
+ ),
422
+ )}
423
+ </div>
424
+ )}
425
+ </div>
426
+
427
+ {/* OAuth Providers */}
428
+ {oauthEntries.length > 0 && (
429
+ <div className="flex flex-col gap-4">
430
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
431
+ <h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
432
+ OAuth Providers
433
+ </h2>
434
+ <div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
435
+ <ModelAvailabilityBadge />
436
+ <button
437
+ onClick={() => handleBatchTest("oauth")}
438
+ disabled={!!testingMode}
439
+ className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
440
+ testingMode === "oauth"
441
+ ? "bg-primary/20 border-primary/40 text-primary animate-pulse"
442
+ : "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
443
+ }`}
444
+ title="Test all OAuth connections"
445
+ aria-label="Test all OAuth connections"
446
+ >
447
+ <span
448
+ className={`material-symbols-outlined text-[14px]${testingMode === "oauth" ? " animate-spin" : ""}`}
449
+ >
450
+ play_arrow
451
+ </span>
452
+ {testingMode === "oauth" ? "Testing..." : "Test All"}
453
+ </button>
454
+ </div>
455
+ </div>
456
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
457
+ {oauthEntries.map(([key, info]) => (
458
+ <ProviderCard
459
+ key={key}
460
+ providerId={key}
461
+ provider={info}
462
+ stats={getProviderStats(key, "oauth")}
463
+ authType="oauth"
464
+ onToggle={(active) => handleToggleProvider(key, "oauth", active)}
465
+ />
466
+ ))}
467
+ </div>
468
+ </div>
469
+ )}
470
+
471
+ {/* Free Tier Providers */}
472
+ {(freeEntries.length > 0 || freeTierEntries.length > 0) && (
473
+ <div className="flex flex-col gap-4">
474
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
475
+ <h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
476
+ Free Tier Providers
477
+ </h2>
478
+ <button
479
+ onClick={() => handleBatchTest("free")}
480
+ disabled={!!testingMode}
481
+ className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
482
+ testingMode === "free"
483
+ ? "bg-primary/20 border-primary/40 text-primary animate-pulse"
484
+ : "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
485
+ }`}
486
+ title="Test all Free connections"
487
+ aria-label="Test all Free provider connections"
488
+ >
489
+ <span
490
+ className={`material-symbols-outlined text-[14px]${testingMode === "free" ? " animate-spin" : ""}`}
491
+ >
492
+ play_arrow
493
+ </span>
494
+ {testingMode === "free" ? "Testing..." : "Test All"}
495
+ </button>
496
+ </div>
497
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
498
+ {freeEntries.map(([key, info]) => (
499
+ <ProviderCard
500
+ key={key}
501
+ providerId={key}
502
+ provider={info}
503
+ stats={getProviderStats(key, "oauth")}
504
+ authType="free"
505
+ onToggle={(active) => handleToggleProvider(key, "oauth", active)}
506
+ />
507
+ ))}
508
+ {freeTierEntries.map(([key, info]) => (
509
+ <ApiKeyProviderCard
510
+ key={key}
511
+ providerId={key}
512
+ provider={info}
513
+ stats={getProviderStats(key, "apikey")}
514
+ authType="apikey"
515
+ onToggle={(active) => handleToggleProvider(key, "apikey", active)}
516
+ />
517
+ ))}
518
+ </div>
519
+ </div>
520
+ )}
521
+
522
+ {/* API Key Providers — fixed list */}
523
+ {apikeyEntries.length > 0 && (
524
+ <div className="flex flex-col gap-4">
525
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
526
+ <h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
527
+ API Key Providers{" "}
528
+ </h2>
529
+ <button
530
+ onClick={() => handleBatchTest("apikey")}
531
+ disabled={!!testingMode}
532
+ className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
533
+ testingMode === "apikey"
534
+ ? "bg-primary/20 border-primary/40 text-primary animate-pulse"
535
+ : "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
536
+ }`}
537
+ title="Test all API Key connections"
538
+ aria-label="Test all API Key connections"
539
+ >
540
+ <span
541
+ className={`material-symbols-outlined text-[14px]${testingMode === "apikey" ? " animate-spin" : ""}`}
542
+ >
543
+ play_arrow
544
+ </span>
545
+ {testingMode === "apikey" ? "Testing..." : "Test All"}
546
+ </button>
547
+ </div>
548
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
549
+ {visibleApikeyEntries.map(([key, info]) => (
550
+ <ApiKeyProviderCard
551
+ key={key}
552
+ providerId={key}
553
+ provider={info}
554
+ stats={getProviderStats(key, "apikey")}
555
+ authType="apikey"
556
+ onToggle={(active) => handleToggleProvider(key, "apikey", active)}
557
+ />
558
+ ))}
559
+ </div>
560
+ {apikeyEntries.length > 0 && (
561
+ <Pagination
562
+ currentPage={activeApikeyPage}
563
+ pageSize={apikeyPageSize}
564
+ totalItems={apikeyEntries.length}
565
+ onPageChange={setApikeyPage}
566
+ onPageSizeChange={(size) => {
567
+ setApikeyPageSize(size);
568
+ setApikeyPage(1);
569
+ }}
570
+ />
571
+ )}
572
+ </div>
573
+ )}
574
+
575
+ {/* Web Cookie Providers — use browser subscription cookie instead of API key */}
576
+ {/* <div className="flex flex-col gap-4">
577
+ <div className="flex items-center justify-between">
578
+ <h2 className="text-xl font-semibold flex items-center gap-2">
579
+ Web Cookie Providers{" "}
580
+ </h2>
581
+ </div>
582
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
583
+ {Object.entries(WEB_COOKIE_PROVIDERS).map(([key, info]) => (
584
+ <ApiKeyProviderCard
585
+ key={key}
586
+ providerId={key}
587
+ provider={info}
588
+ stats={getProviderStats(key, "apikey")}
589
+ authType="apikey"
590
+ onToggle={(active) => handleToggleProvider(key, "apikey", active)}
591
+ />
592
+ ))}
593
+ </div>
594
+ </div> */}
595
+
596
+ <AddOpenAICompatibleModal
597
+ isOpen={showAddCompatibleModal}
598
+ onClose={() => setShowAddCompatibleModal(false)}
599
+ onCreated={(node) => {
600
+ setProviderNodes((prev) => [...prev, node]);
601
+ setShowAddCompatibleModal(false);
602
+ }}
603
+ />
604
+ <AddAnthropicCompatibleModal
605
+ isOpen={showAddAnthropicCompatibleModal}
606
+ onClose={() => setShowAddAnthropicCompatibleModal(false)}
607
+ onCreated={(node) => {
608
+ setProviderNodes((prev) => [...prev, node]);
609
+ setShowAddAnthropicCompatibleModal(false);
610
+ }}
611
+ />
612
+
613
+ {/* Test Results Modal */}
614
+ {testResults && (
615
+ <div
616
+ className="fixed inset-0 z-50 flex items-start justify-center px-3 pt-[6vh] sm:pt-[10vh]"
617
+ onClick={() => setTestResults(null)}
618
+ >
619
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
620
+ <div
621
+ className="relative bg-surface border border-border rounded-xl w-full max-w-[600px] max-h-[86vh] sm:max-h-[80vh] overflow-y-auto shadow-2xl"
622
+ onClick={(e) => e.stopPropagation()}
623
+ >
624
+ <div className="sticky top-0 z-10 flex items-center justify-between px-5 py-3 border-b border-border bg-surface/95 backdrop-blur-sm rounded-t-xl">
625
+ <h3 className="font-semibold">Test Results</h3>
626
+ <button
627
+ onClick={() => setTestResults(null)}
628
+ className="p-1 rounded-lg hover:bg-bg text-text-muted hover:text-text-main transition-colors"
629
+ aria-label="Close test results"
630
+ >
631
+ <span className="material-symbols-outlined text-lg">close</span>
632
+ </button>
633
+ </div>
634
+ <div className="p-5">
635
+ <ProviderTestResultsView results={testResults} />
636
+ </div>
637
+ </div>
638
+ </div>
639
+ )}
640
+ </div>
641
+ );
642
+ }
643
+
644
+ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
645
+ const { connected, error, errorCode, errorTime, allDisabled } = stats;
646
+ const isNoAuth = !!provider.noAuth;
647
+
648
+ const dotColors = {
649
+ free: "bg-green-500",
650
+ oauth: "bg-blue-500",
651
+ apikey: "bg-amber-500",
652
+ compatible: "bg-orange-500",
653
+ };
654
+ const dotLabels = {
655
+ free: "Free",
656
+ oauth: "OAuth",
657
+ apikey: "API Key",
658
+ compatible: "Compatible",
659
+ };
660
+
661
+ return (
662
+ <Link href={`/dashboard/providers/${providerId}`} className="group min-w-0">
663
+ <Card
664
+ padding="xs"
665
+ className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
666
+ >
667
+ <div className="flex min-w-0 items-center justify-between gap-3">
668
+ <div className="flex min-w-0 items-center gap-3">
669
+ <div
670
+ className="size-8 shrink-0 rounded-lg flex items-center justify-center"
671
+ style={{
672
+ backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}`,
673
+ }}
674
+ >
675
+ <ProviderIcon
676
+ src={`/providers/${provider.id}.png`}
677
+ alt={provider.name}
678
+ size={30}
679
+ className="object-contain rounded-lg max-w-[32px] max-h-[32px]"
680
+ fallbackText={
681
+ provider.textIcon || provider.id.slice(0, 2).toUpperCase()
682
+ }
683
+ fallbackColor={provider.color}
684
+ />
685
+ </div>
686
+ <div className="min-w-0">
687
+ <h3 className="truncate font-semibold">{provider.name}</h3>
688
+ <div className="flex min-w-0 items-center gap-1.5 text-xs flex-wrap">
689
+ {allDisabled ? (
690
+ <Badge variant="default" size="sm">
691
+ <span className="flex items-center gap-1">
692
+ <span className="material-symbols-outlined text-[12px]">
693
+ pause_circle
694
+ </span>
695
+ Disabled
696
+ </span>
697
+ </Badge>
698
+ ) : isNoAuth ? (
699
+ <Badge variant="success" size="sm" dot>Ready</Badge>
700
+ ) : (
701
+ <>
702
+ {getStatusDisplay(connected, error, errorCode)}
703
+ {errorTime && (
704
+ <span className="text-text-muted">{errorTime}</span>
705
+ )}
706
+ </>
707
+ )}
708
+ </div>
709
+ </div>
710
+ </div>
711
+ <div className="flex shrink-0 items-center gap-2">
712
+ {stats.total > 0 && (
713
+ <div
714
+ className="opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100"
715
+ onClick={(e) => {
716
+ e.preventDefault();
717
+ e.stopPropagation();
718
+ onToggle(!allDisabled ? false : true);
719
+ }}
720
+ >
721
+ <Toggle
722
+ size="sm"
723
+ checked={!allDisabled}
724
+ onChange={() => {}}
725
+ title={allDisabled ? "Enable provider" : "Disable provider"}
726
+ />
727
+ </div>
728
+ )}
729
+ </div>
730
+ </div>
731
+ </Card>
732
+ </Link>
733
+ );
734
+ }
735
+
736
+ ProviderCard.propTypes = {
737
+ providerId: PropTypes.string.isRequired,
738
+ provider: PropTypes.shape({
739
+ id: PropTypes.string.isRequired,
740
+ name: PropTypes.string.isRequired,
741
+ color: PropTypes.string,
742
+ textIcon: PropTypes.string,
743
+ }).isRequired,
744
+ stats: PropTypes.shape({
745
+ connected: PropTypes.number,
746
+ error: PropTypes.number,
747
+ errorCode: PropTypes.string,
748
+ errorTime: PropTypes.string,
749
+ }).isRequired,
750
+ authType: PropTypes.string,
751
+ onToggle: PropTypes.func,
752
+ };
753
+
754
+ function ApiKeyProviderCard({
755
+ providerId,
756
+ provider,
757
+ stats,
758
+ authType,
759
+ onToggle,
760
+ }) {
761
+ const { connected, error, errorCode, errorTime, allDisabled } = stats;
762
+ const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
763
+ const isAnthropicCompatible = providerId.startsWith(
764
+ ANTHROPIC_COMPATIBLE_PREFIX,
765
+ );
766
+
767
+ const dotColors = {
768
+ free: "bg-green-500",
769
+ oauth: "bg-blue-500",
770
+ apikey: "bg-amber-500",
771
+ compatible: "bg-orange-500",
772
+ };
773
+ const dotLabels = {
774
+ free: "Free",
775
+ oauth: "OAuth",
776
+ apikey: "API Key",
777
+ compatible: "Compatible",
778
+ };
779
+
780
+ const getIconPath = () => {
781
+ if (isCompatible)
782
+ return provider.apiType === "responses"
783
+ ? "/providers/oai-r.png"
784
+ : "/providers/oai-cc.png";
785
+ if (isAnthropicCompatible) return "/providers/anthropic-m.png";
786
+ return `/providers/${provider.id}.png`;
787
+ };
788
+
789
+ return (
790
+ <Link href={`/dashboard/providers/${providerId}`} className="group min-w-0">
791
+ <Card
792
+ padding="xs"
793
+ className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
794
+ >
795
+ <div className="flex min-w-0 items-center justify-between gap-3">
796
+ <div className="flex min-w-0 items-center gap-3">
797
+ <div
798
+ className="size-8 shrink-0 rounded-lg flex items-center justify-center"
799
+ style={{
800
+ backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}`,
801
+ }}
802
+ >
803
+ <ProviderIcon
804
+ src={getIconPath()}
805
+ alt={provider.name}
806
+ size={30}
807
+ className="object-contain rounded-lg max-w-[30px] max-h-[30px]"
808
+ fallbackText={
809
+ provider.textIcon || provider.id.slice(0, 2).toUpperCase()
810
+ }
811
+ fallbackColor={provider.color}
812
+ />
813
+ </div>
814
+ <div className="min-w-0">
815
+ <h3 className="truncate font-semibold">{provider.name}</h3>
816
+ <div className="flex min-w-0 items-center gap-1.5 text-xs flex-wrap">
817
+ {allDisabled ? (
818
+ <Badge variant="default" size="sm">
819
+ <span className="flex items-center gap-1">
820
+ <span className="material-symbols-outlined text-[12px]">
821
+ pause_circle
822
+ </span>
823
+ Disabled
824
+ </span>
825
+ </Badge>
826
+ ) : (
827
+ <>
828
+ {getStatusDisplay(connected, error, errorCode)}
829
+ {isCompatible && (
830
+ <Badge variant="default" size="sm">
831
+ {provider.apiType === "responses"
832
+ ? "Responses"
833
+ : "Chat"}
834
+ </Badge>
835
+ )}
836
+ {isAnthropicCompatible && (
837
+ <Badge variant="default" size="sm">
838
+ Messages
839
+ </Badge>
840
+ )}
841
+ {errorTime && (
842
+ <span className="text-text-muted">{errorTime}</span>
843
+ )}
844
+ </>
845
+ )}
846
+ </div>
847
+ </div>
848
+ </div>
849
+ <div className="flex shrink-0 items-center gap-2">
850
+ {stats.total > 0 && (
851
+ <div
852
+ className="opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100"
853
+ onClick={(e) => {
854
+ e.preventDefault();
855
+ e.stopPropagation();
856
+ onToggle(!allDisabled ? false : true);
857
+ }}
858
+ >
859
+ <Toggle
860
+ size="sm"
861
+ checked={!allDisabled}
862
+ onChange={() => {}}
863
+ title={allDisabled ? "Enable provider" : "Disable provider"}
864
+ />
865
+ </div>
866
+ )}
867
+ </div>
868
+ </div>
869
+ </Card>
870
+ </Link>
871
+ );
872
+ }
873
+
874
+ ApiKeyProviderCard.propTypes = {
875
+ providerId: PropTypes.string.isRequired,
876
+ provider: PropTypes.shape({
877
+ id: PropTypes.string.isRequired,
878
+ name: PropTypes.string.isRequired,
879
+ color: PropTypes.string,
880
+ textIcon: PropTypes.string,
881
+ apiType: PropTypes.string,
882
+ }).isRequired,
883
+ stats: PropTypes.shape({
884
+ connected: PropTypes.number,
885
+ error: PropTypes.number,
886
+ errorCode: PropTypes.string,
887
+ errorTime: PropTypes.string,
888
+ }).isRequired,
889
+ authType: PropTypes.string,
890
+ onToggle: PropTypes.func,
891
+ };
892
+
893
+ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
894
+ const [formData, setFormData] = useState({
895
+ name: "",
896
+ prefix: "",
897
+ apiType: "chat",
898
+ baseUrl: "https://api.openai.com/v1",
899
+ });
900
+ const [submitting, setSubmitting] = useState(false);
901
+ const [checkKey, setCheckKey] = useState("");
902
+ const [checkModelId, setCheckModelId] = useState("");
903
+ const [validating, setValidating] = useState(false);
904
+ const [validationResult, setValidationResult] = useState(null);
905
+
906
+ const apiTypeOptions = [
907
+ { value: "chat", label: "Chat Completions" },
908
+ { value: "responses", label: "Responses API" },
909
+ ];
910
+
911
+ useEffect(() => {
912
+ const defaultBaseUrl = "https://api.openai.com/v1";
913
+ setFormData((prev) => ({ ...prev, baseUrl: defaultBaseUrl }));
914
+ }, [formData.apiType]);
915
+
916
+ const handleSubmit = async () => {
917
+ if (
918
+ !formData.name.trim() ||
919
+ !formData.prefix.trim() ||
920
+ !formData.baseUrl.trim()
921
+ )
922
+ return;
923
+ setSubmitting(true);
924
+ try {
925
+ const res = await fetch("/api/provider-nodes", {
926
+ method: "POST",
927
+ headers: { "Content-Type": "application/json" },
928
+ body: JSON.stringify({
929
+ name: formData.name,
930
+ prefix: formData.prefix,
931
+ apiType: formData.apiType,
932
+ baseUrl: formData.baseUrl,
933
+ type: "openai-compatible",
934
+ }),
935
+ });
936
+ const data = await res.json();
937
+ if (res.ok) {
938
+ onCreated(data.node);
939
+ setFormData({
940
+ name: "",
941
+ prefix: "",
942
+ apiType: "chat",
943
+ baseUrl: "https://api.openai.com/v1",
944
+ });
945
+ setCheckKey("");
946
+ setValidationResult(null);
947
+ }
948
+ } catch (error) {
949
+ console.log("Error creating OpenAI Compatible node:", error);
950
+ } finally {
951
+ setSubmitting(false);
952
+ }
953
+ };
954
+
955
+ const handleValidate = async () => {
956
+ setValidating(true);
957
+ try {
958
+ const res = await fetch("/api/provider-nodes/validate", {
959
+ method: "POST",
960
+ headers: { "Content-Type": "application/json" },
961
+ body: JSON.stringify({
962
+ baseUrl: formData.baseUrl,
963
+ apiKey: checkKey,
964
+ type: "openai-compatible",
965
+ modelId: checkModelId.trim() || undefined,
966
+ }),
967
+ });
968
+ const data = await res.json();
969
+ setValidationResult(data);
970
+ } catch {
971
+ setValidationResult({ valid: false, error: "Network error" });
972
+ } finally {
973
+ setValidating(false);
974
+ }
975
+ };
976
+
977
+ // Helper to render validation result
978
+ const renderValidationResult = () => {
979
+ if (!validationResult) return null;
980
+ const { valid, error, method } = validationResult;
981
+
982
+ if (valid) {
983
+ return (
984
+ <>
985
+ <Badge variant="success">Valid</Badge>
986
+ {method === "chat" && (
987
+ <span className="text-sm text-text-muted">
988
+ (via inference test)
989
+ </span>
990
+ )}
991
+ </>
992
+ );
993
+ }
994
+ return (
995
+ <div className="flex flex-col gap-1">
996
+ <Badge variant="error">Invalid</Badge>
997
+ {error && <span className="text-sm text-red-500">{error}</span>}
998
+ </div>
999
+ );
1000
+ };
1001
+
1002
+ return (
1003
+ <Modal isOpen={isOpen} title="Add OpenAI Compatible" onClose={onClose}>
1004
+ <div className="flex flex-col gap-4">
1005
+ <Input
1006
+ label="Name"
1007
+ value={formData.name}
1008
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
1009
+ placeholder="OpenAI Compatible (Prod)"
1010
+ hint="Required. A friendly label for this node."
1011
+ />
1012
+ <Input
1013
+ label="Prefix"
1014
+ value={formData.prefix}
1015
+ onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
1016
+ placeholder="oc-prod"
1017
+ hint="Required. Used as the provider prefix for model IDs."
1018
+ />
1019
+ <Select
1020
+ label="API Type"
1021
+ options={apiTypeOptions}
1022
+ value={formData.apiType}
1023
+ onChange={(e) =>
1024
+ setFormData({ ...formData, apiType: e.target.value })
1025
+ }
1026
+ />
1027
+ <Input
1028
+ label="Base URL"
1029
+ value={formData.baseUrl}
1030
+ onChange={(e) =>
1031
+ setFormData({ ...formData, baseUrl: e.target.value })
1032
+ }
1033
+ placeholder="https://api.openai.com/v1"
1034
+ hint="Use the base URL (ending in /v1) for your OpenAI-compatible API."
1035
+ />
1036
+ <Input
1037
+ label="API Key (for Check)"
1038
+ type="password"
1039
+ value={checkKey}
1040
+ onChange={(e) => setCheckKey(e.target.value)}
1041
+ />
1042
+ <Input
1043
+ label="Model ID (optional)"
1044
+ value={checkModelId}
1045
+ onChange={(e) => setCheckModelId(e.target.value)}
1046
+ placeholder="e.g. gpt-4, claude-3-opus"
1047
+ hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
1048
+ />
1049
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
1050
+ <Button
1051
+ onClick={handleValidate}
1052
+ disabled={!checkKey || validating || !formData.baseUrl.trim()}
1053
+ variant="secondary"
1054
+ className="w-full sm:w-auto"
1055
+ >
1056
+ {validating ? "Checking..." : "Check"}
1057
+ </Button>
1058
+ {renderValidationResult()}
1059
+ </div>
1060
+ <div className="flex flex-col gap-2 sm:flex-row">
1061
+ <Button
1062
+ onClick={handleSubmit}
1063
+ fullWidth
1064
+ disabled={
1065
+ !formData.name.trim() ||
1066
+ !formData.prefix.trim() ||
1067
+ !formData.baseUrl.trim() ||
1068
+ submitting
1069
+ }
1070
+ >
1071
+ {submitting ? "Creating..." : "Create"}
1072
+ </Button>
1073
+ <Button onClick={onClose} variant="ghost" fullWidth>
1074
+ Cancel
1075
+ </Button>
1076
+ </div>
1077
+ </div>
1078
+ </Modal>
1079
+ );
1080
+ }
1081
+
1082
+ AddOpenAICompatibleModal.propTypes = {
1083
+ isOpen: PropTypes.bool.isRequired,
1084
+ onClose: PropTypes.func.isRequired,
1085
+ onCreated: PropTypes.func.isRequired,
1086
+ };
1087
+
1088
+ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
1089
+ const [formData, setFormData] = useState({
1090
+ name: "",
1091
+ prefix: "",
1092
+ baseUrl: "https://api.anthropic.com/v1",
1093
+ });
1094
+ const [submitting, setSubmitting] = useState(false);
1095
+ const [checkKey, setCheckKey] = useState("");
1096
+ const [checkModelId, setCheckModelId] = useState("");
1097
+ const [validating, setValidating] = useState(false);
1098
+ const [validationResult, setValidationResult] = useState(null); // { valid, error, method }
1099
+
1100
+ useEffect(() => {
1101
+ if (isOpen) {
1102
+ setValidationResult(null);
1103
+ setCheckKey("");
1104
+ setCheckModelId("");
1105
+ }
1106
+ }, [isOpen]);
1107
+
1108
+ const handleSubmit = async () => {
1109
+ if (
1110
+ !formData.name.trim() ||
1111
+ !formData.prefix.trim() ||
1112
+ !formData.baseUrl.trim()
1113
+ )
1114
+ return;
1115
+ setSubmitting(true);
1116
+ try {
1117
+ const res = await fetch("/api/provider-nodes", {
1118
+ method: "POST",
1119
+ headers: { "Content-Type": "application/json" },
1120
+ body: JSON.stringify({
1121
+ name: formData.name,
1122
+ prefix: formData.prefix,
1123
+ baseUrl: formData.baseUrl,
1124
+ type: "anthropic-compatible",
1125
+ }),
1126
+ });
1127
+ const data = await res.json();
1128
+ if (res.ok) {
1129
+ onCreated(data.node);
1130
+ setFormData({
1131
+ name: "",
1132
+ prefix: "",
1133
+ baseUrl: "https://api.anthropic.com/v1",
1134
+ });
1135
+ setCheckKey("");
1136
+ setValidationResult(null);
1137
+ }
1138
+ } catch (error) {
1139
+ console.log("Error creating Anthropic Compatible node:", error);
1140
+ } finally {
1141
+ setSubmitting(false);
1142
+ }
1143
+ };
1144
+
1145
+ const handleValidate = async () => {
1146
+ setValidating(true);
1147
+ try {
1148
+ const res = await fetch("/api/provider-nodes/validate", {
1149
+ method: "POST",
1150
+ headers: { "Content-Type": "application/json" },
1151
+ body: JSON.stringify({
1152
+ baseUrl: formData.baseUrl,
1153
+ apiKey: checkKey,
1154
+ type: "anthropic-compatible",
1155
+ modelId: checkModelId.trim() || undefined,
1156
+ }),
1157
+ });
1158
+ const data = await res.json();
1159
+ setValidationResult(data);
1160
+ } catch {
1161
+ setValidationResult({ valid: false, error: "Network error" });
1162
+ } finally {
1163
+ setValidating(false);
1164
+ }
1165
+ };
1166
+
1167
+ // Helper to render validation result
1168
+ const renderValidationResult = () => {
1169
+ if (!validationResult) return null;
1170
+ const { valid, error, method } = validationResult;
1171
+
1172
+ if (valid) {
1173
+ return (
1174
+ <>
1175
+ <Badge variant="success">Valid</Badge>
1176
+ {method === "chat" && (
1177
+ <span className="text-sm text-text-muted">
1178
+ (via inference test)
1179
+ </span>
1180
+ )}
1181
+ </>
1182
+ );
1183
+ }
1184
+ return (
1185
+ <div className="flex flex-col gap-1">
1186
+ <Badge variant="error">Invalid</Badge>
1187
+ {error && <span className="text-sm text-red-500">{error}</span>}
1188
+ </div>
1189
+ );
1190
+ };
1191
+
1192
+ return (
1193
+ <Modal isOpen={isOpen} title="Add Anthropic Compatible" onClose={onClose}>
1194
+ <div className="flex flex-col gap-4">
1195
+ <Input
1196
+ label="Name"
1197
+ value={formData.name}
1198
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
1199
+ placeholder="Anthropic Compatible (Prod)"
1200
+ hint="Required. A friendly label for this node."
1201
+ />
1202
+ <Input
1203
+ label="Prefix"
1204
+ value={formData.prefix}
1205
+ onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
1206
+ placeholder="ac-prod"
1207
+ hint="Required. Used as the provider prefix for model IDs."
1208
+ />
1209
+ <Input
1210
+ label="Base URL"
1211
+ value={formData.baseUrl}
1212
+ onChange={(e) =>
1213
+ setFormData({ ...formData, baseUrl: e.target.value })
1214
+ }
1215
+ placeholder="https://api.anthropic.com/v1"
1216
+ hint="Use the base URL (ending in /v1) for your Anthropic-compatible API. The system will append /messages."
1217
+ />
1218
+ <Input
1219
+ label="API Key (for Check)"
1220
+ type="password"
1221
+ value={checkKey}
1222
+ onChange={(e) => setCheckKey(e.target.value)}
1223
+ />
1224
+ <Input
1225
+ label="Model ID (optional)"
1226
+ value={checkModelId}
1227
+ onChange={(e) => setCheckModelId(e.target.value)}
1228
+ placeholder="e.g. claude-3-opus"
1229
+ hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
1230
+ />
1231
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
1232
+ <Button
1233
+ onClick={handleValidate}
1234
+ disabled={!checkKey || validating || !formData.baseUrl.trim()}
1235
+ variant="secondary"
1236
+ className="w-full sm:w-auto"
1237
+ >
1238
+ {validating ? "Checking..." : "Check"}
1239
+ </Button>
1240
+ {renderValidationResult()}
1241
+ </div>
1242
+ <div className="flex flex-col gap-2 sm:flex-row">
1243
+ <Button
1244
+ onClick={handleSubmit}
1245
+ fullWidth
1246
+ disabled={
1247
+ !formData.name.trim() ||
1248
+ !formData.prefix.trim() ||
1249
+ !formData.baseUrl.trim() ||
1250
+ submitting
1251
+ }
1252
+ >
1253
+ {submitting ? "Creating..." : "Create"}
1254
+ </Button>
1255
+ <Button onClick={onClose} variant="ghost" fullWidth>
1256
+ Cancel
1257
+ </Button>
1258
+ </div>
1259
+ </div>
1260
+ </Modal>
1261
+ );
1262
+ }
1263
+
1264
+ AddAnthropicCompatibleModal.propTypes = {
1265
+ isOpen: PropTypes.bool.isRequired,
1266
+ onClose: PropTypes.func.isRequired,
1267
+ onCreated: PropTypes.func.isRequired,
1268
+ };
1269
+
1270
+ function ProviderTestResultsView({ results }) {
1271
+ if (results.error && !results.results) {
1272
+ return (
1273
+ <div className="text-center py-6">
1274
+ <span className="material-symbols-outlined text-red-500 text-[32px] mb-2 block">
1275
+ error
1276
+ </span>
1277
+ <p className="text-sm text-red-400">{results.error}</p>
1278
+ </div>
1279
+ );
1280
+ }
1281
+
1282
+ const { summary, mode } = results;
1283
+ const items = results.results || [];
1284
+ const modeLabel =
1285
+ {
1286
+ oauth: "OAuth",
1287
+ free: "Free",
1288
+ apikey: "API Key",
1289
+ provider: "Provider",
1290
+ all: "All",
1291
+ }[mode] || mode;
1292
+
1293
+ return (
1294
+ <div className="flex min-w-0 flex-col gap-3">
1295
+ {summary && (
1296
+ <div className="flex flex-wrap items-center gap-2 text-xs mb-1 sm:gap-3">
1297
+ <span className="text-text-muted">{modeLabel} Test</span>
1298
+ <span className="px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400 font-medium">
1299
+ {summary.passed} passed
1300
+ </span>
1301
+ {summary.failed > 0 && (
1302
+ <span className="px-2 py-0.5 rounded bg-red-500/15 text-red-400 font-medium">
1303
+ {summary.failed} failed
1304
+ </span>
1305
+ )}
1306
+ <span className="text-text-muted sm:ml-auto">
1307
+ {summary.total} tested
1308
+ </span>
1309
+ </div>
1310
+ )}
1311
+ {items.map((r, i) => (
1312
+ <div
1313
+ key={r.connectionId || i}
1314
+ className="flex min-w-0 flex-wrap items-center gap-2 rounded-lg bg-black/[0.03] px-3 py-2 text-xs dark:bg-white/[0.03] sm:flex-nowrap"
1315
+ >
1316
+ <span
1317
+ className={`material-symbols-outlined text-[16px] ${r.valid ? "text-emerald-500" : "text-red-500"}`}
1318
+ >
1319
+ {r.valid ? "check_circle" : "error"}
1320
+ </span>
1321
+ <div className="min-w-0 flex-[1_1_160px]">
1322
+ <span className="block truncate font-medium sm:inline">
1323
+ {r.connectionName}
1324
+ </span>
1325
+ <span className="block truncate text-text-muted sm:ml-1.5 sm:inline">
1326
+ ({r.provider})
1327
+ </span>
1328
+ </div>
1329
+ {r.latencyMs !== undefined && (
1330
+ <span className="shrink-0 text-text-muted font-mono tabular-nums">
1331
+ {r.latencyMs}ms
1332
+ </span>
1333
+ )}
1334
+ <span
1335
+ className={`shrink-0 text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${
1336
+ r.valid
1337
+ ? "bg-emerald-500/15 text-emerald-400"
1338
+ : "bg-red-500/15 text-red-400"
1339
+ }`}
1340
+ >
1341
+ {r.valid ? "OK" : r.diagnosis?.type || "ERROR"}
1342
+ </span>
1343
+ </div>
1344
+ ))}
1345
+ {items.length === 0 && (
1346
+ <div className="text-center py-4 text-text-muted text-sm">
1347
+ No active connections found for this group.
1348
+ </div>
1349
+ )}
1350
+ </div>
1351
+ );
1352
+ }
1353
+
1354
+ ProviderTestResultsView.propTypes = {
1355
+ results: PropTypes.shape({
1356
+ mode: PropTypes.string,
1357
+ results: PropTypes.array,
1358
+ summary: PropTypes.shape({
1359
+ total: PropTypes.number,
1360
+ passed: PropTypes.number,
1361
+ failed: PropTypes.number,
1362
+ }),
1363
+ error: PropTypes.string,
1364
+ }).isRequired,
1365
+ };