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,2053 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+ import { useParams, useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import Image from "next/image";
7
+ import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, CodeBuddyQuotaCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal, Pagination } from "@/shared/components";
8
+ import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers";
9
+ import { getModelsByProviderId } from "@/shared/constants/models";
10
+ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
11
+ import { translate } from "@/i18n/runtime";
12
+ import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
13
+ import { classifyConnectionStatus, CONNECTION_STATUS_FILTERS, filterConnectionByStatus, isTerminalConnectionStatus } from "@/shared/utils/connectionStatus";
14
+ import ModelRow from "./ModelRow";
15
+ import PassthroughModelsSection from "./PassthroughModelsSection";
16
+ import CompatibleModelsSection from "./CompatibleModelsSection";
17
+ import ConnectionRow from "./ConnectionRow";
18
+ import AddApiKeyModal from "./AddApiKeyModal";
19
+ import EditCompatibleNodeModal from "./EditCompatibleNodeModal";
20
+ import AddCustomModelModal from "./AddCustomModelModal";
21
+
22
+ const ONE_BY_ONE_DELAY_MS = 1000;
23
+ const KIRO_BULK_JOB_STORAGE_KEY = "kiro-bulk-import-active-job";
24
+ const KIRO_BULK_JOB_EXPIRED_MESSAGE = "Bulk import progress expired or was cleared.";
25
+ const CONNECTIONS_DEFAULT_PAGE_SIZE = 20;
26
+ const MODELS_DEFAULT_PAGE_SIZE = 20;
27
+ const PROVIDER_DETAIL_FETCH_TIMEOUT_MS = 8000;
28
+
29
+ function sleep(ms) {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+
33
+ function isBulkJobTerminal(status) {
34
+ return ["completed", "cancelled", "failed"].includes(status);
35
+ }
36
+
37
+ function isBulkJobActive(status) {
38
+ return ["queued", "running", "needs_manual"].includes(status);
39
+ }
40
+
41
+ async function fetchBulkJobById(jobId) {
42
+ if (!jobId) return null;
43
+ const res = await fetch(`/api/oauth/kiro/bulk-import/${jobId}`, { cache: "no-store" });
44
+ const data = await res.json();
45
+ return { res, data };
46
+ }
47
+
48
+ async function fetchLatestBulkJob(scope = "active") {
49
+ const res = await fetch(`/api/oauth/kiro/bulk-import/latest?scope=${encodeURIComponent(scope)}`, { cache: "no-store" });
50
+ const data = await res.json();
51
+ return { res, data };
52
+ }
53
+
54
+ async function fetchJsonWithTimeout(url) {
55
+ const controller = new AbortController();
56
+ const timeout = window.setTimeout(() => controller.abort(), PROVIDER_DETAIL_FETCH_TIMEOUT_MS);
57
+
58
+ try {
59
+ const response = await fetch(url, {
60
+ cache: "no-store",
61
+ signal: controller.signal,
62
+ });
63
+ let data = null;
64
+ try {
65
+ data = await response.json();
66
+ } catch {
67
+ data = null;
68
+ }
69
+ return { response, data };
70
+ } finally {
71
+ window.clearTimeout(timeout);
72
+ }
73
+ }
74
+
75
+ export default function ProviderDetailPage() {
76
+ const params = useParams();
77
+ const router = useRouter();
78
+ const providerId = params.id;
79
+ const [connections, setConnections] = useState([]);
80
+ const [loading, setLoading] = useState(true);
81
+ const [providerNode, setProviderNode] = useState(null);
82
+ const [proxyPools, setProxyPools] = useState([]);
83
+ const [showOAuthModal, setShowOAuthModal] = useState(false);
84
+ const [showIFlowCookieModal, setShowIFlowCookieModal] = useState(false);
85
+ const [showCodeBuddyQuotaCookieModal, setShowCodeBuddyQuotaCookieModal] = useState(false);
86
+ const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
87
+ const [addConnectionError, setAddConnectionError] = useState("");
88
+ const [showEditModal, setShowEditModal] = useState(false);
89
+ const [showEditNodeModal, setShowEditNodeModal] = useState(false);
90
+ const [showBulkProxyModal, setShowBulkProxyModal] = useState(false);
91
+ const [selectedConnection, setSelectedConnection] = useState(null);
92
+ const [modelAliases, setModelAliases] = useState({});
93
+ const [headerImgError, setHeaderImgError] = useState(false);
94
+ const [modelTestResults, setModelTestResults] = useState({});
95
+ const [modelsTestError, setModelsTestError] = useState("");
96
+ const [testingModelId, setTestingModelId] = useState(null);
97
+ const [showAddCustomModel, setShowAddCustomModel] = useState(false);
98
+ const [selectedConnectionIds, setSelectedConnectionIds] = useState([]);
99
+ const [bulkProxyPoolId, setBulkProxyPoolId] = useState("__none__");
100
+ const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false);
101
+ const [providerStrategy, setProviderStrategy] = useState(null);
102
+ const [providerStickyLimit, setProviderStickyLimit] = useState("");
103
+ const [thinkingMode, setThinkingMode] = useState("auto");
104
+ const [suggestedModels, setSuggestedModels] = useState([]);
105
+ const [kiloFreeModels, setKiloFreeModels] = useState([]);
106
+ const [disabledModelIds, setDisabledModelIds] = useState([]);
107
+ const [confirmState, setConfirmState] = useState(null);
108
+ const [showAgRiskModal, setShowAgRiskModal] = useState(false);
109
+ const [oneByOneRunning, setOneByOneRunning] = useState(false);
110
+ const [oneByOneStopping, setOneByOneStopping] = useState(false);
111
+ const [oneByOneCurrentConnectionId, setOneByOneCurrentConnectionId] = useState(null);
112
+ const [oneByOneResults, setOneByOneResults] = useState({});
113
+ const [oneByOneSummary, setOneByOneSummary] = useState(null);
114
+ const stopOneByOneRef = useRef(false);
115
+ const [importingQoderModels, setImportingQoderModels] = useState(false);
116
+ const [kiroBulkJob, setKiroBulkJob] = useState(null);
117
+ const [kiroBulkNotice, setKiroBulkNotice] = useState("");
118
+ const [connectionsPage, setConnectionsPage] = useState(1);
119
+ const [connectionsPageSize, setConnectionsPageSize] = useState(CONNECTIONS_DEFAULT_PAGE_SIZE);
120
+ const [connectionStatusFilter, setConnectionStatusFilter] = useState("all");
121
+ const [modelsPage, setModelsPage] = useState(1);
122
+ const [modelsPageSize, setModelsPageSize] = useState(MODELS_DEFAULT_PAGE_SIZE);
123
+ const { copied, copy } = useCopyToClipboard();
124
+ const kiroBulkSuccessRef = useRef(0);
125
+
126
+ const AG_RISK_STORAGE_KEY = "ag_risk_confirmed";
127
+
128
+ const clearKiroBulkProgress = useCallback(() => {
129
+ setKiroBulkJob(null);
130
+ setKiroBulkNotice("");
131
+ kiroBulkSuccessRef.current = 0;
132
+
133
+ if (typeof window !== "undefined") {
134
+ window.localStorage.removeItem(KIRO_BULK_JOB_STORAGE_KEY);
135
+ }
136
+ }, []);
137
+
138
+ const openOAuthConnection = () => {
139
+ setShowOAuthModal(true);
140
+ };
141
+
142
+ const triggerOAuthConnection = () => {
143
+ if (providerId === "kiro" || providerId === "codebuddy") {
144
+ router.push(`/dashboard/automation?provider=${providerId}`);
145
+ return;
146
+ }
147
+ if (providerId === "kiro" && kiroBulkJob?.jobId && isBulkJobTerminal(kiroBulkJob.status)) {
148
+ clearKiroBulkProgress();
149
+ }
150
+ if (providerId === "antigravity" && typeof window !== "undefined") {
151
+ const confirmed = window.localStorage.getItem(AG_RISK_STORAGE_KEY) === "true";
152
+ if (!confirmed) {
153
+ setShowAgRiskModal(true);
154
+ return;
155
+ }
156
+ }
157
+ if (isOAuth) {
158
+ openOAuthConnection();
159
+ return;
160
+ }
161
+ setAddConnectionError("");
162
+ setShowAddApiKeyModal(true);
163
+ };
164
+
165
+ const triggerApiKeyConnection = () => {
166
+ setAddConnectionError("");
167
+ setShowAddApiKeyModal(true);
168
+ };
169
+
170
+ const triggerAddConnection = () => {
171
+ if (isOAuth) {
172
+ triggerOAuthConnection();
173
+ return;
174
+ }
175
+ triggerApiKeyConnection();
176
+ };
177
+
178
+ const handleAgRiskConfirm = () => {
179
+ if (typeof window !== "undefined") {
180
+ window.localStorage.setItem(AG_RISK_STORAGE_KEY, "true");
181
+ }
182
+ setShowAgRiskModal(false);
183
+ if (isOAuth) {
184
+ openOAuthConnection();
185
+ return;
186
+ }
187
+ triggerApiKeyConnection();
188
+ };
189
+
190
+ const providerInfo = providerNode
191
+ ? {
192
+ id: providerNode.id,
193
+ name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"),
194
+ color: providerNode.type === "anthropic-compatible" ? "#D97757" : "#10A37F",
195
+ textIcon: providerNode.type === "anthropic-compatible" ? "AC" : "OC",
196
+ apiType: providerNode.apiType,
197
+ baseUrl: providerNode.baseUrl,
198
+ type: providerNode.type,
199
+ }
200
+ : (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId] || FREE_TIER_PROVIDERS[providerId] || WEB_COOKIE_PROVIDERS[providerId]);
201
+ const authModes = providerInfo?.authModes || [];
202
+ const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId] || authModes.includes("oauth");
203
+ const supportsApiKeyAuth = !!APIKEY_PROVIDERS[providerId] || authModes.includes("apikey");
204
+ const isFreeNoAuth = !!FREE_PROVIDERS[providerId]?.noAuth;
205
+ const models = getModelsByProviderId(providerId);
206
+ const providerAlias = getProviderAlias(providerId);
207
+
208
+ const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
209
+ const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
210
+ const isCompatible = isOpenAICompatible || isAnthropicCompatible;
211
+ const hasDualAuthModes = !isCompatible && isOAuth && supportsApiKeyAuth;
212
+ const usesAutomationLogin = providerId === "kiro" || providerId === "codebuddy";
213
+ const oauthConnectionLabel = providerId === "xai" ? "Grok Build OAuth" : "OAuth";
214
+ const apiKeyConnectionLabel = providerId === "xai" ? "xAI API Key" : "API Key";
215
+ const thinkingConfig = AI_PROVIDERS[providerId]?.thinkingConfig || THINKING_CONFIG.extended;
216
+
217
+ const providerStorageAlias = isCompatible ? providerId : providerAlias;
218
+ const providerDisplayAlias = isCompatible
219
+ ? (providerNode?.prefix || providerId)
220
+ : providerAlias;
221
+
222
+ const fetchDisabledModels = useCallback(async () => {
223
+ try {
224
+ const res = await fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}`, { cache: "no-store" });
225
+ const data = await res.json();
226
+ if (res.ok) setDisabledModelIds(data.ids || []);
227
+ } catch (error) {
228
+ console.log("Error fetching disabled models:", error);
229
+ }
230
+ }, [providerStorageAlias]);
231
+
232
+ const handleDisableModel = async (modelId) => {
233
+ try {
234
+ const res = await fetch("/api/models/disabled", {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({ providerAlias: providerStorageAlias, ids: [modelId] }),
238
+ });
239
+ if (res.ok) await fetchDisabledModels();
240
+ } catch (error) {
241
+ console.log("Error disabling model:", error);
242
+ }
243
+ };
244
+
245
+ const handleEnableModel = async (modelId) => {
246
+ try {
247
+ const res = await fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}&id=${encodeURIComponent(modelId)}`, { method: "DELETE" });
248
+ if (res.ok) await fetchDisabledModels();
249
+ } catch (error) {
250
+ console.log("Error enabling model:", error);
251
+ }
252
+ };
253
+
254
+ const handleDisableAll = async (ids) => {
255
+ if (!ids.length) return;
256
+ setConfirmState({
257
+ title: "Disable All Models",
258
+ message: `Disable all ${ids.length} model(s)?`,
259
+ onConfirm: async () => {
260
+ setConfirmState(null);
261
+ try {
262
+ const res = await fetch("/api/models/disabled", {
263
+ method: "POST",
264
+ headers: { "Content-Type": "application/json" },
265
+ body: JSON.stringify({ providerAlias: providerStorageAlias, ids }),
266
+ });
267
+ if (res.ok) await fetchDisabledModels();
268
+ } catch (error) {
269
+ console.log("Error disabling all models:", error);
270
+ }
271
+ }
272
+ });
273
+ };
274
+
275
+ const handleEnableAll = async () => {
276
+ try {
277
+ const res = await fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}`, { method: "DELETE" });
278
+ if (res.ok) await fetchDisabledModels();
279
+ } catch (error) {
280
+ console.log("Error enabling all models:", error);
281
+ }
282
+ };
283
+
284
+ // Define callbacks BEFORE the useEffect that uses them
285
+ const fetchAliases = useCallback(async () => {
286
+ try {
287
+ const res = await fetch("/api/models/alias");
288
+ const data = await res.json();
289
+ if (res.ok) {
290
+ setModelAliases(data.aliases || {});
291
+ }
292
+ } catch (error) {
293
+ console.log("Error fetching aliases:", error);
294
+ }
295
+ }, []);
296
+
297
+ // Fetch free models from Kilo API for kilocode provider
298
+ useEffect(() => {
299
+ if (providerId !== "kilocode") return;
300
+ fetch("/api/providers/kilo/free-models")
301
+ .then((res) => res.json())
302
+ .then((data) => { if (data.models?.length) setKiloFreeModels(data.models); })
303
+ .catch(() => {});
304
+ }, [providerId]);
305
+
306
+ const fetchConnections = useCallback(async () => {
307
+ try {
308
+ const [connectionsResult, nodesResult, proxyPoolsResult, settingsResult] = await Promise.allSettled([
309
+ fetchJsonWithTimeout("/api/providers"),
310
+ fetchJsonWithTimeout("/api/provider-nodes"),
311
+ fetchJsonWithTimeout("/api/proxy-pools?isActive=true"),
312
+ fetchJsonWithTimeout("/api/settings"),
313
+ ]);
314
+
315
+ const connectionsRes = connectionsResult.status === "fulfilled" ? connectionsResult.value.response : null;
316
+ const connectionsData = connectionsResult.status === "fulfilled" ? connectionsResult.value.data : {};
317
+ const nodesRes = nodesResult.status === "fulfilled" ? nodesResult.value.response : null;
318
+ const nodesData = nodesResult.status === "fulfilled" ? nodesResult.value.data : {};
319
+ const proxyPoolsRes = proxyPoolsResult.status === "fulfilled" ? proxyPoolsResult.value.response : null;
320
+ const proxyPoolsData = proxyPoolsResult.status === "fulfilled" ? proxyPoolsResult.value.data : {};
321
+ const settingsRes = settingsResult.status === "fulfilled" ? settingsResult.value.response : null;
322
+ const settingsData = settingsRes?.ok ? (settingsResult.value.data || {}) : {};
323
+
324
+ if (connectionsRes?.ok) {
325
+ const filtered = (connectionsData.connections || []).filter(c => c.provider === providerId);
326
+ setConnections(filtered);
327
+ }
328
+ if (proxyPoolsRes?.ok) {
329
+ setProxyPools(proxyPoolsData.proxyPools || []);
330
+ }
331
+ // Load per-provider strategy override
332
+ const override = (settingsData.providerStrategies || {})[providerId] || {};
333
+ setProviderStrategy(override.fallbackStrategy || null);
334
+ setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1");
335
+ // Load per-provider thinking config
336
+ const thinkingCfg = (settingsData.providerThinking || {})[providerId] || {};
337
+ setThinkingMode(thinkingCfg.mode || "auto");
338
+ if (nodesRes?.ok) {
339
+ let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null;
340
+
341
+ // Newly created compatible nodes can be briefly unavailable on one worker.
342
+ // Retry a few times before showing "Provider not found".
343
+ if (!node && isCompatible) {
344
+ for (let attempt = 0; attempt < 3; attempt += 1) {
345
+ await new Promise((resolve) => setTimeout(resolve, 150));
346
+ const { response: retryRes, data: retryData } = await fetchJsonWithTimeout("/api/provider-nodes");
347
+ if (!retryRes.ok) continue;
348
+ node = (retryData.nodes || []).find((entry) => entry.id === providerId) || null;
349
+ if (node) break;
350
+ }
351
+ }
352
+
353
+ setProviderNode(node);
354
+ }
355
+ } catch (error) {
356
+ console.log("Error fetching connections:", error);
357
+ } finally {
358
+ setLoading(false);
359
+ }
360
+ }, [providerId, isCompatible]);
361
+
362
+ const handleUpdateNode = async (formData) => {
363
+ try {
364
+ const res = await fetch(`/api/provider-nodes/${providerId}`, {
365
+ method: "PUT",
366
+ headers: { "Content-Type": "application/json" },
367
+ body: JSON.stringify(formData),
368
+ });
369
+ const data = await res.json();
370
+ if (res.ok) {
371
+ setProviderNode(data.node);
372
+ await fetchConnections();
373
+ setShowEditNodeModal(false);
374
+ }
375
+ } catch (error) {
376
+ console.log("Error updating provider node:", error);
377
+ }
378
+ };
379
+
380
+ const saveProviderStrategy = async (strategy, stickyLimit) => {
381
+ try {
382
+ const settingsRes = await fetch("/api/settings", { cache: "no-store" });
383
+ const settingsData = settingsRes.ok ? await settingsRes.json() : {};
384
+ const current = settingsData.providerStrategies || {};
385
+
386
+ // Build override: null strategy means remove override, use global
387
+ const override = {};
388
+ if (strategy) override.fallbackStrategy = strategy;
389
+ if (strategy === "round-robin" && stickyLimit !== "") {
390
+ override.stickyRoundRobinLimit = Number(stickyLimit) || 3;
391
+ }
392
+
393
+ const updated = { ...current };
394
+ if (Object.keys(override).length === 0) {
395
+ delete updated[providerId];
396
+ } else {
397
+ updated[providerId] = override;
398
+ }
399
+
400
+ await fetch("/api/settings", {
401
+ method: "PATCH",
402
+ headers: { "Content-Type": "application/json" },
403
+ body: JSON.stringify({ providerStrategies: updated }),
404
+ });
405
+ } catch (error) {
406
+ console.log("Error saving provider strategy:", error);
407
+ }
408
+ };
409
+
410
+ const handleRoundRobinToggle = (enabled) => {
411
+ const strategy = enabled ? "round-robin" : null;
412
+ const sticky = enabled ? (providerStickyLimit || "1") : providerStickyLimit;
413
+ if (enabled && !providerStickyLimit) setProviderStickyLimit("1");
414
+ setProviderStrategy(strategy);
415
+ saveProviderStrategy(strategy, sticky);
416
+ };
417
+
418
+ const handleStickyLimitChange = (value) => {
419
+ setProviderStickyLimit(value);
420
+ saveProviderStrategy("round-robin", value);
421
+ };
422
+
423
+ const saveThinkingConfig = async (mode) => {
424
+ try {
425
+ const settingsRes = await fetch("/api/settings", { cache: "no-store" });
426
+ const settingsData = settingsRes.ok ? await settingsRes.json() : {};
427
+ const current = settingsData.providerThinking || {};
428
+ const updated = { ...current };
429
+ if (!mode || mode === "auto") {
430
+ delete updated[providerId];
431
+ } else {
432
+ updated[providerId] = { mode };
433
+ }
434
+ await fetch("/api/settings", {
435
+ method: "PATCH",
436
+ headers: { "Content-Type": "application/json" },
437
+ body: JSON.stringify({ providerThinking: updated }),
438
+ });
439
+ } catch (error) {
440
+ console.log("Error saving thinking config:", error);
441
+ }
442
+ };
443
+
444
+ const handleThinkingModeChange = (mode) => {
445
+ setThinkingMode(mode);
446
+ saveThinkingConfig(mode);
447
+ };
448
+
449
+ useEffect(() => {
450
+ fetchConnections();
451
+ fetchAliases();
452
+ fetchDisabledModels();
453
+ }, [fetchConnections, fetchAliases, fetchDisabledModels]);
454
+
455
+ useEffect(() => {
456
+ setConnectionsPage(1);
457
+ setModelsPage(1);
458
+ setConnectionStatusFilter("all");
459
+ }, [providerId]);
460
+
461
+ useEffect(() => {
462
+ setConnectionsPage(1);
463
+ }, [connectionStatusFilter, connectionsPageSize]);
464
+
465
+ useEffect(() => {
466
+ setModelsPage(1);
467
+ }, [modelsPageSize, disabledModelIds.length, models.length, kiloFreeModels.length]);
468
+
469
+ useEffect(() => {
470
+ if (providerId !== "kiro" || typeof window === "undefined") return;
471
+ const storedJobId = window.localStorage.getItem(KIRO_BULK_JOB_STORAGE_KEY);
472
+
473
+ let cancelled = false;
474
+
475
+ const restoreJob = async () => {
476
+ try {
477
+ setKiroBulkNotice("");
478
+
479
+ if (kiroBulkJob?.jobId) return;
480
+
481
+ const latest = await fetchLatestBulkJob();
482
+ if (!cancelled && latest.res.ok && latest.data?.job) {
483
+ setKiroBulkJob(latest.data.job);
484
+ kiroBulkSuccessRef.current = latest.data.job.summary?.success || 0;
485
+ window.localStorage.setItem(KIRO_BULK_JOB_STORAGE_KEY, latest.data.job.jobId);
486
+ return;
487
+ }
488
+
489
+ if (storedJobId) {
490
+ const direct = await fetchBulkJobById(storedJobId);
491
+ if (!cancelled && direct?.res.ok && direct.data?.job && isBulkJobActive(direct.data.job.status)) {
492
+ setKiroBulkJob(direct.data.job);
493
+ kiroBulkSuccessRef.current = direct.data.job.summary?.success || 0;
494
+ return;
495
+ }
496
+
497
+ clearKiroBulkProgress();
498
+ setKiroBulkNotice(KIRO_BULK_JOB_EXPIRED_MESSAGE);
499
+ }
500
+ } catch {
501
+ if (!cancelled) {
502
+ setKiroBulkNotice(KIRO_BULK_JOB_EXPIRED_MESSAGE);
503
+ }
504
+ }
505
+ };
506
+
507
+ void restoreJob();
508
+
509
+ return () => {
510
+ cancelled = true;
511
+ };
512
+ }, [clearKiroBulkProgress, kiroBulkJob?.jobId, providerId]);
513
+
514
+ useEffect(() => {
515
+ if (providerId !== "kiro" || !kiroBulkJob?.jobId || isBulkJobTerminal(kiroBulkJob.status)) return undefined;
516
+
517
+ const interval = window.setInterval(async () => {
518
+ try {
519
+ const current = await fetchBulkJobById(kiroBulkJob.jobId);
520
+ if (current?.res.ok && current.data?.job) {
521
+ const nextSuccessCount = current.data.job.summary?.success || 0;
522
+ if (nextSuccessCount > kiroBulkSuccessRef.current) {
523
+ kiroBulkSuccessRef.current = nextSuccessCount;
524
+ await fetchConnections();
525
+ }
526
+
527
+ setKiroBulkNotice("");
528
+ setKiroBulkJob(current.data.job);
529
+ return;
530
+ }
531
+
532
+ if (current?.res.status === 404) {
533
+ const latest = await fetchLatestBulkJob();
534
+ if (latest.res.ok && latest.data?.job) {
535
+ const nextSuccessCount = latest.data.job.summary?.success || 0;
536
+ if (nextSuccessCount > kiroBulkSuccessRef.current) {
537
+ kiroBulkSuccessRef.current = nextSuccessCount;
538
+ await fetchConnections();
539
+ }
540
+
541
+ setKiroBulkNotice("");
542
+ setKiroBulkJob(latest.data.job);
543
+ if (typeof window !== "undefined") {
544
+ window.localStorage.setItem(KIRO_BULK_JOB_STORAGE_KEY, latest.data.job.jobId);
545
+ }
546
+ return;
547
+ }
548
+
549
+ clearKiroBulkProgress();
550
+ setKiroBulkNotice(KIRO_BULK_JOB_EXPIRED_MESSAGE);
551
+ }
552
+ } catch {
553
+ // Keep the last snapshot visible and allow the user to reopen the modal.
554
+ }
555
+ }, 2000);
556
+
557
+ return () => window.clearInterval(interval);
558
+ }, [clearKiroBulkProgress, fetchConnections, kiroBulkJob?.jobId, kiroBulkJob?.status, providerId]);
559
+
560
+ // Fetch suggested models from provider's public API (if configured)
561
+ useEffect(() => {
562
+ const fetcher = (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId] || FREE_TIER_PROVIDERS[providerId])?.modelsFetcher;
563
+ if (!fetcher) return;
564
+ fetchSuggestedModels(fetcher).then(setSuggestedModels);
565
+ }, [providerId]);
566
+
567
+ const handleSetAlias = async (modelId, alias, providerAliasOverride = providerAlias) => {
568
+ const fullModel = `${providerAliasOverride}/${modelId}`;
569
+ try {
570
+ const res = await fetch("/api/models/alias", {
571
+ method: "PUT",
572
+ headers: { "Content-Type": "application/json" },
573
+ body: JSON.stringify({ model: fullModel, alias }),
574
+ });
575
+ if (res.ok) {
576
+ await fetchAliases();
577
+ } else {
578
+ const data = await res.json();
579
+ alert(data.error || "Failed to set alias");
580
+ }
581
+ } catch (error) {
582
+ console.log("Error setting alias:", error);
583
+ }
584
+ };
585
+
586
+ const handleDeleteAlias = async (alias) => {
587
+ try {
588
+ const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, {
589
+ method: "DELETE",
590
+ });
591
+ if (res.ok) {
592
+ await fetchAliases();
593
+ }
594
+ } catch (error) {
595
+ console.log("Error deleting alias:", error);
596
+ }
597
+ };
598
+
599
+ // Fetch Qoder model list and automatically add to available models
600
+ const handleImportQoderModels = async () => {
601
+ if (importingQoderModels) return;
602
+ const activeConnection = connections.find((conn) => conn.isActive !== false);
603
+ if (!activeConnection) {
604
+ alert(translate("Please add an active Qoder connection first"));
605
+ return;
606
+ }
607
+
608
+ setImportingQoderModels(true);
609
+ try {
610
+ const res = await fetch(`/api/providers/${activeConnection.id}/models`);
611
+ const data = await res.json();
612
+ if (!res.ok) {
613
+ alert(data.error || translate("Failed to fetch models"));
614
+ return;
615
+ }
616
+ const models = data.models || [];
617
+ if (models.length === 0) {
618
+ alert(translate("No models returned"));
619
+ return;
620
+ }
621
+
622
+ let importedCount = 0;
623
+ for (const model of models) {
624
+ const modelId = model.id || model.name;
625
+ if (!modelId) continue;
626
+
627
+ // Qoder model ID format may be "qoder/auto" or "auto", need to remove prefix
628
+ const cleanModelId = modelId.replace(/^qoder\//, "");
629
+ const fullModel = `${providerStorageAlias}/${cleanModelId}`;
630
+
631
+ // Check if already exists
632
+ if (Object.values(modelAliases).includes(fullModel)) {
633
+ continue;
634
+ }
635
+
636
+ // Use model ID as alias
637
+ const alias = cleanModelId;
638
+ if (modelAliases[alias]) {
639
+ continue;
640
+ }
641
+
642
+ await handleSetAlias(cleanModelId, alias, providerStorageAlias);
643
+ importedCount += 1;
644
+ }
645
+
646
+ if (importedCount === 0) {
647
+ alert(translate("All models already exist, no new models added"));
648
+ } else {
649
+ alert(translate("Successfully added") + ` ${importedCount} ` + translate("models"));
650
+ }
651
+ } catch (error) {
652
+ console.log("Error importing Qoder models:", error);
653
+ alert(translate("Error fetching models") + ": " + error.message);
654
+ } finally {
655
+ setImportingQoderModels(false);
656
+ }
657
+ };
658
+
659
+ const handleRunOneByOneTest = async () => {
660
+ if (oneByOneRunning || connections.length === 0) return;
661
+
662
+ const queuedState = Object.fromEntries(
663
+ connections.map((connection) => [connection.id, { state: "queued", error: null }]),
664
+ );
665
+
666
+ stopOneByOneRef.current = false;
667
+ setOneByOneRunning(true);
668
+ setOneByOneStopping(false);
669
+ setOneByOneCurrentConnectionId(null);
670
+ setOneByOneResults(queuedState);
671
+ setOneByOneSummary({ total: connections.length, completed: 0, passed: 0, failed: 0, stopped: false });
672
+
673
+ let passed = 0;
674
+ let failed = 0;
675
+
676
+ try {
677
+ for (let index = 0; index < connections.length; index += 1) {
678
+ if (stopOneByOneRef.current) {
679
+ setOneByOneSummary({
680
+ total: connections.length,
681
+ completed: index,
682
+ passed,
683
+ failed,
684
+ stopped: true,
685
+ });
686
+ break;
687
+ }
688
+
689
+ const connection = connections[index];
690
+ setOneByOneCurrentConnectionId(connection.id);
691
+ setOneByOneResults((prev) => ({
692
+ ...prev,
693
+ [connection.id]: { state: "testing", error: null },
694
+ }));
695
+
696
+ try {
697
+ const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" });
698
+ const data = await res.json();
699
+ const valid = !!data.valid;
700
+
701
+ if (valid) {
702
+ passed += 1;
703
+ } else {
704
+ failed += 1;
705
+ }
706
+
707
+ setOneByOneResults((prev) => ({
708
+ ...prev,
709
+ [connection.id]: {
710
+ state: valid ? "success" : "failed",
711
+ error: valid ? null : (data.error || null),
712
+ },
713
+ }));
714
+ } catch (error) {
715
+ failed += 1;
716
+ setOneByOneResults((prev) => ({
717
+ ...prev,
718
+ [connection.id]: {
719
+ state: "failed",
720
+ error: error.message || "Test failed",
721
+ },
722
+ }));
723
+ }
724
+
725
+ setOneByOneSummary({
726
+ total: connections.length,
727
+ completed: index + 1,
728
+ passed,
729
+ failed,
730
+ stopped: false,
731
+ });
732
+
733
+ if (index < connections.length - 1) {
734
+ await sleep(ONE_BY_ONE_DELAY_MS);
735
+ }
736
+ }
737
+ } finally {
738
+ setOneByOneCurrentConnectionId(null);
739
+ setOneByOneRunning(false);
740
+ setOneByOneStopping(false);
741
+ stopOneByOneRef.current = false;
742
+ }
743
+ };
744
+
745
+ const handleStopOneByOneTest = () => {
746
+ if (!oneByOneRunning) return;
747
+ stopOneByOneRef.current = true;
748
+ setOneByOneStopping(true);
749
+ };
750
+
751
+ const handleDelete = async (id) => {
752
+ setConfirmState({
753
+ title: "Delete Connection",
754
+ message: "Delete this connection?",
755
+ onConfirm: async () => {
756
+ setConfirmState(null);
757
+ try {
758
+ const res = await fetch(`/api/providers/${id}`, { method: "DELETE" });
759
+ if (res.ok) {
760
+ setConnections(connections.filter(c => c.id !== id));
761
+ }
762
+ } catch (error) {
763
+ console.log("Error deleting connection:", error);
764
+ }
765
+ }
766
+ });
767
+ };
768
+
769
+ const handleOAuthSuccess = () => {
770
+ fetchConnections();
771
+ setShowOAuthModal(false);
772
+ };
773
+
774
+ const handleKiroBulkJobChange = useCallback((job) => {
775
+ setKiroBulkJob(job);
776
+ setKiroBulkNotice("");
777
+
778
+ if (typeof window === "undefined") return;
779
+
780
+ if (!job?.jobId) {
781
+ clearKiroBulkProgress();
782
+ return;
783
+ }
784
+
785
+ window.localStorage.setItem(KIRO_BULK_JOB_STORAGE_KEY, job.jobId);
786
+ kiroBulkSuccessRef.current = Math.max(
787
+ kiroBulkSuccessRef.current,
788
+ job.summary?.success || 0,
789
+ );
790
+ }, [clearKiroBulkProgress]);
791
+
792
+ const handleIFlowCookieSuccess = () => {
793
+ fetchConnections();
794
+ setShowIFlowCookieModal(false);
795
+ };
796
+
797
+ const handleCodeBuddyQuotaCookieSuccess = () => {
798
+ fetchConnections();
799
+ setShowCodeBuddyQuotaCookieModal(false);
800
+ };
801
+
802
+ const handleSaveApiKey = async (formData) => {
803
+ setAddConnectionError("");
804
+ try {
805
+ const res = await fetch("/api/providers", {
806
+ method: "POST",
807
+ headers: { "Content-Type": "application/json" },
808
+ body: JSON.stringify({ provider: providerId, ...formData }),
809
+ });
810
+
811
+ let data = null;
812
+ try {
813
+ data = await res.json();
814
+ } catch {
815
+ data = null;
816
+ }
817
+
818
+ if (res.ok) {
819
+ await fetchConnections();
820
+ setShowAddApiKeyModal(false);
821
+ return;
822
+ }
823
+
824
+ setAddConnectionError(data?.error || "Failed to save connection");
825
+ } catch (error) {
826
+ console.log("Error saving connection:", error);
827
+ setAddConnectionError("Failed to save connection");
828
+ }
829
+ };
830
+
831
+ const handleUpdateConnection = async (formData) => {
832
+ try {
833
+ const res = await fetch(`/api/providers/${selectedConnection.id}`, {
834
+ method: "PUT",
835
+ headers: { "Content-Type": "application/json" },
836
+ body: JSON.stringify(formData),
837
+ });
838
+ if (res.ok) {
839
+ await fetchConnections();
840
+ setShowEditModal(false);
841
+ }
842
+ } catch (error) {
843
+ console.log("Error updating connection:", error);
844
+ }
845
+ };
846
+
847
+ const handleUpdateConnectionStatus = async (id, isActive) => {
848
+ try {
849
+ const res = await fetch(`/api/providers/${id}`, {
850
+ method: "PUT",
851
+ headers: { "Content-Type": "application/json" },
852
+ body: JSON.stringify({ isActive }),
853
+ });
854
+ if (res.ok) {
855
+ setConnections(prev => prev.map(c => c.id === id ? { ...c, isActive } : c));
856
+ }
857
+ } catch (error) {
858
+ console.log("Error updating connection status:", error);
859
+ }
860
+ };
861
+
862
+ const handleSwapPriority = async (index1, index2) => {
863
+ // Optimistic update state
864
+ const newConnections = [...connections];
865
+ [newConnections[index1], newConnections[index2]] = [newConnections[index2], newConnections[index1]];
866
+ setConnections(newConnections);
867
+
868
+ try {
869
+ await Promise.all([
870
+ fetch(`/api/providers/${newConnections[index1].id}`, {
871
+ method: "PUT",
872
+ headers: { "Content-Type": "application/json" },
873
+ body: JSON.stringify({ priority: index1 }),
874
+ }),
875
+ fetch(`/api/providers/${newConnections[index2].id}`, {
876
+ method: "PUT",
877
+ headers: { "Content-Type": "application/json" },
878
+ body: JSON.stringify({ priority: index2 }),
879
+ }),
880
+ ]);
881
+ } catch (error) {
882
+ console.log("Error swapping priority:", error);
883
+ await fetchConnections();
884
+ }
885
+ };
886
+
887
+ const filteredConnections = connections.filter((conn) => filterConnectionByStatus(conn, connectionStatusFilter));
888
+ const connectionStatusCounts = CONNECTION_STATUS_FILTERS.reduce((acc, filter) => {
889
+ acc[filter.id] = filter.id === "all"
890
+ ? connections.length
891
+ : connections.filter((conn) => filterConnectionByStatus(conn, filter.id)).length;
892
+ return acc;
893
+ }, {});
894
+ const terminalConnections = filteredConnections.filter(isTerminalConnectionStatus);
895
+ const effectiveSelectedConnectionIds = selectedConnectionIds.filter((id) => connections.some((conn) => conn.id === id));
896
+ const selectedConnections = connections.filter((conn) => effectiveSelectedConnectionIds.includes(conn.id));
897
+ const codeBuddyQuotaCookieConnectionIds = providerId === "codebuddy"
898
+ ? (selectedConnections.length > 0 ? selectedConnections : connections).map((conn) => conn.id)
899
+ : [];
900
+ const allSelected = filteredConnections.length > 0 && filteredConnections.every((conn) => effectiveSelectedConnectionIds.includes(conn.id));
901
+
902
+ const openCodeBuddyQuotaCookieModal = () => {
903
+ if (connections.length > 1 && selectedConnections.length === 0) {
904
+ alert("Select the CodeBuddy connection(s) that should use this quota cookie first.");
905
+ return;
906
+ }
907
+ setShowCodeBuddyQuotaCookieModal(true);
908
+ };
909
+
910
+ const toggleSelectConnection = (connectionId) => {
911
+ setSelectedConnectionIds((prev) => (
912
+ prev.includes(connectionId)
913
+ ? prev.filter((id) => id !== connectionId)
914
+ : [...prev, connectionId]
915
+ ));
916
+ };
917
+
918
+ const toggleSelectAllConnections = () => {
919
+ if (allSelected) {
920
+ setSelectedConnectionIds((prev) => prev.filter((id) => !filteredConnections.some((conn) => conn.id === id)));
921
+ return;
922
+ }
923
+ setSelectedConnectionIds((prev) => Array.from(new Set([...prev, ...filteredConnections.map((conn) => conn.id)])));
924
+ };
925
+
926
+ const clearSelection = () => {
927
+ setSelectedConnectionIds([]);
928
+ setBulkProxyPoolId("__none__");
929
+ };
930
+
931
+ const selectedProxySummary = (() => {
932
+ if (selectedConnections.length === 0) return "";
933
+ const poolIds = new Set(selectedConnections.map((conn) => conn.providerSpecificData?.proxyPoolId || "__none__"));
934
+ if (poolIds.size === 1) {
935
+ const onlyId = [...poolIds][0];
936
+ if (onlyId === "__none__") return "All selected currently unbound";
937
+ const pool = proxyPools.find((p) => p.id === onlyId);
938
+ return `All selected currently bound to ${pool?.name || onlyId}`;
939
+ }
940
+ return "Selected connections have mixed proxy bindings";
941
+ })();
942
+
943
+ const openBulkProxyModal = () => {
944
+ if (selectedConnections.length === 0) return;
945
+ const uniquePoolIds = [...new Set(selectedConnections.map((conn) => conn.providerSpecificData?.proxyPoolId || "__none__"))];
946
+ setBulkProxyPoolId(uniquePoolIds.length === 1 ? uniquePoolIds[0] : "__none__");
947
+ setShowBulkProxyModal(true);
948
+ };
949
+
950
+ const closeBulkProxyModal = () => {
951
+ if (bulkUpdatingProxy) return;
952
+ setShowBulkProxyModal(false);
953
+ };
954
+
955
+ const applyProxyAssignments = async (assignments) => {
956
+ setBulkUpdatingProxy(true);
957
+ try {
958
+ let failed = 0;
959
+ for (const { connectionId, proxyPoolId } of assignments) {
960
+ try {
961
+ const res = await fetch(`/api/providers/${connectionId}`, {
962
+ method: "PUT",
963
+ headers: { "Content-Type": "application/json" },
964
+ body: JSON.stringify({ proxyPoolId }),
965
+ });
966
+ if (!res.ok) failed += 1;
967
+ } catch (e) {
968
+ console.log("Error applying proxy for", connectionId, e);
969
+ failed += 1;
970
+ }
971
+ }
972
+ if (failed > 0) alert(`Updated with ${failed} failed request(s).`);
973
+ await fetchConnections();
974
+ setShowBulkProxyModal(false);
975
+ } finally {
976
+ setBulkUpdatingProxy(false);
977
+ }
978
+ };
979
+
980
+ const handleApplySinglePool = (proxyPoolId) => {
981
+ const targets = selectedConnections.map((c) => ({ connectionId: c.id, proxyPoolId }));
982
+ return applyProxyAssignments(targets);
983
+ };
984
+
985
+ const handleApplyOneToOne = () => {
986
+ const activePools = proxyPools.filter((p) => p.isActive === true);
987
+ if (activePools.length === 0) {
988
+ alert("No active proxy pools available.");
989
+ return;
990
+ }
991
+ const targets = selectedConnections.map((c, i) => ({
992
+ connectionId: c.id,
993
+ proxyPoolId: activePools[i % activePools.length].id,
994
+ }));
995
+ return applyProxyAssignments(targets);
996
+ };
997
+
998
+ const handleDeleteTerminalConnections = () => {
999
+ if (terminalConnections.length === 0) return;
1000
+ setConfirmState({
1001
+ title: "Delete Terminal Connections",
1002
+ message: `Delete ${terminalConnections.length} terminal connection(s)? Rate-limited, cooldown, and connection-error accounts are not included.`,
1003
+ onConfirm: async () => {
1004
+ setConfirmState(null);
1005
+ for (const conn of terminalConnections) {
1006
+ try {
1007
+ await fetch(`/api/providers/${conn.id}`, { method: "DELETE" });
1008
+ } catch (error) {
1009
+ console.log("Error deleting terminal connection:", error);
1010
+ }
1011
+ }
1012
+ setSelectedConnectionIds((prev) => prev.filter((id) => !terminalConnections.some((conn) => conn.id === id)));
1013
+ await fetchConnections();
1014
+ },
1015
+ });
1016
+ };
1017
+
1018
+ const isSelected = (connectionId) => effectiveSelectedConnectionIds.includes(connectionId);
1019
+
1020
+ const totalConnectionPages = Math.max(1, Math.ceil(filteredConnections.length / connectionsPageSize));
1021
+ const activeConnectionsPage = Math.min(connectionsPage, totalConnectionPages);
1022
+ const paginatedConnections = filteredConnections.slice(
1023
+ (activeConnectionsPage - 1) * connectionsPageSize,
1024
+ activeConnectionsPage * connectionsPageSize
1025
+ );
1026
+ const totalKiroPages = totalConnectionPages;
1027
+ const activeKiroConnectionsPage = activeConnectionsPage;
1028
+ const kiroConnectionsPage = activeConnectionsPage;
1029
+ const KIRO_CONNECTIONS_PER_PAGE = connectionsPageSize;
1030
+ const setKiroConnectionsPage = setConnectionsPage;
1031
+
1032
+ const connectionsList = (
1033
+ <div className="flex min-w-0 flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
1034
+ {paginatedConnections
1035
+ .map((conn, index) => {
1036
+ const globalIndex = connections.findIndex((item) => item.id === conn.id);
1037
+ return (
1038
+ <div key={conn.id} className="flex min-w-0 items-stretch">
1039
+ <label className="flex w-8 shrink-0 items-center justify-center">
1040
+ <input
1041
+ type="checkbox"
1042
+ checked={isSelected(conn.id)}
1043
+ onChange={() => toggleSelectConnection(conn.id)}
1044
+ className="size-4 rounded border-black/20 dark:border-white/20"
1045
+ aria-label={`Select ${conn.name || conn.email || conn.id}`}
1046
+ />
1047
+ </label>
1048
+ <div className="flex-1 min-w-0">
1049
+ <ConnectionRow
1050
+ connection={conn}
1051
+ proxyPools={proxyPools}
1052
+ isOAuth={isOAuth}
1053
+ isFirst={globalIndex === 0}
1054
+ isLast={globalIndex === connections.length - 1}
1055
+ onMoveUp={() => handleSwapPriority(globalIndex, globalIndex - 1)}
1056
+ onMoveDown={() => handleSwapPriority(globalIndex, globalIndex + 1)}
1057
+ onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)}
1058
+ onUpdateProxy={async (proxyPoolId) => {
1059
+ try {
1060
+ const res = await fetch(`/api/providers/${conn.id}`, {
1061
+ method: "PUT",
1062
+ headers: { "Content-Type": "application/json" },
1063
+ body: JSON.stringify({ proxyPoolId: proxyPoolId || null }),
1064
+ });
1065
+ if (res.ok) {
1066
+ setConnections(prev => prev.map(c =>
1067
+ c.id === conn.id
1068
+ ? { ...c, providerSpecificData: { ...c.providerSpecificData, proxyPoolId: proxyPoolId || null } }
1069
+ : c
1070
+ ));
1071
+ }
1072
+ } catch (error) {
1073
+ console.log("Error updating proxy:", error);
1074
+ }
1075
+ }}
1076
+ onEdit={() => {
1077
+ setSelectedConnection(conn);
1078
+ setShowEditModal(true);
1079
+ }}
1080
+ onDelete={() => handleDelete(conn.id)}
1081
+ oneByOneStatus={oneByOneResults[conn.id] || null}
1082
+ />
1083
+ </div>
1084
+ </div>
1085
+ );
1086
+ })}
1087
+ </div>
1088
+ );
1089
+
1090
+ const activePools = proxyPools.filter((p) => p.isActive === true);
1091
+
1092
+ const bulkActionModal = (
1093
+ <Modal
1094
+ isOpen={showBulkProxyModal}
1095
+ onClose={closeBulkProxyModal}
1096
+ title={`Apply Proxy (${selectedConnections.length} selected)`}
1097
+ >
1098
+ <div className="flex flex-col gap-3">
1099
+ <div className="flex flex-col">
1100
+ <button
1101
+ onClick={handleApplyOneToOne}
1102
+ disabled={bulkUpdatingProxy || activePools.length === 0}
1103
+ className="flex items-center gap-2 rounded-lg px-3 py-2 text-left transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-50"
1104
+ >
1105
+ <span className="material-symbols-outlined text-text-muted text-[18px]">sync_alt</span>
1106
+ <span className="text-sm text-text-main">One-to-one (rotate)</span>
1107
+ </button>
1108
+ <button
1109
+ onClick={() => handleApplySinglePool(null)}
1110
+ disabled={bulkUpdatingProxy}
1111
+ className="flex items-center gap-2 rounded-lg px-3 py-2 text-left transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-50"
1112
+ >
1113
+ <span className="material-symbols-outlined text-text-muted text-[18px]">link_off</span>
1114
+ <span className="text-sm text-text-main">None (unbind all)</span>
1115
+ </button>
1116
+ {proxyPools.map((pool) => (
1117
+ <button
1118
+ key={pool.id}
1119
+ onClick={() => handleApplySinglePool(pool.id)}
1120
+ disabled={bulkUpdatingProxy || pool.isActive !== true}
1121
+ className="flex items-center gap-2 rounded-lg px-3 py-2 text-left transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-50"
1122
+ >
1123
+ <span className="material-symbols-outlined text-text-muted text-[18px]">lan</span>
1124
+ <span className="truncate text-sm text-text-main">{pool.name}</span>
1125
+ {pool.isActive !== true && (
1126
+ <span className="text-[10px] text-text-muted">(inactive)</span>
1127
+ )}
1128
+ </button>
1129
+ ))}
1130
+ </div>
1131
+
1132
+ {bulkUpdatingProxy && <p className="text-xs text-text-muted">Applying...</p>}
1133
+
1134
+ <Button onClick={closeBulkProxyModal} variant="ghost" fullWidth disabled={bulkUpdatingProxy}>
1135
+ Cancel
1136
+ </Button>
1137
+ </div>
1138
+ </Modal>
1139
+ );
1140
+
1141
+ const handleTestModel = async (modelId) => {
1142
+ if (testingModelId) return;
1143
+ setTestingModelId(modelId);
1144
+ try {
1145
+ const res = await fetch("/api/models/test", {
1146
+ method: "POST",
1147
+ headers: { "Content-Type": "application/json" },
1148
+ body: JSON.stringify({ model: `${providerStorageAlias}/${modelId}` }),
1149
+ });
1150
+ const data = await res.json();
1151
+ setModelTestResults((prev) => ({ ...prev, [modelId]: data.ok ? "ok" : "error" }));
1152
+ setModelsTestError(data.ok ? "" : (data.error || "Model not reachable"));
1153
+ } catch {
1154
+ setModelTestResults((prev) => ({ ...prev, [modelId]: "error" }));
1155
+ setModelsTestError("Network error");
1156
+ } finally {
1157
+ setTestingModelId(null);
1158
+ }
1159
+ };
1160
+
1161
+ const renderModelsSection = () => {
1162
+ if (isCompatible) {
1163
+ return (
1164
+ <CompatibleModelsSection
1165
+ providerStorageAlias={providerStorageAlias}
1166
+ providerDisplayAlias={providerDisplayAlias}
1167
+ modelAliases={modelAliases}
1168
+ copied={copied}
1169
+ onCopy={copy}
1170
+ onSetAlias={handleSetAlias}
1171
+ onDeleteAlias={handleDeleteAlias}
1172
+ connections={connections}
1173
+ isAnthropic={isAnthropicCompatible}
1174
+ />
1175
+ );
1176
+ }
1177
+ // Combine hardcoded models with Kilo free models (deduplicated)
1178
+ // Exclude non-llm models (embedding, tts, etc.) — they have dedicated pages under media-providers
1179
+ const allModels = [
1180
+ ...models,
1181
+ ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
1182
+ ].filter((m) => !m.type || m.type === "llm");
1183
+ const disabledSet = new Set(disabledModelIds);
1184
+ const displayModels = allModels.filter((m) => !disabledSet.has(m.id));
1185
+ const disabledDisplayModels = allModels.filter((m) => disabledSet.has(m.id));
1186
+ // Custom models added by user (stored as aliases: modelId → providerAlias/modelId)
1187
+ const customModels = Object.entries(modelAliases)
1188
+ .filter(([alias, fullModel]) => {
1189
+ const prefix = `${providerStorageAlias}/`;
1190
+ if (!fullModel.startsWith(prefix)) return false;
1191
+ const modelId = fullModel.slice(prefix.length);
1192
+ // Only show if not already in hardcoded list
1193
+ // For passthroughModels, include all aliases (model IDs may contain slashes like "anthropic/claude-3")
1194
+ if (providerInfo.passthroughModels) return !models.some((m) => m.id === modelId);
1195
+ return !models.some((m) => m.id === modelId) && alias === modelId;
1196
+ })
1197
+ .map(([alias, fullModel]) => ({
1198
+ id: fullModel.slice(`${providerStorageAlias}/`.length),
1199
+ alias,
1200
+ fullModel,
1201
+ }));
1202
+ const totalModelPages = Math.max(1, Math.ceil(displayModels.length / modelsPageSize));
1203
+ const activeModelsPage = Math.min(modelsPage, totalModelPages);
1204
+ const paginatedDisplayModels = displayModels.slice(
1205
+ (activeModelsPage - 1) * modelsPageSize,
1206
+ activeModelsPage * modelsPageSize
1207
+ );
1208
+
1209
+ return (
1210
+ <div className="flex flex-wrap gap-3">
1211
+ {/* Custom models first */}
1212
+ {customModels.map((model) => (
1213
+ <ModelRow
1214
+ key={model.id}
1215
+ model={{ id: model.id }}
1216
+ fullModel={`${providerDisplayAlias}/${model.id}`}
1217
+ alias={model.alias}
1218
+ copied={copied}
1219
+ onCopy={copy}
1220
+ onSetAlias={() => {}}
1221
+ onDeleteAlias={() => handleDeleteAlias(model.alias)}
1222
+ testStatus={modelTestResults[model.id]}
1223
+ onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
1224
+ isTesting={testingModelId === model.id}
1225
+ isCustom
1226
+ isFree={false}
1227
+ />
1228
+ ))}
1229
+
1230
+ {paginatedDisplayModels.map((model) => {
1231
+ const fullModel = `${providerStorageAlias}/${model.id}`;
1232
+ const oldFormatModel = `${providerId}/${model.id}`;
1233
+ const existingAlias = Object.entries(modelAliases).find(
1234
+ ([, m]) => m === fullModel || m === oldFormatModel
1235
+ )?.[0];
1236
+ return (
1237
+ <ModelRow
1238
+ key={model.id}
1239
+ model={model}
1240
+ fullModel={`${providerDisplayAlias}/${model.id}`}
1241
+ alias={existingAlias}
1242
+ copied={copied}
1243
+ onCopy={copy}
1244
+ onSetAlias={(alias) => handleSetAlias(model.id, alias, providerStorageAlias)}
1245
+ onDeleteAlias={() => handleDeleteAlias(existingAlias)}
1246
+ testStatus={modelTestResults[model.id]}
1247
+ onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
1248
+ isTesting={testingModelId === model.id}
1249
+ isFree={model.isFree}
1250
+ onDisable={() => handleDisableModel(model.id)}
1251
+ />
1252
+ );
1253
+ })}
1254
+
1255
+ {/* Add model button — inline, same style as model chips */}
1256
+ {displayModels.length > modelsPageSize && (
1257
+ <div className="w-full">
1258
+ <Pagination
1259
+ currentPage={activeModelsPage}
1260
+ pageSize={modelsPageSize}
1261
+ totalItems={displayModels.length}
1262
+ onPageChange={setModelsPage}
1263
+ onPageSizeChange={(size) => {
1264
+ setModelsPageSize(size);
1265
+ setModelsPage(1);
1266
+ }}
1267
+ />
1268
+ </div>
1269
+ )}
1270
+
1271
+ <button
1272
+ onClick={() => setShowAddCustomModel(true)}
1273
+ className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-primary/40 px-3 py-2 text-xs text-primary transition-colors hover:border-primary hover:bg-primary/5 sm:w-auto"
1274
+ >
1275
+ <span className="material-symbols-outlined text-sm">add</span>
1276
+ Add Model
1277
+ </button>
1278
+
1279
+ {/* Import Qoder models button — only show for qoder provider */}
1280
+ {providerId === "qoder" && connections.some((conn) => conn.isActive !== false) && (
1281
+ <button
1282
+ onClick={handleImportQoderModels}
1283
+ disabled={importingQoderModels}
1284
+ className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-blue-500/40 px-3 py-2 text-xs text-blue-600 dark:text-blue-400 transition-colors hover:border-blue-500 hover:bg-blue-500/5 sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
1285
+ >
1286
+ <span className="material-symbols-outlined text-sm" style={importingQoderModels ? { animation: "spin 1s linear infinite" } : undefined}>
1287
+ {importingQoderModels ? "progress_activity" : "download"}
1288
+ </span>
1289
+ {importingQoderModels ? translate("Fetching...") : translate("Fetch Qoder Models")}
1290
+ </button>
1291
+ )}
1292
+
1293
+ {/* Suggested models from provider API — show only models not yet added */}
1294
+ {suggestedModels.length > 0 && (() => {
1295
+ const addedFullModels = new Set(Object.values(modelAliases));
1296
+ const hardcodedIds = new Set(models.map((m) => m.id));
1297
+ const notAdded = suggestedModels.filter(
1298
+ (m) => !addedFullModels.has(`${providerStorageAlias}/${m.id}`) && !hardcodedIds.has(m.id)
1299
+ );
1300
+ if (notAdded.length === 0) return null;
1301
+ return (
1302
+ <div className="w-full mt-2">
1303
+ <p className="text-xs text-text-muted mb-2">Suggested free models (≥200k context):</p>
1304
+ <div className="flex flex-wrap gap-2">
1305
+ {notAdded.map((m) => (
1306
+ <button
1307
+ key={m.id}
1308
+ onClick={async () => {
1309
+ const alias = m.id.split("/").pop();
1310
+ await handleSetAlias(m.id, alias, providerStorageAlias);
1311
+ }}
1312
+ className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg border border-black/10 dark:border-white/10 text-xs text-text-muted hover:text-primary hover:border-primary/40 hover:bg-primary/5 transition-colors"
1313
+ title={`${m.name} · ${(m.contextLength / 1000).toFixed(0)}k ctx`}
1314
+ >
1315
+ <span className="material-symbols-outlined text-[13px]">add</span>
1316
+ {m.id.split("/").pop()}
1317
+ </button>
1318
+ ))}
1319
+ </div>
1320
+ </div>
1321
+ );
1322
+ })()}
1323
+
1324
+ {/* Disabled models — restorable */}
1325
+ {disabledDisplayModels.length > 0 && (
1326
+ <div className="w-full mt-2">
1327
+ <p className="text-xs text-text-muted mb-2">Disabled models ({disabledDisplayModels.length}):</p>
1328
+ <div className="flex flex-wrap gap-2">
1329
+ {disabledDisplayModels.map((m) => (
1330
+ <button
1331
+ key={m.id}
1332
+ onClick={() => handleEnableModel(m.id)}
1333
+ className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg border border-dashed border-black/10 dark:border-white/10 text-xs text-text-muted hover:text-primary hover:border-primary/40 hover:bg-primary/5 transition-colors"
1334
+ title="Restore model"
1335
+ >
1336
+ <span className="material-symbols-outlined text-[13px]">add</span>
1337
+ {m.id}
1338
+ </button>
1339
+ ))}
1340
+ </div>
1341
+ </div>
1342
+ )}
1343
+ </div>
1344
+ );
1345
+ };
1346
+
1347
+ if (loading) {
1348
+ return (
1349
+ <div className="flex flex-col gap-8">
1350
+ <CardSkeleton />
1351
+ <CardSkeleton />
1352
+ </div>
1353
+ );
1354
+ }
1355
+
1356
+ if (!providerInfo) {
1357
+ return (
1358
+ <div className="text-center py-20">
1359
+ <p className="text-text-muted">Provider not found</p>
1360
+ <Link href="/dashboard/providers" className="text-primary mt-4 inline-block">
1361
+ Back to Providers
1362
+ </Link>
1363
+ </div>
1364
+ );
1365
+ }
1366
+
1367
+ // Determine icon path: OpenAI Compatible providers use specialized icons
1368
+ const getHeaderIconPath = () => {
1369
+ if (isOpenAICompatible && providerInfo.apiType) {
1370
+ return providerInfo.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png";
1371
+ }
1372
+ if (isAnthropicCompatible) {
1373
+ return "/providers/anthropic-m.png";
1374
+ }
1375
+ return `/providers/${providerInfo.id}.png`;
1376
+ };
1377
+
1378
+ return (
1379
+ <div className="flex min-w-0 flex-col gap-6 px-1 sm:gap-8 sm:px-0">
1380
+ {/* Header */}
1381
+ <div className="min-w-0">
1382
+ <Link
1383
+ href="/dashboard/providers"
1384
+ className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary transition-colors mb-4"
1385
+ >
1386
+ <span className="material-symbols-outlined text-lg">arrow_back</span>
1387
+ Back to Providers
1388
+ </Link>
1389
+ <div className="flex min-w-0 items-center gap-3 sm:gap-4">
1390
+ <div
1391
+ className="flex size-12 shrink-0 items-center justify-center rounded-lg"
1392
+ style={{ backgroundColor: `${providerInfo.color}15` }}
1393
+ >
1394
+ {headerImgError ? (
1395
+ <span className="text-sm font-bold" style={{ color: providerInfo.color }}>
1396
+ {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()}
1397
+ </span>
1398
+ ) : (
1399
+ <Image
1400
+ src={getHeaderIconPath()}
1401
+ alt={providerInfo.name}
1402
+ width={48}
1403
+ height={48}
1404
+ className="max-h-12 max-w-12 rounded-lg object-contain"
1405
+ sizes="48px"
1406
+ onError={() => setHeaderImgError(true)}
1407
+ />
1408
+ )}
1409
+ </div>
1410
+ <div className="min-w-0">
1411
+ <div className="flex items-center gap-3 flex-wrap">
1412
+ <h1 className="truncate text-2xl font-semibold tracking-tight sm:text-3xl">{providerInfo.name}</h1>
1413
+ {(providerInfo.notice?.apiKeyUrl || providerInfo.notice?.signupUrl || providerInfo.website) && (
1414
+ <a
1415
+ href={providerInfo.notice?.apiKeyUrl || providerInfo.notice?.signupUrl || providerInfo.website}
1416
+ target="_blank"
1417
+ rel="noopener noreferrer"
1418
+ className="text-xs text-primary hover:underline inline-flex items-center gap-1"
1419
+ >
1420
+ <span className="material-symbols-outlined text-sm">open_in_new</span>
1421
+ {providerInfo.notice?.apiKeyUrl ? "Get API Key" : "Sign up / Learn more"}
1422
+ </a>
1423
+ )}
1424
+ </div>
1425
+ <p className="text-text-muted">
1426
+ {connections.length} connection{connections.length === 1 ? "" : "s"}
1427
+ </p>
1428
+ </div>
1429
+ </div>
1430
+ </div>
1431
+
1432
+ {providerInfo.deprecated && (
1433
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
1434
+ <span className="material-symbols-outlined text-[16px] text-yellow-500 mt-0.5 shrink-0">warning</span>
1435
+ <p className="text-xs text-red-600 dark:text-yellow-400 leading-relaxed">{providerInfo.deprecationNotice}</p>
1436
+ </div>
1437
+ )}
1438
+
1439
+ {providerInfo.notice?.text && !providerInfo.deprecated && (
1440
+ <div className="flex flex-col gap-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-2 sm:flex-row sm:items-center">
1441
+ <span className="material-symbols-outlined text-[16px] text-blue-500 shrink-0">info</span>
1442
+ <p className="min-w-0 flex-1 text-xs leading-relaxed text-blue-600 dark:text-blue-400">{providerInfo.notice.text}</p>
1443
+ {providerInfo.notice.apiKeyUrl && (
1444
+ <a
1445
+ href={providerInfo.notice.apiKeyUrl}
1446
+ target="_blank"
1447
+ rel="noopener noreferrer"
1448
+ className="inline-flex justify-center rounded bg-blue-500 px-2 py-1 text-xs font-medium text-white transition-colors hover:bg-blue-600 sm:py-0.5"
1449
+ >
1450
+ Get API Key →
1451
+ </a>
1452
+ )}
1453
+ </div>
1454
+ )}
1455
+
1456
+ {isCompatible && providerNode && (
1457
+ <Card>
1458
+ <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
1459
+ <div className="min-w-0">
1460
+ <h2 className="text-lg font-semibold">{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}</h2>
1461
+ <p className="break-all text-sm text-text-muted">
1462
+ {isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
1463
+ {isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")}
1464
+ </p>
1465
+ </div>
1466
+ <div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
1467
+ <Button
1468
+ size="sm"
1469
+ icon="add"
1470
+ onClick={() => {
1471
+ setAddConnectionError("");
1472
+ setShowAddApiKeyModal(true);
1473
+ }}
1474
+ className="w-full sm:w-auto"
1475
+ >
1476
+ Add API Key
1477
+ </Button>
1478
+ <Button
1479
+ size="sm"
1480
+ variant="secondary"
1481
+ icon="edit"
1482
+ onClick={() => setShowEditNodeModal(true)}
1483
+ className="w-full sm:w-auto"
1484
+ >
1485
+ Edit
1486
+ </Button>
1487
+ <Button
1488
+ size="sm"
1489
+ variant="secondary"
1490
+ icon="delete"
1491
+ onClick={async () => {
1492
+ setConfirmState({
1493
+ title: "Delete Compatible Node",
1494
+ message: `Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`,
1495
+ onConfirm: async () => {
1496
+ setConfirmState(null);
1497
+ try {
1498
+ const res = await fetch(`/api/provider-nodes/${providerId}`, { method: "DELETE" });
1499
+ if (res.ok) {
1500
+ router.push("/dashboard/providers");
1501
+ }
1502
+ } catch (error) {
1503
+ console.log("Error deleting provider node:", error);
1504
+ }
1505
+ }
1506
+ });
1507
+ }}
1508
+ className="w-full sm:w-auto"
1509
+ >
1510
+ Delete
1511
+ </Button>
1512
+ </div>
1513
+ </div>
1514
+ </Card>
1515
+ )}
1516
+
1517
+ {/* Connections */}
1518
+ {isFreeNoAuth ? (
1519
+ <NoAuthProxyCard providerId={providerId} />
1520
+ ) : (
1521
+ <Card>
1522
+ <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1523
+ <h2 className="text-lg font-semibold">Connections</h2>
1524
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
1525
+ {connections.length > 0 && proxyPools.length > 0 && (
1526
+ <Button
1527
+ size="sm"
1528
+ variant="secondary"
1529
+ icon="lan"
1530
+ onClick={openBulkProxyModal}
1531
+ disabled={selectedConnections.length === 0}
1532
+ >
1533
+ Apply Proxy
1534
+ </Button>
1535
+ )}
1536
+ {connections.length > 0 && (
1537
+ <>
1538
+ <Button
1539
+ size="sm"
1540
+ variant="secondary"
1541
+ icon="sync"
1542
+ onClick={handleRunOneByOneTest}
1543
+ disabled={oneByOneRunning}
1544
+ >
1545
+ {oneByOneRunning ? "Testing Connection One-by-One..." : "Test Connection One-by-One"}
1546
+ </Button>
1547
+ {oneByOneRunning && (
1548
+ <Button
1549
+ size="sm"
1550
+ variant="ghost"
1551
+ icon="stop"
1552
+ onClick={handleStopOneByOneTest}
1553
+ disabled={oneByOneStopping}
1554
+ >
1555
+ {oneByOneStopping ? "Stopping..." : "Stop"}
1556
+ </Button>
1557
+ )}
1558
+ </>
1559
+ )}
1560
+ {/* Thinking config */}
1561
+ {/* {thinkingConfig && (
1562
+ <div className="flex items-center gap-2">
1563
+ <span className="text-xs text-text-muted font-medium">Thinking</span>
1564
+ <select
1565
+ value={thinkingMode}
1566
+ onChange={(e) => handleThinkingModeChange(e.target.value)}
1567
+ className="text-xs px-2 py-1 border border-border rounded-md bg-background focus:outline-none focus:border-primary"
1568
+ >
1569
+ {thinkingConfig.options.map((opt) => (
1570
+ <option key={opt} value={opt}>{opt.charAt(0).toUpperCase() + opt.slice(1)}</option>
1571
+ ))}
1572
+ </select>
1573
+ </div>
1574
+ )} */}
1575
+ {/* Round Robin toggle */}
1576
+ <div className="flex flex-wrap items-center gap-2">
1577
+ <span className="text-xs text-text-muted font-medium">Round Robin</span>
1578
+ <Toggle
1579
+ checked={providerStrategy === "round-robin"}
1580
+ onChange={handleRoundRobinToggle}
1581
+ />
1582
+ {providerStrategy === "round-robin" && (
1583
+ <div className="flex items-center gap-1.5">
1584
+ <span className="text-xs text-text-muted">Sticky:</span>
1585
+ <input
1586
+ type="number"
1587
+ min={1}
1588
+ value={providerStickyLimit}
1589
+ onChange={(e) => handleStickyLimitChange(e.target.value)}
1590
+ placeholder="1"
1591
+ className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary"
1592
+ />
1593
+ </div>
1594
+ )}
1595
+ </div>
1596
+ </div>
1597
+ </div>
1598
+
1599
+ {connections.length > 0 && (
1600
+ <div className="mb-4 flex flex-col gap-3 rounded-lg border border-black/5 bg-black/[0.02] p-3 dark:border-white/5 dark:bg-white/[0.02]">
1601
+ <div className="flex flex-wrap items-center gap-2">
1602
+ <label className="flex items-center gap-1.5 text-xs text-text-muted">
1603
+ <input
1604
+ type="checkbox"
1605
+ checked={allSelected}
1606
+ onChange={toggleSelectAllConnections}
1607
+ className="size-4 rounded border-black/20 dark:border-white/20"
1608
+ />
1609
+ {allSelected ? "Unselect visible" : "Select visible"}
1610
+ </label>
1611
+ <Badge variant="default">{filteredConnections.length} visible</Badge>
1612
+ {selectedConnections.length > 0 && (
1613
+ <Badge variant="default">{selectedConnections.length} selected</Badge>
1614
+ )}
1615
+ {selectedProxySummary && (
1616
+ <span className="text-xs text-text-muted">{selectedProxySummary}</span>
1617
+ )}
1618
+ {selectedConnections.length > 0 && (
1619
+ <Button size="sm" variant="ghost" onClick={clearSelection}>
1620
+ Clear
1621
+ </Button>
1622
+ )}
1623
+ {terminalConnections.length > 0 && (
1624
+ <Button
1625
+ size="sm"
1626
+ variant="secondary"
1627
+ icon="delete"
1628
+ onClick={handleDeleteTerminalConnections}
1629
+ >
1630
+ Delete Terminal ({terminalConnections.length})
1631
+ </Button>
1632
+ )}
1633
+ </div>
1634
+ <div className="flex flex-wrap gap-1.5">
1635
+ {CONNECTION_STATUS_FILTERS.map((filter) => (
1636
+ <button
1637
+ key={filter.id}
1638
+ type="button"
1639
+ onClick={() => setConnectionStatusFilter(filter.id)}
1640
+ className={`rounded-full border px-2.5 py-1 text-xs transition-colors ${
1641
+ connectionStatusFilter === filter.id
1642
+ ? "border-primary/50 bg-primary/10 text-primary"
1643
+ : "border-border text-text-muted hover:border-primary/30 hover:text-text-main"
1644
+ }`}
1645
+ >
1646
+ {filter.label}
1647
+ <span className="ml-1 text-[10px] opacity-70">{connectionStatusCounts[filter.id] || 0}</span>
1648
+ </button>
1649
+ ))}
1650
+ </div>
1651
+ </div>
1652
+ )}
1653
+
1654
+ {connections.length === 0 ? (
1655
+ <>
1656
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1657
+ <div className="flex items-center gap-3">
1658
+ <div className="inline-flex items-center justify-center w-9 h-9 rounded-full bg-primary/10 text-primary shrink-0">
1659
+ <span className="material-symbols-outlined text-[18px]">{isOAuth ? "lock" : "key"}</span>
1660
+ </div>
1661
+ <div className="min-w-0">
1662
+ <p className="text-sm text-text-muted">No connections yet</p>
1663
+ {hasDualAuthModes && (
1664
+ <p className="text-xs text-text-muted">
1665
+ Choose {oauthConnectionLabel} or {apiKeyConnectionLabel}.
1666
+ </p>
1667
+ )}
1668
+ </div>
1669
+ </div>
1670
+ <div className="flex gap-2">
1671
+ {hasDualAuthModes ? (
1672
+ <>
1673
+ <Button size="sm" icon="lock" variant="secondary" onClick={triggerOAuthConnection}>
1674
+ {oauthConnectionLabel}
1675
+ </Button>
1676
+ <Button size="sm" icon="key" onClick={triggerApiKeyConnection}>
1677
+ {apiKeyConnectionLabel}
1678
+ </Button>
1679
+ </>
1680
+ ) : (
1681
+ <>
1682
+ {!isCompatible && providerId === "iflow" && (
1683
+ <Button size="sm" icon="cookie" variant="secondary" onClick={() => setShowIFlowCookieModal(true)}>
1684
+ Cookie
1685
+ </Button>
1686
+ )}
1687
+ <Button
1688
+ size="sm"
1689
+ icon={usesAutomationLogin ? "automation" : "add"}
1690
+ onClick={triggerAddConnection}
1691
+ >
1692
+ {usesAutomationLogin ? "Open Automation" : (isCompatible ? "Add API Key" : (providerId === "iflow" ? "OAuth" : "Add Connection"))}
1693
+ </Button>
1694
+ </>
1695
+ )}
1696
+ </div>
1697
+ </div>
1698
+ {providerId === "kiro" && kiroBulkJob?.jobId && (
1699
+ <div className="mt-4 flex flex-col gap-3 rounded-lg border border-primary/20 bg-primary/[0.06] px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
1700
+ <div>
1701
+ <p className="text-sm font-semibold text-text-main">Bulk import progress is still available</p>
1702
+ <p className="text-xs text-text-muted">
1703
+ Status: {kiroBulkJob.status} | Success: {kiroBulkJob.summary?.success || 0} | Running: {kiroBulkJob.summary?.running || 0} | Queued: {kiroBulkJob.summary?.queued || 0}
1704
+ </p>
1705
+ </div>
1706
+ <div className="flex gap-2">
1707
+ <Button
1708
+ size="sm"
1709
+ icon="monitoring"
1710
+ variant="secondary"
1711
+ onClick={() => router.push("/dashboard/automation?provider=kiro")}
1712
+ >
1713
+ Resume Bulk Progress
1714
+ </Button>
1715
+ {isBulkJobTerminal(kiroBulkJob.status) && (
1716
+ <Button
1717
+ size="sm"
1718
+ variant="ghost"
1719
+ icon="close"
1720
+ onClick={clearKiroBulkProgress}
1721
+ >
1722
+ Clear
1723
+ </Button>
1724
+ )}
1725
+ </div>
1726
+ </div>
1727
+ )}
1728
+ {providerId === "kiro" && !kiroBulkJob?.jobId && kiroBulkNotice && (
1729
+ <div className="mt-4 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">
1730
+ {kiroBulkNotice}
1731
+ </div>
1732
+ )}
1733
+ </>
1734
+ ) : (
1735
+ <>
1736
+ {oneByOneSummary && (
1737
+ <div className="mb-4 rounded-lg border border-black/10 bg-black/[0.02] px-3 py-2 text-xs text-text-muted dark:border-white/10 dark:bg-white/[0.03]">
1738
+ <div className="flex flex-wrap items-center gap-3">
1739
+ <span>Total: {oneByOneSummary.total}</span>
1740
+ <span>Completed: {oneByOneSummary.completed}</span>
1741
+ <span>Passed: {oneByOneSummary.passed}</span>
1742
+ <span>Failed: {oneByOneSummary.failed}</span>
1743
+ {oneByOneSummary.stopped && (
1744
+ <span className="text-amber-600 dark:text-amber-400">Stopped</span>
1745
+ )}
1746
+ {oneByOneRunning && oneByOneCurrentConnectionId && (
1747
+ <span>Running: {connections.find((conn) => conn.id === oneByOneCurrentConnectionId)?.name || oneByOneCurrentConnectionId}</span>
1748
+ )}
1749
+ </div>
1750
+ </div>
1751
+ )}
1752
+ {providerId === "kiro" && kiroBulkJob?.jobId && (
1753
+ <div className="mb-4 flex flex-col gap-3 rounded-lg border border-primary/20 bg-primary/[0.06] px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
1754
+ <div>
1755
+ <p className="text-sm font-semibold text-text-main">Bulk import progress is still available</p>
1756
+ <p className="text-xs text-text-muted">
1757
+ Status: {kiroBulkJob.status} | Success: {kiroBulkJob.summary?.success || 0} | Running: {kiroBulkJob.summary?.running || 0} | Queued: {kiroBulkJob.summary?.queued || 0}
1758
+ </p>
1759
+ </div>
1760
+ <div className="flex gap-2">
1761
+ <Button
1762
+ size="sm"
1763
+ icon="monitoring"
1764
+ variant="secondary"
1765
+ onClick={() => router.push("/dashboard/automation?provider=kiro")}
1766
+ >
1767
+ Resume Bulk Progress
1768
+ </Button>
1769
+ {isBulkJobTerminal(kiroBulkJob.status) && (
1770
+ <Button
1771
+ size="sm"
1772
+ variant="ghost"
1773
+ icon="close"
1774
+ onClick={clearKiroBulkProgress}
1775
+ >
1776
+ Clear
1777
+ </Button>
1778
+ )}
1779
+ </div>
1780
+ </div>
1781
+ )}
1782
+ {providerId === "kiro" && !kiroBulkJob?.jobId && kiroBulkNotice && (
1783
+ <div className="mb-4 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">
1784
+ {kiroBulkNotice}
1785
+ </div>
1786
+ )}
1787
+ {connectionsList}
1788
+ {providerId === "kiro" && totalKiroPages > 1 && (
1789
+ <div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1790
+ <p className="text-sm text-text-muted">
1791
+ Page {kiroConnectionsPage} of {totalKiroPages} • Showing {(kiroConnectionsPage - 1) * KIRO_CONNECTIONS_PER_PAGE + 1}-
1792
+ {Math.min(kiroConnectionsPage * KIRO_CONNECTIONS_PER_PAGE, filteredConnections.length)} of {filteredConnections.length} connections
1793
+ </p>
1794
+ <div className="flex gap-2">
1795
+ <Button
1796
+ size="sm"
1797
+ variant="secondary"
1798
+ disabled={activeKiroConnectionsPage <= 1}
1799
+ onClick={() => setKiroConnectionsPage((prev) => Math.max(1, prev - 1))}
1800
+ >
1801
+ Previous
1802
+ </Button>
1803
+ <Button
1804
+ size="sm"
1805
+ variant="secondary"
1806
+ disabled={activeKiroConnectionsPage >= totalKiroPages}
1807
+ onClick={() => setKiroConnectionsPage((prev) => Math.min(totalKiroPages, prev + 1))}
1808
+ >
1809
+ Next
1810
+ </Button>
1811
+ </div>
1812
+ </div>
1813
+ )}
1814
+ {providerId !== "kiro" && filteredConnections.length > 0 && (
1815
+ <Pagination
1816
+ currentPage={activeConnectionsPage}
1817
+ pageSize={connectionsPageSize}
1818
+ totalItems={filteredConnections.length}
1819
+ onPageChange={setConnectionsPage}
1820
+ onPageSizeChange={(size) => {
1821
+ setConnectionsPageSize(size);
1822
+ setConnectionsPage(1);
1823
+ }}
1824
+ className="mt-2"
1825
+ />
1826
+ )}
1827
+ {connections.length > 0 && filteredConnections.length === 0 && (
1828
+ <div className="py-8 text-center text-sm text-text-muted">
1829
+ No connections match this status filter.
1830
+ </div>
1831
+ )}
1832
+ {!isCompatible && (
1833
+ <div className="mt-4 grid grid-cols-1 gap-2 sm:flex">
1834
+ {providerId === "iflow" && (
1835
+ <Button
1836
+ size="sm"
1837
+ icon="cookie"
1838
+ variant="secondary"
1839
+ onClick={() => setShowIFlowCookieModal(true)}
1840
+ title="Add connection using browser cookie"
1841
+ className="w-full sm:w-auto"
1842
+ >
1843
+ Cookie
1844
+ </Button>
1845
+ )}
1846
+ {providerId === "codebuddy" && (
1847
+ <Button
1848
+ size="sm"
1849
+ icon="cookie"
1850
+ variant="secondary"
1851
+ onClick={openCodeBuddyQuotaCookieModal}
1852
+ title="Attach CodeBuddy web cookie for quota tracking"
1853
+ className="w-full sm:w-auto"
1854
+ >
1855
+ Quota Cookie
1856
+ </Button>
1857
+ )}
1858
+ {hasDualAuthModes ? (
1859
+ <>
1860
+ <Button
1861
+ size="sm"
1862
+ icon="lock"
1863
+ variant="secondary"
1864
+ onClick={triggerOAuthConnection}
1865
+ className="w-full sm:w-auto"
1866
+ >
1867
+ {oauthConnectionLabel}
1868
+ </Button>
1869
+ <Button
1870
+ size="sm"
1871
+ icon="key"
1872
+ onClick={triggerApiKeyConnection}
1873
+ className="w-full sm:w-auto"
1874
+ >
1875
+ {apiKeyConnectionLabel}
1876
+ </Button>
1877
+ </>
1878
+ ) : (
1879
+ <Button
1880
+ size="sm"
1881
+ icon={usesAutomationLogin ? "automation" : "add"}
1882
+ onClick={triggerAddConnection}
1883
+ className="w-full sm:w-auto"
1884
+ >
1885
+ {usesAutomationLogin ? "Open Automation" : "Add"}
1886
+ </Button>
1887
+ )}
1888
+ </div>
1889
+ )}
1890
+ </>
1891
+ )}
1892
+ </Card>
1893
+ )}
1894
+
1895
+ {/* Models */}
1896
+ <Card>
1897
+ <div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
1898
+ <h2 className="text-lg font-semibold">
1899
+ {"Available Models"}
1900
+ </h2>
1901
+ {!isCompatible && (() => {
1902
+ const allIds = [
1903
+ ...models,
1904
+ ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
1905
+ ].filter((m) => !m.type || m.type === "llm").map((m) => m.id);
1906
+ const activeIds = allIds.filter((id) => !disabledModelIds.includes(id));
1907
+ return (
1908
+ <div className="flex gap-2">
1909
+ {disabledModelIds.length > 0 && (
1910
+ <Button size="sm" variant="secondary" icon="restart_alt" onClick={handleEnableAll}>
1911
+ Active All
1912
+ </Button>
1913
+ )}
1914
+ {activeIds.length > 0 && (
1915
+ <Button size="sm" variant="secondary" icon="block" onClick={() => handleDisableAll(activeIds)}>
1916
+ Disable All
1917
+ </Button>
1918
+ )}
1919
+ </div>
1920
+ );
1921
+ })()}
1922
+ </div>
1923
+ {!!modelsTestError && (
1924
+ <p className="text-xs text-red-500 mb-3 break-words">{modelsTestError}</p>
1925
+ )}
1926
+ {renderModelsSection()}
1927
+ </Card>
1928
+
1929
+ {bulkActionModal}
1930
+
1931
+ {/* Modals */}
1932
+ {providerId === "kiro" ? (
1933
+ <KiroOAuthWrapper
1934
+ isOpen={showOAuthModal}
1935
+ providerInfo={providerInfo}
1936
+ onSuccess={handleOAuthSuccess}
1937
+ onRefresh={fetchConnections}
1938
+ initialBulkJobId={kiroBulkJob?.jobId || null}
1939
+ onBulkJobChange={handleKiroBulkJobChange}
1940
+ onClose={() => setShowOAuthModal(false)}
1941
+ />
1942
+ ) : providerId === "cursor" ? (
1943
+ <CursorAuthModal
1944
+ isOpen={showOAuthModal}
1945
+ onSuccess={handleOAuthSuccess}
1946
+ onClose={() => setShowOAuthModal(false)}
1947
+ />
1948
+ ) : providerId === "gitlab" ? (
1949
+ <GitLabAuthModal
1950
+ isOpen={showOAuthModal}
1951
+ providerInfo={providerInfo}
1952
+ onSuccess={handleOAuthSuccess}
1953
+ onClose={() => setShowOAuthModal(false)}
1954
+ />
1955
+ ) : (
1956
+ <OAuthModal
1957
+ isOpen={showOAuthModal}
1958
+ provider={providerId}
1959
+ providerInfo={providerInfo}
1960
+ onSuccess={handleOAuthSuccess}
1961
+ onClose={() => setShowOAuthModal(false)}
1962
+ />
1963
+ )}
1964
+ {providerId === "iflow" && (
1965
+ <IFlowCookieModal
1966
+ isOpen={showIFlowCookieModal}
1967
+ onSuccess={handleIFlowCookieSuccess}
1968
+ onClose={() => setShowIFlowCookieModal(false)}
1969
+ />
1970
+ )}
1971
+ {providerId === "codebuddy" && (
1972
+ <CodeBuddyQuotaCookieModal
1973
+ isOpen={showCodeBuddyQuotaCookieModal}
1974
+ connectionIds={codeBuddyQuotaCookieConnectionIds}
1975
+ onSuccess={handleCodeBuddyQuotaCookieSuccess}
1976
+ onClose={() => setShowCodeBuddyQuotaCookieModal(false)}
1977
+ />
1978
+ )}
1979
+ <AddApiKeyModal
1980
+ isOpen={showAddApiKeyModal}
1981
+ provider={providerId}
1982
+ providerName={providerInfo.name}
1983
+ isCompatible={isCompatible}
1984
+ isAnthropic={isAnthropicCompatible}
1985
+ authType={providerInfo?.authType}
1986
+ authHint={providerInfo?.authHint}
1987
+ website={providerInfo?.website}
1988
+ proxyPools={proxyPools}
1989
+ error={addConnectionError}
1990
+ onSave={handleSaveApiKey}
1991
+ onBulkDone={fetchConnections}
1992
+ onClose={() => {
1993
+ setAddConnectionError("");
1994
+ setShowAddApiKeyModal(false);
1995
+ }}
1996
+ />
1997
+ <EditConnectionModal
1998
+ isOpen={showEditModal}
1999
+ connection={selectedConnection}
2000
+ proxyPools={proxyPools}
2001
+ onSave={handleUpdateConnection}
2002
+ onClose={() => setShowEditModal(false)}
2003
+ />
2004
+ {isCompatible && (
2005
+ <EditCompatibleNodeModal
2006
+ isOpen={showEditNodeModal}
2007
+ node={providerNode}
2008
+ onSave={handleUpdateNode}
2009
+ onClose={() => setShowEditNodeModal(false)}
2010
+ isAnthropic={isAnthropicCompatible}
2011
+ />
2012
+ )}
2013
+ {!isCompatible && (
2014
+ <AddCustomModelModal
2015
+ isOpen={showAddCustomModel}
2016
+ providerAlias={providerStorageAlias}
2017
+ providerDisplayAlias={providerDisplayAlias}
2018
+ onSave={async (modelId) => {
2019
+ // For passthrough providers (OpenRouter), use last segment as alias to avoid slash conflicts
2020
+ const alias = providerInfo?.passthroughModels
2021
+ ? modelId.split("/").pop()
2022
+ : modelId;
2023
+ await handleSetAlias(modelId, alias, providerStorageAlias);
2024
+ setShowAddCustomModel(false);
2025
+ }}
2026
+ onClose={() => setShowAddCustomModel(false)}
2027
+ />
2028
+ )}
2029
+
2030
+ {/* AG Risk Confirmation Modal */}
2031
+ <ConfirmModal
2032
+ isOpen={showAgRiskModal}
2033
+ onClose={() => setShowAgRiskModal(false)}
2034
+ onConfirm={handleAgRiskConfirm}
2035
+ title="Risk Notice"
2036
+ message={providerInfo?.deprecationNotice}
2037
+ confirmText="I Understand, Continue"
2038
+ cancelText="Cancel"
2039
+ variant="danger"
2040
+ />
2041
+
2042
+ {/* Confirm Modal */}
2043
+ <ConfirmModal
2044
+ isOpen={!!confirmState}
2045
+ onClose={() => setConfirmState(null)}
2046
+ onConfirm={confirmState?.onConfirm}
2047
+ title={confirmState?.title || "Confirm"}
2048
+ message={confirmState?.message}
2049
+ variant="danger"
2050
+ />
2051
+ </div>
2052
+ );
2053
+ }