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,1394 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
4
+ import ProviderIcon from "@/shared/components/ProviderIcon";
5
+ import QuotaTable from "./QuotaTable";
6
+ import Toggle from "@/shared/components/Toggle";
7
+ import { parseQuotaData, calculatePercentage } from "./utils";
8
+ import Card from "@/shared/components/Card";
9
+ import { EditConnectionModal } from "@/shared/components";
10
+ import {
11
+ AI_PROVIDERS,
12
+ USAGE_SUPPORTED_PROVIDERS,
13
+ } from "@/shared/constants/providers";
14
+
15
+ function getConnectionLabel(connection) {
16
+ const isEmail = (value) =>
17
+ typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
18
+ if (isEmail(connection.email)) return connection.email;
19
+ if (isEmail(connection.name)) return connection.name;
20
+ return connection.name;
21
+ }
22
+
23
+ function getConnectionQuotaRemaining(connection, quotaData) {
24
+ const quota = quotaData[connection.id]?.quotas?.[0];
25
+ if (!quota) return Number.POSITIVE_INFINITY;
26
+ if (typeof quota.remaining === "number") return quota.remaining;
27
+ return Number.POSITIVE_INFINITY;
28
+ }
29
+
30
+ function mergeQuotaRows(rows = []) {
31
+ const merged = new Map();
32
+
33
+ rows.forEach((quota) => {
34
+ if (!quota?.name) return;
35
+
36
+ const current = merged.get(quota.name) || {
37
+ name: quota.name,
38
+ used: 0,
39
+ total: 0,
40
+ resetAt: quota.resetAt || null,
41
+ };
42
+
43
+ current.used += Number(quota.used) || 0;
44
+ current.total += Number(quota.total) || 0;
45
+
46
+ if (quota.resetAt) {
47
+ if (!current.resetAt || new Date(quota.resetAt).getTime() < new Date(current.resetAt).getTime()) {
48
+ current.resetAt = quota.resetAt;
49
+ }
50
+ }
51
+
52
+ merged.set(quota.name, current);
53
+ });
54
+
55
+ return [...merged.values()];
56
+ }
57
+
58
+ function getProviderDisplayName(provider) {
59
+ return AI_PROVIDERS[provider]?.name || provider;
60
+ }
61
+
62
+ function buildProviderAggregateCard(provider, providerConnections, quotaData, loading, errors) {
63
+ if (!providerConnections.length) return null;
64
+ const allQuotas = [];
65
+ const errorMessages = [];
66
+ let message = null;
67
+ let isLoading = false;
68
+
69
+ providerConnections.forEach((connection) => {
70
+ const quotaEntry = quotaData[connection.id];
71
+ if (Array.isArray(quotaEntry?.quotas)) {
72
+ allQuotas.push(...quotaEntry.quotas);
73
+ }
74
+ if (quotaEntry?.message && !message) {
75
+ message = quotaEntry.message;
76
+ }
77
+ if (errors[connection.id]) {
78
+ errorMessages.push(errors[connection.id]);
79
+ }
80
+ if (loading[connection.id]) {
81
+ isLoading = true;
82
+ }
83
+ });
84
+
85
+ return {
86
+ id: `${provider}-aggregate`,
87
+ provider,
88
+ name: getProviderDisplayName(provider),
89
+ email: `${providerConnections.length} accounts`,
90
+ isActive: providerConnections.some((connection) => connection.isActive ?? true),
91
+ isAggregate: true,
92
+ quota: {
93
+ quotas: mergeQuotaRows(allQuotas),
94
+ message: allQuotas.length === 0 ? (message || null) : null,
95
+ },
96
+ isLoading,
97
+ error: !allQuotas.length && errorMessages.length > 0 ? errorMessages[0] : null,
98
+ connectionIds: providerConnections.map((connection) => connection.id),
99
+ };
100
+ }
101
+
102
+ function buildProviderAggregateCards(connections, quotaData, loading, errors) {
103
+ const groups = new Map();
104
+
105
+ connections.forEach((connection) => {
106
+ if (!connection.provider) return;
107
+ if (!groups.has(connection.provider)) {
108
+ groups.set(connection.provider, []);
109
+ }
110
+ groups.get(connection.provider).push(connection);
111
+ });
112
+
113
+ return [...groups.entries()]
114
+ .map(([provider, providerConnections]) =>
115
+ buildProviderAggregateCard(provider, providerConnections, quotaData, loading, errors),
116
+ )
117
+ .filter(Boolean);
118
+ }
119
+
120
+ function sortVisibleConnections(
121
+ connections,
122
+ quotaData,
123
+ expiringFirst,
124
+ providerFilter,
125
+ quotaSortMode,
126
+ ) {
127
+ if (providerFilter === "codex" && quotaSortMode !== "default") {
128
+ return [...connections].sort((a, b) => {
129
+ const remainingA = getConnectionQuotaRemaining(a, quotaData);
130
+ const remainingB = getConnectionQuotaRemaining(b, quotaData);
131
+ const remainingDiff =
132
+ quotaSortMode === "remaining-asc"
133
+ ? remainingA - remainingB
134
+ : remainingB - remainingA;
135
+
136
+ if (remainingDiff !== 0) return remainingDiff;
137
+ return (getConnectionLabel(a) || "").localeCompare(
138
+ getConnectionLabel(b) || "",
139
+ );
140
+ });
141
+ }
142
+
143
+ if (!expiringFirst) return connections;
144
+
145
+ const getEarliestResetTime = (connection) => {
146
+ const resetTimes = (quotaData[connection.id]?.quotas || [])
147
+ .map((quota) =>
148
+ quota.resetAt
149
+ ? new Date(quota.resetAt).getTime()
150
+ : Number.POSITIVE_INFINITY,
151
+ )
152
+ .filter((time) => Number.isFinite(time));
153
+ return resetTimes.length > 0
154
+ ? Math.min(...resetTimes)
155
+ : Number.POSITIVE_INFINITY;
156
+ };
157
+
158
+ return [...connections].sort((a, b) => {
159
+ const expiryDiff = getEarliestResetTime(a) - getEarliestResetTime(b);
160
+ if (expiryDiff !== 0) return expiryDiff;
161
+ return (
162
+ (a.provider || "").localeCompare(b.provider || "") ||
163
+ (getConnectionLabel(a) || "").localeCompare(getConnectionLabel(b) || "")
164
+ );
165
+ });
166
+ }
167
+
168
+ function buildLoadingState(connections) {
169
+ const nextLoadingState = {};
170
+ connections.forEach((connection) => {
171
+ nextLoadingState[connection.id] = true;
172
+ });
173
+ return nextLoadingState;
174
+ }
175
+
176
+ function filterQuotaStateByConnections(state, connections) {
177
+ const visibleIds = new Set(connections.map((connection) => connection.id));
178
+ return Object.fromEntries(
179
+ Object.entries(state).filter(([id]) => visibleIds.has(id)),
180
+ );
181
+ }
182
+
183
+ function getConnectionsPageRange(pagination) {
184
+ if (!pagination.total) {
185
+ return { start: 0, end: 0 };
186
+ }
187
+
188
+ const start = (pagination.page - 1) * pagination.pageSize + 1;
189
+ const end = Math.min(pagination.page * pagination.pageSize, pagination.total);
190
+ return { start, end };
191
+ }
192
+
193
+ function getConnectionsEmptyMessage(totals, providerFilter, accountFilter) {
194
+ if (!totals.eligibleConnections) {
195
+ return {
196
+ icon: "cloud_off",
197
+ title: "No Providers Connected",
198
+ description:
199
+ "Connect to providers with OAuth to track your API quota limits and usage.",
200
+ };
201
+ }
202
+
203
+ if (!totals.providerFilteredConnections) {
204
+ return {
205
+ icon: "filter_alt_off",
206
+ title: "No Accounts Match Current Filters",
207
+ description:
208
+ providerFilter === "all"
209
+ ? "Try changing the account status filter to see more quota trackers."
210
+ : `No ${accountFilter === "inactive" ? "turned off" : accountFilter === "active" ? "active" : "matching"} accounts found for ${providerFilter}.`,
211
+ };
212
+ }
213
+
214
+ return {
215
+ icon: "filter_alt_off",
216
+ title: "No Accounts On This Page",
217
+ description:
218
+ "Try moving to another page or refreshing the current filters.",
219
+ };
220
+ }
221
+
222
+ function sortRequestFromExpiringFirst(expiringFirst) {
223
+ return expiringFirst ? "expiring" : "priority";
224
+ }
225
+
226
+ function getPageSizeLabel(pageSize, isCustomPageSize) {
227
+ return isCustomPageSize ? `Custom: ${pageSize} / page` : `${pageSize} / page`;
228
+ }
229
+
230
+ function getConnectionsPaginationSummary(pagination) {
231
+ const { start, end } = getConnectionsPageRange(pagination);
232
+ return `Showing ${start}-${end} of ${pagination.total}`;
233
+ }
234
+
235
+ function getSafePagination(pagination, fallbackPageSize) {
236
+ return (
237
+ pagination || {
238
+ page: 1,
239
+ pageSize: fallbackPageSize,
240
+ total: 0,
241
+ totalPages: 1,
242
+ }
243
+ );
244
+ }
245
+
246
+ function getSafeTotals(totals, fallbackTotal = 0) {
247
+ return (
248
+ totals || {
249
+ eligibleConnections: fallbackTotal,
250
+ providerFilteredConnections: fallbackTotal,
251
+ }
252
+ );
253
+ }
254
+
255
+ function shouldResetPage(previousValue, nextValue) {
256
+ return previousValue !== nextValue;
257
+ }
258
+
259
+ function getPaginationPageValue(dataPagination, fallbackPage) {
260
+ return dataPagination?.page || fallbackPage;
261
+ }
262
+
263
+ function getProviderOptions(dataProviderOptions) {
264
+ return dataProviderOptions || [];
265
+ }
266
+
267
+ async function reconcileConnectionsPage(fetchConnections, targetPage) {
268
+ const nextConnections = await fetchConnections(targetPage);
269
+ return nextConnections;
270
+ }
271
+
272
+ const QUOTA_CACHE_KEY = "quotaCacheData";
273
+
274
+ function getQuotaCache() {
275
+ if (typeof window === "undefined") return {};
276
+ try {
277
+ const cached = window.localStorage.getItem(QUOTA_CACHE_KEY);
278
+ return cached ? JSON.parse(cached) : {};
279
+ } catch (error) {
280
+ console.error("Error reading quota cache:", error);
281
+ return {};
282
+ }
283
+ }
284
+
285
+ function setQuotaCache(connectionId, quotaEntry) {
286
+ if (typeof window === "undefined") return;
287
+ try {
288
+ const cache = getQuotaCache();
289
+ cache[connectionId] = {
290
+ ...quotaEntry,
291
+ cachedAt: new Date().toISOString(),
292
+ };
293
+ window.localStorage.setItem(QUOTA_CACHE_KEY, JSON.stringify(cache));
294
+ } catch (error) {
295
+ console.error("Error writing quota cache:", error);
296
+ }
297
+ }
298
+
299
+ const REFRESH_INTERVAL_MS = 60000; // 60 seconds
300
+ const DEPLETED_QUOTA_THRESHOLD = 5; // percent
301
+ const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh";
302
+ const ACCOUNT_FILTER_OPTIONS = [
303
+ { value: "all", label: "All accounts" },
304
+ { value: "active", label: "Active" },
305
+ { value: "inactive", label: "Turned off" },
306
+ ];
307
+ const QUOTA_DISPLAY_MODE_OPTIONS = [
308
+ { value: "single", label: "Satuan", icon: "account_circle" },
309
+ { value: "bulk", label: "Bulk", icon: "stacks" },
310
+ ];
311
+ const QUOTA_SORT_OPTIONS = [
312
+ { value: "default", label: "Default quota order" },
313
+ { value: "remaining-asc", label: "% quota: low to high" },
314
+ { value: "remaining-desc", label: "% quota: high to low" },
315
+ ];
316
+ const CONNECTIONS_PAGE_SIZE = 20;
317
+ const ACCOUNT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
318
+ const ACCOUNT_PAGE_SIZE_MAX = 500;
319
+
320
+ export default function ProviderLimits() {
321
+ const [connections, setConnections] = useState([]);
322
+ const [quotaData, setQuotaData] = useState({});
323
+ const [loading, setLoading] = useState({});
324
+ const [errors, setErrors] = useState({});
325
+ const [autoRefresh, setAutoRefresh] = useState(true);
326
+ const [lastUpdated, setLastUpdated] = useState(null);
327
+ const [hasHydratedAutoRefresh, setHasHydratedAutoRefresh] = useState(false);
328
+ const [refreshingAll, setRefreshingAll] = useState(false);
329
+ const [countdown, setCountdown] = useState(60);
330
+ const [connectionsLoading, setConnectionsLoading] = useState(true);
331
+ const [deletingId, setDeletingId] = useState(null);
332
+ const [togglingId, setTogglingId] = useState(null);
333
+ const [showEditModal, setShowEditModal] = useState(false);
334
+ const [selectedConnection, setSelectedConnection] = useState(null);
335
+ const [proxyPools, setProxyPools] = useState([]);
336
+ const [providerFilter, setProviderFilter] = useState("all");
337
+ const [providerOptions, setProviderOptions] = useState([]);
338
+ const [displayMode, setDisplayMode] = useState("single");
339
+ const [accountFilter, setAccountFilter] = useState("all");
340
+ const [quotaSortMode, setQuotaSortMode] = useState("default");
341
+ const [expiringFirst, setExpiringFirst] = useState(false);
342
+ const [providerMenuOpen, setProviderMenuOpen] = useState(false);
343
+ const [bulkToggling, setBulkToggling] = useState(false);
344
+ const [page, setPage] = useState(1);
345
+ const [pageSize, setPageSize] = useState(CONNECTIONS_PAGE_SIZE);
346
+ const [customPageSizeInput, setCustomPageSizeInput] = useState(
347
+ String(CONNECTIONS_PAGE_SIZE),
348
+ );
349
+ const [pagination, setPagination] = useState({
350
+ page: 1,
351
+ pageSize: CONNECTIONS_PAGE_SIZE,
352
+ total: 0,
353
+ totalPages: 1,
354
+ });
355
+ const [totals, setTotals] = useState({
356
+ eligibleConnections: 0,
357
+ providerFilteredConnections: 0,
358
+ });
359
+
360
+ const intervalRef = useRef(null);
361
+ const countdownRef = useRef(null);
362
+
363
+ const fetchConnections = useCallback(
364
+ async (targetPage = page) => {
365
+ try {
366
+ const params = new URLSearchParams({
367
+ page: String(targetPage),
368
+ pageSize: String(pageSize),
369
+ accountStatus: accountFilter,
370
+ sort: "priority",
371
+ });
372
+
373
+ if (providerFilter !== "all") {
374
+ params.set("provider", providerFilter);
375
+ }
376
+
377
+ const response = await fetch(
378
+ `/api/providers/client?${params.toString()}`,
379
+ );
380
+ if (!response.ok) throw new Error("Failed to fetch connections");
381
+
382
+ const data = await response.json();
383
+ const connectionList = data.connections || [];
384
+ const nextPagination = getSafePagination(data.pagination, pageSize);
385
+ const nextTotals = getSafeTotals(data.totals, connectionList.length);
386
+
387
+ setConnections(connectionList);
388
+ setProviderOptions(getProviderOptions(data.providerOptions));
389
+ setPagination(nextPagination);
390
+ setTotals(nextTotals);
391
+ setPage(getPaginationPageValue(data.pagination, targetPage));
392
+ return connectionList;
393
+ } catch (error) {
394
+ console.error("Error fetching connections:", error);
395
+ setConnections([]);
396
+ setProviderOptions([]);
397
+ setPagination({ page: 1, pageSize, total: 0, totalPages: 1 });
398
+ setTotals({ eligibleConnections: 0, providerFilteredConnections: 0 });
399
+ return [];
400
+ }
401
+ },
402
+ [accountFilter, page, pageSize, providerFilter],
403
+ );
404
+
405
+ // Fetch quota for a specific connection
406
+ const fetchQuota = useCallback(async (connectionId, provider) => {
407
+ setLoading((prev) => ({ ...prev, [connectionId]: true }));
408
+ setErrors((prev) => ({ ...prev, [connectionId]: null }));
409
+
410
+ try {
411
+ console.log(
412
+ `[ProviderLimits] Fetching quota for ${provider} (${connectionId})`,
413
+ );
414
+ const response = await fetch(`/api/usage/${connectionId}`);
415
+
416
+ if (!response.ok) {
417
+ const errorData = await response.json().catch(() => ({}));
418
+ const errorMsg = errorData.error || response.statusText;
419
+
420
+ // Handle different error types gracefully
421
+ if (response.status === 404) {
422
+ // Connection not found - skip silently
423
+ console.warn(
424
+ `[ProviderLimits] Connection not found for ${provider}, skipping`,
425
+ );
426
+ return;
427
+ }
428
+
429
+ if (response.status === 401) {
430
+ // Auth error - show message instead of throwing
431
+ console.warn(
432
+ `[ProviderLimits] Auth error for ${provider}:`,
433
+ errorMsg,
434
+ );
435
+ const quotaEntry = {
436
+ quotas: [],
437
+ message: errorMsg,
438
+ };
439
+ setQuotaData((prev) => ({
440
+ ...prev,
441
+ [connectionId]: quotaEntry,
442
+ }));
443
+ setQuotaCache(connectionId, quotaEntry);
444
+ return;
445
+ }
446
+
447
+ throw new Error(`HTTP ${response.status}: ${errorMsg}`);
448
+ }
449
+
450
+ const data = await response.json();
451
+ console.log(`[ProviderLimits] Got quota for ${provider}:`, data);
452
+
453
+ // Parse quota data using provider-specific parser
454
+ const parsedQuotas = parseQuotaData(provider, data);
455
+
456
+ const quotaEntry = {
457
+ quotas: parsedQuotas,
458
+ plan: data.plan || null,
459
+ message: data.message || null,
460
+ raw: data,
461
+ };
462
+
463
+ setQuotaData((prev) => ({
464
+ ...prev,
465
+ [connectionId]: quotaEntry,
466
+ }));
467
+ setQuotaCache(connectionId, quotaEntry);
468
+ } catch (error) {
469
+ console.error(
470
+ `[ProviderLimits] Error fetching quota for ${provider} (${connectionId}):`,
471
+ error,
472
+ );
473
+ setErrors((prev) => ({
474
+ ...prev,
475
+ [connectionId]: error.message || "Failed to fetch quota",
476
+ }));
477
+ } finally {
478
+ setLoading((prev) => ({ ...prev, [connectionId]: false }));
479
+ }
480
+ }, []);
481
+
482
+ // Refresh quota for a specific provider
483
+ const refreshProvider = useCallback(
484
+ async (connectionId, provider) => {
485
+ await fetchQuota(connectionId, provider);
486
+ setLastUpdated(new Date());
487
+ },
488
+ [fetchQuota],
489
+ );
490
+
491
+ const refreshAggregate = useCallback(
492
+ async (connectionIds, provider) => {
493
+ if (!connectionIds?.length) return;
494
+
495
+ setLoading((prev) => ({
496
+ ...prev,
497
+ ...Object.fromEntries(connectionIds.map((id) => [id, true])),
498
+ }));
499
+
500
+ await Promise.all(connectionIds.map((id) => fetchQuota(id, provider)));
501
+ setLastUpdated(new Date());
502
+ },
503
+ [fetchQuota],
504
+ );
505
+
506
+ const handleDeleteConnection = useCallback(
507
+ async (id) => {
508
+ if (!confirm("Delete this connection?")) return;
509
+ setDeletingId(id);
510
+ try {
511
+ const res = await fetch(`/api/providers/${id}`, { method: "DELETE" });
512
+ if (res.ok) {
513
+ setQuotaData((prev) => {
514
+ const next = { ...prev };
515
+ delete next[id];
516
+ return next;
517
+ });
518
+ setLoading((prev) => {
519
+ const next = { ...prev };
520
+ delete next[id];
521
+ return next;
522
+ });
523
+ setErrors((prev) => {
524
+ const next = { ...prev };
525
+ delete next[id];
526
+ return next;
527
+ });
528
+
529
+ if (typeof window !== "undefined") {
530
+ try {
531
+ const cache = getQuotaCache();
532
+ if (cache[id]) {
533
+ delete cache[id];
534
+ window.localStorage.setItem(
535
+ QUOTA_CACHE_KEY,
536
+ JSON.stringify(cache),
537
+ );
538
+ }
539
+ } catch (e) {
540
+ console.error("Error deleting cache entry:", e);
541
+ }
542
+ }
543
+
544
+ await reconcileConnectionsPage(fetchConnections, page);
545
+ }
546
+ } catch (error) {
547
+ console.error("Error deleting connection:", error);
548
+ } finally {
549
+ setDeletingId(null);
550
+ }
551
+ },
552
+ [fetchConnections, page],
553
+ );
554
+
555
+ const handleToggleConnectionActive = useCallback(
556
+ async (id, isActive) => {
557
+ setTogglingId(id);
558
+ try {
559
+ const res = await fetch(`/api/providers/${id}`, {
560
+ method: "PUT",
561
+ headers: { "Content-Type": "application/json" },
562
+ body: JSON.stringify({ isActive }),
563
+ });
564
+ if (res.ok) {
565
+ setQuotaData((prev) => {
566
+ const next = { ...prev };
567
+ return next;
568
+ });
569
+ await reconcileConnectionsPage(fetchConnections, page);
570
+ }
571
+ } catch (error) {
572
+ console.error("Error updating connection status:", error);
573
+ } finally {
574
+ setTogglingId(null);
575
+ }
576
+ },
577
+ [fetchConnections, page],
578
+ );
579
+
580
+ const handleUpdateConnection = useCallback(
581
+ async (formData) => {
582
+ if (!selectedConnection?.id) return;
583
+ const connectionId = selectedConnection.id;
584
+ const provider = selectedConnection.provider;
585
+ try {
586
+ const res = await fetch(`/api/providers/${connectionId}`, {
587
+ method: "PUT",
588
+ headers: { "Content-Type": "application/json" },
589
+ body: JSON.stringify(formData),
590
+ });
591
+ if (res.ok) {
592
+ await fetchConnections();
593
+ setShowEditModal(false);
594
+ setSelectedConnection(null);
595
+ if (USAGE_SUPPORTED_PROVIDERS.includes(provider)) {
596
+ await fetchQuota(connectionId, provider);
597
+ }
598
+ }
599
+ } catch (error) {
600
+ console.error("Error saving connection:", error);
601
+ }
602
+ },
603
+ [selectedConnection, fetchConnections, fetchQuota],
604
+ );
605
+
606
+ useEffect(() => {
607
+ let cancelled = false;
608
+ fetch("/api/proxy-pools?isActive=true", { cache: "no-store" })
609
+ .then((res) => res.json())
610
+ .then((data) => {
611
+ if (!cancelled && data?.proxyPools) {
612
+ setProxyPools(data.proxyPools);
613
+ }
614
+ })
615
+ .catch(() => {});
616
+ return () => {
617
+ cancelled = true;
618
+ };
619
+ }, []);
620
+
621
+ const refreshAll = useCallback(async () => {
622
+ if (refreshingAll) return;
623
+
624
+ setRefreshingAll(true);
625
+ setCountdown(60);
626
+
627
+ try {
628
+ const visibleConnections = await fetchConnections(page);
629
+
630
+ setLoading(buildLoadingState(visibleConnections));
631
+ setErrors((prev) =>
632
+ filterQuotaStateByConnections(prev, visibleConnections),
633
+ );
634
+ setQuotaData((prev) =>
635
+ filterQuotaStateByConnections(prev, visibleConnections),
636
+ );
637
+
638
+ await Promise.all(
639
+ visibleConnections.map((conn) => fetchQuota(conn.id, conn.provider)),
640
+ );
641
+
642
+ setLastUpdated(new Date());
643
+ } catch (error) {
644
+ console.error("Error refreshing all providers:", error);
645
+ } finally {
646
+ setRefreshingAll(false);
647
+ }
648
+ }, [refreshingAll, fetchConnections, fetchQuota, page]);
649
+
650
+ useEffect(() => {
651
+ const initializeData = async () => {
652
+ setConnectionsLoading(true);
653
+ const visibleConnections = await fetchConnections(page);
654
+ setConnectionsLoading(false);
655
+
656
+ // Always fetch fresh quota on mount, no cache display
657
+ setLoading(buildLoadingState(visibleConnections));
658
+ setErrors((prev) =>
659
+ filterQuotaStateByConnections(prev, visibleConnections),
660
+ );
661
+ setQuotaData((prev) =>
662
+ filterQuotaStateByConnections(prev, visibleConnections),
663
+ );
664
+
665
+ await Promise.all(
666
+ visibleConnections.map((conn) => fetchQuota(conn.id, conn.provider)),
667
+ );
668
+ setLastUpdated(new Date());
669
+ };
670
+
671
+ initializeData();
672
+ }, [fetchConnections, fetchQuota, page]);
673
+
674
+ useEffect(() => {
675
+ if (typeof window === "undefined") return;
676
+ const stored = window.localStorage.getItem(AUTO_REFRESH_STORAGE_KEY);
677
+ setAutoRefresh(stored === null ? true : stored === "true");
678
+ setHasHydratedAutoRefresh(true);
679
+ }, []);
680
+
681
+ // Persist auto-refresh preference
682
+ useEffect(() => {
683
+ if (typeof window === "undefined" || !hasHydratedAutoRefresh) return;
684
+ window.localStorage.setItem(AUTO_REFRESH_STORAGE_KEY, String(autoRefresh));
685
+ }, [autoRefresh, hasHydratedAutoRefresh]);
686
+
687
+ // Auto-refresh interval
688
+ useEffect(() => {
689
+ if (!hasHydratedAutoRefresh || !autoRefresh) {
690
+ if (intervalRef.current) {
691
+ clearInterval(intervalRef.current);
692
+ intervalRef.current = null;
693
+ }
694
+ if (countdownRef.current) {
695
+ clearInterval(countdownRef.current);
696
+ countdownRef.current = null;
697
+ }
698
+ return;
699
+ }
700
+
701
+ // Main refresh interval
702
+ intervalRef.current = setInterval(() => {
703
+ refreshAll();
704
+ }, REFRESH_INTERVAL_MS);
705
+
706
+ // Countdown interval
707
+ countdownRef.current = setInterval(() => {
708
+ setCountdown((prev) => {
709
+ if (prev <= 1) return 60;
710
+ return prev - 1;
711
+ });
712
+ }, 1000);
713
+
714
+ return () => {
715
+ if (intervalRef.current) clearInterval(intervalRef.current);
716
+ if (countdownRef.current) clearInterval(countdownRef.current);
717
+ };
718
+ }, [autoRefresh, refreshAll, hasHydratedAutoRefresh]);
719
+
720
+ // Pause auto-refresh when tab is hidden (Page Visibility API)
721
+ useEffect(() => {
722
+ const handleVisibilityChange = () => {
723
+ if (document.hidden) {
724
+ if (intervalRef.current) {
725
+ clearInterval(intervalRef.current);
726
+ intervalRef.current = null;
727
+ }
728
+ if (countdownRef.current) {
729
+ clearInterval(countdownRef.current);
730
+ countdownRef.current = null;
731
+ }
732
+ } else if (autoRefresh && hasHydratedAutoRefresh) {
733
+ // Resume auto-refresh when tab becomes visible
734
+ intervalRef.current = setInterval(refreshAll, REFRESH_INTERVAL_MS);
735
+ countdownRef.current = setInterval(() => {
736
+ setCountdown((prev) => (prev <= 1 ? 60 : prev - 1));
737
+ }, 1000);
738
+ }
739
+ };
740
+
741
+ document.addEventListener("visibilitychange", handleVisibilityChange);
742
+ return () => {
743
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
744
+ };
745
+ }, [autoRefresh, refreshAll, hasHydratedAutoRefresh]);
746
+
747
+ const sortedConnections = useMemo(
748
+ () =>
749
+ sortVisibleConnections(
750
+ connections,
751
+ quotaData,
752
+ expiringFirst,
753
+ providerFilter,
754
+ quotaSortMode,
755
+ ),
756
+ [connections, quotaData, expiringFirst, providerFilter, quotaSortMode],
757
+ );
758
+
759
+ const providerAggregateCards = useMemo(
760
+ () => buildProviderAggregateCards(sortedConnections, quotaData, loading, errors),
761
+ [sortedConnections, quotaData, loading, errors],
762
+ );
763
+
764
+ const renderedConnections = useMemo(() => {
765
+ if (displayMode === "bulk") return providerAggregateCards;
766
+ return sortedConnections;
767
+ }, [displayMode, providerAggregateCards, sortedConnections]);
768
+
769
+ // Connection is depleted when any quota entry hit the threshold
770
+ const isConnectionDepleted = (conn) => {
771
+ const quotas = quotaData[conn.id]?.quotas;
772
+ if (!quotas?.length) return false;
773
+ return quotas.some((q) => {
774
+ if (!q.total || q.total <= 0) return false;
775
+ return calculatePercentage(q.used, q.total) <= DEPLETED_QUOTA_THRESHOLD;
776
+ });
777
+ };
778
+
779
+ const bulkSetActive = useCallback(
780
+ async (targetIds, isActive) => {
781
+ if (!targetIds.length || bulkToggling) return;
782
+ setBulkToggling(true);
783
+ try {
784
+ await Promise.all(
785
+ targetIds.map((id) =>
786
+ fetch(`/api/providers/${id}`, {
787
+ method: "PUT",
788
+ headers: { "Content-Type": "application/json" },
789
+ body: JSON.stringify({ isActive }),
790
+ }),
791
+ ),
792
+ );
793
+ await reconcileConnectionsPage(fetchConnections, page);
794
+ } catch (error) {
795
+ console.error("Error bulk toggling connections:", error);
796
+ } finally {
797
+ setBulkToggling(false);
798
+ }
799
+ },
800
+ [bulkToggling, fetchConnections, page],
801
+ );
802
+
803
+ const handleDisableDepleted = () => {
804
+ const ids = sortedConnections
805
+ .filter((c) => (c.isActive ?? true) && isConnectionDepleted(c))
806
+ .map((c) => c.id);
807
+ bulkSetActive(ids, false);
808
+ };
809
+
810
+ const handleEnableAvailable = () => {
811
+ const ids = sortedConnections
812
+ .filter((c) => !(c.isActive ?? true) && !isConnectionDepleted(c))
813
+ .map((c) => c.id);
814
+ bulkSetActive(ids, true);
815
+ };
816
+
817
+ const selectedProviderLabel =
818
+ providerFilter === "all" ? "All providers" : providerFilter;
819
+ const hasEligibleConnections = totals.eligibleConnections > 0;
820
+ const hasVisibleConnections = renderedConnections.length > 0;
821
+ const emptyState = getConnectionsEmptyMessage(
822
+ totals,
823
+ providerFilter,
824
+ accountFilter,
825
+ );
826
+ const connectionsPageSummary = getConnectionsPaginationSummary(pagination);
827
+ const isCustomPageSize = !ACCOUNT_PAGE_SIZE_OPTIONS.includes(pageSize);
828
+ const pageSizeLabel = getPageSizeLabel(pageSize, isCustomPageSize);
829
+
830
+ if (!connectionsLoading && !hasEligibleConnections) {
831
+ return (
832
+ <Card padding="lg">
833
+ <div className="text-center py-12">
834
+ <span className="material-symbols-outlined text-[64px] text-text-muted opacity-20">
835
+ cloud_off
836
+ </span>
837
+ <h3 className="mt-4 text-lg font-semibold text-text-primary">
838
+ No Providers Connected
839
+ </h3>
840
+ <p className="mt-2 text-sm text-text-muted max-w-md mx-auto">
841
+ Connect to providers with OAuth to track your API quota limits and
842
+ usage.
843
+ </p>
844
+ </div>
845
+ </Card>
846
+ );
847
+ }
848
+
849
+ if (!connectionsLoading && !hasVisibleConnections) {
850
+ return (
851
+ <Card padding="lg">
852
+ <div className="text-center py-12">
853
+ <span className="material-symbols-outlined text-[64px] text-text-muted opacity-20">
854
+ {emptyState.icon}
855
+ </span>
856
+ <h3 className="mt-4 text-lg font-semibold text-text-primary">
857
+ {emptyState.title}
858
+ </h3>
859
+ <p className="mt-2 text-sm text-text-muted max-w-md mx-auto">
860
+ {emptyState.description}
861
+ </p>
862
+ </div>
863
+ </Card>
864
+ );
865
+ }
866
+
867
+ return (
868
+ <div className="space-y-6">
869
+ {/* Header Controls */}
870
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-end">
871
+ <div className="flex flex-wrap items-center gap-1.5">
872
+ <div className="relative">
873
+ <button
874
+ type="button"
875
+ onClick={() => setProviderMenuOpen((prev) => !prev)}
876
+ className="flex h-8 items-center justify-between gap-1 rounded-lg border border-black/10 bg-black/[0.02] px-2 text-xs text-text-primary transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10"
877
+ aria-haspopup="menu"
878
+ aria-expanded={providerMenuOpen}
879
+ title="Filter quota providers"
880
+ >
881
+ <span className="flex min-w-0 items-center gap-1.5">
882
+ {providerFilter === "all" ? (
883
+ <span className="material-symbols-outlined text-[14px] text-text-muted">
884
+ apps
885
+ </span>
886
+ ) : (
887
+ <ProviderIcon
888
+ src={`/providers/${providerFilter}.png`}
889
+ alt={providerFilter}
890
+ size={18}
891
+ className="size-[18px] rounded object-contain"
892
+ fallbackText={providerFilter.slice(0, 2).toUpperCase()}
893
+ />
894
+ )}
895
+ <span className="truncate capitalize hidden lg:inline">
896
+ {selectedProviderLabel}
897
+ </span>
898
+ </span>
899
+ <span className="material-symbols-outlined text-[14px] text-text-muted">
900
+ expand_more
901
+ </span>
902
+ </button>
903
+
904
+ {providerMenuOpen && (
905
+ <>
906
+ <button
907
+ type="button"
908
+ className="fixed inset-0 z-30 bg-transparent"
909
+ aria-label="Close provider filter"
910
+ onClick={() => setProviderMenuOpen(false)}
911
+ />
912
+ <div className="absolute left-0 z-40 mt-2 w-64 overflow-hidden rounded-2xl border border-black/10 bg-surface/95 p-1.5 shadow-xl shadow-black/10 backdrop-blur dark:border-white/10 dark:bg-surface/95 sm:w-72">
913
+ <button
914
+ type="button"
915
+ onClick={() => {
916
+ if (shouldResetPage(providerFilter, "all")) {
917
+ setPage(1);
918
+ }
919
+ setProviderFilter("all");
920
+ setProviderMenuOpen(false);
921
+ }}
922
+ className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === "all" ? "bg-primary/10 text-primary" : "text-text-primary hover:bg-black/5 dark:hover:bg-white/10"}`}
923
+ >
924
+ <span className="material-symbols-outlined text-[22px]">
925
+ apps
926
+ </span>
927
+ <span className="font-medium">All providers</span>
928
+ {providerFilter === "all" && (
929
+ <span className="material-symbols-outlined ml-auto text-[20px]">
930
+ check
931
+ </span>
932
+ )}
933
+ </button>
934
+ <div className="my-1 h-px bg-black/10 dark:bg-white/10" />
935
+ <div className="max-h-72 overflow-y-auto pr-1">
936
+ {providerOptions.map((provider) => (
937
+ <button
938
+ key={provider}
939
+ type="button"
940
+ onClick={() => {
941
+ if (shouldResetPage(providerFilter, provider)) {
942
+ setPage(1);
943
+ }
944
+ setProviderFilter(provider);
945
+ setProviderMenuOpen(false);
946
+ }}
947
+ className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === provider ? "bg-primary/10 text-primary" : "text-text-primary hover:bg-black/5 dark:hover:bg-white/10"}`}
948
+ >
949
+ <ProviderIcon
950
+ src={`/providers/${provider}.png`}
951
+ alt={provider}
952
+ size={24}
953
+ className="size-6 rounded-md object-contain"
954
+ fallbackText={provider.slice(0, 2).toUpperCase()}
955
+ />
956
+ <span className="font-medium capitalize">
957
+ {provider}
958
+ </span>
959
+ {providerFilter === provider && (
960
+ <span className="material-symbols-outlined ml-auto text-[20px]">
961
+ check
962
+ </span>
963
+ )}
964
+ </button>
965
+ ))}
966
+ </div>
967
+ </div>
968
+ </>
969
+ )}
970
+ </div>
971
+ <div className="inline-flex h-8 overflow-hidden rounded-lg border border-black/10 bg-black/[0.02] dark:border-white/10 dark:bg-white/[0.03]">
972
+ {QUOTA_DISPLAY_MODE_OPTIONS.map((option) => (
973
+ <button
974
+ key={option.value}
975
+ type="button"
976
+ onClick={() => setDisplayMode(option.value)}
977
+ aria-pressed={displayMode === option.value}
978
+ className={`flex h-full items-center gap-1.5 px-2 text-xs transition-colors ${displayMode === option.value ? "bg-primary text-white" : "text-text-primary hover:bg-black/5 dark:hover:bg-white/10"}`}
979
+ title={`Show quota in ${option.label} mode`}
980
+ >
981
+ <span className="material-symbols-outlined text-[14px]">
982
+ {option.icon}
983
+ </span>
984
+ <span className="hidden sm:inline">{option.label}</span>
985
+ </button>
986
+ ))}
987
+ </div>
988
+ <select
989
+ value={accountFilter}
990
+ onChange={(event) => {
991
+ const nextValue = event.target.value;
992
+ if (shouldResetPage(accountFilter, nextValue)) {
993
+ setPage(1);
994
+ }
995
+ setAccountFilter(nextValue);
996
+ }}
997
+ className="h-8 rounded-lg border border-black/10 bg-black/[0.02] px-2 text-xs text-text-primary outline-none transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10"
998
+ aria-label="Filter accounts by status"
999
+ >
1000
+ {ACCOUNT_FILTER_OPTIONS.map((option) => (
1001
+ <option key={option.value} value={option.value}>
1002
+ {option.label}
1003
+ </option>
1004
+ ))}
1005
+ </select>
1006
+
1007
+ {providerFilter === "codex" && (
1008
+ <select
1009
+ value={quotaSortMode}
1010
+ onChange={(event) => setQuotaSortMode(event.target.value)}
1011
+ className="h-8 rounded-lg border border-black/10 bg-black/[0.02] px-2 text-xs text-text-primary outline-none transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10"
1012
+ aria-label="Sort Codex quotas by remaining"
1013
+ >
1014
+ {QUOTA_SORT_OPTIONS.map((option) => (
1015
+ <option key={option.value} value={option.value}>
1016
+ {option.label}
1017
+ </option>
1018
+ ))}
1019
+ </select>
1020
+ )}
1021
+
1022
+ <button
1023
+ type="button"
1024
+ onClick={() => setExpiringFirst((prev) => !prev)}
1025
+ aria-pressed={expiringFirst}
1026
+ className={`flex h-8 shrink-0 items-center gap-1 rounded-lg border px-2 text-xs transition-colors ${expiringFirst ? "border-amber-500/40 bg-amber-500/10 text-amber-500" : "border-black/10 text-text-primary hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"}`}
1027
+ title="Sort accounts by earliest quota reset time"
1028
+ >
1029
+ <span className="material-symbols-outlined text-[14px]">
1030
+ hourglass_top
1031
+ </span>
1032
+ <span className="hidden sm:inline">Expiring first</span>
1033
+ </button>
1034
+
1035
+ {/* Bulk: disable depleted */}
1036
+ <button
1037
+ type="button"
1038
+ onClick={handleDisableDepleted}
1039
+ disabled={bulkToggling}
1040
+ className="flex h-8 shrink-0 items-center gap-1 rounded-lg border border-red-500/30 px-2 text-xs text-red-500 transition-colors hover:bg-red-500/10 disabled:opacity-50"
1041
+ title="Disable connections with depleted quota on the current page"
1042
+ >
1043
+ <span className="material-symbols-outlined text-[14px]">block</span>
1044
+ <span className="hidden sm:inline">Turn off Empty</span>
1045
+ </button>
1046
+
1047
+ {/* Bulk: enable available */}
1048
+ <button
1049
+ type="button"
1050
+ onClick={handleEnableAvailable}
1051
+ disabled={bulkToggling}
1052
+ className="flex h-8 shrink-0 items-center gap-1 rounded-lg border border-emerald-500/30 px-2 text-xs text-emerald-500 transition-colors hover:bg-emerald-500/10 disabled:opacity-50"
1053
+ title="Enable connections that still have quota on the current page"
1054
+ >
1055
+ <span className="material-symbols-outlined text-[14px]">
1056
+ check_circle
1057
+ </span>
1058
+ <span className="hidden sm:inline">Turn on Available</span>
1059
+ </button>
1060
+
1061
+ {/* Auto-refresh toggle */}
1062
+ <button
1063
+ onClick={() => setAutoRefresh((prev) => !prev)}
1064
+ className="flex h-8 shrink-0 items-center gap-1 rounded-lg border border-black/10 px-2 text-xs transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"
1065
+ title={autoRefresh ? "Disable auto-refresh" : "Enable auto-refresh"}
1066
+ >
1067
+ <span
1068
+ className={`material-symbols-outlined text-[14px] ${
1069
+ autoRefresh ? "text-primary" : "text-text-muted"
1070
+ }`}
1071
+ >
1072
+ {autoRefresh ? "toggle_on" : "toggle_off"}
1073
+ </span>
1074
+ <span className="hidden text-text-primary sm:inline">
1075
+ Auto-refresh
1076
+ </span>
1077
+ {autoRefresh && (
1078
+ <span className="text-[10px] text-text-muted tabular-nums">
1079
+ ({countdown}s)
1080
+ </span>
1081
+ )}
1082
+ </button>
1083
+
1084
+ {/* Refresh all button */}
1085
+ <button
1086
+ type="button"
1087
+ onClick={refreshAll}
1088
+ disabled={refreshingAll}
1089
+ className="flex h-8 shrink-0 items-center gap-1 rounded-lg border border-black/10 px-2 text-xs text-text-primary transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5 disabled:opacity-50"
1090
+ title="Refresh all"
1091
+ >
1092
+ <span
1093
+ className={`material-symbols-outlined text-[14px] ${refreshingAll ? "animate-spin" : ""}`}
1094
+ >
1095
+ refresh
1096
+ </span>
1097
+ </button>
1098
+ </div>
1099
+ </div>
1100
+
1101
+ {/* Provider cards: 2 columns, compact */}
1102
+ {expiringFirst && (
1103
+ <div className="rounded-xl border border-amber-500/20 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-300">
1104
+ Expiring-first currently reorders accounts inside the current page.
1105
+ Cross-page ordering still follows backend pagination.
1106
+ </div>
1107
+ )}
1108
+
1109
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1110
+ {renderedConnections.map((conn) => {
1111
+ const isAggregate = conn.isAggregate === true;
1112
+ const quota = isAggregate ? conn.quota : quotaData[conn.id];
1113
+ const isLoading = isAggregate ? conn.isLoading : loading[conn.id];
1114
+ const error = isAggregate ? conn.error : errors[conn.id];
1115
+
1116
+ // Use table layout for all providers
1117
+ const isInactive = conn.isActive === false;
1118
+ const rowBusy = isAggregate ? false : deletingId === conn.id || togglingId === conn.id;
1119
+
1120
+ return (
1121
+ <Card
1122
+ key={conn.id}
1123
+ padding="none"
1124
+ className={`min-w-0 ${isInactive ? "opacity-60" : ""}`}
1125
+ >
1126
+ <div className="px-3 py-2 border-b border-black/10 dark:border-white/10">
1127
+ <div className="flex items-center justify-between gap-2">
1128
+ <div className="flex items-center gap-2 min-w-0">
1129
+ <div className="w-8 h-8 shrink-0 rounded-md flex items-center justify-center overflow-hidden">
1130
+ <ProviderIcon
1131
+ src={`/providers/${conn.provider}.png`}
1132
+ alt={conn.provider}
1133
+ size={32}
1134
+ className="object-contain"
1135
+ fallbackText={
1136
+ conn.provider?.slice(0, 2).toUpperCase() || "PR"
1137
+ }
1138
+ />
1139
+ </div>
1140
+ <div className="min-w-0">
1141
+ <h3 className="text-sm font-semibold text-text-primary capitalize truncate">
1142
+ {conn.name || getProviderDisplayName(conn.provider)}
1143
+ </h3>
1144
+ {getConnectionLabel(conn) ? (
1145
+ <p className="text-xs text-text-muted truncate">
1146
+ {getConnectionLabel(conn)}
1147
+ </p>
1148
+ ) : null}
1149
+ </div>
1150
+ </div>
1151
+
1152
+ <div className="flex items-center gap-1 shrink-0">
1153
+ <button
1154
+ type="button"
1155
+ onClick={() => (
1156
+ isAggregate
1157
+ ? refreshAggregate(conn.connectionIds, conn.provider)
1158
+ : refreshProvider(conn.id, conn.provider)
1159
+ )}
1160
+ disabled={isLoading || rowBusy}
1161
+ aria-label="Refresh quota"
1162
+ className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
1163
+ title="Refresh quota"
1164
+ >
1165
+ <span
1166
+ className={`material-symbols-outlined text-[18px] text-text-muted ${isLoading ? "animate-spin" : ""}`}
1167
+ >
1168
+ refresh
1169
+ </span>
1170
+ </button>
1171
+ {!isAggregate && (
1172
+ <>
1173
+ <button
1174
+ type="button"
1175
+ onClick={() => {
1176
+ setSelectedConnection(conn);
1177
+ setShowEditModal(true);
1178
+ }}
1179
+ disabled={rowBusy}
1180
+ aria-label="Edit connection"
1181
+ className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors disabled:opacity-50"
1182
+ title="Edit connection"
1183
+ >
1184
+ <span className="material-symbols-outlined text-[18px]">
1185
+ edit
1186
+ </span>
1187
+ </button>
1188
+ <button
1189
+ type="button"
1190
+ onClick={() => handleDeleteConnection(conn.id)}
1191
+ disabled={rowBusy}
1192
+ aria-label="Delete connection"
1193
+ className="p-1.5 rounded-lg hover:bg-red-500/10 text-red-500 transition-colors disabled:opacity-50"
1194
+ title="Delete connection"
1195
+ >
1196
+ <span
1197
+ className={`material-symbols-outlined text-[18px] ${deletingId === conn.id ? "animate-pulse" : ""}`}
1198
+ >
1199
+ delete
1200
+ </span>
1201
+ </button>
1202
+ <div
1203
+ className="inline-flex items-center pl-0.5"
1204
+ title={
1205
+ (conn.isActive ?? true)
1206
+ ? "Disable connection"
1207
+ : "Enable connection"
1208
+ }
1209
+ >
1210
+ <Toggle
1211
+ size="sm"
1212
+ checked={conn.isActive ?? true}
1213
+ disabled={rowBusy}
1214
+ onChange={(nextActive) =>
1215
+ handleToggleConnectionActive(conn.id, nextActive)
1216
+ }
1217
+ />
1218
+ </div>
1219
+ </>
1220
+ )}
1221
+ </div>
1222
+ </div>
1223
+ </div>
1224
+
1225
+ <div className="px-2 py-1.5">
1226
+ {isLoading ? (
1227
+ <div className="text-center py-5 text-text-muted">
1228
+ <span className="material-symbols-outlined text-[28px] animate-spin">
1229
+ progress_activity
1230
+ </span>
1231
+ </div>
1232
+ ) : error ? (
1233
+ <div className="text-center py-5">
1234
+ <span className="material-symbols-outlined text-[28px] text-red-500">
1235
+ error
1236
+ </span>
1237
+ <p className="mt-1.5 text-xs text-text-muted">{error}</p>
1238
+ </div>
1239
+ ) : quota?.message ? (
1240
+ <div className="text-center py-5">
1241
+ <p className="text-xs text-text-muted">{quota.message}</p>
1242
+ </div>
1243
+ ) : (
1244
+ <QuotaTable
1245
+ quotas={quota?.quotas}
1246
+ compact
1247
+ sortMode="default"
1248
+ showSortLabel={
1249
+ conn.provider === "codex" && quotaSortMode !== "default"
1250
+ }
1251
+ />
1252
+ )}
1253
+ </div>
1254
+ </Card>
1255
+ );
1256
+ })}
1257
+ </div>
1258
+
1259
+ <div className="rounded-xl border border-black/10 bg-black/[0.02] px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]">
1260
+ <div className="flex flex-wrap items-center justify-between gap-2">
1261
+ <span className="text-xs text-text-muted">{connectionsPageSummary}</span>
1262
+ <div className="flex flex-wrap items-center gap-2">
1263
+ <select
1264
+ value={isCustomPageSize ? "custom" : String(pageSize)}
1265
+ onChange={(event) => {
1266
+ const nextValue = event.target.value;
1267
+ if (nextValue === "custom") return;
1268
+ const nextPageSize = Number.parseInt(nextValue, 10);
1269
+ if (Number.isFinite(nextPageSize)) {
1270
+ setPage(1);
1271
+ setPageSize(nextPageSize);
1272
+ setCustomPageSizeInput(String(nextPageSize));
1273
+ }
1274
+ }}
1275
+ className="h-8 rounded-lg border border-black/10 bg-black/[0.02] px-2 text-xs text-text-primary outline-none transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10"
1276
+ aria-label="Accounts per page"
1277
+ >
1278
+ {ACCOUNT_PAGE_SIZE_OPTIONS.map((option) => (
1279
+ <option key={option} value={String(option)}>
1280
+ {option} / page
1281
+ </option>
1282
+ ))}
1283
+ <option value="custom">Custom</option>
1284
+ </select>
1285
+ <input
1286
+ type="number"
1287
+ min="1"
1288
+ max={String(ACCOUNT_PAGE_SIZE_MAX)}
1289
+ inputMode="numeric"
1290
+ value={customPageSizeInput}
1291
+ onChange={(event) => setCustomPageSizeInput(event.target.value)}
1292
+ onBlur={() => {
1293
+ const parsedValue = Number.parseInt(customPageSizeInput, 10);
1294
+ if (!Number.isFinite(parsedValue)) {
1295
+ setCustomPageSizeInput(String(pageSize));
1296
+ return;
1297
+ }
1298
+ const nextPageSize = Math.min(ACCOUNT_PAGE_SIZE_MAX, Math.max(1, parsedValue));
1299
+ setPage(1);
1300
+ setPageSize(nextPageSize);
1301
+ setCustomPageSizeInput(String(nextPageSize));
1302
+ }}
1303
+ onKeyDown={(event) => {
1304
+ if (event.key !== "Enter") return;
1305
+ const parsedValue = Number.parseInt(customPageSizeInput, 10);
1306
+ if (!Number.isFinite(parsedValue)) {
1307
+ setCustomPageSizeInput(String(pageSize));
1308
+ return;
1309
+ }
1310
+ const nextPageSize = Math.min(ACCOUNT_PAGE_SIZE_MAX, Math.max(1, parsedValue));
1311
+ setPage(1);
1312
+ setPageSize(nextPageSize);
1313
+ setCustomPageSizeInput(String(nextPageSize));
1314
+ }}
1315
+ className="h-8 w-20 rounded-lg border border-black/10 bg-black/[0.02] px-2 text-xs text-text-primary outline-none transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10"
1316
+ aria-label="Custom accounts per page"
1317
+ placeholder="Custom"
1318
+ />
1319
+ <span className="text-xs text-text-muted">Page {pagination.page} / {pagination.totalPages}</span>
1320
+ </div>
1321
+ <div className="flex items-center gap-1.5">
1322
+ <button
1323
+ type="button"
1324
+ onClick={() => setPage(1)}
1325
+ disabled={
1326
+ pagination.page <= 1 || connectionsLoading || refreshingAll
1327
+ }
1328
+ className="flex h-8 items-center rounded-lg border border-black/10 px-3 text-xs text-text-primary transition-colors hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-40 dark:border-white/10 dark:hover:bg-white/5"
1329
+ >
1330
+ First Page
1331
+ </button>
1332
+ <button
1333
+ type="button"
1334
+ onClick={() =>
1335
+ setPage((currentPage) => Math.max(1, currentPage - 1))
1336
+ }
1337
+ disabled={
1338
+ pagination.page <= 1 || connectionsLoading || refreshingAll
1339
+ }
1340
+ className="flex h-8 w-8 items-center justify-center rounded-lg border border-black/10 text-text-primary transition-colors hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-40 dark:border-white/10 dark:hover:bg-white/5"
1341
+ aria-label="Previous accounts page"
1342
+ >
1343
+ <span className="material-symbols-outlined text-[16px]">
1344
+ chevron_left
1345
+ </span>
1346
+ </button>
1347
+ <button
1348
+ type="button"
1349
+ onClick={() =>
1350
+ setPage((currentPage) =>
1351
+ Math.min(pagination.totalPages, currentPage + 1),
1352
+ )
1353
+ }
1354
+ disabled={
1355
+ pagination.page >= pagination.totalPages ||
1356
+ connectionsLoading ||
1357
+ refreshingAll
1358
+ }
1359
+ className="flex h-8 w-8 items-center justify-center rounded-lg border border-black/10 text-text-primary transition-colors hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-40 dark:border-white/10 dark:hover:bg-white/5"
1360
+ aria-label="Next accounts page"
1361
+ >
1362
+ <span className="material-symbols-outlined text-[16px]">
1363
+ chevron_right
1364
+ </span>
1365
+ </button>
1366
+ <button
1367
+ type="button"
1368
+ onClick={() => setPage(pagination.totalPages)}
1369
+ disabled={
1370
+ pagination.page >= pagination.totalPages ||
1371
+ connectionsLoading ||
1372
+ refreshingAll
1373
+ }
1374
+ className="flex h-8 items-center rounded-lg border border-black/10 px-3 text-xs text-text-primary transition-colors hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-40 dark:border-white/10 dark:hover:bg-white/5"
1375
+ >
1376
+ Last Page
1377
+ </button>
1378
+ </div>
1379
+ </div>
1380
+ </div>
1381
+
1382
+ <EditConnectionModal
1383
+ isOpen={showEditModal}
1384
+ connection={selectedConnection}
1385
+ proxyPools={proxyPools}
1386
+ onSave={handleUpdateConnection}
1387
+ onClose={() => {
1388
+ setShowEditModal(false);
1389
+ setSelectedConnection(null);
1390
+ }}
1391
+ />
1392
+ </div>
1393
+ );
1394
+ }