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