From f5cc3a36fbea7f310ef8ced58148407135aa3d41 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 24 Apr 2026 12:16:21 -0700 Subject: [PATCH] [eric] remove vendored 9router (now fetched from npm at build), remove Copilot, and fix subs + tools --- .gitignore | 6 +- 9router/.dockerignore | 32 - 9router/.env.example | 38 - 9router/.github/dependabot.yml | 2 - 9router/.github/workflows/docker-publish.yml | 70 - 9router/.gitignore | 70 - 9router/.gitmodules | 3 - 9router/.npmignore | 60 - 9router/.vscode/settings.json | 20 - 9router/CHANGELOG.md | 91 - 9router/Dockerfile | 35 - 9router/LICENSE | 21 - 9router/README.md | 1211 --------- 9router/cloud/.gitignore | 3 - 9router/cloud/README.md | 25 - 9router/cloud/jsconfig.json | 12 - 9router/cloud/migrations/0001_init.sql | 9 - 9router/cloud/package.json | 17 - 9router/cloud/src/handlers/cache.js | 37 - 9router/cloud/src/handlers/chat.js | 305 --- 9router/cloud/src/handlers/cleanup.js | 34 - 9router/cloud/src/handlers/countTokens.js | 46 - 9router/cloud/src/handlers/embeddings.js | 285 --- 9router/cloud/src/handlers/forward.js | 75 - 9router/cloud/src/handlers/forwardRaw.js | 173 -- 9router/cloud/src/handlers/sync.js | 227 -- 9router/cloud/src/handlers/verify.js | 60 - 9router/cloud/src/index.js | 233 -- 9router/cloud/src/services/landingPage.js | 27 - 9router/cloud/src/services/storage.js | 88 - 9router/cloud/src/services/tokenRefresh.js | 11 - 9router/cloud/src/stubs/usageDb.js | 8 - 9router/cloud/src/utils/apiKey.js | 72 - 9router/cloud/src/utils/logger.js | 84 - 9router/cloud/wrangler.toml | 17 - 9router/docs/ARCHITECTURE.md | 557 ---- 9router/eslint.config.mjs | 16 - 9router/i18n/README.vi.md | 1310 ---------- 9router/i18n/README.zh-CN.md | 1306 ---------- 9router/images/9router.png | Bin 758319 -> 0 bytes 9router/jsconfig.json | 12 - 9router/next.config.mjs | 64 - 9router/open-sse/.npmignore | 8 - 9router/open-sse/config/appConstants.js | 130 - 9router/open-sse/config/codexInstructions.js | 120 - 9router/open-sse/config/constants.js | 4 - .../config/defaultThinkingSignature.js | 7 - 9router/open-sse/config/ollamaModels.js | 19 - 9router/open-sse/config/providerModels.js | 390 --- 9router/open-sse/config/providers.js | 313 --- 9router/open-sse/config/runtimeConfig.js | 92 - 9router/open-sse/executors/antigravity.js | 261 -- 9router/open-sse/executors/base.js | 130 - 9router/open-sse/executors/codex.js | 108 - 9router/open-sse/executors/cursor.js | 742 ------ 9router/open-sse/executors/default.js | 213 -- 9router/open-sse/executors/gemini-cli.js | 67 - 9router/open-sse/executors/github.js | 305 --- 9router/open-sse/executors/iflow.js | 104 - 9router/open-sse/executors/index.js | 45 - 9router/open-sse/executors/kiro.js | 448 ---- 9router/open-sse/executors/vertex.js | 120 - 9router/open-sse/handlers/chatCore.js | 213 -- .../handlers/chatCore/nonStreamingHandler.js | 204 -- .../handlers/chatCore/requestDetail.js | 102 - .../handlers/chatCore/sseToJsonHandler.js | 161 -- .../handlers/chatCore/streamingHandler.js | 99 - 9router/open-sse/handlers/embeddingsCore.js | 302 --- 9router/open-sse/handlers/responsesHandler.js | 103 - 9router/open-sse/index.js | 74 - 9router/open-sse/services/accountFallback.js | 251 -- 9router/open-sse/services/combo.js | 111 - 9router/open-sse/services/compact.js | 71 - 9router/open-sse/services/model.js | 169 -- 9router/open-sse/services/projectId.js | 307 --- 9router/open-sse/services/provider.js | 356 --- 9router/open-sse/services/tokenRefresh.js | 731 ------ 9router/open-sse/services/usage.js | 708 ------ .../transformer/responsesTransformer.js | 439 ---- .../transformer/streamToJsonConverter.js | 103 - 9router/open-sse/translator/formats.js | 34 - .../translator/helpers/claudeHelper.js | 215 -- .../translator/helpers/geminiHelper.js | 337 --- .../translator/helpers/maxTokensHelper.js | 25 - .../translator/helpers/openaiHelper.js | 127 - .../translator/helpers/responsesApiHelper.js | 129 - .../translator/helpers/toolCallHelper.js | 111 - 9router/open-sse/translator/index.js | 204 -- .../request/antigravity-to-openai.js | 223 -- .../translator/request/claude-to-openai.js | 241 -- .../translator/request/gemini-to-openai.js | 147 -- .../translator/request/openai-responses.js | 277 -- .../translator/request/openai-to-claude.js | 348 --- .../translator/request/openai-to-cursor.js | 181 -- .../translator/request/openai-to-gemini.js | 449 ---- .../translator/request/openai-to-kiro.js | 306 --- .../translator/request/openai-to-kiro.old.js | 278 -- .../translator/request/openai-to-ollama.js | 159 -- .../translator/response/claude-to-openai.js | 206 -- .../translator/response/cursor-to-openai.js | 30 - .../translator/response/gemini-to-openai.js | 240 -- .../translator/response/kiro-to-openai.js | 189 -- .../translator/response/ollama-to-openai.js | 152 -- .../translator/response/openai-responses.js | 571 ----- .../response/openai-to-antigravity.js | 120 - .../translator/response/openai-to-claude.js | 223 -- 9router/open-sse/utils/bypassHandler.js | 298 --- 9router/open-sse/utils/claudeCloaking.js | 60 - 9router/open-sse/utils/cursorChecksum.js | 149 -- 9router/open-sse/utils/cursorProtobuf.js | 904 ------- 9router/open-sse/utils/error.js | 185 -- 9router/open-sse/utils/ollamaTransform.js | 85 - 9router/open-sse/utils/proxyFetch.js | 258 -- 9router/open-sse/utils/requestLogger.js | 260 -- 9router/open-sse/utils/sessionManager.js | 82 - 9router/open-sse/utils/stream.js | 392 --- 9router/open-sse/utils/streamHandler.js | 138 - 9router/open-sse/utils/streamHelpers.js | 122 - 9router/open-sse/utils/usageTracking.js | 325 --- 9router/package.json | 49 - 9router/postcss.config.mjs | 6 - 9router/public/favicon.svg | 11 - 9router/public/file.svg | 1 - 9router/public/globe.svg | 1 - 9router/public/i18n/literals/ar.json | 196 -- 9router/public/i18n/literals/bn.json | 196 -- 9router/public/i18n/literals/cs.json | 196 -- 9router/public/i18n/literals/da.json | 196 -- 9router/public/i18n/literals/de.json | 196 -- 9router/public/i18n/literals/el.json | 196 -- 9router/public/i18n/literals/es.json | 196 -- 9router/public/i18n/literals/fi.json | 196 -- 9router/public/i18n/literals/fr.json | 196 -- 9router/public/i18n/literals/he.json | 196 -- 9router/public/i18n/literals/hi.json | 196 -- 9router/public/i18n/literals/hu.json | 196 -- 9router/public/i18n/literals/id.json | 196 -- 9router/public/i18n/literals/it.json | 196 -- 9router/public/i18n/literals/ja.json | 196 -- 9router/public/i18n/literals/ko.json | 196 -- 9router/public/i18n/literals/nl.json | 196 -- 9router/public/i18n/literals/no.json | 196 -- 9router/public/i18n/literals/pl.json | 196 -- 9router/public/i18n/literals/pt-BR.json | 196 -- 9router/public/i18n/literals/pt-PT.json | 196 -- 9router/public/i18n/literals/ro.json | 196 -- 9router/public/i18n/literals/ru.json | 196 -- 9router/public/i18n/literals/sv.json | 196 -- 9router/public/i18n/literals/th.json | 196 -- 9router/public/i18n/literals/tl.json | 196 -- 9router/public/i18n/literals/tr.json | 196 -- 9router/public/i18n/literals/uk.json | 196 -- 9router/public/i18n/literals/ur.json | 196 -- 9router/public/i18n/literals/vi.json | 196 -- 9router/public/i18n/literals/zh-CN.json | 515 ---- 9router/public/i18n/literals/zh-TW.json | 196 -- 9router/public/icons/icon-192.svg | 4 - 9router/public/icons/icon-512.svg | 4 - 9router/public/next.svg | 1 - 9router/public/providers/alicode-intl.png | Bin 4859 -> 0 bytes 9router/public/providers/alicode.png | Bin 4859 -> 0 bytes 9router/public/providers/anthropic-m.png | Bin 5133 -> 0 bytes 9router/public/providers/anthropic.png | Bin 2736 -> 0 bytes 9router/public/providers/antigravity.png | Bin 11588 -> 0 bytes 9router/public/providers/cerebras.png | Bin 2295 -> 0 bytes 9router/public/providers/chutes.png | Bin 4730 -> 0 bytes 9router/public/providers/claude.png | Bin 11797 -> 0 bytes 9router/public/providers/cline.png | Bin 15547 -> 0 bytes 9router/public/providers/codex.png | Bin 5819 -> 0 bytes 9router/public/providers/cohere.png | Bin 2791 -> 0 bytes 9router/public/providers/continue.png | Bin 33371 -> 0 bytes 9router/public/providers/copilot.png | Bin 27346 -> 0 bytes 9router/public/providers/cursor.png | Bin 7736 -> 0 bytes 9router/public/providers/deepseek.png | Bin 2393 -> 0 bytes 9router/public/providers/droid.png | Bin 6875 -> 0 bytes 9router/public/providers/fireworks.png | Bin 2612 -> 0 bytes 9router/public/providers/gemini-cli.png | Bin 11646 -> 0 bytes 9router/public/providers/gemini.png | Bin 9927 -> 0 bytes 9router/public/providers/github.png | Bin 27346 -> 0 bytes 9router/public/providers/glm-cn.png | Bin 2548 -> 0 bytes 9router/public/providers/glm.png | Bin 2548 -> 0 bytes 9router/public/providers/groq.png | Bin 2882 -> 0 bytes 9router/public/providers/iflow.png | Bin 10327 -> 0 bytes 9router/public/providers/kilocode.png | Bin 3334 -> 0 bytes 9router/public/providers/kimi-coding.png | Bin 18477 -> 0 bytes 9router/public/providers/kimi.png | Bin 18477 -> 0 bytes 9router/public/providers/kiro.png | Bin 8905 -> 0 bytes 9router/public/providers/minimax-cn.png | Bin 15349 -> 0 bytes 9router/public/providers/minimax.png | Bin 15349 -> 0 bytes 9router/public/providers/mistral.png | Bin 2106 -> 0 bytes 9router/public/providers/nebius.png | Bin 2335 -> 0 bytes 9router/public/providers/nvidia.png | Bin 2582 -> 0 bytes 9router/public/providers/oai-cc.png | Bin 10137 -> 0 bytes 9router/public/providers/oai-r.png | Bin 9041 -> 0 bytes 9router/public/providers/ollama-local.png | Bin 7487 -> 0 bytes 9router/public/providers/ollama.png | Bin 7487 -> 0 bytes 9router/public/providers/openai.png | Bin 1117 -> 0 bytes 9router/public/providers/openclaw.png | Bin 16740 -> 0 bytes 9router/public/providers/opencode.png | Bin 16378 -> 0 bytes 9router/public/providers/openrouter.png | Bin 8824 -> 0 bytes 9router/public/providers/perplexity.png | Bin 2108 -> 0 bytes 9router/public/providers/qwen.png | Bin 14534 -> 0 bytes 9router/public/providers/roo.png | Bin 3084 -> 0 bytes 9router/public/providers/siliconflow.png | Bin 7250 -> 0 bytes 9router/public/providers/together.png | Bin 2024 -> 0 bytes 9router/public/providers/vertex-partner.png | Bin 10060 -> 0 bytes 9router/public/providers/vertex.png | Bin 10060 -> 0 bytes 9router/public/providers/xai.png | Bin 2651 -> 0 bytes 9router/public/sw.js | 22 - 9router/public/vercel.svg | 1 - 9router/public/window.svg | 1 - 9router/scripts/translate-readme.js | 201 -- .../dashboard/cli-tools/CLIToolsPageClient.js | 206 -- .../components/AntigravityToolCard.js | 478 ---- .../cli-tools/components/ClaudeToolCard.js | 388 --- .../cli-tools/components/CodexToolCard.js | 336 --- .../cli-tools/components/CopilotToolCard.js | 295 --- .../cli-tools/components/DefaultToolCard.js | 293 --- .../cli-tools/components/DroidToolCard.js | 339 --- .../cli-tools/components/MitmServerCard.js | 241 -- .../cli-tools/components/MitmToolCard.js | 299 --- .../cli-tools/components/OpenClawToolCard.js | 352 --- .../cli-tools/components/OpenCodeToolCard.js | 308 --- .../dashboard/cli-tools/components/index.js | 11 - .../(dashboard)/dashboard/cli-tools/page.js | 7 - .../app/(dashboard)/dashboard/combos/page.js | 470 ---- .../dashboard/console-log/ConsoleLogClient.js | 91 - .../(dashboard)/dashboard/console-log/page.js | 8 - .../dashboard/endpoint/EndpointPageClient.js | 772 ------ .../(dashboard)/dashboard/endpoint/page.js | 7 - .../dashboard/mitm/MitmPageClient.js | 108 - .../app/(dashboard)/dashboard/mitm/page.js | 5 - 9router/src/app/(dashboard)/dashboard/page.js | 7 - .../app/(dashboard)/dashboard/profile/page.js | 631 ----- .../dashboard/providers/[id]/page.js | 2235 ----------------- .../dashboard/providers/[id]/page.new.js | 1724 ------------- .../components/ModelAvailabilityBadge.js | 185 -- .../dashboard/providers/new/page.js | 220 -- .../(dashboard)/dashboard/providers/page.js | 1036 -------- .../(dashboard)/dashboard/proxy-pools/page.js | 489 ---- .../app/(dashboard)/dashboard/quota/page.js | 11 - .../(dashboard)/dashboard/translator/page.js | 300 --- .../usage/components/OverviewCards.js | 35 - .../ProviderLimits/ProviderLimitCard.js | 190 -- .../ProviderLimits/QuotaProgressBar.js | 127 - .../components/ProviderLimits/QuotaTable.js | 160 -- .../usage/components/ProviderLimits/index.js | 416 --- .../usage/components/ProviderLimits/utils.js | 201 -- .../usage/components/ProviderTopology.js | 266 -- .../usage/components/RequestDetailsTab.js | 425 ---- .../dashboard/usage/components/UsageChart.js | 141 -- .../dashboard/usage/components/UsageTable.js | 247 -- .../app/(dashboard)/dashboard/usage/page.js | 64 - 9router/src/app/(dashboard)/layout.js | 6 - 9router/src/app/api/auth/login/route.js | 54 - 9router/src/app/api/auth/logout/route.js | 8 - .../cli-tools/antigravity-mitm/alias/route.js | 51 - .../api/cli-tools/antigravity-mitm/route.js | 133 - .../api/cli-tools/claude-settings/route.js | 194 -- .../app/api/cli-tools/codex-settings/route.js | 218 -- .../api/cli-tools/copilot-settings/route.js | 148 -- .../app/api/cli-tools/droid-settings/route.js | 175 -- .../api/cli-tools/openclaw-settings/route.js | 180 -- .../api/cli-tools/opencode-settings/route.js | 153 -- 9router/src/app/api/cloud/auth/route.js | 50 - .../app/api/cloud/credentials/update/route.js | 57 - .../src/app/api/cloud/model/resolve/route.js | 50 - .../src/app/api/cloud/models/alias/route.js | 72 - 9router/src/app/api/combos/[id]/route.js | 71 - 9router/src/app/api/combos/route.js | 48 - 9router/src/app/api/init/route.js | 7 - 9router/src/app/api/keys/[id]/route.js | 58 - 9router/src/app/api/keys/route.js | 42 - 9router/src/app/api/locale/route.js | 30 - 9router/src/app/api/models/alias/route.js | 53 - 9router/src/app/api/models/route.js | 55 - 9router/src/app/api/models/test/route.js | 89 - .../api/oauth/[provider]/[action]/route.js | 170 -- .../app/api/oauth/cursor/auto-import/route.js | 186 -- .../src/app/api/oauth/cursor/import/route.js | 100 - .../src/app/api/oauth/iflow/cookie/route.js | 137 - .../app/api/oauth/kiro/auto-import/route.js | 85 - .../src/app/api/oauth/kiro/import/route.js | 56 - .../api/oauth/kiro/social-authorize/route.js | 43 - .../api/oauth/kiro/social-exchange/route.js | 67 - 9router/src/app/api/openswarm-config/route.js | 29 - 9router/src/app/api/pricing/route.js | 134 - .../src/app/api/provider-nodes/[id]/route.js | 93 - 9router/src/app/api/provider-nodes/route.js | 83 - .../app/api/provider-nodes/validate/route.js | 171 -- .../app/api/providers/[id]/models/route.js | 445 ---- 9router/src/app/api/providers/[id]/route.js | 189 -- .../api/providers/[id]/test-models/route.js | 111 - .../src/app/api/providers/[id]/test/route.js | 23 - .../app/api/providers/[id]/test/testUtils.js | 546 ---- 9router/src/app/api/providers/client/route.js | 20 - 9router/src/app/api/providers/route.js | 186 -- .../src/app/api/providers/test-batch/route.js | 131 - .../src/app/api/providers/validate/route.js | 255 -- 9router/src/app/api/proxy-pools/[id]/route.js | 118 - .../app/api/proxy-pools/[id]/test/route.js | 37 - 9router/src/app/api/proxy-pools/route.js | 90 - .../src/app/api/settings/database/route.js | 36 - .../src/app/api/settings/proxy-test/route.js | 23 - .../app/api/settings/require-login/route.js | 12 - 9router/src/app/api/settings/route.js | 74 - 9router/src/app/api/shutdown/route.js | 12 - 9router/src/app/api/tags/route.js | 18 - .../app/api/translator/console-logs/route.js | 24 - .../translator/console-logs/stream/route.js | 70 - 9router/src/app/api/translator/load/route.js | 45 - 9router/src/app/api/translator/save/route.js | 44 - 9router/src/app/api/translator/send/route.js | 58 - .../src/app/api/translator/translate/route.js | 90 - 9router/src/app/api/tunnel/disable/route.js | 12 - 9router/src/app/api/tunnel/enable/route.js | 16 - 9router/src/app/api/tunnel/status/route.js | 12 - .../src/app/api/usage/[connectionId]/route.js | 152 -- 9router/src/app/api/usage/chart/route.js | 21 - 9router/src/app/api/usage/history/route.js | 12 - 9router/src/app/api/usage/logs/route.js | 12 - 9router/src/app/api/usage/providers/route.js | 42 - .../app/api/usage/request-details/route.js | 57 - .../src/app/api/usage/request-logs/route.js | 13 - 9router/src/app/api/usage/stats/route.js | 23 - 9router/src/app/api/usage/stream/route.js | 81 - 9router/src/app/api/v1/api/chat/route.js | 38 - .../src/app/api/v1/chat/completions/route.js | 37 - 9router/src/app/api/v1/embeddings/route.js | 21 - .../app/api/v1/messages/count_tokens/route.js | 52 - 9router/src/app/api/v1/messages/route.js | 52 - 9router/src/app/api/v1/models/route.js | 149 -- 9router/src/app/api/v1/responses/route.js | 31 - 9router/src/app/api/v1/route.js | 32 - .../app/api/v1beta/models/[...path]/route.js | 329 --- 9router/src/app/api/v1beta/models/route.js | 44 - 9router/src/app/api/version/route.js | 45 - 9router/src/app/callback/page.js | 191 -- .../app/dashboard/settings/pricing/page.js | 173 -- 9router/src/app/favicon.ico | Bin 25931 -> 0 bytes 9router/src/app/globals.css | 219 -- .../landing/components/AnimatedBackground.js | 57 - .../src/app/landing/components/Features.js | 133 - .../app/landing/components/FlowAnimation.js | 121 - 9router/src/app/landing/components/Footer.js | 61 - .../src/app/landing/components/GetStarted.js | 99 - .../src/app/landing/components/HeroSection.js | 47 - .../src/app/landing/components/HowItWorks.js | 66 - .../src/app/landing/components/Navigation.js | 72 - 9router/src/app/landing/page.js | 106 - 9router/src/app/layout.js | 50 - 9router/src/app/login/page.js | 124 - 9router/src/app/manifest.js | 30 - 9router/src/app/page.js | 7 - 9router/src/dashboardGuard.js | 47 - 9router/src/i18n/RuntimeI18nProvider.js | 27 - 9router/src/i18n/config.js | 146 -- 9router/src/i18n/runtime.js | 162 -- 9router/src/lib/consoleLogBuffer.js | 79 - 9router/src/lib/initCloudSync.js | 22 - 9router/src/lib/localDb.js | 1112 -------- 9router/src/lib/network/connectionProxy.js | 58 - 9router/src/lib/network/initOutboundProxy.js | 22 - 9router/src/lib/network/outboundProxy.js | 68 - 9router/src/lib/network/proxyTest.js | 91 - 9router/src/lib/oauth/constants/oauth.js | 237 -- 9router/src/lib/oauth/providers.js | 1002 -------- 9router/src/lib/oauth/services/antigravity.js | 325 --- 9router/src/lib/oauth/services/claude.js | 136 - 9router/src/lib/oauth/services/codex.js | 145 -- 9router/src/lib/oauth/services/cursor.js | 179 -- 9router/src/lib/oauth/services/gemini.js | 240 -- 9router/src/lib/oauth/services/github.js | 225 -- 9router/src/lib/oauth/services/iflow.js | 202 -- 9router/src/lib/oauth/services/index.js | 16 - 9router/src/lib/oauth/services/kiro.js | 313 --- 9router/src/lib/oauth/services/oauth.js | 157 -- 9router/src/lib/oauth/services/openai.js | 123 - 9router/src/lib/oauth/services/qwen.js | 170 -- 9router/src/lib/oauth/utils/banner.js | 63 - 9router/src/lib/oauth/utils/pkce.js | 38 - 9router/src/lib/oauth/utils/server.js | 116 - 9router/src/lib/oauth/utils/ui.js | 48 - 9router/src/lib/requestDetailsDb.js | 278 -- 9router/src/lib/tunnel/cloudflared.js | 326 --- 9router/src/lib/tunnel/state.js | 53 - 9router/src/lib/tunnel/tunnelManager.js | 169 -- 9router/src/lib/usage/fetcher.js | 208 -- 9router/src/lib/usageDb.js | 809 ------ 9router/src/mitm/cert/generate.js | 32 - 9router/src/mitm/cert/install.js | 176 -- 9router/src/mitm/cert/rootCA.js | 173 -- 9router/src/mitm/config.js | 27 - 9router/src/mitm/dns/dnsConfig.js | 218 -- 9router/src/mitm/handlers/antigravity.js | 20 - 9router/src/mitm/handlers/base.js | 50 - 9router/src/mitm/handlers/copilot.js | 35 - 9router/src/mitm/handlers/cursor.js | 15 - 9router/src/mitm/handlers/kiro.js | 20 - 9router/src/mitm/logger.js | 8 - 9router/src/mitm/manager.js | 618 ----- 9router/src/mitm/paths.js | 16 - 9router/src/mitm/server.js | 233 -- 9router/src/models/index.js | 35 - 9router/src/proxy.js | 5 - 9router/src/server-init.js | 17 - 9router/src/shared/components/Avatar.js | 88 - 9router/src/shared/components/Badge.js | 55 - 9router/src/shared/components/Button.js | 56 - 9router/src/shared/components/Card.js | 122 - .../src/shared/components/CursorAuthModal.js | 215 -- 9router/src/shared/components/Drawer.js | 89 - 9router/src/shared/components/Footer.js | 132 - 9router/src/shared/components/Header.js | 153 -- .../src/shared/components/IFlowCookieModal.js | 132 - 9router/src/shared/components/Input.js | 69 - .../src/shared/components/KiroAuthModal.js | 383 --- .../src/shared/components/KiroOAuthWrapper.js | 103 - .../shared/components/KiroSocialOAuthModal.js | 205 -- .../src/shared/components/LanguageSwitcher.js | 180 -- 9router/src/shared/components/Loading.js | 77 - .../shared/components/ManualConfigModal.js | 46 - 9router/src/shared/components/Modal.js | 143 -- .../src/shared/components/ModelSelectModal.js | 356 --- 9router/src/shared/components/OAuthModal.js | 470 ---- 9router/src/shared/components/Pagination.js | 146 -- 9router/src/shared/components/PricingModal.js | 208 -- .../src/shared/components/RequestLogger.js | 121 - .../src/shared/components/SegmentedControl.js | 48 - 9router/src/shared/components/Select.js | 70 - 9router/src/shared/components/Sidebar.js | 273 -- .../src/shared/components/ThemeProvider.js | 15 - 9router/src/shared/components/ThemeToggle.js | 47 - 9router/src/shared/components/Toggle.js | 90 - 9router/src/shared/components/Tooltip.js | 19 - 9router/src/shared/components/UsageStats.js | 465 ---- 9router/src/shared/components/index.js | 33 - .../shared/components/layouts/AuthLayout.js | 29 - .../components/layouts/DashboardLayout.js | 102 - .../src/shared/components/layouts/index.js | 4 - 9router/src/shared/constants/cliTools.js | 249 -- 9router/src/shared/constants/colors.js | 77 - 9router/src/shared/constants/config.js | 67 - 9router/src/shared/constants/index.js | 4 - 9router/src/shared/constants/models.js | 35 - 9router/src/shared/constants/pricing.js | 876 ------- 9router/src/shared/constants/providers.js | 120 - 9router/src/shared/hooks/index.js | 2 - .../src/shared/hooks/useCopyToClipboard.js | 29 - 9router/src/shared/hooks/useTheme.js | 60 - .../src/shared/services/cloudSyncScheduler.js | 122 - 9router/src/shared/services/initializeApp.js | 228 -- .../shared/services/initializeCloudSync.js | 37 - 9router/src/shared/utils/api.js | 93 - 9router/src/shared/utils/apiKey.js | 98 - 9router/src/shared/utils/clineAuth.js | 37 - 9router/src/shared/utils/cloud.js | 40 - 9router/src/shared/utils/cn.js | 11 - 9router/src/shared/utils/index.js | 40 - 9router/src/shared/utils/machine.js | 18 - 9router/src/shared/utils/machineId.js | 58 - 9router/src/sse/handlers/chat.js | 215 -- 9router/src/sse/handlers/embeddings.js | 142 -- 9router/src/sse/services/auth.js | 267 -- 9router/src/sse/services/model.js | 68 - 9router/src/sse/services/tokenRefresh.js | 288 --- 9router/src/sse/utils/logger.js | 75 - 9router/src/store/index.js | 6 - 9router/src/store/notificationStore.js | 45 - 9router/src/store/providerStore.js | 48 - 9router/src/store/themeStore.js | 54 - 9router/src/store/userStore.js | 20 - 9router/tester/translator/testFromFile.js | 148 -- 9router/tests/.gitignore | 4 - 9router/tests/README.md | 51 - 9router/tests/package.json | 17 - 9router/tests/unit/embeddings.cloud.test.js | 524 ---- 9router/tests/unit/embeddingsCore.test.js | 586 ----- .../unit/oauth-cursor-auto-import.test.js | 184 -- 9router/tests/unit/openai-to-claude.test.js | 124 - .../tests/unit/provider-validation.test.js | 271 -- .../translator-request-normalization.test.js | 118 - 9router/tests/vitest.config.js | 21 - backend/apps/agents/agent_manager.py | 277 +- backend/apps/agents/agents.py | 27 +- backend/apps/agents/anthropic_proxy.py | 176 ++ backend/apps/agents/providers/registry.py | 126 +- backend/apps/agents/tools/web.py | 25 +- backend/apps/agents/web_mcp_server.py | 199 ++ backend/apps/nine_router.py | 295 ++- backend/apps/outputs/outputs.py | 2 +- backend/apps/settings/credentials.py | 8 +- backend/apps/settings/models.py | 4 - backend/apps/settings/settings.py | 48 +- backend/apps/web/__init__.py | 0 backend/apps/web/web.py | 388 +++ backend/main.py | 4 +- backend/requirements.txt | 3 +- electron/main.js | 29 +- electron/package-lock.json | 4 +- electron/package.json | 4 +- electron/preload.js | 11 + .../src/app/components/OnboardingModal.tsx | 95 +- .../src/app/pages/AgentChat/ChatInput.tsx | 16 + .../app/pages/AgentChat/ToolCallBubble.tsx | 33 +- frontend/src/app/pages/Settings/Settings.tsx | 71 +- scripts/build-app-win.ps1 | 51 +- scripts/build-app.sh | 50 +- scripts/fetch-router.ps1 | 36 + scripts/fetch-router.sh | 32 + 510 files changed, 1736 insertions(+), 73260 deletions(-) delete mode 100644 9router/.dockerignore delete mode 100644 9router/.env.example delete mode 100644 9router/.github/dependabot.yml delete mode 100644 9router/.github/workflows/docker-publish.yml delete mode 100644 9router/.gitignore delete mode 100644 9router/.gitmodules delete mode 100644 9router/.npmignore delete mode 100644 9router/.vscode/settings.json delete mode 100644 9router/CHANGELOG.md delete mode 100644 9router/Dockerfile delete mode 100644 9router/LICENSE delete mode 100644 9router/README.md delete mode 100644 9router/cloud/.gitignore delete mode 100644 9router/cloud/README.md delete mode 100644 9router/cloud/jsconfig.json delete mode 100644 9router/cloud/migrations/0001_init.sql delete mode 100644 9router/cloud/package.json delete mode 100644 9router/cloud/src/handlers/cache.js delete mode 100644 9router/cloud/src/handlers/chat.js delete mode 100644 9router/cloud/src/handlers/cleanup.js delete mode 100644 9router/cloud/src/handlers/countTokens.js delete mode 100644 9router/cloud/src/handlers/embeddings.js delete mode 100644 9router/cloud/src/handlers/forward.js delete mode 100644 9router/cloud/src/handlers/forwardRaw.js delete mode 100644 9router/cloud/src/handlers/sync.js delete mode 100644 9router/cloud/src/handlers/verify.js delete mode 100644 9router/cloud/src/index.js delete mode 100644 9router/cloud/src/services/landingPage.js delete mode 100644 9router/cloud/src/services/storage.js delete mode 100644 9router/cloud/src/services/tokenRefresh.js delete mode 100644 9router/cloud/src/stubs/usageDb.js delete mode 100644 9router/cloud/src/utils/apiKey.js delete mode 100644 9router/cloud/src/utils/logger.js delete mode 100644 9router/cloud/wrangler.toml delete mode 100644 9router/docs/ARCHITECTURE.md delete mode 100644 9router/eslint.config.mjs delete mode 100644 9router/i18n/README.vi.md delete mode 100644 9router/i18n/README.zh-CN.md delete mode 100644 9router/images/9router.png delete mode 100644 9router/jsconfig.json delete mode 100644 9router/next.config.mjs delete mode 100644 9router/open-sse/.npmignore delete mode 100644 9router/open-sse/config/appConstants.js delete mode 100644 9router/open-sse/config/codexInstructions.js delete mode 100644 9router/open-sse/config/constants.js delete mode 100644 9router/open-sse/config/defaultThinkingSignature.js delete mode 100644 9router/open-sse/config/ollamaModels.js delete mode 100644 9router/open-sse/config/providerModels.js delete mode 100644 9router/open-sse/config/providers.js delete mode 100644 9router/open-sse/config/runtimeConfig.js delete mode 100644 9router/open-sse/executors/antigravity.js delete mode 100644 9router/open-sse/executors/base.js delete mode 100644 9router/open-sse/executors/codex.js delete mode 100644 9router/open-sse/executors/cursor.js delete mode 100644 9router/open-sse/executors/default.js delete mode 100644 9router/open-sse/executors/gemini-cli.js delete mode 100644 9router/open-sse/executors/github.js delete mode 100644 9router/open-sse/executors/iflow.js delete mode 100644 9router/open-sse/executors/index.js delete mode 100644 9router/open-sse/executors/kiro.js delete mode 100644 9router/open-sse/executors/vertex.js delete mode 100644 9router/open-sse/handlers/chatCore.js delete mode 100644 9router/open-sse/handlers/chatCore/nonStreamingHandler.js delete mode 100644 9router/open-sse/handlers/chatCore/requestDetail.js delete mode 100644 9router/open-sse/handlers/chatCore/sseToJsonHandler.js delete mode 100644 9router/open-sse/handlers/chatCore/streamingHandler.js delete mode 100644 9router/open-sse/handlers/embeddingsCore.js delete mode 100644 9router/open-sse/handlers/responsesHandler.js delete mode 100644 9router/open-sse/index.js delete mode 100644 9router/open-sse/services/accountFallback.js delete mode 100644 9router/open-sse/services/combo.js delete mode 100644 9router/open-sse/services/compact.js delete mode 100644 9router/open-sse/services/model.js delete mode 100644 9router/open-sse/services/projectId.js delete mode 100644 9router/open-sse/services/provider.js delete mode 100644 9router/open-sse/services/tokenRefresh.js delete mode 100644 9router/open-sse/services/usage.js delete mode 100644 9router/open-sse/transformer/responsesTransformer.js delete mode 100644 9router/open-sse/transformer/streamToJsonConverter.js delete mode 100644 9router/open-sse/translator/formats.js delete mode 100644 9router/open-sse/translator/helpers/claudeHelper.js delete mode 100644 9router/open-sse/translator/helpers/geminiHelper.js delete mode 100644 9router/open-sse/translator/helpers/maxTokensHelper.js delete mode 100644 9router/open-sse/translator/helpers/openaiHelper.js delete mode 100644 9router/open-sse/translator/helpers/responsesApiHelper.js delete mode 100644 9router/open-sse/translator/helpers/toolCallHelper.js delete mode 100644 9router/open-sse/translator/index.js delete mode 100644 9router/open-sse/translator/request/antigravity-to-openai.js delete mode 100644 9router/open-sse/translator/request/claude-to-openai.js delete mode 100644 9router/open-sse/translator/request/gemini-to-openai.js delete mode 100644 9router/open-sse/translator/request/openai-responses.js delete mode 100644 9router/open-sse/translator/request/openai-to-claude.js delete mode 100644 9router/open-sse/translator/request/openai-to-cursor.js delete mode 100644 9router/open-sse/translator/request/openai-to-gemini.js delete mode 100644 9router/open-sse/translator/request/openai-to-kiro.js delete mode 100644 9router/open-sse/translator/request/openai-to-kiro.old.js delete mode 100644 9router/open-sse/translator/request/openai-to-ollama.js delete mode 100644 9router/open-sse/translator/response/claude-to-openai.js delete mode 100644 9router/open-sse/translator/response/cursor-to-openai.js delete mode 100644 9router/open-sse/translator/response/gemini-to-openai.js delete mode 100644 9router/open-sse/translator/response/kiro-to-openai.js delete mode 100644 9router/open-sse/translator/response/ollama-to-openai.js delete mode 100644 9router/open-sse/translator/response/openai-responses.js delete mode 100644 9router/open-sse/translator/response/openai-to-antigravity.js delete mode 100644 9router/open-sse/translator/response/openai-to-claude.js delete mode 100644 9router/open-sse/utils/bypassHandler.js delete mode 100644 9router/open-sse/utils/claudeCloaking.js delete mode 100644 9router/open-sse/utils/cursorChecksum.js delete mode 100644 9router/open-sse/utils/cursorProtobuf.js delete mode 100644 9router/open-sse/utils/error.js delete mode 100644 9router/open-sse/utils/ollamaTransform.js delete mode 100644 9router/open-sse/utils/proxyFetch.js delete mode 100644 9router/open-sse/utils/requestLogger.js delete mode 100644 9router/open-sse/utils/sessionManager.js delete mode 100644 9router/open-sse/utils/stream.js delete mode 100644 9router/open-sse/utils/streamHandler.js delete mode 100644 9router/open-sse/utils/streamHelpers.js delete mode 100644 9router/open-sse/utils/usageTracking.js delete mode 100644 9router/package.json delete mode 100644 9router/postcss.config.mjs delete mode 100644 9router/public/favicon.svg delete mode 100644 9router/public/file.svg delete mode 100644 9router/public/globe.svg delete mode 100644 9router/public/i18n/literals/ar.json delete mode 100644 9router/public/i18n/literals/bn.json delete mode 100644 9router/public/i18n/literals/cs.json delete mode 100644 9router/public/i18n/literals/da.json delete mode 100644 9router/public/i18n/literals/de.json delete mode 100644 9router/public/i18n/literals/el.json delete mode 100644 9router/public/i18n/literals/es.json delete mode 100644 9router/public/i18n/literals/fi.json delete mode 100644 9router/public/i18n/literals/fr.json delete mode 100644 9router/public/i18n/literals/he.json delete mode 100644 9router/public/i18n/literals/hi.json delete mode 100644 9router/public/i18n/literals/hu.json delete mode 100644 9router/public/i18n/literals/id.json delete mode 100644 9router/public/i18n/literals/it.json delete mode 100644 9router/public/i18n/literals/ja.json delete mode 100644 9router/public/i18n/literals/ko.json delete mode 100644 9router/public/i18n/literals/nl.json delete mode 100644 9router/public/i18n/literals/no.json delete mode 100644 9router/public/i18n/literals/pl.json delete mode 100644 9router/public/i18n/literals/pt-BR.json delete mode 100644 9router/public/i18n/literals/pt-PT.json delete mode 100644 9router/public/i18n/literals/ro.json delete mode 100644 9router/public/i18n/literals/ru.json delete mode 100644 9router/public/i18n/literals/sv.json delete mode 100644 9router/public/i18n/literals/th.json delete mode 100644 9router/public/i18n/literals/tl.json delete mode 100644 9router/public/i18n/literals/tr.json delete mode 100644 9router/public/i18n/literals/uk.json delete mode 100644 9router/public/i18n/literals/ur.json delete mode 100644 9router/public/i18n/literals/vi.json delete mode 100644 9router/public/i18n/literals/zh-CN.json delete mode 100644 9router/public/i18n/literals/zh-TW.json delete mode 100644 9router/public/icons/icon-192.svg delete mode 100644 9router/public/icons/icon-512.svg delete mode 100644 9router/public/next.svg delete mode 100644 9router/public/providers/alicode-intl.png delete mode 100644 9router/public/providers/alicode.png delete mode 100644 9router/public/providers/anthropic-m.png delete mode 100644 9router/public/providers/anthropic.png delete mode 100644 9router/public/providers/antigravity.png delete mode 100644 9router/public/providers/cerebras.png delete mode 100644 9router/public/providers/chutes.png delete mode 100644 9router/public/providers/claude.png delete mode 100644 9router/public/providers/cline.png delete mode 100644 9router/public/providers/codex.png delete mode 100644 9router/public/providers/cohere.png delete mode 100644 9router/public/providers/continue.png delete mode 100644 9router/public/providers/copilot.png delete mode 100644 9router/public/providers/cursor.png delete mode 100644 9router/public/providers/deepseek.png delete mode 100644 9router/public/providers/droid.png delete mode 100644 9router/public/providers/fireworks.png delete mode 100644 9router/public/providers/gemini-cli.png delete mode 100644 9router/public/providers/gemini.png delete mode 100644 9router/public/providers/github.png delete mode 100644 9router/public/providers/glm-cn.png delete mode 100644 9router/public/providers/glm.png delete mode 100644 9router/public/providers/groq.png delete mode 100644 9router/public/providers/iflow.png delete mode 100644 9router/public/providers/kilocode.png delete mode 100644 9router/public/providers/kimi-coding.png delete mode 100644 9router/public/providers/kimi.png delete mode 100644 9router/public/providers/kiro.png delete mode 100644 9router/public/providers/minimax-cn.png delete mode 100644 9router/public/providers/minimax.png delete mode 100644 9router/public/providers/mistral.png delete mode 100644 9router/public/providers/nebius.png delete mode 100644 9router/public/providers/nvidia.png delete mode 100644 9router/public/providers/oai-cc.png delete mode 100644 9router/public/providers/oai-r.png delete mode 100644 9router/public/providers/ollama-local.png delete mode 100644 9router/public/providers/ollama.png delete mode 100644 9router/public/providers/openai.png delete mode 100644 9router/public/providers/openclaw.png delete mode 100644 9router/public/providers/opencode.png delete mode 100644 9router/public/providers/openrouter.png delete mode 100644 9router/public/providers/perplexity.png delete mode 100644 9router/public/providers/qwen.png delete mode 100644 9router/public/providers/roo.png delete mode 100644 9router/public/providers/siliconflow.png delete mode 100644 9router/public/providers/together.png delete mode 100644 9router/public/providers/vertex-partner.png delete mode 100644 9router/public/providers/vertex.png delete mode 100644 9router/public/providers/xai.png delete mode 100644 9router/public/sw.js delete mode 100644 9router/public/vercel.svg delete mode 100644 9router/public/window.svg delete mode 100755 9router/scripts/translate-readme.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/AntigravityToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/components/index.js delete mode 100644 9router/src/app/(dashboard)/dashboard/cli-tools/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/combos/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/console-log/ConsoleLogClient.js delete mode 100644 9router/src/app/(dashboard)/dashboard/console-log/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js delete mode 100644 9router/src/app/(dashboard)/dashboard/endpoint/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js delete mode 100644 9router/src/app/(dashboard)/dashboard/mitm/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/profile/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/providers/[id]/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/providers/[id]/page.new.js delete mode 100644 9router/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js delete mode 100644 9router/src/app/(dashboard)/dashboard/providers/new/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/providers/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/proxy-pools/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/quota/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/translator/page.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/OverviewCards.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/UsageChart.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/components/UsageTable.js delete mode 100644 9router/src/app/(dashboard)/dashboard/usage/page.js delete mode 100644 9router/src/app/(dashboard)/layout.js delete mode 100644 9router/src/app/api/auth/login/route.js delete mode 100644 9router/src/app/api/auth/logout/route.js delete mode 100644 9router/src/app/api/cli-tools/antigravity-mitm/alias/route.js delete mode 100644 9router/src/app/api/cli-tools/antigravity-mitm/route.js delete mode 100644 9router/src/app/api/cli-tools/claude-settings/route.js delete mode 100644 9router/src/app/api/cli-tools/codex-settings/route.js delete mode 100644 9router/src/app/api/cli-tools/copilot-settings/route.js delete mode 100644 9router/src/app/api/cli-tools/droid-settings/route.js delete mode 100644 9router/src/app/api/cli-tools/openclaw-settings/route.js delete mode 100644 9router/src/app/api/cli-tools/opencode-settings/route.js delete mode 100644 9router/src/app/api/cloud/auth/route.js delete mode 100644 9router/src/app/api/cloud/credentials/update/route.js delete mode 100644 9router/src/app/api/cloud/model/resolve/route.js delete mode 100644 9router/src/app/api/cloud/models/alias/route.js delete mode 100644 9router/src/app/api/combos/[id]/route.js delete mode 100644 9router/src/app/api/combos/route.js delete mode 100644 9router/src/app/api/init/route.js delete mode 100644 9router/src/app/api/keys/[id]/route.js delete mode 100644 9router/src/app/api/keys/route.js delete mode 100644 9router/src/app/api/locale/route.js delete mode 100644 9router/src/app/api/models/alias/route.js delete mode 100644 9router/src/app/api/models/route.js delete mode 100644 9router/src/app/api/models/test/route.js delete mode 100644 9router/src/app/api/oauth/[provider]/[action]/route.js delete mode 100644 9router/src/app/api/oauth/cursor/auto-import/route.js delete mode 100644 9router/src/app/api/oauth/cursor/import/route.js delete mode 100644 9router/src/app/api/oauth/iflow/cookie/route.js delete mode 100644 9router/src/app/api/oauth/kiro/auto-import/route.js delete mode 100644 9router/src/app/api/oauth/kiro/import/route.js delete mode 100644 9router/src/app/api/oauth/kiro/social-authorize/route.js delete mode 100644 9router/src/app/api/oauth/kiro/social-exchange/route.js delete mode 100644 9router/src/app/api/openswarm-config/route.js delete mode 100644 9router/src/app/api/pricing/route.js delete mode 100644 9router/src/app/api/provider-nodes/[id]/route.js delete mode 100644 9router/src/app/api/provider-nodes/route.js delete mode 100644 9router/src/app/api/provider-nodes/validate/route.js delete mode 100644 9router/src/app/api/providers/[id]/models/route.js delete mode 100644 9router/src/app/api/providers/[id]/route.js delete mode 100644 9router/src/app/api/providers/[id]/test-models/route.js delete mode 100644 9router/src/app/api/providers/[id]/test/route.js delete mode 100644 9router/src/app/api/providers/[id]/test/testUtils.js delete mode 100644 9router/src/app/api/providers/client/route.js delete mode 100644 9router/src/app/api/providers/route.js delete mode 100644 9router/src/app/api/providers/test-batch/route.js delete mode 100644 9router/src/app/api/providers/validate/route.js delete mode 100644 9router/src/app/api/proxy-pools/[id]/route.js delete mode 100644 9router/src/app/api/proxy-pools/[id]/test/route.js delete mode 100644 9router/src/app/api/proxy-pools/route.js delete mode 100644 9router/src/app/api/settings/database/route.js delete mode 100644 9router/src/app/api/settings/proxy-test/route.js delete mode 100644 9router/src/app/api/settings/require-login/route.js delete mode 100644 9router/src/app/api/settings/route.js delete mode 100644 9router/src/app/api/shutdown/route.js delete mode 100644 9router/src/app/api/tags/route.js delete mode 100644 9router/src/app/api/translator/console-logs/route.js delete mode 100644 9router/src/app/api/translator/console-logs/stream/route.js delete mode 100644 9router/src/app/api/translator/load/route.js delete mode 100644 9router/src/app/api/translator/save/route.js delete mode 100644 9router/src/app/api/translator/send/route.js delete mode 100644 9router/src/app/api/translator/translate/route.js delete mode 100644 9router/src/app/api/tunnel/disable/route.js delete mode 100644 9router/src/app/api/tunnel/enable/route.js delete mode 100644 9router/src/app/api/tunnel/status/route.js delete mode 100644 9router/src/app/api/usage/[connectionId]/route.js delete mode 100644 9router/src/app/api/usage/chart/route.js delete mode 100644 9router/src/app/api/usage/history/route.js delete mode 100644 9router/src/app/api/usage/logs/route.js delete mode 100644 9router/src/app/api/usage/providers/route.js delete mode 100644 9router/src/app/api/usage/request-details/route.js delete mode 100644 9router/src/app/api/usage/request-logs/route.js delete mode 100644 9router/src/app/api/usage/stats/route.js delete mode 100644 9router/src/app/api/usage/stream/route.js delete mode 100644 9router/src/app/api/v1/api/chat/route.js delete mode 100644 9router/src/app/api/v1/chat/completions/route.js delete mode 100644 9router/src/app/api/v1/embeddings/route.js delete mode 100644 9router/src/app/api/v1/messages/count_tokens/route.js delete mode 100644 9router/src/app/api/v1/messages/route.js delete mode 100644 9router/src/app/api/v1/models/route.js delete mode 100644 9router/src/app/api/v1/responses/route.js delete mode 100644 9router/src/app/api/v1/route.js delete mode 100644 9router/src/app/api/v1beta/models/[...path]/route.js delete mode 100644 9router/src/app/api/v1beta/models/route.js delete mode 100644 9router/src/app/api/version/route.js delete mode 100644 9router/src/app/callback/page.js delete mode 100644 9router/src/app/dashboard/settings/pricing/page.js delete mode 100644 9router/src/app/favicon.ico delete mode 100644 9router/src/app/globals.css delete mode 100644 9router/src/app/landing/components/AnimatedBackground.js delete mode 100644 9router/src/app/landing/components/Features.js delete mode 100644 9router/src/app/landing/components/FlowAnimation.js delete mode 100644 9router/src/app/landing/components/Footer.js delete mode 100644 9router/src/app/landing/components/GetStarted.js delete mode 100644 9router/src/app/landing/components/HeroSection.js delete mode 100644 9router/src/app/landing/components/HowItWorks.js delete mode 100644 9router/src/app/landing/components/Navigation.js delete mode 100644 9router/src/app/landing/page.js delete mode 100644 9router/src/app/layout.js delete mode 100644 9router/src/app/login/page.js delete mode 100644 9router/src/app/manifest.js delete mode 100644 9router/src/app/page.js delete mode 100644 9router/src/dashboardGuard.js delete mode 100644 9router/src/i18n/RuntimeI18nProvider.js delete mode 100644 9router/src/i18n/config.js delete mode 100644 9router/src/i18n/runtime.js delete mode 100644 9router/src/lib/consoleLogBuffer.js delete mode 100644 9router/src/lib/initCloudSync.js delete mode 100644 9router/src/lib/localDb.js delete mode 100644 9router/src/lib/network/connectionProxy.js delete mode 100644 9router/src/lib/network/initOutboundProxy.js delete mode 100644 9router/src/lib/network/outboundProxy.js delete mode 100644 9router/src/lib/network/proxyTest.js delete mode 100644 9router/src/lib/oauth/constants/oauth.js delete mode 100644 9router/src/lib/oauth/providers.js delete mode 100644 9router/src/lib/oauth/services/antigravity.js delete mode 100644 9router/src/lib/oauth/services/claude.js delete mode 100644 9router/src/lib/oauth/services/codex.js delete mode 100644 9router/src/lib/oauth/services/cursor.js delete mode 100644 9router/src/lib/oauth/services/gemini.js delete mode 100644 9router/src/lib/oauth/services/github.js delete mode 100644 9router/src/lib/oauth/services/iflow.js delete mode 100644 9router/src/lib/oauth/services/index.js delete mode 100644 9router/src/lib/oauth/services/kiro.js delete mode 100644 9router/src/lib/oauth/services/oauth.js delete mode 100644 9router/src/lib/oauth/services/openai.js delete mode 100644 9router/src/lib/oauth/services/qwen.js delete mode 100644 9router/src/lib/oauth/utils/banner.js delete mode 100644 9router/src/lib/oauth/utils/pkce.js delete mode 100644 9router/src/lib/oauth/utils/server.js delete mode 100644 9router/src/lib/oauth/utils/ui.js delete mode 100644 9router/src/lib/requestDetailsDb.js delete mode 100644 9router/src/lib/tunnel/cloudflared.js delete mode 100644 9router/src/lib/tunnel/state.js delete mode 100644 9router/src/lib/tunnel/tunnelManager.js delete mode 100644 9router/src/lib/usage/fetcher.js delete mode 100644 9router/src/lib/usageDb.js delete mode 100644 9router/src/mitm/cert/generate.js delete mode 100644 9router/src/mitm/cert/install.js delete mode 100644 9router/src/mitm/cert/rootCA.js delete mode 100644 9router/src/mitm/config.js delete mode 100644 9router/src/mitm/dns/dnsConfig.js delete mode 100644 9router/src/mitm/handlers/antigravity.js delete mode 100644 9router/src/mitm/handlers/base.js delete mode 100644 9router/src/mitm/handlers/copilot.js delete mode 100644 9router/src/mitm/handlers/cursor.js delete mode 100644 9router/src/mitm/handlers/kiro.js delete mode 100644 9router/src/mitm/logger.js delete mode 100644 9router/src/mitm/manager.js delete mode 100644 9router/src/mitm/paths.js delete mode 100644 9router/src/mitm/server.js delete mode 100644 9router/src/models/index.js delete mode 100644 9router/src/proxy.js delete mode 100644 9router/src/server-init.js delete mode 100644 9router/src/shared/components/Avatar.js delete mode 100644 9router/src/shared/components/Badge.js delete mode 100644 9router/src/shared/components/Button.js delete mode 100644 9router/src/shared/components/Card.js delete mode 100644 9router/src/shared/components/CursorAuthModal.js delete mode 100644 9router/src/shared/components/Drawer.js delete mode 100644 9router/src/shared/components/Footer.js delete mode 100644 9router/src/shared/components/Header.js delete mode 100644 9router/src/shared/components/IFlowCookieModal.js delete mode 100644 9router/src/shared/components/Input.js delete mode 100644 9router/src/shared/components/KiroAuthModal.js delete mode 100644 9router/src/shared/components/KiroOAuthWrapper.js delete mode 100644 9router/src/shared/components/KiroSocialOAuthModal.js delete mode 100644 9router/src/shared/components/LanguageSwitcher.js delete mode 100644 9router/src/shared/components/Loading.js delete mode 100644 9router/src/shared/components/ManualConfigModal.js delete mode 100644 9router/src/shared/components/Modal.js delete mode 100644 9router/src/shared/components/ModelSelectModal.js delete mode 100644 9router/src/shared/components/OAuthModal.js delete mode 100644 9router/src/shared/components/Pagination.js delete mode 100644 9router/src/shared/components/PricingModal.js delete mode 100644 9router/src/shared/components/RequestLogger.js delete mode 100644 9router/src/shared/components/SegmentedControl.js delete mode 100644 9router/src/shared/components/Select.js delete mode 100644 9router/src/shared/components/Sidebar.js delete mode 100644 9router/src/shared/components/ThemeProvider.js delete mode 100644 9router/src/shared/components/ThemeToggle.js delete mode 100644 9router/src/shared/components/Toggle.js delete mode 100644 9router/src/shared/components/Tooltip.js delete mode 100644 9router/src/shared/components/UsageStats.js delete mode 100644 9router/src/shared/components/index.js delete mode 100644 9router/src/shared/components/layouts/AuthLayout.js delete mode 100644 9router/src/shared/components/layouts/DashboardLayout.js delete mode 100644 9router/src/shared/components/layouts/index.js delete mode 100644 9router/src/shared/constants/cliTools.js delete mode 100644 9router/src/shared/constants/colors.js delete mode 100644 9router/src/shared/constants/config.js delete mode 100644 9router/src/shared/constants/index.js delete mode 100644 9router/src/shared/constants/models.js delete mode 100644 9router/src/shared/constants/pricing.js delete mode 100644 9router/src/shared/constants/providers.js delete mode 100644 9router/src/shared/hooks/index.js delete mode 100644 9router/src/shared/hooks/useCopyToClipboard.js delete mode 100644 9router/src/shared/hooks/useTheme.js delete mode 100644 9router/src/shared/services/cloudSyncScheduler.js delete mode 100644 9router/src/shared/services/initializeApp.js delete mode 100644 9router/src/shared/services/initializeCloudSync.js delete mode 100644 9router/src/shared/utils/api.js delete mode 100644 9router/src/shared/utils/apiKey.js delete mode 100644 9router/src/shared/utils/clineAuth.js delete mode 100644 9router/src/shared/utils/cloud.js delete mode 100644 9router/src/shared/utils/cn.js delete mode 100644 9router/src/shared/utils/index.js delete mode 100644 9router/src/shared/utils/machine.js delete mode 100644 9router/src/shared/utils/machineId.js delete mode 100644 9router/src/sse/handlers/chat.js delete mode 100644 9router/src/sse/handlers/embeddings.js delete mode 100644 9router/src/sse/services/auth.js delete mode 100644 9router/src/sse/services/model.js delete mode 100644 9router/src/sse/services/tokenRefresh.js delete mode 100644 9router/src/sse/utils/logger.js delete mode 100644 9router/src/store/index.js delete mode 100644 9router/src/store/notificationStore.js delete mode 100644 9router/src/store/providerStore.js delete mode 100644 9router/src/store/themeStore.js delete mode 100644 9router/src/store/userStore.js delete mode 100755 9router/tester/translator/testFromFile.js delete mode 100644 9router/tests/.gitignore delete mode 100644 9router/tests/README.md delete mode 100644 9router/tests/package.json delete mode 100644 9router/tests/unit/embeddings.cloud.test.js delete mode 100644 9router/tests/unit/embeddingsCore.test.js delete mode 100644 9router/tests/unit/oauth-cursor-auto-import.test.js delete mode 100644 9router/tests/unit/openai-to-claude.test.js delete mode 100644 9router/tests/unit/provider-validation.test.js delete mode 100644 9router/tests/unit/translator-request-normalization.test.js delete mode 100644 9router/tests/vitest.config.js create mode 100644 backend/apps/agents/anthropic_proxy.py create mode 100755 backend/apps/agents/web_mcp_server.py create mode 100644 backend/apps/web/__init__.py create mode 100644 backend/apps/web/web.py create mode 100644 scripts/fetch-router.ps1 create mode 100755 scripts/fetch-router.sh diff --git a/.gitignore b/.gitignore index 1dafc777..81fa781f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,8 @@ electron/build-staging/ electron/node_modules/ electron/package-lock.json -# 9Router build artifacts -9router/node_modules/ -9router/.next/ -9router/package-lock.json +# Router is fetched from npm at build time into electron/build-staging/router. +# No local router/ directory is tracked. # Pre-installed MCP server dependencies backend/npm-servers/*/node_modules/ diff --git a/9router/.dockerignore b/9router/.dockerignore deleted file mode 100644 index 5b921cd9..00000000 --- a/9router/.dockerignore +++ /dev/null @@ -1,32 +0,0 @@ -# VCS -.git -**/.git - -# Editor -.vscode -**/.vscode - -# Dependencies and build output -node_modules -.next -out -build -dist -coverage - -# Runtime data and logs -data -logs - -# Local env files (inject at runtime via --env-file or -e) -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# Debug logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* diff --git a/9router/.env.example b/9router/.env.example deleted file mode 100644 index afac229a..00000000 --- a/9router/.env.example +++ /dev/null @@ -1,38 +0,0 @@ -# 9Router environment contract -# This file reflects actual runtime usage in the current codebase. - -# Required -JWT_SECRET=change-me-to-a-long-random-secret -INITIAL_PASSWORD=change-me -DATA_DIR=/var/lib/9router - -# Recommended runtime variables -PORT=20128 -NODE_ENV=production - -# Recommended security and ops variables -API_KEY_SECRET=endpoint-proxy-api-key-secret -MACHINE_ID_SALT=endpoint-proxy-salt -ENABLE_REQUEST_LOGS=false -OBSERVABILITY_ENABLED=true -AUTH_COOKIE_SECURE=false -REQUIRE_API_KEY=false - -# Cloud sync variables -# Must point to this running instance so internal sync jobs can call /api/sync/cloud. -# Server-side preferred variables: -BASE_URL=http://localhost:20128 -CLOUD_URL=https://9router.com -# Backward-compatible/public variables: -NEXT_PUBLIC_BASE_URL=http://localhost:20128 -NEXT_PUBLIC_CLOUD_URL=https://9router.com - -# Optional outbound proxy variables for upstream provider calls -# Lowercase variants are also supported: http_proxy, https_proxy, all_proxy, no_proxy -# HTTP_PROXY=http://127.0.0.1:7890 -# HTTPS_PROXY=http://127.0.0.1:7890 -# ALL_PROXY=socks5://127.0.0.1:7890 -# NO_PROXY=localhost,127.0.0.1 - -# Currently unused by application runtime (kept as reference) -# INSTANCE_NAME=9router diff --git a/9router/.github/dependabot.yml b/9router/.github/dependabot.yml deleted file mode 100644 index e180032a..00000000 --- a/9router/.github/dependabot.yml +++ /dev/null @@ -1,2 +0,0 @@ -version: 2 -updates: [] diff --git a/9router/.github/workflows/docker-publish.yml b/9router/.github/workflows/docker-publish.yml deleted file mode 100644 index 9bc8b42c..00000000 --- a/9router/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Build and Push Docker Image - -on: - push: - tags: - - 'v*' - # Uncomment to also build on master pushes (rolling latest). - # Pair with concurrency + paths below to avoid excessive builds. - # branches: - # - master - # paths: - # - 'src/**' - # - 'open-sse/**' - # - 'public/**' - # - 'package*.json' - # - 'next.config.*' - # - 'Dockerfile' - workflow_dispatch: - -# Uncomment if re-enabling master push trigger. -# concurrency: -# group: docker-${{ github.ref }} -# cancel-in-progress: true - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v4 - - - uses: docker/setup-buildx-action@v3 - - - name: Log in to Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=sha,prefix= - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max - platforms: linux/amd64 - provenance: false - sbom: false diff --git a/9router/.gitignore b/9router/.gitignore deleted file mode 100644 index 78ff5bc4..00000000 --- a/9router/.gitignore +++ /dev/null @@ -1,70 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* -!.env.example - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -.bin/* -data/ -logs/* -source/* -.cursor/* -docs/* -!docs/ARCHITECTURE.md -test/* -bin/* -open-sse/test/* -RM.vn.md -RM.md -cursor/* -PUBLIC.md -Thanks.md -PUBLIC.en.md -PR/* -package-lock.json - - -#Ignore vscode AI rules -.github/instructions/codacy.instructions.md -README1.md -deploy.sh -ecosystem.config.* -start.sh - diff --git a/9router/.gitmodules b/9router/.gitmodules deleted file mode 100644 index b5240b4d..00000000 --- a/9router/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/mitm/dev"] - path = src/mitm/dev - url = https://github.com/decolua/9router-dev.git diff --git a/9router/.npmignore b/9router/.npmignore deleted file mode 100644 index 9bec212d..00000000 --- a/9router/.npmignore +++ /dev/null @@ -1,60 +0,0 @@ -# Database files - NEVER publish -data/ -**/data/ -**/db.json - -# Development -src/ -docs/ -test/ -agents/ -scripts/ -worker/ -shared-sse/ -copilot-api/ -CLIProxyAPI/ - -# Config files -*.md -!README.md -.gitignore -.env* -jsconfig.json -eslint.config.mjs -postcss.config.mjs -next.config.mjs -tsconfig.json - -# Build artifacts that shouldn't be published -.next/cache/ -.next/standalone/data/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/9router/.vscode/settings.json b/9router/.vscode/settings.json deleted file mode 100644 index 8e3d0f16..00000000 --- a/9router/.vscode/settings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "css.lint.unknownAtRules": "ignore", - "sonarlint.rules": { - "css:S4662": { - "level": "off" - }, - "javascript:S6747": { - "level": "off" - }, - "javascript:S7764": { - "level": "off" - }, - "javascript:S6772": { - "level": "off" - }, - "javascript:S3776": { - "level": "off" - } - } -} diff --git a/9router/CHANGELOG.md b/9router/CHANGELOG.md deleted file mode 100644 index e21b252a..00000000 --- a/9router/CHANGELOG.md +++ /dev/null @@ -1,91 +0,0 @@ -# Unreleased - -## Features -- Added API key visibility toggle (eye icon) to Endpoint dashboard page for improved UX and security. - -# v0.2.66 (2026-02-06) - -## Features -- Added Cursor provider end-to-end support, including OAuth import flow and translator/executor integration (`137f315`, `0a026c7`). -- Enhanced auth/settings flow with `requireLogin` control and `hasPassword` state handling in dashboard/login APIs (`249fc28`). -- Improved usage/quota UX with richer provider limit cards, new quota table, and clearer reset/countdown display (`32aefe5`). -- Added model support for custom providers in UI/combos/model selection (`a7a52be`). -- Expanded model/provider catalog: - - Codex updates: GPT-5.3 support, translation fixes, thinking levels (`127475d`) - - Added Claude Opus 4.6 model (`e8aa3e2`) - - Added MiniMax Coding (CN) provider (`7c609d7`) - - Added iFlow Kimi K2.5 model (`9e357a7`) - - Updated CLI tools with Droid/OpenClaw cards and base URL visibility improvements (`a2122e3`) -- Added auto-validation for provider API keys when saving settings (`b275dfd`). -- Added Docker/runtime deployment docs and architecture documentation updates (`5e4a15b`). - -## Fixes -- Improved local-network compatibility by allowing auth cookie flow over HTTP deployments (`0a394d0`). -- Improved Antigravity quota/stream handling and Droid CLI compatibility behavior (`3c65e0c`, `c612741`, `8c6e3b8`). -- Fixed GitHub Copilot model mapping/selection issues (`95fd950`). -- Hardened local DB behavior with corrupt JSON recovery and schema-shape migration safeguards (`e6ef852`). -- Fixed logout/login edge cases: - - Prevent unintended auto-login after logout (`49df3dc`) - - Avoid infinite loading on failed `/api/settings` responses (`01c9410`) - -# v0.2.56 (2026-02-04) - -## Features -- Added Anthropic-compatible provider support across providers API/UI flow (`da5bdef`). -- Added provider icons to dashboard provider pages/lists (`60bd686`, `8ceb8f2`). -- Enhanced usage tracking pipeline across response handlers/streams with buffered accounting improvements (`a33924b`, `df0e1d6`, `7881db8`). - -## Fixes -- Fixed usage conversion and related provider limits presentation issues (`e6e44ac`). - -# v0.2.52 (2026-02-02) - -## Features -- Implemented Codex Cursor compatibility and Next.js 16 proxy migration updates (`e9b0a73`, `7b864a9`, `1c6dd6d`). -- Added OpenAI-compatible provider nodes with CRUD/validation/test coverage in API and UI (`0a28f9f`). -- Added token expiration and key-validity checks in provider test flow (`686585d`). -- Added Kiro token refresh support in shared token refresh service (`f2ca6f0`). -- Added non-streaming response translation support for multiple formats (`63f2da8`). -- Updated Kiro OAuth wiring and auth-related UI assets/components (`31cc79a`). - -## Fixes -- Fixed cloud translation/request compatibility path (`c7219d0`). -- Fixed Kiro auth modal/flow issues (`85b7bb9`). -- Included Antigravity stability fixes in translator/executor flow (`2393771`, `8c37b39`). - -# v0.2.43 (2026-01-27) - -## Fixes -- Fixed CLI tools model selection behavior (`a015266`). -- Fixed Kiro translator request handling (`d3dd868`). - -# v0.2.36 (2026-01-19) - -## Features -- Added the Usage dashboard page and related usage stats components (`3804357`). -- Integrated outbound proxy support in Open SSE fetch pipeline (`0943387`). -- Improved OpenAI compatibility and build stability across endpoint/profile/providers flows (`d9b8e48`). - -## Fixes -- Fixed combo fallback behavior (`e6ca119`). -- Resolved SonarQube findings, Next.js image warnings, and build/lint cleanups (`7058b06`, `0848dd5`). - -# v0.2.31 (2026-01-18) - -## Fixes -- Fixed Kiro token refresh and executor behavior (`6b22b1f`, `1d481c2`). -- Fixed Kiro request translation handling (`eff52f7`, `da15660`). - -# v0.2.27 (2026-01-15) - -## Features -- Added Kiro provider support with OAuth flow (`26b61e5`). - -## Fixes -- Fixed Codex provider behavior (`26b61e5`). - -# v0.2.21 (2026-01-12) - -## Changes -- README updates. -- Antigravity bug fixes. diff --git a/9router/Dockerfile b/9router/Dockerfile deleted file mode 100644 index b5bcae14..00000000 --- a/9router/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM node:20-alpine AS builder -WORKDIR /app - -COPY package*.json ./ -RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi - -COPY . ./ -ENV NEXT_TELEMETRY_DISABLED=1 -RUN npm run build - -FROM node:20-alpine AS runner -WORKDIR /app - -LABEL org.opencontainers.image.title="9router" - -ENV NODE_ENV=production -ENV PORT=20128 -ENV HOSTNAME=0.0.0.0 -ENV NEXT_TELEMETRY_DISABLED=1 - -COPY --from=builder /app/public ./public -COPY --from=builder /app/.next/static ./.next/static -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/open-sse ./open-sse - -RUN mkdir -p /app/data - -# Fix permissions at runtime (handles mounted volumes) -RUN printf '#!/bin/sh\nchown -R node:node /app/data 2>/dev/null; exec su-exec node "$@"\n' > /entrypoint.sh && chmod +x /entrypoint.sh -RUN apk add --no-cache su-exec - -EXPOSE 20128 - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["node", "server.js"] diff --git a/9router/LICENSE b/9router/LICENSE deleted file mode 100644 index 92192354..00000000 --- a/9router/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024-2026 decolua and contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/9router/README.md b/9router/README.md deleted file mode 100644 index fa08180e..00000000 --- a/9router/README.md +++ /dev/null @@ -1,1211 +0,0 @@ -
- 9Router Dashboard - - # 9Router - Free AI Router - - **Never stop coding. Auto-route to FREE & cheap AI models with smart fallback.** - - **Connect All AI Code Tools (Claude Code, Cursor, Antigravity, Copilot, Codex, Gemini, OpenCode, Cline, OpenClaw...) to 40+ AI Providers & 100+ Models.** - - [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) - [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router) - [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) - - [🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [📖 Setup](#-setup-guide) • [🌐 Website](https://9router.com) - - [🇻🇳 Tiếng Việt](./i18n/README.vi.md) • [🇨🇳 中文](./i18n/README.zh-CN.md) -
- ---- - -## 🤔 Why 9Router? - -**Stop wasting money and hitting limits:** - -- ❌ Subscription quota expires unused every month -- ❌ Rate limits stop you mid-coding -- ❌ Expensive APIs ($20-50/month per provider) -- ❌ Manual switching between providers - -**9Router solves this:** - -- ✅ **Maximize subscriptions** - Track quota, use every bit before reset -- ✅ **Auto fallback** - Subscription → Cheap → Free, zero downtime -- ✅ **Multi-account** - Round-robin between accounts per provider -- ✅ **Universal** - Works with Claude Code, Codex, Gemini CLI, Cursor, Cline, any CLI tool - ---- - -## 🔄 How It Works - -``` -┌─────────────┐ -│ Your CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...) -│ Tool │ -└──────┬──────┘ - │ http://localhost:20128/v1 - ↓ -┌─────────────────────────────────────────┐ -│ 9Router (Smart Router) │ -│ • Format translation (OpenAI ↔ Claude) │ -│ • Quota tracking │ -│ • Auto token refresh │ -└──────┬──────────────────────────────────┘ - │ - ├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, Gemini CLI - │ ↓ quota exhausted - ├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M) - │ ↓ budget limit - └─→ [Tier 3: FREE] iFlow, Qwen, Kiro (unlimited) - -Result: Never stop coding, minimal cost -``` - ---- - -## ⚡ Quick Start - -**1. Install globally:** - -```bash -npm install -g 9router -9router -``` - -🎉 Dashboard opens at `http://localhost:20128` - -**2. Connect a FREE provider (no signup needed):** - -Dashboard → Providers → Connect **Claude Code** or **Antigravity** → OAuth login → Done! - -**3. Use in your CLI tool:** - -``` -Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline Settings: - Endpoint: http://localhost:20128/v1 - API Key: [copy from dashboard] - Model: if/kimi-k2-thinking -``` - -**That's it!** Start coding with FREE AI models. - -**Alternative: run from source (this repository):** - -This repository package is private (`9router-app`), so source/Docker execution is the expected local development path. - -```bash -cp .env.example .env -npm install -PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev -``` - -Production mode: - -```bash -npm run build -PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start -``` - -Default URLs: -- Dashboard: `http://localhost:20128/dashboard` -- OpenAI-compatible API: `http://localhost:20128/v1` - ---- - -## 🎥 Video Tutorial - -
- -### 📺 Complete Setup Guide - 9Router + Claude Code FREE - -[![9Router + Claude Code Setup](https://img.youtube.com/vi/raEyZPg5xE0/maxresdefault.jpg)](https://www.youtube.com/watch?v=raEyZPg5xE0) - -**🎬 Watch the complete step-by-step tutorial:** -- ✅ 9Router installation & setup -- ✅ FREE Claude Sonnet 4.5 configuration -- ✅ Claude Code integration -- ✅ Live coding demonstration - -**⏱️ Duration:** 20 minutes | **👥 By:** Developer Community - -[▶️ Watch on YouTube](https://www.youtube.com/watch?v=o3qYCyjrFYg) - -
- ---- - -## 🛠️ Supported CLI Tools - -9Router works seamlessly with all major AI coding tools: - -
- - - - - - - - - - - - - - - - - -
- Claude Code
- Claude-Code -
- OpenClaw
- OpenClaw -
- Codex
- Codex -
- OpenCode
- OpenCode -
- Cursor
- Cursor -
- Antigravity
- Antigravity -
- Cline
- Cline -
- Continue
- Continue -
- Droid
- Droid -
- Roo
- Roo -
- Copilot
- Copilot -
- Kilo Code
- Kilo Code -
-
- ---- - -## 🌐 Supported Providers - -### 🔐 OAuth Providers - -
- - - - - - - - -
- Claude Code
- Claude-Code -
- Antigravity
- Antigravity -
- Codex
- Codex -
- GitHub
- GitHub -
- Cursor
- Cursor -
-
- -### 🆓 Free Providers - -
- - - - - - - -
- iFlow
- iFlow AI
- 8+ models • Unlimited -
- Qwen
- Qwen Code
- 3+ models • Unlimited -
- Gemini CLI
- Gemini CLI
- 180K/month FREE -
- Kiro
- Kiro AI
- Claude • Unlimited -
-
- -### 🔑 API Key Providers (40+) - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- OpenRouter
- OpenRouter -
- GLM
- GLM -
- Kimi
- Kimi -
- MiniMax
- MiniMax -
- OpenAI
- OpenAI -
- Anthropic
- Anthropic -
- Gemini
- Gemini -
- DeepSeek
- DeepSeek -
- Groq
- Groq -
- xAI
- xAI -
- Mistral
- Mistral -
- Perplexity
- Perplexity -
- Together
- Together AI -
- Fireworks
- Fireworks -
- Cerebras
- Cerebras -
- Cohere
- Cohere -
- NVIDIA
- NVIDIA -
- SiliconFlow
- SiliconFlow -
-

...and 20+ more providers including Nebius, Chutes, Hyperbolic, and custom OpenAI/Anthropic compatible endpoints

-
- ---- - -## 💡 Key Features - -| Feature | What It Does | Why It Matters | -|---------|--------------|----------------| -| 🎯 **Smart 3-Tier Fallback** | Auto-route: Subscription → Cheap → Free | Never stop coding, zero downtime | -| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown | Maximize subscription value | -| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini seamless | Works with any CLI tool | -| 👥 **Multi-Account Support** | Multiple accounts per provider | Load balancing + redundancy | -| 🔄 **Auto Token Refresh** | OAuth tokens refresh automatically | No manual re-login needed | -| 🎨 **Custom Combos** | Create unlimited model combinations | Tailor fallback to your needs | -| 📝 **Request Logging** | Debug mode with full request/response logs | Troubleshoot issues easily | -| 💾 **Cloud Sync** | Sync config across devices | Same setup everywhere | -| 📊 **Usage Analytics** | Track tokens, cost, trends over time | Optimize spending | -| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloudflare Workers | Flexible deployment options | - -
-📖 Feature Details - -### 🎯 Smart 3-Tier Fallback - -Create combos with automatic fallback: - -``` -Combo: "my-coding-stack" - 1. cc/claude-opus-4-6 (your subscription) - 2. glm/glm-4.7 (cheap backup, $0.6/1M) - 3. if/kimi-k2-thinking (free fallback) - -→ Auto switches when quota runs out or errors occur -``` - -### 📊 Real-Time Quota Tracking - -- Token consumption per provider -- Reset countdown (5-hour, daily, weekly) -- Cost estimation for paid tiers -- Monthly spending reports - -### 🔄 Format Translation - -Seamless translation between formats: -- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **OpenAI Responses** -- Your CLI tool sends OpenAI format → 9Router translates → Provider receives native format -- Works with any tool that supports custom OpenAI endpoints - -### 👥 Multi-Account Support - -- Add multiple accounts per provider -- Auto round-robin or priority-based routing -- Fallback to next account when one hits quota - -### 🔄 Auto Token Refresh - -- OAuth tokens automatically refresh before expiration -- No manual re-authentication needed -- Seamless experience across all providers - -### 🎨 Custom Combos - -- Create unlimited model combinations -- Mix subscription, cheap, and free tiers -- Name your combos for easy access -- Share combos across devices with Cloud Sync - -### 📝 Request Logging - -- Enable debug mode for full request/response logs -- Track API calls, headers, and payloads -- Troubleshoot integration issues -- Export logs for analysis - -### 💾 Cloud Sync - -- Sync providers, combos, and settings across devices -- Automatic background sync -- Secure encrypted storage -- Access your setup from anywhere - -#### Cloud Runtime Notes - -- Prefer server-side cloud variables in production: - - `BASE_URL` (internal callback URL used by sync scheduler) - - `CLOUD_URL` (cloud sync endpoint base) -- `NEXT_PUBLIC_BASE_URL` and `NEXT_PUBLIC_CLOUD_URL` are still supported for compatibility/UI, but server runtime now prioritizes `BASE_URL`/`CLOUD_URL`. -- Cloud sync requests now use timeout + fail-fast behavior to avoid UI hanging when cloud DNS/network is unavailable. - -### 📊 Usage Analytics - -- Track token usage per provider and model -- Cost estimation and spending trends -- Monthly reports and insights -- Optimize your AI spending - -> **💡 IMPORTANT - Understanding Dashboard Costs:** -> -> The "cost" displayed in Usage Analytics is **for tracking and comparison purposes only**. -> 9Router itself **never charges** you anything. You only pay providers directly (if using paid services). -> -> **Example:** If your dashboard shows "$290 total cost" while using iFlow models, this represents -> what you would have paid using paid APIs directly. Your actual cost = **$0** (iFlow is free unlimited). -> -> Think of it as a "savings tracker" showing how much you're saving by using free models or -> routing through 9Router! - -### 🌐 Deploy Anywhere - -- 💻 **Localhost** - Default, works offline -- ☁️ **VPS/Cloud** - Share across devices -- 🐳 **Docker** - One-command deployment -- 🚀 **Cloudflare Workers** - Global edge network - -
- ---- - -## 💰 Pricing at a Glance - -| Tier | Provider | Cost | Quota Reset | Best For | -|------|----------|------|-------------|----------| -| **💳 SUBSCRIPTION** | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed | -| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | -| | Gemini CLI | **FREE** | 180K/mo + 1K/day | Everyone! | -| | GitHub Copilot | $10-19/mo | Monthly | GitHub users | -| **💰 CHEAP** | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | -| | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option | -| | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost | -| **🆓 FREE** | iFlow | $0 | Unlimited | 8 models free | -| | Qwen | $0 | Unlimited | 3 models free | -| | Kiro | $0 | Unlimited | Claude free | - -**💡 Pro Tip:** Start with Gemini CLI (180K free/month) + iFlow (unlimited free) combo = $0 cost! - ---- - -### 📊 Understanding 9Router Costs & Billing - -**9Router Billing Reality:** - -✅ **9Router software = FREE forever** (open source, never charges) -✅ **Dashboard "costs" = Display/tracking only** (not actual bills) -✅ **You pay providers directly** (subscriptions or API fees) -✅ **FREE providers stay FREE** (iFlow, Kiro, Qwen = $0 unlimited) -❌ **9Router never sends invoices** or charges your card - -**How Cost Display Works:** - -The dashboard shows **estimated costs** as if you were using paid APIs directly. This is **not billing** - it's a comparison tool to show your savings. - -**Example Scenario:** -``` -Dashboard Display: -• Total Requests: 1,662 -• Total Tokens: 47M -• Display Cost: $290 - -Reality Check: -• Provider: iFlow (FREE unlimited) -• Actual Payment: $0.00 -• What $290 Means: Amount you SAVED by using free models! -``` - -**Payment Rules:** -- **Subscription providers** (Claude Code, Codex): Pay them directly via their websites -- **Cheap providers** (GLM, MiniMax): Pay them directly, 9Router just routes -- **FREE providers** (iFlow, Kiro, Qwen): Genuinely free forever, no hidden charges -- **9Router**: Never charges anything, ever - ---- - -## 🎯 Use Cases - -### Case 1: "I have Claude Pro subscription" - -**Problem:** Quota expires unused, rate limits during heavy coding - -**Solution:** -``` -Combo: "maximize-claude" - 1. cc/claude-opus-4-6 (use subscription fully) - 2. glm/glm-4.7 (cheap backup when quota out) - 3. if/kimi-k2-thinking (free emergency fallback) - -Monthly cost: $20 (subscription) + ~$5 (backup) = $25 total -vs. $20 + hitting limits = frustration -``` - -### Case 2: "I want zero cost" - -**Problem:** Can't afford subscriptions, need reliable AI coding - -**Solution:** -``` -Combo: "free-forever" - 1. gc/gemini-3-flash (180K free/month) - 2. if/kimi-k2-thinking (unlimited free) - 3. qw/qwen3-coder-plus (unlimited free) - -Monthly cost: $0 -Quality: Production-ready models -``` - -### Case 3: "I need 24/7 coding, no interruptions" - -**Problem:** Deadlines, can't afford downtime - -**Solution:** -``` -Combo: "always-on" - 1. cc/claude-opus-4-6 (best quality) - 2. cx/gpt-5.2-codex (second subscription) - 3. glm/glm-4.7 (cheap, resets daily) - 4. minimax/MiniMax-M2.1 (cheapest, 5h reset) - 5. if/kimi-k2-thinking (free unlimited) - -Result: 5 layers of fallback = zero downtime -Monthly cost: $20-200 (subscriptions) + $10-20 (backup) -``` - -### Case 4: "I want FREE AI in OpenClaw" - -**Problem:** Need AI assistant in messaging apps (WhatsApp, Telegram, Slack...), completely free - -**Solution:** -``` -Combo: "openclaw-free" - 1. if/glm-4.7 (unlimited free) - 2. if/minimax-m2.1 (unlimited free) - 3. if/kimi-k2-thinking (unlimited free) - -Monthly cost: $0 -Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... -``` - ---- - -## ❓ Frequently Asked Questions - -
-📊 Why does my dashboard show high costs? - -The dashboard tracks your token usage and displays **estimated costs** as if you were using paid APIs directly. This is **not actual billing** - it's a reference to show how much you're saving by using free models or existing subscriptions through 9Router. - -**Example:** -- **Dashboard shows:** "$290 total cost" -- **Reality:** You're using iFlow (FREE unlimited) -- **Your actual cost:** **$0.00** -- **What $290 means:** Amount you **saved** by using free models instead of paid APIs! - -The cost display is a "savings tracker" to help you understand your usage patterns and optimization opportunities. - -
- -
-💳 Will I be charged by 9Router? - -**No.** 9Router is free, open-source software that runs on your own computer. It never charges you anything. - -**You only pay:** -- ✅ **Subscription providers** (Claude Code $20/mo, Codex $20-200/mo) → Pay them directly on their websites -- ✅ **Cheap providers** (GLM, MiniMax) → Pay them directly, 9Router just routes your requests -- ❌ **9Router itself** → **Never charges anything, ever** - -9Router is a local proxy/router. It doesn't have your credit card, can't send invoices, and has no billing system. It's completely free software. - -
- -
-🆓 Are FREE providers really unlimited? - -**Yes!** Providers marked as FREE (iFlow, Kiro, Qwen) are genuinely unlimited with **no hidden charges**. - -These are free services offered by those respective companies: -- **iFlow**: Free unlimited access to 8+ models via OAuth -- **Kiro**: Free unlimited Claude models via AWS Builder ID -- **Qwen**: Free unlimited access to Qwen models via device auth - -9Router just routes your requests to them - there's no "catch" or future billing. They're truly free services, and 9Router makes them easy to use with fallback support. - -**Note:** Some subscription providers (Antigravity, GitHub Copilot) may have free preview periods that could become paid later, but this would be clearly announced by those providers, not 9Router. - -
- -
-💰 How do I minimize my actual AI costs? - -**Free-First Strategy:** - -1. **Start with 100% free combo:** - ``` - 1. gc/gemini-3-flash (180K/month free from Google) - 2. if/kimi-k2-thinking (unlimited free from iFlow) - 3. qw/qwen3-coder-plus (unlimited free from Qwen) - ``` - **Cost: $0/month** - -2. **Add cheap backup** only if you need it: - ``` - 4. glm/glm-4.7 ($0.6/1M tokens) - ``` - **Additional cost: Only pay for what you actually use** - -3. **Use subscription providers last:** - - Only if you already have them - - 9Router helps maximize their value through quota tracking - -**Result:** Most users can operate at $0/month using only free tiers! - -
- -
-📈 What if my usage suddenly spikes? - -9Router's smart fallback prevents surprise charges: - -**Scenario:** You're on a coding sprint and blow through your quotas - -**Without 9Router:** -- ❌ Hit rate limit → Work stops → Frustration -- ❌ Or: Accidentally rack up huge API bills - -**With 9Router:** -- ✅ Subscription hits limit → Auto-fallback to cheap tier -- ✅ Cheap tier gets expensive → Auto-fallback to free tier -- ✅ Never stop coding → Predictable costs - -**You're in control:** Set spending limits per provider in dashboard, and 9Router respects them. - -
- ---- - -## 📖 Setup Guide - -
-🔐 Subscription Providers (Maximize Value) - -### Claude Code (Pro/Max) - -```bash -Dashboard → Providers → Connect Claude Code -→ OAuth login → Auto token refresh -→ 5-hour + weekly quota tracking - -Models: - cc/claude-opus-4-6 - cc/claude-sonnet-4-5-20250929 - cc/claude-haiku-4-5-20251001 -``` - -**Pro Tip:** Use Opus for complex tasks, Sonnet for speed. 9Router tracks quota per model! - -### OpenAI Codex (Plus/Pro) - -```bash -Dashboard → Providers → Connect Codex -→ OAuth login (port 1455) -→ 5-hour + weekly reset - -Models: - cx/gpt-5.2-codex - cx/gpt-5.1-codex-max -``` - -### Gemini CLI (FREE 180K/month!) - -```bash -Dashboard → Providers → Connect Gemini CLI -→ Google OAuth -→ 180K completions/month + 1K/day - -Models: - gc/gemini-3-flash-preview - gc/gemini-2.5-pro -``` - -**Best Value:** Huge free tier! Use this before paid tiers. - -### GitHub Copilot - -```bash -Dashboard → Providers → Connect GitHub -→ OAuth via GitHub -→ Monthly reset (1st of month) - -Models: - gh/gpt-5 - gh/claude-4.5-sonnet - gh/gemini-3-pro -``` - -
- -
-💰 Cheap Providers (Backup) - -### GLM-4.7 (Daily reset, $0.6/1M) - -1. Sign up: [Zhipu AI](https://open.bigmodel.cn/) -2. Get API key from Coding Plan -3. Dashboard → Add API Key: - - Provider: `glm` - - API Key: `your-key` - -**Use:** `glm/glm-4.7` - -**Pro Tip:** Coding Plan offers 3× quota at 1/7 cost! Reset daily 10:00 AM. - -### MiniMax M2.1 (5h reset, $0.20/1M) - -1. Sign up: [MiniMax](https://www.minimax.io/) -2. Get API key -3. Dashboard → Add API Key - -**Use:** `minimax/MiniMax-M2.1` - -**Pro Tip:** Cheapest option for long context (1M tokens)! - -### Kimi K2 ($9/month flat) - -1. Subscribe: [Moonshot AI](https://platform.moonshot.ai/) -2. Get API key -3. Dashboard → Add API Key - -**Use:** `kimi/kimi-latest` - -**Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost! - -
- -
-🆓 FREE Providers (Emergency Backup) - -### iFlow (8 FREE models) - -```bash -Dashboard → Connect iFlow -→ iFlow OAuth login -→ Unlimited usage - -Models: - if/kimi-k2-thinking - if/qwen3-coder-plus - if/glm-4.7 - if/minimax-m2 - if/deepseek-r1 -``` - -### Qwen (3 FREE models) - -```bash -Dashboard → Connect Qwen -→ Device code authorization -→ Unlimited usage - -Models: - qw/qwen3-coder-plus - qw/qwen3-coder-flash -``` - -### Kiro (Claude FREE) - -```bash -Dashboard → Connect Kiro -→ AWS Builder ID or Google/GitHub -→ Unlimited usage - -Models: - kr/claude-sonnet-4.5 - kr/claude-haiku-4.5 -``` - -
- -
-🎨 Create Combos - -### Example 1: Maximize Subscription → Cheap Backup - -``` -Dashboard → Combos → Create New - -Name: premium-coding -Models: - 1. cc/claude-opus-4-6 (Subscription primary) - 2. glm/glm-4.7 (Cheap backup, $0.6/1M) - 3. minimax/MiniMax-M2.1 (Cheapest fallback, $0.20/1M) - -Use in CLI: premium-coding - -Monthly cost example (100M tokens): - 80M via Claude (subscription): $0 extra - 15M via GLM: $9 - 5M via MiniMax: $1 - Total: $10 + your subscription -``` - -### Example 2: Free-Only (Zero Cost) - -``` -Name: free-combo -Models: - 1. gc/gemini-3-flash-preview (180K free/month) - 2. if/kimi-k2-thinking (unlimited) - 3. qw/qwen3-coder-plus (unlimited) - -Cost: $0 forever! -``` - -
- -
-🔧 CLI Integration - -### Cursor IDE - -``` -Settings → Models → Advanced: - OpenAI API Base URL: http://localhost:20128/v1 - OpenAI API Key: [from 9router dashboard] - Model: cc/claude-opus-4-6 -``` - -Or use combo: `premium-coding` - -### Claude Code - -Edit `~/.claude/config.json`: - -```json -{ - "anthropic_api_base": "http://localhost:20128/v1", - "anthropic_api_key": "your-9router-api-key" -} -``` - -### Codex CLI - -```bash -export OPENAI_BASE_URL="http://localhost:20128" -export OPENAI_API_KEY="your-9router-api-key" - -codex "your prompt" -``` - -### OpenClaw - -**Option 1 — Dashboard (recommended):** - -``` -Dashboard → CLI Tools → OpenClaw → Select Model → Apply -``` - -**Option 2 — Manual:** Edit `~/.openclaw/openclaw.json`: - -```json -{ - "agents": { - "defaults": { - "model": { - "primary": "9router/if/glm-4.7" - } - } - }, - "models": { - "providers": { - "9router": { - "baseUrl": "http://127.0.0.1:20128/v1", - "apiKey": "sk_9router", - "api": "openai-completions", - "models": [ - { - "id": "if/glm-4.7", - "name": "glm-4.7" - } - ] - } - } - } -} -``` - -> **Note:** OpenClaw only works with local 9Router. Use `127.0.0.1` instead of `localhost` to avoid IPv6 resolution issues. - -### Cline / Continue / RooCode - -``` -Provider: OpenAI Compatible -Base URL: http://localhost:20128/v1 -API Key: [from dashboard] -Model: cc/claude-opus-4-6 -``` - -
- -
-🚀 Deployment - -### VPS Deployment - -```bash -# Clone and install -git clone https://github.com/decolua/9router.git -cd 9router -npm install -npm run build - -# Configure -export JWT_SECRET="your-secure-secret-change-this" -export INITIAL_PASSWORD="your-password" -export DATA_DIR="/var/lib/9router" -export PORT="20128" -export HOSTNAME="0.0.0.0" -export NODE_ENV="production" -export NEXT_PUBLIC_BASE_URL="http://localhost:20128" -export NEXT_PUBLIC_CLOUD_URL="https://9router.com" -export API_KEY_SECRET="endpoint-proxy-api-key-secret" -export MACHINE_ID_SALT="endpoint-proxy-salt" - -# Start -npm run start - -# Or use PM2 -npm install -g pm2 -pm2 start npm --name 9router -- start -pm2 save -pm2 startup -``` - -### Docker - -```bash -# Build image (from repository root) -docker build -t 9router . - -# Run container (command used in current setup) -docker run -d \ - --name 9router \ - -p 20128:20128 \ - --env-file /root/dev/9router/.env \ - -v 9router-data:/app/data \ - -v 9router-usage:/root/.9router \ - 9router -``` - -Portable command (if you are already at repository root): - -```bash -docker run -d \ - --name 9router \ - -p 20128:20128 \ - --env-file ./.env \ - -v 9router-data:/app/data \ - -v 9router-usage:/root/.9router \ - 9router -``` - -Container defaults: -- `PORT=20128` -- `HOSTNAME=0.0.0.0` - -Useful commands: - -```bash -docker logs -f 9router -docker restart 9router -docker stop 9router && docker rm 9router -``` - -### Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `JWT_SECRET` | `9router-default-secret-change-me` | JWT signing secret for dashboard auth cookie (**change in production**) | -| `INITIAL_PASSWORD` | `123456` | First login password when no saved hash exists | -| `DATA_DIR` | `~/.9router` | Main app database location (`db.json`) | -| `PORT` | framework default | Service port (`20128` in examples) | -| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) | -| `NODE_ENV` | runtime default | Set `production` for deploy | -| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL used by cloud sync jobs | -| `CLOUD_URL` | `https://9router.com` | Server-side cloud sync endpoint base URL | -| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | Backward-compatible/public base URL (prefer `BASE_URL` for server runtime) | -| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | Backward-compatible/public cloud URL (prefer `CLOUD_URL` for server runtime) | -| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys | -| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Salt for stable machine ID hashing | -| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs under `logs/` | -| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (set `true` behind HTTPS reverse proxy) | -| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` routes (recommended for internet-exposed deploys) | -| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | empty | Optional outbound proxy for upstream provider calls | - -Notes: -- Lowercase proxy variables are also supported: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`. -- `.env` is not baked into Docker image (`.dockerignore`); inject runtime config with `--env-file` or `-e`. -- On Windows, `APPDATA` can be used for local storage path resolution. -- `INSTANCE_NAME` appears in older docs/env templates, but is currently not used at runtime. - -### Runtime Files and Storage - -- Main app state: `${DATA_DIR}/db.json` (providers, combos, aliases, keys, settings), managed by `src/lib/localDb.js`. -- Usage history and logs: `~/.9router/usage.json` and `~/.9router/log.txt`, managed by `src/lib/usageDb.js`. -- Optional request/translator logs: `/logs/...` when `ENABLE_REQUEST_LOGS=true`. -- Usage storage currently follows `~/.9router` path logic and is independent from `DATA_DIR`. - -
- ---- - -## 📊 Available Models - -
-View all available models - -**Claude Code (`cc/`)** - Pro/Max: -- `cc/claude-opus-4-6` -- `cc/claude-sonnet-4-5-20250929` -- `cc/claude-haiku-4-5-20251001` - -**Codex (`cx/`)** - Plus/Pro: -- `cx/gpt-5.2-codex` -- `cx/gpt-5.1-codex-max` - -**Gemini CLI (`gc/`)** - FREE: -- `gc/gemini-3-flash-preview` -- `gc/gemini-2.5-pro` - -**GitHub Copilot (`gh/`)**: -- `gh/gpt-5` -- `gh/claude-4.5-sonnet` - -**GLM (`glm/`)** - $0.6/1M: -- `glm/glm-4.7` - -**MiniMax (`minimax/`)** - $0.2/1M: -- `minimax/MiniMax-M2.1` - -**iFlow (`if/`)** - FREE: -- `if/kimi-k2-thinking` -- `if/qwen3-coder-plus` -- `if/deepseek-r1` - -**Qwen (`qw/`)** - FREE: -- `qw/qwen3-coder-plus` -- `qw/qwen3-coder-flash` - -**Kiro (`kr/`)** - FREE: -- `kr/claude-sonnet-4.5` -- `kr/claude-haiku-4.5` - -
- ---- - -## 🐛 Troubleshooting - -**"Language model did not provide messages"** -- Provider quota exhausted → Check dashboard quota tracker -- Solution: Use combo fallback or switch to cheaper tier - -**Rate limiting** -- Subscription quota out → Fallback to GLM/MiniMax -- Add combo: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking` - -**OAuth token expired** -- Auto-refreshed by 9Router -- If issues persist: Dashboard → Provider → Reconnect - -**High costs** -- Check usage stats in Dashboard -- Switch primary model to GLM/MiniMax -- Use free tier (Gemini CLI, iFlow) for non-critical tasks - -**Dashboard opens on wrong port** -- Set `PORT=20128` and `NEXT_PUBLIC_BASE_URL=http://localhost:20128` - -**First login not working** -- Check `INITIAL_PASSWORD` in `.env` -- If unset, fallback password is `123456` - -**No request logs under `logs/`** -- Set `ENABLE_REQUEST_LOGS=true` - ---- - -## 🛠️ Tech Stack - -- **Runtime**: Node.js 20+ -- **Framework**: Next.js 16 -- **UI**: React 19 + Tailwind CSS 4 -- **Database**: LowDB (JSON file-based) -- **Streaming**: Server-Sent Events (SSE) -- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys - ---- - -## 📝 API Reference - -### Chat Completions - -```bash -POST http://localhost:20128/v1/chat/completions -Authorization: Bearer your-api-key -Content-Type: application/json - -{ - "model": "cc/claude-opus-4-6", - "messages": [ - {"role": "user", "content": "Write a function to..."} - ], - "stream": true -} -``` - -### List Models - -```bash -GET http://localhost:20128/v1/models -Authorization: Bearer your-api-key - -→ Returns all models + combos in OpenAI format -``` - -## 📧 Support - -- **Website**: [9router.com](https://9router.com) -- **GitHub**: [github.com/decolua/9router](https://github.com/decolua/9router) -- **Issues**: [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues) - ---- - -## 👥 Contributors - -Thanks to all contributors who helped make 9Router better! - -[![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors) - ---- - -## 📊 Star Chart - -[![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) - - - -## 🔀 Forks - -**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — A full-featured TypeScript fork of 9Router. Adds 36+ providers, 4-tier auto-fallback, multi-modal APIs (images, embeddings, audio, TTS), circuit breaker, semantic cache, LLM evaluations, and a polished dashboard. 368+ unit tests. Available via npm and Docker. - ---- - -## 🙏 Acknowledgments - -Special thanks to **CLIProxyAPI** - the original Go implementation that inspired this JavaScript port. - ---- - -## 📄 License - -MIT License - see [LICENSE](LICENSE) for details. - ---- - -
- Built with ❤️ for developers who code 24/7 -
- diff --git a/9router/cloud/.gitignore b/9router/cloud/.gitignore deleted file mode 100644 index 03d963cb..00000000 --- a/9router/cloud/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.wrangler/* -node_modules/* -**node_modules/* diff --git a/9router/cloud/README.md b/9router/cloud/README.md deleted file mode 100644 index 5b096dd3..00000000 --- a/9router/cloud/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# 9Router Cloud Worker - -Deploy your own Cloudflare Worker to access 9Router from anywhere. - -## Setup - -```bash -# 1. Login to Cloudflare -npm install -g wrangler -wrangler login - -# 2. Install dependencies -cd app/cloud -npm install - -# 3. Create KV & D1, then paste IDs into wrangler.toml -wrangler kv namespace create KV -wrangler d1 create proxy-db - -# 4. Init database & deploy -wrangler d1 execute proxy-db --remote --file=./migrations/0001_init.sql -npm run deploy -``` - -Copy your Worker URL → 9Router Dashboard → **Endpoint** → **Setup Cloud** → paste → **Save** → **Enable Cloud**. diff --git a/9router/cloud/jsconfig.json b/9router/cloud/jsconfig.json deleted file mode 100644 index 6f1c2a85..00000000 --- a/9router/cloud/jsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "open-sse": ["../open-sse"], - "open-sse/*": ["../open-sse/*"] - }, - "module": "ESNext", - "moduleResolution": "bundler", - "target": "ESNext" - } -} diff --git a/9router/cloud/migrations/0001_init.sql b/9router/cloud/migrations/0001_init.sql deleted file mode 100644 index ce13021a..00000000 --- a/9router/cloud/migrations/0001_init.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Migration: Create machines table -CREATE TABLE IF NOT EXISTS machines ( - machineId TEXT PRIMARY KEY, - data TEXT NOT NULL, - updatedAt TEXT NOT NULL -); - --- Index for faster lookups -CREATE INDEX IF NOT EXISTS idx_machines_updatedAt ON machines(updatedAt); diff --git a/9router/cloud/package.json b/9router/cloud/package.json deleted file mode 100644 index ee1ba8aa..00000000 --- a/9router/cloud/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "9router-cloud", - "version": "0.2.13", - "private": true, - "type": "module", - "description": "9Router Cloud Worker - Self-hosted Cloudflare Worker proxy", - "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy" - }, - "dependencies": { - "open-sse": "file:../open-sse" - }, - "devDependencies": { - "wrangler": "^3.0.0" - } -} diff --git a/9router/cloud/src/handlers/cache.js b/9router/cloud/src/handlers/cache.js deleted file mode 100644 index b5a50b8d..00000000 --- a/9router/cloud/src/handlers/cache.js +++ /dev/null @@ -1,37 +0,0 @@ -import { errorResponse } from "open-sse/utils/error.js"; -import { extractBearerToken, parseApiKey } from "../utils/apiKey.js"; -import * as log from "../utils/logger.js"; - -export async function handleCacheClear(request, env) { - const apiKey = extractBearerToken(request); - if (!apiKey) { - return errorResponse(401, "Missing API key"); - } - - try { - const body = await request.json().catch(() => ({})); - - // Get machineId from API key or body - let machineId = body.machineId; - if (!machineId) { - const parsed = await parseApiKey(apiKey); - machineId = parsed?.machineId; - } - - if (!machineId) { - return errorResponse(400, "Missing machineId"); - } - - // No cache layer to clear anymore - log.info("CACHE", `Cache clear requested for machine: ${machineId} (no-op)`); - - return new Response(JSON.stringify({ success: true, machineId, message: "No cache layer" }), { - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }); - } catch (error) { - return errorResponse(500, error.message); - } -} \ No newline at end of file diff --git a/9router/cloud/src/handlers/chat.js b/9router/cloud/src/handlers/chat.js deleted file mode 100644 index a6b8e4d5..00000000 --- a/9router/cloud/src/handlers/chat.js +++ /dev/null @@ -1,305 +0,0 @@ -import { getModelInfoCore } from "open-sse/services/model.js"; -import { handleChatCore } from "open-sse/handlers/chatCore.js"; -import { errorResponse } from "open-sse/utils/error.js"; -import { checkFallbackError, isAccountUnavailable, getUnavailableUntil, getEarliestRateLimitedUntil, formatRetryAfter } from "open-sse/services/accountFallback.js"; -import { getComboModelsFromData, handleComboChat } from "open-sse/services/combo.js"; -import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js"; -import * as log from "../utils/logger.js"; -import { refreshTokenByProvider } from "../services/tokenRefresh.js"; -import { parseApiKey, extractBearerToken } from "../utils/apiKey.js"; -import { getMachineData, saveMachineData } from "../services/storage.js"; - -const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; - -async function getModelInfo(modelStr, machineId, env) { - const data = await getMachineData(machineId, env); - return getModelInfoCore(modelStr, data?.modelAliases || {}); -} - -/** - * Handle chat request - * @param {Request} request - * @param {Object} env - * @param {Object} ctx - * @param {string|null} machineIdOverride - machineId from URL (old format) or null (new format - extract from key) - */ -export async function handleChat(request, env, ctx, machineIdOverride = null) { - if (request.method === "OPTIONS") { - return new Response(null, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "*" - } - }); - } - - // Determine machineId: from URL (old) or from API key (new) - let machineId = machineIdOverride; - - if (!machineId) { - // New format: extract machineId from API key - const apiKey = extractBearerToken(request); - if (!apiKey) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - - const parsed = await parseApiKey(apiKey); - if (!parsed) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key format"); - - if (!parsed.isNewFormat || !parsed.machineId) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "API key does not contain machineId. Use /{machineId}/v1/... endpoint for old format keys."); - } - - machineId = parsed.machineId; - } - - if (!await validateApiKey(request, machineId, env)) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - - let body; - try { - body = await request.json(); - } catch { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); - } - - log.info("CHAT", `${machineId} | ${body.model}`, { stream: body.stream !== false }); - - const modelStr = body.model; - if (!modelStr) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model"); - - // Check if model is a combo - const data = await getMachineData(machineId, env); - const comboModels = getComboModelsFromData(modelStr, data?.combos || []); - - if (comboModels) { - log.info("COMBO", `"${modelStr}" with ${comboModels.length} models`); - return handleComboChat({ - body, - models: comboModels, - handleSingleModel: (reqBody, model) => handleSingleModelChat(reqBody, model, machineId, env), - log - }); - } - - // Single model request - return handleSingleModelChat(body, modelStr, machineId, env); -} - -/** - * Handle single model chat request - */ -async function handleSingleModelChat(body, modelStr, machineId, env) { - const modelInfo = await getModelInfo(modelStr, machineId, env); - if (!modelInfo.provider) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format"); - - const { provider, model } = modelInfo; - log.info("MODEL", `${provider.toUpperCase()} | ${model}`); - - let excludeConnectionId = null; - let lastError = null; - let lastStatus = null; - - while (true) { - const credentials = await getProviderCredentials(machineId, provider, env, excludeConnectionId); - if (!credentials || credentials.allRateLimited) { - if (credentials?.allRateLimited) { - const retryAfterSec = Math.ceil((new Date(credentials.retryAfter).getTime() - Date.now()) / 1000); - const errorMsg = lastError || credentials.lastError || "Unavailable"; - const msg = `[${provider}/${model}] ${errorMsg} (${credentials.retryAfterHuman})`; - const status = lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE; - log.warn("CHAT", `${provider.toUpperCase()} | ${msg}`); - return new Response( - JSON.stringify({ error: { message: msg } }), - { status, headers: { "Content-Type": "application/json", "Retry-After": String(Math.max(retryAfterSec, 1)) } } - ); - } - if (!excludeConnectionId) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${provider}`); - } - log.warn("CHAT", `${provider.toUpperCase()} | no more accounts`); - return new Response( - JSON.stringify({ error: lastError || "All accounts unavailable" }), - { status: lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, headers: { "Content-Type": "application/json" } } - ); - } - - log.debug("CHAT", `account=${credentials.id}`, { provider }); - - const refreshedCredentials = await checkAndRefreshToken(machineId, provider, credentials, env); - - // Use shared chatCore - const result = await handleChatCore({ - body, - modelInfo: { provider, model }, - credentials: refreshedCredentials, - log, - onCredentialsRefreshed: async (newCreds) => { - await updateCredentials(machineId, credentials.id, newCreds, env); - }, - onRequestSuccess: async () => { - // Clear error status only if currently has error (optimization) - await clearAccountError(machineId, credentials.id, credentials, env); - } - }); - - if (result.success) return result.response; - - const { shouldFallback } = checkFallbackError(result.status, result.error); - - if (shouldFallback) { - log.warn("FALLBACK", `${provider.toUpperCase()} | ${credentials.id} | ${result.status}`); - await markAccountUnavailable(machineId, credentials.id, result.status, result.error, env); - excludeConnectionId = credentials.id; - lastError = result.error; - lastStatus = result.status; - continue; - } - - return result.response; - } -} - -async function checkAndRefreshToken(machineId, provider, credentials, env) { - if (!credentials.expiresAt) return credentials; - - const expiresAt = new Date(credentials.expiresAt).getTime(); - if (expiresAt - Date.now() >= TOKEN_EXPIRY_BUFFER_MS) return credentials; - - log.debug("TOKEN", `${provider.toUpperCase()} | expiring, refreshing`); - - const newCredentials = await refreshTokenByProvider(provider, credentials); - if (newCredentials?.accessToken) { - await updateCredentials(machineId, credentials.id, newCredentials, env); - return { - ...credentials, - accessToken: newCredentials.accessToken, - refreshToken: newCredentials.refreshToken || credentials.refreshToken, - expiresAt: newCredentials.expiresIn - ? new Date(Date.now() + newCredentials.expiresIn * 1000).toISOString() - : credentials.expiresAt - }; - } - - return credentials; -} - -async function validateApiKey(request, machineId, env) { - const authHeader = request.headers.get("Authorization"); - if (!authHeader?.startsWith("Bearer ")) return false; - - const apiKey = authHeader.slice(7); - const data = await getMachineData(machineId, env); - return data?.apiKeys?.some(k => k.key === apiKey) || false; -} - -async function getProviderCredentials(machineId, provider, env, excludeConnectionId = null) { - const data = await getMachineData(machineId, env); - if (!data?.providers) return null; - - const providerConnections = Object.entries(data.providers) - .filter(([connId, conn]) => { - if (conn.provider !== provider || !conn.isActive) return false; - if (excludeConnectionId && connId === excludeConnectionId) return false; - if (isAccountUnavailable(conn.rateLimitedUntil)) return false; - return true; - }) - .sort((a, b) => (a[1].priority || 999) - (b[1].priority || 999)); - - if (providerConnections.length === 0) { - // Check if accounts exist but all rate limited - const allConnections = Object.entries(data.providers) - .filter(([, conn]) => conn.provider === provider && conn.isActive) - .map(([, conn]) => conn); - const earliest = getEarliestRateLimitedUntil(allConnections); - if (earliest) { - const rateLimitedConns = allConnections.filter(c => c.rateLimitedUntil && new Date(c.rateLimitedUntil).getTime() > Date.now()); - const earliestConn = rateLimitedConns.sort((a, b) => new Date(a.rateLimitedUntil) - new Date(b.rateLimitedUntil))[0]; - return { - allRateLimited: true, - retryAfter: earliest, - retryAfterHuman: formatRetryAfter(earliest), - lastError: earliestConn?.lastError || null, - lastErrorCode: earliestConn?.errorCode || null - }; - } - return null; - } - - const [connectionId, connection] = providerConnections[0]; - - return { - id: connectionId, - apiKey: connection.apiKey, - accessToken: connection.accessToken, - refreshToken: connection.refreshToken, - expiresAt: connection.expiresAt, - projectId: connection.projectId, - copilotToken: connection.providerSpecificData?.copilotToken, - providerSpecificData: connection.providerSpecificData, - // Include current status for optimization check - status: connection.status, - lastError: connection.lastError, - rateLimitedUntil: connection.rateLimitedUntil - }; -} - -async function markAccountUnavailable(machineId, connectionId, status, errorText, env) { - const data = await getMachineData(machineId, env); - if (!data?.providers?.[connectionId]) return; - - const conn = data.providers[connectionId]; - const backoffLevel = conn.backoffLevel || 0; - const { cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel); - const rateLimitedUntil = getUnavailableUntil(cooldownMs); - const reason = typeof errorText === "string" ? errorText.slice(0, 100) : "Provider error"; - - data.providers[connectionId].rateLimitedUntil = rateLimitedUntil; - data.providers[connectionId].status = "unavailable"; - data.providers[connectionId].lastError = reason; - data.providers[connectionId].errorCode = status || null; - data.providers[connectionId].lastErrorAt = new Date().toISOString(); - data.providers[connectionId].backoffLevel = newBackoffLevel ?? backoffLevel; - data.providers[connectionId].updatedAt = new Date().toISOString(); - - await saveMachineData(machineId, data, env); - log.warn("ACCOUNT", `${connectionId} | unavailable until ${rateLimitedUntil} (backoff=${newBackoffLevel ?? backoffLevel})`); -} - -async function clearAccountError(machineId, connectionId, currentCredentials, env) { - // Only update if currently has error status (optimization) - const hasError = currentCredentials.status === "unavailable" || - currentCredentials.lastError || - currentCredentials.rateLimitedUntil; - - if (!hasError) return; // Skip if already clean - - const data = await getMachineData(machineId, env); - if (!data?.providers?.[connectionId]) return; - - data.providers[connectionId].status = "active"; - data.providers[connectionId].lastError = null; - data.providers[connectionId].lastErrorAt = null; - data.providers[connectionId].rateLimitedUntil = null; - data.providers[connectionId].backoffLevel = 0; - data.providers[connectionId].updatedAt = new Date().toISOString(); - - await saveMachineData(machineId, data, env); - log.info("ACCOUNT", `${connectionId} | error cleared`); -} - -async function updateCredentials(machineId, connectionId, newCredentials, env) { - const data = await getMachineData(machineId, env); - if (!data?.providers?.[connectionId]) return; - - data.providers[connectionId].accessToken = newCredentials.accessToken; - if (newCredentials.refreshToken) data.providers[connectionId].refreshToken = newCredentials.refreshToken; - if (newCredentials.expiresIn) { - data.providers[connectionId].expiresAt = new Date(Date.now() + newCredentials.expiresIn * 1000).toISOString(); - data.providers[connectionId].expiresIn = newCredentials.expiresIn; - } - data.providers[connectionId].updatedAt = new Date().toISOString(); - - await saveMachineData(machineId, data, env); - log.debug("TOKEN", `credentials updated | ${connectionId}`); -} diff --git a/9router/cloud/src/handlers/cleanup.js b/9router/cloud/src/handlers/cleanup.js deleted file mode 100644 index 3c638455..00000000 --- a/9router/cloud/src/handlers/cleanup.js +++ /dev/null @@ -1,34 +0,0 @@ -import * as log from "../utils/logger.js"; - -const RETENTION_DAYS = 7; - -/** - * Cleanup old machine data from D1 - * Runs daily via cron trigger - */ -export async function handleCleanup(env) { - const cutoffDate = new Date(Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString(); - - log.info("CLEANUP", `Deleting records older than ${cutoffDate}`); - - try { - const result = await env.DB.prepare("DELETE FROM machines WHERE updatedAt < ?") - .bind(cutoffDate) - .run(); - - log.info("CLEANUP", `Deleted ${result.meta?.changes || 0} old records`); - - return { - success: true, - deleted: result.meta?.changes || 0, - cutoffDate - }; - } catch (error) { - log.error("CLEANUP", error.message); - return { - success: false, - error: error.message - }; - } -} - diff --git a/9router/cloud/src/handlers/countTokens.js b/9router/cloud/src/handlers/countTokens.js deleted file mode 100644 index bfb2cf11..00000000 --- a/9router/cloud/src/handlers/countTokens.js +++ /dev/null @@ -1,46 +0,0 @@ -import { errorResponse } from "open-sse/utils/error.js"; - -const CORS_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "POST, OPTIONS", - "Access-Control-Allow-Headers": "*" -}; - -/** - * Handle POST /{machineId}/v1/messages/count_tokens - * Mock token count response based on content length - */ -export async function handleCountTokens(request, env) { - let body; - try { - body = await request.json(); - } catch { - return errorResponse(400, "Invalid JSON body"); - } - - // Estimate token count based on content length - const messages = body.messages || []; - let totalChars = 0; - - for (const msg of messages) { - if (typeof msg.content === "string") { - totalChars += msg.content.length; - } else if (Array.isArray(msg.content)) { - for (const part of msg.content) { - if (part.type === "text" && part.text) { - totalChars += part.text.length; - } - } - } - } - - // Rough estimate: ~4 chars per token - const inputTokens = Math.ceil(totalChars / 4); - - return new Response(JSON.stringify({ - input_tokens: inputTokens - }), { - headers: { "Content-Type": "application/json", ...CORS_HEADERS } - }); -} - diff --git a/9router/cloud/src/handlers/embeddings.js b/9router/cloud/src/handlers/embeddings.js deleted file mode 100644 index 41a91080..00000000 --- a/9router/cloud/src/handlers/embeddings.js +++ /dev/null @@ -1,285 +0,0 @@ -import { getModelInfoCore } from "open-sse/services/model.js"; -import { handleEmbeddingsCore } from "open-sse/handlers/embeddingsCore.js"; -import { errorResponse } from "open-sse/utils/error.js"; -import { - checkFallbackError, - isAccountUnavailable, - getEarliestRateLimitedUntil, - getUnavailableUntil, - formatRetryAfter -} from "open-sse/services/accountFallback.js"; -import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js"; -import * as log from "../utils/logger.js"; -import { parseApiKey, extractBearerToken } from "../utils/apiKey.js"; -import { getMachineData, saveMachineData } from "../services/storage.js"; - -/** - * Handle POST /v1/embeddings and /{machineId}/v1/embeddings requests. - * - * Follows the same auth + fallback pattern as handleChat: - * 1. Resolve machineId (from URL or API key) - * 2. Validate API key - * 3. Parse model → provider/model - * 4. Get provider credentials with fallback loop - * 5. Delegate to handleEmbeddingsCore (open-sse) - * - * @param {Request} request - * @param {object} env - Cloudflare env bindings - * @param {object} ctx - Execution context - * @param {string|null} machineIdOverride - From URL path (old format), or null (new format) - */ -export async function handleEmbeddings(request, env, ctx, machineIdOverride = null) { - if (request.method === "OPTIONS") { - return new Response(null, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "*" - } - }); - } - - // Resolve machineId - let machineId = machineIdOverride; - - if (!machineId) { - const apiKey = extractBearerToken(request); - if (!apiKey) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); - - const parsed = await parseApiKey(apiKey); - if (!parsed) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key format"); - - if (!parsed.isNewFormat || !parsed.machineId) { - return errorResponse( - HTTP_STATUS.BAD_REQUEST, - "API key does not contain machineId. Use /{machineId}/v1/... endpoint for old format keys." - ); - } - machineId = parsed.machineId; - } - - // Validate API key - if (!await validateApiKey(request, machineId, env)) { - return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); - } - - // Parse body - let body; - try { - body = await request.json(); - } catch { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); - } - - const modelStr = body.model; - if (!modelStr) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model"); - - if (!body.input) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: input"); - - log.info("EMBEDDINGS", `${machineId} | ${modelStr}`); - - // Resolve model info - const data = await getMachineData(machineId, env); - const modelInfo = await getModelInfoCore(modelStr, data?.modelAliases || {}); - if (!modelInfo.provider) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format"); - - const { provider, model } = modelInfo; - log.info("EMBEDDINGS_MODEL", `${provider.toUpperCase()} | ${model}`); - - // Provider credential + fallback loop (mirrors handleChat) - let excludeConnectionId = null; - let lastError = null; - let lastStatus = null; - - while (true) { - const credentials = await getProviderCredentials(machineId, provider, env, excludeConnectionId); - - if (!credentials || credentials.allRateLimited) { - if (credentials?.allRateLimited) { - const retryAfterSec = Math.ceil( - (new Date(credentials.retryAfter).getTime() - Date.now()) / 1000 - ); - const errorMsg = lastError || credentials.lastError || "Unavailable"; - const msg = `[${provider}/${model}] ${errorMsg} (${credentials.retryAfterHuman})`; - const status = lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE; - log.warn("EMBEDDINGS", `${provider.toUpperCase()} | ${msg}`); - return new Response( - JSON.stringify({ error: { message: msg } }), - { - status, - headers: { - "Content-Type": "application/json", - "Retry-After": String(Math.max(retryAfterSec, 1)) - } - } - ); - } - if (!excludeConnectionId) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${provider}`); - } - log.warn("EMBEDDINGS", `${provider.toUpperCase()} | no more accounts`); - return new Response( - JSON.stringify({ error: lastError || "All accounts unavailable" }), - { - status: lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, - headers: { "Content-Type": "application/json" } - } - ); - } - - log.debug("EMBEDDINGS", `account=${credentials.id}`, { provider }); - - const result = await handleEmbeddingsCore({ - body, - modelInfo: { provider, model }, - credentials, - log, - onCredentialsRefreshed: async (newCreds) => { - await updateCredentials(machineId, credentials.id, newCreds, env); - }, - onRequestSuccess: async () => { - await clearAccountError(machineId, credentials.id, credentials, env); - } - }); - - if (result.success) return result.response; - - const { shouldFallback } = checkFallbackError(result.status, result.error); - - if (shouldFallback) { - log.warn("EMBEDDINGS_FALLBACK", `${provider.toUpperCase()} | ${credentials.id} | ${result.status}`); - await markAccountUnavailable(machineId, credentials.id, result.status, result.error, env); - excludeConnectionId = credentials.id; - lastError = result.error; - lastStatus = result.status; - continue; - } - - return result.response; - } -} - -// ─── Helpers (same as chat.js) ─────────────────────────────────────────────── - -async function validateApiKey(request, machineId, env) { - const authHeader = request.headers.get("Authorization"); - if (!authHeader?.startsWith("Bearer ")) return false; - - const apiKey = authHeader.slice(7); - const data = await getMachineData(machineId, env); - return data?.apiKeys?.some(k => k.key === apiKey) || false; -} - -async function getProviderCredentials(machineId, provider, env, excludeConnectionId = null) { - const data = await getMachineData(machineId, env); - if (!data?.providers) return null; - - const providerConnections = Object.entries(data.providers) - .filter(([connId, conn]) => { - if (conn.provider !== provider || !conn.isActive) return false; - if (excludeConnectionId && connId === excludeConnectionId) return false; - if (isAccountUnavailable(conn.rateLimitedUntil)) return false; - return true; - }) - .sort((a, b) => (a[1].priority || 999) - (b[1].priority || 999)); - - if (providerConnections.length === 0) { - const allConnections = Object.entries(data.providers) - .filter(([, conn]) => conn.provider === provider && conn.isActive) - .map(([, conn]) => conn); - const earliest = getEarliestRateLimitedUntil(allConnections); - if (earliest) { - const rateLimitedConns = allConnections.filter( - c => c.rateLimitedUntil && new Date(c.rateLimitedUntil).getTime() > Date.now() - ); - const earliestConn = rateLimitedConns.sort( - (a, b) => new Date(a.rateLimitedUntil) - new Date(b.rateLimitedUntil) - )[0]; - return { - allRateLimited: true, - retryAfter: earliest, - retryAfterHuman: formatRetryAfter(earliest), - lastError: earliestConn?.lastError || null, - lastErrorCode: earliestConn?.errorCode || null - }; - } - return null; - } - - const [connectionId, connection] = providerConnections[0]; - return { - id: connectionId, - apiKey: connection.apiKey, - accessToken: connection.accessToken, - refreshToken: connection.refreshToken, - expiresAt: connection.expiresAt, - projectId: connection.projectId, - providerSpecificData: connection.providerSpecificData, - status: connection.status, - lastError: connection.lastError, - rateLimitedUntil: connection.rateLimitedUntil - }; -} - -async function markAccountUnavailable(machineId, connectionId, status, errorText, env) { - const data = await getMachineData(machineId, env); - if (!data?.providers?.[connectionId]) return; - - const conn = data.providers[connectionId]; - const backoffLevel = conn.backoffLevel || 0; - const { cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel); - const rateLimitedUntil = getUnavailableUntil(cooldownMs); - const reason = typeof errorText === "string" ? errorText.slice(0, 100) : "Provider error"; - - data.providers[connectionId].rateLimitedUntil = rateLimitedUntil; - data.providers[connectionId].status = "unavailable"; - data.providers[connectionId].lastError = reason; - data.providers[connectionId].errorCode = status || null; - data.providers[connectionId].lastErrorAt = new Date().toISOString(); - data.providers[connectionId].backoffLevel = newBackoffLevel ?? backoffLevel; - data.providers[connectionId].updatedAt = new Date().toISOString(); - - await saveMachineData(machineId, data, env); - log.warn("EMBEDDINGS_ACCOUNT", `${connectionId} | unavailable until ${rateLimitedUntil}`); -} - -async function clearAccountError(machineId, connectionId, currentCredentials, env) { - const hasError = - currentCredentials.status === "unavailable" || - currentCredentials.lastError || - currentCredentials.rateLimitedUntil; - - if (!hasError) return; - - const data = await getMachineData(machineId, env); - if (!data?.providers?.[connectionId]) return; - - data.providers[connectionId].status = "active"; - data.providers[connectionId].lastError = null; - data.providers[connectionId].lastErrorAt = null; - data.providers[connectionId].rateLimitedUntil = null; - data.providers[connectionId].backoffLevel = 0; - data.providers[connectionId].updatedAt = new Date().toISOString(); - - await saveMachineData(machineId, data, env); - log.info("EMBEDDINGS_ACCOUNT", `${connectionId} | error cleared`); -} - -async function updateCredentials(machineId, connectionId, newCredentials, env) { - const data = await getMachineData(machineId, env); - if (!data?.providers?.[connectionId]) return; - - data.providers[connectionId].accessToken = newCredentials.accessToken; - if (newCredentials.refreshToken) - data.providers[connectionId].refreshToken = newCredentials.refreshToken; - if (newCredentials.expiresIn) { - data.providers[connectionId].expiresAt = new Date( - Date.now() + newCredentials.expiresIn * 1000 - ).toISOString(); - data.providers[connectionId].expiresIn = newCredentials.expiresIn; - } - data.providers[connectionId].updatedAt = new Date().toISOString(); - - await saveMachineData(machineId, data, env); - log.debug("EMBEDDINGS_TOKEN", `credentials updated | ${connectionId}`); -} diff --git a/9router/cloud/src/handlers/forward.js b/9router/cloud/src/handlers/forward.js deleted file mode 100644 index ab89aec4..00000000 --- a/9router/cloud/src/handlers/forward.js +++ /dev/null @@ -1,75 +0,0 @@ -// CF headers to remove -const CF_HEADERS = [ - "cf-connecting-ip", "cf-connecting-ip6", "cf-ray", "cf-visitor", - "cf-ipcountry", "cf-tracking-id", "cf-connecting-ip6-policy", - "x-real-ip", "x-forwarded-for", "x-forwarded-proto", "x-forwarded-host" -]; - -// Forward request to any endpoint -export async function handleForward(request) { - try { - const url = new URL(request.url); - const clientIp = request.headers.get("CF-Connecting-IP") || ""; - const { targetUrl, headers = {}, body } = await request.json(); - - if (!targetUrl) { - return new Response(JSON.stringify({ error: "targetUrl is required" }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); - } - - // Filter out CF headers from input - const cleanHeaders = {}; - for (const [key, value] of Object.entries(headers)) { - if (!CF_HEADERS.includes(key.toLowerCase())) { - cleanHeaders[key] = value; - } - } - - // Set standard forwarding headers - cleanHeaders["X-Client-IP"] = clientIp; - cleanHeaders["X-Forwarded-Proto"] = url.protocol.replace(":", ""); - cleanHeaders["X-Forwarded-Host"] = url.host; - cleanHeaders["X-From-Worker"] = "1"; - - console.log("[FORWARD] Target:", targetUrl); - console.log("[FORWARD] Headers:", JSON.stringify(cleanHeaders)); - - // Create Request object to have more control over headers - const outgoingRequest = new Request(targetUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...cleanHeaders - }, - body: JSON.stringify(body) - }); - - // Use fetch with cf options to minimize auto-added headers - const response = await fetch(outgoingRequest, { - cf: { - // Disable automatic features that add headers - scrapeShield: false, - minify: false, - mirage: false, - polish: "off" - } - }); - - // Stream response back to client - return new Response(response.body, { - status: response.status, - headers: { - "Content-Type": response.headers.get("Content-Type") || "application/json", - "Access-Control-Allow-Origin": "*" - } - }); - } catch (error) { - console.error("[FORWARD] Error:", error.message); - return new Response(JSON.stringify({ error: error.message }), { - status: 500, - headers: { "Content-Type": "application/json" } - }); - } -} diff --git a/9router/cloud/src/handlers/forwardRaw.js b/9router/cloud/src/handlers/forwardRaw.js deleted file mode 100644 index 87d34193..00000000 --- a/9router/cloud/src/handlers/forwardRaw.js +++ /dev/null @@ -1,173 +0,0 @@ -import { connect } from "cloudflare:sockets"; - -// Forward request via raw TCP socket (bypasses CF auto headers) -export async function handleForwardRaw(request) { - try { - const { targetUrl, headers = {}, body } = await request.json(); - - if (!targetUrl) { - return new Response(JSON.stringify({ error: "targetUrl is required" }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); - } - - const url = new URL(targetUrl); - const host = url.hostname; - const port = url.port || (url.protocol === "https:" ? 443 : 80); - const path = url.pathname + url.search; - const isHttps = url.protocol === "https:"; - - console.log("[FORWARD_RAW] Connecting to:", host, port, isHttps ? "(TLS)" : ""); - - // Connect to target server - let secureSocket; - if (isHttps) { - // For HTTPS, connect directly with TLS enabled - console.log("[FORWARD_RAW] Creating TLS socket..."); - secureSocket = connect({ - hostname: host, - port: parseInt(port), - secureTransport: "on" - }); - console.log("[FORWARD_RAW] TLS socket created"); - } else { - secureSocket = connect({ hostname: host, port: parseInt(port) }); - } - - console.log("[FORWARD_RAW] Socket object:", secureSocket); - console.log("[FORWARD_RAW] Socket opened:", secureSocket.opened); - - // Wait for socket to be ready - try { - console.log("[FORWARD_RAW] Waiting for socket to open..."); - await secureSocket.opened; - console.log("[FORWARD_RAW] Socket opened successfully"); - } catch (openError) { - console.error("[FORWARD_RAW] Socket open error:", openError.message); - throw openError; - } - - console.log("[FORWARD_RAW] Getting writer and reader..."); - const writer = secureSocket.writable.getWriter(); - const reader = secureSocket.readable.getReader(); - console.log("[FORWARD_RAW] Writer and reader obtained"); - - // Build raw HTTP request - const bodyStr = JSON.stringify(body); - const requestHeaders = { - "Host": host, - "Content-Type": "application/json", - "Content-Length": new TextEncoder().encode(bodyStr).length.toString(), - "Connection": "close", - ...headers - }; - - // Build HTTP request string - let httpRequest = `POST ${path} HTTP/1.1\r\n`; - for (const [key, value] of Object.entries(requestHeaders)) { - httpRequest += `${key}: ${value}\r\n`; - } - httpRequest += `\r\n${bodyStr}`; - - console.log("[FORWARD_RAW] Sending request:", httpRequest.substring(0, 300)); - console.log("[FORWARD_RAW] Full request length:", httpRequest.length); - - // Send request - try { - console.log("[FORWARD_RAW] Writing to socket..."); - await writer.write(new TextEncoder().encode(httpRequest)); - console.log("[FORWARD_RAW] Write complete, closing writer..."); - await writer.close(); - console.log("[FORWARD_RAW] Writer closed"); - } catch (writeError) { - console.error("[FORWARD_RAW] Write error:", writeError.message); - throw writeError; - } - - // Read response with timeout - console.log("[FORWARD_RAW] Starting to read response..."); - let responseData = new Uint8Array(0); - let attempts = 0; - const maxAttempts = 100; // 10 seconds max - - while (attempts < maxAttempts) { - console.log("[FORWARD_RAW] Reading attempt:", attempts); - const { done, value } = await reader.read(); - console.log("[FORWARD_RAW] Read result - done:", done, "value length:", value?.length); - if (done) break; - if (value) { - const newData = new Uint8Array(responseData.length + value.length); - newData.set(responseData); - newData.set(value, responseData.length); - responseData = newData; - - // Check if we have complete response (has headers end marker) - const text = new TextDecoder().decode(responseData); - if (text.includes("\r\n\r\n")) { - // Check if we have Content-Length and received all body - const headerEnd = text.indexOf("\r\n\r\n"); - const headers = text.substring(0, headerEnd).toLowerCase(); - const contentLengthMatch = headers.match(/content-length:\s*(\d+)/); - if (contentLengthMatch) { - const expectedLength = parseInt(contentLengthMatch[1]); - const bodyReceived = text.length - headerEnd - 4; - if (bodyReceived >= expectedLength) { - console.log("[FORWARD_RAW] Complete response received"); - break; - } - } - } - } - attempts++; - } - - console.log("[FORWARD_RAW] Read loop finished, total bytes:", responseData.length); - - const responseText = new TextDecoder().decode(responseData); - console.log("[FORWARD_RAW] Response received:", responseText.substring(0, 500)); - - // Parse HTTP response - const headerEndIndex = responseText.indexOf("\r\n\r\n"); - if (headerEndIndex === -1) { - console.log("[FORWARD_RAW] Full response data:", responseText); - throw new Error("Invalid HTTP response - no header end found"); - } - - const headerPart = responseText.substring(0, headerEndIndex); - const bodyPart = responseText.substring(headerEndIndex + 4); - - // Parse status line - const statusLine = headerPart.split("\r\n")[0]; - const statusMatch = statusLine.match(/HTTP\/[\d.]+ (\d+)/); - const status = statusMatch ? parseInt(statusMatch[1]) : 200; - - // Parse headers - const responseHeaders = {}; - const headerLines = headerPart.split("\r\n").slice(1); - for (const line of headerLines) { - const colonIndex = line.indexOf(":"); - if (colonIndex > 0) { - const key = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim(); - responseHeaders[key.toLowerCase()] = value; - } - } - - return new Response(bodyPart, { - status, - headers: { - "Content-Type": responseHeaders["content-type"] || "application/json", - "Access-Control-Allow-Origin": "*" - } - }); - - } catch (error) { - console.error("[FORWARD_RAW] Error:", error.message, error.stack); - return new Response(JSON.stringify({ error: error.message }), { - status: 500, - headers: { "Content-Type": "application/json" } - }); - } -} - diff --git a/9router/cloud/src/handlers/sync.js b/9router/cloud/src/handlers/sync.js deleted file mode 100644 index 93d4b766..00000000 --- a/9router/cloud/src/handlers/sync.js +++ /dev/null @@ -1,227 +0,0 @@ -import * as log from "../utils/logger.js"; -import { getMachineData, saveMachineData, deleteMachineData } from "../services/storage.js"; - -const CORS_HEADERS = { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" -}; - -// Removed: WORKER_FIELDS and WORKER_SPECIFIC_FIELDS -// Now syncing entire provider based on updatedAt (simpler logic) - -export async function handleSync(request, env, ctx) { - const url = new URL(request.url); - const machineId = url.pathname.split("/")[2]; // /sync/:machineId - - // Handle CORS preflight - if (request.method === "OPTIONS") { - return new Response(null, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "*" - } - }); - } - - if (!machineId) { - log.warn("SYNC", "Missing machineId in path"); - return jsonResponse({ error: "Missing machineId" }, 400); - } - - // Route by method - switch (request.method) { - case "GET": - return handleGet(machineId, env); - case "POST": - return handlePost(request, machineId, env); - case "DELETE": - return handleDelete(machineId, env); - default: - return jsonResponse({ error: "Method not allowed" }, 405); - } -} - -/** - * GET /sync/:machineId - Return merged data for Web to update - */ -async function handleGet(machineId, env) { - const data = await getMachineData(machineId, env); - - if (!data) { - log.warn("SYNC", "No data found", { machineId }); - return jsonResponse({ error: "No data found" }, 404); - } - - log.info("SYNC", "Data retrieved", { machineId }); - return jsonResponse({ - success: true, - data - }); -} - -/** - * POST /sync/:machineId - Merge Web data with Worker data - * providers stored by ID (supports multiple connections per provider) - */ -async function handlePost(request, machineId, env) { - let body; - try { - body = await request.json(); - } catch { - log.warn("SYNC", "Invalid JSON body", { machineId }); - return jsonResponse({ error: "Invalid JSON body" }, 400); - } - - // Validate required fields - if (!body.providers || !Array.isArray(body.providers)) { - log.warn("SYNC", "Missing or invalid providers array", { machineId }); - return jsonResponse({ error: "Missing providers array" }, 400); - } - - const existingData = await getMachineData(machineId, env) || { providers: {}, modelAliases: {}, apiKeys: [] }; - - // Merge providers by ID - const mergedProviders = {}; - const changes = { updated: [], fromWorker: [] }; - - for (const webProvider of body.providers) { - const providerId = webProvider.id; - if (!providerId) { - log.warn("SYNC", "Provider missing id", { provider: webProvider.provider }); - continue; - } - - const workerProvider = existingData.providers[providerId]; - - if (workerProvider) { - // Merge: token fields from Worker, config fields from Web - mergedProviders[providerId] = mergeProvider(webProvider, workerProvider, changes, providerId); - } else { - // New provider from Web - mergedProviders[providerId] = formatProviderData(webProvider); - changes.updated.push(providerId); - } - } - - // Prepare final data - modelAliases, apiKeys, combos always from Web - const finalData = { - providers: mergedProviders, - modelAliases: body.modelAliases || existingData.modelAliases || {}, - combos: body.combos || existingData.combos || [], - apiKeys: body.apiKeys || existingData.apiKeys || [], - updatedAt: new Date().toISOString() - }; - - // Store in D1 + invalidate cache - await saveMachineData(machineId, finalData, env); - - log.info("SYNC", "Data synced successfully", { - machineId, - providerCount: Object.keys(mergedProviders).length, - changes - }); - - return jsonResponse({ - success: true, - data: finalData, - changes - }); -} - -/** - * DELETE /sync/:machineId - Clear cache when Worker is disabled - */ -async function handleDelete(machineId, env) { - await deleteMachineData(machineId, env); - - log.info("SYNC", "Data deleted", { machineId }); - return jsonResponse({ - success: true, - message: "Data deleted successfully" - }); -} - -/** - * Merge provider data: compare updatedAt to decide which source to use - * Simple logic: newer wins (sync entire provider) - */ -function mergeProvider(webProvider, workerProvider, changes, providerId) { - const webTime = new Date(webProvider.updatedAt || 0).getTime(); - const workerTime = new Date(workerProvider.updatedAt || 0).getTime(); - - let merged; - - if (workerTime > webTime) { - // Cloud has newer data - use entire Cloud provider - merged = formatProviderData(workerProvider); - changes.fromWorker.push(providerId); - } else { - // Server has newer data - use entire Server provider - merged = formatProviderData(webProvider); - changes.updated.push(providerId); - } - - // Always update timestamp - merged.updatedAt = new Date().toISOString(); - return merged; -} - -/** - * Format provider data for storage - */ -function formatProviderData(provider) { - return { - id: provider.id, - provider: provider.provider, - authType: provider.authType, - name: provider.name, - displayName: provider.displayName, - email: provider.email, - priority: provider.priority, - globalPriority: provider.globalPriority, - defaultModel: provider.defaultModel, - accessToken: provider.accessToken, - refreshToken: provider.refreshToken, - expiresAt: provider.expiresAt, - expiresIn: provider.expiresIn, - tokenType: provider.tokenType, - scope: provider.scope, - idToken: provider.idToken, - projectId: provider.projectId, - apiKey: provider.apiKey, - providerSpecificData: provider.providerSpecificData || {}, - isActive: provider.isActive, - status: provider.status || "active", - lastError: provider.lastError || null, - lastErrorAt: provider.lastErrorAt || null, - errorCode: provider.errorCode || null, - rateLimitedUntil: provider.rateLimitedUntil || null, - createdAt: provider.createdAt, - updatedAt: provider.updatedAt || new Date().toISOString() - }; -} - -/** - * Update provider status (called when token refresh fails or API errors) - */ -export function updateProviderStatus(providers, providerId, status, error = null, errorCode = null) { - if (providers[providerId]) { - providers[providerId].status = status; - providers[providerId].lastError = error; - providers[providerId].lastErrorAt = error ? new Date().toISOString() : null; - providers[providerId].errorCode = errorCode; - providers[providerId].updatedAt = new Date().toISOString(); - } - return providers; -} - -/** - * Helper to create JSON response - */ -function jsonResponse(data, status = 200) { - return new Response(JSON.stringify(data), { - status, - headers: CORS_HEADERS - }); -} diff --git a/9router/cloud/src/handlers/verify.js b/9router/cloud/src/handlers/verify.js deleted file mode 100644 index cf5e4a7a..00000000 --- a/9router/cloud/src/handlers/verify.js +++ /dev/null @@ -1,60 +0,0 @@ -import { parseApiKey, extractBearerToken } from "../utils/apiKey.js"; -import { getMachineData } from "../services/storage.js"; - -/** - * Verify API key endpoint - * @param {Request} request - * @param {Object} env - * @param {string|null} machineIdOverride - machineId from URL (old format) or null (new format) - */ -export async function handleVerify(request, env, machineIdOverride = null) { - const apiKey = extractBearerToken(request); - if (!apiKey) { - return jsonResponse({ error: "Missing or invalid Authorization header" }, 401); - } - - // Determine machineId: from URL (old) or from API key (new) - let machineId = machineIdOverride; - - if (!machineId) { - const parsed = await parseApiKey(apiKey); - if (!parsed) { - return jsonResponse({ error: "Invalid API key format" }, 401); - } - - if (!parsed.isNewFormat || !parsed.machineId) { - return jsonResponse({ error: "API key does not contain machineId" }, 400); - } - - machineId = parsed.machineId; - } - - const data = await getMachineData(machineId, env); - - if (!data) { - return jsonResponse({ error: "Machine not found" }, 404); - } - - const isValid = data.apiKeys?.some(k => k.key === apiKey) || false; - - if (!isValid) { - return jsonResponse({ error: "Invalid API key" }, 401); - } - - return jsonResponse({ - valid: true, - machineId, - providersCount: Object.keys(data.providers || {}).length - }); -} - -function jsonResponse(data, status = 200) { - return new Response(JSON.stringify(data), { - status, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }); -} - diff --git a/9router/cloud/src/index.js b/9router/cloud/src/index.js deleted file mode 100644 index 1385b8c3..00000000 --- a/9router/cloud/src/index.js +++ /dev/null @@ -1,233 +0,0 @@ -import { initTranslators } from "open-sse/translator/index.js"; -import { ollamaModels } from "open-sse/config/ollamaModels.js"; -import { transformToOllama } from "open-sse/utils/ollamaTransform.js"; -import * as log from "./utils/logger.js"; - -// Static imports for handlers (avoid dynamic import CPU cost) -import { handleCleanup } from "./handlers/cleanup.js"; -import { handleCacheClear } from "./handlers/cache.js"; -import { handleSync } from "./handlers/sync.js"; -import { handleChat } from "./handlers/chat.js"; -import { handleVerify } from "./handlers/verify.js"; -import { handleTestClaude } from "./handlers/testClaude.js"; -import { handleForward } from "./handlers/forward.js"; -import { handleForwardRaw } from "./handlers/forwardRaw.js"; -import { handleEmbeddings } from "./handlers/embeddings.js"; -import { createLandingPageResponse } from "./services/landingPage.js"; - -// Initialize translators at module load (static imports) -initTranslators(); - -// Helper to add CORS headers to response -function addCorsHeaders(response) { - const newHeaders = new Headers(response.headers); - newHeaders.set("Access-Control-Allow-Origin", "*"); - newHeaders.set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); - newHeaders.set("Access-Control-Allow-Headers", "*"); - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: newHeaders - }); -} - -const worker = { - async scheduled(event, env, ctx) { - const result = await handleCleanup(env); - log.info("SCHEDULED", "Cleanup completed", result); - }, - - async fetch(request, env, ctx) { - const startTime = Date.now(); - const url = new URL(request.url); - let path = url.pathname; - - // Normalize /v1/v1/* → /v1/* - if (path.startsWith("/v1/v1/")) { - path = path.replace("/v1/v1/", "/v1/"); - } else if (path === "/v1/v1") { - path = "/v1"; - } - - log.request(request.method, path); - - // CORS preflight - if (request.method === "OPTIONS") { - return new Response(null, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "*" - } - }); - } - - try { - // Routes - - // Landing page - if (path === "/" && request.method === "GET") { - const response = createLandingPageResponse(); - log.response(response.status, Date.now() - startTime); - return response; - } - - if (path === "/health" && request.method === "GET") { - log.response(200, Date.now() - startTime); - return new Response(JSON.stringify({ status: "ok" }), { - headers: { "Content-Type": "application/json" } - }); - } - - // Ollama compatible - list models - if (path === "/api/tags" && request.method === "GET") { - log.response(200, Date.now() - startTime); - return new Response(JSON.stringify(ollamaModels), { - headers: { "Content-Type": "application/json" } - }); - } - - if (path === "/cache/clear" && request.method === "POST") { - const response = await handleCacheClear(request, env); - log.response(response.status, Date.now() - startTime); - return response; - } - - // Sync provider data by machineId (GET, POST, DELETE) - if (path.startsWith("/sync/") && ["GET", "POST", "DELETE"].includes(request.method)) { - const response = await handleSync(request, env, ctx); - log.response(response.status, Date.now() - startTime); - return response; - } - - // ========== NEW FORMAT: /v1/... (machineId in API key) ========== - - // New format: /v1/chat/completions - if (path === "/v1/chat/completions" && request.method === "POST") { - const response = await handleChat(request, env, ctx, null); - log.response(response.status, Date.now() - startTime); - return addCorsHeaders(response); - } - - // New format: /v1/messages (Claude format) - if (path === "/v1/messages" && request.method === "POST") { - const response = await handleChat(request, env, ctx, null); - log.response(response.status, Date.now() - startTime); - return addCorsHeaders(response); - } - - // New format: /v1/embeddings - if (path === "/v1/embeddings" && request.method === "POST") { - const response = await handleEmbeddings(request, env, ctx, null); - log.response(response.status, Date.now() - startTime); - return addCorsHeaders(response); - } - - // New format: /v1/responses (OpenAI Responses API - Codex CLI) - if (path === "/v1/responses" && request.method === "POST") { - const response = await handleChat(request, env, ctx, null); - log.response(response.status, Date.now() - startTime); - return response; - } - - // New format: /v1/verify - if (path === "/v1/verify" && request.method === "GET") { - const response = await handleVerify(request, env, null); - log.response(response.status, Date.now() - startTime); - return addCorsHeaders(response); - } - - // New format: /v1/api/chat (Ollama format) - if (path === "/v1/api/chat" && request.method === "POST") { - const clonedReq = request.clone(); - const body = await clonedReq.json(); - const response = await handleChat(request, env, ctx, null); - const ollamaResponse = transformToOllama(response, body.model || "llama3.2"); - log.response(200, Date.now() - startTime); - return ollamaResponse; - } - - // ========== OLD FORMAT: /{machineId}/v1/... ========== - - // Machine ID based chat endpoint - if (path.match(/^\/[^\/]+\/v1\/chat\/completions$/) && request.method === "POST") { - const machineId = path.split("/")[1]; - const response = await handleChat(request, env, ctx, machineId); - log.response(response.status, Date.now() - startTime); - return response; - } - - // Machine ID based embeddings endpoint - if (path.match(/^\/[^\/]+\/v1\/embeddings$/) && request.method === "POST") { - const machineId = path.split("/")[1]; - const response = await handleEmbeddings(request, env, ctx, machineId); - log.response(response.status, Date.now() - startTime); - return addCorsHeaders(response); - } - - // Machine ID based messages endpoint (Claude format) - if (path.match(/^\/[^\/]+\/v1\/messages$/) && request.method === "POST") { - const machineId = path.split("/")[1]; - const response = await handleChat(request, env, ctx, machineId); - log.response(response.status, Date.now() - startTime); - return response; - } - - // Machine ID based api/chat endpoint (Ollama format) - if (path.match(/^\/[^\/]+\/v1\/api\/chat$/) && request.method === "POST") { - const machineId = path.split("/")[1]; - const clonedReq = request.clone(); - const body = await clonedReq.json(); - const response = await handleChat(request, env, ctx, machineId); - const ollamaResponse = transformToOllama(response, body.model || "llama3.2"); - log.response(200, Date.now() - startTime); - return ollamaResponse; - } - - // Machine ID based verify endpoint - if (path.match(/^\/[^\/]+\/v1\/verify$/) && request.method === "GET") { - const machineId = path.split("/")[1]; - const response = await handleVerify(request, env, machineId); - log.response(response.status, Date.now() - startTime); - return response; - } - - // Test Claude - forward to Anthropic API - if (path === "/testClaude" && request.method === "POST") { - const response = await handleTestClaude(request); - log.response(response.status, Date.now() - startTime); - return response; - } - - // Forward request to any endpoint - if (path === "/forward" && request.method === "POST") { - const response = await handleForward(request); - log.response(response.status, Date.now() - startTime); - return response; - } - - // Forward request via raw TCP socket (bypasses CF auto headers) - if (path === "/forward-raw" && request.method === "POST") { - const response = await handleForwardRaw(request); - log.response(response.status, Date.now() - startTime); - return response; - } - - log.warn("ROUTER", "Not found", { path }); - return new Response(JSON.stringify({ error: "Not Found" }), { - status: 404, - headers: { "Content-Type": "application/json" } - }); - - } catch (error) { - log.error("ROUTER", error.message, { stack: error.stack }); - return new Response(JSON.stringify({ error: error.message }), { - status: 500, - headers: { "Content-Type": "application/json" } - }); - } - } -}; - -export default worker; - diff --git a/9router/cloud/src/services/landingPage.js b/9router/cloud/src/services/landingPage.js deleted file mode 100644 index dd6e91ae..00000000 --- a/9router/cloud/src/services/landingPage.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Landing Page Service - * Simple health check page for self-hosted worker - */ - -/** - * Create landing page response - * @returns {Response} HTML response - */ -export function createLandingPageResponse() { - const html = ` -9Router Worker - -
-

9Router Worker

-

Worker is running. Configure this URL in your 9Router dashboard.

-
-`; - - return new Response(html, { - status: 200, - headers: { - "Content-Type": "text/html; charset=utf-8", - "Cache-Control": "public, max-age=3600" - } - }); -} diff --git a/9router/cloud/src/services/storage.js b/9router/cloud/src/services/storage.js deleted file mode 100644 index 11a4fb6c..00000000 --- a/9router/cloud/src/services/storage.js +++ /dev/null @@ -1,88 +0,0 @@ -import * as log from "../utils/logger.js"; - -// Request-scoped cache for getMachineData (avoids multiple D1 queries per request) -const requestCache = new Map(); -const CACHE_TTL_MS = 5000; - -/** - * Get machine data from D1 (with request-scope caching) - * @param {string} machineId - * @param {Object} env - * @returns {Promise} - */ -export async function getMachineData(machineId, env) { - const cached = requestCache.get(machineId); - if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { - return cached.data; - } - - const row = await env.DB.prepare("SELECT data FROM machines WHERE machineId = ?") - .bind(machineId) - .first(); - - if (!row) { - log.debug("STORAGE", `Not found: ${machineId}`); - return null; - } - - const data = JSON.parse(row.data); - requestCache.set(machineId, { data, timestamp: Date.now() }); - log.debug("STORAGE", `Retrieved: ${machineId}`); - return data; -} - -/** - * Save machine data to D1 - * @param {string} machineId - * @param {Object} data - * @param {Object} env - */ -export async function saveMachineData(machineId, data, env) { - const now = new Date().toISOString(); - data.updatedAt = now; - - // Upsert to D1 - await env.DB.prepare(` - INSERT INTO machines (machineId, data, updatedAt) - VALUES (?, ?, ?) - ON CONFLICT(machineId) DO UPDATE SET data = ?, updatedAt = ? - `) - .bind(machineId, JSON.stringify(data), now, JSON.stringify(data), now) - .run(); - - // Update cache after save - requestCache.set(machineId, { data, timestamp: Date.now() }); - log.debug("STORAGE", `Saved: ${machineId}`); -} - -/** - * Delete machine data from D1 - * @param {string} machineId - * @param {Object} env - */ -export async function deleteMachineData(machineId, env) { - await env.DB.prepare("DELETE FROM machines WHERE machineId = ?") - .bind(machineId) - .run(); - - // Clear cache after delete - requestCache.delete(machineId); - log.debug("STORAGE", `Deleted: ${machineId}`); -} - -/** - * Update specific fields in machine data (for token refresh, rate limit, etc.) - * @param {string} machineId - * @param {string} connectionId - * @param {Object} updates - * @param {Object} env - */ -export async function updateMachineProvider(machineId, connectionId, updates, env) { - const data = await getMachineData(machineId, env); - if (!data?.providers?.[connectionId]) return; - - Object.assign(data.providers[connectionId], updates); - data.providers[connectionId].updatedAt = new Date().toISOString(); - - await saveMachineData(machineId, data, env); -} \ No newline at end of file diff --git a/9router/cloud/src/services/tokenRefresh.js b/9router/cloud/src/services/tokenRefresh.js deleted file mode 100644 index a9ad8e6f..00000000 --- a/9router/cloud/src/services/tokenRefresh.js +++ /dev/null @@ -1,11 +0,0 @@ -// Re-export from open-sse with worker logger -import * as log from "../utils/logger.js"; -import { - TOKEN_EXPIRY_BUFFER_MS as BUFFER_MS, - refreshTokenByProvider as _refreshTokenByProvider -} from "open-sse/services/tokenRefresh.js"; - -export const TOKEN_EXPIRY_BUFFER_MS = BUFFER_MS; - -export const refreshTokenByProvider = (provider, credentials) => - _refreshTokenByProvider(provider, credentials, log); diff --git a/9router/cloud/src/stubs/usageDb.js b/9router/cloud/src/stubs/usageDb.js deleted file mode 100644 index 0f3a2520..00000000 --- a/9router/cloud/src/stubs/usageDb.js +++ /dev/null @@ -1,8 +0,0 @@ -// Stub for cloud worker - no-op async functions -export async function saveRequestUsage() {} -export function trackPendingRequest() {} -export async function appendRequestLog() {} -export async function getUsageDb() { return { data: { history: [] } }; } -export async function getUsageHistory() { return []; } -export async function getUsageStats() { return {}; } -export async function getRecentLogs() { return []; } diff --git a/9router/cloud/src/utils/apiKey.js b/9router/cloud/src/utils/apiKey.js deleted file mode 100644 index 680466a8..00000000 --- a/9router/cloud/src/utils/apiKey.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * API Key utilities for Worker - * Supports both formats: - * - New: sk-{machineId}-{keyId}-{crc8} - * - Old: sk-{random8} - */ - -const API_KEY_SECRET = "endpoint-proxy-api-key-secret"; - -/** - * Generate CRC (8-char HMAC) using Web Crypto API - */ -async function generateCrc(machineId, keyId) { - const encoder = new TextEncoder(); - const keyData = encoder.encode(API_KEY_SECRET); - const data = encoder.encode(machineId + keyId); - - const key = await crypto.subtle.importKey( - "raw", - keyData, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"] - ); - - const signature = await crypto.subtle.sign("HMAC", key, data); - const hashArray = Array.from(new Uint8Array(signature)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); - - return hashHex.slice(0, 8); -} - -/** - * Parse API key and extract machineId + keyId - * @param {string} apiKey - * @returns {Promise<{ machineId: string, keyId: string, isNewFormat: boolean } | null>} - */ -export async function parseApiKey(apiKey) { - if (!apiKey || !apiKey.startsWith("sk-")) return null; - - const parts = apiKey.split("-"); - - // New format: sk-{machineId}-{keyId}-{crc8} = 4 parts - if (parts.length === 4) { - const [, machineId, keyId, crc] = parts; - - // Verify CRC - const expectedCrc = await generateCrc(machineId, keyId); - if (crc !== expectedCrc) return null; - - return { machineId, keyId, isNewFormat: true }; - } - - // Old format: sk-{random8} = 2 parts - if (parts.length === 2) { - return { machineId: null, keyId: parts[1], isNewFormat: false }; - } - - return null; -} - -/** - * Extract Bearer token from Authorization header - * @param {Request} request - * @returns {string | null} - */ -export function extractBearerToken(request) { - const authHeader = request.headers.get("Authorization"); - if (!authHeader || !authHeader.startsWith("Bearer ")) return null; - return authHeader.slice(7); -} - diff --git a/9router/cloud/src/utils/logger.js b/9router/cloud/src/utils/logger.js deleted file mode 100644 index 8fabcf05..00000000 --- a/9router/cloud/src/utils/logger.js +++ /dev/null @@ -1,84 +0,0 @@ -// Logger utility for worker - -const LOG_LEVELS = { - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3 -}; - -const LEVEL = LOG_LEVELS.INFO; - -// ANSI color codes -const COLORS = { - reset: "\x1b[0m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - cyan: "\x1b[36m" -}; - -function formatTime() { - return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }); -} - -function formatInline(data) { - if (!data) return ""; - if (typeof data === "string") return data; - try { - return Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" | "); - } catch { - return String(data); - } -} - -export function debug(tag, message, data) { - if (LEVEL <= LOG_LEVELS.DEBUG) { - const extra = data ? ` | ${formatInline(data)}` : ""; - console.log(`[${formatTime()}] 🔍 [${tag}] ${message}${extra}`); - } -} - -export function info(tag, message, data) { - if (LEVEL <= LOG_LEVELS.INFO) { - const extra = data ? ` | ${formatInline(data)}` : ""; - console.log(`[${formatTime()}] ℹ️ [${tag}] ${message}${extra}`); - } -} - -export function warn(tag, message, data) { - if (LEVEL <= LOG_LEVELS.WARN) { - const extra = data ? ` | ${formatInline(data)}` : ""; - console.warn(`${COLORS.yellow}[${formatTime()}] ⚠️ [${tag}] ${message}${extra}${COLORS.reset}`); - } -} - -export function error(tag, message, data) { - if (LEVEL <= LOG_LEVELS.ERROR) { - const extra = data ? ` | ${formatInline(data)}` : ""; - console.error(`${COLORS.red}[${formatTime()}] ❌ [${tag}] ${message}${extra}${COLORS.reset}`); - } -} - -export function request(method, path, extra) { - const data = extra ? ` | ${formatInline(extra)}` : ""; - console.log(`[${formatTime()}] 📥 ${method} ${path}${data}`); -} - -export function response(status, duration, extra) { - const icon = status < 400 ? "📤" : "💥"; - const data = extra ? ` | ${formatInline(extra)}` : ""; - console.log(`[${formatTime()}] ${icon} ${status} (${duration}ms)${data}`); -} - -export function stream(event, data) { - const extra = data ? ` | ${formatInline(data)}` : ""; - console.log(`[${formatTime()}] 🌊 [STREAM] ${event}${extra}`); -} - -// Mask sensitive data -export function maskKey(key) { - if (!key || key.length < 8) return "***"; - return `${key.slice(0, 4)}...${key.slice(-4)}`; -} diff --git a/9router/cloud/wrangler.toml b/9router/cloud/wrangler.toml deleted file mode 100644 index 18c7a7ad..00000000 --- a/9router/cloud/wrangler.toml +++ /dev/null @@ -1,17 +0,0 @@ -name = "9router" -main = "src/index.js" -compatibility_date = "2024-09-23" -compatibility_flags = ["nodejs_compat"] - -[alias] -"@/lib/usageDb.js" = "./src/stubs/usageDb.js" - -# Step 3: Paste your KV & D1 IDs here -[[kv_namespaces]] -binding = "KV" -id = "YOUR_KV_NAMESPACE_ID" - -[[d1_databases]] -binding = "DB" -database_name = "proxy-db" -database_id = "YOUR_D1_DATABASE_ID" diff --git a/9router/docs/ARCHITECTURE.md b/9router/docs/ARCHITECTURE.md deleted file mode 100644 index 548c3190..00000000 --- a/9router/docs/ARCHITECTURE.md +++ /dev/null @@ -1,557 +0,0 @@ -# 9Router Architecture - -_Last updated: 2026-02-06_ - -## Executive Summary - -9Router is a local AI routing gateway and dashboard built on Next.js. -It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic across multiple upstream providers with translation, fallback, token refresh, and usage tracking. - -Core capabilities: - -- OpenAI-compatible API surface for CLI/tools -- Request/response translation across provider formats -- Model combo fallback (multi-model sequence) -- Account-level fallback (multi-account per provider) -- OAuth + API-key provider connection management -- Local persistence for providers, keys, aliases, combos, settings, pricing -- Usage/cost tracking and request logging -- Optional cloud sync for multi-device/state sync - -Primary runtime model: - -- Next.js app routes under `src/app/api/*` implement both dashboard APIs and compatibility APIs -- A shared SSE/routing core in `src/sse/*` + `open-sse/*` handles provider execution, translation, streaming, fallback, and usage - -## Scope and Boundaries - -### In Scope - -- Local gateway runtime -- Dashboard management APIs -- Provider authentication and token refresh -- Request translation and SSE streaming -- Local state + usage persistence -- Optional cloud sync orchestration - -### Out of Scope - -- Cloud service implementation behind `NEXT_PUBLIC_CLOUD_URL` -- Provider SLA/control plane outside local process -- External CLI binaries themselves (Claude CLI, Codex CLI, etc.) - -## High-Level System Context - -```mermaid -flowchart LR - subgraph Clients[Developer Clients] - C1[Claude Code] - C2[Codex CLI] - C3[OpenClaw / Droid / Cline / Continue / Roo] - C4[Custom OpenAI-compatible clients] - BROWSER[Browser Dashboard] - end - - subgraph Router[9Router Local Process] - API[V1 Compatibility API\n/v1/*] - DASH[Dashboard + Management API\n/api/*] - CORE[SSE + Translation Core\nopen-sse + src/sse] - DB[(db.json)] - UDB[(usage.json + log.txt)] - end - - subgraph Upstreams[Upstream Providers] - P1[OAuth Providers\nClaude/Codex/Gemini/Qwen/iFlow/GitHub/Kiro/Cursor/Antigravity] - P2[API Key Providers\nOpenAI/Anthropic/OpenRouter/GLM/Kimi/MiniMax] - P3[Compatible Nodes\nOpenAI-compatible / Anthropic-compatible] - end - - subgraph Cloud[Optional Cloud Sync] - CLOUD[Cloud Sync Endpoint\nNEXT_PUBLIC_CLOUD_URL] - end - - C1 --> API - C2 --> API - C3 --> API - C4 --> API - BROWSER --> DASH - - API --> CORE - DASH --> DB - CORE --> DB - CORE --> UDB - - CORE --> P1 - CORE --> P2 - CORE --> P3 - - DASH --> CLOUD -``` - -## Core Runtime Components - -## 1) API and Routing Layer (Next.js App Routes) - -Main directories: - -- `src/app/api/v1/*` and `src/app/api/v1beta/*` for compatibility APIs -- `src/app/api/*` for management/configuration APIs -- Next rewrites in `next.config.mjs` map `/v1/*` to `/api/v1/*` - -Important compatibility routes: - -- `src/app/api/v1/chat/completions/route.js` -- `src/app/api/v1/messages/route.js` -- `src/app/api/v1/responses/route.js` -- `src/app/api/v1/models/route.js` -- `src/app/api/v1/messages/count_tokens/route.js` -- `src/app/api/v1beta/models/route.js` -- `src/app/api/v1beta/models/[...path]/route.js` - -Management domains: - -- Auth/settings: `src/app/api/auth/*`, `src/app/api/settings/*` -- Providers/connections: `src/app/api/providers*` -- Provider nodes: `src/app/api/provider-nodes*` -- OAuth: `src/app/api/oauth/*` -- Keys/aliases/combos/pricing: `src/app/api/keys*`, `src/app/api/models/alias`, `src/app/api/combos*`, `src/app/api/pricing` -- Usage: `src/app/api/usage/*` -- Sync/cloud: `src/app/api/sync/*`, `src/app/api/cloud/*` -- CLI tooling helpers: `src/app/api/cli-tools/*` - -## 2) SSE + Translation Core - -Main flow modules: - -- Entry: `src/sse/handlers/chat.js` -- Core orchestration: `open-sse/handlers/chatCore.js` -- Provider execution adapters: `open-sse/executors/*` -- Format detection/provider config: `open-sse/services/provider.js` -- Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js` -- Account fallback logic: `open-sse/services/accountFallback.js` -- Translation registry: `open-sse/translator/index.js` -- Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js` -- Usage extraction/normalization: `open-sse/utils/usageTracking.js` - -## 3) Persistence Layer - -Primary state DB: - -- `src/lib/localDb.js` -- file: `${DATA_DIR}/db.json` (or `~/.9router/db.json` when `DATA_DIR` is unset) -- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing - -Usage DB: - -- `src/lib/usageDb.js` -- files: `~/.9router/usage.json`, `~/.9router/log.txt` -- note: currently independent from `DATA_DIR` - -## 4) Auth + Security Surfaces - -- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js` -- API key generation/verification: `src/shared/utils/apiKey.js` -- Provider secrets persisted in `providerConnections` entries -- Optional proxy support for upstream calls via env proxy variables (`open-sse/utils/proxyFetch.js`) - -## 5) Cloud Sync - -- Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js` -- Periodic task: `src/shared/services/cloudSyncScheduler.js` -- Control route: `src/app/api/sync/cloud/route.js` - -## Request Lifecycle (`/v1/chat/completions`) - -```mermaid -sequenceDiagram - autonumber - participant Client as CLI/SDK Client - participant Route as /api/v1/chat/completions - participant Chat as src/sse/handlers/chat - participant Core as open-sse/handlers/chatCore - participant Model as Model Resolver - participant Auth as Credential Selector - participant Exec as Provider Executor - participant Prov as Upstream Provider - participant Stream as Stream Translator - participant Usage as usageDb - - Client->>Route: POST /v1/chat/completions - Route->>Chat: handleChat(request) - Chat->>Model: parse/resolve model or combo - - alt Combo model - Chat->>Chat: iterate combo models (handleComboChat) - end - - Chat->>Auth: getProviderCredentials(provider) - Auth-->>Chat: active account + tokens/api key - - Chat->>Core: handleChatCore(body, modelInfo, credentials) - Core->>Core: detect source format - Core->>Core: translate request to target format - Core->>Exec: execute(provider, transformedBody) - Exec->>Prov: upstream API call - Prov-->>Exec: SSE/JSON response - Exec-->>Core: response + metadata - - alt 401/403 - Core->>Exec: refreshCredentials() - Exec-->>Core: updated tokens - Core->>Exec: retry request - end - - Core->>Stream: translate/normalize stream to client format - Stream-->>Client: SSE chunks / JSON response - - Stream->>Usage: extract usage + persist history/log -``` - -## Combo + Account Fallback Flow - -```mermaid -flowchart TD - A[Incoming model string] --> B{Is combo name?} - B -- Yes --> C[Load combo models sequence] - B -- No --> D[Single model path] - - C --> E[Try model N] - E --> F[Resolve provider/model] - D --> F - - F --> G[Select account credentials] - G --> H{Credentials available?} - H -- No --> I[Return provider unavailable] - H -- Yes --> J[Execute request] - - J --> K{Success?} - K -- Yes --> L[Return response] - K -- No --> M{Fallback-eligible error?} - - M -- No --> N[Return error] - M -- Yes --> O[Mark account unavailable cooldown] - O --> P{Another account for provider?} - P -- Yes --> G - P -- No --> Q{In combo with next model?} - Q -- Yes --> E - Q -- No --> R[Return all unavailable] -``` - -Fallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics. - -## OAuth Onboarding and Token Refresh Lifecycle - -```mermaid -sequenceDiagram - autonumber - participant UI as Dashboard UI - participant OAuth as /api/oauth/[provider]/[action] - participant ProvAuth as Provider Auth Server - participant DB as localDb - participant Test as /api/providers/[id]/test - participant Exec as Provider Executor - - UI->>OAuth: GET authorize or device-code - OAuth->>ProvAuth: create auth/device flow - ProvAuth-->>OAuth: auth URL or device code payload - OAuth-->>UI: flow data - - UI->>OAuth: POST exchange or poll - OAuth->>ProvAuth: token exchange/poll - ProvAuth-->>OAuth: access/refresh tokens - OAuth->>DB: createProviderConnection(oauth data) - OAuth-->>UI: success + connection id - - UI->>Test: POST /api/providers/[id]/test - Test->>Exec: validate credentials / optional refresh - Exec-->>Test: valid or refreshed token info - Test->>DB: update status/tokens/errors - Test-->>UI: validation result -``` - -Refresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`. - -## Cloud Sync Lifecycle (Enable / Sync / Disable) - -```mermaid -sequenceDiagram - autonumber - participant UI as Endpoint Page UI - participant Sync as /api/sync/cloud - participant DB as localDb - participant Cloud as External Cloud Sync - participant Claude as ~/.claude/settings.json - - UI->>Sync: POST action=enable - Sync->>DB: set cloudEnabled=true - Sync->>DB: ensure API key exists - Sync->>Cloud: POST /sync/{machineId} (providers/aliases/combos/keys) - Cloud-->>Sync: sync result - Sync->>Cloud: GET /{machineId}/v1/verify - Sync-->>UI: enabled + verification status - - UI->>Sync: POST action=sync - Sync->>Cloud: POST /sync/{machineId} - Cloud-->>Sync: remote data - Sync->>DB: update newer local tokens/status - Sync-->>UI: synced - - UI->>Sync: POST action=disable - Sync->>DB: set cloudEnabled=false - Sync->>Cloud: DELETE /sync/{machineId} - Sync->>Claude: switch ANTHROPIC_BASE_URL back to local (if needed) - Sync-->>UI: disabled -``` - -Periodic sync is triggered by `CloudSyncScheduler` when cloud is enabled. - -## Data Model and Storage Map - -```mermaid -erDiagram - SETTINGS ||--o{ PROVIDER_CONNECTION : controls - PROVIDER_NODE ||--o{ PROVIDER_CONNECTION : backs_compatible_provider - PROVIDER_CONNECTION ||--o{ USAGE_ENTRY : emits_usage - - SETTINGS { - boolean cloudEnabled - number stickyRoundRobinLimit - boolean requireLogin - string password_hash - } - - PROVIDER_CONNECTION { - string id - string provider - string authType - string name - number priority - boolean isActive - string apiKey - string accessToken - string refreshToken - string expiresAt - string testStatus - string lastError - string rateLimitedUntil - json providerSpecificData - } - - PROVIDER_NODE { - string id - string type - string name - string prefix - string apiType - string baseUrl - } - - MODEL_ALIAS { - string alias - string targetModel - } - - COMBO { - string id - string name - string[] models - } - - API_KEY { - string id - string name - string key - string machineId - boolean isActive - } - - USAGE_ENTRY { - string provider - string model - number prompt_tokens - number completion_tokens - string connectionId - string timestamp - } -``` - -Physical storage files: - -- main state: `${DATA_DIR}/db.json` (or `~/.9router/db.json`) -- usage stats: `~/.9router/usage.json` -- request log lines: `~/.9router/log.txt` -- optional translator/request debug sessions: `/logs/...` - -## Deployment Topology - -```mermaid -flowchart LR - subgraph LocalHost[Developer Host] - CLI[CLI Tools] - Browser[Dashboard Browser] - end - - subgraph ContainerOrProcess[9Router Runtime] - Next[Next.js Server\nPORT=20128] - Core[SSE Core + Executors] - MainDB[(db.json)] - UsageDB[(usage.json/log.txt)] - end - - subgraph External[External Services] - Providers[AI Providers] - SyncCloud[Cloud Sync Service] - end - - CLI --> Next - Browser --> Next - Next --> Core - Next --> MainDB - Core --> MainDB - Core --> UsageDB - Core --> Providers - Next --> SyncCloud -``` - -## Module Mapping (Decision-Critical) - -### Route and API Modules - -- `src/app/api/v1/*`, `src/app/api/v1beta/*`: compatibility APIs -- `src/app/api/providers*`: provider CRUD, validation, testing -- `src/app/api/provider-nodes*`: custom compatible node management -- `src/app/api/oauth/*`: OAuth/device-code flows -- `src/app/api/keys*`: local API key lifecycle -- `src/app/api/models/alias`: alias management -- `src/app/api/combos*`: fallback combo management -- `src/app/api/pricing`: pricing overrides for cost calculation -- `src/app/api/usage/*`: usage and logs APIs -- `src/app/api/sync/*` + `src/app/api/cloud/*`: cloud sync and cloud-facing helpers -- `src/app/api/cli-tools/*`: local CLI config writers/checkers - -### Routing and Execution Core - -- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop -- `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup -- `open-sse/executors/*`: provider-specific network and format behavior - -### Translation Registry and Format Converters - -- `open-sse/translator/index.js`: translator registry and orchestration -- Request translators: `open-sse/translator/request/*` -- Response translators: `open-sse/translator/response/*` -- Format constants: `open-sse/translator/formats.js` - -### Persistence - -- `src/lib/localDb.js`: persistent config/state -- `src/lib/usageDb.js`: usage history and rolling request logs - -## Provider Executor Coverage - -Specialized executors: - -- `antigravity` -- `gemini-cli` -- `github` -- `kiro` -- `codex` -- `cursor` - -Default executor path: - -- all other providers (including compatible node providers) use `open-sse/executors/default.js` - -## Format Translation Coverage - -Detected source formats include: - -- `openai` -- `openai-responses` -- `claude` -- `gemini` - -Target formats include: - -- OpenAI chat/Responses -- Claude -- Gemini/Gemini-CLI/Antigravity envelope -- Kiro -- Cursor - -Translations are selected dynamically based on source payload shape and provider target format. - -## Failure Modes and Resilience - -## 1) Account/Provider Availability - -- provider account cooldown on transient/rate/auth errors -- account fallback before failing request -- combo model fallback when current model/provider path is exhausted - -## 2) Token Expiry - -- pre-check and refresh with retry for refreshable providers -- 401/403 retry after refresh attempt in core path - -## 3) Stream Safety - -- disconnect-aware stream controller -- translation stream with end-of-stream flush and `[DONE]` handling -- usage estimation fallback when provider usage metadata is missing - -## 4) Cloud Sync Degradation - -- sync errors are surfaced but local runtime continues -- scheduler has retry-capable logic, but periodic execution currently calls single-attempt sync by default - -## 5) Data Integrity - -- DB shape migration/repair for missing keys -- corrupt JSON reset safeguards for localDb and usageDb - -## Observability and Operational Signals - -Runtime visibility sources: - -- console logs from `src/sse/utils/logger.js` -- per-request usage aggregates in `usage.json` -- textual request status log in `log.txt` -- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true` -- dashboard usage endpoints (`/api/usage/*`) for UI consumption - -## Security-Sensitive Boundaries - -- JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing -- Initial password fallback (`INITIAL_PASSWORD`, default `123456`) must be overridden in real deployments -- API key HMAC secret (`API_KEY_SECRET`) secures generated local API key format -- Provider secrets (API keys/tokens) are persisted in local DB and should be protected at filesystem level -- Cloud sync endpoints rely on API key auth + machine id semantics - -## Environment and Runtime Matrix - -Environment variables actively used by code: - -- App/auth: `JWT_SECRET`, `INITIAL_PASSWORD` -- Storage: `DATA_DIR` -- Security hashing: `API_KEY_SECRET`, `MACHINE_ID_SALT` -- Logging: `ENABLE_REQUEST_LOGS` -- Sync/cloud URLing: `NEXT_PUBLIC_BASE_URL`, `NEXT_PUBLIC_CLOUD_URL` -- Outbound proxy: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` and lowercase variants -- Platform/runtime helpers (not app-specific config): `APPDATA`, `NODE_ENV`, `PORT`, `HOSTNAME` - -## Known Architectural Notes - -1. `usageDb` currently stores under `~/.9router` and does not follow `DATA_DIR`. -2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`. -3. Request logger writes full headers/body when enabled; treat log directory as sensitive. -4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability. - -## Operational Verification Checklist - -- Build from source: `cd /root/dev/9router && npm run build` -- Build Docker image: `cd /root/dev/9router && docker build -t 9router .` -- Start service and verify: -- `GET /api/settings` -- `GET /api/v1/models` -- CLI target base URL should be `http://:20128/v1` when `PORT=20128` diff --git a/9router/eslint.config.mjs b/9router/eslint.config.mjs deleted file mode 100644 index f4438352..00000000 --- a/9router/eslint.config.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; - -const eslintConfig = defineConfig([ - ...nextVitals, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ]), -]); - -export default eslintConfig; diff --git a/9router/i18n/README.vi.md b/9router/i18n/README.vi.md deleted file mode 100644 index 65dce166..00000000 --- a/9router/i18n/README.vi.md +++ /dev/null @@ -1,1310 +0,0 @@ -Dưới đây là bản dịch tiếng Việt của tài liệu Markdown, giữ nguyên toàn bộ cú pháp và cấu trúc kỹ thuật. - -
- Bảng điều khiển 9Router - - # 9Router - Free AI Router - - **Không bao giờ ngừng code. Tự động định tuyến tới các mô hình AI MIỄN PHÍ & giá rẻ với cơ chế dự phòng thông minh.** - - **Nhà cung cấp AI Miễn cho OpenClaw.** - -

- OpenClaw -

- - [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) - [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router) - [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) - - [🚀 Bắt đầu nhanh](#-quick-start) • [💡 Tính năng](#-key-features) • [📖 Cài đặt](#-setup-guide) • [🌐 Website](https://9router.com) -
- ---- - -## 🤔 Tại sao chọn 9Router? - -**Ngừng lãng phí tiền bạc và gặp phải giới hạn:** - -- ❌ Hạn mức gói đăng ký hết hạn mỗi tháng mà không dùng hết -- ❌ Giới hạn tốc độ (rate limit) ngăn bạn giữaừng khi code -- ❌ Các API đắt đỏ ($20-50/tháng cho mỗi nhà cung cấp) -- ❌ Phải chuyển đổi thủ công giữa các nhà cung cấp - -**9Router giải quyết vấn đề này:** - -- ✅ **Tối đa hóa gói đăng ký** - Theo dõi hạn mức, sử dụng từng bit trước khi reset -- ✅ **Tự động dự phòng** - Gói đăng ký → Giá rẻ → Miễn phí, thời gian chết bằng không -- ✅ **Đa tài khoản** - Vòng tròn (round-robin) các tài khoản của mỗi nhà cung cấp -- ✅ **Phổ quát** - Hoạt động với Claude Code, Codex, Gemini CLI, Cursor, Cline, bất kỳ công cụ CLI nào - ---- - -## 🔄 Cách thức hoạt động - -``` -┌─────────────┐ -│ Your CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...) -│ Tool │ -└──────┬──────┘ - │ http://localhost:20128/v1 - ↓ -┌────────────────────────────────────────┐ -│ 9Router (Smart Router) │ -│ • Format translation (OpenAI ↔ Claude) │ -│ • Quota tracking │ -│ • Auto token refresh │ -└──────┬──────────────────────────────────┘ - │ - ├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, Gemini CLI - │ ↓ quota exhausted - ├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M) - │ budget limit - └─→ [Tier 3: FREE] iFlow, Qwen, Kiro (unlimited) - -Result: Never stop coding, minimal cost -``` - ---- - -## ⚡ Bắt đầu nhanh - -**1. Cài đặt toàn cục:** - -```bash -npm install -g 9router -9router -``` - -🎉 Bảng điều khiển mở tại `http://localhost:20128` - -**2. Kết nối nhà cung cấp MIỄN PHÍ (không cần đăng ký):** - -Bảng điều khiển → Providers -> Kết nối **ude Code** hoặc **Antigravity** -> Đăng nhập OAuth -> Xong! - -**3. Sử dụng trong công cụ CLI của bạn:** - -``` -Cài đặt Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline: - Endpoint: http://localhost:20128/v1 - API Key: [sao chép từ bảng điều khiển] - Model: if/kimi-k2-thinking -``` - -**Xong rồi!** Bắt đầu code với các mô hình AI MIỄN PHÍ. - -**Phương án khác: chạy từ nguồn (k lưu trữ này):** - -Gói kho lưu trữ này là riêng tư (`9router-app`), vì vậy việc thực thi nguồn/Docker là đường dẫn phát triển cục bộ dự kiến. - -```bash -cp .env.example .env -npm install -PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev -``` - -Chế độ Production: - -```bash -npm run build -PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start -``` - -URL mặc định: -- Bảng điều khiển: `http://localhost:20128/dashboard` -- API tương thích OpenAI: `http://localhost:20128/v1` - ---- - -## 🎥 Hướng dẫn Video - -
- -### 📺 Hướng dẫn thiết lập hoàn chỉnh - 9Router + Claude Code MIỄN PHÍ - -[![Thiết lập 9Router + Claude Code](https://img.youtube.com/vi/raEyZPg5xE0/maxresdefault.jpg)](https://www.youtube.com/watch?v=raEyZPg5xE0) - -**🎬 Xem hướng dẫn từng đầy đủ:** -- ✅ Cài đặt & thiết lập 9Router -- ✅ Cấu hình Claude Sonnet 4.5 MIỄN PHÍ -- ✅ Tích hợp Claude Code -- ✅ Thử nghiệm code trực tiếp - -**⏱️ Thời lượng:** 20 phút | **👥 Bởi:** Cộng đồng Nhà phát triển - -[▶️ Xem trên YouTube](https://www.youtube.com/watch?v=o3qYCyjrFYg) - -
- ---- - -## 🛠️ Các công cụ CLI được hỗ trợ - -9Router hoạt động liền mạch với tất cả các công cụ code AI chính: - -
- - - - - - - - - - - - - - - - - -
- Claude Code
- Claude-Code -
- OpenClaw
- OpenClaw -
- Codex
- Codex -
- OpenCode
- OpenCode -
- Cursor
- Cursor -
- Antigravity
- Antigravity -
- Cline
- Cline -
- Continue
- Continue -
- Droid
- Droid -
- Roo
- Roo -
- Copilot
- Copilot -
- Kilo Code
- Kilo Code -
-
- ---- - -## Các nhà cung cấp được hỗ trợ - -### 🔐 Các nhà cung cấp OAuth - -
- - - - - - - - -
- Claude Code
- Claude-Code -
- Antigravity
- Antigravity -
- Codex
- Codex -
- GitHub
- GitHub -
- Cursor
- Cursor -
-
- -### 🆓 Các nhà cung cấp Miễn phí - -
- - - - - - - -
- iFlow
- iFlow AI
- 8+ mô hình • Không giới hạn -
- Qwen
- Qwen Code
- 3+ mô hình • Không giới hạn -
- Gemini CLI
- Gemini CLI
- 180K/tháng MIỄN PHÍ -
- Kiro
- Kiro AI
- Claude • Không giới hạn -
-
- -### 🔑 Các nhà cung cấp API Key (40+) - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- OpenRouter
- OpenRouter -
- GLM
- GLM -
- Kimi
- Kimi -
- MiniMax
- MiniMax -
- OpenAI
- OpenAI -
- Anthropic
- Anthropic -
- Gemini
- Gemini -
- DeepSeek
- DeepSeek -
- Groq
- Groq -
- xAI
- xAI -
- Mistral
- Mistral -
- Perplexity
- Perplexity -
- Together
- Together AI -
- Fireworks
- Fireworks -
- Cerebras
- Cerebras -
- Cohere
- Cohere -
- NVIDIA
- NVIDIA -
- SiliconFlow
- SiliconFlow -
-

...và hơn 20 nhà cung cấp khác bao gồm Nebius, Chutes, Hyperbolic và các endpoint tương thích OpenAI/Anthropic tùy chỉnh

-
- ---- - -## 💡 Các tính năng chính - -| Tính năng | Công dụng | Tại sao nó trọng | -|---------|--------------|----------------| -| 🎯 **Smart 3-Tier Fallback** | Tự động định tuyến: Gói đăng ký → Giá rẻ → Miễn phí | Không bao giờ ngừng code, thời gian chết bằng không | -| 📊 **Theo dõi hạn mức thời gian thực** | Đếm token trực tiếp + đếm ngược reset | Tối đa hóa giá trị gói đăng ký | -| 🔄 **Dịch chuyển định dạng** | OpenAI ↔ Claude ↔ Gemini liền mạch | Hoạt động với mọi công cụ CLI | -| 👥 **Hỗ trợ Đa tài khoản** | Nhiều tài khoản cho mỗi nhà cung cấp | Cân bằng tải + dự phòng | -| 🔄 **Tự động làm mới Token** | Token OAuth tự động làm mới | Không cần đăng nhập lại thủ công | -| 🎨 **Combo tùy chỉnh** | Tạo tổ hợp mô hình không giới hạn | Điều chỉnh dự phòng theo nhu cầu | -| 📝 **Ghi log Request** | Chế độ gỡ lỗi với log request/response đầy đủ | Dễ dàng khắc phục sự cố | -| 💾 **Đồng bộ đám mây** | Đồng bộ cấu hình giữa các thiết bị | Cài đặt giống nhau ở mọi nơi | -| 📊 **Phân tích sử dụng** | Theo dõi token, chi phí, xu hướng theo thời gian | Tối ưu hóa chi tiêu | -| 🌐 **Triển khai ở bất cứ đâu** | Localhost, VPS, Docker, Cloudflare Workers | Tùy chọn triển khai linh hoạt | - -
-📖 Chi tiết tính năng - -### 🎯 Smart 3-Tier Fallback - -Tạo combo với tính năng phòng tự động: - -``` -Combo: "my-coding-stack" - 1. cc/claude-opus-4-6 (gói đăng ký của bạn) - 2. glm/glm-4.7 (backup giá rẻ, $0.6/1M) - 3. if/kimi-k2-thinking (dự phòng miễn phí) - -→ Tự động chuyển đổi khi hết hạn mức hoặc xảy ra lỗi -``` - -### 📊 Theo dõi hạn mức thời gian thực - -- Mức tiêu thụ token cho mỗi nhà cung cấp -- Đếm ngược reset (5 giờ, hàng ngày, hàng tuần) -- Ước tính chi phí cho các tầng trả phí -- Báo cáo chi tiêu hàng tháng - -### 🔄 Dịch chuyển định dạng - -Dịch chuyển liền mạch giữa các định dạng: -- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **OpenAI Responses** -- Công cụ CLI của bạn gửi định dạng OpenAI → 9Router dịch chuyển → Nhà cung cấp nhận định dạng gốc -- Hoạt động với mọi công cụ hỗ trợ endpoint OpenAI tùy chỉnh - -### 👥 Hỗ trợ Đa tài khoản - -- Thêm nhiều tài khoản cho mỗi nhà cung cấp -- Định tuyến vòng tròn (round-robin) hoặc dựa trên ưu tiên tự động -- Dự phòng sang tài khoản tiếp theo khi một tài khoản chạm hạn mức - -### 🔄 Tự động làm mới Token - -- Token OAuth tự động làm mới trước khi hết hạn -- Không cần xác thực lại thủ công -- Trải nghiệm liền mạch trên mọi nhà cung cấp - -### 🎨 Combo tùy chỉnh - -- Tạo tổ hợp mô hình không hạn -- Kết hợp các tầng gói đăng ký, giá rẻ và miễn phí -- Đặt tên combo để dễ truy cập -- Chia sẻ combo giữa các thiết bị với Đồng bộ đám mây - -### 📝 Ghi log Request - -- Bật chế độ gỡ lỗi để xem log request/response đầy đủ -- Theo dõi các lệnh gọi API, tiêu đề và payload -- Khắc phục sự cố tích hợp -- Xuất log để phân tích - -### 💾 Đồng bộ đám mây - -- Đồng bộ nhà cung cấp, combo và c đặt giữa các thiết bị -- Tự động đồng bộ nền -- Lưu trữ được mã hóa an toàn -- Truy cập cài đặt của bạn từ bất cứ đâu - -#### Ghi chú Runtime Đám mây - -- Ưu tiên biến đám mây phía máy chủ trong môi trường production: - - `BASE_URL` (URL callback nội bộ được sử dụng bởi bộ lập lịch đồng bộ) - - `CLOUD_URL` (cơ sở endpoint đồng bộ đám mây) -- `NEXT_PUBLIC_BASE_URL` và `NEXT_PUBLIC_CLOUD_URL` vẫn được hỗ trợ để thích/UI, nhưng runtime máy chủ hiện ưu tiên `BASE_URL`/`CLOUD_URL`. -- Các yêu cầu đồng bộ đám mây hiện sử dụng thời gian chờ + hành vi fail-fast để tránh treo UI khi DNS/mạng đám mây không khả dụng. - -### 📊 Phân tích sử dụng - -- Theo dõi mức sử dụng token theo nhà cung cấp và mô hình -- Ước tính chi phí và xu hướng chi tiêu -- Báo cáo và thông tin chi tiết hàng tháng -- Tối ưu hóa chi tiêu AI của bạn - -> **💡AN TRỌNG - Hiểu về Chi phí trên Bảng điều khiển:** -> -> "Chi phí" hiển thị trong Phân tích sử dụng là **chỉ để theo dõi và so sánh**. -> Bản thân 9Router **không bao giờ thu phí** bạn bất cứ thứ gì. Bạn chỉ trả tiền trực tiếp cho các nhà cung cấp (nếu sử dụng dịch vụ trả phí). -> -> **Ví dụ:** Nếu bảng điều khiển của bạn hiển thị "tổng chi phí $290" trong khi sử dụng các mô hìnhFlow, điều này đại diện cho -> số tiền bạn sẽ phải trả nếu sử dụng API trả phí trực tiếp. Chi phí thực tế của bạn = **$0** (iFlow miễn phí không giới hạn). -> -> Hãy coi nó như một "trình theo dõi tiết kiệm" cho thấy bạn đang tiết kiệm được bao nhiêu bằng cách sử dụng các mô hình miễn phí hoặc -> định tuyến qua 9Router! - -### 🌐 Triển khai ở bất cứ đâu - -- 💻 **Localhost** - Mặc định, hoạt động ngoại tuyến - ☁️ **VPS/Cloud** - Chia sẻ giữa các thiết bị -- 🐳 **Docker** - Triển khai bằng một lệnh -- 🚀 **Cloudflare Workers** - Mạng edge toàn cầu - -
- ---- - -## 💰 Tổng quan về giá - -| Hạng mục | Nhà cung cấp | Chi phí | Reset Hạn mức | Tốt nhất cho | -|------|----------|------|-------------|----------| -| **💳 GÓI ĐĂNG KÝ** | Claude Code (Pro) | $20/tháng | 5h + hàng tuần | Đã đăng ký rồi | -| | Codex (Plus/Pro) | $20-200/tháng | 5h + hàng tuần | Người dùng OpenAI | -| | Gemini CLI | **MIỄN PHÍ** | 180K/tháng + 1K/ngày | Tất cả mọi người! | -| | GitHub Copilot | $10-19/tháng | Hàng tháng | Người dùng GitHub | -| **💰 GIÁ RẺ** | GLM-4.7 | $0.6/1M | 10AM hàng ngày | Backup ngân sách | -| | MiniMax M21 | $0.2/1M | 5 giờ luân phiên | Lựa chọn rẻ nhất | -| | Kimi K2 | $9/tháng cố định | 10M token/tháng | Chi phí dự đoán được | -| **🆓 MIỄN PHÍ** | iFlow | $0 | Không giới hạn | 8 mô hình miễn phí | -| | Qwen | $0 | Không giới hạn | 3 mô hình miễn phí | -| | Kiro | $0 | Không giới hạn | Claude miễn phí | - -**💡 Mẹo Chuyên nghiệp:** Bắt với combo Gemini CLI (180K miễn phí/tháng) + iFlow (không giới hạn miễn phí) = chi phí $0! - ---- - -### 📊 Hiểu về Chi phí & Thanh toán của 9Router - -**Thực tế Thanh toán 9Router:** - -✅ **Phần mềm 9Router = MIỄN PHÍ mãi mãi** (mã nguồn mở, không bao giờ thu phí) -✅ **"Chi phí" trên bảng điều khiển = Chỉ để Hiển thị/Theo dõi** (không phải hóa đơn thực tế) - **Bạn trả tiền trực tiếp cho nhà cung cấp** (gói đăng ký hoặc phí API) -✅ **Nhà cung cấp MIỄN PHÍ vẫn MIỄN PHÍ** (iFlow, Kiro, Qwen = $0 không giới hạn) -❌ **9Router không bao giờ gửi hóa đơn** hoặc tính phí thẻ của bạn - -**Cách Hoạt động của Hiển thị Chi phí:** - -Bảng điều khiển hiển thị **chi phí ước tính** như thể bạn đang sử dụng API trả phí trực tiếp. Đây **không phải là thanh toán** - đó là công cụ so sánh để cho thấy mức tiết kiệm của bạn. - -**Kịch bản Ví dụ:** -``` -Hiển thị trên Bảng điều khiển: -• Tổng số Request: 1,662 -• Tổng số Token: 47M -• Chi phí Hiển thị: $290 - -Kiểm tra Thực tế: -• Nhà cung cấp: iFlow (MIỄN PHÍ không giới hạn) -• Thanh toán Thực tế: $0.00 -• Ý nghĩa của $290: Số tiền bạn TIẾT KIỆM được bằng cách sử dụng mô hình miễn phí! -``` - -**Quy tắc Thanh toán:** -- **Nhà cung cấp gói đăng ký** (Claude Code, Codex): Trả tiền trực tiếp cho họ qua website của họ -- **Nhà cung cấp giá rẻ** (GLM, MiniMax): Trả tiền trực tiếp cho họ, 9Router chỉ định tuyến -- **Nhà cung cấp MIỄN PHÍ** (iFlow, Kiro, Qwen): Thực sự miễn phí mãi mãi, không có phí ẩn -- **9**: Không bao giờ thu phí bất cứ thứ gì, ever - ---- - -## 🎯 Trường hợp sử dụng - -### Trường hợp 1: "Tôi có gói đăng ký Claude Pro" - -**Vấn đề:** Hạn mức hết hạn không dùng, giới hạn tốc độ khi code nặng - -**Giải pháp:** -``` -Combo: "maximize-claude" - 1. cc/claude-opus-4-6 (sử dụng đầy đủ gói đăng ký) - 2. glm/glm-4.7 (backup giá rẻ khi hết hạn mức 3. if/kimi-k2-thinking (dự phòng khẩn cấp miễn phí) - -Chi phí hàng tháng: $20 (gói đăng ký) + ~$5 (backup) = $25 tổng cộng -so với $20 + chạm giới hạn = sự thất vọng -``` - -### Trường hợp 2: "Tôi muốn chi phí bằng không" - -**Vấn đề:** Không đủ khả năng trả gói đăng ký, cần code AI đáng tin cậy - -**Giải pháp:** -``` -Combo: "free-forever" - 1. gc/gini-3-flash (180K miễn phí/tháng) - 2. if/kimi-k2-thinking (không giới hạn miễn phí) - 3. qw/qwen3-coder-plus (không giới hạn miễn phí) - -Chi phí hàng tháng: $0 -Chất lượng: Các mô hình sẵn sàng cho production -``` - -### Trường hợp 3: "Tôi cần code 24/7, không gián đoạn" - -**Vấn đề:** Deadline, không thể để thời gian chết - -**Giải pháp:** -``` -Combo: "always-on" - 1. cc/claude-opus-4-6 (chất lượng tốt nhất) - 2. cx/gpt-5.2-codex (gói đăng ký thứ hai) - 3. glm/glm-4.7 (giá rẻ, reset hàng ngày) - 4. minimax/MiniMax-M2.1 (rẻ nhất, reset 5h) - 5. if/kimi-k2-thinking (miễn phí không giới hạn) - -Kết quả: 5 lớp dự phòng = thời gian chết bằng không -Chi phí tháng: $20-200 (gói đăng ký) + $10-20 (backup) -``` - -### Trường hợp 4: "Tôi muốn AI MIỄN PHÍ trong OpenClaw" - -**Vấn đề:** Cần trợ lý AI trong các ứng dụng nhắn tin (WhatsApp, Telegram, Slack...), hoàn toàn miễn phí - -**Giải pháp:** -``` -Combo: "openclaw-free" - 1. if/glm-4.7 (không giới hạn miễn phí) - 2. if/minimax-m2.1 (không giới hạn phí) - 3. if/kimi-k2-thinking (không giới hạn miễn phí) - -Chi phí hàng tháng: $0 -Truy cập qua: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... -``` - ---- - -## ❓ Các câu hỏi thường gặp - -
-📊 Tại sao bảng điều khiển của tôi hiển thị chi phí cao? - -Bảng điều khiển theo dõi mức sử dụng token của bạn và hiển thị **chi phí ước tính** như thể bạn đang sử dụng API trả phí trực tiếp. Đâykhông phải là thanh toán thực tế** - đó là tài liệu tham khảo để cho thấy bạn đang tiết kiệm được bao nhiêu bằng cách sử dụng các mô hình miễn phí hoặc gói đăng ký hiện có thông qua 9Router. - -**Ví dụ:** -- **Bảng điều khiển hiển thị:** "Tổng chi phí $290" -- **Thực tế:** Bạn đang sử dụng iFlow (MIỄN PHÍ không giới hạn) -- **Chi phí thực tế của bạn:** **$0.00** -- **Ý nghĩa của $290:** Số bạn **tiết kiệm** được bằng cách sử dụng các mô hình miễn phí thay vì API trả phí! - -Màn hình chi phí là một "trình theo dõi tiết kiệm" để giúp bạn hiểu các mẫu sử dụng và cơ hội tối ưu hóa. - -
- -
-💳 Tôi có bị 9Router tính phí không? - -**Không.** 9Router là phần mềm miễn phí, mã nguồn mở chạy trên máy tính của chính bạn. Nó không bao giờ tính phí bạn bất cứ thứ gì. - -**Bạn chỉ trả tiền:** -- ✅ **Nhà cung cấp gói đăng ký** (Claude Code $20/tháng, Codex $20-200/tháng) → Trả tiền trực tiếp cho họ trên website của họ -- ✅ **Nhà cung cấp giá rẻ** (GLM, MiniMax) → Trả tiền trực tiếp cho họ, 9Router chỉ định tuyến yêu cầu của bạn -- ❌ **Bản thân 9Router** → **Không bao giờ tính phí bất cứ thứ gì, ever** - -9Router là một proxy/router cục bộ. Nó không cóẻ tín dụng của bạn, không thể gửi hóa đơn và không có hệ thống thanh toán. Đó là phần mềm hoàn toàn miễn phí. - -
- -
-🆓 Các nhà cung cấp MIỄN PHÍ có thực sự không giới hạn không? - -**Có!** Các nhà cung cấp được đánh dấu là MIỄN PHÍ (iFlow, Kiro, Qwen) thực sự không giới hạn với **không có phí ẩn**. - -Đây là các dịch vụ miễn phí được cung cấp bởi các công ty tương ứng: -- **iFlow**: Truy cập miễn phí không giới hạn vào hơn 8 mô hình qua OAuth -- **Kiro**: Các mô hình Claude miễn phí không giới hạn qua AWS Builder ID -- **Qwen**: Truy cập miễn phí không giới hạn vào các mô hình Qwen qua xác thực thiết bị - -9Router chỉ định tuyến yêu cầu của bạn đến họ - không có "cạm bẫy" hay thanh toán trong tương lai. Đó là các dịch vụ thực sự miễn phí, và 9Router giúp chúng dễ sử dụng với hỗ trợ dự phòng. - -**Lưu ý:** số nhà cung cấp gói đăng ký (Antigravity, GitHub Copilot) có thể có các khoảng thời gian dùng thử miễn phí có thể trở thành trả phí sau này, nhưng điều này sẽ được các nhà cung cấp đó thông báo rõ ràng, không phải 9Router. - -
- -
-💰 Làm thế nào để giảm thiểu chi phí AI thực tế của tôi? - -**Chiến lược Ưu tiên Miễn phí:** - -1. **Bắt đầu với combo 100% miễn phí:** - ``` - 1. gc/gemini-3-flash (180K/tháng miễn phí từ Google) - 2. if/kimi-k2-thinking (không giới hạn miễn phí từ iFlow) - 3. qw/qwen3-coder-plus (không giới hạn miễn phí từ Qwen) - ``` - **Chi phí: $0/tháng** - -2. **Thêm backup giá rẻ** chỉ khi bạn cần: - ``` - 4. glm/glm-4.7 ($0.6/1M token) - ``` - **Chi phí bổ sung:** Chỉ trả tiền cho những gì bạn sự sử dụng - -3. **Sử dụng nhà cung cấp gói đăng ký cuối cùng:** - - Chỉ khi bạn đã có chúng - - 9Router giúp tối đa hóa giá trị của chúng thông qua theo dõi hạn mức - -**Kết quả:** Hầu hết người dùng có thể hoạt động ở mức $0/tháng chỉ sử dụng các tầng miễn phí! - -
- -
-📈 Điều gì xảy ra nếu mức sử dụng của tôi đột ngột tăng vọt? - -Cơ chế dự phòng thông minh của 9Router ngăn chặn các khoản phí bất ngờ: - -**Kịch bản:** Bạn đang trong giai đoạn code nước rút và vượt qua các hạn mức - -**Không có 9Router:** -- ❌ Chạm giới hạn tốc độ → Công việc dừng lại → Thất vọng -- ❌ Hoặc: Vô tình tích lũy hóa đơn API khổng lồ - -**Có 9Router:** -- ✅ Gói đăng ký chạm giới hạn → Tự động dự phòng sang tầng giá rẻ -- ✅ Tầng giá rẻ trở nên đắt đỏ → Tự động dự phòng sang tầng miễn phí -- ✅ Không bao giờ ngừng code → Chi phí dự đoán được - -**Bạn nắm quyền kiểm soát:** Đặt giới hạn chi tiêu cho mỗi nhà cung cấp trong bảng điều khiển, và 9Router sẽ tuân thủ chúng. - -
- ---- - -## 📖 Hướng dẫn thiết lập - -
-🔐 Các nhà cung cấp Gói đăng ký (Tối đa hóa Giá trị) - -### Claude Code (Pro/Max) - -```bash -Bảng điều khiển → Providers → Kết nối Claude Code -→ Đăng nhập OAuth → Tự động làm mới token -→ Theo dõi hạn mức 5 giờ + hàng tuần - -Các mô hình: - cc/claude-opus-4-6 - cc/claude-sonnet-4-5-20250929 - cc/claude-haiku-4-5-20251001 -``` - -**Mẹo Chuyên nghiệp:** Sử dụng Opus cho các tác vụ phức tạp, Sonnet cho tốc độ. 9Router theo dõi hạn mức cho mỗi mô hình! - -### OpenAI Codex (Plus/Pro) - -```bash -Bảng điều khiển → Providers → Kết nối Codex -→ Đăng nhập OAuth (cổng 1455) -→ Reset 5 giờ + hàng tuần - -Các mô hình: - cx/gpt-5.2-codex - cx/gpt-5.1-codex-max -``` - -### Gemini CLI (MIỄN PHÍ 180K/tháng!) - -```bash -Bảng điều khiển → Providers → Kết nối Gemini CLI -→ Google OAuth -→ 180K hoàn thành/tháng + 1K/ngày - -Các hình: - gc/gemini-3-flash-preview - gc/gemini-2.5-pro -``` - -**Giá trị tốt nhất:** Tầng miễn phí khổng lồ! Sử dụng cái này trước các tầng trả phí. - -### GitHub Copilot - -```bash -Bảng điều khiển → Providers → Kết nối GitHub -→ OAuth qua GitHub -→ Reset hàng tháng (ngày 1 của tháng) - -Các mô hình: - gh/gpt-5 - gh/claude-4.5-sonnet - gh/gemini-3-pro -` - -
- -
-💰 Các nhà cung cấp Giá rẻ (Backup) - -### GLM-4.7 (Reset hàng ngày, $0.6/1M) - -1. Đăng ký: [Zhipu AI](https://open.bigmodel.cn/) -2. Lấy API key từ Coding Plan -3. Bảng điều khiển → Thêm API Key: - - Nhà cung cấp: `glm` - - API Key: `your-key` - -**Sử dụng:** `glm/glm-4.7` - -**Mẹo Ch nghiệp:** Coding Plan cung cấp hạn mức gấp 3 lần với chi phí 1/7! Reset hàng ngày lúc 10:00 AM. - -### MiniMax M2.1 (Reset 5h, $0.20/1M) - -1. Đăng ký: [MiniMax](https://www.minimax.io/) -2. Lấy API key -3. Bảng điều khiển → Thêm API Key - -**Sử dụng:** `minimax/MiniMax-M2.1` - -**Mẹo Chuyên nghiệp:** Lựa chọn rẻ nhất cho ngữ cảnh dài (1M)! - -### Kimi K2 ($9/tháng cố định) - -1. Đăng ký: [Moonshot AI](https://platform.moonshot.ai/) -2. Lấy API key -3. Bảng điều khiển → Thêm API Key - -**Sử dụng:** `kimi/kimi-latest` - -**Mẹo Chuyên nghiệp:** Cố định $9/tháng cho 10M token = chi phí thực tế $0.90/1M! - -
- -
-🆓 Các nhà cung cấp MIỄN PHÍ (Dự phòng Khẩn cấpb> - -### iFlow (8 mô hình MIỄN PHÍ) - -```bash -Bảng điều khiển → Kết nối iFlow -→ Đăng nhập OAuth iFlow -→ Sử dụng không giới hạn - -Các mô hình: - if/kimi-k2-thinking - if/qwen3-coder-plus - if/glm-4.7 - if/minimax-m2 - if/deepseek-r1 -``` - -### Qwen (3 mô hình MIỄN PHÍ) - -```bash -Bảng điều khiển → Kết nối Qwen - Ủy quyền mã thiết bị -→ Sử dụng không giới hạn - -Các mô hình: - qw/qwen3-coder-plus - qw/qwen3-coder-flash -``` - -### Kiro (Claude MIỄN PHÍ) - -```bash -Bảng điều khiển → Kết nối Kiro -→ AWS Builder ID hoặc Google/GitHub -→ Sử dụng không giới hạn - -Các mô hình: - kr/claude-sonnet-4.5 - kr/claude-haiku-4.5 -``` - -
- -
-🎨 Tạo Combo - -### Ví dụ 1: Tối đa hóa Gói đăng ký → Backup Giá rẻ - -``` -Bảng điều khiển → Combos → Tạo Mới - -Tên: premium-coding -Các mô hình: - 1. cc/claude-opus-4-6 (Gói đăng ký chính) - 2. glm/glm-4.7 (Backup giá rẻ, $0.6/1M) - 3. minimax/MiniMax-M2.1 (Dự phòng rẻ nhất, $0.20/M) - -Sử dụng trong CLI: premium-coding - -Ví dụ chi phí hàng tháng (100M token): - 80M qua Claude (gói đăng ký): $0 thêm - 15M qua GLM: $9 - 5M qua MiniMax: $1 - Tổng: $10 + gói đăng ký của bạn -``` - -### Ví dụ 2: Chỉ Miễn phí (Chi phí bằng không) - -``` -Tên: free-combo -Các mô hình: - 1. gc/gemini-3-flash-preview (180K miễn phíáng) - 2. if/kimi-k2-thinking (không giới hạn) - 3. qw/qwen3-coder-plus (không giới hạn) - -Chi phí: $0 mãi mãi! -``` - -
- -
-🔧 Tích hợp CLI - -### Cursor IDE - -``` -Settings → Models → Advanced: - OpenAI API Base URL: http://localhost:20128/v1 - OpenAI API Key: [từ bảng điều khiển 9router] - Model: cc/claude-opus-4-6 -``Hoặc sử dụng combo: `premium-coding` - -### Claude Code - -Chỉnh sửa `~/.claude/config.json`: - -```json -{ - "anthropic_api_base": "http://localhost:20128/v1", - "anthropic_api_key": "your-9router-api-key" -} -``` - -### Codex CLI - -```bash -export OPENAI_BASE_URL="http://localhost:20128" -export OPENAI_API_KEY="your-9router-api-key" - -codex "prompt của bạn" -``` - -### OpenClaw - -**Phương án 1 — B điều khiển (khuyên dùng):** - -``` -Bảng điều khiển → CLI Tools → OpenClaw → Chọn Mô hình → Áp dụng -``` - -**Phương án 2 — Thủ công:** Chỉnh sửa `~/.openclaw/openclaw.json`: - -```json -{ - "agents": { - "defaults": { - "model": { - "primary": "9router/if/glm-4.7" - } - } - }, - "models": { - "providers": { - "9router": { - "baseUrl": "://127.0.0.1:20128/v1", - "apiKey": "sk_9router", - "api": "openai-completions", - "models": [ - { - "id": "if/glm-4.7", - "name": "glm-4.7" - } - ] - } - } - } -} -``` - -> **Lưu ý:** OpenClaw chỉ hoạt động với 9Router cục bộ. Sử dụng `127.0.0.1` thay vì `localhost` để tránh các vấn đề phân giải6. - -### Cline / Continue / RooCode - -``` -Provider: OpenAI Compatible -Base URL: http://localhost:20128/v1 -API Key: [từ bảng điều khiển] -Model: cc/claude-opus-4-6 -``` - -
- -
-🚀 Triển khai - -### Triển khai VPS - -```bash -# Clone và cài đặt -git clone https://github.com/decolua/9router.git -cd 9router -npm install -npm run build - -# Cấu hình -export JWT="your-secure-secret-change-this" -export INITIAL_PASSWORD="your-password" -export DATA_DIR="/var/lib/9router" -export PORT="20128" -export HOSTNAME="0.0.0.0" -export NODE_ENV="production" -export NEXT_PUBLIC_BASE_URL="http://localhost:20128" -export NEXT_PUBLIC_CLOUD_URL="https://9router.com" -export API_KEY_SECRET="endpoint-proxy-api-key-secret" -export MACHINE_ID_SALT="endpoint-proxy-salt" - -# Khởi động -npm run start - -# Hoặc sử dụng PM2 -npm install -g pm2 -pm2 start --name 9router -- start -pm2 save -pm2 startup -``` - -### Docker - -```bash -# Build image (từ gốc kho lưu trữ) -docker build -t 9router . - -# Chạy container (lệnh được sử dụng trong thiết lập hiện tại) -docker run -d \ - --name 9router \ - -p 20128:20128 \ - --env-file /root/dev/9router/.env \ - -v 9router-data:/app/data \ - -v 9router-usage:/root/.9router \ - 9router -``` - -Lệnh di động (nếu bạn đã ở gốc kho lưu trữ): - -```bash -docker run -d \ - --name 9router \ - -p 20128:20128 \ - --env-file ./.env \ - -v 9router-data:/app/data \ - -v 9router-usage:/root/.9router \ - 9router -``` - -Mặc định container: -- `PORT=20128` -- `HOSTNAME=0.0.0.0` - -Các lệnh hữu ích: - -```bash -docker logs -f 9router - restart 9router -docker stop 9router && docker rm 9router -``` - -### Biến môi trường - -| Biến | Mặc định | Mô tả | -|----------|---------|-------------| -| `JWT_SECRET` | `9router-default-secret-change-me` | Bí mật ký JWT cho cookie xác thực bảng điều khiển (**thay đổi trong production**) | -| `INITIAL_PASSWORD` | `123456` | Mật khẩu đăng nhập đầu tiên khi không có hash đã lưu tồn tại | -| `DATA_DIR` | `~/.9router` |ị trí cơ sở dữ liệu ứng dụng chính (`db.json`) | -| `PORT` | framework default | Cổng dịch vụ (`20128` trong các ví dụ) | -| `HOSTNAME` | framework default | Bind host (Docker mặc định là `0.0.0.0`) | -| `NODE_ENV` | runtime default | Đặt `production` để triển khai | -| `BASE_URL` | `http://localhost:20128` | URL cơ sở nội bộ phía máy chủ được sử dụng bởi các tác vụ đồng bộ đám mây | -| `CLOUD_URL` | `https://9router.com` | URL cơ sở endpoint đồng bộ đám mây phía máy chủ | -| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | URL cơ sở tương thích ngược/công khai (ưu tiên `BASE_URL` cho runtime máy chủ) | -| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | URL đám mây tương thích ngược/công khai (ưu tiên `CLOUD_URL` cho runtime máy chủ) | -| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | B mật HMAC cho các API key được tạo | -| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Salt cho việc băm ID máy ổn định | -| `ENABLE_REQUEST_LOGS` | `false` | Bật log request/response dưới `logs/` | -| `AUTH_COOKIE_SECURE` | `false` | Buộc cookie xác thực `Secure` (đặt `true` phía reverse proxy HTTPS) | -| `REQUIRE_API_KEY` | `false` | Thực thi Bearer API key trên các route `/v1/*` (khuyên dùng cho triển khai xúc internet) | -| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | empty | Proxy gửi đi tùy chọn cho các lệnh gọi nhà cung cấp upstream | - -Ghi chú: -- Các biến proxy chữ thường cũng được hỗ trợ: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`. -- `.env` không được nướng vào image Docker (`.dockerignore`); tiêm cấu hình runtime với `--env-file` hoặc `-e`. -- Trên Windows, `APPDATA` có thể được sử dụng cho việc phân giải đường dẫn lưuữ cục bộ. -- `INSTANCE_NAME` xuất hiện trong các tài liệu/mẫu env cũ hơn, nhưng hiện không được sử dụng trong runtime. - -### Tệp Runtime và Lưu trữ - -- Trạng thái ứng dụng chính: `${DATA_DIR}/db.json` (nhà cung cấp, combo, alias, key, cài đặt), được quản lý bởi `src/lib/localDb.js`. -- Lịch sử sử dụng và log: `~/.9router/usage.json` và `~/.9router/log.txt`, được quản lý bởi `src/lib/usageDb.js`. -- request/translator tùy chọn: `/logs/...` khi `ENABLE_REQUEST_LOGS=true`. -- Lưu trữ sử dụng hiện tại tuân theo logic đường dẫn `~/.9router` và độc lập với `DATA_DIR`. - -
- ---- - -## 📊 Các mô hình có sẵn - -
-Xem tất cả các mô hình có sẵn - -**Claude Code (`cc/`)** - Pro/Max: -- `cc/claude-opus-4-6` -- `cc/claude-sonnet-4-5-2025029` -- `cc/claude-haiku-4-5-20251001` - -**Codex (`cx/`)** - Plus/Pro: -- `cx/gpt-5.2-codex` -- `cx/gpt-5.1-codex-max` - -**Gemini CLI (`gc/`)** - MIỄN PHÍ: -- `gc/gemini-3-flash-preview` -- `gc/gemini-2.5-pro` - -**GitHub Copilot (`gh/`)**: -- `gh/gpt-5` -- `gh/claude-.5-sonnet` - -**GLM (`glm/`)** - $0.6/1M: -- `glm/glm-4.7` - -**MiniMax (`minimax/`)** - $0.2/1M: -- `minimax/MiniMax-M2.1` - -**iFlow (`if/`)** - MIỄN PHÍ: -- `if/kimi-k2-thinking` -- `if/qwen3-coder-plus` -- `if/deepseek-r1` - -**Qwen (`qw/`)** - MIỄN PHÍ: -- `qw/q3-coder-plus` -- `qw/qwen3-coder-flash` - -**Kiro (`kr/`)** - MIỄN PHÍ: -- `kr/claude-sonnet-4.5` -- `kr/claude-haiku-4.5` - -
- ---- - -## 🐛 Khắc phục sự cố - -**"Language model did not provide messages"** -- Hết hạn mức nhà cung cấp → Kiểm tra trình theo dõi hạn mức bảng điều khiển -- Giải pháp: Sử dụng dự phòng combo hoặc chuyển sang tầng rẻ hơn - -**Gi hạn tốc độ (Rate limiting)** -- Hết hạn mức gói đăng ký → Dự phòng sang GLM/MiniMax -- Thêm combo: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking` - -**Token OAuth hết hạn** -- Tự động làm mới bởi 9Router -- Nếu sự cố vẫn tiếp diễn: Bảng điều khiển → Nhà cung cấp → Kết nối lại - -**Chi phí cao** -- Kiểm tra thống kê sử dụng trong Bảng điều khiển -- Chuyển mô hình chính sang GLM/MiniMax -- Sử dụng tầng miễn phí (Gemini CLI, iFlow) cho các tác vụ không quan trọng - -**Bảng điều khiển mở sai cổng** -- Đặt `PORT=20128` và `NEXT_PUBLIC_BASE_URL=http://localhost:20128` - -**Lỗi đồng bộ đám mây** -- Xác minh `BASE_URL` trỏ đến phiên bản đang chạy của bạn (ví dụ: `http://localhost:20128`) -- Xác minh `CLOUD_URL` trỏ đến endpoint đám mây dự kiến của bạn (ví dụ: `https://9router.com`) -- Giữ các giá trị `NEXT_PUBLIC_*` phù hợp với giá trị phía máy chủ khi có thể. - -**Endpoint đám mây `stream=false` trả về 500 (`Unexpected token 'd'...`)** -- Triệu chứng thường xuất hiện trên endpoint đám mây công khai (`https://9router.com/v1`) cho các lệnh gọi không phát trực tiếp (non-streaming). -- Nguyên nhân gốc rễ: upstream trả về payload SSE (`data: ...`) trong khi client mong đợi JSON. --ải pháp thay thế: sử dụng `stream=true` cho các lệnh gọi trực tiếp đến đám mây. -- Runtime 9Router cục bộ bao gồm dự phòng SSE→JSON cho các lệnh gọi không phát trực tiếp khi upstream trả về `text/event-stream`. - -**Đám mây báo đã kết nối, nhưng yêu cầu vẫn thất bại với `Invalid API key`** -- Tạo một key mới từ bảng điều khiển cục bộ (`/api/keys`) và chạy đồng bộ đám mây (`Enable Cloud` sau đó `Sync Now`). -- Các key cũ/chưa đồng bộ vẫn có thể trả về `401` trên đám mây ngay cả khi endpoint cục bộ hoạt động. - -**Đăng nhập lần đầu không hoạt động** -- Kiểm tra `INITIAL_PASSWORD` trong `.env` -- Nếu chưa đặt, mật khẩu dự phòng là `123456` - -**Không có log request dưới `logs/`** -- Đặt `ENABLE_REQUEST_LOGS=true` - ---- - -## 🛠️ Tech Stack - -- **Runtime**: Node.js 20+ -- **Framework**: Next.js 16 -- **UI**: React 19 + Tailwind 4 -- **Database**: LowDB (dựa trên tệp JSON) -- **Streaming**: Server-Sent Events (SSE) -- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys - ---- - -## 📝 Tài liệu tham khảo API - -### Chat Completions - -```bash -POST http://localhost:20128/v1/chat/completions -Authorization: Bearer your-api-key -Content-Type: application/json - -{ - "model": "cc/claude-opus-4-6", - "messages": [ - {"role":user", "content": "Viết một hàm để..."} - ], - "stream": true -} -``` - -### Liệt kê Mô hình - -```bash -GET http://localhost:20128/v1/models -Authorization: Bearer your-api-key - -→ Trả về tất cả các mô hình + combo ở định dạng OpenAI -``` - -### Các Endpoint Tương thích - -- `POST /v1/chat/completions` -- `POST /v1/messages` -- `POST /v1/responses` -- `GET /v1/models` -- `POST /v1/messages/count_tokens` -- `GET /v1beta/models` -- `POST /v1beta/models/{...path}` (Gemini-style `generateContent`) -- `POST /v1/api/chat` (đường dẫn chuyển đổi kiểu Ollama) - -### Kịch bản Xác thực Đám mây - -Đã thêm các kịch bản kiểm tra dưới `tester/security/`: - -- `tester/security/test-docker-hardening.sh` - - Build image Docker và xác thực các kiểm tra hardening (`/api/cloud/auth` auth guard, `REQUIRE_API_KEY`, hành vi cookie xác thực bảo). -- `tester/security/test-cloud-openai-compatible.sh` - - Gửi một yêu cầu tương thích OpenAI trực tiếp đến endpoint đám mây (`https://9router.com/v1/chat/completions`) với mô hình/key được cung cấp. -- `tester/security/test-cloud-sync-and-call.sh` - - Quy trình end-to-end: tạo key cục bộ -> bật/đồng bộ đám mây -> gọi endpoint đám mây với thử lại. - - Bao gồm kiểm tra dự phòng với `stream=true` để phân biệt lỗi xác thực với các vấn đề phân tích phát trực tiếp. - -Ghi chú bảo mật cho các kịch bản kiểm tra đám mây: - -- Không bao giờ hardcode các API key thực trong kịch bản/commit. -- Chỉ cung cấp key qua các biến môi trường: - - `API_KEY`, `CLOUD_API_KEY`, hoặc `OPENAI_API_KEY` (được hỗ trợ bởi `test-cloud-openai-compatible.sh`) -- Ví dụ: - -```bash -OPENAI_API_KEY="your-cloud-key" bash tester/security/test-cloud-openai-compatible.sh -``` - -Hành vi dự kiến từ việc xác thực gần đây: - -- cục bộ (`http://127.0.0.1:20128/v1/chat/completions`): hoạt động với `stream=false` và `stream=true`. -- Runtime Docker (cùng đường dẫn API được expose bởi container): các kiểm tra hardening đạt, cloud auth guard hoạt động, chế độ API key nghiêm ngặt hoạt động khi được bật. -- Endpoint đám mây công khai (`https://9router.com/v1/chat/completions`): - - `stream=true`: dự kiến thành công (trả về các khối SSE). - - `stream=false`: có thể thất bại với `500` + lỗi phân tích (`Unexpected token 'd'`) khi upstream trả về nội dung SSE cho đường dẫn client không phát trực tiếp. - -### API Quản lý và Bảng điều khiển - -- Xác thực/cài đặt: `/api/auth/login`, `/api/auth/logout`, `/api/settings`, `/api/settings/require-login` -- Quản lý nhà cung cấp: `/api/providers`, `/api/providers/[id]`, `/api/providers/[id]/test`, `/api/providers/[id]/models`, `/api/providers/validate`, `/api/provider-n*` -- Luồng OAuth: `/api/oauth/[provider]/[action]` (+ các import cụ thể theo nhà cung cấp như Cursor/Kiro) -- Cấu hình định tuyến: `/api/models/alias`, `/api/combos*`, `/api/keys*`, `/api/pricing` -- Sử dụng/log: `/api/usage/history`, `/api/usage/logs`, `/api/usage/request-logs`, `/api/usage/[connectionId]` -- Đồng bộ đám mây: `/api/sync/cloud`, `/api/sync/initialize`, `/api/cloud/*` --ợ giúp CLI: `/api/cli-tools/claude-settings`, `/api/cli-tools/codex-settings`, `/api/cli-tools/droid-settings`, `/api/cli-tools/openclaw-settings` - -### Hành vi Xác thực - -- Các route Bảng điều khiển (`/dashboard/*`) sử dụng bảo vệ cookie `auth_token`. -- Đăng nhập sử dụng hash mật khẩu đã lưu khi có mặt; nếu không, nó dự phòng vào `INITIAL_PASSWORD`. -- `requireLogin` có thể được chuyển đổi qua `/api/settings/require-login`. - -### Xử lý Yêu cầu (C cao) - -1. Client gửi yêu cầu đến `/v1/*`. -2. Trình xử lý route gọi `handleChat` (`src/sse/handlers/chat.js`). -3. Mô hình được giải quyết (nhà cung cấp/mô hình trực tiếp hoặc giải quyết alias/combo). -4. Thông tin xác thực được chọn từ DB cục bộ với bộ lọc khả dụng tài khoản. -5. `handleChatCore` (`open-sse/handlers/chatCore.js`) phát hiện định dạng và dịch chuyển yêu cầu. -6. Trình thực thi nhà cung cấp gửi cầu upstream. -7. Luồng được dịch chuyển lại thành định dạng client khi cần. -8. Sử dụng/log được ghi lại (`src/lib/usageDb.js`). -9. Dự phòng áp dụng trên lỗi nhà cung cấp/tài khoản/mô hình theo quy tắc combo. - -Tài liệu tham khảo kiến trúc đầy đủ: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) - ---- - -## 📧 Hỗ trợ - -- **Website**: [9router.com](https://9router.com) -- **GitHub**: [github.com/decolua/9](https://github.com/decolua/9router) -- **Issues**: [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues) - ---- - -## 👥 Người đóng góp - -Cảm ơn tất cả những người đã đóng góp giúp 9Router tốt hơn! - -[![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=100&columns=20&anon=1)](https://github.com/decolua/9router/graphs/contributors) - ---- - -## 📊 Star Chart - -[![ Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) - -### Cách Đóng góp - -1. Fork kho lưu trữ -2. Tạo nhánh tính năng của bạn (`git checkout -b feature/amazing-feature`) -3. Commit các thay đổi của bạn (`git commit -m 'Add amazing feature'`) -4. Push lên nhánh (`git push origin feature/amazing-feature`) -5. Mở một Pull Request - -Xem [CONTRIBUTING.md](CONTRIBUT.md) để biết hướng dẫn chi tiết. - ---- - -## 🔀 Forks - -**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — Một fork TypeScript đầy đủ tính năng của 9Router. Thêm 36+ nhà cung cấp, tự động dự phòng 4 tầng, API đa phương thức (hình ảnh, embedding, âm thanh, TTS), circuit breaker, bộ nhớ đệm ngữ nghĩa, đánh giá LLM và bảng điều khiển được tinh chỉnh. 368+ bài kiểm tra đơn vị. Có sẵn qua npm và. - ---- - -## 🙏 Lời cảm ơn - -Cảm ơn đặc biệt đến **CLIProxyAPI** - bản triển khai Go gốc đã truyền cảm hứng cho bản chuyển đổi JavaScript này. - ---- - -## 📄 Giấy phép - -Giấy phép MIT - xem [LICENSE](LICENSE) để biết chi tiết. - ---- - -
- Được xây dựng với ❤️ cho các nhà phát triển code 24/7 -
\ No newline at end of file diff --git a/9router/i18n/README.zh-CN.md b/9router/i18n/README.zh-CN.md deleted file mode 100644 index 945fdef9..00000000 --- a/9router/i18n/README.zh-CN.md +++ /dev/null @@ -1,1306 +0,0 @@ -
- 9Router Dashboard - - # 9Router - 免费 AI 路由器 - - **永不停歇的编程体验。智能回退,自动路由到免费和廉价的 AI 模型。** - - **OpenClaw 的免费 AI 提供商。** - -

- OpenClaw -

- - [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) - [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router) - [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) - - [🚀 快速开始](#-quick-start) • [💡 特性](#-key-features) • [📖 设置](#-setup) • [🌐 网站](https://9router.com) -
- ---- - -## 🤔 为什么选择 9Router? - -**停止浪费金钱和触碰限制:** - -- ❌ 订阅配额每月未使用即过期 -- ❌ 编程中途遭遇速率限制 -- ❌ 昂贵的 API(每个提供商 $20-50/月) -- ❌ 手动在提供商之间切换 - -**9Router 解决方案:** - -- ✅ **最大化订阅价值** - 追踪配额,在重置前用尽每一分 -- ✅ **自动回退** - 订阅 廉价 → 免费,零停机时间 -- ✅ **多账户** - 每个提供商的账户间轮询 -- ✅ **通用性** - 适用于 Claude Code, Codex, Gemini CLI, Cursor, Cline, 任何 CLI 工具 - ---- - -## 🔄 工作原理 - -``` -┌─────────────┐ -│ Your CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...) -│ Tool │ -└──────┬──────┘ - │ http://localhost:201281 - ↓ -┌─────────────────────────────────────────┐ -│ 9Router (Smart Router) │ -│ • Format translation (OpenAI ↔ Claude) │ -│ • Quota tracking │ -│ • Auto token refresh │ -└──────┬──────────────────────────────────┘ - │ - ├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, Gemini CLI - │ ↓ quota exhausted - ├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M) - │ ↓ budget limit - └─→ [Tier 3: FREE] iFlow, Qwen, Kiro (unlimited) - -Result: Never stop coding, minimal cost -``` - ---- - -## ⚡ 快速开始 - -**1. 全局安装:** - -```bash -npm install -g 9router -9router -``` - -🎉 仪表板将在 `http://localhost:20128` 打开 - -**2. 连接免费提供商(无需注册):** - -仪表板 → 提供商 → 连接 **Claude Code** 或 **Antigr** → OAuth 登录 → 完成! - -**3. 在您的 CLI 工具中使用:** - -``` -Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline 设置: - Endpoint: http://localhost:20128/v1 - API Key: [从仪表板复制] - Model: if/kimi-k2-thinking -``` - -**就是这样!** 开始使用免费 AI 模型编程。 - -**替代方案:从源码运行(此仓库):** - -此仓库包是私有的(`9router-app`),因此源码/Docker 执行是预期的本地开发路径。 - -```bash -cp .env.example .env -npm install -PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev -``` - -生产模式: - -```bash -npm run build -PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start -``` - -默认 URL: -- 仪表板:`http://localhost:20128/dashboard` -- OpenAI 兼容 API:`http://localhost:20128/v1` - ---- - -## 🎥 视频教程 - -
- -### 📺完整设置指南 - 9Router + Claude Code 免费 - -[![9Router + Claude Code Setup](https://img.youtube.com/vi/raEyZPg5xE0/maxresdefault.jpg)](https://www.youtube.com/watch?v=raEyZPg5xE0) - -**🎬 观看完整的分步教程:** -- ✅ 9Router 安装与设置 -- ✅ 免费 Claude Sonnet 4.5 配置 -- ✅ Claude Code 集成 -- ✅ 实时编程演示 - -**⏱️ 时长:** 20 分钟 | **👥 作者** 开发者社区 - -[▶️ 在 YouTube 上观看](https://www.youtube.com/watch?v=o3qYCyjrFYg) - -
- ---- - -## 🛠️ 支持的 CLI 工具 - -9Router 与所有主流 AI 编程工具无缝协作: - -
- - - - - - - - - - - - - - - - - -
- Claude Code
- Claude-Code -
- OpenClaw
- OpenClaw -
- Codex
- Codex -
- OpenCode
- OpenCode -
- Cursor
- Cursor -
- Antigravity
- Antigravity -
- Cline
- Cline -
- Continue
- Continue -
- Droid
- Droid -
- Roo
- Roo -
- Copilot
- Copilot -
- Kilo Code
- Kilo Code -
-
- ---- - -## 🌐 支持的提供商 - -### 🔐 OAuth 提供商 - -
- - - - - - - - -
- Claude Code
- Claude-Code -
- Antigravity
- Antigravity -
- Codex
- Codex -
- GitHub
- GitHub -
- Cursor
- Cursor -
-
- -### 🆓 免费提供商 - -
- - - - - - - -
- iFlow
- iFlow AI
- 8+ 模型 无限制 -
- Qwen
- Qwen Code
- 3+ 模型 • 无限制 -
- Gemini CLI
- Gemini CLI
- 180K/月 免费 -
- Kiro
- Kiro AI
- Claude • 无限制 -
-
- -### 🔑 API Key 提供商 (40+) - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- OpenRouter
- OpenRouter -
- GLM
- GLM -
- Kimi
- Kimi -
- MiniMax
- MiniMax -
- OpenAI
- OpenAI -
- Anthropic
- Anthropic -
- Gemini
- Gemini -
- DeepSeek
- DeepSeek -
- Groq
- Groq -
- xAI
- xAI -
- Mistral
- Mistral -
- Perplexity
- Perplexity -
- Together
- Together AI -
- Fireworks
- Fireworks -
- Cerebras
- Cerebras -
- Cohere
- Cohere -
- NVIDIA
- NVIDIA -
- SiliconFlow
- SiliconFlow -
-

...以及 20+ 更多提供商,包括 Nebius, Chutes, Hyperbolic 和自定义 OpenAI/Anthropic 兼容端点

-
- ---- - -## 💡 核心特性 - -| 特性 | 功能 | 重要性 | -|---------|--------------|----------------| -| **智能 3 层回退** | 自动路由:订阅 → 廉价 → 免费 | 永不停止编程,零停机时间 | -| 📊 **实时配额追踪** | 实时 Token 计数 + 重置倒计时 | 最大化订阅价值 | -| 🔄 **格式转换** | OpenAI ↔ Claude ↔ Gemini 无缝转换 | 适用于任何 CLI 工具 | -| 👥 **多账户支持** | 每个提供商多个账户 | 负载均衡 + 冗余 | -| 🔄 **自动 Token 刷新** | OAuth token 自动刷新 |需手动重新登录 | -| 🎨 **自定义组合** | 创建无限模型组合 | 根据需求定制回退策略 | -| 📝 **请求日志** | 调试模式包含完整请求/响应日志 | 轻松排查问题 | -| 💾 **云端同步** | 跨设备同步配置 | 到处都是相同的设置 | -| 📊 **使用分析** | 追踪 Token、成本、趋势 | 优化支出 | -| 🌐 **随处部署** | 本地主机、VPS、Docker、Cloudflare Workers | 灵活的部署选项 | - -
-📖 特性详情 - -### 🎯 智能 3 层回退 - -创建具有自动回退功能的组合: - -``` -Combo: "my-coding-stack" - 1. cc/claude-opus-4-6 (your subscription) - 2. glm/glm-4.7 (cheap backup, $0.6/1M) - 3. if/kimi-k2-thinking (free fallback) - -→ Auto switches when quota runs out or errors occur -``` - -### 📊 实时配额追踪 - -- 每个提供商的 Token 消 -- 重置倒计时(5 小时、每日、每周) -- 付费层的成本估算 -- 月度支出报告 - -### 🔄 格式转换 - -格式间无缝转换: -- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **OpenAI Responses** -- 您的 CLI 工具发送 OpenAI 格式 → 9Router 转换 → 提供商接收原生格式 -- 适用于任何支持自定义 OpenAI 端点的工具 - -### 👥 多账户支持 - -- 每个提供商添加多个账户 -- 自动轮询或基于优先级的 -- 当一个账户达到配额时回退到下一个 - -### 🔄 自动 Token 刷新 - -- OAuth token 在过期前自动刷新 -- 无需手动重新认证 -- 所有提供商的无缝体验 - -### 🎨 自定义组合 - -- 创建无限模型组合 -- 混合订阅、廉价和免费层 -- 为您的组合命名以便访问 -- 通过云端同步跨设备共享组合 - -### 📝 请求日志 - -- 启用调试模式以获取完整请求/响应日志 -- 追踪 API 调用、标头和负载 -- 排查集成 -- 导出日志进行分析 - -### 💾 云端同步 - -- 跨设备同步提供商、组合和设置 -- 自动后台同步 -- 安全加密存储 -- 从任何地方访问您的设置 - -#### 云端运行说明 - -- 在生产环境中优先使用服务器端云变量: - - `BASE_URL`(同步调度器使用的内部回调 URL) - - `CLOUD_URL`(云端同步端点基础 URL) -- `NEXT_PUBLIC_BASE_URL` 和 `NEXT_PUBLIC_CLOUD_URL` 仍支持兼容性/UI,但服务器运行时现在优先使用 `BASE_URL`/`C_URL`。 -- 云端同步请求现在使用超时 + 快速失败行为,以避免在云端 DNS/网络不可用时 UI 挂起。 - -### 📊 使用分析 - -- 追踪每个提供商和模型的 Token 使用情况 -- 成本估算和支出趋势 -- 月度报告和洞察 -- 优化您的 AI 支出 - -> **💡 重要 - 理解仪表板成本:** -> -> 使用分析中显示的“成本”**仅用于追踪和比较目的**。 -> 9Router 本身**从不向您收费**。您只需直接向提供商付款(如果使用付费服务)。 -> -> **示例:** 如果您的仪表板在使用 iFlow 模型时显示“$290 总成本”,这代表 -> 您直接使用付费 API 时需要支付的金额。您的实际成本 = **$0**(iFlow 是免费无限制的)。 -> -> 将其视为“节省追踪器”,显示您通过使用免费模型或 -> 通过 9Router 路由节省了多少! - -### 🌐 随处部署 - -- 💻 **本地主机** - 默认,离线工作 -- ☁️ **VPS/云** 跨设备共享 -- 🐳 **Docker** - 一键部署 -- 🚀 **Cloudflare Workers** - 全球边缘网络 - -
- ---- - -## 💰 定价一览 - -| 层级 | 提供商 | 成本 | 配额重置 | 最适合 | -|------|----------|------|-------------|----------| -| **💳 订阅** | Claude Code (Pro) | $20/月 | 5h + 每周 | 已订阅用户 | -| | Codex (Plus/Pro) | $20-200/月 | 5h + 每周 OpenAI 用户 | -| | Gemini CLI | **免费** | 180K/月 + 1K/天 | 所有人! | -| | GitHub Copilot | $10-19/月 | 每月 | GitHub 用户 | -| **💰 廉价** | GLM-4.7 | $0.6/1M | 每日 10AM | 预算备份 | -| | MiniMax M2.1 | $0.2/1M | 5 小时滚动 | 最便宜选项 | -| | Kimi K2 | $9/月固定 | 10M tokens/月 | 可预测成本 | -| **🆓 免费** | iFlow | $0 | 无限制 | 8 个模型免费 | -| | Qwen | $0 | 无限制 | 3 个模型免费 | -| | Kiro | $0 | 无限制 | Claude 免费 | - -**💡 专业提示:** 从 Gemini CLI(180K 免费/月)+ iFlow(无限制免费)组合开始 = $0 成本! - ---- - -### 📊 理解 9Router 成本和计费 - -**9Router 计费现实:** - -✅ **9Router 软件 = 永远免费**开源,从不收费) -✅ **仪表板“成本” = 仅显示/追踪**(非实际账单) -✅ **您直接向提供商付款**(订阅或 API 费用) -✅ **免费提供商保持免费**(iFlow, Kiro, Qwen = $0 无限制) -❌ **9Router 从不发送发票**或向您的卡收费 - -**成本显示如何工作:** - -仪表板显示**估算成本**,就像您直接使用付费 API 一样。这**不是计费** - 它是一个比较工具,用于显示您的节省。 - -**示例场景:``` -仪表板显示: -• 总请求数:1,662 -• 总 Token 数:47M -• 显示成本:$290 - -现实检查: -• 提供商:iFlow(免费无限制) -• 实际付款:$0.00 -• $290 的含义:您通过使用免费模型节省的金额! -``` - -**付款规则:** -- **订阅提供商**(Claude Code, Codex):通过他们的网站直接向他们付款 -- **廉价提供商**(GLM, MiniMax):直接向他们付款,9Router 只是路由 -- **免费**(iFlow, Kiro, Qwen):真正永远免费,没有隐藏费用 -- **9Router**:从不收取任何费用,永远 - ---- - -## 🎯 使用案例 - -### 案例 1:“我有 Claude Pro 订阅” - -**问题:** 配额未使用即过期,重度编程时遇到速率限制 - -**解决方案:** -``` -Combo: "maximize-claude" - 1. cc/claude-opus-4-6 (use subscription fully) - 2. glm/glm-4.7 (cheap backup when quota out) - 3 if/kimi-k2-thinking (free emergency fallback) - -Monthly cost: $20 (subscription) + ~$5 (backup) = $25 total -vs. $20 + hitting limits = frustration -``` - -### 案例 2:“我想要零成本” - -**问题:** 负担不起订阅,需要可靠的 AI 编程 - -**解决方案:** -``` -Combo: "free-forever" - 1. gc/gemini-3-flash (180K free/month) - 2. if/kimi-k2-thinking (unlimited free) - 3. qw/qwen3-c-plus (unlimited free) - -Monthly cost: $0 -Quality: Production-ready models -``` - -### 案例 3:“我需要 24/7 编程,无中断” - -**问题:** 截止日期,不能承受停机 - -**解决方案:** -``` -Combo: "always-on" - 1. cc/claude-opus-4-6 (best quality) - 2. cx/gpt-5.2-codex (second subscription) - 3. glm/glm-4.7 (cheap, resets daily) - 4. minimaxMiniMax-M2.1 (cheapest, 5h reset) - 5. if/kimi-k2-thinking (free unlimited) - -Result: 5 layers of fallback = zero downtime -Monthly cost: $20-200 (subscriptions) + $10-20 (backup) -``` - -### 案例 4:“我想在 OpenClaw 中使用免费 AI” - -**问题:** 需要在消息应用(WhatsApp, Telegram, Slack...)中使用 AI 助手,完全免费 - -**解决方案:** -``` -Combo: "openclaw-free" - 1. if/glm-4.7 (unlimited free) - 2. if/minimax-m2.1 (unlimited free) - 3. if/kimi-k2-thinking (unlimited free) - -Monthly cost: $0 -Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... -``` - ---- - -## ❓ 常见问题 - -
-📊 为什么我的仪表板显示高成本? - -仪表板追踪您的 Token 使用情况,并显示**估算成本**,就像您直接使用付费 API 一样。这**不是实际计费** - 它是一个参考,显示您通过 9Router 使用免费模型或现有订阅节省了多少。 - -**示例:** -- **仪表板显示:**“$290 总成本” -- **现实:** 您正在使用 iFlow(免费无限制) -- **您的实际成本:** **$0.00** -- **$290 的含义:** 您通过使用免费模型而不是付费 API **节省**的金额! - -成本显示是一个“节省追踪器”,帮助您了解使用模式和优化机会。 - -
- -
-💳 9Router 会向我收费吗? - -**不会。** 9 是免费的开源软件,在您自己的计算机上运行。它从不向您收费。 - -**您只需支付:** -- ✅ **订阅提供商**(Claude Code $20/月, Codex $20-200/月)→ 在他们的网站上直接向他们付款 -- ✅ **廉价提供商**(GLM, MiniMax)→ 直接向他们付款,9Router 只是路由您的请求 -- ❌ **9Router 本身** → **从不收取任何费用,永远** - -9Router 是本地代理/路由器。它没有您的信用卡,不能发送发票,也没有计费系统。完全免费的软件。 - -
- -
-🆓 免费提供商真的无限制吗? - -**是的!** 标记为免费(iFlow, Kiro, Qwen)的提供商是真正无限制的,**没有隐藏费用**。 - -这些是各自公司提供的免费服务: -- **iFlow**:通过 OAuth 免费无限制访问 8+ 模型 -- **Kiro**:通过 AWS Builder ID 免费无限制 Claude 模型 -- **Qwen**:通过设备认证免费无限制访问 Qwen 模型 - -Router 只是将您的请求路由到它们 - 没有“陷阱”或未来计费。它们是真正的免费服务,9Router 使它们易于使用并支持回退。 - -**注意:** 一些订阅提供商(Antigravity, GitHub Copilot)可能有免费预览期,后来可能变成付费,但这会由这些提供商明确宣布,而不是 9Router。 - -
- -
-💰 如何最小化我的实际 AI 成本? - -**免费优先策略:** - -1. **从 100% 免费组合开始:** - ``` - 1. gc/gini-3-flash (180K/month free from Google) - 2. if/kimi-k2-thinking (unlimited free from iFlow) - 3. qw/qwen3-coder-plus (unlimited free from Qwen) - ``` - **成本:$0/月** - -2. **仅在需要时添加廉价备份:** - ``` - 4. glm/glm-4.7 ($0.6/1M tokens) - ``` - **额外成本:仅为您实际使用的付费** - -3. **最后使用订阅提供商:** - - 仅当您已经拥有它们时 - - 9Router 通过配额追踪帮助最大化其价值 - -**结果:** 大多数用户可以仅使用免费层以 $0/月运行! - -
- -
-📈 如果我的使用量突然激增怎么办? - -9Router 的智能回退可防止意外费用: - -**场景:** 您正在进行编程冲刺并耗尽了配额 - -**没有 9Router:** -- ❌ 遇到速率限制 → 工作停止 → 沮丧 -- ❌ 或:意外累积巨额 API 账单 - -**有 9Router:** -- ✅订阅达到限制 → 自动回退到廉价层 -- ✅ 廉价层变得昂贵 → 自动回退到免费层 -- ✅ 永不停止编程 → 可预测的成本 - -**您在控制中:** 在仪表板中设置每个提供商的支出限制,9Router 会遵守它们。 - -
- ---- - -## 📖 设置指南 - -
-🔐 订阅提供商(最大化价值) - -### Claude Code (Pro/Max) - -```bash -Dashboard → Providers → Connect Claude Code -→ OAuth login → Auto token refresh -→ 5-hour + weekly quota tracking - -Models: - cc/claude-opus-4-6 - cc/claude-sonnet-4-5-20250929 - cc/claude-haiku-4-5-20251001 -``` - -**专业提示:** 使用 Opus 处理复杂任务,Sonnet 追求速度。9Router 追踪每个模型的配额! - -### OpenAI Codex (Plus/Pro) - -```bash -Dashboard → Providers → Connect Codex -→ OAuth login (port 1455) -→ 5-hour + weekly reset - -Models: - /gpt-5.2-codex - cx/gpt-5.1-codex-max -``` - -### Gemini CLI(免费 180K/月!) - -```bash -Dashboard → Providers → Connect Gemini CLI -→ Google OAuth -→ 180K completions/month + 1K/day - -Models: - gc/gemini-3-flash-preview - gc/gemini-2.5-pro -``` - -**最佳价值:** 巨大的免费层!在付费层之前使用这个。 - -### GitHub Copilot - -```bash -Dashboard → Providers → Connect GitHub -→ OAuth via -→ Monthly reset (1st of month) - -Models: - gh/gpt-5 - gh/claude-4.5-sonnet - gh/gemini-3-pro -``` - -
- -
-💰 廉价提供商(备份) - -### GLM-4.7(每日重置,$0.6/1M) - -1. 注册:[Zhipu AI](https://open.bigmodel.cn/) -2. 从 Coding Plan 获取 API key -3. 仪表板 → 添加 API Key: - - Provider: `glm` - - API Key: `your-key` - -**使用:** `glm/glm-4.7` - -**专业提示:** Coding Plan 以 1/7 的成本提供 3× 配额!每日 10:00 AM 重置。 - -### MiniMax M2.1(5h 重置,$0.20/1M) - -1. 注册:[MiniMax](https://www.minimax.io/) -2. 获取 API key -3. 仪表板 → 添加 API Key - -**使用:** `minimax/MiniMax-M2.1` - -**专业提示:** 长上下文(1M tokens)的最便宜选项! - -### Kimi K2($9/月固定) - -1. 订阅:[Moonshot AI](https://platform.moonshot.ai/) -2. 获取 API key -3. 仪表板 → 添加 API Key - -**使用:** `kimi/kimi-latest` - -**专业提示:** 固定 $9/月可获得 10M tokens = $0.90/1M 实际成本! - -
- -
-🆓 免费提供商(紧急备份) - -### i(8 个免费模型) - -```bash -Dashboard → Connect iFlow -→ iFlow OAuth login -→ Unlimited usage - -Models: - if/kimi-k2-thinking - if/qwen3-coder-plus - if/glm-4.7 - if/minimax-m2 - if/deepseek-r1 -``` - -### Qwen(3 个免费模型) - -```bash -Dashboard → Connect Qwen -→ Device code authorization -→ Unlimited usage - -Models: - qw/qwen3-coder-plus - qw/qwen3-coder-flash -``` - -### Kiro(Claude 免费```bash -Dashboard → Connect Kiro -→ AWS Builder ID or Google/GitHub -→ Unlimited usage - -Models: - kr/claude-sonnet-4.5 - kr/claude-haiku-4.5 -``` - -
- -
-🎨 创建组合 - -### 示例 1:最大化订阅 → 廉价备份 - -``` -Dashboard → Combos → Create New - -Name: premium-coding -Models: - 1. cc/claude-opus-4-6 (Subscription primary) - 2. glm/glm4.7 (Cheap backup, $0.6/1M) - 3. minimax/MiniMax-M2.1 (Cheapest fallback, $0.20/1M) - -Use in CLI: premium-coding - -Monthly cost example (100M tokens): - 80M via Claude (subscription): $0 extra - 15M via GLM: $9 - 5M via MiniMax: $1 - Total: $10 + your subscription -``` - -### 示例 2:仅免费(零成本) - -``` -Name: free-combo -Models: - 1. gc/gemini-3-flash-preview (180K free/month) - 2. if/kimi-k2-thinking (unlimited) - 3. qw/qwen3-coder-plus (unlimited) - -Cost: $0 forever! -``` - -
- -
-🔧 CLI 集成 - -### Cursor IDE - -``` -Settings → Models → Advanced: - OpenAI API Base URL: http://localhost:20128/v1 - OpenAI API Key: [from 9router dashboard] - Model: cc/claude-opus-4-6 -``` - -使用组合:`premium-coding` - -### Claude Code - -编辑 `~/.claude/config.json`: - -```json -{ - "anthropic_api_base": "http://localhost:20128/v1", - "anthropic_api_key": "your-9router-api-key" -} -``` - -### Codex CLI - -```bash -export OPENAI_BASE_URL="http://localhost:20128" -export OPENAI_API_KEY="your-9router-api-key" - -codex "your prompt" -``` - -### OpenClaw - -**选项 1 — 仪表板(推荐):** - -``` -Dashboard → CLI Tools →Claw → Select Model → Apply -``` - -**选项 2 — 手动:** 编辑 `~/.openclaw/openclaw.json`: - -```json -{ - "agents": { - "defaults": { - "model": { - "primary": "9router/if/glm-4.7" - } - } - }, - "models": { - "providers": { - "9router": { - "baseUrl": "http://127.0.0.1:20128/v1", - "apiKey": "sk_9router", - "api": "openai-completions", - "models": [ - { - "id": "if/glm-4.7", - "name": "glm-4.7" - } - ] - } - } - } -} -``` - -> **注意:** OpenClaw 仅适用于本地 9Router。使用 `127.0.0.1` 而不是 `localhost` 以避免 IPv6 解析问题。 - -### Cline / Continue / RooCode - -``` -Provider: OpenAI Compatible -Base URL: http://localhost:20128/v1 -API Key: [from dashboard] -Model: cc/claudeus-4-6 -``` - -
- -
-🚀 部署 - -### VPS 部署 - -```bash -# Clone and install -git clone https://github.com/decolua/9router.git -cd 9router -npm install -npm run build - -# Configure -export JWT_SECRET="your-secure-secret-change-this" -export INITIAL_PASSWORD="your-password" -export DATA_DIR="/var/lib/9router" -export PORT="20128" -export HOSTNAME="0.0.0.0" -export NODE_ENV="production" -export NEXT_PUBLIC_BASE_URLhttp://localhost:20128" -export NEXT_PUBLIC_CLOUD_URL="https://9router.com" -export API_KEY_SECRET="endpoint-proxy-api-key-secret" -export MACHINE_ID_SALT="endpoint-proxy-salt" - -# Start -npm run start - -# Or use PM2 -npm install -g pm2 -pm2 start npm --name 9router -- start -pm2 save -pm2 startup -``` - -### Docker - -```bash -# Build image (from repository root) -docker build -t 9router . - -# Run container (command used in current setup) -docker run -d \ - --name 9router -p 20128:20128 \ - --env-file /root/dev/9router/.env \ - -v 9router-data:/app/data \ - -v 9router-usage:/root/.9router \ - 9router -``` - -便携式命令(如果您已在仓库根目录): - -```bash -docker run -d \ - --name 9router \ - -p 20128:20128 \ - --env-file ./.env \ - -v 9router-data:/app/data \ - -v 9router-usage:/root/.9router \ - 9 -``` - -容器默认值: -- `PORT=20128` -- `HOSTNAME=0.0.0.0` - -有用命令: - -```bash -docker logs -f 9router -docker restart 9router -docker stop 9router && docker rm 9router -``` - -### 环境变量 - -| 变量 | 默认值 | 描述 | -|----------|---------|-------------| -| `JWT_SECRET` | `9router-default-secret-change-me` | 仪表板认证 cookie 的 JWT 签名密钥(**生产环境中请更改**) | -| `INITIAL_PASSWORD | `123456` | 当没有保存的哈希时的首次登录密码 | -| `DATA_DIR` | `~/.9router` | 主应用数据库位置(`db.json`) | -| `PORT` | 框架默认值 | 服务端口(示例中为 `20128`) | -| `HOSTNAME` | 框架默认值 | 绑定主机(Docker 默认为 `0.0.0.0`) | -| `NODE_ENV` | 运行时默认值 | 部署时设置 `production` | -| `BASE_URL` |http://localhost:20128` | 云同步作业使用的服务器端内部基础 URL | -| `CLOUD_URL` | `https://9router.com` | 服务器端云同步端点基础 URL | -| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | 向后兼容/公共基础 URL(服务器运行时优先使用 `BASE_URL`) | -| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | 向后兼容/公共云 URL(服务器运行时优先使用 `CLOUD_URL`) | -| `API_KEY_SECRET` | `endpoint-proxy-api-secret` | 生成的 API Key 的 HMAC 密钥 | -| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | 稳定机器 ID 哈希的盐值 | -| `ENABLE_REQUEST_LOGS` | `false` | 在 `logs/` 下启用请求/响应日志 | -| `AUTH_COOKIE_SECURE` | `false` | 强制 `Secure` 认证 cookie(在 HTTPS 反向代理后设置 `true`) | -| `REQUIRE_API_KEY` | `false` | 在 `/v1/*` 路由上强制执行 Bearer API key推荐用于暴露在互联网的部署) | -| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | 空 | 上游提供商调用的可选出站代理 | - -注意: -- 也支持小写代理变量:`http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`。 -- `.env` 不会烘焙到 Docker 镜像中(`.dockerignore`);使用 `--env-file` 或 `-e` 注入运行时配置。 -- 在 Windows 上,`APPDATA` 可用于本地存储路径解析。 -- `INSTANCE_NAME` 出现在旧/环境模板中,但目前运行时未使用。 - -### 运行时文件和存储 - -- 主应用状态:`${DATA_DIR}/db.json`(提供商、组合、别名、密钥、设置),由 `src/lib/localDb.js` 管理。 -- 使用历史和日志:`~/.9router/usage.json` 和 `~/.9router/log.txt`,由 `src/lib/usageDb.js` 管理。 -- 可选请求/转换器日志:当 `ENABLE_REQUEST_LOGS=true` 时为 `/logs/...`。 -- 使用存储当前遵循 `~/.9` 路径逻辑,独立于 `DATA_DIR`。 - -
- ---- - -## 📊 可用模型 - -
-查看所有可用模型 - -**Claude Code (`cc/`)** - Pro/Max: -- `cc/claude-opus-4-6` -- `cc/claude-sonnet-4-5-20250929` -- `cc/claude-haiku-4-5-20251001` - -**Codex (`cx/`)** - Plus/Pro: -- `cx/gpt-5.2-codex- `cx/gpt-5.1-codex-max` - -**Gemini CLI (`gc/`)** - 免费: -- `gc/gemini-3-flash-preview` -- `gc/gemini-2.5-pro` - -**GitHub Copilot (`gh/`)**: -- `gh/gpt-5` -- `gh/claude-4.5-sonnet` - -**GLM (`glm/`)** - $0.6/1M: -- `glm/glm-4.7` - -**MiniMax (`minimax/`)** - $0.2/1M: -- `imax/MiniMax-M2.1` - -**iFlow (`if/`)** - 免费: -- `if/kimi-k2-thinking` -- `if/qwen3-coder-plus` -- `if/deepseek-r1` - -**Qwen (`qw/`)** - 免费: -- `qw/qwen3-coder-plus` -- `qw/qwen3-coder-flash` - -**Kiro (`kr/`)** - 免费: -- `kr/claude-sonnet-4.5` -- `kr/claude-haiku-4.5` - -
- ---- - -## 🐛 故障排除 - -“Language model did not provide messages”** -- 提供商配额耗尽 → 检查仪表板配额追踪器 -- 解决方案:使用组合回退或切换到更便宜的层 - -**速率限制** -- 订阅配额用完 → 回退到 GLM/MiniMax -- 添加组合:`cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking` - -**OAuth token 过期** -- 由 9Router 自动刷新 -- 如果问题持续:仪表板 → 提供商 → 重新 - -**高成本** -- 在仪表板中检查使用统计 -- 将主要模型切换为 GLM/MiniMax -- 对非关键任务使用免费层(Gemini CLI, iFlow) - -**仪表板在错误的端口打开** -- 设置 `PORT=20128` 和 `NEXT_PUBLIC_BASE_URL=http://localhost:20128` - -**云端同步错误** -- 验证 `BASE_URL` 指向您正在运行的实例(例如:`http://localhost:20128`) -- 验证 `CLOUD_URL` 指向您预期的云端端点(例如:`https://9router.com`) -- 尽可能保持 `NEXT_PUBLIC_*` 值与服务器端值一致。 - -**云端端点 `stream=false` 返回 500(`Unexpected token 'd'...`)** -- 症状通常出现在公共云端端点(`https://9router.com/v1`)的非流式调用上。 -- 根本原因:上游返回 SSE 负载(`data: ...`)而客户端期望 JSON。 -- 变通方法:对云端直接调用使用 `stream=true`。 -- 当上游返回 `text/event-stream` 时,本地 9Router 运行时包含 SSE→JSON 回退用于非流式调用。 - -**云端显示已连接,但请求仍然失败并显示 `Invalid API key`** -- 从本地仪表板(`/api/keys`)创建新密钥并运行云端同步(`Enable Cloud` 然后 `Sync Now`)。 -- 旧/未同步的密钥即使在本地端点工作的情况下,仍可能在云端返回 `401`。 - -**首次登录不工作** -- 检查 `.env` 中的 `INITIAL_PASSWORD` -- 如果未设置,回退密码是 `123456` - -**`logs/` 下没有请求日志** -- 设置 `ENABLE_REQUEST_LOGS=true` - ---- - -## 🛠️ 技术栈 - -- **运行时**:Node.js 20+ -- **框架**:Next.js 16 -- **UI**:React 19 + Tailwind CSS 4 -- **数据库**:LowDB(基于 JSON 文件) -- **流式传输**:Server-Sent Events (SSE) -- **认证**:OAuth 2.0 (PKCE) + JWT + API Keys - ---- - -## 📝 API 参考 - -### Chat Completions - -```bash -POST httplocalhost:20128/v1/chat/completions -Authorization: Bearer your-api-key -Content-Type: application/json - -{ - "model": "cc/claude-opus-4-6", - "messages": [ - {"role": "user", "content": "Write a function to..."} - ], - "stream": true -} -``` - -### 列出模型 - -```bash -GET http://localhost:20128/v1/models -Authorization: Bearer your-api-key - -→ Returns all models + combos in OpenAI format -``` - -### 兼容性端点 - -- ` /v1/chat/completions` -- `POST /v1/messages` -- `POST /v1/responses` -- `GET /v1/models` -- `POST /v1/messages/count_tokens` -- `GET /v1beta/models` -- `POST /v1beta/models/{...path}`(Gemini 风格 `generateContent`) -- `POST /v1/api/chat`(Ollama 风格转换路径) - -### 云端验证脚本 - -在 `tester/security/` 下添加了测试脚本: - -- `tester/security/test-docker-hardening.sh` - - 构建 Docker 镜像并验证加固检查(`/api/cloud/auth` 认证保护、`REQUIRE_API_KEY`、安全认证 cookie 行为)。 -- `tester/security/test-cloud-openai-compatible.sh` - - 使用提供的模型/密钥向云端端点(`https://9router.com/v1/chat/completions`)发送直接的 OpenAI 兼容请求。 -- `tester/security/test-cloud-sync-and-call.sh` - - 端到端流程:创建本地密钥 -> 启用/同步云端 -> 带重试调用云端端点。 - - 包含使用 `stream` 的回退检查,以区分认证错误和非流式解析问题。 - -云端测试脚本的安全说明: - -- 永远不要在脚本/提交中硬编码真实的 API 密钥。 -- 仅通过环境变量提供密钥: - - `API_KEY`, `CLOUD_API_KEY`, 或 `OPENAI_API_KEY`(由 `test-cloud-openai-compatible.sh` 支持) -- 示例: - -```bash -OPENAI_API_KEY="your-cloud-key" bash tester/security/test-cloud-openai-compatible.sh -``` - -最近验证的预期行为: - -- 本地运行时(`http://127.0.0.1:20128/v1/chat/completions`):使用 `stream=false` 和 `stream=true` 都可以工作。 -- Docker 运行时(容器暴露的相同 API 路径):加固检查通过,云端认证保护工作,启用时严格 API 密钥模式工作。 -- 公共云端端点(`https://9router.com/v1/chat/completions`): - - `stream=true`:预期成功(返回 SSE 块)。 - - `stream=false`:当上游向非流式客户端路径返回 SSE 内容时,可能失败并显示 `500` + 解析错误(`Unexpected token 'd'`)。 - -### 仪表板和管理 API - -- 认证/设置:`/api/auth/login`, `/api/auth/logout`, `/api/settings`, `/api/settings/require-login` -- 提供商管理:`/api/providers`, `/api/providers/[id]`, `/api/providers/[id]/test`, `/api/providers/[id]/models`, `/api/providers/validate`, `/api/provider-nodes*` -- OAuth 流程:`/api/oauth/[provider]/[action]`(+ 特定提供商导入如 Cursor/Kiro) - 路由配置:`/api/models/alias`, `/api/combos*`, `/api/keys*`, `/api/pricing` -- 使用/日志:`/api/usage/history`, `/api/usage/logs`, `/api/usage/request-logs`, `/api/usage/[connectionId]` -- 云端同步:`/api/sync/cloud`, `/api/sync/initialize`, `/api/cloud/*` -- CLI 助手:`/api/cli-tools/claude-settings`, `/api/cli-tools/codex-settings`, `/api/cli-tools/droid-settings`, `/api/cli-tools/openaw-settings` - -### 认证行为 - -- 仪表板路由(`/dashboard/*`)使用 `auth_token` cookie 保护。 -- 登录时如果存在保存的密码哈希则使用;否则回退到 `INITIAL_PASSWORD`。 -- `requireLogin` 可以通过 `/api/settings/require-login` 切换。 - -### 请求处理(高级) - -1. 客户端向 `/v1/*` 发送请求。 -2. 路由处理器调用 `handleChat`(`src/sse/handlers/chat.js`)。 -3. 模型被解析直接提供商/模型或别名/组合解析)。 -4. 从本地数据库选择凭据,并进行账户可用性过滤。 -5. `handleChatCore`(`open-sse/handlers/chatCore.js`)检测格式并转换请求。 -6. 提供商执行器发送上游请求。 -7. 需要时将流转换回客户端格式。 -8. 记录使用/日志(`src/lib/usageDb.js`)。 -9. 根据组合规则在提供商/账户/模型错误时应用回退。 - -完整架构参考:[`docs/ARCHITECTURE`](docs/ARCHITECTURE.md) - ---- - -## 📧 支持 - -- **网站**:[9router.com](https://9router.com) -- **GitHub**:[github.com/decolua/9router](https://github.com/decolua/9router) -- **问题**:[github.com/decolua/9router/issues](https://github.com/decolua/9router/issues) - ---- - -## 👥 贡献者 - -感谢所有帮助让 9Router 变得更好的贡献者! - -[![Contributors](https://contrib.rocks/image?repo=decolua/router&max=100&columns=20&anon=1)](https://github.com/decolua/9router/graphs/contributors) - ---- - -## 📊 Star 图表 - -[![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) - -### 如何贡献 - -1. Fork 仓库 -2. 创建您的功能分支(`git checkout -b feature/amazing-feature`) -3. 提交您的更改(`git commit -m 'Add amazing feature'`) -4 推送到分支(`git push origin feature/amazing-feature`) -5. 打开 Pull Request - -详细指南请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。 - ---- - -## 🔀 分支 - -**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — 9Router 的全功能 TypeScript 分支。添加了 36+ 提供商、4 层自动回退、多模态 API(图像、嵌入、音频、TTS)、熔断器、语义缓存、LLM 评估和精美的仪表板。8+ 单元测试。通过 npm 和 Docker 可用。 - ---- - -## 🙏 致谢 - -特别感谢 **CLIProxyAPI** - 启发这个 JavaScript 移植的原始 Go 实现。 - ---- - -## 📄 许可证 - -MIT License - 详情请参阅 [LICENSE](LICENSE)。 - ---- - -
- 用 ❤️ 为 24/7 编程的开发者构建 -
\ No newline at end of file diff --git a/9router/images/9router.png b/9router/images/9router.png deleted file mode 100644 index 283210a2304bd3e161249544f2ca485283c65c24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 758319 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�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

X9~jHlzW$NY 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

EauDCxuZ&zU0M#QOVog@5`#0s%k1qVzu4 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%0Dsm5YB#wGPTpPL!XeN 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

    GA}SEef#`hrio>J^l|aU~K$_ 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 { - // Ignore fs/path modules in browser bundle - if (!isServer) { - config.resolve.fallback = { - ...config.resolve.fallback, - fs: false, - path: false, - }; - } - // Stop watching logs directory to prevent HMR during streaming. - // Also ignore Windows XP-era junction points in user profile that loop - // back on themselves and cause `EPERM: operation not permitted, scandir` - // on GitHub Actions runners (cascades into FlightClientEntryPlugin - // crash). Local Windows users with normal accounts don't hit this. - // Single regex (webpack rejects mixed regex + glob strings in array). - config.watchOptions = { - ...config.watchOptions, - ignored: /[\\/](logs|\.next|Application Data|Local Settings)[\\/]/, - }; - // Disable webpack's user-profile + node_modules snapshotting that - // triggers the same EPERM scan during `next build` on CI Windows. - config.snapshot = { - ...(config.snapshot || {}), - managedPaths: [], - immutablePaths: [], - }; - return config; - }, - async rewrites() { - return [ - { - source: "/v1/v1/:path*", - destination: "/api/v1/:path*" - }, - { - source: "/v1/v1", - destination: "/api/v1" - }, - { - source: "/codex/:path*", - destination: "/api/v1/responses" - }, - { - source: "/v1/:path*", - destination: "/api/v1/:path*" - }, - { - source: "/v1", - destination: "/api/v1" - } - ]; - } -}; - -export default nextConfig; diff --git a/9router/open-sse/.npmignore b/9router/open-sse/.npmignore deleted file mode 100644 index 0b7b5690..00000000 --- a/9router/open-sse/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules/ -*.log -.DS_Store -test/ -*.test.js -.env -.env.* - diff --git a/9router/open-sse/config/appConstants.js b/9router/open-sse/config/appConstants.js deleted file mode 100644 index 042fe2ef..00000000 --- a/9router/open-sse/config/appConstants.js +++ /dev/null @@ -1,130 +0,0 @@ -import { platform, arch } from "os"; - -// === Gemini CLI === -export const GEMINI_CLI_VERSION = "0.31.0"; -export const GEMINI_CLI_API_CLIENT = "google-genai-sdk/1.41.0 gl-node/v22.19.0"; - -export function geminiCLIUserAgent(model = "unknown") { - const os = platform() === "win32" ? "windows" : platform(); - return `GeminiCLI/${GEMINI_CLI_VERSION}/${model || "unknown"} (${os}; ${arch()})`; -} - -// === GitHub Copilot === -export const GITHUB_COPILOT = { - VSCODE_VERSION: "1.110.0", - COPILOT_CHAT_VERSION: "0.38.0", - USER_AGENT: "GitHubCopilotChat/0.38.0", - API_VERSION: "2025-04-01", -}; - -// === Antigravity enums === -export const IDE_TYPE = { - UNSPECIFIED: 0, - JETSKI: 10, - ANTIGRAVITY: 9, - PLUGINS: 7 -}; - -export const PLATFORM = { - UNSPECIFIED: 0, - DARWIN_AMD64: 1, - DARWIN_ARM64: 2, - LINUX_AMD64: 3, - LINUX_ARM64: 4, - WINDOWS_AMD64: 5 -}; - -export const PLUGIN_TYPE = { - UNSPECIFIED: 0, - CLOUD_CODE: 1, - GEMINI: 2 -}; - -export function getPlatformEnum() { - const os = platform(); - const architecture = arch(); - if (os === "darwin") return architecture === "arm64" ? PLATFORM.DARWIN_ARM64 : PLATFORM.DARWIN_AMD64; - if (os === "linux") return architecture === "arm64" ? PLATFORM.LINUX_ARM64 : PLATFORM.LINUX_AMD64; - if (os === "win32") return PLATFORM.WINDOWS_AMD64; - return PLATFORM.UNSPECIFIED; -} - -export function getPlatformUserAgent() { - return `antigravity/1.104.0 ${platform()}/${arch()}`; -} - -export const CLIENT_METADATA = { - ideType: IDE_TYPE.ANTIGRAVITY, - platform: getPlatformEnum(), - pluginType: PLUGIN_TYPE.GEMINI -}; - -// Internal anti-loop header -export const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" }; - -// Antigravity chat/stream headers -export const ANTIGRAVITY_HEADERS = { - "User-Agent": `antigravity/1.107.0 ${platform()}/${arch()}` -}; - -// Cloud Code Assist API -export const CLOUD_CODE_API = { - loadCodeAssist: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", - onboardUser: "https://cloudcode-pa.googleapis.com/v1internal:onboardUser", -}; - -export const LOAD_CODE_ASSIST_HEADERS = { - "Content-Type": "application/json", - "User-Agent": "google-api-nodejs-client/9.15.1", - "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", - "Client-Metadata": JSON.stringify({ ideType: IDE_TYPE.ANTIGRAVITY, platform: getPlatformEnum(), pluginType: PLUGIN_TYPE.GEMINI }), -}; - -export const LOAD_CODE_ASSIST_METADATA = { - ideType: IDE_TYPE.ANTIGRAVITY, - platform: getPlatformEnum(), - pluginType: PLUGIN_TYPE.GEMINI, -}; - -// System prompts -export const CLAUDE_SYSTEM_PROMPT = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; -export const ANTIGRAVITY_DEFAULT_SYSTEM = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**"; - -// OAuth endpoints -export const OAUTH_ENDPOINTS = { - google: { - token: "https://oauth2.googleapis.com/token", - auth: "https://accounts.google.com/o/oauth2/auth" - }, - openai: { - token: "https://auth.openai.com/oauth/token", - auth: "https://auth.openai.com/oauth/authorize" - }, - anthropic: { - token: "https://api.anthropic.com/v1/oauth/token", - auth: "https://api.anthropic.com/v1/oauth/authorize" - }, - qwen: { - token: "https://chat.qwen.ai/api/v1/oauth2/token", - auth: "https://chat.qwen.ai/api/v1/oauth2/device/code" - }, - iflow: { - token: "https://iflow.cn/oauth/token", - auth: "https://iflow.cn/oauth" - }, - github: { - token: "https://github.com/login/oauth/access_token", - auth: "https://github.com/login/oauth/authorize", - deviceCode: "https://github.com/login/device/code" - } -}; - -// Generate Kimi OAuth custom headers -export function buildKimiHeaders() { - return { - "X-Msh-Platform": "9router", - "X-Msh-Version": "2.1.2", - "X-Msh-Device-Model": typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown", - "X-Msh-Device-Id": `kimi-${Date.now()}` - }; -} diff --git a/9router/open-sse/config/codexInstructions.js b/9router/open-sse/config/codexInstructions.js deleted file mode 100644 index b903ac06..00000000 --- a/9router/open-sse/config/codexInstructions.js +++ /dev/null @@ -1,120 +0,0 @@ -// Default instructions for Codex models -// Source: CLIProxyAPI internal/misc/codex_instructions/ - -export const CODEX_DEFAULT_INSTRUCTIONS = `You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer. - -## General - -- When searching for text or files, prefer using \`rg\` or \`rg --files\` respectively because \`rg\` is much faster than alternatives like \`grep\`. (If the \`rg\` command is not found, then use alternatives.) - -## Editing constraints - -- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. -- Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like "Assigns the value to the variable", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare. -- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase). -- You may be in a dirty git worktree. - * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user. - * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes. - * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them. - * If the changes are in unrelated files, just ignore them and don't revert them. -- Do not amend a commit unless explicitly requested to do so. -- While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed. -- **NEVER** use destructive commands like \`git reset --hard\` or \`git checkout --\` unless specifically requested or approved by the user. - -## Plan tool - -When using the planning tool: -- Skip using the planning tool for straightforward tasks (roughly the easiest 25%). -- Do not make single-step plans. -- When you made a plan, update it after having performed one of the sub-tasks that you shared on the plan. - -## Codex CLI harness, sandboxing, and approvals - -The Codex CLI harness supports several different configurations for sandboxing and escalation approvals that the user can choose from. - -Filesystem sandboxing defines which files can be read or written. The options for \`sandbox_mode\` are: -- **read-only**: The sandbox only permits reading files. -- **workspace-write**: The sandbox permits reading files, and editing files in \`cwd\` and \`writable_roots\`. Editing files in other directories requires approval. -- **danger-full-access**: No filesystem sandboxing - all commands are permitted. - -Network sandboxing defines whether network can be accessed without approval. Options for \`network_access\` are: -- **restricted**: Requires approval -- **enabled**: No approval needed - -Approvals are your mechanism to get user consent to run shell commands without the sandbox. Possible configuration options for \`approval_policy\` are -- **untrusted**: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands. -- **on-failure**: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox. -- **on-request**: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. (Note that this mode is not always available. If it is, you'll see parameters for it in the \`shell\` command description.) -- **never**: This is a non-interactive mode where you may NEVER ask the user for approval to run commands. Instead, you must always persist and work around constraints to solve the task for the user. You MUST do your utmost best to finish the task and validate your work before yielding. If this mode is paired with \`danger-full-access\`, take advantage of it to deliver the best outcome for the user. Further, in this mode, your default testing philosophy is overridden: Even if you don't see local patterns for testing, you may add tests and scripts to validate your work. Just remove them before yielding. - -When you are running with \`approval_policy == on-request\`, and sandboxing enabled, here are scenarios where you'll need to request approval: -- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var) -- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files. -- You are running sandboxed and need to run a command that requires network access (e.g. installing packages) -- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the \`sandbox_permissions\` and \`justification\` parameters - do not message the user before requesting approval for the command. -- You are about to take a potentially destructive action such as an \`rm\` or \`git reset\` that the user did not explicitly ask for -- (for all of these, you should weigh alternative paths that do not require approval) - -When \`sandbox_mode\` is set to read-only, you'll need to request approval for any command that isn't a read. - -You will be told what filesystem sandboxing, network sandboxing, and approval mode are active in a developer or user message. If you are not told about this, assume that you are running with workspace-write, network sandboxing enabled, and approval on-failure. - -Although they introduce friction to the user because your work is paused until the user responds, you should leverage them when necessary to accomplish important work. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to "never", in which case never ask for approvals. - -When requesting approval to execute a command that will require escalated privileges: - - Provide the \`sandbox_permissions\` parameter with the value \`"require_escalated"\` - - Include a short, 1 sentence explanation for why you need escalated permissions in the justification parameter - -## Special user requests - -- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as \`date\`), you should do so. -- If the user asks for a "review", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps. - -## Frontend tasks -When doing frontend design tasks, avoid collapsing into "AI slop" or safe, average-looking layouts. -Aim for interfaces that feel intentional, bold, and a bit surprising. -- Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system). -- Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No purple bias or dark mode bias. -- Motion: Use a few meaningful animations (page-load, staggered reveals) instead of generic micro-motions. -- Background: Don't rely on flat, single-color backgrounds; use gradients, shapes, or subtle patterns to build atmosphere. -- Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs. -- Ensure the page loads properly on both desktop and mobile - -Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language. - -## Presenting your work and final message - -You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. - -- Default: be very concise; friendly coding teammate tone. -- Ask only when needed; suggest ideas; mirror the user's style. -- For substantial work, summarize clearly; follow final‑answer formatting. -- Skip heavy formatting for simple confirmations. -- Don't dump large files you've written; reference paths only. -- No "save/copy this file" - User is on the same machine. -- Offer logical next steps (tests, commits, build) briefly; add verify steps if you couldn't do something. -- For code changes: - * Lead with a quick explanation of the change, and then give more details on the context covering where and why a change was made. Do not start this explanation with "summary", just jump right in. - * If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. - * When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number. -- The user does not command execution outputs. When asked to show the output of a command (e.g. \`git show\`), relay the important details in your answer or summarize the key lines so the user understands the result. - -### Final answer structure and style guidelines - -- Plain text; CLI handles styling. Use structure only when it helps scanability. -- Headers: optional; short Title Case (1-3 words) wrapped in **…**; no blank line before the first bullet; add only if they truly help. -- Bullets: use - ; merge related points; keep to one line when possible; 4–6 per list ordered by importance; keep phrasing consistent. -- Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **. -- Code samples or multi-line snippets should be wrapped in fenced code blocks; include an info string as often as possible. -- Structure: group related bullets; order sections general → specific → supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task. -- Tone: collaborative, concise, factual; present tense, active voice; self‑contained; no "above/below"; parallel wording. -- Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short—wrap/reformat if long; avoid naming formatting styles in answers. -- Adaptation: code explanations → precise, structured with code refs; simple tasks → lead with outcome; big changes → logical walkthrough + rationale + next actions; casual one-offs → plain sentences, no headers/bullets. -- File References: When referencing files in your response follow the below rules: - * Use inline code to make file paths clickable. - * Each reference should have a stand alone path. Even if it's the same file. - * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. - * Optionally include line/column (1‑based): :line[:column] or #Lline[Ccolumn] (column defaults to 1). - * Do not use URIs like file://, vscode://, or https://. - * Do not provide range of lines - * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\\repo\\project\\main.rs:12:5`; diff --git a/9router/open-sse/config/constants.js b/9router/open-sse/config/constants.js deleted file mode 100644 index 7607376d..00000000 --- a/9router/open-sse/config/constants.js +++ /dev/null @@ -1,4 +0,0 @@ -// Barrel re-export — consumers can migrate to specific files over time -export * from "./providers.js"; -export * from "./appConstants.js"; -export * from "./runtimeConfig.js"; diff --git a/9router/open-sse/config/defaultThinkingSignature.js b/9router/open-sse/config/defaultThinkingSignature.js deleted file mode 100644 index 1a97d1c9..00000000 --- a/9router/open-sse/config/defaultThinkingSignature.js +++ /dev/null @@ -1,7 +0,0 @@ -// Default signature for thinking mode when no signature from thinkingStore -export const DEFAULT_THINKING_CLAUDE_SIGNATURE = "EpwGCkYIChgCKkCzVUuRrg7CcglSUWEef4rH6o35g9UYS8ZPe0/VomQTBsFx6sttYNj5l8GqgW6ejuHyYqpFToxIbZl0bw17l5dJEgzCnqDO0Z8fRlMrNgsaDLS1cnCjC53KBqE0CCIwAADQdo1eO+7qPAmo8J4WR3JPmr92S97kmvr5K1iPMiOpkZNj8mEXW8uzBoOJs/9ZKoMFiqHJ3UObwaJDqFOW70E9oCwDoc6jesaWVAEdN5vWfKMpIkjFJjECdjIdkxyJNJ8Ib8yXVal3qwE7uThoPRqSZDdHB5mmwPEjWE/90cSYCbtX2YsJki1265CabBb8/QEkODXg4kgRrL+c8e8rRXz/dr1RswvaPuzEdGKHRNi9UooNUeOK4/ebx1KkP9YZttyohN9GWqlts36kOoW0Cfie/ABDgF9g534BPth/sstxDM6d79QlRmh6NxizyTF74DXJI34u0M4tTRchqE5pAq85SgdJaa+dix1yJPMji8m6nZkwJbscJb9rdc2MKyKWjz8QL2+rTSSuZ2F1k1qSsW0xNcI7qLcI12Vncfn/VqY6YOIZy/saZBR0ezXvN6g+UYbuIdyVg7AyIFZt3nbrO7/kmOEb2VKzygwklHGEIJHfFgMpH3JSrAzbZIowVHOF7VaJ+KXRFDCFin7hHTOiOsdg+1ij1mML9Z/x/9CP4b7OUcaQm1llDZPSHc6rZMNL3DdB+fW5YfmNgKU35S+7AMtA10nVILzDAk1UV4T2K9Do09JlI6rjOs9UuULlIN2Z0eE8YTlANR6uQcw7lMcdfqYE8tke4rDKc2dDiaS5vVe45VewICNpdXGN11yw8QqH7p27CR1HtN30e0tHXOR3bIwWk/Yb6O5fTaKG6Ri8e5ZCPvdD9HqepVi188nM0iTjJqL58F3ni04ECIhcbyaQWnuTes1Kw4CMwiZDLQkk8Hgz7HkUOf1btQTF/0nhD7ry0n0hAEg2PaDM3V6TjOjf4hEldRmeqERcQF1PfgKb6ZM12rlIIfUqKACczWJSzTV158+47HX36o0cgux6nFlv/DE+sEiRVxgB"; - -export const DEFAULT_THINKING_GEMINI_SIGNATURE = "EuwGCukGAXLI2nxwZIq54WWSoL/YN0P3TsDZ7zRnLi8g0S4aVr2HUGxvaHKySuY6HAVzcE0GPGjXrytLIldxthSvfxgUlJh6Qa9Z+Oj5QZBlYdg6HaJ6yuY5R7waE6rdwBsRf7Ft2j3DJ9rMi9qhWFqApewYtPhls3VHtuvND3l8Rm09+lbAXQs6KKWEWrxNLKTBkfpMgXhRERc/TQRMZu1twAablm6/Zk1tsYRvfWKLsNbeKF+CCojJdXJKvnR/8Ouuoa+Y2Ti20hcW7aZIIjZDFYPU//k6Ybmhg69J/imbFai2ckhfLaisqdDkdoIiBJScTOUvYqP6AE9d4MsydSC+UlhIMk4hoP76R8vUSCZRMkjOaDXstf/QoVZKbt94wyRZgAJ1G0BqI8L5ow86kLpA4wJEtxsRGymOE4bKUvApveBakYDNM9APkf+LbtbzWSseGjoZcSlycF9iN8Q2XNYKRrHbv3Lr5Y8JjdH/5y/6SHkNehTEZugaeGnSPSyCTWto1kQgHpxdWmhkLfJGNUGLmue7Mesj4TSms4J33mRpYVhNB/J333FCqIP0hr/E7BkkjEn7yZ4X7SQlh+xKPurapsnHRwiKmtsilmEFrnTE9iQr+pMr6M29qqFNv1tr5yumbaJw8JW9sB15tNsRv+dW6BjNanbsKz7HCgKUBc8tGy+7YuhXzAfViyRefcjK7eZW0Fbyt7AbybJTKz78W8NH7ye6LAwzOebXpeZ4D43fNIt8bKh26qgduSQv/7o+pAflkuqHZ99YWgHQ8h8OkZFi3eOiSYjsjhdZ/czWOdoPI/OnqIldzMPF5YlrKBLFX8VhRKVmqgsmWf5PHGulHhMkVlS+XG2UIseGy69ARa93D78Gsa+1n1kJr7EEB7Rh+27vUMxVYLdz1yMSvE5nalTAlg/ZeG8+XQ0cHuAI3KbQpHW2Q++RdXfm5JzD5WdJZUU+Zn8t8UUn85BH4RxZLeE0qJikgSsKoYVBc6YhiMjhPgkR95ReimY4Z0xCJdRo1gjexOFeODZMpQF6Yxnoic7IrdgsFA3iePTbFnPp3IAM1fAThWhXJUn3QInUOTd5o1qmTmn6REbL15g/JQNl+dqUoPkhleeb2V3kjqp1okmO3wMZbPknR3S1LZNmlS72/iBQUm+n2b/RCn4PjmM2"; - -export const DEFAULT_THINKING_TEXT = "..."; - diff --git a/9router/open-sse/config/ollamaModels.js b/9router/open-sse/config/ollamaModels.js deleted file mode 100644 index 2c04b4f0..00000000 --- a/9router/open-sse/config/ollamaModels.js +++ /dev/null @@ -1,19 +0,0 @@ -export const ollamaModels = { - models: [ - { - name: "llama3.2", - modified_at: "2025-12-26T00:00:00Z", - size: 2000000000, - digest: "abc123def456", - details: { format: "gguf", family: "llama", parameter_size: "3B", quantization_level: "Q4_K_M" } - }, - { - name: "qwen2.5", - modified_at: "2025-12-26T00:00:00Z", - size: 4000000000, - digest: "def456abc123", - details: { format: "gguf", family: "qwen", parameter_size: "7B", quantization_level: "Q4_K_M" } - } - ] -}; - diff --git a/9router/open-sse/config/providerModels.js b/9router/open-sse/config/providerModels.js deleted file mode 100644 index 6dfa65cf..00000000 --- a/9router/open-sse/config/providerModels.js +++ /dev/null @@ -1,390 +0,0 @@ -import { PROVIDERS } from "./providers.js"; - -// Provider models - Single source of truth -// Key = alias (cc, cx, gc, qw, if, ag, gh for OAuth; id for API Key) -// Field "provider" for special cases (e.g. AntiGravity models that call different backends) - -export const PROVIDER_MODELS = { - // OAuth Providers (using alias) - cc: [ // Claude Code - { id: "claude-opus-4-6", name: "Claude Opus 4.6" }, - { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, - { id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus" }, - { id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" }, - { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, - ], - cx: [ // OpenAI Codex - { id: "gpt-5.4", name: "GPT 5.4" }, - // GPT 5.3 Codex - all thinking levels - { id: "gpt-5.3-codex", name: "GPT 5.3 Codex" }, - { id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex (xHigh)" }, - { id: "gpt-5.3-codex-high", name: "GPT 5.3 Codex (High)" }, - { id: "gpt-5.3-codex-low", name: "GPT 5.3 Codex (Low)" }, - { id: "gpt-5.3-codex-none", name: "GPT 5.3 Codex (None)" }, - { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, - // Mini - medium and high only - { id: "gpt-5.1-codex-mini", name: "GPT 5.1 Codex Mini" }, - { id: "gpt-5.1-codex-mini-high", name: "GPT 5.1 Codex Mini (High)" }, - // Other models - { id: "gpt-5.2-codex", name: "GPT 5.2 Codex" }, - { id: "gpt-5.2", name: "GPT 5.2" }, - { id: "gpt-5.1-codex-max", name: "GPT 5.1 Codex Max" }, - { id: "gpt-5.1-codex", name: "GPT 5.1 Codex" }, - { id: "gpt-5.1", name: "GPT 5.1" }, - { id: "gpt-5-codex", name: "GPT 5 Codex" }, - { id: "gpt-5-codex-mini", name: "GPT 5 Codex Mini" }, - ], - gc: [ // Gemini CLI - { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, - { id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" }, - ], - qw: [ // Qwen Code - // { id: "qwen3-coder-next", name: "Qwen3 Coder Next" }, - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" }, - { id: "vision-model", name: "Qwen3 Vision Model" }, - { id: "coder-model", name: "Qwen3.5 Coder Model" }, - ], - if: [ // iFlow AI - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "qwen3-max", name: "Qwen3 Max" }, - { id: "qwen3-vl-plus", name: "Qwen3 VL Plus" }, - { id: "qwen3-max-preview", name: "Qwen3 Max Preview" }, - { id: "qwen3-235b", name: "Qwen3 235B A22B" }, - { id: "qwen3-235b-a22b-instruct", name: "Qwen3 235B A22B Instruct" }, - { id: "qwen3-235b-a22b-thinking-2507", name: "Qwen3 235B A22B Thinking" }, - { id: "qwen3-32b", name: "Qwen3 32B" }, - { id: "kimi-k2", name: "Kimi K2" }, - { id: "deepseek-v3.2", name: "DeepSeek V3.2 Exp" }, - { id: "deepseek-v3.1", name: "DeepSeek V3.1 Terminus" }, - { id: "deepseek-v3", name: "DeepSeek V3 671B" }, - { id: "deepseek-r1", name: "DeepSeek R1" }, - { id: "glm-4.7", name: "GLM 4.7" }, - { id: "iflow-rome-30ba3b", name: "iFlow ROME" }, - ], - ag: [ // Antigravity - special case: models call different backends - { id: "gemini-3.1-pro-high", name: "Gemini 3 Pro High" }, - { id: "gemini-3.1-pro-low", name: "Gemini 3 Pro Low" }, - { id: "gemini-3-flash", name: "Gemini 3 Flash" }, - { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, - { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking" }, - { id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium" }, - ], - gh: [ // GitHub Copilot - OpenAI models - { id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" }, - { id: "gpt-4", name: "GPT-4" }, - { id: "gpt-4o", name: "GPT-4o" }, - { id: "gpt-4o-mini", name: "GPT-4o mini" }, - { id: "gpt-4.1", name: "GPT-4.1" }, - { id: "gpt-5", name: "GPT-5" }, - { id: "gpt-5-mini", name: "GPT-5 Mini" }, - { id: "gpt-5-codex", name: "GPT-5 Codex" }, - { id: "gpt-5.1", name: "GPT-5.1" }, - { id: "gpt-5.1-codex", name: "GPT-5.1 Codex" }, - { id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" }, - { id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" }, - { id: "gpt-5.2", name: "GPT-5.2" }, - { id: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, - { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, - { id: "gpt-5.4", name: "GPT-5.4" }, - // GitHub Copilot - Anthropic models - { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, - { id: "claude-opus-4.1", name: "Claude Opus 4.1" }, - { id: "claude-opus-4.5", name: "Claude Opus 4.5" }, - { id: "claude-sonnet-4", name: "Claude Sonnet 4" }, - { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, - { id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" }, - { id: "claude-opus-4.6", name: "Claude Opus 4.6" }, - // GitHub Copilot - Google models - { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" }, - { id: "gemini-3-flash-preview", name: "Gemini 3 Flash" }, - { id: "gemini-3-pro-preview", name: "Gemini 3 Pro" }, - // GitHub Copilot - Other models - { id: "grok-code-fast-1", name: "Grok Code Fast 1" }, - { id: "oswe-vscode-prime", name: "Raptor Mini" }, - ], - kr: [ // Kiro AI - // { id: "claude-opus-4.5", name: "Claude Opus 4.5" }, - { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, - { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, - { id: "deepseek-3.2", name: "DeepSeek 3.2" }, - { id: "deepseek-3.1", name: "DeepSeek 3.1" }, - { id: "qwen3-coder-next", name: "Qwen3 Coder Next" }, - ], - cu: [ // Cursor IDE - { id: "default", name: "Auto (Server Picks)" }, - { id: "claude-4.5-opus-high-thinking", name: "Claude 4.5 Opus High Thinking" }, - { id: "claude-4.5-opus-high", name: "Claude 4.5 Opus High" }, - { id: "claude-4.5-sonnet-thinking", name: "Claude 4.5 Sonnet Thinking" }, - { id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet" }, - { id: "claude-4.5-haiku", name: "Claude 4.5 Haiku" }, - { id: "claude-4.5-opus", name: "Claude 4.5 Opus" }, - { id: "gpt-5.2-codex", name: "GPT 5.2 Codex" }, - { id: "claude-4.6-opus-max", name: "Claude 4.6 Opus Max" }, - { id: "claude-4.6-sonnet-medium-thinking", name: "Claude 4.6 Sonnet Medium Thinking" }, - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, - { id: "gpt-5.2", name: "GPT 5.2" }, - { id: "gpt-5.3-codex", name: "GPT 5.3 Codex" }, - ], - kmc: [ // Kimi Coding - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" }, - { id: "kimi-latest", name: "Kimi Latest" }, - ], - kc: [ // KiloCode - { id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4" }, - { id: "anthropic/claude-opus-4-20250514", name: "Claude Opus 4" }, - { id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro" }, - { id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash" }, - { id: "openai/gpt-4.1", name: "GPT-4.1" }, - { id: "openai/o3", name: "o3" }, - { id: "deepseek/deepseek-chat", name: "DeepSeek Chat" }, - { id: "deepseek/deepseek-reasoner", name: "DeepSeek Reasoner" }, - ], - cl: [ // Cline - { id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" }, - { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }, - { id: "openai/gpt-5.3-codex", name: "GPT-5.3 Codex" }, - { id: "openai/gpt-5.4", name: "GPT-5.4" }, - { id: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" }, - { id: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" }, - { id: "kwaipilot/kat-coder-pro", name: "KAT Coder Pro" }, - ], - - // API Key Providers (alias = id) - openai: [ - { id: "gpt-4o", name: "GPT-4o" }, - { id: "gpt-5-mini", name: "GPT-5 Mini" }, - { id: "gpt-4-turbo", name: "GPT-4 Turbo" }, - { id: "o1", name: "O1" }, - { id: "o1-mini", name: "O1 Mini" }, - ], - anthropic: [ - { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" }, - { id: "claude-opus-4-20250514", name: "Claude Opus 4" }, - { id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" }, - ], - gemini: [ - { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" }, - { id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" }, - { id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" }, - { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, - { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" }, - { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" }, - { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" }, - // Embedding models - { id: "gemini-embedding-001", name: "Gemini Embedding 001", type: "embedding" }, - { id: "text-embedding-005", name: "Text Embedding 005", type: "embedding" }, - { id: "text-embedding-004", name: "Text Embedding 004 (Legacy)", type: "embedding" }, - ], - openrouter: [ - { id: "auto", name: "Auto (Best Available)" }, - ], - glm: [ - { id: "glm-5", name: "GLM 5" }, - { id: "glm-4.7", name: "GLM 4.7" }, - { id: "glm-4.6v", name: "GLM 4.6V (Vision)" }, - ], - "glm-cn": [ - { id: "glm-5", name: "GLM 5" }, - { id: "glm-4.7", name: "GLM-4.7" }, - { id: "glm-4.6", name: "GLM-4.6" }, - { id: "glm-4.5-air", name: "GLM-4.5-Air" }, - ], - kimi: [ - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" }, - { id: "kimi-latest", name: "Kimi Latest" }, - ], - minimax: [ - { id: "MiniMax-M2.7", name: "MiniMax M2.7" }, - { id: "MiniMax-M2.5", name: "MiniMax M2.5" }, - { id: "MiniMax-M2.1", name: "MiniMax M2.1" }, - ], - "minimax-cn": [ - { id: "MiniMax-M2.7", name: "MiniMax M2.7" }, - { id: "MiniMax-M2.5", name: "MiniMax M2.5" }, - { id: "MiniMax-M2.1", name: "MiniMax M2.1" }, - ], - alicode: [ - { id: "qwen3.5-plus", name: "Qwen3.5 Plus" }, - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "glm-5", name: "GLM 5" }, - { id: "MiniMax-M2.5", name: "MiniMax M2.5" }, - { id: "qwen3-max-2026-01-23", name: "Qwen3 Max" }, - { id: "qwen3-coder-next", name: "Qwen3 Coder Next" }, - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "glm-4.7", name: "GLM 4.7" }, - ], - "alicode-intl": [ - { id: "qwen3.5-plus", name: "Qwen3.5 Plus" }, - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "glm-5", name: "GLM 5" }, - { id: "MiniMax-M2.5", name: "MiniMax M2.5" }, - { id: "qwen3-coder-next", name: "Qwen3 Coder Next" }, - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "glm-4.7", name: "GLM 4.7" }, - ], - deepseek: [ - { id: "deepseek-chat", name: "DeepSeek V3.2 Chat" }, - { id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" }, - ], - groq: [ - { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" }, - { id: "meta-llama/llama-4-maverick-17b-128e-instruct", name: "Llama 4 Maverick" }, - { id: "qwen/qwen3-32b", name: "Qwen3 32B" }, - { id: "openai/gpt-oss-120b", name: "GPT-OSS 120B" }, - ], - xai: [ - { id: "grok-4", name: "Grok 4" }, - { id: "grok-4-fast-reasoning", name: "Grok 4 Fast Reasoning" }, - { id: "grok-code-fast-1", name: "Grok Code Fast" }, - { id: "grok-3", name: "Grok 3" }, - ], - mistral: [ - { id: "mistral-large-latest", name: "Mistral Large 3" }, - { id: "codestral-latest", name: "Codestral" }, - { id: "mistral-medium-latest", name: "Mistral Medium 3" }, - ], - perplexity: [ - { id: "sonar-pro", name: "Sonar Pro" }, - { id: "sonar", name: "Sonar" }, - ], - together: [ - { id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", name: "Llama 3.3 70B Turbo" }, - { id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" }, - { id: "Qwen/Qwen3-235B-A22B", name: "Qwen3 235B" }, - { id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", name: "Llama 4 Maverick" }, - ], - fireworks: [ - { id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1" }, - { id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B" }, - { id: "accounts/fireworks/models/qwen3-235b-a22b", name: "Qwen3 235B" }, - ], - cerebras: [ - { id: "gpt-oss-120b", name: "GPT OSS 120B" }, - { id: "zai-glm-4.7", name: "ZAI GLM 4.7" }, - { id: "llama-3.3-70b", name: "Llama 3.3 70B" }, - { id: "llama-4-scout-17b-16e-instruct", name: "Llama 4 Scout" }, - { id: "qwen-3-235b-a22b-instruct-2507", name: "Qwen3 235B A22B" }, - { id: "qwen-3-32b", name: "Qwen3 32B" }, - ], - cohere: [ - { id: "command-r-plus-08-2024", name: "Command R+ (Aug 2024)" }, - { id: "command-r-08-2024", name: "Command R (Aug 2024)" }, - { id: "command-a-03-2025", name: "Command A (Mar 2025)" }, - ], - nvidia: [ - { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5" }, - { id: "z-ai/glm4.7", name: "GLM 4.7" }, - { id: "deepseek-ai/deepseek-v3.2", name: "DeepSeek V3.2" }, - { id: "nvidia/llama-3.3-70b-instruct", name: "Llama 3.3 70B" }, - { id: "meta/llama-4-maverick-17b-128e-instruct", name: "Llama 4 Maverick" }, - { id: "deepseek/deepseek-r1", name: "DeepSeek R1" }, - ], - nebius: [ - { id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B Instruct" }, - ], - siliconflow: [ - { id: "deepseek-ai/DeepSeek-V3.2", name: "DeepSeek V3.2" }, - { id: "deepseek-ai/DeepSeek-V3.1", name: "DeepSeek V3.1" }, - { id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" }, - { id: "Qwen/Qwen3-235B-A22B-Instruct-2507", name: "Qwen3 235B" }, - { id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", name: "Qwen3 Coder 480B" }, - { id: "Qwen/Qwen3-32B", name: "Qwen3 32B" }, - { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5" }, - { id: "zai-org/GLM-4.7", name: "GLM 4.7" }, - { id: "openai/gpt-oss-120b", name: "GPT OSS 120B" }, - { id: "baidu/ERNIE-4.5-300B-A47B", name: "ERNIE 4.5 300B" }, - ], - hyperbolic: [ - { id: "Qwen/QwQ-32B", name: "QwQ 32B" }, - { id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" }, - { id: "deepseek-ai/DeepSeek-V3", name: "DeepSeek V3" }, - { id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B" }, - { id: "meta-llama/Llama-3.2-3B-Instruct", name: "Llama 3.2 3B" }, - { id: "Qwen/Qwen2.5-72B-Instruct", name: "Qwen 2.5 72B" }, - { id: "Qwen/Qwen2.5-Coder-32B-Instruct", name: "Qwen 2.5 Coder 32B" }, - { id: "NousResearch/Hermes-3-Llama-3.1-70B", name: "Hermes 3 70B" }, - ], - ollama: [ - { id: "gpt-oss:120b", name: "GPT OSS 120B" }, - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "glm-5", name: "GLM 5" }, - { id: "minimax-m2.5", name: "MiniMax M2.5" }, - { id: "glm-4.7-flash", name: "GLM 4.7 Flash" }, - { id: "qwen3.5", name: "Qwen3.5" }, - ], - vertex: [ - { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" }, - { id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" }, - { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, - { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" }, - ], - "vertex-partner": [ - { id: "deepseek-ai/deepseek-v3.2-maas", name: "DeepSeek V3.2 (Vertex)" }, - { id: "qwen/qwen3-next-80b-a3b-thinking-maas", name: "Qwen3 Next 80B Thinking (Vertex)" }, - { id: "qwen/qwen3-next-80b-a3b-instruct-maas", name: "Qwen3 Next 80B Instruct (Vertex)" }, - { id: "zai-org/glm-5-maas", name: "GLM-5 (Vertex)" }, - ], -}; - -// Helper functions -export function getProviderModels(aliasOrId) { - return PROVIDER_MODELS[aliasOrId] || []; -} - -export function getDefaultModel(aliasOrId) { - const models = PROVIDER_MODELS[aliasOrId]; - return models?.[0]?.id || null; -} - -export function isValidModel(aliasOrId, modelId, passthroughProviders = new Set()) { - if (passthroughProviders.has(aliasOrId)) return true; - const models = PROVIDER_MODELS[aliasOrId]; - if (!models) return false; - return models.some(m => m.id === modelId); -} - -export function findModelName(aliasOrId, modelId) { - const models = PROVIDER_MODELS[aliasOrId]; - if (!models) return modelId; - const found = models.find(m => m.id === modelId); - return found?.name || modelId; -} - -export function getModelTargetFormat(aliasOrId, modelId) { - const models = PROVIDER_MODELS[aliasOrId]; - if (!models) return null; - const found = models.find(m => m.id === modelId); - return found?.targetFormat || null; -} - -// OAuth providers that use short aliases (everything else: alias = id) -const OAUTH_ALIASES = { - claude: "cc", - codex: "cx", - "gemini-cli": "gc", - qwen: "qw", - iflow: "if", - antigravity: "ag", - github: "gh", - kiro: "kr", - cursor: "cu", - "kimi-coding": "kmc", - kilocode: "kc", - cline: "cl", - vertex: "vertex", - "vertex-partner": "vertex-partner", -}; - -// Derived from PROVIDERS — no need to maintain manually -export const PROVIDER_ID_TO_ALIAS = Object.fromEntries( - Object.keys(PROVIDERS).map(id => [id, OAUTH_ALIASES[id] || id]) -); - -export function getModelsByProviderId(providerId) { - const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId; - return PROVIDER_MODELS[alias] || []; -} diff --git a/9router/open-sse/config/providers.js b/9router/open-sse/config/providers.js deleted file mode 100644 index 91a31326..00000000 --- a/9router/open-sse/config/providers.js +++ /dev/null @@ -1,313 +0,0 @@ -import { platform, arch } from "os"; - -// === OS/Arch helpers === -function mapStainlessOs() { - switch (platform()) { - case "darwin": return "MacOS"; - case "win32": return "Windows"; - case "linux": return "Linux"; - case "freebsd": return "FreeBSD"; - default: return `Other::${platform()}`; - } -} - -function mapStainlessArch() { - switch (arch()) { - case "x64": return "x64"; - case "arm64": return "arm64"; - case "ia32": return "x86"; - default: return `other::${arch()}`; - } -} - -// Shared Claude-compatible API headers (reused across claude-format providers) -const CLAUDE_API_HEADERS = { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14" -}; - -// Shared baseUrls -const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1/messages"; - -export const PROVIDERS = { - claude: { - baseUrl: "https://api.anthropic.com/v1/messages", - format: "claude", - headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05", - "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/2.1.63 (external, cli)", - "X-App": "cli", - "X-Stainless-Helper-Method": "stream", - "X-Stainless-Retry-Count": "0", - "X-Stainless-Runtime-Version": "v24.3.0", - "X-Stainless-Package-Version": "0.74.0", - "X-Stainless-Runtime": "node", - "X-Stainless-Lang": "js", - "X-Stainless-Arch": mapStainlessArch(), - "X-Stainless-Os": mapStainlessOs(), - "X-Stainless-Timeout": "600" - }, - clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", - tokenUrl: "https://api.anthropic.com/v1/oauth/token" - }, - gemini: { - baseUrl: "https://generativelanguage.googleapis.com/v1beta/models", - format: "gemini", - clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com", - clientSecret: "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" - }, - "gemini-cli": { - baseUrl: "https://cloudcode-pa.googleapis.com/v1internal", - format: "gemini-cli", - clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com", - clientSecret: "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" - }, - codex: { - baseUrl: "https://chatgpt.com/backend-api/codex/responses", - format: "openai-responses", - headers: { - "originator": "codex-cli", - "User-Agent": "codex-cli/1.0.18 (macOS; arm64)" - }, - clientId: "app_EMoamEEZ73f0CkXaXp7hrann", - clientSecret: "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", - tokenUrl: "https://auth.openai.com/oauth/token" - }, - qwen: { - baseUrl: "https://portal.qwen.ai/v1/chat/completions", - format: "openai", - headers: { - "User-Agent": "google-api-nodejs-client/9.15.1", - "X-Goog-Api-Client": "gl-node/22.17.0" - }, - clientId: "f0304373b74a44d2b584a3fb70ca9e56", - tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token", - authUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code" - }, - iflow: { - baseUrl: "https://apis.iflow.cn/v1/chat/completions", - format: "openai", - headers: { "User-Agent": "iFlow-Cli" }, - clientId: "10009311001", - clientSecret: "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW", - tokenUrl: "https://iflow.cn/oauth/token", - authUrl: "https://iflow.cn/oauth" - }, - antigravity: { - baseUrls: [ - "https://daily-cloudcode-pa.googleapis.com", - "https://daily-cloudcode-pa.sandbox.googleapis.com", - ], - format: "antigravity", - headers: { "User-Agent": `antigravity/1.107.0 ${platform()}/${arch()}` }, - clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com", - clientSecret: "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" - }, - openrouter: { - baseUrl: "https://openrouter.ai/api/v1/chat/completions", - format: "openai", - headers: { - "HTTP-Referer": "https://endpoint-proxy.local", - "X-Title": "Endpoint Proxy" - } - }, - openai: { - baseUrl: "https://api.openai.com/v1/chat/completions", - format: "openai" - }, - glm: { - baseUrl: "https://api.z.ai/api/anthropic/v1/messages", - format: "claude", - headers: { ...CLAUDE_API_HEADERS } - }, - "glm-cn": { - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions", - format: "openai", - headers: {} - }, - kimi: { - baseUrl: KIMI_CODING_BASE_URL, - format: "claude", - headers: { ...CLAUDE_API_HEADERS } - }, - minimax: { - baseUrl: "https://api.minimax.io/anthropic/v1/messages", - format: "claude", - headers: { ...CLAUDE_API_HEADERS } - }, - "minimax-cn": { - baseUrl: "https://api.minimaxi.com/anthropic/v1/messages", - format: "claude", - headers: { ...CLAUDE_API_HEADERS } - }, - alicode: { - baseUrl: "https://coding.dashscope.aliyuncs.com/v1/chat/completions", - format: "openai", - headers: {} - }, - "alicode-intl": { - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", - format: "openai", - headers: {} - }, - github: { - baseUrl: "https://api.githubcopilot.com/chat/completions", - responsesUrl: "https://api.githubcopilot.com/responses", - format: "openai", - headers: { - "copilot-integration-id": "vscode-chat", - "editor-version": "vscode/1.110.0", - "editor-plugin-version": "copilot-chat/0.38.0", - "user-agent": "GitHubCopilotChat/0.38.0", - "openai-intent": "conversation-panel", - "x-github-api-version": "2025-04-01", - "x-vscode-user-agent-library-version": "electron-fetch", - "X-Initiator": "user", - "Accept": "application/json", - "Content-Type": "application/json" - } - }, - kiro: { - baseUrl: "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse", - format: "kiro", - headers: { - "Content-Type": "application/json", - "Accept": "application/vnd.amazon.eventstream", - "X-Amz-Target": "AmazonCodeWhispererStreamingService.GenerateAssistantResponse", - "User-Agent": "AWS-SDK-JS/3.0.0 kiro-ide/1.0.0", - "X-Amz-User-Agent": "aws-sdk-js/3.0.0 kiro-ide/1.0.0" - }, - tokenUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken", - authUrl: "https://prod.us-east-1.auth.desktop.kiro.dev" - }, - cursor: { - baseUrl: "https://api2.cursor.sh", - chatPath: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools", - format: "cursor", - headers: { - "connect-accept-encoding": "gzip", - "connect-protocol-version": "1", - "Content-Type": "application/connect+proto", - "User-Agent": "connect-es/1.6.1" - }, - clientVersion: "1.1.3" - }, - "kimi-coding": { - baseUrl: KIMI_CODING_BASE_URL, - format: "claude", - headers: { ...CLAUDE_API_HEADERS }, - clientId: "17e5f671-d194-4dfb-9706-5516cb48c098", - tokenUrl: "https://auth.kimi.com/api/oauth/token", - refreshUrl: "https://auth.kimi.com/api/oauth/token" - }, - kilocode: { - baseUrl: "https://api.kilo.ai/api/openrouter/chat/completions", - format: "openai", - headers: {} - }, - cline: { - baseUrl: "https://api.cline.bot/api/v1/chat/completions", - format: "openai", - headers: { - "HTTP-Referer": "https://cline.bot", - "X-Title": "Cline" - }, - tokenUrl: "https://api.cline.bot/api/v1/auth/token", - refreshUrl: "https://api.cline.bot/api/v1/auth/refresh" - }, - nvidia: { - baseUrl: "https://integrate.api.nvidia.com/v1/chat/completions", - format: "openai" - }, - anthropic: { - baseUrl: "https://api.anthropic.com/v1/messages", - format: "claude", - headers: { ...CLAUDE_API_HEADERS } - }, - deepseek: { - baseUrl: "https://api.deepseek.com/chat/completions", - format: "openai" - }, - groq: { - baseUrl: "https://api.groq.com/openai/v1/chat/completions", - format: "openai" - }, - xai: { - baseUrl: "https://api.x.ai/v1/chat/completions", - format: "openai" - }, - mistral: { - baseUrl: "https://api.mistral.ai/v1/chat/completions", - format: "openai" - }, - perplexity: { - baseUrl: "https://api.perplexity.ai/chat/completions", - format: "openai" - }, - together: { - baseUrl: "https://api.together.xyz/v1/chat/completions", - format: "openai" - }, - fireworks: { - baseUrl: "https://api.fireworks.ai/inference/v1/chat/completions", - format: "openai" - }, - cerebras: { - baseUrl: "https://api.cerebras.ai/v1/chat/completions", - format: "openai" - }, - cohere: { - baseUrl: "https://api.cohere.ai/v1/chat/completions", - format: "openai" - }, - nebius: { - baseUrl: "https://api.studio.nebius.ai/v1/chat/completions", - format: "openai" - }, - siliconflow: { - baseUrl: "https://api.siliconflow.cn/v1/chat/completions", - format: "openai" - }, - hyperbolic: { - baseUrl: "https://api.hyperbolic.xyz/v1/chat/completions", - format: "openai" - }, - deepgram: { - baseUrl: "https://api.deepgram.com/v1/listen", - format: "openai" - }, - assemblyai: { - baseUrl: "https://api.assemblyai.com/v1/audio/transcriptions", - format: "openai" - }, - nanobanana: { - baseUrl: "https://api.nanobananaapi.ai/v1/chat/completions", - format: "openai" - }, - chutes: { - baseUrl: "https://llm.chutes.ai/v1/chat/completions", - format: "openai" - }, - ollama: { - baseUrl: "https://ollama.com/api/chat", - format: "ollama" - }, - "ollama-local": { - baseUrl: "http://localhost:11434/api/chat", - format: "ollama" - }, - // Vertex AI - Gemini models via Service Account JSON - // baseUrl is not used; VertexExecutor.buildUrl() constructs it dynamically - vertex: { - baseUrl: "https://aiplatform.googleapis.com", - format: "gemini" - }, - // Vertex AI - Partner models (Claude, Llama, Mistral, GLM) via SA JSON - // Uses OpenAI-compatible global endpoint (or rawPredict for Anthropic) - "vertex-partner": { - baseUrl: "https://aiplatform.googleapis.com", - format: "openai" - }, -}; diff --git a/9router/open-sse/config/runtimeConfig.js b/9router/open-sse/config/runtimeConfig.js deleted file mode 100644 index 19ac15ab..00000000 --- a/9router/open-sse/config/runtimeConfig.js +++ /dev/null @@ -1,92 +0,0 @@ -// HTTP status codes -export const HTTP_STATUS = { - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - PAYMENT_REQUIRED: 402, - FORBIDDEN: 403, - NOT_FOUND: 404, - NOT_ACCEPTABLE: 406, - REQUEST_TIMEOUT: 408, - RATE_LIMITED: 429, - SERVER_ERROR: 500, - BAD_GATEWAY: 502, - SERVICE_UNAVAILABLE: 503, - GATEWAY_TIMEOUT: 504 -}; - -// OpenAI-compatible error types mapping -export const ERROR_TYPES = { - [HTTP_STATUS.BAD_REQUEST]: { type: "invalid_request_error", code: "bad_request" }, - [HTTP_STATUS.UNAUTHORIZED]: { type: "authentication_error", code: "invalid_api_key" }, - [HTTP_STATUS.FORBIDDEN]: { type: "permission_error", code: "insufficient_quota" }, - [HTTP_STATUS.NOT_FOUND]: { type: "invalid_request_error", code: "model_not_found" }, - [HTTP_STATUS.NOT_ACCEPTABLE]: { type: "invalid_request_error", code: "model_not_supported" }, - [HTTP_STATUS.RATE_LIMITED]: { type: "rate_limit_error", code: "rate_limit_exceeded" }, - [HTTP_STATUS.SERVER_ERROR]: { type: "server_error", code: "internal_server_error" }, - [HTTP_STATUS.BAD_GATEWAY]: { type: "server_error", code: "bad_gateway" }, - [HTTP_STATUS.SERVICE_UNAVAILABLE]: { type: "server_error", code: "service_unavailable" }, - [HTTP_STATUS.GATEWAY_TIMEOUT]: { type: "server_error", code: "gateway_timeout" } -}; - -// Default error messages per status code -export const DEFAULT_ERROR_MESSAGES = { - [HTTP_STATUS.BAD_REQUEST]: "Bad request", - [HTTP_STATUS.UNAUTHORIZED]: "Invalid API key provided", - [HTTP_STATUS.FORBIDDEN]: "You exceeded your current quota", - [HTTP_STATUS.NOT_FOUND]: "Model not found", - [HTTP_STATUS.NOT_ACCEPTABLE]: "Model not supported", - [HTTP_STATUS.RATE_LIMITED]: "Rate limit exceeded", - [HTTP_STATUS.SERVER_ERROR]: "Internal server error", - [HTTP_STATUS.BAD_GATEWAY]: "Bad gateway - upstream provider error", - [HTTP_STATUS.SERVICE_UNAVAILABLE]: "Service temporarily unavailable", - [HTTP_STATUS.GATEWAY_TIMEOUT]: "Gateway timeout" -}; - -// Cache TTLs (seconds) -export const CACHE_TTL = { - userInfo: 300, // 5 minutes - modelAlias: 3600 // 1 hour -}; - -// Memory management config -export const MEMORY_CONFIG = { - sessionTtlMs: 2 * 60 * 60 * 1000, - sessionCleanupIntervalMs: 30 * 60 * 1000, - dnsCacheTtlMs: 5 * 60 * 1000, - proxyDispatchersMaxSize: 20, -}; - -// Default token limits -export const DEFAULT_MAX_TOKENS = 64000; -export const DEFAULT_MIN_TOKENS = 32000; - -// Retry config for 429 responses -export const RETRY_CONFIG = { - maxAttempts: 2, - delayMs: 2000 -}; - -// Exponential backoff config for rate limits -export const BACKOFF_CONFIG = { - base: 1000, - max: 2 * 60 * 1000, - maxLevel: 15 -}; - -// Error-based cooldown times -export const COOLDOWN_MS = { - unauthorized: 2 * 60 * 1000, - paymentRequired: 2 * 60 * 1000, - notFound: 2 * 60 * 1000, - transient: 30 * 1000, - requestNotAllowed: 5 * 1000, - // Legacy aliases - rateLimit: 2 * 60 * 1000, - serviceUnavailable: 2 * 1000, - authExpired: 2 * 60 * 1000 -}; - -// Requests containing these texts will bypass provider -export const SKIP_PATTERNS = [ - "Please write a 5-10 word title for the following conversation:" -]; diff --git a/9router/open-sse/executors/antigravity.js b/9router/open-sse/executors/antigravity.js deleted file mode 100644 index 3912f603..00000000 --- a/9router/open-sse/executors/antigravity.js +++ /dev/null @@ -1,261 +0,0 @@ -import crypto from "crypto"; -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER } from "../config/appConstants.js"; -import { HTTP_STATUS } from "../config/runtimeConfig.js"; -import { deriveSessionId } from "../utils/sessionManager.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; - -const MAX_RETRY_AFTER_MS = 10000; - -export class AntigravityExecutor extends BaseExecutor { - constructor() { - super("antigravity", PROVIDERS.antigravity); - } - - buildUrl(model, stream, urlIndex = 0) { - const baseUrls = this.getBaseUrls(); - const baseUrl = baseUrls[urlIndex] || baseUrls[0]; - const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; - return `${baseUrl}/v1internal:${action}`; - } - - buildHeaders(credentials, stream = true, sessionId = null) { - return { - "Content-Type": "application/json", - "Authorization": `Bearer ${credentials.accessToken}`, - "User-Agent": this.config.headers?.["User-Agent"] || ANTIGRAVITY_HEADERS["User-Agent"], - [INTERNAL_REQUEST_HEADER.name]: INTERNAL_REQUEST_HEADER.value, - ...(sessionId && { "X-Machine-Session-Id": sessionId }), - "Accept": stream ? "text/event-stream" : "application/json" - }; - } - - transformRequest(model, body, stream, credentials) { - const projectId = credentials?.projectId || this.generateProjectId(); - - // Fix contents for Claude models via Antigravity - const contents = body.request?.contents?.map(c => { - let role = c.role; - // functionResponse must be role "user" for Claude models - if (c.parts?.some(p => p.functionResponse)) { - role = "user"; - } - // Strip thought-only parts, keep thoughtSignature on functionCall parts (Gemini 3+ requires it) - const parts = c.parts?.filter(p => { - if (p.thought && !p.functionCall) return false; - if (p.thoughtSignature && !p.functionCall && !p.text) return false; - return true; - }); - if (role !== c.role || parts?.length !== c.parts?.length) { - return { ...c, role, parts }; - } - return c; - }); - - const transformedRequest = { - ...body.request, - ...(contents && { contents }), - sessionId: body.request?.sessionId || deriveSessionId(credentials?.email || credentials?.connectionId), - safetySettings: undefined, - toolConfig: body.request?.tools?.length > 0 - ? { functionCallingConfig: { mode: "VALIDATED" } } - : body.request?.toolConfig - }; - - return { - ...body, - project: projectId, - model: model, - userAgent: "antigravity", - requestType: "agent", - requestId: `agent-${crypto.randomUUID()}`, - request: transformedRequest - }; - } - - async refreshCredentials(credentials, log) { - if (!credentials.refreshToken) return null; - - try { - const response = await fetch(OAUTH_ENDPOINTS.google.token, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: credentials.refreshToken, - client_id: this.config.clientId, - client_secret: this.config.clientSecret - }) - }); - - if (!response.ok) return null; - - const tokens = await response.json(); - log?.info?.("TOKEN", "Antigravity refreshed"); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || credentials.refreshToken, - expiresIn: tokens.expires_in, - projectId: credentials.projectId - }; - } catch (error) { - log?.error?.("TOKEN", `Antigravity refresh error: ${error.message}`); - return null; - } - } - - generateProjectId() { - const adj = ["useful", "bright", "swift", "calm", "bold"][Math.floor(Math.random() * 5)]; - const noun = ["fuze", "wave", "spark", "flow", "core"][Math.floor(Math.random() * 5)]; - return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`; - } - - generateSessionId() { - return crypto.randomUUID() + Date.now().toString(); - } - - parseRetryHeaders(headers) { - if (!headers?.get) return null; - - const retryAfter = headers.get('retry-after'); - if (retryAfter) { - const seconds = parseInt(retryAfter, 10); - if (!isNaN(seconds) && seconds > 0) return seconds * 1000; - - const date = new Date(retryAfter); - if (!isNaN(date.getTime())) { - const diff = date.getTime() - Date.now(); - return diff > 0 ? diff : null; - } - } - - const resetAfter = headers.get('x-ratelimit-reset-after'); - if (resetAfter) { - const seconds = parseInt(resetAfter, 10); - if (!isNaN(seconds) && seconds > 0) return seconds * 1000; - } - - const resetTimestamp = headers.get('x-ratelimit-reset'); - if (resetTimestamp) { - const ts = parseInt(resetTimestamp, 10) * 1000; - const diff = ts - Date.now(); - return diff > 0 ? diff : null; - } - - return null; - } - - // Parse retry time from Antigravity error message body - // Format: "Your quota will reset after 2h7m23s" or "1h30m" or "45m" or "30s" - parseRetryFromErrorMessage(errorMessage) { - if (!errorMessage || typeof errorMessage !== "string") return null; - - const match = errorMessage.match(/reset after (\d+h)?(\d+m)?(\d+s)?/i); - if (!match) return null; - - let totalMs = 0; - if (match[1]) totalMs += parseInt(match[1]) * 3600 * 1000; // hours - if (match[2]) totalMs += parseInt(match[2]) * 60 * 1000; // minutes - if (match[3]) totalMs += parseInt(match[3]) * 1000; // seconds - - return totalMs > 0 ? totalMs : null; - } - - async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { - const fallbackCount = this.getFallbackCount(); - let lastError = null; - let lastStatus = 0; - const MAX_AUTO_RETRIES = 3; - const MAX_RETRY_AFTER_RETRIES = 3; - const retryAttemptsByUrl = {}; // Track retry attempts per URL - const retryAfterAttemptsByUrl = {}; // Track Retry-After retries per URL - - for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) { - const url = this.buildUrl(model, stream, urlIndex); - const transformedBody = this.transformRequest(model, body, stream, credentials); - const sessionId = transformedBody.request?.sessionId; - const headers = this.buildHeaders(credentials, stream, sessionId); - - // Initialize retry counters for this URL - if (!retryAttemptsByUrl[urlIndex]) { - retryAttemptsByUrl[urlIndex] = 0; - } - if (!retryAfterAttemptsByUrl[urlIndex]) { - retryAfterAttemptsByUrl[urlIndex] = 0; - } - - try { - const response = await proxyAwareFetch(url, { - method: "POST", - headers, - body: JSON.stringify(transformedBody), - signal - }, proxyOptions); - - if (response.status === HTTP_STATUS.RATE_LIMITED || response.status === HTTP_STATUS.SERVICE_UNAVAILABLE) { - // Try to get retry time from headers first - let retryMs = this.parseRetryHeaders(response.headers); - - // If no retry time in headers, try to parse from error message body - if (!retryMs) { - try { - const errorBody = await response.clone().text(); - const errorJson = JSON.parse(errorBody); - const errorMessage = errorJson?.error?.message || errorJson?.message || ""; - retryMs = this.parseRetryFromErrorMessage(errorMessage); - } catch (e) { - // Ignore parse errors, will fall back to exponential backoff - } - } - - if (retryMs && retryMs <= MAX_RETRY_AFTER_MS && retryAfterAttemptsByUrl[urlIndex] < MAX_RETRY_AFTER_RETRIES) { - retryAfterAttemptsByUrl[urlIndex]++; - log?.debug?.("RETRY", `${response.status} with Retry-After: ${Math.ceil(retryMs / 1000)}s, waiting... (${retryAfterAttemptsByUrl[urlIndex]}/${MAX_RETRY_AFTER_RETRIES})`); - await new Promise(resolve => setTimeout(resolve, retryMs)); - urlIndex--; - continue; - } - - // Auto retry only for 429 when retryMs is 0 or undefined - if (response.status === HTTP_STATUS.RATE_LIMITED && (!retryMs || retryMs === 0) && retryAttemptsByUrl[urlIndex] < MAX_AUTO_RETRIES) { - retryAttemptsByUrl[urlIndex]++; - // Exponential backoff: 2s, 4s, 8s... - const backoffMs = Math.min(1000 * (2 ** retryAttemptsByUrl[urlIndex]), MAX_RETRY_AFTER_MS); - log?.debug?.("RETRY", `429 auto retry ${retryAttemptsByUrl[urlIndex]}/${MAX_AUTO_RETRIES} after ${backoffMs / 1000}s`); - await new Promise(resolve => setTimeout(resolve, backoffMs)); - urlIndex--; - continue; - } - - log?.debug?.("RETRY", `${response.status}, Retry-After ${retryMs ? `too long (${Math.ceil(retryMs / 1000)}s)` : 'missing'}, trying fallback`); - lastStatus = response.status; - - if (urlIndex + 1 < fallbackCount) { - continue; - } - } - - if (this.shouldRetry(response.status, urlIndex)) { - log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`); - lastStatus = response.status; - continue; - } - - return { response, url, headers, transformedBody }; - } catch (error) { - lastError = error; - if (urlIndex + 1 < fallbackCount) { - log?.debug?.("RETRY", `Error on ${url}, trying fallback ${urlIndex + 1}`); - continue; - } - throw error; - } - } - - throw lastError || new Error(`All ${fallbackCount} URLs failed with status ${lastStatus}`); - } -} - -export default AntigravityExecutor; diff --git a/9router/open-sse/executors/base.js b/9router/open-sse/executors/base.js deleted file mode 100644 index 61fb0f6e..00000000 --- a/9router/open-sse/executors/base.js +++ /dev/null @@ -1,130 +0,0 @@ -import { HTTP_STATUS, RETRY_CONFIG } from "../config/runtimeConfig.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; - -/** - * BaseExecutor - Base class for provider executors - */ -export class BaseExecutor { - constructor(provider, config) { - this.provider = provider; - this.config = config; - } - - getProvider() { - return this.provider; - } - - getBaseUrls() { - return this.config.baseUrls || (this.config.baseUrl ? [this.config.baseUrl] : []); - } - - getFallbackCount() { - return this.getBaseUrls().length || 1; - } - - buildUrl(model, stream, urlIndex = 0, credentials = null) { - if (this.provider?.startsWith?.("openai-compatible-")) { - const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.openai.com/v1"; - const normalized = baseUrl.replace(/\/$/, ""); - const path = this.provider.includes("responses") ? "/responses" : "/chat/completions"; - return `${normalized}${path}`; - } - const baseUrls = this.getBaseUrls(); - return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl; - } - - buildHeaders(credentials, stream = true) { - const headers = { - "Content-Type": "application/json", - ...this.config.headers - }; - - if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } else if (credentials.apiKey) { - headers["Authorization"] = `Bearer ${credentials.apiKey}`; - } - - if (stream) { - headers["Accept"] = "text/event-stream"; - } - - return headers; - } - - // Override in subclass for provider-specific transformations - transformRequest(model, body, stream, credentials) { - return body; - } - - shouldRetry(status, urlIndex) { - return status === HTTP_STATUS.RATE_LIMITED && urlIndex + 1 < this.getFallbackCount(); - } - - // Override in subclass for provider-specific refresh - async refreshCredentials(credentials, log) { - return null; - } - - needsRefresh(credentials) { - if (!credentials.expiresAt) return false; - const expiresAtMs = new Date(credentials.expiresAt).getTime(); - return expiresAtMs - Date.now() < 5 * 60 * 1000; - } - - parseError(response, bodyText) { - return { status: response.status, message: bodyText || `HTTP ${response.status}` }; - } - - async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { - const fallbackCount = this.getFallbackCount(); - let lastError = null; - let lastStatus = 0; - const retryAttemptsByUrl = {}; - - for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) { - const url = this.buildUrl(model, stream, urlIndex, credentials); - const transformedBody = this.transformRequest(model, body, stream, credentials); - const headers = this.buildHeaders(credentials, stream); - - if (!retryAttemptsByUrl[urlIndex]) retryAttemptsByUrl[urlIndex] = 0; - - try { - const response = await proxyAwareFetch(url, { - method: "POST", - headers, - body: JSON.stringify(transformedBody), - signal - }, proxyOptions); - - // Retry 429 with fixed delay before falling back to next URL - if (response.status === HTTP_STATUS.RATE_LIMITED && retryAttemptsByUrl[urlIndex] < RETRY_CONFIG.maxAttempts) { - retryAttemptsByUrl[urlIndex]++; - log?.debug?.("RETRY", `429 retry ${retryAttemptsByUrl[urlIndex]}/${RETRY_CONFIG.maxAttempts} after ${RETRY_CONFIG.delayMs / 1000}s`); - await new Promise(resolve => setTimeout(resolve, RETRY_CONFIG.delayMs)); - urlIndex--; - continue; - } - - if (this.shouldRetry(response.status, urlIndex)) { - log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`); - lastStatus = response.status; - continue; - } - - return { response, url, headers, transformedBody }; - } catch (error) { - lastError = error; - if (urlIndex + 1 < fallbackCount) { - log?.debug?.("RETRY", `Error on ${url}, trying fallback ${urlIndex + 1}`); - continue; - } - throw error; - } - } - - throw lastError || new Error(`All ${fallbackCount} URLs failed with status ${lastStatus}`); - } -} - -export default BaseExecutor; diff --git a/9router/open-sse/executors/codex.js b/9router/open-sse/executors/codex.js deleted file mode 100644 index ca062ecb..00000000 --- a/9router/open-sse/executors/codex.js +++ /dev/null @@ -1,108 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js"; -import { PROVIDERS } from "../config/providers.js"; -import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js"; - -/** - * Codex Executor - handles OpenAI Codex API (Responses API format) - * Automatically injects default instructions if missing - */ -export class CodexExecutor extends BaseExecutor { - constructor() { - super("codex", PROVIDERS.codex); - } - - /** - * Override headers to add session_id per request - */ - buildHeaders(credentials, stream = true) { - const headers = super.buildHeaders(credentials, stream); - headers["session_id"] = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; - return headers; - } - - /** - * Transform request before sending - inject default instructions if missing - */ - transformRequest(model, body, stream, credentials) { - // Convert string input to array format (Codex API requires input as array) - const normalized = normalizeResponsesInput(body.input); - if (normalized) body.input = normalized; - - // Ensure input is present and non-empty (Codex API rejects empty input) - if (!body.input || (Array.isArray(body.input) && body.input.length === 0)) { - body.input = [{ type: "message", role: "user", content: [{ type: "input_text", text: "..." }] }]; - } - - // Normalize image content: image_url → input_image (Responses API format) - if (Array.isArray(body.input)) { - for (const item of body.input) { - if (Array.isArray(item.content)) { - item.content = item.content.map(c => { - if (c.type === "image_url") { - const url = typeof c.image_url === "string" ? c.image_url : c.image_url?.url; - return { type: "input_image", image_url: url, detail: c.image_url?.detail || "auto" }; - } - return c; - }); - } - } - } - - // Ensure streaming is enabled (Codex API requires it) - body.stream = true; - - // If no instructions provided, inject default Codex instructions - if (!body.instructions || body.instructions.trim() === "") { - body.instructions = CODEX_DEFAULT_INSTRUCTIONS; - } - - // Ensure store is false (Codex requirement) - body.store = false; - - // Extract thinking level from model name suffix - // e.g., gpt-5.3-codex-high → high, gpt-5.3-codex → medium (default) - const effortLevels = ['none', 'low', 'medium', 'high', 'xhigh']; - let modelEffort = null; - for (const level of effortLevels) { - if (model.endsWith(`-${level}`)) { - modelEffort = level; - // Strip suffix from model name for actual API call - body.model = body.model.replace(`-${level}`, ''); - break; - } - } - - // Priority: explicit reasoning.effort > reasoning_effort param > model suffix > default (medium) - if (!body.reasoning) { - const effort = body.reasoning_effort || modelEffort || 'medium'; - body.reasoning = { effort, summary: "auto" }; - } else if (!body.reasoning.summary) { - body.reasoning.summary = "auto"; - } - delete body.reasoning_effort; - - // Include reasoning encrypted content (required by Codex backend for reasoning models) - if (body.reasoning && body.reasoning.effort && body.reasoning.effort !== 'none') { - body.include = ["reasoning.encrypted_content"]; - } - - // Remove unsupported parameters for Codex API - delete body.temperature; - delete body.top_p; - delete body.frequency_penalty; - delete body.presence_penalty; - delete body.logprobs; - delete body.top_logprobs; - delete body.n; - delete body.seed; - delete body.max_tokens; - delete body.user; // Cursor sends this but Codex doesn't support it - delete body.prompt_cache_retention; // Cursor sends this but Codex doesn't support it - delete body.metadata; // Cursor sends this but Codex doesn't support it - delete body.stream_options; // Cursor sends this but Codex doesn't support it - delete body.safety_identifier; // Droid CLI sends this but Codex doesn't support it - - return body; - } -} diff --git a/9router/open-sse/executors/cursor.js b/9router/open-sse/executors/cursor.js deleted file mode 100644 index 18b9adf6..00000000 --- a/9router/open-sse/executors/cursor.js +++ /dev/null @@ -1,742 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { HTTP_STATUS } from "../config/runtimeConfig.js"; -import { - generateCursorBody, - parseConnectRPCFrame, - extractTextFromResponse -} from "../utils/cursorProtobuf.js"; -import { buildCursorHeaders } from "../utils/cursorChecksum.js"; -import { estimateUsage } from "../utils/usageTracking.js"; -import { FORMATS } from "../translator/formats.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; -import zlib from "zlib"; - -// Detect cloud environment -const isCloudEnv = () => { - if (typeof caches !== "undefined" && typeof caches === "object") return true; - if (typeof EdgeRuntime !== "undefined") return true; - return false; -}; - -// Lazy import http2 (only in Node.js environment) -let http2 = null; -if (!isCloudEnv()) { - try { - http2 = await import("http2"); - } catch { - // http2 not available - } -} - -const COMPRESS_FLAG = { - NONE: 0x00, - GZIP: 0x01, - TRAILER: 0x02, - GZIP_TRAILER: 0x03 -}; - -const CURSOR_STREAM_DEBUG = process.env.CURSOR_STREAM_DEBUG === "1"; -const debugLog = (...args) => { - if (CURSOR_STREAM_DEBUG) console.log(...args); -}; - -function decompressPayload(payload, flags) { - // Check if payload is JSON error (starts with {"error") - if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) { - try { - const text = payload.toString("utf-8"); - if (text.startsWith('{"error"')) { - debugLog(`[DECOMPRESS] Detected JSON error, skipping decompression`); - return payload; - } - } catch {} - } - - if ( - flags === COMPRESS_FLAG.GZIP || - flags === COMPRESS_FLAG.TRAILER || - flags === COMPRESS_FLAG.GZIP_TRAILER - ) { - // Primary: try gzip decompression (standard gzip header 0x1f 0x8b) - try { - return zlib.gunzipSync(payload); - } catch (gzipErr) { - // Fallback: TRAILER and GZIP_TRAILER frames sometimes use raw zlib deflate format - try { - return zlib.inflateSync(payload); - } catch (deflateErr) { - // Last resort: try raw deflate (no zlib header) - try { - return zlib.inflateRawSync(payload); - } catch (rawErr) { - debugLog( - `[DECOMPRESS ERROR] flags=${flags}, payloadSize=${payload.length}, gzip=${gzipErr.message}, deflate=${deflateErr.message}, raw=${rawErr.message}` - ); - debugLog( - `[DECOMPRESS ERROR] First 50 bytes (hex):`, - payload.slice(0, 50).toString("hex") - ); - return payload; - } - } - } - } - return payload; -} - -function createErrorResponse(jsonError) { - const errorMsg = jsonError?.error?.details?.[0]?.debug?.details?.title - || jsonError?.error?.details?.[0]?.debug?.details?.detail - || jsonError?.error?.message - || "API Error"; - - const isRateLimit = jsonError?.error?.code === "resource_exhausted"; - - return new Response(JSON.stringify({ - error: { - message: errorMsg, - type: isRateLimit ? "rate_limit_error" : "api_error", - code: jsonError?.error?.details?.[0]?.debug?.error || "unknown" - } - }), { - status: isRateLimit ? HTTP_STATUS.RATE_LIMITED : HTTP_STATUS.BAD_REQUEST, - headers: { "Content-Type": "application/json" } - }); -} - -export class CursorExecutor extends BaseExecutor { - constructor() { - super("cursor", PROVIDERS.cursor); - } - - buildUrl() { - return `${this.config.baseUrl}${this.config.chatPath}`; - } - - buildHeaders(credentials) { - const accessToken = credentials.accessToken; - const machineId = credentials.providerSpecificData?.machineId; - const ghostMode = credentials.providerSpecificData?.ghostMode !== false; - - if (!machineId) { - throw new Error("Machine ID is required for Cursor API"); - } - - return buildCursorHeaders(accessToken, machineId, ghostMode); - } - - transformRequest(model, body, stream, credentials) { - // Messages are already translated by chatCore (claude→openai→cursor) - // Do NOT call buildCursorRequest again — double-translation drops tool_results - const messages = body.messages || []; - const tools = body.tools || []; - const reasoningEffort = body.reasoning_effort || null; - return generateCursorBody(messages, model, tools, reasoningEffort); - } - - async makeFetchRequest(url, headers, body, signal, proxyOptions = null) { - const response = await proxyAwareFetch(url, { - method: "POST", - headers, - body, - signal - }, proxyOptions); - - return { - status: response.status, - headers: Object.fromEntries(response.headers.entries()), - body: Buffer.from(await response.arrayBuffer()) - }; - } - - makeHttp2Request(url, headers, body, signal) { - if (!http2) { - throw new Error("http2 module not available"); - } - - const HTTP2_TIMEOUT_MS = 60000; // 60s max — prevent hung sessions - - return new Promise((resolve, reject) => { - const urlObj = new URL(url); - const client = http2.connect(`https://${urlObj.host}`); - const chunks = []; - let responseHeaders = {}; - let settled = false; - - // Ensure client is always closed on settle - const finish = (fn) => (...args) => { - if (settled) return; - settled = true; - clearTimeout(hangTimeout); - client.close(); - fn(...args); - }; - - // Hard timeout: close session if server never responds - const hangTimeout = setTimeout(finish(() => { - reject(new Error("HTTP/2 request timed out")); - }), HTTP2_TIMEOUT_MS); - - client.on("error", finish(reject)); - - const req = client.request({ - ":method": "POST", - ":path": urlObj.pathname, - ":authority": urlObj.host, - ":scheme": "https", - ...headers - }); - - req.on("response", (hdrs) => { responseHeaders = hdrs; }); - req.on("data", (chunk) => { chunks.push(chunk); }); - req.on("end", finish(() => { - resolve({ - status: responseHeaders[":status"], - headers: responseHeaders, - body: Buffer.concat(chunks) - }); - })); - req.on("error", finish(reject)); - - if (signal) { - const onAbort = finish(() => reject(new Error("Request aborted"))); - signal.addEventListener("abort", onAbort, { once: true }); - } - - req.write(body); - req.end(); - }); - } - - async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { - const url = this.buildUrl(); - const headers = this.buildHeaders(credentials); - const transformedBody = this.transformRequest(model, body, stream, credentials); - - try { - const shouldForceFetch = proxyOptions?.enabled === true || proxyOptions?.connectionProxyEnabled === true; - const response = (http2 && !shouldForceFetch) - ? await this.makeHttp2Request(url, headers, transformedBody, signal) - : await this.makeFetchRequest(url, headers, transformedBody, signal, proxyOptions); - - if (response.status !== 200) { - const errorText = response.body?.toString() || "Unknown error"; - const errorResponse = new Response(JSON.stringify({ - error: { - message: `[${response.status}]: ${errorText}`, - type: "invalid_request_error", - code: "" - } - }), { - status: response.status, - headers: { "Content-Type": "application/json" } - }); - return { response: errorResponse, url, headers, transformedBody: body }; - } - - const transformedResponse = stream !== false - ? this.transformProtobufToSSE(response.body, model, body) - : this.transformProtobufToJSON(response.body, model, body); - - return { response: transformedResponse, url, headers, transformedBody: body }; - } catch (error) { - const errorResponse = new Response(JSON.stringify({ - error: { - message: error.message, - type: "connection_error", - code: "" - } - }), { - status: HTTP_STATUS.SERVER_ERROR, - headers: { "Content-Type": "application/json" } - }); - return { response: errorResponse, url, headers, transformedBody: body }; - } - } - - transformProtobufToJSON(buffer, model, body) { - const responseId = `chatcmpl-cursor-${Date.now()}`; - const created = Math.floor(Date.now() / 1000); - - let offset = 0; - let totalContent = ""; - const toolCalls = []; - const toolCallsMap = new Map(); // Track streaming tool calls by ID - const finalizedIds = new Set(); - let frameCount = 0; - - debugLog(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`); - - while (offset < buffer.length) { - if (offset + 5 > buffer.length) { - debugLog( - `[CURSOR BUFFER] Reached end, offset=${offset}, remaining=${buffer.length - offset}` - ); - break; - } - - const flags = buffer[offset]; - const length = buffer.readUInt32BE(offset + 1); - - debugLog( - `[CURSOR BUFFER] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}` - ); - - if (offset + 5 + length > buffer.length) { - debugLog( - `[CURSOR BUFFER] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}` - ); - break; - } - - let payload = buffer.slice(offset + 5, offset + 5 + length); - offset += 5 + length; - frameCount++; - - payload = decompressPayload(payload, flags); - if (!payload) { - debugLog(`[CURSOR BUFFER] Frame ${frameCount}: decompression failed, skipping`); - continue; - } - - // Check for JSON error frames (byte guard: skip toString on non-JSON frames) - if (payload.length > 0 && payload[0] === 0x7b) { - try { - const text = payload.toString("utf-8"); - if (text.includes('"error"')) { - const hasContent = totalContent || toolCallsMap.size > 0; - debugLog( - `[CURSOR BUFFER] Error frame (hasContent=${hasContent}): ${text.slice(0, 500)}` - ); - if (hasContent) { - break; - } - return createErrorResponse(JSON.parse(text)); - } - } catch {} - } - - const result = extractTextFromResponse(new Uint8Array(payload)); - debugLog(`[CURSOR DECODED] Frame ${frameCount}:`, result); - - if (result.error) { - const hasContent = totalContent || toolCallsMap.size > 0; - debugLog(`[CURSOR BUFFER] Decoded error (hasContent=${hasContent}): ${result.error}`); - if (hasContent) { - break; - } - return new Response( - JSON.stringify({ - error: { - message: result.error, - type: "rate_limit_error", - code: "rate_limited" - } - }), - { - status: HTTP_STATUS.RATE_LIMITED, - headers: { "Content-Type": "application/json" } - } - ); - } - - if (result.toolCall) { - const tc = result.toolCall; - - if (toolCallsMap.has(tc.id)) { - // Accumulate arguments for existing tool call - const existing = toolCallsMap.get(tc.id); - existing.function.arguments += tc.function.arguments; - existing.isLast = tc.isLast; - } else { - // New tool call - toolCallsMap.set(tc.id, { ...tc }); - } - - // Push to final array when isLast is true - if (tc.isLast) { - const finalToolCall = toolCallsMap.get(tc.id); - finalizedIds.add(tc.id); - toolCalls.push({ - id: finalToolCall.id, - type: finalToolCall.type, - function: { - name: finalToolCall.function.name, - arguments: finalToolCall.function.arguments - } - }); - } - } - - if (result.text) totalContent += result.text; - } - - debugLog( - `[CURSOR BUFFER] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, finalized toolCalls: ${toolCalls.length}` - ); - - // Finalize all remaining tool calls in map (in case stream ended without isLast=true) - for (const [id, tc] of toolCallsMap.entries()) { - // Check if already in final array - if (!finalizedIds.has(id)) { - debugLog(`[CURSOR BUFFER] Finalizing incomplete tool call: ${id}, isLast=${tc.isLast}`); - toolCalls.push({ - id: tc.id, - type: tc.type, - function: { - name: tc.function.name, - arguments: tc.function.arguments - } - }); - } - } - - debugLog(`[CURSOR BUFFER] Final toolCalls count: ${toolCalls.length}`); - - - const message = { - role: "assistant", - content: totalContent || null - }; - - if (toolCalls.length > 0) { - message.tool_calls = toolCalls; - } - - const usage = estimateUsage(body, totalContent.length, FORMATS.OPENAI); - - const completion = { - id: responseId, - object: "chat.completion", - created, - model, - choices: [{ - index: 0, - message, - finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop" - }], - usage - }; - - return new Response(JSON.stringify(completion), { - status: 200, - headers: { "Content-Type": "application/json" } - }); - } - - transformProtobufToSSE(buffer, model, body) { - const responseId = `chatcmpl-cursor-${Date.now()}`; - const created = Math.floor(Date.now() / 1000); - - const chunks = []; - let offset = 0; - let totalContent = ""; - const toolCalls = []; - const toolCallsMap = new Map(); // Track streaming tool calls by ID - const finalizedIds = new Set(); - const emittedToolCallIds = new Set(); - let frameCount = 0; - - debugLog(`[CURSOR BUFFER SSE] Total length: ${buffer.length} bytes`); - - while (offset < buffer.length) { - if (offset + 5 > buffer.length) { - debugLog( - `[CURSOR BUFFER SSE] Reached end, offset=${offset}, remaining=${buffer.length - offset}` - ); - break; - } - - const flags = buffer[offset]; - const length = buffer.readUInt32BE(offset + 1); - - debugLog( - `[CURSOR BUFFER SSE] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}` - ); - - if (offset + 5 + length > buffer.length) { - debugLog( - `[CURSOR BUFFER SSE] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}` - ); - break; - } - - let payload = buffer.slice(offset + 5, offset + 5 + length); - offset += 5 + length; - frameCount++; - - payload = decompressPayload(payload, flags); - if (!payload) { - debugLog(`[CURSOR BUFFER SSE] Frame ${frameCount}: decompression failed, skipping`); - continue; - } - - // Check for JSON error frames (byte-guard: only decode if starts with '{') - if (payload[0] === 0x7b) { - try { - const text = payload.toString("utf-8"); - if (text.includes('"error"')) { - const hasContent = chunks.length > 0 || totalContent || toolCallsMap.size > 0; - debugLog( - `[CURSOR BUFFER SSE] Error frame (hasContent=${hasContent}): ${text.slice(0, 500)}` - ); - if (hasContent) { - break; - } - return createErrorResponse(JSON.parse(text)); - } - } catch {} - } - - const result = extractTextFromResponse(new Uint8Array(payload)); - debugLog(`[CURSOR DECODED SSE] Frame ${frameCount}:`, result); - - if (result.error) { - const hasContent = chunks.length > 0 || totalContent || toolCallsMap.size > 0; - debugLog(`[CURSOR BUFFER SSE] Decoded error (hasContent=${hasContent}): ${result.error}`); - if (hasContent) { - break; - } - return new Response( - JSON.stringify({ - error: { - message: result.error, - type: "rate_limit_error", - code: "rate_limited" - } - }), - { - status: HTTP_STATUS.RATE_LIMITED, - headers: { "Content-Type": "application/json" } - } - ); - } - - if (result.toolCall) { - const tc = result.toolCall; - - if (chunks.length === 0) { - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { role: "assistant", content: "" }, - finish_reason: null - } - ] - })}\n\n` - ); - } - - if (toolCallsMap.has(tc.id)) { - // Accumulate arguments for existing tool call - const existing = toolCallsMap.get(tc.id); - const oldArgsLen = existing.function.arguments.length; - existing.function.arguments += tc.function.arguments; - existing.isLast = tc.isLast; - - // Stream the delta arguments - if (tc.function.arguments) { - emittedToolCallIds.add(tc.id); - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: existing.index, - id: tc.id, - type: "function", - function: { - name: tc.function.name, - arguments: tc.function.arguments - } - } - ] - }, - finish_reason: null - } - ] - })}\n\n` - ); - } - } else { - // New tool call - assign index and add to map - const toolCallIndex = toolCalls.length; - finalizedIds.add(tc.id); - toolCalls.push({ ...tc, index: toolCallIndex }); - toolCallsMap.set(tc.id, { ...tc, index: toolCallIndex }); - - // Stream initial tool call with name - emittedToolCallIds.add(tc.id); - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: toolCallIndex, - id: tc.id, - type: "function", - function: { - name: tc.function.name, - arguments: tc.function.arguments - } - } - ] - }, - finish_reason: null - } - ] - })}\n\n` - ); - } - } - - if (result.text) { - totalContent += result.text; - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: - chunks.length === 0 && toolCalls.length === 0 - ? { role: "assistant", content: result.text } - : { content: result.text }, - finish_reason: null - } - ] - })}\n\n` - ); - } - } - - debugLog( - `[CURSOR BUFFER SSE] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, toolCalls array: ${toolCalls.length}` - ); - - // Finalize all remaining tool calls in map (stream may have ended without isLast=true) - for (const [id, tc] of toolCallsMap.entries()) { - if (!finalizedIds.has(id)) { - debugLog(`[CURSOR BUFFER SSE] Finalizing incomplete tool call: ${id}, isLast=${tc.isLast}`); - const toolCallIndex = toolCalls.length; - toolCalls.push({ - id: tc.id, - type: tc.type, - index: toolCallIndex, - function: { - name: tc.function.name, - arguments: tc.function.arguments - } - }); - - // Emit SSE chunk for the finalized tool call if not already emitted - if (!emittedToolCallIds.has(tc.id)) { - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: toolCallIndex, - id: tc.id, - type: "function", - function: { - name: tc.function.name, - arguments: tc.function.arguments - } - } - ] - }, - finish_reason: null - } - ] - })}\n\n` - ); - } - } - } - - if (chunks.length === 0 && toolCalls.length === 0) { - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { role: "assistant", content: "" }, - finish_reason: null - } - ] - })}\n\n` - ); - } - - const usage = estimateUsage(body, totalContent.length, FORMATS.OPENAI); - - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: {}, - finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop" - } - ], - usage - })}\n\n` - ); - chunks.push("data: [DONE]\n\n"); - - return new Response(chunks.join(""), { - status: 200, - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive" - } - }); - } - - async refreshCredentials() { - return null; - } -} - -export default CursorExecutor; diff --git a/9router/open-sse/executors/default.js b/9router/open-sse/executors/default.js deleted file mode 100644 index eb3e19d0..00000000 --- a/9router/open-sse/executors/default.js +++ /dev/null @@ -1,213 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js"; -import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js"; - -export class DefaultExecutor extends BaseExecutor { - constructor(provider) { - super(provider, PROVIDERS[provider] || PROVIDERS.openai); - } - - buildUrl(model, stream, urlIndex = 0, credentials = null) { - if (this.provider?.startsWith?.("openai-compatible-")) { - const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.openai.com/v1"; - const normalized = baseUrl.replace(/\/$/, ""); - const path = this.provider.includes("responses") ? "/responses" : "/chat/completions"; - return `${normalized}${path}`; - } - if (this.provider?.startsWith?.("anthropic-compatible-")) { - const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.anthropic.com/v1"; - const normalized = baseUrl.replace(/\/$/, ""); - return `${normalized}/messages`; - } - switch (this.provider) { - case "claude": - case "glm": - case "kimi": - case "minimax": - case "minimax-cn": - return `${this.config.baseUrl}?beta=true`; - case "kimi-coding": - return `${this.config.baseUrl}?beta=true`; - case "gemini": - return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`; - default: - return this.config.baseUrl; - } - } - - buildHeaders(credentials, stream = true) { - const headers = { "Content-Type": "application/json", ...this.config.headers }; - - switch (this.provider) { - case "gemini": - credentials.apiKey ? headers["x-goog-api-key"] = credentials.apiKey : headers["Authorization"] = `Bearer ${credentials.accessToken}`; - break; - case "claude": - credentials.apiKey ? headers["x-api-key"] = credentials.apiKey : headers["Authorization"] = `Bearer ${credentials.accessToken}`; - break; - case "glm": - case "kimi": - case "minimax": - case "minimax-cn": - headers["x-api-key"] = credentials.apiKey || credentials.accessToken; - break; - case "kimi-coding": - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - Object.assign(headers, buildKimiHeaders()); - break; - default: - if (this.provider?.startsWith?.("anthropic-compatible-")) { - if (credentials.apiKey) { - headers["x-api-key"] = credentials.apiKey; - } else if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - if (!headers["anthropic-version"]) { - headers["anthropic-version"] = "2023-06-01"; - } - } else if (this.provider === "kilocode") { - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - if (credentials.providerSpecificData?.orgId) { - headers["X-Kilocode-OrganizationID"] = credentials.providerSpecificData.orgId; - } - } else if (this.provider === "cline") { - Object.assign(headers, buildClineHeaders(credentials.apiKey || credentials.accessToken)); - } else { - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - } - } - - if (stream) headers["Accept"] = "text/event-stream"; - return headers; - } - - async refreshCredentials(credentials, log) { - if (!credentials.refreshToken) return null; - - const refreshers = { - claude: () => this.refreshWithJSON(OAUTH_ENDPOINTS.anthropic.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.claude.clientId }), - codex: () => this.refreshWithForm(OAUTH_ENDPOINTS.openai.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.codex.clientId, scope: "openid profile email offline_access" }), - qwen: () => this.refreshWithForm(OAUTH_ENDPOINTS.qwen.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.qwen.clientId }), - iflow: () => this.refreshIflow(credentials.refreshToken), - gemini: () => this.refreshGoogle(credentials.refreshToken), - kiro: () => this.refreshKiro(credentials.refreshToken), - cline: () => this.refreshCline(credentials.refreshToken), - "kimi-coding": () => this.refreshKimiCoding(credentials.refreshToken), - kilocode: () => this.refreshKilocode(credentials.refreshToken) - }; - - const refresher = refreshers[this.provider]; - if (!refresher) return null; - - try { - const result = await refresher(); - if (result) log?.info?.("TOKEN", `${this.provider} refreshed`); - return result; - } catch (error) { - log?.error?.("TOKEN", `${this.provider} refresh error: ${error.message}`); - return null; - } - } - - async refreshWithJSON(url, body) { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json", "Accept": "application/json" }, - body: JSON.stringify(body) - }); - if (!response.ok) return null; - const tokens = await response.json(); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || body.refresh_token, expiresIn: tokens.expires_in }; - } - - async refreshWithForm(url, params) { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, - body: new URLSearchParams(params) - }); - if (!response.ok) return null; - const tokens = await response.json(); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || params.refresh_token, expiresIn: tokens.expires_in }; - } - - async refreshIflow(refreshToken) { - const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`); - const response = await fetch(OAUTH_ENDPOINTS.iflow.token, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "Authorization": `Basic ${basicAuth}` }, - body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.iflow.clientId, client_secret: PROVIDERS.iflow.clientSecret }) - }); - if (!response.ok) return null; - const tokens = await response.json(); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } - - async refreshGoogle(refreshToken) { - const response = await fetch(OAUTH_ENDPOINTS.google.token, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, - body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: this.config.clientId, client_secret: this.config.clientSecret }) - }); - if (!response.ok) return null; - const tokens = await response.json(); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } - - async refreshKiro(refreshToken) { - const response = await fetch(PROVIDERS.kiro.tokenUrl, { - method: "POST", - headers: { "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "kiro-cli/1.0.0" }, - body: JSON.stringify({ refreshToken }) - }); - if (!response.ok) return null; - const tokens = await response.json(); - return { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken || refreshToken, expiresIn: tokens.expiresIn }; - } - - async refreshCline(refreshToken) { - console.log('[DEBUG] Refreshing Cline token, refreshToken length:', refreshToken?.length); - const response = await fetch("https://api.cline.bot/api/v1/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json", "Accept": "application/json" }, - body: JSON.stringify({ refreshToken, grantType: "refresh_token", clientType: "extension" }) - }); - console.log('[DEBUG] Cline refresh response status:', response.status); - if (!response.ok) { - const errorText = await response.text(); - console.log('[DEBUG] Cline refresh error:', errorText); - return null; - } - const payload = await response.json(); - console.log('[DEBUG] Cline refresh payload:', JSON.stringify(payload).substring(0, 200)); - const data = payload?.data || payload; - const expiresAtIso = data?.expiresAt; - const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined; - console.log('[DEBUG] Cline refresh success, expiresIn:', expiresIn); - return { accessToken: data?.accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn }; - } - - async refreshKimiCoding(refreshToken) { - const kimiHeaders = buildKimiHeaders(); - const response = await fetch("https://auth.kimi.com/api/oauth/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", - ...kimiHeaders - }, - body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: "17e5f671-d194-4dfb-9706-5516cb48c098" }) - }); - if (!response.ok) return null; - const tokens = await response.json(); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } - - async refreshKilocode(refreshToken) { - // Kilocode uses device code flow, no refresh token support - return null; - } -} - -export default DefaultExecutor; diff --git a/9router/open-sse/executors/gemini-cli.js b/9router/open-sse/executors/gemini-cli.js deleted file mode 100644 index 45470c87..00000000 --- a/9router/open-sse/executors/gemini-cli.js +++ /dev/null @@ -1,67 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { OAUTH_ENDPOINTS, GEMINI_CLI_API_CLIENT, geminiCLIUserAgent } from "../config/appConstants.js"; - -export class GeminiCLIExecutor extends BaseExecutor { - constructor() { - super("gemini-cli", PROVIDERS["gemini-cli"]); - } - - buildUrl(model, stream, urlIndex = 0) { - const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; - return `${this.config.baseUrl}:${action}`; - } - - buildHeaders(credentials, stream = true) { - return { - "Content-Type": "application/json", - "Authorization": `Bearer ${credentials.accessToken}`, - "User-Agent": geminiCLIUserAgent(this._currentModel), - "X-Goog-Api-Client": GEMINI_CLI_API_CLIENT, - "Accept": stream ? "text/event-stream" : "application/json" - }; - } - - transformRequest(model, body, stream, credentials) { - // Store model for use in buildHeaders (called by base.execute after transformRequest) - this._currentModel = model; - if (!body.project && credentials?.projectId) { - body.project = credentials.projectId; - } - return body; - } - - async refreshCredentials(credentials, log) { - if (!credentials.refreshToken) return null; - - try { - const response = await fetch(OAUTH_ENDPOINTS.google.token, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: credentials.refreshToken, - client_id: this.config.clientId, - client_secret: this.config.clientSecret - }) - }); - - if (!response.ok) return null; - - const tokens = await response.json(); - log?.info?.("TOKEN", "Gemini CLI refreshed"); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || credentials.refreshToken, - expiresIn: tokens.expires_in, - projectId: credentials.projectId - }; - } catch (error) { - log?.error?.("TOKEN", `Gemini CLI refresh error: ${error.message}`); - return null; - } - } -} - -export default GeminiCLIExecutor; diff --git a/9router/open-sse/executors/github.js b/9router/open-sse/executors/github.js deleted file mode 100644 index b174007a..00000000 --- a/9router/open-sse/executors/github.js +++ /dev/null @@ -1,305 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../config/appConstants.js"; -import { HTTP_STATUS } from "../config/runtimeConfig.js"; -import { openaiToOpenAIResponsesRequest } from "../translator/request/openai-responses.js"; -import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-responses.js"; -import { initState } from "../translator/index.js"; -import { parseSSELine, formatSSE } from "../utils/streamHelpers.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; -import crypto from "crypto"; - -export class GithubExecutor extends BaseExecutor { - constructor() { - super("github", PROVIDERS.github); - this.knownCodexModels = new Set(); - } - - buildUrl(model, stream, urlIndex = 0) { - return this.config.baseUrl; - } - - buildHeaders(credentials, stream = true) { - const token = credentials.copilotToken || credentials.accessToken; - return { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json", - "copilot-integration-id": "vscode-chat", - "editor-version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`, - "editor-plugin-version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`, - "user-agent": GITHUB_COPILOT.USER_AGENT, - "openai-intent": "conversation-panel", - "x-github-api-version": GITHUB_COPILOT.API_VERSION, - "x-request-id": crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`, - "x-vscode-user-agent-library-version": "electron-fetch", - "X-Initiator": "user", - "Accept": stream ? "text/event-stream" : "application/json" - }; - } - - // Sanitize messages for GitHub Copilot /chat/completions endpoint. - // The endpoint only accepts 'text' and 'image_url' content part types. - // Tool-related content (tool_use, tool_result, thinking) must be serialized as text. - sanitizeMessagesForChatCompletions(body) { - if (!body?.messages) return body; - - const sanitized = { ...body }; - - // Handle response_format for Claude models via GitHub - // GitHub's internal translation doesn't respect response_format, so we inject it as a system prompt - // AND prepend a reminder to the last user message for maximum effectiveness - if (body.response_format && body.model?.includes('claude')) { - const responseFormat = body.response_format; - let systemInstruction = ''; - if (responseFormat.type === 'json_schema' && responseFormat.json_schema?.schema) { - systemInstruction = 'CRITICAL: You must ONLY output raw JSON. Never use markdown code blocks. Never use backticks. Never wrap JSON in triple backticks. Output ONLY the raw JSON object.'; - } else if (responseFormat.type === 'json_object') { - systemInstruction = 'CRITICAL: You must ONLY output raw JSON. Never use markdown code blocks. Never use backticks.'; - } - if (systemInstruction) { - // Add to system message - const systemIdx = body.messages.findIndex(m => m.role === 'system'); - if (systemIdx >= 0) { - body.messages[systemIdx].content = systemInstruction + '\n\n' + body.messages[systemIdx].content; - } else { - body.messages.unshift({ role: 'system', content: systemInstruction }); - } - - // Also prepend to the last user message as a reminder - const lastUserIdx = body.messages.map((m, i) => m.role === 'user' ? i : -1).filter(i => i >= 0).pop(); - if (lastUserIdx >= 0) { - const userMsg = body.messages[lastUserIdx]; - const userContent = typeof userMsg.content === 'string' ? userMsg.content : JSON.stringify(userMsg.content); - userMsg.content = 'Respond with ONLY raw JSON (no markdown, no backticks, no code blocks): ' + userContent; - } - } - } - sanitized.messages = body.messages.map(msg => { - // assistant messages with only tool_calls have content: null — leave as-is - if (!msg.content) return msg; - - // String content is always fine - if (typeof msg.content === "string") return msg; - - // Array content: filter/convert unsupported part types - if (Array.isArray(msg.content)) { - const cleanContent = msg.content - .map(part => { - if (part.type === "text") return part; - if (part.type === "image_url") return part; - // Serialize tool_use, tool_result, thinking, etc. as text - const text = part.text || part.content || JSON.stringify(part); - return { type: "text", text: typeof text === "string" ? text : JSON.stringify(text) }; - }) - .filter(part => part.text !== ""); // remove empty text parts - - // If all content was stripped (e.g. only tool_result with no text), drop content - return { ...msg, content: cleanContent.length > 0 ? cleanContent : null }; - } - - return msg; - }); - - return sanitized; - } - - async execute(options) { - const { model, log } = options; - - // Only use /responses for models that are explicitly known to need it (e.g. gpt codex models) - if (this.knownCodexModels.has(model)) { - log?.debug("GITHUB", `Using cached /responses route for ${model}`); - return this.executeWithResponsesEndpoint(options); - } - - // Sanitize messages before sending to /chat/completions - // This handles Claude models on GitHub Copilot which reject non-text/image_url content types - const sanitizedOptions = { - ...options, - body: this.sanitizeMessagesForChatCompletions(options.body) - }; - - const result = await super.execute({ ...sanitizedOptions, proxyOptions: options.proxyOptions || null }); - - if (result.response.status === HTTP_STATUS.BAD_REQUEST) { - const errorBody = await result.response.clone().text(); - - if (errorBody.includes("not accessible via the /chat/completions endpoint")) { - log?.warn("GITHUB", `Model ${model} requires /responses. Switching...`); - this.knownCodexModels.add(model); - return this.executeWithResponsesEndpoint(options); - } - } - - return result; - } - - async executeWithResponsesEndpoint({ model, body, stream, credentials, signal, log, proxyOptions = null }) { - const url = this.config.responsesUrl; - const headers = this.buildHeaders(credentials, stream); - - const transformedBody = openaiToOpenAIResponsesRequest(model, body, stream, credentials); - - log?.debug("GITHUB", "Sending translated request to /responses"); - - const response = await proxyAwareFetch(url, { - method: "POST", - headers, - body: JSON.stringify(transformedBody), - signal - }, proxyOptions); - - if (!response.ok) { - return { response, url, headers, transformedBody }; - } - - const state = initState("openai-responses"); - state.model = model; - - const decoder = new TextDecoder(); - let buffer = ""; - - const transformStream = new TransformStream({ - async transform(chunk, controller) { - buffer += decoder.decode(chunk, { stream: true }); - const lines = buffer.split("\n"); - - buffer = lines.pop() || ""; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - - const parsed = parseSSELine(trimmed); - if (!parsed) continue; - - if (parsed.done && stream === true) { - controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")); - continue; - } - - const converted = openaiResponsesToOpenAIResponse(parsed, state); - if (converted) { - const sseString = formatSSE(converted, "openai"); - controller.enqueue(new TextEncoder().encode(sseString)); - } - } - }, - flush(controller) { - if (buffer.trim()) { - const parsed = parseSSELine(buffer.trim()); - if (parsed && !parsed.done) { - const converted = openaiResponsesToOpenAIResponse(parsed, state); - if (converted) { - controller.enqueue(new TextEncoder().encode(formatSSE(converted, "openai"))); - } - } - } - } - }); - - if (!response.body) { - return { response: new Response("", { status: response.status, headers: response.headers }), url, headers, transformedBody }; - } - const convertedStream = response.body.pipeThrough(transformStream); - - return { - response: new Response(convertedStream, { - status: response.status, - statusText: response.statusText, - headers: response.headers - }), - url, - headers, - transformedBody - }; - } - - async refreshCopilotToken(githubAccessToken, log) { - try { - const response = await fetch("https://api.github.com/copilot_internal/v2/token", { - headers: { - "Authorization": `token ${githubAccessToken}`, - "User-Agent": GITHUB_COPILOT.USER_AGENT, - "Editor-Version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`, - "Editor-Plugin-Version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`, - "Accept": "application/json", - "x-github-api-version": GITHUB_COPILOT.API_VERSION - } - }); - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN", `Copilot token refresh failed: ${response.status} ${errorText}`); - return null; - } - const data = await response.json(); - log?.info?.("TOKEN", "Copilot token refreshed"); - return { token: data.token, expiresAt: data.expires_at }; - } catch (error) { - log?.error?.("TOKEN", `Copilot refresh error: ${error.message}`); - return null; - } - } - - async refreshGitHubToken(refreshToken, log) { - try { - const response = await fetch(OAUTH_ENDPOINTS.github.token, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: this.config.clientId, - client_secret: this.config.clientSecret - }) - }); - if (!response.ok) return null; - const tokens = await response.json(); - log?.info?.("TOKEN", "GitHub token refreshed"); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } catch (error) { - log?.error?.("TOKEN", `GitHub refresh error: ${error.message}`); - return null; - } - } - - async refreshCredentials(credentials, log) { - let copilotResult = await this.refreshCopilotToken(credentials.accessToken, log); - - if (!copilotResult && credentials.refreshToken) { - const githubTokens = await this.refreshGitHubToken(credentials.refreshToken, log); - if (githubTokens?.accessToken) { - copilotResult = await this.refreshCopilotToken(githubTokens.accessToken, log); - if (copilotResult) { - return { ...githubTokens, copilotToken: copilotResult.token, copilotTokenExpiresAt: copilotResult.expiresAt }; - } - return githubTokens; - } - } - - if (copilotResult) { - return { accessToken: credentials.accessToken, refreshToken: credentials.refreshToken, copilotToken: copilotResult.token, copilotTokenExpiresAt: copilotResult.expiresAt }; - } - - return null; - } - - needsRefresh(credentials) { - // Always refresh if no copilotToken - if (!credentials.copilotToken) return true; - - if (credentials.copilotTokenExpiresAt) { - // Handle both Unix timestamp (seconds) and ISO string - let expiresAtMs = credentials.copilotTokenExpiresAt; - if (typeof expiresAtMs === "number" && expiresAtMs < 1e12) { - expiresAtMs = expiresAtMs * 1000; // Convert seconds to ms - } else if (typeof expiresAtMs === "string") { - expiresAtMs = new Date(expiresAtMs).getTime(); - } - if (expiresAtMs - Date.now() < 5 * 60 * 1000) return true; - } - return super.needsRefresh(credentials); - } -} - -export default GithubExecutor; diff --git a/9router/open-sse/executors/iflow.js b/9router/open-sse/executors/iflow.js deleted file mode 100644 index 701c8c9a..00000000 --- a/9router/open-sse/executors/iflow.js +++ /dev/null @@ -1,104 +0,0 @@ -import crypto from "crypto"; -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; - -/** - * IFlowExecutor - Executor for iFlow API with HMAC-SHA256 signature - */ -export class IFlowExecutor extends BaseExecutor { - constructor() { - super("iflow", PROVIDERS.iflow); - } - - /** - * Generate UUID v4 - * @returns {string} UUID v4 string - */ - generateUUID() { - return crypto.randomUUID(); - } - - /** - * Create iFlow signature using HMAC-SHA256 - * @param {string} userAgent - User agent string - * @param {string} sessionID - Session ID - * @param {number} timestamp - Unix timestamp in milliseconds - * @param {string} apiKey - API key for signing - * @returns {string} Hex-encoded signature - */ - createIFlowSignature(userAgent, sessionID, timestamp, apiKey) { - if (!apiKey) return ""; - const payload = `${userAgent}:${sessionID}:${timestamp}`; - const hmac = crypto.createHmac("sha256", apiKey); - hmac.update(payload); - return hmac.digest("hex"); - } - - /** - * Build headers with iFlow-specific signature - * @param {object} credentials - Provider credentials - * @param {boolean} stream - Whether streaming is enabled - * @returns {object} Headers object - */ - buildHeaders(credentials, stream = true) { - // Generate session ID and timestamp - const sessionID = `session-${this.generateUUID()}`; - const timestamp = Date.now(); - - // Get user agent from config - const userAgent = this.config.headers["User-Agent"] || "iFlow-Cli"; - - // Get API key (prefer apiKey, fallback to accessToken) - const apiKey = credentials.apiKey || credentials.accessToken || ""; - - // Create signature - const signature = this.createIFlowSignature(userAgent, sessionID, timestamp, apiKey); - - // Build headers - const headers = { - "Content-Type": "application/json", - ...this.config.headers, - "session-id": sessionID, - "x-iflow-timestamp": timestamp.toString(), - "x-iflow-signature": signature - }; - - // Add authorization - if (credentials.apiKey) { - headers["Authorization"] = `Bearer ${credentials.apiKey}`; - } - - // Add streaming header - if (stream) { - headers["Accept"] = "text/event-stream"; - } - - return headers; - } - - /** - * Build URL for iFlow API - * @param {string} model - Model name - * @param {boolean} stream - Whether streaming is enabled - * @param {number} urlIndex - URL index for fallback - * @param {object} credentials - Provider credentials - * @returns {string} API URL - */ - buildUrl(model, stream, urlIndex = 0, credentials = null) { - return this.config.baseUrl; - } - - /** - * Transform request body (passthrough for iFlow) - * @param {string} model - Model name - * @param {object} body - Request body - * @param {boolean} stream - Whether streaming is enabled - * @param {object} credentials - Provider credentials - * @returns {object} Transformed body - */ - transformRequest(model, body, stream, credentials) { - return body; - } -} - -export default IFlowExecutor; diff --git a/9router/open-sse/executors/index.js b/9router/open-sse/executors/index.js deleted file mode 100644 index ac76eb77..00000000 --- a/9router/open-sse/executors/index.js +++ /dev/null @@ -1,45 +0,0 @@ -import { AntigravityExecutor } from "./antigravity.js"; -import { GeminiCLIExecutor } from "./gemini-cli.js"; -import { GithubExecutor } from "./github.js"; -import { IFlowExecutor } from "./iflow.js"; -import { KiroExecutor } from "./kiro.js"; -import { CodexExecutor } from "./codex.js"; -import { CursorExecutor } from "./cursor.js"; -import { VertexExecutor } from "./vertex.js"; -import { DefaultExecutor } from "./default.js"; - -const executors = { - antigravity: new AntigravityExecutor(), - "gemini-cli": new GeminiCLIExecutor(), - github: new GithubExecutor(), - iflow: new IFlowExecutor(), - kiro: new KiroExecutor(), - codex: new CodexExecutor(), - cursor: new CursorExecutor(), - cu: new CursorExecutor(), // Alias for cursor - vertex: new VertexExecutor("vertex"), - "vertex-partner": new VertexExecutor("vertex-partner"), -}; - -const defaultCache = new Map(); - -export function getExecutor(provider) { - if (executors[provider]) return executors[provider]; - if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider)); - return defaultCache.get(provider); -} - -export function hasSpecializedExecutor(provider) { - return !!executors[provider]; -} - -export { BaseExecutor } from "./base.js"; -export { AntigravityExecutor } from "./antigravity.js"; -export { GeminiCLIExecutor } from "./gemini-cli.js"; -export { GithubExecutor } from "./github.js"; -export { IFlowExecutor } from "./iflow.js"; -export { KiroExecutor } from "./kiro.js"; -export { CodexExecutor } from "./codex.js"; -export { CursorExecutor } from "./cursor.js"; -export { VertexExecutor } from "./vertex.js"; -export { DefaultExecutor } from "./default.js"; diff --git a/9router/open-sse/executors/kiro.js b/9router/open-sse/executors/kiro.js deleted file mode 100644 index cc9ebdcc..00000000 --- a/9router/open-sse/executors/kiro.js +++ /dev/null @@ -1,448 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { v4 as uuidv4 } from "uuid"; -import { refreshKiroToken } from "../services/tokenRefresh.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; - -/** - * KiroExecutor - Executor for Kiro AI (AWS CodeWhisperer) - * Uses AWS CodeWhisperer streaming API with AWS EventStream binary format - */ -export class KiroExecutor extends BaseExecutor { - constructor() { - super("kiro", PROVIDERS.kiro); - } - - buildHeaders(credentials, stream = true) { - const headers = { - ...this.config.headers, - "Amz-Sdk-Request": "attempt=1; max=3", - "Amz-Sdk-Invocation-Id": uuidv4() - }; - - if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - - return headers; - } - - transformRequest(model, body, stream, credentials) { - return body; - } - - /** - * Custom execute for Kiro - handles AWS EventStream binary response - */ - async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { - const url = this.buildUrl(model, stream, 0); - const headers = this.buildHeaders(credentials, stream); - const transformedBody = this.transformRequest(model, body, stream, credentials); - - const response = await proxyAwareFetch(url, { - method: "POST", - headers, - body: JSON.stringify(transformedBody), - signal - }, proxyOptions); - - if (!response.ok) { - return { response, url, headers, transformedBody }; - } - - // For Kiro, we need to transform the binary EventStream to SSE - // Create a TransformStream to convert binary to SSE text - const transformedResponse = this.transformEventStreamToSSE(response, model); - - return { response: transformedResponse, url, headers, transformedBody }; - } - - /** - * Transform AWS EventStream binary response to SSE text stream - * Using TransformStream instead of ReadableStream.pull() to avoid Workers timeout - */ - transformEventStreamToSSE(response, model) { - let buffer = new Uint8Array(0); - let chunkIndex = 0; - const responseId = `chatcmpl-${Date.now()}`; - const created = Math.floor(Date.now() / 1000); - const state = { - endDetected: false, - finishEmitted: false, - hasToolCalls: false, - toolCallIndex: 0, - seenToolIds: new Map() - }; - - const transformStream = new TransformStream({ - async transform(chunk, controller) { - // Append to buffer - const newBuffer = new Uint8Array(buffer.length + chunk.length); - newBuffer.set(buffer); - newBuffer.set(chunk, buffer.length); - buffer = newBuffer; - - // Parse events from buffer - let iterations = 0; - const maxIterations = 1000; - while (buffer.length >= 16 && iterations < maxIterations) { - iterations++; - const view = new DataView(buffer.buffer, buffer.byteOffset); - const totalLength = view.getUint32(0, false); - - if (totalLength < 16 || totalLength > buffer.length || buffer.length < totalLength) break; - - const eventData = buffer.slice(0, totalLength); - buffer = buffer.slice(totalLength); - - const event = parseEventFrame(eventData); - if (!event) continue; - - const eventType = event.headers[":event-type"] || ""; - - // Track total content length for token estimation - if (!state.totalContentLength) state.totalContentLength = 0; - if (!state.contextUsagePercentage) state.contextUsagePercentage = 0; - - // Handle assistantResponseEvent - if (eventType === "assistantResponseEvent" && event.payload?.content) { - const content = event.payload.content; - state.totalContentLength += content.length; - - const chunk = { - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: chunkIndex === 0 - ? { role: "assistant", content } - : { content }, - finish_reason: null - }] - }; - chunkIndex++; - controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); - } - - // Handle codeEvent - if (eventType === "codeEvent" && event.payload?.content) { - const chunk = { - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: { content: event.payload.content }, - finish_reason: null - }] - }; - chunkIndex++; - controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); - } - - // Handle toolUseEvent - if (eventType === "toolUseEvent" && event.payload) { - state.hasToolCalls = true; - const toolUse = event.payload; - const toolUses = Array.isArray(toolUse) ? toolUse : [toolUse]; - - for (const singleToolUse of toolUses) { - const toolCallId = singleToolUse.toolUseId || `call_${Date.now()}`; - const toolName = singleToolUse.name || ""; - const toolInput = singleToolUse.input; - - let toolIndex; - const isNewTool = !state.seenToolIds.has(toolCallId); - - if (isNewTool) { - toolIndex = state.toolCallIndex++; - state.seenToolIds.set(toolCallId, toolIndex); - - const startChunk = { - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: { - ...(chunkIndex === 0 ? { role: "assistant" } : {}), - tool_calls: [{ - index: toolIndex, - id: toolCallId, - type: "function", - function: { - name: toolName, - arguments: "" - } - }] - }, - finish_reason: null - }] - }; - chunkIndex++; - controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(startChunk)}\n\n`)); - } else { - toolIndex = state.seenToolIds.get(toolCallId); - } - - if (toolInput !== undefined) { - let argumentsStr; - - if (typeof toolInput === 'string') { - argumentsStr = toolInput; - } else if (typeof toolInput === 'object') { - argumentsStr = JSON.stringify(toolInput); - } else { - continue; - } - - const argsChunk = { - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: { - tool_calls: [{ - index: toolIndex, - function: { - arguments: argumentsStr - } - }] - }, - finish_reason: null - }] - }; - chunkIndex++; - controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(argsChunk)}\n\n`)); - } - } - } - - // Handle messageStopEvent - if (eventType === "messageStopEvent") { - const chunk = { - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: {}, - finish_reason: state.hasToolCalls ? "tool_calls" : "stop" - }] - }; - state.finishEmitted = true; - controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); - } - - // Handle contextUsageEvent to extract contextUsagePercentage - if (eventType === "contextUsageEvent" && event.payload?.contextUsagePercentage) { - state.contextUsagePercentage = event.payload.contextUsagePercentage; - // Mark that we received context usage event - state.hasContextUsage = true; - } - - // Handle meteringEvent - mark that we received it - if (eventType === "meteringEvent") { - state.hasMeteringEvent = true; - } - - // Handle metricsEvent for token usage - if (eventType === "metricsEvent") { - // Extract usage data from metricsEvent payload - const metrics = event.payload?.metricsEvent || event.payload; - if (metrics && typeof metrics === 'object') { - const inputTokens = metrics.inputTokens || 0; - const outputTokens = metrics.outputTokens || 0; - - if (inputTokens > 0 || outputTokens > 0) { - state.usage = { - prompt_tokens: inputTokens, - completion_tokens: outputTokens, - total_tokens: inputTokens + outputTokens - }; - } - } - } - - // Emit final chunk only after receiving BOTH meteringEvent AND contextUsageEvent - if (state.hasMeteringEvent && state.hasContextUsage && !state.finishEmitted) { - state.finishEmitted = true; - - // Estimate tokens if not available from events - if (!state.usage) { - // Estimate output tokens from content length - const estimatedOutputTokens = state.totalContentLength > 0 - ? Math.max(1, Math.floor(state.totalContentLength / 4)) - : 0; - - // Estimate input tokens from contextUsagePercentage - // Kiro models typically have 200k context window - const estimatedInputTokens = state.contextUsagePercentage > 0 - ? Math.floor(state.contextUsagePercentage * 200000 / 100) - : 0; - - state.usage = { - prompt_tokens: estimatedInputTokens, - completion_tokens: estimatedOutputTokens, - total_tokens: estimatedInputTokens + estimatedOutputTokens - }; - } - - const finishChunk = { - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: {}, - finish_reason: state.hasToolCalls ? "tool_calls" : "stop" - }] - }; - - // Include usage in final chunk if available - if (state.usage) { - finishChunk.usage = state.usage; - } - - controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finishChunk)}\n\n`)); - } - } - - if (iterations >= maxIterations) { - console.warn("[Kiro] Max iterations reached in event parsing"); - } - }, - - flush(controller) { - // Emit finish chunk if not already sent - if (!state.finishEmitted) { - state.finishEmitted = true; - const finishChunk = { - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: {}, - finish_reason: state.hasToolCalls ? "tool_calls" : "stop" - }] - }; - controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finishChunk)}\n\n`)); - } - - // Send final done message - controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")); - } - }); - - // Pipe response body through transform stream - if (!response.body) { - return new Response("data: [DONE]\n\n", { status: response.status, headers: { "Content-Type": "text/event-stream" } }); - } - const transformedStream = response.body.pipeThrough(transformStream); - - return new Response(transformedStream, { - status: response.status, - statusText: response.statusText, - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive" - } - }); - } - - async refreshCredentials(credentials, log) { - if (!credentials.refreshToken) return null; - - try { - // Use centralized refreshKiroToken function (handles both AWS SSO OIDC and Social Auth) - const result = await refreshKiroToken( - credentials.refreshToken, - credentials.providerSpecificData, - log - ); - - return result; - } catch (error) { - log?.error?.("TOKEN", `Kiro refresh error: ${error.message}`); - return null; - } - } -} - -/** - * Parse AWS EventStream frame - */ -function parseEventFrame(data) { - try { - const view = new DataView(data.buffer, data.byteOffset); - const headersLength = view.getUint32(4, false); - - // Parse headers - const headers = {}; - let offset = 12; // After prelude - const headerEnd = 12 + headersLength; - - while (offset < headerEnd && offset < data.length) { - const nameLen = data[offset]; - offset++; - if (offset + nameLen > data.length) break; - - const name = new TextDecoder().decode(data.slice(offset, offset + nameLen)); - offset += nameLen; - - const headerType = data[offset]; - offset++; - - if (headerType === 7) { // String type - const valueLen = (data[offset] << 8) | data[offset + 1]; - offset += 2; - if (offset + valueLen > data.length) break; - - const value = new TextDecoder().decode(data.slice(offset, offset + valueLen)); - offset += valueLen; - headers[name] = value; - } else { - break; - } - } - - // Parse payload - const payloadStart = 12 + headersLength; - const payloadEnd = data.length - 4; // Exclude message CRC - - let payload = null; - if (payloadEnd > payloadStart) { - const payloadStr = new TextDecoder().decode(data.slice(payloadStart, payloadEnd)); - - // Skip empty or whitespace-only payloads - if (!payloadStr || !payloadStr.trim()) { - return { headers, payload: null }; - } - - try { - payload = JSON.parse(payloadStr); - } catch (parseError) { - // Log parse error for debugging - console.warn(`[Kiro] Failed to parse payload: ${parseError.message} | payload: ${payloadStr.substring(0, 100)}`); - payload = { raw: payloadStr }; - } - } - - return { headers, payload }; - } catch { - return null; - } -} - -export default KiroExecutor; diff --git a/9router/open-sse/executors/vertex.js b/9router/open-sse/executors/vertex.js deleted file mode 100644 index 8928faf3..00000000 --- a/9router/open-sse/executors/vertex.js +++ /dev/null @@ -1,120 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { parseVertexSaJson, refreshVertexToken } from "../services/tokenRefresh.js"; -import { proxyAwareFetch } from "../utils/proxyFetch.js"; - -// Cache project IDs resolved from raw API keys { apiKey → projectId } -const projectIdCache = new Map(); - -/** - * Resolve GCP project ID from a raw Vertex API key. - * Sends a dummy 404 request and parses "projects/{id}" from the error message. - */ -async function resolveProjectId(apiKey) { - if (projectIdCache.has(apiKey)) return projectIdCache.get(apiKey); - - const res = await fetch( - `https://aiplatform.googleapis.com/v1/publishers/google/models/__probe__:generateContent?key=${apiKey}`, - { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" } - ); - const json = await res.json().catch(() => null); - const msg = json?.[0]?.error?.message || json?.error?.message || ""; - const match = msg.match(/projects\/([^/]+)\//); - const projectId = match?.[1] || null; - - if (projectId) projectIdCache.set(apiKey, projectId); - return projectId; -} - -/** - * VertexExecutor - Google Cloud Vertex AI - * - * "vertex" → Gemini models via regional/global Vertex endpoint - * "vertex-partner" → Partner models (Llama, Mistral, GLM, DeepSeek, Qwen) - * via global OpenAI-compatible endpoint - * - * Auth: SA JSON (stored as apiKey) → JWT assertion → Bearer token (via jose) - * Token is minted/cached in tokenRefresh.js, not here. - */ -export class VertexExecutor extends BaseExecutor { - constructor(providerId = "vertex") { - super(providerId, PROVIDERS[providerId] || {}); - } - - buildUrl(model, stream, urlIndex = 0, credentials = null) { - const saJson = parseVertexSaJson(credentials?.apiKey); - const rawKey = !saJson ? credentials?.apiKey : null; - const projectId = saJson?.project_id || credentials?.providerSpecificData?.projectId; - - if (this.provider === "vertex-partner") { - // Partner models require project_id in path regardless of auth method - if (!projectId) throw new Error("Vertex partner models require a project_id. Add it in providerSpecificData or use Service Account JSON."); - const url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/global/endpoints/openapi/chat/completions`; - return rawKey ? `${url}?key=${rawKey}` : url; - } - - // Gemini on Vertex: always use global publishers endpoint - const action = stream ? "streamGenerateContent" : "generateContent"; - let url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${model}:${action}`; - - if (rawKey) url += `?key=${rawKey}`; - return url; - } - - buildHeaders(credentials, stream = true) { - const headers = { "Content-Type": "application/json" }; - - // Only set Bearer token if using SA JSON flow (raw key goes in URL ?key=) - if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - - if (stream) headers["Accept"] = "text/event-stream"; - - return headers; - } - - async refreshCredentials(credentials, log) { - const saJson = parseVertexSaJson(credentials?.apiKey); - if (!saJson) return null; - - const result = await refreshVertexToken(saJson, log); - if (!result) return null; - - return { accessToken: result.accessToken, expiresAt: result.expiresAt }; - } - - async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { - const saJson = parseVertexSaJson(credentials?.apiKey); - - // SA JSON flow: mint Bearer token (cached) - if (saJson) { - const result = await refreshVertexToken(saJson, log); - if (!result?.accessToken) throw new Error("Vertex: failed to mint access token from Service Account JSON"); - credentials.accessToken = result.accessToken; - } - - // vertex-partner with raw key: auto-resolve project_id if not provided - if (this.provider === "vertex-partner" && !saJson && !credentials?.providerSpecificData?.projectId) { - const projectId = await resolveProjectId(credentials.apiKey); - if (!projectId) throw new Error("Vertex: could not resolve project_id from API key. Please add it manually in provider settings."); - log?.debug?.("VERTEX", `Resolved project_id: ${projectId}`); - credentials.providerSpecificData = { ...credentials.providerSpecificData, projectId }; - } - - const url = this.buildUrl(model, stream, 0, credentials); - const headers = this.buildHeaders(credentials, stream); - const transformedBody = this.transformRequest(model, body, stream, credentials); - - const response = await proxyAwareFetch(url, { - method: "POST", - headers, - body: JSON.stringify(transformedBody), - signal, - }, proxyOptions); - - return { response, url, headers, transformedBody }; - } -} - -export default VertexExecutor; diff --git a/9router/open-sse/handlers/chatCore.js b/9router/open-sse/handlers/chatCore.js deleted file mode 100644 index b61ed427..00000000 --- a/9router/open-sse/handlers/chatCore.js +++ /dev/null @@ -1,213 +0,0 @@ -import { detectFormat, getTargetFormat } from "../services/provider.js"; -import { translateRequest } from "../translator/index.js"; -import { FORMATS } from "../translator/formats.js"; -import { COLORS } from "../utils/stream.js"; -import { createStreamController } from "../utils/streamHandler.js"; -import { refreshWithRetry } from "../services/tokenRefresh.js"; -import { createRequestLogger } from "../utils/requestLogger.js"; -import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js"; -import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js"; -import { HTTP_STATUS } from "../config/runtimeConfig.js"; -import { handleBypassRequest } from "../utils/bypassHandler.js"; -import { trackPendingRequest, appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js"; -import { getExecutor } from "../executors/index.js"; -import { buildRequestDetail, extractRequestConfig } from "./chatCore/requestDetail.js"; -import { handleForcedSSEToJson } from "./chatCore/sseToJsonHandler.js"; -import { handleNonStreamingResponse } from "./chatCore/nonStreamingHandler.js"; -import { handleStreamingResponse, buildOnStreamComplete } from "./chatCore/streamingHandler.js"; - -/** - * Core chat handler - shared between SSE and Worker - * @param {object} options.body - Request body - * @param {object} options.modelInfo - { provider, model } - * @param {object} options.credentials - Provider credentials - * @param {string} options.sourceFormatOverride - Override detected source format (e.g. "openai-responses") - */ -export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, sourceFormatOverride }) { - const { provider, model } = modelInfo; - const requestStartTime = Date.now(); - - const sourceFormat = sourceFormatOverride || detectFormat(body); - - // Check for bypass patterns (warmup, skip, cc naming) - const bypassResponse = handleBypassRequest(body, model, userAgent, ccFilterNaming); - if (bypassResponse) return bypassResponse; - - const alias = PROVIDER_ID_TO_ALIAS[provider] || provider; - const modelTargetFormat = getModelTargetFormat(alias, model); - const targetFormat = modelTargetFormat || getTargetFormat(provider); - - const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI; - const providerRequiresStreaming = provider === "openai" || provider === "codex"; - let stream = providerRequiresStreaming ? true : (body.stream !== false); - - // Check client Accept header preference for non-streaming requests - // This fixes AI SDK compatibility where clients send Accept: application/json - const acceptHeader = clientRawRequest?.headers?.accept || ""; - const clientPrefersJson = acceptHeader.includes("application/json"); - const clientPrefersSSE = acceptHeader.includes("text/event-stream"); - if (clientPrefersJson && !clientPrefersSSE && body.stream !== true) { - stream = false; - } - - const reqLogger = await createRequestLogger(sourceFormat, targetFormat, model); - if (clientRawRequest) reqLogger.logClientRawRequest(clientRawRequest.endpoint, clientRawRequest.body, clientRawRequest.headers); - reqLogger.logRawRequest(body); - log?.debug?.("FORMAT", `${sourceFormat} → ${targetFormat} | stream=${stream}`); - - let translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger); - if (!translatedBody) { - trackPendingRequest(model, provider, connectionId, false, true); - return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Failed to translate request for ${sourceFormat} → ${targetFormat}`); - } - const toolNameMap = translatedBody._toolNameMap; - delete translatedBody._toolNameMap; - translatedBody.model = model; - - const executor = getExecutor(provider); - trackPendingRequest(model, provider, connectionId, true); - appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => {}); - - const msgCount = translatedBody.messages?.length || translatedBody.input?.length || translatedBody.contents?.length || translatedBody.request?.contents?.length || 0; - log?.debug?.("REQUEST", `${provider.toUpperCase()} | ${model} | ${msgCount} msgs`); - - const streamController = createStreamController({ - onDisconnect: (reason) => { - trackPendingRequest(model, provider, connectionId, false); - if (onDisconnect) onDisconnect(reason); - }, - onError: () => trackPendingRequest(model, provider, connectionId, false), - log, provider, model - }); - - const proxyOptions = { - connectionProxyEnabled: credentials?.providerSpecificData?.connectionProxyEnabled === true, - connectionProxyUrl: credentials?.providerSpecificData?.connectionProxyUrl || "", - connectionNoProxy: credentials?.providerSpecificData?.connectionNoProxy || "", - }; - - if (proxyOptions.connectionProxyEnabled && proxyOptions.connectionProxyUrl) { - let maskedProxyUrl = proxyOptions.connectionProxyUrl; - try { - const parsed = new URL(proxyOptions.connectionProxyUrl); - const host = parsed.hostname || ""; - const port = parsed.port ? `:${parsed.port}` : ""; - const protocol = parsed.protocol || "http:"; - maskedProxyUrl = `${protocol}//${host}${port}`; - } catch { - // Keep raw if URL parsing fails - } - - const poolId = credentials?.providerSpecificData?.connectionProxyPoolId || "none"; - const connectionName = credentials?.connectionName || credentials?.connectionId || "unknown"; - log?.info?.("PROXY", `${provider.toUpperCase()} | ${model} | conn=${connectionName} | pool=${poolId} | url=${maskedProxyUrl}`); - } - - if (proxyOptions.connectionProxyEnabled && proxyOptions.connectionNoProxy) { - const connectionName = credentials?.connectionName || credentials?.connectionId || "unknown"; - log?.debug?.("PROXY", `${provider.toUpperCase()} | ${model} | conn=${connectionName} | no_proxy=${proxyOptions.connectionNoProxy}`); - } - - // Execute request - let providerResponse, providerUrl, providerHeaders, finalBody; - try { - const result = await executor.execute({ model, body: translatedBody, stream, credentials, signal: streamController.signal, log, proxyOptions }); - providerResponse = result.response; - providerUrl = result.url; - providerHeaders = result.headers; - finalBody = result.transformedBody; - reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody); - } catch (error) { - trackPendingRequest(model, provider, connectionId, false, true); - appendRequestLog({ model, provider, connectionId, status: `FAILED ${error.name === "AbortError" ? 499 : HTTP_STATUS.BAD_GATEWAY}` }).catch(() => {}); - saveRequestDetail(buildRequestDetail({ - provider, model, connectionId, - latency: { ttft: 0, total: Date.now() - requestStartTime }, - tokens: { prompt_tokens: 0, completion_tokens: 0 }, - request: extractRequestConfig(body, stream), - providerRequest: translatedBody || null, - response: { error: error.message || String(error), status: error.name === "AbortError" ? 499 : 502, thinking: null }, - status: "error" - })).catch(() => {}); - - if (error.name === "AbortError") { - streamController.handleError(error); - return createErrorResult(499, "Request aborted"); - } - const errMsg = formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY); - console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`); - return createErrorResult(HTTP_STATUS.BAD_GATEWAY, errMsg); - } - - // Handle 401/403 - try token refresh - if (providerResponse.status === HTTP_STATUS.UNAUTHORIZED || providerResponse.status === HTTP_STATUS.FORBIDDEN) { - try { - const newCredentials = await refreshWithRetry(() => executor.refreshCredentials(credentials, log), 3, log); - if (newCredentials?.accessToken || newCredentials?.copilotToken) { - log?.info?.("TOKEN", `${provider.toUpperCase()} | refreshed`); - Object.assign(credentials, newCredentials); - if (onCredentialsRefreshed) { - try { await onCredentialsRefreshed(newCredentials); } catch (e) { log?.warn?.("TOKEN", `onCredentialsRefreshed failed: ${e.message}`); } - } - try { - const retryResult = await executor.execute({ model, body: translatedBody, stream, credentials, signal: streamController.signal, log, proxyOptions }); - if (retryResult.response.ok) { providerResponse = retryResult.response; providerUrl = retryResult.url; } - } catch { log?.warn?.("TOKEN", `${provider.toUpperCase()} | retry after refresh failed`); } - } else { - log?.warn?.("TOKEN", `${provider.toUpperCase()} | refresh failed`); - } - } catch (e) { - log?.warn?.("TOKEN", `${provider.toUpperCase()} | refresh threw: ${e.message}`); - } - } - - // Provider returned error - if (!providerResponse.ok) { - trackPendingRequest(model, provider, connectionId, false, true); - const { statusCode, message, retryAfterMs } = await parseUpstreamError(providerResponse, provider); - appendRequestLog({ model, provider, connectionId, status: `FAILED ${statusCode}` }).catch(() => {}); - saveRequestDetail(buildRequestDetail({ - provider, model, connectionId, - latency: { ttft: 0, total: Date.now() - requestStartTime }, - tokens: { prompt_tokens: 0, completion_tokens: 0 }, - request: extractRequestConfig(body, stream), - providerRequest: finalBody || translatedBody || null, - response: { error: message, status: statusCode, thinking: null }, - status: "error" - })).catch(() => {}); - - const errMsg = formatProviderError(new Error(message), provider, model, statusCode); - console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`); - if (retryAfterMs && provider === "antigravity") { - log?.debug?.("RETRY", `Antigravity quota reset in ${Math.ceil(retryAfterMs / 1000)}s`); - } - reqLogger.logError(new Error(message), finalBody || translatedBody); - return createErrorResult(statusCode, errMsg, retryAfterMs); - } - - const sharedCtx = { provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess }; - const appendLog = (extra) => appendRequestLog({ model, provider, connectionId, ...extra }).catch(() => {}); - const trackDone = () => trackPendingRequest(model, provider, connectionId, false); - - // Provider forced streaming but client wants JSON - if (!clientRequestedStreaming && providerRequiresStreaming) { - const result = await handleForcedSSEToJson({ ...sharedCtx, providerResponse, sourceFormat, trackDone, appendLog }); - if (result) { streamController.handleComplete(); return result; } - } - - // True non-streaming response - if (!stream) { - const result = await handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, reqLogger, trackDone, appendLog }); - streamController.handleComplete(); - return result; - } - - // Streaming response - const { onStreamComplete } = buildOnStreamComplete({ ...sharedCtx }); - return handleStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, streamController, onStreamComplete }); -} - -export function isTokenExpiringSoon(expiresAt, bufferMs = 5 * 60 * 1000) { - if (!expiresAt) return false; - return new Date(expiresAt).getTime() - Date.now() < bufferMs; -} diff --git a/9router/open-sse/handlers/chatCore/nonStreamingHandler.js b/9router/open-sse/handlers/chatCore/nonStreamingHandler.js deleted file mode 100644 index 8b92852a..00000000 --- a/9router/open-sse/handlers/chatCore/nonStreamingHandler.js +++ /dev/null @@ -1,204 +0,0 @@ -import { FORMATS } from "../../translator/formats.js"; -import { needsTranslation } from "../../translator/index.js"; -import { ollamaBodyToOpenAI } from "../../translator/response/ollama-to-openai.js"; -import { addBufferToUsage, filterUsageForFormat } from "../../utils/usageTracking.js"; -import { createErrorResult } from "../../utils/error.js"; -import { HTTP_STATUS } from "../../config/runtimeConfig.js"; -import { parseSSEToOpenAIResponse } from "./sseToJsonHandler.js"; -import { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, saveUsageStats } from "./requestDetail.js"; -import { appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js"; - -/** - * Translate non-streaming response body from provider format → OpenAI format. - */ -export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) { - if (targetFormat === sourceFormat || targetFormat === FORMATS.OPENAI) return responseBody; - - // Gemini / Antigravity - if (targetFormat === FORMATS.GEMINI || targetFormat === FORMATS.ANTIGRAVITY || targetFormat === FORMATS.GEMINI_CLI) { - const response = responseBody.response || responseBody; - if (!response?.candidates?.[0]) return responseBody; - - const candidate = response.candidates[0]; - const content = candidate.content; - const usage = response.usageMetadata || responseBody.usageMetadata; - let textContent = "", reasoningContent = ""; - const toolCalls = []; - - if (content?.parts) { - for (const part of content.parts) { - if (part.thought === true && part.text) reasoningContent += part.text; - else if (part.text !== undefined) textContent += part.text; - if (part.functionCall) { - toolCalls.push({ - id: `call_${part.functionCall.name}_${Date.now()}_${toolCalls.length}`, - type: "function", - function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args || {}) } - }); - } - } - } - - const message = { role: "assistant" }; - if (textContent) message.content = textContent; - if (reasoningContent) message.reasoning_content = reasoningContent; - if (toolCalls.length > 0) message.tool_calls = toolCalls; - if (!message.content && !message.tool_calls) message.content = ""; - - let finishReason = (candidate.finishReason || "stop").toLowerCase(); - if (finishReason === "stop" && toolCalls.length > 0) finishReason = "tool_calls"; - - const result = { - id: `chatcmpl-${response.responseId || Date.now()}`, - object: "chat.completion", - created: Math.floor(new Date(response.createTime || Date.now()).getTime() / 1000), - model: response.modelVersion || "gemini", - choices: [{ index: 0, message, finish_reason: finishReason }] - }; - - if (usage) { - result.usage = { - prompt_tokens: (usage.promptTokenCount || 0) + (usage.thoughtsTokenCount || 0), - completion_tokens: usage.candidatesTokenCount || 0, - total_tokens: usage.totalTokenCount || 0 - }; - if (usage.thoughtsTokenCount > 0) { - result.usage.completion_tokens_details = { reasoning_tokens: usage.thoughtsTokenCount }; - } - } - return result; - } - - // Claude - if (targetFormat === FORMATS.CLAUDE) { - if (!responseBody.content) return responseBody; - - let textContent = "", thinkingContent = ""; - const toolCalls = []; - - for (const block of responseBody.content) { - if (block.type === "text") { - // Strip markdown code block markers (e.g. kimi wraps JSON in ```json...```) - const raw = block.text ?? ""; - const text = raw.replace(/^\s*```\s*json\s*\n?/i, "").replace(/\n?\s*```\s*$/i, ""); - textContent += text; - } else if (block.type === "thinking") thinkingContent += block.thinking || ""; - else if (block.type === "tool_use") { - toolCalls.push({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input || {}) } }); - } - } - - const message = { role: "assistant" }; - if (textContent) message.content = textContent; - if (thinkingContent) message.reasoning_content = thinkingContent; - if (toolCalls.length > 0) message.tool_calls = toolCalls; - if (!message.content && !message.tool_calls) message.content = ""; - - let finishReason = responseBody.stop_reason || "stop"; - if (finishReason === "end_turn") finishReason = "stop"; - if (finishReason === "tool_use") finishReason = "tool_calls"; - - const result = { - id: `chatcmpl-${responseBody.id || Date.now()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: responseBody.model || "claude", - choices: [{ index: 0, message, finish_reason: finishReason }] - }; - - if (responseBody.usage) { - result.usage = { - prompt_tokens: responseBody.usage.input_tokens || 0, - completion_tokens: responseBody.usage.output_tokens || 0, - total_tokens: (responseBody.usage.input_tokens || 0) + (responseBody.usage.output_tokens || 0) - }; - } - return result; - } - - // Ollama - if (targetFormat === FORMATS.OLLAMA) { - return ollamaBodyToOpenAI(responseBody); - } - - return responseBody; -} - -/** - * Handle non-streaming response from provider. - */ -export async function handleNonStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, trackDone, appendLog }) { - trackDone(); - const contentType = providerResponse.headers.get("content-type") || ""; - let responseBody; - - if (contentType.includes("text/event-stream")) { - const sseText = await providerResponse.text(); - const parsed = parseSSEToOpenAIResponse(sseText, model); - if (!parsed) { - appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` }); - return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "Invalid SSE response for non-streaming request"); - } - responseBody = parsed; - } else { - try { - responseBody = await providerResponse.json(); - } catch (err) { - appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` }); - console.error(`[ChatCore] Failed to parse JSON from ${provider}:`, err.message); - return createErrorResult(HTTP_STATUS.BAD_GATEWAY, `Invalid JSON response from ${provider}`); - } - } - - reqLogger.logProviderResponse(providerResponse.status, providerResponse.statusText, providerResponse.headers, responseBody); - if (onRequestSuccess) await onRequestSuccess(); - - const usage = extractUsageFromResponse(responseBody); - appendLog({ tokens: usage, status: "200 OK" }); - saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint }); - - const translatedResponse = needsTranslation(targetFormat, sourceFormat) - ? translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) - : responseBody; - - // Ensure OpenAI-required fields - if (!translatedResponse.object) translatedResponse.object = "chat.completion"; - if (!translatedResponse.created) translatedResponse.created = Math.floor(Date.now() / 1000); - - // Strip Azure-specific fields - delete translatedResponse.prompt_filter_results; - if (translatedResponse?.choices) { - for (const choice of translatedResponse.choices) delete choice.content_filter_results; - } - - if (translatedResponse?.usage) { - translatedResponse.usage = filterUsageForFormat(addBufferToUsage(translatedResponse.usage), sourceFormat); - } - - reqLogger.logConvertedResponse(translatedResponse); - - const totalLatency = Date.now() - requestStartTime; - saveRequestDetail(buildRequestDetail({ - provider, model, connectionId, - latency: { ttft: totalLatency, total: totalLatency }, - tokens: usage || { prompt_tokens: 0, completion_tokens: 0 }, - request: extractRequestConfig(body, stream), - providerRequest: finalBody || translatedBody || null, - providerResponse: responseBody || null, - response: { - content: translatedResponse?.choices?.[0]?.message?.content || translatedResponse?.content || null, - thinking: translatedResponse?.choices?.[0]?.message?.reasoning_content || translatedResponse?.reasoning_content || null, - finish_reason: translatedResponse?.choices?.[0]?.finish_reason || "unknown" - }, - status: "success" - }, { endpoint: clientRawRequest?.endpoint || null })).catch(err => { - console.error("[RequestDetail] Failed to save:", err.message); - }); - - return { - success: true, - response: new Response(JSON.stringify(translatedResponse), { - headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } - }) - }; -} diff --git a/9router/open-sse/handlers/chatCore/requestDetail.js b/9router/open-sse/handlers/chatCore/requestDetail.js deleted file mode 100644 index d9dde1a3..00000000 --- a/9router/open-sse/handlers/chatCore/requestDetail.js +++ /dev/null @@ -1,102 +0,0 @@ -import { saveRequestUsage, appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js"; -import { COLORS } from "../../utils/stream.js"; - -const OPTIONAL_PARAMS = [ - "temperature", "top_p", "top_k", - "max_tokens", "max_completion_tokens", - "thinking", "reasoning", "enable_thinking", - "presence_penalty", "frequency_penalty", - "seed", "stop", "tools", "tool_choice", - "response_format", "prediction", "store", "metadata", - "n", "logprobs", "top_logprobs", "logit_bias", - "user", "parallel_tool_calls" -]; - -export function extractRequestConfig(body, stream) { - const config = { messages: body.messages || [], model: body.model, stream }; - for (const param of OPTIONAL_PARAMS) { - if (body[param] !== undefined) config[param] = body[param]; - } - return config; -} - -export function extractUsageFromResponse(responseBody) { - if (!responseBody || typeof responseBody !== "object") return null; - - // Claude format - if (responseBody.usage?.input_tokens !== undefined) { - return { - prompt_tokens: responseBody.usage.input_tokens || 0, - completion_tokens: responseBody.usage.output_tokens || 0, - cache_read_input_tokens: responseBody.usage.cache_read_input_tokens, - cache_creation_input_tokens: responseBody.usage.cache_creation_input_tokens - }; - } - - // OpenAI format - if (responseBody.usage?.prompt_tokens !== undefined) { - return { - prompt_tokens: responseBody.usage.prompt_tokens || 0, - completion_tokens: responseBody.usage.completion_tokens || 0, - cached_tokens: responseBody.usage.prompt_tokens_details?.cached_tokens, - reasoning_tokens: responseBody.usage.completion_tokens_details?.reasoning_tokens - }; - } - - // Gemini format - if (responseBody.usageMetadata) { - return { - prompt_tokens: responseBody.usageMetadata.promptTokenCount || 0, - completion_tokens: responseBody.usageMetadata.candidatesTokenCount || 0, - reasoning_tokens: responseBody.usageMetadata.thoughtsTokenCount - }; - } - - return null; -} - -export function buildRequestDetail(base, overrides = {}) { - return { - provider: base.provider || "unknown", - model: base.model || "unknown", - connectionId: base.connectionId || undefined, - timestamp: new Date().toISOString(), - latency: base.latency || { ttft: 0, total: 0 }, - tokens: base.tokens || { prompt_tokens: 0, completion_tokens: 0 }, - request: base.request, - providerRequest: base.providerRequest || null, - providerResponse: base.providerResponse || null, - response: base.response || {}, - status: base.status || "success", - ...overrides - }; -} - -export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, endpoint, label = "USAGE" }) { - if (!tokens || typeof tokens !== "object") return; - - const inTokens = tokens.input_tokens ?? tokens.prompt_tokens ?? 0; - const outTokens = tokens.output_tokens ?? tokens.completion_tokens ?? 0; - - if (inTokens === 0 && outTokens === 0) return; - - const time = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); - const accountSuffix = connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""; - console.log(`${COLORS.green}[${time}] 📊 [${label}] ${provider.toUpperCase()} | in=${inTokens} | out=${outTokens}${accountSuffix}${COLORS.reset}`); - - // Normalize to OpenAI token shape for storage - const normalized = { - prompt_tokens: tokens.prompt_tokens ?? tokens.input_tokens ?? 0, - completion_tokens: tokens.completion_tokens ?? tokens.output_tokens ?? 0 - }; - - saveRequestUsage({ - provider: provider || "unknown", - model: model || "unknown", - tokens: normalized, - timestamp: new Date().toISOString(), - connectionId: connectionId || undefined, - apiKey: apiKey || undefined, - endpoint: endpoint || null - }).catch(() => {}); -} diff --git a/9router/open-sse/handlers/chatCore/sseToJsonHandler.js b/9router/open-sse/handlers/chatCore/sseToJsonHandler.js deleted file mode 100644 index 8df3996c..00000000 --- a/9router/open-sse/handlers/chatCore/sseToJsonHandler.js +++ /dev/null @@ -1,161 +0,0 @@ -import { convertResponsesStreamToJson } from "../../transformer/streamToJsonConverter.js"; -import { createErrorResult } from "../../utils/error.js"; -import { HTTP_STATUS } from "../../config/runtimeConfig.js"; -import { FORMATS } from "../../translator/formats.js"; -import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js"; -import { saveRequestDetail, appendRequestLog } from "@/lib/usageDb.js"; - -/** - * Parse OpenAI-style SSE text into a single chat completion JSON. - * Used when provider forces streaming but client wants non-streaming. - */ -export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) { - const chunks = []; - - for (const line of String(rawSSE || "").split("\n")) { - const trimmed = line.trim(); - if (!trimmed.startsWith("data:")) continue; - const payload = trimmed.slice(5).trim(); - if (!payload || payload === "[DONE]") continue; - try { chunks.push(JSON.parse(payload)); } catch { /* ignore malformed lines */ } - } - - if (chunks.length === 0) return null; - - const first = chunks[0]; - const contentParts = []; - const reasoningParts = []; - let finishReason = "stop"; - let usage = null; - - for (const chunk of chunks) { - const choice = chunk?.choices?.[0]; - const delta = choice?.delta || {}; - if (typeof delta.content === "string" && delta.content.length > 0) contentParts.push(delta.content); - if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) reasoningParts.push(delta.reasoning_content); - if (choice?.finish_reason) finishReason = choice.finish_reason; - if (chunk?.usage && typeof chunk.usage === "object") usage = chunk.usage; - } - - const message = { role: "assistant", content: contentParts.join("") }; - if (reasoningParts.length > 0) message.reasoning_content = reasoningParts.join(""); - - const result = { - id: first.id || `chatcmpl-${Date.now()}`, - object: "chat.completion", - created: first.created || Math.floor(Date.now() / 1000), - model: first.model || fallbackModel || "unknown", - choices: [{ index: 0, message, finish_reason: finishReason }] - }; - if (usage) result.usage = usage; - return result; -} - -/** - * Handle case: provider forced streaming but client wants JSON. - * Supports both Codex/Responses API SSE and standard Chat Completions SSE. - */ -export async function handleForcedSSEToJson({ providerResponse, sourceFormat, provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, trackDone, appendLog }) { - const contentType = providerResponse.headers.get("content-type") || ""; - const isSSE = contentType.includes("text/event-stream") || (contentType === "" && provider === "codex"); - if (!isSSE) return null; // not handled here - - trackDone(); - - const ctx = { - provider, model, connectionId, - request: extractRequestConfig(body, stream), - providerRequest: finalBody || translatedBody || null - }; - - // Codex/Responses API SSE path - const isCodexResponsesApi = provider === "codex" || sourceFormat === FORMATS.OPENAI_RESPONSES; - if (isCodexResponsesApi) { - try { - const jsonResponse = await convertResponsesStreamToJson(providerResponse.body); - if (onRequestSuccess) await onRequestSuccess(); - - const usage = jsonResponse.usage || {}; - appendLog({ tokens: usage, status: "200 OK" }); - saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint }); - - const msgItem = jsonResponse.output?.find(item => item.type === "message"); - const textContent = msgItem?.content?.find(c => c.type === "output_text")?.text || msgItem?.content?.[0]?.text || null; - const totalLatency = Date.now() - requestStartTime; - - saveRequestDetail(buildRequestDetail({ - ...ctx, - latency: { ttft: totalLatency, total: totalLatency }, - tokens: { prompt_tokens: usage.input_tokens || 0, completion_tokens: usage.output_tokens || 0 }, - response: { content: textContent, thinking: null, finish_reason: jsonResponse.status || "unknown" }, - status: "success" - }, { endpoint: clientRawRequest?.endpoint || null })).catch(() => {}); - - // Client is Responses API → return as-is - if (sourceFormat === FORMATS.OPENAI_RESPONSES) { - return { success: true, response: new Response(JSON.stringify(jsonResponse), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }) }; - } - - // Build client-format response - const inTokens = usage.input_tokens || 0; - const outTokens = usage.output_tokens || 0; - let finalResp; - - if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI) { - finalResp = { - response: { - candidates: [{ content: { role: "model", parts: [{ text: textContent || "" }] }, finishReason: "STOP", index: 0 }], - usageMetadata: { promptTokenCount: inTokens, candidatesTokenCount: outTokens, totalTokenCount: inTokens + outTokens }, - modelVersion: model, - responseId: jsonResponse.id || `resp_${Date.now()}` - } - }; - } else { - finalResp = { - id: jsonResponse.id || `chatcmpl-${Date.now()}`, - object: "chat.completion", - created: jsonResponse.created_at || Math.floor(Date.now() / 1000), - model: jsonResponse.model || model, - choices: [{ index: 0, message: { role: "assistant", content: textContent || "" }, finish_reason: jsonResponse.status === "completed" ? "stop" : (jsonResponse.status || "stop") }], - usage: { prompt_tokens: inTokens, completion_tokens: outTokens, total_tokens: inTokens + outTokens } - }; - } - - return { success: true, response: new Response(JSON.stringify(finalResp), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }) }; - } catch (err) { - console.error("[ChatCore] Responses API SSE→JSON failed:", err); - return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "Failed to convert streaming response to JSON"); - } - } - - // Standard Chat Completions SSE path - try { - const sseText = await providerResponse.text(); - const parsed = parseSSEToOpenAIResponse(sseText, model); - if (!parsed) return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "Invalid SSE response for non-streaming request"); - - if (onRequestSuccess) await onRequestSuccess(); - - const usage = parsed.usage || {}; - appendLog({ tokens: usage, status: "200 OK" }); - saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint }); - - const totalLatency = Date.now() - requestStartTime; - saveRequestDetail(buildRequestDetail({ - ...ctx, - latency: { ttft: totalLatency, total: totalLatency }, - tokens: usage, - response: { - content: parsed.choices?.[0]?.message?.content || null, - thinking: parsed.choices?.[0]?.message?.reasoning_content || null, - finish_reason: parsed.choices?.[0]?.finish_reason || "unknown" - }, - status: "success" - }, { endpoint: clientRawRequest?.endpoint || null })).catch(() => {}); - - return { success: true, response: new Response(JSON.stringify(parsed), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }) }; - } catch (err) { - console.error("[ChatCore] Chat Completions SSE→JSON failed:", err); - return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "Failed to convert streaming response to JSON"); - } -} diff --git a/9router/open-sse/handlers/chatCore/streamingHandler.js b/9router/open-sse/handlers/chatCore/streamingHandler.js deleted file mode 100644 index 03d12c13..00000000 --- a/9router/open-sse/handlers/chatCore/streamingHandler.js +++ /dev/null @@ -1,99 +0,0 @@ -import { FORMATS } from "../../translator/formats.js"; -import { needsTranslation } from "../../translator/index.js"; -import { createSSETransformStreamWithLogger, createPassthroughStreamWithLogger } from "../../utils/stream.js"; -import { pipeWithDisconnect } from "../../utils/streamHandler.js"; -import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js"; -import { saveRequestDetail } from "@/lib/usageDb.js"; - -const SSE_HEADERS = { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "Access-Control-Allow-Origin": "*" -}; - -/** - * Determine which SSE transform stream to use based on provider/format. - */ -function buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey }) { - const isDroidCLI = userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli"); - const needsCodexTranslation = provider === "codex" && targetFormat === FORMATS.OPENAI_RESPONSES && !isDroidCLI; - - if (needsCodexTranslation) { - // Codex returns Responses API SSE → translate to client format - let codexTarget; - if (sourceFormat === FORMATS.OPENAI_RESPONSES) codexTarget = FORMATS.OPENAI_RESPONSES; - else if (sourceFormat === FORMATS.CLAUDE) codexTarget = FORMATS.CLAUDE; - else if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI) codexTarget = FORMATS.ANTIGRAVITY; - else codexTarget = FORMATS.OPENAI; - return createSSETransformStreamWithLogger(FORMATS.OPENAI_RESPONSES, codexTarget, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey); - } - - if (needsTranslation(targetFormat, sourceFormat)) { - return createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey); - } - - return createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId, body, onStreamComplete, apiKey); -} - -/** - * Handle streaming response — pipe provider SSE through transform stream to client. - */ -export function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, streamController, onStreamComplete }) { - if (onRequestSuccess) onRequestSuccess(); - - const transformStream = buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey }); - const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController); - - const streamDetailId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; - saveRequestDetail(buildRequestDetail({ - provider, model, connectionId, - latency: { ttft: 0, total: Date.now() - requestStartTime }, - tokens: { prompt_tokens: 0, completion_tokens: 0 }, - request: extractRequestConfig(body, stream), - providerRequest: finalBody || translatedBody || null, - providerResponse: "[Streaming - raw response not captured]", - response: { content: "[Streaming in progress...]", thinking: null, type: "streaming" }, - status: "success" - }, { id: streamDetailId })).catch(err => { - console.error("[RequestDetail] Failed to save streaming request:", err.message); - }); - - return { - success: true, - response: new Response(transformedBody, { headers: SSE_HEADERS }) - }; -} - -/** - * Build onStreamComplete callback for streaming usage tracking. - */ -export function buildOnStreamComplete({ provider, model, connectionId, apiKey, requestStartTime, body, stream, finalBody, translatedBody, clientRawRequest }) { - const streamDetailId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; - - const onStreamComplete = (contentObj, usage, ttftAt) => { - const latency = { - ttft: ttftAt ? ttftAt - requestStartTime : Date.now() - requestStartTime, - total: Date.now() - requestStartTime - }; - const safeContent = contentObj?.content || "[Empty streaming response]"; - const safeThinking = contentObj?.thinking || null; - - saveRequestDetail(buildRequestDetail({ - provider, model, connectionId, - latency, - tokens: usage || { prompt_tokens: 0, completion_tokens: 0 }, - request: extractRequestConfig(body, stream), - providerRequest: finalBody || translatedBody || null, - providerResponse: safeContent, - response: { content: safeContent, thinking: safeThinking, type: "streaming" }, - status: "success" - }, { id: streamDetailId })).catch(err => { - console.error("[RequestDetail] Failed to update streaming content:", err.message); - }); - - saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, label: "STREAM USAGE" }); - }; - - return { onStreamComplete, streamDetailId }; -} diff --git a/9router/open-sse/handlers/embeddingsCore.js b/9router/open-sse/handlers/embeddingsCore.js deleted file mode 100644 index b9eb6360..00000000 --- a/9router/open-sse/handlers/embeddingsCore.js +++ /dev/null @@ -1,302 +0,0 @@ -import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js"; -import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js"; -import { HTTP_STATUS } from "../config/runtimeConfig.js"; -import { getExecutor } from "../executors/index.js"; -import { refreshWithRetry } from "../services/tokenRefresh.js"; - -// Google AI (Gemini) provider aliases / identifiers -const GEMINI_PROVIDERS = new Set(["gemini", "google_ai_studio"]); - -/** - * Check whether a provider targets the Google AI (Gemini) embeddings API. - * @param {string} provider - */ -function isGeminiProvider(provider) { - return GEMINI_PROVIDERS.has(provider); -} - -/** - * Build the embeddings request body for the target provider. - * - * - OpenAI / openai-compatible / openrouter: standard { model, input } format. - * - Google AI (Gemini): different format per API spec. - * - Single input → embedContent body: { model, content: { parts: [{ text }] } } - * - Batch input → batchEmbedContents body: { requests: [{ model, content: { parts: [{ text }] } }] } - */ -function buildEmbeddingsBody(provider, model, input, encodingFormat) { - if (isGeminiProvider(provider)) { - // Normalize model name: Gemini API expects "models/" prefix - const geminiModel = model.startsWith("models/") ? model : `models/${model}`; - - if (Array.isArray(input)) { - // Batch request - return { - requests: input.map((text) => ({ - model: geminiModel, - content: { parts: [{ text: String(text) }] } - })) - }; - } else { - // Single request - return { - model: geminiModel, - content: { parts: [{ text: String(input) }] } - }; - } - } - - // Default: OpenAI format - const body = { model, input }; - if (encodingFormat) { - body.encoding_format = encodingFormat; - } - return body; -} - -/** - * Build the URL for the embeddings endpoint based on the provider. - * @param {string} provider - * @param {string} model - * @param {object} credentials - * @param {string|string[]} input - used to select single vs batch endpoint for Gemini - */ -function buildEmbeddingsUrl(provider, model, credentials, input) { - if (isGeminiProvider(provider)) { - const apiKey = credentials.apiKey || credentials.accessToken; - // Normalize model name for URL path - const modelPath = model.startsWith("models/") ? model : `models/${model}`; - - if (Array.isArray(input)) { - // batchEmbedContents for array input (keeps response format consistent even for length=1) - return `https://generativelanguage.googleapis.com/v1beta/${modelPath}:batchEmbedContents?key=${encodeURIComponent(apiKey)}`; - } - return `https://generativelanguage.googleapis.com/v1beta/${modelPath}:embedContent?key=${encodeURIComponent(apiKey)}`; - } - - switch (provider) { - case "openai": - return "https://api.openai.com/v1/embeddings"; - case "openrouter": - return "https://openrouter.ai/api/v1/embeddings"; - default: - // openai-compatible providers: use their baseUrl + /embeddings - if (provider?.startsWith?.("openai-compatible-")) { - const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.openai.com/v1"; - return `${baseUrl.replace(/\/$/, "")}/embeddings`; - } - // For other providers, attempt to use their base URL pattern with /embeddings path - return null; - } -} - -/** - * Build headers for the embeddings request. - */ -function buildEmbeddingsHeaders(provider, credentials) { - const headers = { "Content-Type": "application/json" }; - - if (isGeminiProvider(provider)) { - // Gemini API uses API key as query param — no Authorization header needed - return headers; - } - - switch (provider) { - case "openai": - case "openrouter": - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - if (provider === "openrouter") { - headers["HTTP-Referer"] = "https://endpoint-proxy.local"; - headers["X-Title"] = "Endpoint Proxy"; - } - break; - default: - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - } - - return headers; -} - -/** - * Normalize the embeddings response to OpenAI format. - * - * Gemini single response: - * { embedding: { values: [0.1, 0.2, ...] } } - * - * Gemini batch response: - * { embeddings: [{ values: [...] }, ...] } - * - * Target OpenAI format: - * { object: "list", data: [{ object: "embedding", index: 0, embedding: [...] }], model, usage: {...} } - */ -function normalizeEmbeddingsResponse(responseBody, model, provider) { - // Already in OpenAI format - if (responseBody.object === "list" && Array.isArray(responseBody.data)) { - return responseBody; - } - - if (isGeminiProvider(provider)) { - let embeddingItems = []; - - if (Array.isArray(responseBody.embeddings)) { - // Batch response - embeddingItems = responseBody.embeddings.map((emb, idx) => ({ - object: "embedding", - index: idx, - embedding: emb.values || [] - })); - } else if (responseBody.embedding?.values) { - // Single response - embeddingItems = [{ - object: "embedding", - index: 0, - embedding: responseBody.embedding.values - }]; - } - - return { - object: "list", - data: embeddingItems, - model, - usage: { - prompt_tokens: 0, - total_tokens: 0 - } - }; - } - - // Try to handle alternate formats gracefully - return responseBody; -} - -/** - * Core embeddings handler — shared between Worker and SSE server. - * - * @param {object} options - * @param {object} options.body - Parsed request body { model, input, encoding_format } - * @param {object} options.modelInfo - { provider, model } - * @param {object} options.credentials - Provider credentials - * @param {object} [options.log] - Logger - * @param {function} [options.onCredentialsRefreshed] - Called when creds are refreshed - * @param {function} [options.onRequestSuccess] - Called on success (clear error state) - * @returns {Promise<{ success: boolean, response: Response, status?: number, error?: string }>} - */ -export async function handleEmbeddingsCore({ - body, - modelInfo, - credentials, - log, - onCredentialsRefreshed, - onRequestSuccess -}) { - const { provider, model } = modelInfo; - - // Validate input - const input = body.input; - if (!input) { - return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: input"); - } - if (typeof input !== "string" && !Array.isArray(input)) { - return createErrorResult(HTTP_STATUS.BAD_REQUEST, "input must be a string or array of strings"); - } - - const encodingFormat = body.encoding_format || "float"; - - // Determine embeddings URL - const url = buildEmbeddingsUrl(provider, model, credentials, input); - if (!url) { - return createErrorResult( - HTTP_STATUS.BAD_REQUEST, - `Provider '${provider}' does not support embeddings. Use openai, openrouter, gemini, or an openai-compatible provider.` - ); - } - - const headers = buildEmbeddingsHeaders(provider, credentials); - const requestBody = buildEmbeddingsBody(provider, model, input, encodingFormat); - - log?.debug?.("EMBEDDINGS", `${provider.toUpperCase()} | ${model} | input_type=${Array.isArray(input) ? `array[${input.length}]` : "string"}`); - - let providerResponse; - try { - providerResponse = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(requestBody) - }); - } catch (error) { - const errMsg = formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY); - log?.debug?.("EMBEDDINGS", `Fetch error: ${errMsg}`); - return createErrorResult(HTTP_STATUS.BAD_GATEWAY, errMsg); - } - - // Handle 401/403 — try token refresh - if ( - providerResponse.status === HTTP_STATUS.UNAUTHORIZED || - providerResponse.status === HTTP_STATUS.FORBIDDEN - ) { - const executor = getExecutor(provider); - const newCredentials = await refreshWithRetry( - () => executor.refreshCredentials(credentials, log), - 3, - log - ); - - if (newCredentials?.accessToken || newCredentials?.apiKey) { - log?.info?.("TOKEN", `${provider.toUpperCase()} | refreshed for embeddings`); - Object.assign(credentials, newCredentials); - if (onCredentialsRefreshed && newCredentials) { - await onCredentialsRefreshed(newCredentials); - } - - // Retry with refreshed credentials - try { - const retryHeaders = buildEmbeddingsHeaders(provider, credentials); - // Rebuild URL for Gemini since API key is embedded in query param - const retryUrl = isGeminiProvider(provider) - ? buildEmbeddingsUrl(provider, model, credentials, input) - : url; - - providerResponse = await fetch(retryUrl, { - method: "POST", - headers: retryHeaders, - body: JSON.stringify(requestBody) - }); - } catch (retryError) { - log?.warn?.("TOKEN", `${provider.toUpperCase()} | retry after refresh failed`); - } - } else { - log?.warn?.("TOKEN", `${provider.toUpperCase()} | refresh failed`); - } - } - - if (!providerResponse.ok) { - const { statusCode, message } = await parseUpstreamError(providerResponse, provider); - const errMsg = formatProviderError(new Error(message), provider, model, statusCode); - log?.debug?.("EMBEDDINGS", `Provider error: ${errMsg}`); - return createErrorResult(statusCode, errMsg); - } - - let responseBody; - try { - responseBody = await providerResponse.json(); - } catch (parseError) { - return createErrorResult(HTTP_STATUS.BAD_GATEWAY, `Invalid JSON response from ${provider}`); - } - - if (onRequestSuccess) { - await onRequestSuccess(); - } - - const normalized = normalizeEmbeddingsResponse(responseBody, model, provider); - - log?.debug?.("EMBEDDINGS", `Success | usage=${JSON.stringify(normalized.usage || {})}`); - - return { - success: true, - response: new Response(JSON.stringify(normalized), { - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }) - }; -} diff --git a/9router/open-sse/handlers/responsesHandler.js b/9router/open-sse/handlers/responsesHandler.js deleted file mode 100644 index 823ff8c3..00000000 --- a/9router/open-sse/handlers/responsesHandler.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Responses API Handler for Workers - * Converts Chat Completions to Codex Responses API format - */ - -import { handleChatCore } from "./chatCore.js"; -import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.js"; -import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.js"; -import { convertResponsesStreamToJson } from "../transformer/streamToJsonConverter.js"; - -/** - * Handle /v1/responses request - * @param {object} options - * @param {object} options.body - Request body (Responses API format) - * @param {object} options.modelInfo - { provider, model } - * @param {object} options.credentials - Provider credentials - * @param {object} options.log - Logger instance (optional) - * @param {function} options.onCredentialsRefreshed - Callback when credentials are refreshed - * @param {function} options.onRequestSuccess - Callback when request succeeds - * @param {function} options.onDisconnect - Callback when client disconnects - * @param {string} options.connectionId - Connection ID for usage tracking - * @returns {Promise<{success: boolean, response?: Response, status?: number, error?: string}>} - */ -export async function handleResponsesCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, connectionId }) { - // Convert Responses API format to Chat Completions format - const convertedBody = convertResponsesApiFormat(body); - - // Preserve client's stream preference (matches OpenClaw behavior) - // Default to false if omitted: Boolean(undefined) = false - const clientRequestedStreaming = convertedBody.stream === true; - if (convertedBody.stream === undefined) { - convertedBody.stream = false; - } - - // Call chat core handler — force sourceFormat so streaming path knows this is a Responses API client - const result = await handleChatCore({ - body: convertedBody, - modelInfo, - credentials, - log, - onCredentialsRefreshed, - onRequestSuccess, - onDisconnect, - connectionId, - sourceFormatOverride: "openai-responses" - }); - - if (!result.success || !result.response) { - return result; - } - - const response = result.response; - const contentType = response.headers.get("Content-Type") || ""; - - // Case 1: Client wants non-streaming, but got SSE (provider forced it, e.g., Codex) - if (!clientRequestedStreaming && contentType.includes("text/event-stream")) { - try { - const jsonResponse = await convertResponsesStreamToJson(response.body); - - return { - success: true, - response: new Response(JSON.stringify(jsonResponse), { - status: 200, - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-cache", - "Access-Control-Allow-Origin": "*" - } - }) - }; - } catch (error) { - console.error("[Responses API] Stream-to-JSON conversion failed:", error); - return { - success: false, - status: 500, - error: "Failed to convert streaming response to JSON" - }; - } - } - - // Case 2: Client wants streaming, got SSE - transform it - if (clientRequestedStreaming && contentType.includes("text/event-stream")) { - const transformStream = createResponsesApiTransformStream(null); - const transformedBody = response.body.pipeThrough(transformStream); - - return { - success: true, - response: new Response(transformedBody, { - status: 200, - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "Access-Control-Allow-Origin": "*" - } - }) - }; - } - - // Case 3: Non-SSE response (error or non-streaming from provider) - return as-is - return result; -} - diff --git a/9router/open-sse/index.js b/9router/open-sse/index.js deleted file mode 100644 index dde009c5..00000000 --- a/9router/open-sse/index.js +++ /dev/null @@ -1,74 +0,0 @@ -// Patch global fetch with proxy support (must be first) -import "./utils/proxyFetch.js"; - -// Config -export { PROVIDERS } from "./config/providers.js"; -export { OAUTH_ENDPOINTS, CLAUDE_SYSTEM_PROMPT } from "./config/appConstants.js"; -export { CACHE_TTL, DEFAULT_MAX_TOKENS, COOLDOWN_MS, BACKOFF_CONFIG } from "./config/runtimeConfig.js"; -export { - PROVIDER_MODELS, - getProviderModels, - getDefaultModel, - isValidModel, - findModelName, - getModelTargetFormat, - PROVIDER_ID_TO_ALIAS, - getModelsByProviderId -} from "./config/providerModels.js"; - -// Translator -export { FORMATS } from "./translator/formats.js"; -export { - register, - translateRequest, - translateResponse, - needsTranslation, - initState, - initTranslators -} from "./translator/index.js"; - -// Services -export { - detectFormat, - getProviderConfig, - buildProviderUrl, - buildProviderHeaders, - getTargetFormat -} from "./services/provider.js"; - -export { parseModel, resolveModelAliasFromMap, getModelInfoCore } from "./services/model.js"; - -export { - checkFallbackError, - isAccountUnavailable, - getUnavailableUntil, - filterAvailableAccounts -} from "./services/accountFallback.js"; - -export { - TOKEN_EXPIRY_BUFFER_MS, - refreshAccessToken, - refreshClaudeOAuthToken, - refreshGoogleToken, - refreshQwenToken, - refreshCodexToken, - refreshIflowToken, - refreshGitHubToken, - refreshCopilotToken, - getAccessToken, - refreshTokenByProvider -} from "./services/tokenRefresh.js"; - -// Handlers -export { handleChatCore, isTokenExpiringSoon } from "./handlers/chatCore.js"; -export { createStreamController, pipeWithDisconnect, createDisconnectAwareStream } from "./utils/streamHandler.js"; - -// Executors -export { getExecutor, hasSpecializedExecutor } from "./executors/index.js"; - -// Utils -export { errorResponse, formatProviderError } from "./utils/error.js"; -export { - createSSETransformStreamWithLogger, - createPassthroughStreamWithLogger -} from "./utils/stream.js"; diff --git a/9router/open-sse/services/accountFallback.js b/9router/open-sse/services/accountFallback.js deleted file mode 100644 index 573a5d64..00000000 --- a/9router/open-sse/services/accountFallback.js +++ /dev/null @@ -1,251 +0,0 @@ -import { COOLDOWN_MS, BACKOFF_CONFIG, HTTP_STATUS } from "../config/runtimeConfig.js"; - -/** - * Calculate exponential backoff cooldown for rate limits (429) - * Level 0: 1s, Level 1: 2s, Level 2: 4s... → max 2 min - * @param {number} backoffLevel - Current backoff level - * @returns {number} Cooldown in milliseconds - */ -export function getQuotaCooldown(backoffLevel = 0) { - const cooldown = BACKOFF_CONFIG.base * Math.pow(2, backoffLevel); - return Math.min(cooldown, BACKOFF_CONFIG.max); -} - -/** - * Check if error should trigger account fallback (switch to next account) - * @param {number} status - HTTP status code - * @param {string} errorText - Error message text - * @param {number} backoffLevel - Current backoff level for exponential backoff - * @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number }} - */ -export function checkFallbackError(status, errorText, backoffLevel = 0) { - // Check error message FIRST - specific patterns take priority over status codes - if (errorText) { - const errorStr = typeof errorText === "string" ? errorText : JSON.stringify(errorText); - const lowerError = errorStr.toLowerCase(); - - if (lowerError.includes("no credentials")) { - return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound }; - } - - if (lowerError.includes("request not allowed")) { - return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed }; - } - - // Rate limit keywords - exponential backoff - if ( - lowerError.includes("rate limit") || - lowerError.includes("too many requests") || - lowerError.includes("quota exceeded") || - lowerError.includes("capacity") || - lowerError.includes("overloaded") - ) { - const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel); - return { - shouldFallback: true, - cooldownMs: getQuotaCooldown(backoffLevel), - newBackoffLevel: newLevel - }; - } - } - - if (status === HTTP_STATUS.UNAUTHORIZED) { - return { shouldFallback: true, cooldownMs: COOLDOWN_MS.unauthorized }; - } - - if (status === HTTP_STATUS.PAYMENT_REQUIRED || status === HTTP_STATUS.FORBIDDEN) { - return { shouldFallback: true, cooldownMs: COOLDOWN_MS.paymentRequired }; - } - - if (status === HTTP_STATUS.NOT_FOUND) { - return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound }; - } - - // 429 - Rate limit with exponential backoff - if (status === HTTP_STATUS.RATE_LIMITED) { - const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel); - return { - shouldFallback: true, - cooldownMs: getQuotaCooldown(backoffLevel), - newBackoffLevel: newLevel - }; - } - - // Transient errors - const transientStatuses = [ - HTTP_STATUS.NOT_ACCEPTABLE, HTTP_STATUS.REQUEST_TIMEOUT, - HTTP_STATUS.SERVER_ERROR, HTTP_STATUS.BAD_GATEWAY, - HTTP_STATUS.SERVICE_UNAVAILABLE, HTTP_STATUS.GATEWAY_TIMEOUT - ]; - if (transientStatuses.includes(status)) { - return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient }; - } - - // All other errors - fallback with transient cooldown - return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient }; -} - -/** - * Check if account is currently unavailable (cooldown not expired) - */ -export function isAccountUnavailable(unavailableUntil) { - if (!unavailableUntil) return false; - return new Date(unavailableUntil).getTime() > Date.now(); -} - -/** - * Calculate unavailable until timestamp - */ -export function getUnavailableUntil(cooldownMs) { - return new Date(Date.now() + cooldownMs).toISOString(); -} - -/** - * Get the earliest rateLimitedUntil from a list of accounts - * @param {Array} accounts - Array of account objects with rateLimitedUntil - * @returns {string|null} Earliest rateLimitedUntil ISO string, or null - */ -export function getEarliestRateLimitedUntil(accounts) { - let earliest = null; - const now = Date.now(); - for (const acc of accounts) { - if (!acc.rateLimitedUntil) continue; - const until = new Date(acc.rateLimitedUntil).getTime(); - if (until <= now) continue; - if (!earliest || until < earliest) earliest = until; - } - if (!earliest) return null; - return new Date(earliest).toISOString(); -} - -/** - * Format rateLimitedUntil to human-readable "reset after Xm Ys" - * @param {string} rateLimitedUntil - ISO timestamp - * @returns {string} e.g. "reset after 2m 30s" - */ -export function formatRetryAfter(rateLimitedUntil) { - if (!rateLimitedUntil) return ""; - const diffMs = new Date(rateLimitedUntil).getTime() - Date.now(); - if (diffMs <= 0) return "reset after 0s"; - const totalSec = Math.ceil(diffMs / 1000); - const h = Math.floor(totalSec / 3600); - const m = Math.floor((totalSec % 3600) / 60); - const s = totalSec % 60; - const parts = []; - if (h > 0) parts.push(`${h}h`); - if (m > 0) parts.push(`${m}m`); - if (s > 0 || parts.length === 0) parts.push(`${s}s`); - return `reset after ${parts.join(" ")}`; -} - -/** Prefix for model lock flat fields on connection record */ -export const MODEL_LOCK_PREFIX = "modelLock_"; - -/** Special key used when no model is known (account-level lock) */ -export const MODEL_LOCK_ALL = `${MODEL_LOCK_PREFIX}__all`; - -/** Build the flat field key for a model lock */ -export function getModelLockKey(model) { - return model ? `${MODEL_LOCK_PREFIX}${model}` : MODEL_LOCK_ALL; -} - -/** - * Check if a model lock on a connection is still active. - * Reads flat field `modelLock_${model}` (or `modelLock___all` when model=null). - */ -export function isModelLockActive(connection, model) { - const key = getModelLockKey(model); - const expiry = connection[key] || connection[MODEL_LOCK_ALL]; - if (!expiry) return false; - return new Date(expiry).getTime() > Date.now(); -} - -/** - * Get earliest active model lock expiry across all modelLock_* fields. - * Used for UI cooldown display. - */ -export function getEarliestModelLockUntil(connection) { - if (!connection) return null; - let earliest = null; - const now = Date.now(); - for (const [key, val] of Object.entries(connection)) { - if (!key.startsWith(MODEL_LOCK_PREFIX) || !val) continue; - const t = new Date(val).getTime(); - if (t <= now) continue; - if (!earliest || t < earliest) earliest = t; - } - return earliest ? new Date(earliest).toISOString() : null; -} - -/** - * Build update object to set a model lock on a connection. - */ -export function buildModelLockUpdate(model, cooldownMs) { - const key = getModelLockKey(model); - return { [key]: new Date(Date.now() + cooldownMs).toISOString() }; -} - -/** - * Build update object to clear all model locks on a connection. - */ -export function buildClearModelLocksUpdate(connection) { - const cleared = {}; - for (const key of Object.keys(connection)) { - if (key.startsWith(MODEL_LOCK_PREFIX)) cleared[key] = null; - } - return cleared; -} - -/** - * Filter available accounts (not in cooldown) - */ -export function filterAvailableAccounts(accounts, excludeId = null) { - const now = Date.now(); - return accounts.filter(acc => { - if (excludeId && acc.id === excludeId) return false; - if (acc.rateLimitedUntil) { - const until = new Date(acc.rateLimitedUntil).getTime(); - if (until > now) return false; - } - return true; - }); -} - -/** - * Reset account state when request succeeds - * Clears cooldown and resets backoff level to 0 - * @param {object} account - Account object - * @returns {object} Updated account with reset state - */ -export function resetAccountState(account) { - if (!account) return account; - return { - ...account, - rateLimitedUntil: null, - backoffLevel: 0, - lastError: null, - status: "active" - }; -} - -/** - * Apply error state to account - * @param {object} account - Account object - * @param {number} status - HTTP status code - * @param {string} errorText - Error message - * @returns {object} Updated account with error state - */ -export function applyErrorState(account, status, errorText) { - if (!account) return account; - - const backoffLevel = account.backoffLevel || 0; - const { cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel); - - return { - ...account, - rateLimitedUntil: cooldownMs > 0 ? getUnavailableUntil(cooldownMs) : null, - backoffLevel: newBackoffLevel ?? backoffLevel, - lastError: { status, message: errorText, timestamp: new Date().toISOString() }, - status: "error" - }; -} diff --git a/9router/open-sse/services/combo.js b/9router/open-sse/services/combo.js deleted file mode 100644 index 1f134f3c..00000000 --- a/9router/open-sse/services/combo.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Shared combo (model combo) handling with fallback support - */ - -import { checkFallbackError, formatRetryAfter } from "./accountFallback.js"; -import { unavailableResponse } from "../utils/error.js"; - -/** - * Get combo models from combos data - * @param {string} modelStr - Model string to check - * @param {Array|Object} combosData - Array of combos or object with combos - * @returns {string[]|null} Array of models or null if not a combo - */ -export function getComboModelsFromData(modelStr, combosData) { - // Don't check if it's in provider/model format - if (modelStr.includes("/")) return null; - - // Handle both array and object formats - const combos = Array.isArray(combosData) ? combosData : (combosData?.combos || []); - - const combo = combos.find(c => c.name === modelStr); - if (combo && combo.models && combo.models.length > 0) { - return combo.models; - } - return null; -} - -/** - * Handle combo chat with fallback - * @param {Object} options - * @param {Object} options.body - Request body - * @param {string[]} options.models - Array of model strings to try - * @param {Function} options.handleSingleModel - Function to handle single model: (body, modelStr) => Promise - * @param {Object} options.log - Logger object - * @returns {Promise} - */ -export async function handleComboChat({ body, models, handleSingleModel, log }) { - let lastError = null; - let earliestRetryAfter = null; - let lastStatus = null; - - for (let i = 0; i < models.length; i++) { - const modelStr = models[i]; - log.info("COMBO", `Trying model ${i + 1}/${models.length}: ${modelStr}`); - - try { - const result = await handleSingleModel(body, modelStr); - - // Success (2xx) - return response - if (result.ok) { - log.info("COMBO", `Model ${modelStr} succeeded`); - return result; - } - - // Extract error info from response - let errorText = result.statusText || ""; - let retryAfter = null; - try { - const errorBody = await result.clone().json(); - errorText = errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText; - retryAfter = errorBody?.retryAfter || null; - } catch { - // Ignore JSON parse errors - } - - // Track earliest retryAfter across all combo models - if (retryAfter && (!earliestRetryAfter || new Date(retryAfter) < new Date(earliestRetryAfter))) { - earliestRetryAfter = retryAfter; - } - - // Normalize error text to string (Worker-safe) - if (typeof errorText !== "string") { - try { errorText = JSON.stringify(errorText); } catch { errorText = String(errorText); } - } - - // Check if should fallback to next model - const { shouldFallback } = checkFallbackError(result.status, errorText); - - if (!shouldFallback) { - log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status }); - return result; - } - - // Fallback to next model - lastError = errorText || String(result.status); - if (!lastStatus) lastStatus = result.status; - log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status }); - } catch (error) { - // Catch unexpected exceptions to ensure fallback continues - lastError = error.message || String(error); - if (!lastStatus) lastStatus = 500; - log.warn("COMBO", `Model ${modelStr} threw error, trying next`, { error: lastError }); - } - } - - // All models failed - const status = 406; - const msg = lastError || "All combo models unavailable"; - - if (earliestRetryAfter) { - const retryHuman = formatRetryAfter(earliestRetryAfter); - log.warn("COMBO", `All models failed | ${msg} (${retryHuman})`); - return unavailableResponse(status, msg, earliestRetryAfter, retryHuman); - } - - log.warn("COMBO", `All models failed | ${msg}`); - return new Response( - JSON.stringify({ error: { message: msg } }), - { status, headers: { "Content-Type": "application/json" } } - ); -} diff --git a/9router/open-sse/services/compact.js b/9router/open-sse/services/compact.js deleted file mode 100644 index 812cd27b..00000000 --- a/9router/open-sse/services/compact.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Shared combo (model combo) handling with fallback support - */ - -/** - * Get combo models from combos data - * @param {string} modelStr - Model string to check - * @param {Array|Object} combosData - Array of combos or object with combos - * @returns {string[]|null} Array of models or null if not a combo - */ -export function getComboModelsFromData(modelStr, combosData) { - // Don't check if it's in provider/model format - if (modelStr.includes("/")) return null; - - // Handle both array and object formats - const combos = Array.isArray(combosData) ? combosData : (combosData?.combos || []); - - const combo = combos.find(c => c.name === modelStr); - if (combo && combo.models && combo.models.length > 0) { - return combo.models; - } - return null; -} - -/** - * Handle combo chat with fallback - * @param {Object} options - * @param {Object} options.body - Request body - * @param {string[]} options.models - Array of model strings to try - * @param {Function} options.handleSingleModel - Function to handle single model: (body, modelStr) => Promise - * @param {Object} options.log - Logger object - * @returns {Promise} - */ -export async function handleComboChat({ body, models, handleSingleModel, log }) { - let lastError = null; - - for (let i = 0; i < models.length; i++) { - const modelStr = models[i]; - log.info("COMBO", `Trying model ${i + 1}/${models.length}: ${modelStr}`); - - let result; - try { - result = await handleSingleModel(body, modelStr); - } catch (e) { - lastError = `${modelStr}: ${e.message}`; - log.warn("COMBO", `Model threw exception, trying next`, { model: modelStr, error: e.message }); - continue; - } - - // Success or client error - return response - if (result.ok || result.status < 500) { - return result; - } - - // 5xx error - try next model - lastError = `${modelStr}: ${result.statusText || result.status}`; - log.warn("COMBO", `Model failed, trying next`, { model: modelStr, status: result.status }); - } - - log.warn("COMBO", "All models failed"); - - // Return 503 with last error - return new Response( - JSON.stringify({ error: lastError || "All combo models unavailable" }), - { - status: 503, - headers: { "Content-Type": "application/json" } - } - ); -} - diff --git a/9router/open-sse/services/model.js b/9router/open-sse/services/model.js deleted file mode 100644 index 119d7265..00000000 --- a/9router/open-sse/services/model.js +++ /dev/null @@ -1,169 +0,0 @@ -// Provider alias to ID mapping -const ALIAS_TO_PROVIDER_ID = { - cc: "claude", - cx: "codex", - gc: "gemini-cli", - qw: "qwen", - if: "iflow", - ag: "antigravity", - gh: "github", - kr: "kiro", - cu: "cursor", - kc: "kilocode", - kmc: "kimi-coding", - cl: "cline", - // API Key providers - openai: "openai", - anthropic: "anthropic", - gemini: "gemini", - openrouter: "openrouter", - glm: "glm", - kimi: "kimi", - minimax: "minimax", - "minimax-cn": "minimax-cn", - ds: "deepseek", - deepseek: "deepseek", - groq: "groq", - xai: "xai", - mistral: "mistral", - pplx: "perplexity", - perplexity: "perplexity", - together: "together", - fireworks: "fireworks", - cerebras: "cerebras", - cohere: "cohere", - nvidia: "nvidia", - nebius: "nebius", - siliconflow: "siliconflow", - hyp: "hyperbolic", - hyperbolic: "hyperbolic", - dg: "deepgram", - deepgram: "deepgram", - aai: "assemblyai", - assemblyai: "assemblyai", - nb: "nanobanana", - nanobanana: "nanobanana", - ch: "chutes", - chutes: "chutes", - cursor: "cursor", - vx: "vertex", - vertex: "vertex", - vxp: "vertex-partner", - "vertex-partner": "vertex-partner", -}; - -/** - * Resolve provider alias to provider ID - */ -export function resolveProviderAlias(aliasOrId) { - return ALIAS_TO_PROVIDER_ID[aliasOrId] || aliasOrId; -} - -/** - * Parse model string: "alias/model" or "provider/model" or just alias - */ -export function parseModel(modelStr) { - if (!modelStr) { - return { provider: null, model: null, isAlias: false, providerAlias: null }; - } - - // Check if standard format: provider/model or alias/model - if (modelStr.includes("/")) { - const firstSlash = modelStr.indexOf("/"); - const providerOrAlias = modelStr.slice(0, firstSlash); - const model = modelStr.slice(firstSlash + 1); - const provider = resolveProviderAlias(providerOrAlias); - return { provider, model, isAlias: false, providerAlias: providerOrAlias }; - } - - // Alias format (model alias, not provider alias) - return { - provider: null, - model: modelStr, - isAlias: true, - providerAlias: null, - }; -} - -/** - * Resolve model alias from aliases object - * Format: { "alias": "provider/model" } - */ -export function resolveModelAliasFromMap(alias, aliases) { - if (!aliases) return null; - - // Check if alias exists - const resolved = aliases[alias]; - if (!resolved) return null; - - // Resolved value is "provider/model" format - if (typeof resolved === "string" && resolved.includes("/")) { - const firstSlash = resolved.indexOf("/"); - const providerOrAlias = resolved.slice(0, firstSlash); - return { - provider: resolveProviderAlias(providerOrAlias), - model: resolved.slice(firstSlash + 1), - }; - } - - // Or object { provider, model } - if (typeof resolved === "object" && resolved.provider && resolved.model) { - return { - provider: resolveProviderAlias(resolved.provider), - model: resolved.model, - }; - } - - return null; -} - -/** - * Get full model info (parse or resolve) - * @param {string} modelStr - Model string - * @param {object|function} aliasesOrGetter - Aliases object or async function to get aliases - */ -export async function getModelInfoCore(modelStr, aliasesOrGetter) { - const parsed = parseModel(modelStr); - - if (!parsed.isAlias) { - return { - provider: parsed.provider, - model: parsed.model, - }; - } - - // Get aliases (from object or function) - const aliases = - typeof aliasesOrGetter === "function" - ? await aliasesOrGetter() - : aliasesOrGetter; - - // Resolve alias - const resolved = resolveModelAliasFromMap(parsed.model, aliases); - if (resolved) { - return resolved; - } - - // Fallback: infer provider from model name prefix - return { - provider: inferProviderFromModelName(parsed.model), - model: parsed.model, - }; -} - -/** - * Infer provider from model name prefix - * Used as fallback when no provider prefix or alias is given - */ -function inferProviderFromModelName(modelName) { - if (!modelName) return "openai"; - const m = modelName.toLowerCase(); - if (m.startsWith("claude-")) return "anthropic"; - if (m.startsWith("gemini-")) return "gemini"; - if (m.startsWith("gpt-")) return "openai"; - if (m.startsWith("o1") || m.startsWith("o3") || m.startsWith("o4")) - return "openai"; - if (m.startsWith("deepseek-")) return "openrouter"; - // Default fallback - return "openai"; -} diff --git a/9router/open-sse/services/projectId.js b/9router/open-sse/services/projectId.js deleted file mode 100644 index b7aa3002..00000000 --- a/9router/open-sse/services/projectId.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Project ID Service - Fetch and cache real Project IDs from Google Cloud Code API - * - * Reference: CLIProxyAPI internal/auth/antigravity/auth.go (FetchProjectID + OnboardUser) - * - * Instead of generating random project IDs (e.g. "useful-spark-a1b2c"), - * this service fetches the real Project ID bound to the authenticated user's account. - * This significantly reduces the risk of being flagged by Google's anti-abuse systems. - */ - -import { CLOUD_CODE_API, LOAD_CODE_ASSIST_HEADERS, LOAD_CODE_ASSIST_METADATA } from "../config/appConstants.js"; - -// ─── Cache ──────────────────────────────────────────────────────────────────── -// connectionId -> { projectId: string, fetchedAt: number } -const projectIdCache = new Map(); - -/** How long a cached project ID is considered fresh (1 hour). */ -const CACHE_TTL_MS = 60 * 60 * 1000; - -// ─── Pending-fetch deduplication ───────────────────────────────────────────── -// connectionId -> { promise: Promise, controller: AbortController, startedAt: number } -const pendingFetches = new Map(); - -/** Abort and evict a pending fetch that has been running longer than this (2 min). */ -const PENDING_TTL_MS = 2 * 60 * 1000; - -// ─── Periodic cleanup ──────────────────────────────────────────────────────── -/** How often the background sweep runs (10 min). */ -const CLEANUP_INTERVAL_MS = 10 * 60 * 1000; - -let _cleanupTimer = null; - -/** Run one sweep immediately: evict stale cache entries and abort orphaned pending fetches. */ -export function cleanupNow() { - const now = Date.now(); - - for (const [id, entry] of projectIdCache) { - if (!entry || now - entry.fetchedAt >= CACHE_TTL_MS) { - projectIdCache.delete(id); - } - } - - for (const [id, item] of pendingFetches) { - if (!item || typeof item.startedAt !== "number") { - pendingFetches.delete(id); - continue; - } - if (now - item.startedAt > PENDING_TTL_MS) { - try { item.controller.abort(); } catch (_) { /* ignore */ } - pendingFetches.delete(id); - } - } -} - -/** Start the periodic background cleanup (idempotent). Called automatically on module load. */ -export function startCacheCleanup() { - if (_cleanupTimer) return; - _cleanupTimer = setInterval(() => { - try { cleanupNow(); } catch (e) { - console.warn("[ProjectId] cleanup sweep error:", e?.message ?? e); - } - }, CLEANUP_INTERVAL_MS); - // Unref so the timer doesn't prevent Node from exiting when it is otherwise idle - _cleanupTimer?.unref?.(); -} - -/** Stop the periodic background cleanup (e.g. during graceful shutdown). */ -export function stopCacheCleanup() { - if (!_cleanupTimer) return; - clearInterval(_cleanupTimer); - _cleanupTimer = null; -} - -// Start automatically when the module is first imported -startCacheCleanup(); - -// ─── Public API ─────────────────────────────────────────────────────────────── - -/** - * Get the Project ID for a connection, with caching. - * Returns null on failure (callers should fall back to random generation). - * - * @param {string} connectionId - The connection identifier for cache keying - * @param {string} accessToken - Valid OAuth access token - * @returns {Promise} Real project ID or null - */ -export async function getProjectIdForConnection(connectionId, accessToken) { - if (!connectionId || !accessToken) return null; - - // Return cached value if still fresh - const cached = projectIdCache.get(connectionId); - if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { - return cached.projectId; - } - - // Deduplicate concurrent fetches for the same connection - if (pendingFetches.has(connectionId)) { - return pendingFetches.get(connectionId).promise; - } - - // Each fetch gets its own AbortController so it can be canceled via removeConnection() - const controller = new AbortController(); - - const promise = (async () => { - try { - const projectId = await fetchProjectId(accessToken, controller.signal); - if (projectId) { - projectIdCache.set(connectionId, {projectId, fetchedAt: Date.now()}); - return projectId; - } - console.warn("[ProjectId] could not fetch projectId for connection", connectionId.slice(0, 8)); - return null; - } catch (error) { - console.warn(`[ProjectId] Error fetching project ID: ${error.message}`); - return null; - } finally { - pendingFetches.delete(connectionId); - } - })(); - - pendingFetches.set(connectionId, {promise, controller, startedAt: Date.now()}); - return promise; -} - -/** - * Invalidate the cached project ID for a connection. - * Call this when a connection's credentials are fully revoked or refreshed. - */ -export function invalidateProjectId(connectionId) { - projectIdCache.delete(connectionId); -} - -/** - * Fully remove a connection: abort any in-flight fetch and delete its cached project ID. - * Wire this into your connection close / disconnect lifecycle events to prevent memory leaks. - * - * @param {string} connectionId - */ -export function removeConnection(connectionId) { - if (!connectionId) return; - projectIdCache.delete(connectionId); - const pending = pendingFetches.get(connectionId); - if (pending) { - try { pending.controller.abort(); } catch (_) { /* ignore */ } - pendingFetches.delete(connectionId); - } -} - -// ─── Internal helpers ───────────────────────────────────────────────────────── - -/** - * Fetch project ID via loadCodeAssist endpoint. - * Falls back to onboardUser when loadCodeAssist returns no project. - * - * @param {string} accessToken - * @param {AbortSignal} signal - * @returns {Promise} - */ -async function fetchProjectId(accessToken, signal) { - const response = await fetch(CLOUD_CODE_API.loadCodeAssist, { - method: "POST", - headers: { ...LOAD_CODE_ASSIST_HEADERS, "Authorization": `Bearer ${accessToken}` }, - body: JSON.stringify({ metadata: LOAD_CODE_ASSIST_METADATA }), - signal - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => ""); - throw new Error(`loadCodeAssist failed: HTTP ${response.status} ${errorText.slice(0, 200)}`); - } - - const data = await response.json(); - const projectId = extractProjectId(data); - if (projectId) return projectId; - - // Determine the tier to use for onboarding - let tierID = "legacy-tier"; - if (Array.isArray(data.allowedTiers)) { - for (const tier of data.allowedTiers) { - if (tier && typeof tier === "object" && tier.isDefault === true) { - if (tier.id && typeof tier.id === "string" && tier.id.trim()) { - tierID = tier.id.trim(); - break; - } - } - } - } - - return onboardUser(accessToken, tierID, signal); -} - -/** - * Fetch project ID via onboardUser endpoint (polls until done). - * - * @param {string} accessToken - * @param {string} tierID - * @param {AbortSignal} externalSignal – propagated from the connection's AbortController - * @returns {Promise} - */ -async function onboardUser(accessToken, tierID, externalSignal) { - console.log(`[ProjectId] Onboarding user with tier: ${tierID}`); - - const reqBody = { tierId: tierID, metadata: LOAD_CODE_ASSIST_METADATA }; - const MAX_ATTEMPTS = 5; - - for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { - // Bail out immediately if the connection was removed - if (externalSignal?.aborted) return null; - - // Per-attempt timeout controller; forwards external abort as well - const localCtrl = new AbortController(); - const timeoutId = setTimeout(() => localCtrl.abort(), 30_000); - const forwardAbort = () => localCtrl.abort(); - externalSignal?.addEventListener("abort", forwardAbort); - - try { - const response = await fetch(CLOUD_CODE_API.onboardUser, { - method: "POST", - headers: { ...LOAD_CODE_ASSIST_HEADERS, "Authorization": `Bearer ${accessToken}` }, - body: JSON.stringify(reqBody), - signal: localCtrl.signal - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const errorText = await response.text().catch(() => ""); - throw new Error(`onboardUser HTTP ${response.status}: ${errorText.slice(0, 200)}`); - } - - const data = await response.json(); - - if (data.done === true) { - const projectId = extractProjectIdFromOnboard(data); - if (projectId) { - console.log(`[ProjectId] Successfully onboarded, project ID: ${projectId}`); - return projectId; - } - throw new Error("onboardUser done but no project_id in response"); - } - - // Server not done yet – wait and retry - console.log(`[ProjectId] Onboard attempt ${attempt}/${MAX_ATTEMPTS}: not done yet, waiting...`); - await new Promise(resolve => setTimeout(resolve, 2000)); - - } catch (error) { - clearTimeout(timeoutId); - if (error.name === "AbortError") { - console.warn(`[ProjectId] onboardUser attempt ${attempt} aborted (timeout or connection removed)`); - if (externalSignal?.aborted) return null; // connection gone – stop retrying - continue; - } - if (attempt === MAX_ATTEMPTS) { - console.warn(`[ProjectId] onboardUser failed after ${MAX_ATTEMPTS} attempts: ${error.message}`); - return null; - } - // Continue to next attempt instead of throwing (which would skip remaining retries) - console.warn(`[ProjectId] onboardUser attempt ${attempt} failed: ${error.message}, retrying...`); - await new Promise(resolve => setTimeout(resolve, 2000)); - } finally { - clearTimeout(timeoutId); - externalSignal?.removeEventListener("abort", forwardAbort); - } - } - - return null; -} - -/** - * Extract project ID from loadCodeAssist response. - */ -function extractProjectId(data) { - if (!data) return null; - - if (typeof data.cloudaicompanionProject === "string") { - const id = data.cloudaicompanionProject.trim(); - if (id) return id; - } - - if (data.cloudaicompanionProject && typeof data.cloudaicompanionProject === "object") { - const id = data.cloudaicompanionProject.id; - if (typeof id === "string" && id.trim()) return id.trim(); - } - - return null; -} - -/** - * Extract project ID from onboardUser response. - */ -function extractProjectIdFromOnboard(data) { - if (!data?.response) return null; - - const project = data.response.cloudaicompanionProject; - - if (typeof project === "string") { - const id = project.trim(); - if (id) return id; - } - - if (project && typeof project === "object") { - const id = project.id; - if (typeof id === "string" && id.trim()) return id.trim(); - } - - return null; -} diff --git a/9router/open-sse/services/provider.js b/9router/open-sse/services/provider.js deleted file mode 100644 index 286ed441..00000000 --- a/9router/open-sse/services/provider.js +++ /dev/null @@ -1,356 +0,0 @@ -import { PROVIDERS } from "../config/providers.js"; -import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js"; - -const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-"; -const OPENAI_COMPATIBLE_DEFAULTS = { - baseUrl: "https://api.openai.com/v1", -}; - -const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-"; -const ANTHROPIC_COMPATIBLE_DEFAULTS = { - baseUrl: "https://api.anthropic.com/v1", -}; - -function isOpenAICompatible(provider) { - return typeof provider === "string" && provider.startsWith(OPENAI_COMPATIBLE_PREFIX); -} - -function isAnthropicCompatible(provider) { - return typeof provider === "string" && provider.startsWith(ANTHROPIC_COMPATIBLE_PREFIX); -} - -function getOpenAICompatibleType(provider) { - if (!isOpenAICompatible(provider)) return "chat"; - return provider.includes("responses") ? "responses" : "chat"; -} - -function buildOpenAICompatibleUrl(baseUrl, apiType) { - const normalized = baseUrl.replace(/\/$/, ""); - const path = apiType === "responses" ? "/responses" : "/chat/completions"; - return `${normalized}${path}`; -} - -function buildAnthropicCompatibleUrl(baseUrl) { - const normalized = baseUrl.replace(/\/$/, ""); - return `${normalized}/messages`; -} - -function buildQwenBaseUrl(resourceUrl, fallbackBaseUrl) { - const fallback = (fallbackBaseUrl || "").replace(/\/chat\/completions$/, ""); - const raw = typeof resourceUrl === "string" ? resourceUrl.trim() : ""; - if (!raw) return fallback; - if (raw.startsWith("http://") || raw.startsWith("https://")) { - return raw.replace(/\/$/, ""); - } - return `https://${raw.replace(/\/$/, "")}/v1`; -} - -// Detect request format from body structure -export function detectFormat(body) { - // OpenAI Responses API: has input (array or string) instead of messages[] - // The Responses API accepts both input as array and input as a plain string - if (body.input && (Array.isArray(body.input) || typeof body.input === "string") && !body.messages) { - return "openai-responses"; - } - - // Antigravity format: Gemini wrapped in body.request - if (body.request?.contents && body.userAgent === "antigravity") { - return "antigravity"; - } - - // Gemini format: has contents array - if (body.contents && Array.isArray(body.contents)) { - return "gemini"; - } - - // OpenAI-specific indicators (check BEFORE Claude) - // These fields are OpenAI-specific and never appear in Claude format - if ( - body.stream_options || // OpenAI streaming options - body.response_format || // JSON mode, etc. - body.logprobs !== undefined || // Log probabilities - body.top_logprobs !== undefined || - body.n !== undefined || // Number of completions - body.presence_penalty !== undefined || // Penalties - body.frequency_penalty !== undefined || - body.logit_bias || // Token biasing - body.user // User identifier - ) { - return "openai"; - } - - // Claude format: messages with content as array of objects with type - // Claude requires content to be array with specific structure - if (body.messages && Array.isArray(body.messages)) { - const firstMsg = body.messages[0]; - - // If content is array, check if it follows Claude structure - if (firstMsg?.content && Array.isArray(firstMsg.content)) { - const firstContent = firstMsg.content[0]; - - // Claude format has specific types: text, image, tool_use, tool_result - // OpenAI multimodal has: text, image_url (note the difference) - if (firstContent?.type === "text" && !body.model?.includes("/")) { - // Could be Claude or OpenAI multimodal - // Check for Claude-specific fields - if (body.system || body.anthropic_version) { - return "claude"; - } - // Check if image format is Claude (source.type) vs OpenAI (image_url.url) - const hasClaudeImage = firstMsg.content.some(c => - c.type === "image" && c.source?.type === "base64" - ); - const hasOpenAIImage = firstMsg.content.some(c => - c.type === "image_url" && c.image_url?.url - ); - if (hasClaudeImage) return "claude"; - if (hasOpenAIImage) return "openai"; - - // If still unclear, check for tool format - const hasClaudeTool = firstMsg.content.some(c => - c.type === "tool_use" || c.type === "tool_result" - ); - if (hasClaudeTool) return "claude"; - } - } - - // If content is string, it's likely OpenAI (Claude also supports this) - // Check for other Claude-specific indicators - if (body.system !== undefined || body.anthropic_version) { - return "claude"; - } - } - - // Default to OpenAI format - return "openai"; -} - -// Get provider config -export function getProviderConfig(provider) { - if (isOpenAICompatible(provider)) { - const apiType = getOpenAICompatibleType(provider); - return { - ...PROVIDERS.openai, - format: apiType === "responses" ? "openai-responses" : "openai", - baseUrl: OPENAI_COMPATIBLE_DEFAULTS.baseUrl, - }; - } - if (isAnthropicCompatible(provider)) { - return { - ...PROVIDERS.anthropic, // Use Anthropic defaults (header: x-api-key) - format: "claude", - baseUrl: ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl, - }; - } - return PROVIDERS[provider] || PROVIDERS.openai; -} - -// Get number of fallback URLs for provider (for retry logic) -export function getProviderFallbackCount(provider) { - const config = getProviderConfig(provider); - return config.baseUrls?.length || 1; -} - -// Build provider URL -export function buildProviderUrl(provider, model, stream = true, options = {}) { - if (isOpenAICompatible(provider)) { - const apiType = getOpenAICompatibleType(provider); - const baseUrl = options?.baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl; - return buildOpenAICompatibleUrl(baseUrl, apiType); - } - if (isAnthropicCompatible(provider)) { - const baseUrl = options?.baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl; - return buildAnthropicCompatibleUrl(baseUrl); - } - const config = getProviderConfig(provider); - - switch (provider) { - case "claude": - return `${config.baseUrl}?beta=true`; - - case "gemini": { - const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; - return `${config.baseUrl}/${model}:${action}`; - } - - case "gemini-cli": { - const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; - return `${config.baseUrl}:${action}`; - } - - case "antigravity": { - // Use baseUrlIndex from options or default to 0 - const urlIndex = options?.baseUrlIndex || 0; - const baseUrl = config.baseUrls[urlIndex] || config.baseUrls[0]; - const path = stream ? "/v1internal:streamGenerateContent?alt=sse" : "/v1internal:generateContent"; - return `${baseUrl}${path}`; - } - - case "codex": - return config.baseUrl; - - case "qwen": { - const baseUrl = buildQwenBaseUrl(options?.qwenResourceUrl, config.baseUrl); - return `${baseUrl}/chat/completions`; - } - - case "github": - return config.baseUrl; - - case "glm": - case "kimi": - case "minimax": - // Claude-compatible providers - return `${config.baseUrl}?beta=true`; - - default: - return config.baseUrl; - } -} - -// Build provider headers -export function buildProviderHeaders(provider, credentials, stream = true, body = null) { - const config = getProviderConfig(provider); - const headers = { - "Content-Type": "application/json", - ...config.headers - }; - - // Add auth header - // Specific override for Anthropic Compatible - if (isAnthropicCompatible(provider)) { - if (credentials.apiKey) { - headers["x-api-key"] = credentials.apiKey; - // Do NOT send Authorization header when apiKey is present for Anthropic Compatible - // as it causes issues with some providers (e.g. opencode.ai) - } else if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - // Add default Anthropic version if not present (some proxies require it) - if (!headers["anthropic-version"]) { - headers["anthropic-version"] = "2023-06-01"; - } - } else { - switch (provider) { - case "gemini": - if (credentials.apiKey) { - headers["x-goog-api-key"] = credentials.apiKey; - } else if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - break; - - case "antigravity": - case "gemini-cli": - // Antigravity and Gemini CLI use OAuth access token - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - break; - - case "claude": - // Claude uses x-api-key header for API key, or Authorization for OAuth - if (credentials.apiKey) { - headers["x-api-key"] = credentials.apiKey; - } else if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - break; - - case "github": { - // GitHub Copilot requires special headers to mimic VSCode - // Prioritize copilotToken from providerSpecificData, fallback to accessToken - const githubToken = credentials.copilotToken || credentials.accessToken; - // Add headers in exact same order as test endpoint - headers["Authorization"] = `Bearer ${githubToken}`; - headers["Content-Type"] = "application/json"; - headers["copilot-integration-id"] = "vscode-chat"; - headers["editor-version"] = "vscode/1.107.1"; - headers["editor-plugin-version"] = "copilot-chat/0.26.7"; - headers["user-agent"] = "GitHubCopilotChat/0.26.7"; - headers["openai-intent"] = "conversation-panel"; - headers["x-github-api-version"] = "2025-04-01"; - // Generate a UUID for x-request-id (Cloudflare Workers compatible) - headers["x-request-id"] = crypto.randomUUID ? crypto.randomUUID() : - 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - headers["x-vscode-user-agent-library-version"] = "electron-fetch"; - headers["X-Initiator"] = "user"; - headers["Accept"] = "application/json"; - break; - } - - case "codex": - case "qwen": - case "openai": - case "openrouter": - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - break; - - case "cline": - Object.assign(headers, buildClineHeaders(credentials.apiKey || credentials.accessToken)); - break; - - case "glm": - case "kimi": - case "minimax": - // Claude-compatible API providers use x-api-key - headers["x-api-key"] = credentials.apiKey; - break; - - case "vertex": - case "vertex-partner": - // Vertex uses async token minting — headers are set by VertexExecutor._buildHeadersAsync() - // Do NOT set Authorization here; it would leak the raw SA JSON as Bearer token - break; - - default: - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - break; - } - } - - // Stream accept header - if (stream) { - headers["Accept"] = "text/event-stream"; - } - - return headers; -} - -// Get target format for provider -export function getTargetFormat(provider) { - if (isOpenAICompatible(provider)) { - return getOpenAICompatibleType(provider) === "responses" ? "openai-responses" : "openai"; - } - if (isAnthropicCompatible(provider)) { - return "claude"; - } - const config = getProviderConfig(provider); - return config.format || "openai"; -} - -// Check if last message is from user -export function isLastMessageFromUser(body) { - const messages = body.messages || body.contents; - if (!messages?.length) return true; - const lastMsg = messages[messages.length - 1]; - return lastMsg?.role === "user"; -} - -// Check if request has thinking config -export function hasThinkingConfig(body) { - return !!(body.reasoning_effort || body.thinking?.type === "enabled"); -} - -// Normalize thinking config based on last message role -// - If lastMessage is not user → remove thinking config -// - If lastMessage is user AND has thinking config → keep it (force enable) -export function normalizeThinkingConfig(body) { - if (!isLastMessageFromUser(body)) { - delete body.reasoning_effort; - delete body.thinking; - } - return body; -} diff --git a/9router/open-sse/services/tokenRefresh.js b/9router/open-sse/services/tokenRefresh.js deleted file mode 100644 index 60423f75..00000000 --- a/9router/open-sse/services/tokenRefresh.js +++ /dev/null @@ -1,731 +0,0 @@ -import { PROVIDERS } from "../config/providers.js"; -import { OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../config/appConstants.js"; - -// Token expiry buffer (refresh if expires within 5 minutes) -export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; - -/** - * Refresh OAuth access token using refresh token - */ -export async function refreshAccessToken(provider, refreshToken, credentials, log) { - const config = PROVIDERS[provider]; - - if (!config || !config.refreshUrl) { - log?.warn?.("TOKEN_REFRESH", `No refresh URL configured for provider: ${provider}`); - return null; - } - - if (!refreshToken) { - log?.warn?.("TOKEN_REFRESH", `No refresh token available for provider: ${provider}`); - return null; - } - - try { - const response = await fetch(config.refreshUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: config.clientId, - client_secret: config.clientSecret, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", `Failed to refresh token for ${provider}`, { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", `Successfully refreshed token for ${provider}`, { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Error refreshing token for ${provider}`, { - error: error.message, - }); - return null; - } -} - -/** - * Specialized refresh for Claude OAuth tokens - */ -export async function refreshClaudeOAuthToken(refreshToken, log) { - try { - const response = await fetch(OAUTH_ENDPOINTS.anthropic.token, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.claude.clientId, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Claude OAuth token", { status: response.status, error: errorText }); - return null; - } - - const tokens = await response.json(); - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Claude OAuth token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Network error refreshing Claude token: ${error.message}`); - return null; - } -} - -/** - * Specialized refresh for Google providers (Gemini, Antigravity) - */ -export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) { - try { - const response = await fetch(OAUTH_ENDPOINTS.google.token, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: clientId, - client_secret: clientSecret, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Google token", { status: response.status, error: errorText }); - return null; - } - - const tokens = await response.json(); - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Google token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); - return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Network error refreshing Google token: ${error.message}`); - return null; - } -} - -/** - * Specialized refresh for Qwen OAuth tokens - */ -export async function refreshQwenToken(refreshToken, log) { - const endpoint = OAUTH_ENDPOINTS.qwen.token; - - try { - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.qwen.clientId, - }), - }); - - if (response.status === 200) { - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Qwen token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - providerSpecificData: tokens.resource_url - ? { resourceUrl: tokens.resource_url } - : undefined, - }; - } else { - const errorText = await response.text().catch(() => ""); - log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, { - status: response.status, - error: errorText, - }); - } - } catch (error) { - log?.warn?.("TOKEN_REFRESH", `Network error trying Qwen endpoint`, { - error: error.message, - }); - } - - log?.error?.("TOKEN_REFRESH", "Failed to refresh Qwen token"); - return null; -} - -/** - * Specialized refresh for Codex (OpenAI) OAuth tokens - */ -export async function refreshCodexToken(refreshToken, log) { - try { - const response = await fetch(OAUTH_ENDPOINTS.openai.token, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.codex.clientId, - scope: "openid profile email offline_access", - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`); - return null; - } -} - -/** - * Specialized refresh for Kiro (AWS CodeWhisperer) tokens - * Supports both AWS SSO OIDC (Builder ID/IDC) and Social Auth (Google/GitHub) - */ -export async function refreshKiroToken(refreshToken, providerSpecificData, log) { - const authMethod = providerSpecificData?.authMethod; - const clientId = providerSpecificData?.clientId; - const clientSecret = providerSpecificData?.clientSecret; - const region = providerSpecificData?.region; - - // AWS SSO OIDC (Builder ID or IDC) - // If clientId and clientSecret exist, assume AWS SSO OIDC (default to builder-id if authMethod not specified) - if (clientId && clientSecret) { - const isIDC = authMethod === "idc"; - const endpoint = isIDC && region - ? `https://oidc.${region}.amazonaws.com/token` - : "https://oidc.us-east-1.amazonaws.com/token"; - - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - clientId: clientId, - clientSecret: clientSecret, - refreshToken: refreshToken, - grantType: "refresh_token", - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro AWS token", { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro AWS token", { - hasNewAccessToken: !!tokens.accessToken, - expiresIn: tokens.expiresIn, - }); - - return { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken || refreshToken, - expiresIn: tokens.expiresIn, - }; - } - - // Social Auth (Google/GitHub) - use Kiro's refresh endpoint - const response = await fetch(PROVIDERS.kiro.tokenUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - "User-Agent": "kiro-cli/1.0.0", - }, - body: JSON.stringify({ - refreshToken: refreshToken, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro social token", { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro social token", { - hasNewAccessToken: !!tokens.accessToken, - expiresIn: tokens.expiresIn, - }); - - return { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken || refreshToken, - expiresIn: tokens.expiresIn, - }; -} - -/** - * Specialized refresh for iFlow OAuth tokens - */ -export async function refreshIflowToken(refreshToken, log) { - const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`); - - const response = await fetch(OAUTH_ENDPOINTS.iflow.token, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - Authorization: `Basic ${basicAuth}`, - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.iflow.clientId, - client_secret: PROVIDERS.iflow.clientSecret, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh iFlow token", { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed iFlow token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - }; -} - -/** - * Specialized refresh for GitHub Copilot OAuth tokens - */ -export async function refreshGitHubToken(refreshToken, log) { - const response = await fetch(OAUTH_ENDPOINTS.github.token, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.github.clientId, - client_secret: PROVIDERS.github.clientSecret, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh GitHub token", { - status: response.status, - error: errorText, - }); - return null; - } - - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed GitHub token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - }; -} - -/** - * Refresh GitHub Copilot token using GitHub access token - */ -export async function refreshCopilotToken(githubAccessToken, log) { - try { - const response = await fetch("https://api.github.com/copilot_internal/v2/token", { - headers: { - "Authorization": `token ${githubAccessToken}`, - "User-Agent": GITHUB_COPILOT.USER_AGENT, - "Editor-Version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`, - "Editor-Plugin-Version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`, - "Accept": "application/json", - "x-github-api-version": GITHUB_COPILOT.API_VERSION - } - }); - - if (!response.ok) { - const errorText = await response.text(); - log?.error?.("TOKEN_REFRESH", "Failed to refresh Copilot token", { - status: response.status, - error: errorText - }); - return null; - } - - const data = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Copilot token", { - hasToken: !!data.token, - expiresAt: data.expires_at - }); - - return { - token: data.token, - expiresAt: data.expires_at - }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", "Error refreshing Copilot token", { - error: error.message - }); - return null; - } -} - -/** - * Get access token for a specific provider - */ -export async function getAccessToken(provider, credentials, log) { - if (!credentials || !credentials.refreshToken) { - log?.warn?.("TOKEN_REFRESH", `No refresh token available for provider: ${provider}`); - return null; - } - - switch (provider) { - case "gemini": - case "gemini-cli": - case "antigravity": - return await refreshGoogleToken( - credentials.refreshToken, - PROVIDERS[provider].clientId, - PROVIDERS[provider].clientSecret, - log - ); - - case "claude": - return await refreshClaudeOAuthToken(credentials.refreshToken, log); - - case "codex": - return await refreshCodexToken(credentials.refreshToken, log); - - case "qwen": - return await refreshQwenToken(credentials.refreshToken, log); - - case "iflow": - return await refreshIflowToken(credentials.refreshToken, log); - - case "github": - return await refreshGitHubToken(credentials.refreshToken, log); - - case "kiro": - return await refreshKiroToken( - credentials.refreshToken, - credentials.providerSpecificData, - log - ); - - case "vertex": - case "vertex-partner": { - const saJson = parseVertexSaJson(credentials.apiKey); - if (!saJson) return null; - return await refreshVertexToken(saJson, log); - } - - default: - log?.warn?.("TOKEN_REFRESH", `Unsupported provider for token refresh: ${provider}`); - return null; - } -} - -/** - * Refresh token by provider type (helper for handlers) - */ -export async function refreshTokenByProvider(provider, credentials, log) { - if (!credentials.refreshToken) return null; - - switch (provider) { - case "gemini-cli": - case "antigravity": - return refreshGoogleToken( - credentials.refreshToken, - PROVIDERS[provider].clientId, - PROVIDERS[provider].clientSecret, - log - ); - case "claude": - return refreshClaudeOAuthToken(credentials.refreshToken, log); - case "codex": - return refreshCodexToken(credentials.refreshToken, log); - case "qwen": - return refreshQwenToken(credentials.refreshToken, log); - case "iflow": - return refreshIflowToken(credentials.refreshToken, log); - case "github": - return refreshGitHubToken(credentials.refreshToken, log); - case "kiro": - return refreshKiroToken( - credentials.refreshToken, - credentials.providerSpecificData, - log - ); - case "vertex": - case "vertex-partner": { - const saJson = parseVertexSaJson(credentials.apiKey); - if (!saJson) return null; - return refreshVertexToken(saJson, log); - } - default: - return refreshAccessToken(provider, credentials.refreshToken, credentials, log); - } -} - -/** - * Format credentials for provider - */ -export function formatProviderCredentials(provider, credentials, log) { - const config = PROVIDERS[provider]; - if (!config) { - log?.warn?.("TOKEN_REFRESH", `No configuration found for provider: ${provider}`); - return null; - } - - switch (provider) { - case "gemini": - return { - apiKey: credentials.apiKey, - accessToken: credentials.accessToken, - projectId: credentials.projectId - }; - - case "claude": - return { - apiKey: credentials.apiKey, - accessToken: credentials.accessToken - }; - - case "codex": - case "qwen": - case "iflow": - case "openai": - case "openrouter": - return { - apiKey: credentials.apiKey, - accessToken: credentials.accessToken - }; - - case "antigravity": - case "gemini-cli": - return { - accessToken: credentials.accessToken, - refreshToken: credentials.refreshToken, - projectId: credentials.projectId - }; - - default: - return { - apiKey: credentials.apiKey, - accessToken: credentials.accessToken, - refreshToken: credentials.refreshToken - }; - } -} - -/** - * Get all access tokens for a user - */ -export async function getAllAccessTokens(userInfo, log) { - const results = {}; - - if (userInfo.connections && Array.isArray(userInfo.connections)) { - for (const connection of userInfo.connections) { - if (connection.isActive && connection.provider) { - const token = await getAccessToken(connection.provider, { - refreshToken: connection.refreshToken - }, log); - - if (token) { - results[connection.provider] = token; - } - } - } - } - - return results; -} - -/** - * Parse Vertex AI Service Account JSON from apiKey string - */ -export function parseVertexSaJson(apiKey) { - if (typeof apiKey !== "string") return null; - try { - const parsed = JSON.parse(apiKey); - if (parsed.type === "service_account" && parsed.client_email && parsed.private_key && parsed.project_id) { - return parsed; - } - return null; - } catch { - return null; - } -} - -// Cache Vertex tokens keyed by service account email { token, expiresAt } -const vertexTokenCache = new Map(); - -/** - * Mint a short-lived OAuth2 Bearer token for Google Cloud Vertex AI - * using Service Account JSON + jose (RS256 JWT assertion flow). - * Token is cached until 5 minutes before expiry. - */ -export async function refreshVertexToken(saJson, log) { - const cacheKey = saJson.client_email; - const cached = vertexTokenCache.get(cacheKey); - - // Return cached token if still valid (5-min buffer) - if (cached && cached.expiresAt - Date.now() > 5 * 60 * 1000) { - return { accessToken: cached.token, expiresAt: cached.expiresAt }; - } - - try { - const { SignJWT, importPKCS8 } = await import("jose"); - log?.debug?.("TOKEN_REFRESH", `Vertex minting token for ${saJson.client_email}`); - const privateKey = await importPKCS8(saJson.private_key.replace(/\\n/g, "\n"), "RS256"); - const now = Math.floor(Date.now() / 1000); - - const jwt = await new SignJWT({ scope: "https://www.googleapis.com/auth/cloud-platform" }) - .setProtectedHeader({ alg: "RS256" }) - .setIssuer(saJson.client_email) - .setAudience("https://oauth2.googleapis.com/token") - .setIssuedAt(now) - .setExpirationTime(now + 3600) - .sign(privateKey); - - const res = await fetch("https://oauth2.googleapis.com/token", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: jwt, - }), - }); - - if (!res.ok) { - const err = await res.text(); - log?.error?.("TOKEN_REFRESH", `Vertex token mint failed: ${err}`); - return null; - } - - const { access_token, expires_in } = await res.json(); - const expiresAt = Date.now() + (expires_in ?? 3600) * 1000; - - vertexTokenCache.set(cacheKey, { token: access_token, expiresAt }); - log?.info?.("TOKEN_REFRESH", `Vertex token minted for ${saJson.client_email}`); - - return { accessToken: access_token, expiresAt }; - } catch (error) { - log?.error?.("TOKEN_REFRESH", `Vertex token error: ${error.message}`); - return null; - } -} - -/** - * Refresh token with retry and exponential backoff - * Retries on failure with increasing delay: 1s, 2s, 3s... - * @param {function} refreshFn - Async function that returns token or null - * @param {number} maxRetries - Max retry attempts (default 3) - * @param {object} log - Logger instance (optional) - * @returns {Promise} Token result or null if all retries fail - */ -export async function refreshWithRetry(refreshFn, maxRetries = 3, log = null) { - for (let attempt = 0; attempt < maxRetries; attempt++) { - if (attempt > 0) { - const delay = attempt * 1000; - log?.debug?.("TOKEN_REFRESH", `Retry ${attempt}/${maxRetries} after ${delay}ms`); - await new Promise(r => setTimeout(r, delay)); - } - - try { - const result = await refreshFn(); - if (result) return result; - } catch (error) { - log?.warn?.("TOKEN_REFRESH", `Attempt ${attempt + 1}/${maxRetries} failed: ${error.message}`); - } - } - - log?.error?.("TOKEN_REFRESH", `All ${maxRetries} retry attempts failed`); - return null; -} - diff --git a/9router/open-sse/services/usage.js b/9router/open-sse/services/usage.js deleted file mode 100644 index bf0c28ab..00000000 --- a/9router/open-sse/services/usage.js +++ /dev/null @@ -1,708 +0,0 @@ -/** - * Usage Fetcher - Get usage data from provider APIs - */ - -import { CLIENT_METADATA, getPlatformUserAgent } from "../config/appConstants.js"; - -// GitHub API config -const GITHUB_CONFIG = { - apiVersion: "2022-11-28", - userAgent: "GitHubCopilotChat/0.26.7", -}; - -// Antigravity API config (from Quotio) -const ANTIGRAVITY_CONFIG = { - quotaApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels", - loadProjectApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", - tokenUrl: "https://oauth2.googleapis.com/token", - clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com", - clientSecret: "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", - userAgent: getPlatformUserAgent(), -}; - -// Codex (OpenAI) API config -const CODEX_CONFIG = { - usageUrl: "https://chatgpt.com/backend-api/wham/usage", -}; - -// Claude API config -const CLAUDE_CONFIG = { - oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage", - usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage", - settingsUrl: "https://api.anthropic.com/v1/settings", - apiVersion: "2023-06-01", -}; - -/** - * Get usage data for a provider connection - * @param {Object} connection - Provider connection with accessToken - * @returns {Object} Usage data with quotas - */ -export async function getUsageForProvider(connection) { - const { provider, accessToken, providerSpecificData } = connection; - - switch (provider) { - case "github": - return await getGitHubUsage(accessToken, providerSpecificData); - case "gemini-cli": - return await getGeminiUsage(accessToken); - case "antigravity": - return await getAntigravityUsage(accessToken); - case "claude": - return await getClaudeUsage(accessToken); - case "codex": - return await getCodexUsage(accessToken); - case "kiro": - return await getKiroUsage(accessToken, providerSpecificData); - case "qwen": - return await getQwenUsage(accessToken, providerSpecificData); - case "iflow": - return await getIflowUsage(accessToken); - default: - return { message: `Usage API not implemented for ${provider}` }; - } -} - -/** - * Parse reset date/time to ISO string - * Handles multiple formats: Unix timestamp (ms), ISO date string, etc. - */ -function parseResetTime(resetValue) { - if (!resetValue) return null; - - try { - // If it's already a Date object - if (resetValue instanceof Date) { - return resetValue.toISOString(); - } - - // If it's a number (Unix timestamp in milliseconds) - if (typeof resetValue === 'number') { - return new Date(resetValue).toISOString(); - } - - // If it's a string (ISO date or any parseable date string) - if (typeof resetValue === 'string') { - return new Date(resetValue).toISOString(); - } - - return null; - } catch (error) { - console.warn(`Failed to parse reset time: ${resetValue}`, error); - return null; - } -} - -/** - * GitHub Copilot Usage - * Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API - */ -async function getGitHubUsage(accessToken, providerSpecificData) { - try { - if (!accessToken) { - throw new Error("No GitHub access token available. Please re-authorize the connection."); - } - - // copilot_internal/user API requires GitHub OAuth token, not copilotToken - const response = await fetch("https://api.github.com/copilot_internal/user", { - headers: { - "Authorization": `token ${accessToken}`, - "Accept": "application/json", - "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, - "User-Agent": GITHUB_CONFIG.userAgent, - "Editor-Version": "vscode/1.100.0", - "Editor-Plugin-Version": "copilot-chat/0.26.7", - }, - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`GitHub API error: ${error}`); - } - - const data = await response.json(); - - // Handle different response formats (paid vs free) - if (data.quota_snapshots) { - // Paid plan format - const snapshots = data.quota_snapshots; - const resetAt = parseResetTime(data.quota_reset_date); - - return { - plan: data.copilot_plan, - resetDate: data.quota_reset_date, - quotas: { - chat: { ...formatGitHubQuotaSnapshot(snapshots.chat), resetAt }, - completions: { ...formatGitHubQuotaSnapshot(snapshots.completions), resetAt }, - premium_interactions: { ...formatGitHubQuotaSnapshot(snapshots.premium_interactions), resetAt }, - }, - }; - } else if (data.monthly_quotas || data.limited_user_quotas) { - // Free/limited plan format - const monthlyQuotas = data.monthly_quotas || {}; - const usedQuotas = data.limited_user_quotas || {}; - const resetAt = parseResetTime(data.limited_user_reset_date); - - return { - plan: data.copilot_plan || data.access_type_sku, - resetDate: data.limited_user_reset_date, - quotas: { - chat: { - used: usedQuotas.chat || 0, - total: monthlyQuotas.chat || 0, - unlimited: false, - resetAt, - }, - completions: { - used: usedQuotas.completions || 0, - total: monthlyQuotas.completions || 0, - unlimited: false, - resetAt, - }, - }, - }; - } - - return { message: "GitHub Copilot connected. Unable to parse quota data." }; - } catch (error) { - throw new Error(`Failed to fetch GitHub usage: ${error.message}`); - } -} - -function formatGitHubQuotaSnapshot(quota) { - if (!quota) return { used: 0, total: 0, unlimited: true }; - - return { - used: quota.entitlement - quota.remaining, - total: quota.entitlement, - remaining: quota.remaining, - unlimited: quota.unlimited || false, - }; -} - -/** - * Gemini CLI Usage (Google Cloud) - */ -async function getGeminiUsage(accessToken) { - try { - // Gemini CLI uses Google Cloud quotas - // Try to get quota info from Cloud Resource Manager - const response = await fetch( - "https://cloudresourcemanager.googleapis.com/v1/projects?filter=lifecycleState:ACTIVE", - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, - } - ); - - if (!response.ok) { - // Quota API may not be accessible, return generic message - return { message: "Gemini CLI uses Google Cloud quotas. Check Google Cloud Console for details." }; - } - - return { message: "Gemini CLI connected. Usage tracked via Google Cloud Console." }; - } catch (error) { - return { message: "Unable to fetch Gemini usage. Check Google Cloud Console." }; - } -} - -/** - * Antigravity Usage - Fetch quota from Google Cloud Code API - */ -async function getAntigravityUsage(accessToken, providerSpecificData) { - try { - // Fetch subscription info once — reuse for both projectId and plan - const subscriptionInfo = await getAntigravitySubscriptionInfo(accessToken); - const projectId = subscriptionInfo?.cloudaicompanionProject || null; - - // Fetch quota data with timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout - - let response; - try { - response = await fetch(ANTIGRAVITY_CONFIG.quotaApiUrl, { - method: "POST", - headers: { - "Authorization": `Bearer ${accessToken}`, - "User-Agent": ANTIGRAVITY_CONFIG.userAgent, - "Content-Type": "application/json", - "X-Client-Name": "antigravity", - "X-Client-Version": "1.107.0", - "x-request-source": "local", // MITM bypass - }, - body: JSON.stringify({ - ...(projectId ? { project: projectId } : {}) - }), - signal: controller.signal, - }); - } finally { - clearTimeout(timeoutId); - } - - if (response.status === 403) { - return { - message: "Antigravity quota API access forbidden. Chat may still work.", - quotas: {} - }; - } - - if (response.status === 401) { - return { - message: "Antigravity quota API authentication expired. Chat may still work.", - quotas: {} - }; - } - - if (!response.ok) { - throw new Error(`Antigravity API error: ${response.status}`); - } - - const data = await response.json(); - const quotas = {}; - - // Parse model quotas (inspired by vscode-antigravity-cockpit) - if (data.models) { - // Filter only recommended/important models (must match PROVIDER_MODELS ag ids) - const importantModels = [ - 'claude-opus-4-6-thinking', - 'claude-sonnet-4-6', - 'gemini-3.1-pro-high', - 'gemini-3.1-pro-low', - 'gemini-3-flash', - 'gpt-oss-120b-medium', - ]; - - for (const [modelKey, info] of Object.entries(data.models)) { - // Skip models without quota info - if (!info.quotaInfo) { - continue; - } - - // Skip internal models and non-important models - if (info.isInternal || !importantModels.includes(modelKey)) { - continue; - } - - const remainingFraction = info.quotaInfo.remainingFraction || 0; - const remainingPercentage = remainingFraction * 100; - - // Convert percentage to used/total for UI compatibility - const total = 1000; // Normalized base - const remaining = Math.round(total * remainingFraction); - const used = total - remaining; - - // Use modelKey as key (matches PROVIDER_MODELS id) - quotas[modelKey] = { - used, - total, - resetAt: parseResetTime(info.quotaInfo.resetTime), - remainingPercentage, - unlimited: false, - displayName: info.displayName || modelKey, - }; - } - } - - return { - plan: subscriptionInfo?.currentTier?.name || "Unknown", - quotas, - subscriptionInfo, - }; - } catch (error) { - console.error("[Antigravity Usage] Error:", error.message, error.cause); - return { message: `Antigravity error: ${error.message}` }; - } -} - -/** - * Get Antigravity project ID from subscription info - */ -async function getAntigravityProjectId(accessToken) { - try { - const info = await getAntigravitySubscriptionInfo(accessToken); - return info?.cloudaicompanionProject || null; - } catch { - return null; - } -} - -/** - * Get Antigravity subscription info - */ -async function getAntigravitySubscriptionInfo(accessToken) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout - try { - const response = await fetch(ANTIGRAVITY_CONFIG.loadProjectApiUrl, { - method: "POST", - headers: { - "Authorization": `Bearer ${accessToken}`, - "User-Agent": ANTIGRAVITY_CONFIG.userAgent, - "Content-Type": "application/json", - "x-request-source": "local", // MITM bypass - }, - body: JSON.stringify({ metadata: CLIENT_METADATA, mode: 1 }), - signal: controller.signal, - }); - - if (!response.ok) return null; - return await response.json(); - } catch (error) { - console.error("[Antigravity Subscription] Error:", error.message); - return null; - } finally { - clearTimeout(timeoutId); - } -} - -/** - * Claude Usage - Primary: OAuth endpoint, Fallback: legacy settings/org endpoint - */ -async function getClaudeUsage(accessToken) { - try { - // Primary: OAuth usage endpoint (Claude Code consumer OAuth tokens) - const oauthResponse = await fetch(CLAUDE_CONFIG.oauthUsageUrl, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "anthropic-beta": "oauth-2025-04-20", - "anthropic-version": CLAUDE_CONFIG.apiVersion, - }, - }); - - if (oauthResponse.ok) { - const data = await oauthResponse.json(); - const quotas = {}; - - // utilization = % USED (e.g. 87 means 87% used, 13% remaining) - const hasUtilization = (window) => - window && typeof window === "object" && typeof window.utilization === "number"; - - const createQuotaObject = (window) => { - const used = window.utilization; - const remaining = Math.max(0, 100 - used); - return { - used, - total: 100, - remaining, - remainingPercentage: remaining, - resetAt: parseResetTime(window.resets_at), - unlimited: false, - }; - }; - - if (hasUtilization(data.five_hour)) { - quotas["session (5h)"] = createQuotaObject(data.five_hour); - } - - if (hasUtilization(data.seven_day)) { - quotas["weekly (7d)"] = createQuotaObject(data.seven_day); - } - - // Parse model-specific weekly windows (e.g. seven_day_sonnet, seven_day_opus) - for (const [key, value] of Object.entries(data)) { - if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(value)) { - const modelName = key.replace("seven_day_", ""); - quotas[`weekly ${modelName} (7d)`] = createQuotaObject(value); - } - } - - return { - plan: "Claude Code", - extraUsage: data.extra_usage ?? null, - quotas, - }; - } - - // Fallback: legacy settings + org usage endpoint - console.warn(`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`); - return await getClaudeUsageLegacy(accessToken); - } catch (error) { - return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; - } -} - -/** - * Legacy Claude usage for API key / org admin users - */ -async function getClaudeUsageLegacy(accessToken) { - try { - const settingsResponse = await fetch(CLAUDE_CONFIG.settingsUrl, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "anthropic-version": CLAUDE_CONFIG.apiVersion, - }, - }); - - if (settingsResponse.ok) { - const settings = await settingsResponse.json(); - - if (settings.organization_id) { - const usageResponse = await fetch( - CLAUDE_CONFIG.usageUrl.replace("{org_id}", settings.organization_id), - { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "anthropic-version": CLAUDE_CONFIG.apiVersion, - }, - } - ); - - if (usageResponse.ok) { - const usage = await usageResponse.json(); - return { - plan: settings.plan || "Unknown", - organization: settings.organization_name, - quotas: usage, - }; - } - } - - return { - plan: settings.plan || "Unknown", - organization: settings.organization_name, - message: "Claude connected. Usage details require admin access.", - }; - } - - return { message: "Claude connected. Usage API requires admin permissions." }; - } catch (error) { - return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; - } -} - -/** - * Codex (OpenAI) Usage - Fetch from ChatGPT backend API - */ -async function getCodexUsage(accessToken) { - try { - const response = await fetch(CODEX_CONFIG.usageUrl, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", - }, - }); - - if (!response.ok) { - throw new Error(`Codex API error: ${response.status}`); - } - - const data = await response.json(); - - // Parse rate limit info - const rateLimit = data.rate_limit || {}; - const primaryWindow = rateLimit.primary_window || {}; - const secondaryWindow = rateLimit.secondary_window || {}; - - // Parse reset dates (reset_at is Unix timestamp in seconds, multiply by 1000 for ms) - const sessionResetAt = parseResetTime(primaryWindow.reset_at ? primaryWindow.reset_at * 1000 : null); - const weeklyResetAt = parseResetTime(secondaryWindow.reset_at ? secondaryWindow.reset_at * 1000 : null); - - return { - plan: data.plan_type || "unknown", - limitReached: rateLimit.limit_reached || false, - quotas: { - session: { - used: primaryWindow.used_percent || 0, - total: 100, - remaining: 100 - (primaryWindow.used_percent || 0), - resetAt: sessionResetAt, - unlimited: false, - }, - weekly: { - used: secondaryWindow.used_percent || 0, - total: 100, - remaining: 100 - (secondaryWindow.used_percent || 0), - resetAt: weeklyResetAt, - unlimited: false, - }, - }, - }; - } catch (error) { - throw new Error(`Failed to fetch Codex usage: ${error.message}`); - } -} - -/** - * Kiro (AWS CodeWhisperer) Usage - */ -async function getKiroUsage(accessToken, providerSpecificData) { - // Default profileArn fallback - const DEFAULT_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX"; - const profileArn = providerSpecificData?.profileArn || DEFAULT_PROFILE_ARN; - - try { - // Try old API first (POST method) - const payload = { - origin: "AI_EDITOR", - profileArn: profileArn, - resourceType: "AGENTIC_REQUEST", - }; - - const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com", { - method: "POST", - headers: { - "Authorization": `Bearer ${accessToken}`, - "Content-Type": "application/x-amz-json-1.0", - "x-amz-target": "AmazonCodeWhispererService.GetUsageLimits", - "Accept": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorText = await response.text(); - - // Handle authentication errors gracefully - if (response.status === 403 || response.status === 401) { - return { - message: "Kiro quota API authentication expired. Chat may still work.", - quotas: {} - }; - } - - throw new Error(`Kiro API error (${response.status}): ${errorText}`); - } - - const data = await response.json(); - - // Parse usage data from usageBreakdownList - const usageList = data.usageBreakdownList || []; - const quotaInfo = {}; - - // Parse reset time - supports multiple formats (nextDateReset, resetDate, etc.) - const resetAt = parseResetTime(data.nextDateReset || data.resetDate); - - usageList.forEach((breakdown) => { - const resourceType = breakdown.resourceType?.toLowerCase() || "unknown"; - const used = breakdown.currentUsageWithPrecision || 0; - const total = breakdown.usageLimitWithPrecision || 0; - - quotaInfo[resourceType] = { - used, - total, - remaining: total - used, - resetAt, - unlimited: false, - }; - - // Add free trial if available - if (breakdown.freeTrialInfo) { - const freeUsed = breakdown.freeTrialInfo.currentUsageWithPrecision || 0; - const freeTotal = breakdown.freeTrialInfo.usageLimitWithPrecision || 0; - - quotaInfo[`${resourceType}_freetrial`] = { - used: freeUsed, - total: freeTotal, - remaining: freeTotal - freeUsed, - resetAt, - unlimited: false, - }; - } - }); - - return { - plan: data.subscriptionInfo?.subscriptionTitle || "Kiro", - quotas: quotaInfo, - }; - } catch (error) { - // Fallback to new API (GET method) - try { - const params = new URLSearchParams({ - origin: "AI_EDITOR", - profileArn: profileArn, - resourceType: "AGENTIC_REQUEST", - }); - - const fallbackResponse = await fetch(`https://q.us-east-1.amazonaws.com/getUsageLimits?${params}`, { - method: "GET", - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept": "application/json", - }, - }); - - if (!fallbackResponse.ok) { - throw new Error(`Fallback API error (${fallbackResponse.status})`); - } - - const fallbackData = await fallbackResponse.json(); - - // Parse new API response structure - const usageList = fallbackData.usageBreakdownList || []; - const quotaInfo = {}; - const resetAt = parseResetTime(fallbackData.nextDateReset || fallbackData.resetDate); - - usageList.forEach((breakdown) => { - const resourceType = breakdown.resourceType?.toLowerCase() || "unknown"; - const used = breakdown.currentUsageWithPrecision || 0; - const total = breakdown.usageLimitWithPrecision || 0; - - quotaInfo[resourceType] = { - used, - total, - remaining: total - used, - resetAt, - unlimited: false, - }; - - // Add free trial if available - if (breakdown.freeTrialInfo) { - const freeUsed = breakdown.freeTrialInfo.currentUsageWithPrecision || 0; - const freeTotal = breakdown.freeTrialInfo.usageLimitWithPrecision || 0; - - quotaInfo[`${resourceType}_freetrial`] = { - used: freeUsed, - total: freeTotal, - remaining: freeTotal - freeUsed, - resetAt: parseResetTime(breakdown.freeTrialInfo.freeTrialExpiry), - unlimited: false, - }; - } - }); - - return { - plan: fallbackData.subscriptionInfo?.subscriptionTitle || "Kiro", - quotas: quotaInfo, - }; - } catch (fallbackError) { - throw new Error(`Failed to fetch Kiro usage: ${error.message} | Fallback: ${fallbackError.message}`); - } - } -} - -/** - * Qwen Usage - */ -async function getQwenUsage(accessToken, providerSpecificData) { - try { - const resourceUrl = providerSpecificData?.resourceUrl; - if (!resourceUrl) { - return { message: "Qwen connected. No resource URL available." }; - } - - // Qwen may have usage endpoint at resource URL - return { message: "Qwen connected. Usage tracked per request." }; - } catch (error) { - return { message: "Unable to fetch Qwen usage." }; - } -} - -/** - * iFlow Usage - */ -async function getIflowUsage(accessToken) { - try { - // iFlow may have usage endpoint - return { message: "iFlow connected. Usage tracked per request." }; - } catch (error) { - return { message: "Unable to fetch iFlow usage." }; - } -} - diff --git a/9router/open-sse/transformer/responsesTransformer.js b/9router/open-sse/transformer/responsesTransformer.js deleted file mode 100644 index ac84db20..00000000 --- a/9router/open-sse/transformer/responsesTransformer.js +++ /dev/null @@ -1,439 +0,0 @@ -/** - * Responses API Transformer - * Converts OpenAI Chat Completions SSE to Codex Responses API SSE format - * Can be used in both Next.js and Cloudflare Workers - */ - -import fs from "fs"; -import path from "path"; - -// Create log directory for responses (Node.js only) -export function createResponsesLogger(model, logsDir = null) { - // Skip logging in worker environment (no fs) - if (typeof fs.mkdirSync !== "function") { - return null; - } - - const timestamp = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15); - const uniqueId = Math.random().toString(36).slice(2, 8); - const baseDir = logsDir || (typeof process !== "undefined" ? process.cwd() : "."); - const logDir = path.join(baseDir, "logs", `responses_${model}_${timestamp}_${uniqueId}`); - - try { - fs.mkdirSync(logDir, { recursive: true }); - } catch { - return null; - } - - let inputEvents = []; - let outputEvents = []; - - return { - logInput: (event) => { - inputEvents.push(event); - }, - logOutput: (event) => { - outputEvents.push(event); - }, - flush: () => { - try { - fs.writeFileSync(path.join(logDir, "1_input_stream.txt"), inputEvents.join("\n")); - fs.writeFileSync(path.join(logDir, "2_output_stream.txt"), outputEvents.join("\n")); - } catch (e) { - console.log("[RESPONSES] Failed to write logs:", e.message); - } - } - }; -} - -/** - * Create TransformStream that converts Chat Completions SSE to Responses API SSE - * @param {Object} logger - Optional logger instance - * @returns {TransformStream} - */ -export function createResponsesApiTransformStream(logger = null) { - const state = { - seq: 0, - responseId: `resp_${Date.now()}`, - created: Math.floor(Date.now() / 1000), - started: false, - msgTextBuf: {}, - msgItemAdded: {}, - msgContentAdded: {}, - msgItemDone: {}, - reasoningId: "", - reasoningIndex: -1, - reasoningBuf: "", - reasoningPartAdded: false, - reasoningDone: false, - inThinking: false, - funcArgsBuf: {}, - funcNames: {}, - funcCallIds: {}, - funcArgsDone: {}, - funcItemDone: {}, - buffer: "", - completedSent: false - }; - - const encoder = new TextEncoder(); - const nextSeq = () => ++state.seq; - - const emit = (controller, eventType, data) => { - data.sequence_number = nextSeq(); - const output = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; - logger?.logOutput(output.trim()); - controller.enqueue(encoder.encode(output)); - }; - - // Helper to start reasoning - const startReasoning = (controller, idx) => { - if (!state.reasoningId) { - state.reasoningId = `rs_${state.responseId}_${idx}`; - state.reasoningIndex = idx; - - emit(controller, "response.output_item.added", { - type: "response.output_item.added", - output_index: idx, - item: { - id: state.reasoningId, - type: "reasoning", - summary: [] - } - }); - - emit(controller, "response.reasoning_summary_part.added", { - type: "response.reasoning_summary_part.added", - item_id: state.reasoningId, - output_index: idx, - summary_index: 0, - part: { type: "summary_text", text: "" } - }); - state.reasoningPartAdded = true; - } - }; - - const emitReasoningDelta = (controller, text) => { - if (!text) return; - state.reasoningBuf += text; - emit(controller, "response.reasoning_summary_text.delta", { - type: "response.reasoning_summary_text.delta", - item_id: state.reasoningId, - output_index: state.reasoningIndex, - summary_index: 0, - delta: text - }); - }; - - const closeReasoning = (controller) => { - if (state.reasoningId && !state.reasoningDone) { - state.reasoningDone = true; - - emit(controller, "response.reasoning_summary_text.done", { - type: "response.reasoning_summary_text.done", - item_id: state.reasoningId, - output_index: state.reasoningIndex, - summary_index: 0, - text: state.reasoningBuf - }); - - emit(controller, "response.reasoning_summary_part.done", { - type: "response.reasoning_summary_part.done", - item_id: state.reasoningId, - output_index: state.reasoningIndex, - summary_index: 0, - part: { type: "summary_text", text: state.reasoningBuf } - }); - - emit(controller, "response.output_item.done", { - type: "response.output_item.done", - output_index: state.reasoningIndex, - item: { - id: state.reasoningId, - type: "reasoning", - summary: [{ type: "summary_text", text: state.reasoningBuf }] - } - }); - } - }; - - const closeMessage = (controller, idx) => { - if (state.msgItemAdded[idx] && !state.msgItemDone[idx]) { - state.msgItemDone[idx] = true; - const fullText = state.msgTextBuf[idx] || ""; - const msgId = `msg_${state.responseId}_${idx}`; - - emit(controller, "response.output_text.done", { - type: "response.output_text.done", - item_id: msgId, - output_index: parseInt(idx), - content_index: 0, - text: fullText, - logprobs: [] - }); - - emit(controller, "response.content_part.done", { - type: "response.content_part.done", - item_id: msgId, - output_index: parseInt(idx), - content_index: 0, - part: { type: "output_text", annotations: [], logprobs: [], text: fullText } - }); - - emit(controller, "response.output_item.done", { - type: "response.output_item.done", - output_index: parseInt(idx), - item: { - id: msgId, - type: "message", - content: [{ type: "output_text", annotations: [], logprobs: [], text: fullText }], - role: "assistant" - } - }); - } - }; - - const closeToolCall = (controller, idx) => { - const callId = state.funcCallIds[idx]; - if (callId && !state.funcItemDone[idx]) { - const args = state.funcArgsBuf[idx] || "{}"; - - emit(controller, "response.function_call_arguments.done", { - type: "response.function_call_arguments.done", - item_id: `fc_${callId}`, - output_index: parseInt(idx), - arguments: args - }); - - emit(controller, "response.output_item.done", { - type: "response.output_item.done", - output_index: parseInt(idx), - item: { - id: `fc_${callId}`, - type: "function_call", - arguments: args, - call_id: callId, - name: state.funcNames[idx] || "" - } - }); - - state.funcItemDone[idx] = true; - state.funcArgsDone[idx] = true; - } - }; - - const sendCompleted = (controller) => { - if (!state.completedSent) { - state.completedSent = true; - emit(controller, "response.completed", { - type: "response.completed", - response: { - id: state.responseId, - object: "response", - created_at: state.created, - status: "completed", - background: false, - error: null - } - }); - } - }; - - return new TransformStream({ - transform(chunk, controller) { - const text = new TextDecoder().decode(chunk); - logger?.logInput(text.trim()); - state.buffer += text; - - const messages = state.buffer.split("\n\n"); - state.buffer = messages.pop() || ""; - - for (const msg of messages) { - if (!msg.trim()) continue; - - const dataMatch = msg.match(/^data:\s*(.+)$/m); - if (!dataMatch) continue; - - const dataStr = dataMatch[1].trim(); - if (dataStr === "[DONE]") continue; - - let parsed; - try { - parsed = JSON.parse(dataStr); - } catch { - continue; - } - - if (!parsed.choices?.length) continue; - - const choice = parsed.choices[0]; - const idx = choice.index || 0; - const delta = choice.delta || {}; - - // Emit initial events - if (!state.started) { - state.started = true; - state.responseId = parsed.id ? `resp_${parsed.id}` : state.responseId; - - emit(controller, "response.created", { - type: "response.created", - response: { - id: state.responseId, - object: "response", - created_at: state.created, - status: "in_progress", - background: false, - error: null, - output: [] - } - }); - - emit(controller, "response.in_progress", { - type: "response.in_progress", - response: { - id: state.responseId, - object: "response", - created_at: state.created, - status: "in_progress" - } - }); - } - - // Handle reasoning_content (OpenAI native format) - if (delta.reasoning_content) { - startReasoning(controller, idx); - emitReasoningDelta(controller, delta.reasoning_content); - } - - // Handle text content (may contain tags) - if (delta.content) { - let content = delta.content; - - if (content.includes("")) { - state.inThinking = true; - content = content.replace("", ""); - startReasoning(controller, idx); - } - - if (content.includes("")) { - const parts = content.split(""); - const thinkPart = parts[0]; - const textPart = parts.slice(1).join(""); - - if (thinkPart) emitReasoningDelta(controller, thinkPart); - closeReasoning(controller); - state.inThinking = false; - content = textPart; - } - - if (state.inThinking && content) { - emitReasoningDelta(controller, content); - continue; - } - - // Regular text content - if (content) { - if (!state.msgItemAdded[idx]) { - state.msgItemAdded[idx] = true; - const msgId = `msg_${state.responseId}_${idx}`; - - emit(controller, "response.output_item.added", { - type: "response.output_item.added", - output_index: idx, - item: { id: msgId, type: "message", content: [], role: "assistant" } - }); - } - - if (!state.msgContentAdded[idx]) { - state.msgContentAdded[idx] = true; - - emit(controller, "response.content_part.added", { - type: "response.content_part.added", - item_id: `msg_${state.responseId}_${idx}`, - output_index: idx, - content_index: 0, - part: { type: "output_text", annotations: [], logprobs: [], text: "" } - }); - } - - emit(controller, "response.output_text.delta", { - type: "response.output_text.delta", - item_id: `msg_${state.responseId}_${idx}`, - output_index: idx, - content_index: 0, - delta: content, - logprobs: [] - }); - - if (!state.msgTextBuf[idx]) state.msgTextBuf[idx] = ""; - state.msgTextBuf[idx] += content; - } - } - - // Handle tool_calls - if (delta.tool_calls) { - closeMessage(controller, idx); - - for (const tc of delta.tool_calls) { - const tcIdx = tc.index ?? 0; - const newCallId = tc.id; - const funcName = tc.function?.name; - - if (funcName) state.funcNames[tcIdx] = funcName; - - if (!state.funcCallIds[tcIdx] && newCallId) { - state.funcCallIds[tcIdx] = newCallId; - - emit(controller, "response.output_item.added", { - type: "response.output_item.added", - output_index: tcIdx, - item: { - id: `fc_${newCallId}`, - type: "function_call", - arguments: "", - call_id: newCallId, - name: state.funcNames[tcIdx] || "" - } - }); - } - - if (!state.funcArgsBuf[tcIdx]) state.funcArgsBuf[tcIdx] = ""; - - if (tc.function?.arguments) { - const refCallId = state.funcCallIds[tcIdx] || newCallId; - if (refCallId) { - emit(controller, "response.function_call_arguments.delta", { - type: "response.function_call_arguments.delta", - item_id: `fc_${refCallId}`, - output_index: tcIdx, - delta: tc.function.arguments - }); - } - state.funcArgsBuf[tcIdx] += tc.function.arguments; - } - } - } - - // Handle finish_reason - if (choice.finish_reason) { - for (const i in state.msgItemAdded) closeMessage(controller, i); - closeReasoning(controller); - for (const i in state.funcCallIds) closeToolCall(controller, i); - sendCompleted(controller); - } - } - }, - - flush(controller) { - for (const i in state.msgItemAdded) closeMessage(controller, i); - closeReasoning(controller); - for (const i in state.funcCallIds) closeToolCall(controller, i); - sendCompleted(controller); - - logger?.logOutput("data: [DONE]"); - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - logger?.flush(); - } - }); -} - diff --git a/9router/open-sse/transformer/streamToJsonConverter.js b/9router/open-sse/transformer/streamToJsonConverter.js deleted file mode 100644 index 92cb723b..00000000 --- a/9router/open-sse/transformer/streamToJsonConverter.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Stream-to-JSON Converter - * Converts Responses API SSE stream to single JSON response - * Used when client requests non-streaming but provider forces streaming (e.g., Codex) - */ - -/** - * Process a single SSE message and update state accordingly. - */ -function processSSEMessage(msg, state) { - if (!msg.trim()) return; - - const eventMatch = msg.match(/^event:\s*(.+)$/m); - const dataMatch = msg.match(/^data:\s*(.+)$/m); - if (!eventMatch || !dataMatch) return; - - const eventType = eventMatch[1].trim(); - const dataStr = dataMatch[1].trim(); - if (dataStr === "[DONE]") return; - - let parsed; - try { parsed = JSON.parse(dataStr); } - catch { return; } - - if (eventType === "response.created") { - state.responseId = parsed.response?.id || state.responseId; - state.created = parsed.response?.created_at || state.created; - } else if (eventType === "response.output_item.done") { - state.items.set(parsed.output_index ?? 0, parsed.item); - } else if (eventType === "response.completed") { - state.status = "completed"; - if (parsed.response?.usage) { - state.usage.input_tokens = parsed.response.usage.input_tokens || 0; - state.usage.output_tokens = parsed.response.usage.output_tokens || 0; - state.usage.total_tokens = parsed.response.usage.total_tokens || 0; - } - } else if (eventType === "response.failed") { - state.status = "failed"; - } -} - -const EMPTY_RESPONSE = { input_tokens: 0, output_tokens: 0, total_tokens: 0 }; - -/** - * Convert Responses API SSE stream to single JSON response - * @param {ReadableStream} stream - SSE stream from provider - * @returns {Promise} Final JSON response in Responses API format - */ -export async function convertResponsesStreamToJson(stream) { - if (!stream || typeof stream.getReader !== "function") { - return { id: `resp_${Date.now()}`, object: "response", created_at: Math.floor(Date.now() / 1000), status: "failed", output: [], usage: { ...EMPTY_RESPONSE } }; - } - - const reader = stream.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - - const state = { - responseId: "", - created: Math.floor(Date.now() / 1000), - status: "in_progress", - usage: { ...EMPTY_RESPONSE }, - items: new Map() - }; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const messages = buffer.split("\n\n"); - buffer = messages.pop() || ""; - - for (const msg of messages) { - processSSEMessage(msg, state); - } - } - - // Flush remaining buffer (last event may not end with \n\n) - if (buffer.trim()) { - processSSEMessage(buffer, state); - } - } finally { - reader.releaseLock(); - } - - // Build output array from accumulated items (ordered by index) - const output = []; - const maxIndex = state.items.size > 0 ? Math.max(...state.items.keys()) : -1; - for (let i = 0; i <= maxIndex; i++) { - output.push(state.items.get(i) || { type: "message", content: [], role: "assistant" }); - } - - return { - id: state.responseId || `resp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - object: "response", - created_at: state.created, - status: state.status || "completed", - output, - usage: state.usage - }; -} diff --git a/9router/open-sse/translator/formats.js b/9router/open-sse/translator/formats.js deleted file mode 100644 index 4aca172f..00000000 --- a/9router/open-sse/translator/formats.js +++ /dev/null @@ -1,34 +0,0 @@ -// Format identifiers -export const FORMATS = { - OPENAI: "openai", - OPENAI_RESPONSES: "openai-responses", - OPENAI_RESPONSE: "openai-response", - CLAUDE: "claude", - GEMINI: "gemini", - GEMINI_CLI: "gemini-cli", - CODEX: "codex", - ANTIGRAVITY: "antigravity", - KIRO: "kiro", - CURSOR: "cursor", - OLLAMA: "ollama" -}; - -/** - * Detect source format from request URL pathname + body. - * Returns null to fall back to body-based detection. - */ -export function detectFormatByEndpoint(pathname, body) { - // /v1/messages is always claude/anthropic format - if (pathname.includes("/v1/messages")) return FORMATS.CLAUDE; - - // /v1/responses is always openai-responses - if (pathname.includes("/v1/responses")) return FORMATS.OPENAI_RESPONSES; - - // /v1/chat/completions + input[] → treat as openai (Cursor CLI sends Responses body via chat endpoint) - if (pathname.includes("/v1/chat/completions") && Array.isArray(body?.input)) { - return FORMATS.OPENAI; - } - - return null; -} - diff --git a/9router/open-sse/translator/helpers/claudeHelper.js b/9router/open-sse/translator/helpers/claudeHelper.js deleted file mode 100644 index 92383d1a..00000000 --- a/9router/open-sse/translator/helpers/claudeHelper.js +++ /dev/null @@ -1,215 +0,0 @@ -// Claude helper functions for translator -import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js"; -import { adjustMaxTokens } from "./maxTokensHelper.js"; -import { applyCloaking } from "../../utils/claudeCloaking.js"; - -// Check if message has valid non-empty content -export function hasValidContent(msg) { - if (typeof msg.content === "string" && msg.content.trim()) return true; - if (Array.isArray(msg.content)) { - return msg.content.some(block => - (block.type === "text" && block.text?.trim()) || - block.type === "tool_use" || - block.type === "tool_result" - ); - } - return false; -} - -// Fix tool_use/tool_result ordering for Claude API -// 1. Assistant message with tool_use: remove text AFTER tool_use (Claude doesn't allow) -// 2. Merge consecutive same-role messages -export function fixToolUseOrdering(messages) { - if (messages.length <= 1) return messages; - - // Pass 1: Fix assistant messages with tool_use - remove text after tool_use - for (const msg of messages) { - if (msg.role === "assistant" && Array.isArray(msg.content)) { - const hasToolUse = msg.content.some(b => b.type === "tool_use"); - if (hasToolUse) { - // Keep only: thinking blocks + tool_use blocks (remove text blocks after tool_use) - const newContent = []; - let foundToolUse = false; - - for (const block of msg.content) { - if (block.type === "tool_use") { - foundToolUse = true; - newContent.push(block); - } else if (block.type === "thinking" || block.type === "redacted_thinking") { - newContent.push(block); - } else if (!foundToolUse) { - // Keep text blocks BEFORE tool_use - newContent.push(block); - } - // Skip text blocks AFTER tool_use - } - - msg.content = newContent; - } - } - } - - // Pass 2: Merge consecutive same-role messages - const merged = []; - - for (const msg of messages) { - const last = merged[merged.length - 1]; - - if (last && last.role === msg.role) { - // Merge content arrays - const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }]; - const msgContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }]; - - // Put tool_result first, then other content - const toolResults = [...lastContent.filter(b => b.type === "tool_result"), ...msgContent.filter(b => b.type === "tool_result")]; - const otherContent = [...lastContent.filter(b => b.type !== "tool_result"), ...msgContent.filter(b => b.type !== "tool_result")]; - - last.content = [...toolResults, ...otherContent]; - } else { - // Ensure content is array - const content = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }]; - merged.push({ role: msg.role, content: [...content] }); - } - } - - return merged; -} - -// Prepare request for Claude format endpoints -// - Cleanup cache_control -// - Filter empty messages -// - Add thinking block for Anthropic endpoint (provider === "claude") -// - Fix tool_use/tool_result ordering -// - Apply cloaking (billing header + fake user ID) for OAuth tokens -export function prepareClaudeRequest(body, provider = null, apiKey = null) { - // 1. System: remove all cache_control, add only to last block with ttl 1h - if (body.system && Array.isArray(body.system)) { - body.system = body.system.map((block, i) => { - const { cache_control, ...rest } = block; - if (i === body.system.length - 1) { - return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } }; - } - return rest; - }); - } - - // 2. Messages: process in optimized passes - if (body.messages && Array.isArray(body.messages)) { - const len = body.messages.length; - let filtered = []; - - // Pass 1: remove cache_control + filter empty messages - for (let i = 0; i < len; i++) { - const msg = body.messages[i]; - - // Remove cache_control from content blocks - if (Array.isArray(msg.content)) { - for (const block of msg.content) { - delete block.cache_control; - } - } - - // Keep final assistant even if empty, otherwise check valid content - const isFinalAssistant = i === len - 1 && msg.role === "assistant"; - if (isFinalAssistant || hasValidContent(msg)) { - filtered.push(msg); - } - } - - // Pass 1.5: Fix tool_use/tool_result ordering - // Each tool_use must have tool_result in the NEXT message (not same message with other content) - filtered = fixToolUseOrdering(filtered); - - body.messages = filtered; - - // Check if thinking is enabled AND last message is from user - const lastMessage = filtered[filtered.length - 1]; - const lastMessageIsUser = lastMessage?.role === "user"; - const thinkingEnabled = body.thinking?.type === "enabled" && lastMessageIsUser; - - // Pass 2 (reverse): add cache_control to last assistant + handle thinking for Anthropic - let lastAssistantProcessed = false; - for (let i = filtered.length - 1; i >= 0; i--) { - const msg = filtered[i]; - - if (msg.role === "assistant" && Array.isArray(msg.content)) { - // Add cache_control to last block of first (from end) assistant with content - if (!lastAssistantProcessed && msg.content.length > 0) { - msg.content[msg.content.length - 1].cache_control = { type: "ephemeral" }; - lastAssistantProcessed = true; - } - - // Handle thinking blocks for Anthropic endpoint only - if (provider === "claude") { - let hasToolUse = false; - let hasThinking = false; - - // Always replace signature for all thinking blocks - for (const block of msg.content) { - if (block.type === "thinking" || block.type === "redacted_thinking") { - block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE; - hasThinking = true; - } - if (block.type === "tool_use") hasToolUse = true; - } - - // Add thinking block if thinking enabled + has tool_use but no thinking - if (thinkingEnabled && !hasThinking && hasToolUse) { - msg.content.unshift({ - type: "thinking", - thinking: ".", - signature: DEFAULT_THINKING_CLAUDE_SIGNATURE - }); - } - } - } - } - } - - // 3. Tools: filter built-in tools for non-Anthropic providers, then handle cache_control - if (body.tools && Array.isArray(body.tools)) { - // Strip built-in tools (e.g. web_search_20250305) for providers that don't support them - if (provider !== "claude") { - body.tools = body.tools.filter(tool => !tool.type || tool.type === "function"); - } - - // Fix: Anthropic's API rejects empty domain lists on the web_search - // tool with "Empty list of domains is ambiguous". The Claude Code CLI - // sends both `blocked_domains: []` and `allowed_domains: []` meaning - // "no restrictions", but the API wants the fields omitted entirely - // when empty. Clean up here so the fix applies to both direct-API - // and 9Router-subscription paths. - for (const tool of body.tools) { - if (tool.type && tool.type.startsWith("web_search")) { - if (Array.isArray(tool.blocked_domains) && tool.blocked_domains.length === 0) { - delete tool.blocked_domains; - } - if (Array.isArray(tool.allowed_domains) && tool.allowed_domains.length === 0) { - delete tool.allowed_domains; - } - } - } - - body.tools = body.tools.map((tool, i) => { - const { cache_control, ...rest } = tool; - if (i === body.tools.length - 1) { - return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } }; - } - return rest; - }); - - // Remove tools array and tool_choice if empty after filtering - if (body.tools.length === 0) { - delete body.tools; - delete body.tool_choice; - } - } - - // Apply cloaking for OAuth tokens (billing header + fake user ID) - if (provider === "claude" && apiKey) { - body = applyCloaking(body, apiKey); - } - - return body; -} - diff --git a/9router/open-sse/translator/helpers/geminiHelper.js b/9router/open-sse/translator/helpers/geminiHelper.js deleted file mode 100644 index 283d26a5..00000000 --- a/9router/open-sse/translator/helpers/geminiHelper.js +++ /dev/null @@ -1,337 +0,0 @@ -// Gemini helper functions for translator - -// Unsupported JSON Schema constraints that should be removed for Antigravity -// Reference: CLIProxyAPI/internal/util/gemini_schema.go (removeUnsupportedKeywords) -export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [ - // Basic constraints (not supported by Gemini API) - "minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum", - "pattern", "minItems", "maxItems", "format", - // Claude rejects these in VALIDATED mode - "default", "examples", - // JSON Schema meta keywords - "$schema", "$defs", "definitions", "const", "$ref", - // Object validation keywords (not supported) - "additionalProperties", "propertyNames", "patternProperties", - // Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf) - "anyOf", "oneOf", "allOf", "not", - // Dependency keywords (not supported) - "dependencies", "dependentSchemas", "dependentRequired", - // Other unsupported keywords - "title", "if", "then", "else", "contentMediaType", "contentEncoding", - // UI/Styling properties (from Cursor tools - NOT JSON Schema standard) - "cornerRadius", "fillColor", "fontFamily", "fontSize", "fontWeight", - "gap", "padding", "strokeColor", "strokeThickness", "textColor" -]; - -// Default safety settings -export const DEFAULT_SAFETY_SETTINGS = [ - { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" }, - { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" }, - { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" }, - { category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" }, - { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" } -]; - -// Convert OpenAI content to Gemini parts -export function convertOpenAIContentToParts(content) { - const parts = []; - - if (typeof content === "string") { - parts.push({ text: content }); - } else if (Array.isArray(content)) { - for (const item of content) { - if (item.type === "text") { - parts.push({ text: item.text }); - } else if (item.type === "image_url" && item.image_url?.url?.startsWith("data:")) { - const url = item.image_url.url; - const commaIndex = url.indexOf(","); - if (commaIndex !== -1) { - const mimePart = url.substring(5, commaIndex); // skip "data:" - const data = url.substring(commaIndex + 1); - const mimeType = mimePart.split(";")[0]; - - parts.push({ - inlineData: { mime_type: mimeType, data: data } - }); - } - } - } - } - - return parts; -} - -// Extract text content from OpenAI content -export function extractTextContent(content) { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content.filter(c => c.type === "text").map(c => c.text).join(""); - } - return ""; -} - -// Try parse JSON safely -export function tryParseJSON(str) { - if (typeof str !== "string") return str; - try { - return JSON.parse(str); - } catch { - return null; - } -} - -// Generate request ID -export function generateRequestId() { - return `agent-${crypto.randomUUID()}`; -} - -// Generate session ID (binary-compatible format: UUID + timestamp) -export function generateSessionId() { - return crypto.randomUUID() + Date.now().toString(); -} - -// Generate project ID -export function generateProjectId() { - const adjectives = ["useful", "bright", "swift", "calm", "bold"]; - const nouns = ["fuze", "wave", "spark", "flow", "core"]; - const adj = adjectives[Math.floor(Math.random() * adjectives.length)]; - const noun = nouns[Math.floor(Math.random() * nouns.length)]; - return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`; -} - -// Helper: Remove unsupported keywords recursively from object/array -// Also strips all vendor extension fields (x- prefixed) not supported by Gemini -function removeUnsupportedKeywords(obj, keywords) { - if (!obj || typeof obj !== "object") return; - - if (Array.isArray(obj)) { - for (const item of obj) { - removeUnsupportedKeywords(item, keywords); - } - } else { - for (const key of Object.keys(obj)) { - if (keywords.includes(key) || key.startsWith("x-")) { - delete obj[key]; - } - } - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - removeUnsupportedKeywords(value, keywords); - } - } - } -} - -// Convert const to enum -function convertConstToEnum(obj) { - if (!obj || typeof obj !== "object") return; - - if (obj.const !== undefined && !obj.enum) { - obj.enum = [obj.const]; - delete obj.const; - } - - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - convertConstToEnum(value); - } - } -} - -// Convert enum values to strings (Gemini requires string enum values) -function convertEnumValuesToStrings(obj) { - if (!obj || typeof obj !== "object") return; - - if (obj.enum && Array.isArray(obj.enum)) { - obj.enum = obj.enum.map(v => String(v)); - } - - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - convertEnumValuesToStrings(value); - } - } -} - -// Merge allOf schemas -function mergeAllOf(obj) { - if (!obj || typeof obj !== "object") return; - - if (obj.allOf && Array.isArray(obj.allOf)) { - const merged = {}; - - for (const item of obj.allOf) { - if (item.properties) { - if (!merged.properties) merged.properties = {}; - Object.assign(merged.properties, item.properties); - } - if (item.required && Array.isArray(item.required)) { - if (!merged.required) merged.required = []; - for (const req of item.required) { - if (!merged.required.includes(req)) { - merged.required.push(req); - } - } - } - } - - delete obj.allOf; - if (merged.properties) obj.properties = { ...obj.properties, ...merged.properties }; - if (merged.required) obj.required = [...(obj.required || []), ...merged.required]; - } - - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - mergeAllOf(value); - } - } -} - -// Select best schema from anyOf/oneOf -function selectBest(items) { - let bestIdx = 0; - let bestScore = -1; - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - let score = 0; - const type = item.type; - - if (type === "object" || item.properties) { - score = 3; - } else if (type === "array" || item.items) { - score = 2; - } else if (type && type !== "null") { - score = 1; - } - - if (score > bestScore) { - bestScore = score; - bestIdx = i; - } - } - - return bestIdx; -} - -// Flatten anyOf/oneOf -function flattenAnyOfOneOf(obj) { - if (!obj || typeof obj !== "object") return; - - if (obj.anyOf && Array.isArray(obj.anyOf) && obj.anyOf.length > 0) { - const nonNullSchemas = obj.anyOf.filter(s => s && s.type !== "null"); - if (nonNullSchemas.length > 0) { - const bestIdx = selectBest(nonNullSchemas); - const selected = nonNullSchemas[bestIdx]; - delete obj.anyOf; - Object.assign(obj, selected); - } - } - - if (obj.oneOf && Array.isArray(obj.oneOf) && obj.oneOf.length > 0) { - const nonNullSchemas = obj.oneOf.filter(s => s && s.type !== "null"); - if (nonNullSchemas.length > 0) { - const bestIdx = selectBest(nonNullSchemas); - const selected = nonNullSchemas[bestIdx]; - delete obj.oneOf; - Object.assign(obj, selected); - } - } - - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - flattenAnyOfOneOf(value); - } - } -} - -// Flatten type arrays -function flattenTypeArrays(obj) { - if (!obj || typeof obj !== "object") return; - - if (obj.type && Array.isArray(obj.type)) { - const nonNullTypes = obj.type.filter(t => t !== "null"); - obj.type = nonNullTypes.length > 0 ? nonNullTypes[0] : "string"; - } - - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - flattenTypeArrays(value); - } - } -} - -// Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively -// Reference: CLIProxyAPI/internal/util/gemini_schema.go -export function cleanJSONSchemaForAntigravity(schema) { - if (!schema || typeof schema !== "object") return schema; - - // Mutate directly (schema is only used once per request) - let cleaned = schema; - - // Phase 1: Convert and prepare - convertConstToEnum(cleaned); - convertEnumValuesToStrings(cleaned); - - // Phase 2: Flatten complex structures - mergeAllOf(cleaned); - flattenAnyOfOneOf(cleaned); - flattenTypeArrays(cleaned); - - // Phase 3: Remove all unsupported keywords at ALL levels (including inside arrays) - removeUnsupportedKeywords(cleaned, UNSUPPORTED_SCHEMA_CONSTRAINTS); - - // Phase 4: Cleanup required fields recursively - function cleanupRequired(obj) { - if (!obj || typeof obj !== "object") return; - - if (obj.required && Array.isArray(obj.required) && obj.properties) { - const validRequired = obj.required.filter(field => - Object.prototype.hasOwnProperty.call(obj.properties, field) - ); - if (validRequired.length === 0) { - delete obj.required; - } else { - obj.required = validRequired; - } - } - - // Recurse into nested objects - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - cleanupRequired(value); - } - } - } - - cleanupRequired(cleaned); - - // Phase 5: Add placeholder for empty object schemas (Antigravity requirement) - function addPlaceholders(obj) { - if (!obj || typeof obj !== "object") return; - - if (obj.type === "object") { - if (!obj.properties || Object.keys(obj.properties).length === 0) { - obj.properties = { - reason: { - type: "string", - description: "Brief explanation of why you are calling this tool" - } - }; - obj.required = ["reason"]; - } - } - - // Recurse into nested objects - for (const value of Object.values(obj)) { - if (value && typeof value === "object") { - addPlaceholders(value); - } - } - } - - addPlaceholders(cleaned); - - return cleaned; -} - diff --git a/9router/open-sse/translator/helpers/maxTokensHelper.js b/9router/open-sse/translator/helpers/maxTokensHelper.js deleted file mode 100644 index 6a1c044d..00000000 --- a/9router/open-sse/translator/helpers/maxTokensHelper.js +++ /dev/null @@ -1,25 +0,0 @@ -import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../config/runtimeConfig.js"; - -/** - * Adjust max_tokens based on request context - * @param {object} body - Request body - * @returns {number} Adjusted max_tokens - */ -export function adjustMaxTokens(body) { - let maxTokens = body.max_tokens || DEFAULT_MAX_TOKENS; - - // Auto-increase for tool calling to prevent truncated arguments - if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) { - if (maxTokens < DEFAULT_MIN_TOKENS) { - maxTokens = DEFAULT_MIN_TOKENS; - } - } - - // Ensure max_tokens > thinking.budget_tokens (Claude API requirement) - if (body.thinking?.budget_tokens && maxTokens <= body.thinking.budget_tokens) { - maxTokens = DEFAULT_MAX_TOKENS; - } - - return maxTokens; -} - diff --git a/9router/open-sse/translator/helpers/openaiHelper.js b/9router/open-sse/translator/helpers/openaiHelper.js deleted file mode 100644 index 90879b93..00000000 --- a/9router/open-sse/translator/helpers/openaiHelper.js +++ /dev/null @@ -1,127 +0,0 @@ -// OpenAI helper functions for translator - -// Valid OpenAI content block types -export const VALID_OPENAI_CONTENT_TYPES = ["text", "image_url", "image"]; -export const VALID_OPENAI_MESSAGE_TYPES = ["text", "image_url", "image", "tool_calls", "tool_result"]; - -// Filter messages to OpenAI standard format -// Remove: thinking, redacted_thinking, signature, and other non-OpenAI blocks -export function filterToOpenAIFormat(body) { - if (!body.messages || !Array.isArray(body.messages)) return body; - - body.messages = body.messages.map(msg => { - // Keep tool messages as-is (OpenAI format) - if (msg.role === "tool") return msg; - - // Keep assistant messages with tool_calls as-is - if (msg.role === "assistant" && msg.tool_calls) return msg; - - // Handle string content - if (typeof msg.content === "string") return msg; - - // Handle array content - if (Array.isArray(msg.content)) { - const filteredContent = []; - - for (const block of msg.content) { - // Skip thinking blocks - if (block.type === "thinking" || block.type === "redacted_thinking") continue; - - // Only keep valid OpenAI content types - if (VALID_OPENAI_CONTENT_TYPES.includes(block.type)) { - // Remove signature field if exists - const { signature, cache_control, ...cleanBlock } = block; - filteredContent.push(cleanBlock); - } else if (block.type === "tool_use") { - // Convert tool_use to tool_calls format (handled separately) - continue; - } else if (block.type === "tool_result") { - // Keep tool_result but clean it - const { signature, cache_control, ...cleanBlock } = block; - filteredContent.push(cleanBlock); - } - } - - // If all content was filtered, add empty text - if (filteredContent.length === 0) { - filteredContent.push({ type: "text", text: "" }); - } - - return { ...msg, content: filteredContent }; - } - - return msg; - }); - - // Filter out messages with only empty text (but NEVER filter tool messages) - body.messages = body.messages.filter(msg => { - // Always keep tool messages - if (msg.role === "tool") return true; - // Always keep assistant messages with tool_calls - if (msg.role === "assistant" && msg.tool_calls) return true; - - if (typeof msg.content === "string") return msg.content.trim() !== ""; - if (Array.isArray(msg.content)) { - return msg.content.some(b => - (b.type === "text" && b.text?.trim()) || - b.type !== "text" - ); - } - return true; - }); - - // Remove empty tools array (some providers like QWEN reject it) - if (body.tools && Array.isArray(body.tools) && body.tools.length === 0) { - delete body.tools; - } - - // Normalize tools to OpenAI format (from Claude, Gemini, etc.) - if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) { - body.tools = body.tools.map(tool => { - // Already OpenAI format - if (tool.type === "function" && tool.function) return tool; - - // Claude format: {name, description, input_schema} - if (tool.name && (tool.input_schema || tool.description)) { - return { - type: "function", - function: { - name: tool.name, - description: tool.description || "", - parameters: tool.input_schema || { type: "object", properties: {} } - } - }; - } - - // Gemini format: {functionDeclarations: [{name, description, parameters}]} - if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) { - return tool.functionDeclarations.map(fn => ({ - type: "function", - function: { - name: fn.name, - description: fn.description || "", - parameters: fn.parameters || { type: "object", properties: {} } - } - })); - } - - return tool; - }).flat(); - } - - // Normalize tool_choice to OpenAI format - if (body.tool_choice && typeof body.tool_choice === "object") { - const choice = body.tool_choice; - // Claude format: {type: "auto|any|tool", name?: "..."} - if (choice.type === "auto") { - body.tool_choice = "auto"; - } else if (choice.type === "any") { - body.tool_choice = "required"; - } else if (choice.type === "tool" && choice.name) { - body.tool_choice = { type: "function", function: { name: choice.name } }; - } - } - - return body; -} - diff --git a/9router/open-sse/translator/helpers/responsesApiHelper.js b/9router/open-sse/translator/helpers/responsesApiHelper.js deleted file mode 100644 index 8254042c..00000000 --- a/9router/open-sse/translator/helpers/responsesApiHelper.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Normalize Responses API input to array format. - * Accepts string or array, returns array of message items. - * @param {string|Array} input - raw input from Responses API body - * @returns {Array|null} normalized array or null if invalid - */ -export function normalizeResponsesInput(input) { - if (typeof input === "string") { - const text = input.trim() === "" ? "..." : input; - return [{ type: "message", role: "user", content: [{ type: "input_text", text }] }]; - } - if (Array.isArray(input)) return input; - return null; -} - -/** - * Convert OpenAI Responses API format to standard chat completions format - * Responses API uses: { input: [...], instructions: "..." } - * Chat API uses: { messages: [...] } - */ -export function convertResponsesApiFormat(body) { - if (!body.input) return body; - - const result = { ...body }; - result.messages = []; - - // Convert instructions to system message - if (body.instructions) { - result.messages.push({ role: "system", content: body.instructions }); - } - - // Group items by conversation turn - let currentAssistantMsg = null; - let pendingToolCalls = []; - let pendingToolResults = []; - - const inputItems = normalizeResponsesInput(body.input); - if (!inputItems) return body; - - for (const item of inputItems) { - // Determine item type - Droid CLI sends role-based items without 'type' field - // Fallback: if no type but has role property, treat as message - const itemType = item.type || (item.role ? "message" : null); - - if (itemType === "message") { - // Flush any pending assistant message with tool calls - if (currentAssistantMsg) { - result.messages.push(currentAssistantMsg); - currentAssistantMsg = null; - } - // Flush pending tool results - if (pendingToolResults.length > 0) { - for (const tr of pendingToolResults) { - result.messages.push(tr); - } - pendingToolResults = []; - } - - // Convert content: input_text → text, output_text → text, input_image → image_url - const content = Array.isArray(item.content) - ? item.content.map(c => { - if (c.type === "input_text") return { type: "text", text: c.text }; - if (c.type === "output_text") return { type: "text", text: c.text }; - if (c.type === "input_image") { - const url = c.image_url || c.file_id || ""; - return { type: "image_url", image_url: { url, detail: c.detail || "auto" } }; - } - return c; - }) - : item.content; - result.messages.push({ role: item.role, content }); - } - else if (itemType === "function_call") { - // Start or append to assistant message with tool_calls - if (!currentAssistantMsg) { - currentAssistantMsg = { - role: "assistant", - content: null, - tool_calls: [] - }; - } - currentAssistantMsg.tool_calls.push({ - id: item.call_id, - type: "function", - function: { - name: item.name, - arguments: item.arguments - } - }); - } - else if (itemType === "function_call_output") { - // Flush assistant message first if exists - if (currentAssistantMsg) { - result.messages.push(currentAssistantMsg); - currentAssistantMsg = null; - } - // Add tool result - pendingToolResults.push({ - role: "tool", - tool_call_id: item.call_id, - content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) - }); - } - else if (itemType === "reasoning") { - // Skip reasoning items - they are for display only - continue; - } - } - - // Flush remaining - if (currentAssistantMsg) { - result.messages.push(currentAssistantMsg); - } - if (pendingToolResults.length > 0) { - for (const tr of pendingToolResults) { - result.messages.push(tr); - } - } - - // Cleanup Responses API specific fields - delete result.input; - delete result.instructions; - delete result.include; - delete result.prompt_cache_key; - delete result.store; - delete result.reasoning; - - return result; -} diff --git a/9router/open-sse/translator/helpers/toolCallHelper.js b/9router/open-sse/translator/helpers/toolCallHelper.js deleted file mode 100644 index e79a75a6..00000000 --- a/9router/open-sse/translator/helpers/toolCallHelper.js +++ /dev/null @@ -1,111 +0,0 @@ -// Tool call helper functions for translator - -// Generate unique tool call ID -export function generateToolCallId() { - return `call_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`; -} - -// Ensure all tool_calls have id field and arguments is string (some providers require it) -export function ensureToolCallIds(body) { - if (!body.messages || !Array.isArray(body.messages)) return body; - - for (const msg of body.messages) { - if (msg.role === "assistant" && msg.tool_calls && Array.isArray(msg.tool_calls)) { - for (const tc of msg.tool_calls) { - if (!tc.id) { - tc.id = generateToolCallId(); - } - if (!tc.type) { - tc.type = "function"; - } - // Ensure arguments is JSON string, not object - if (tc.function?.arguments && typeof tc.function.arguments !== "string") { - tc.function.arguments = JSON.stringify(tc.function.arguments); - } - } - } - } - - return body; -} - -// Get tool_call ids from assistant message (OpenAI format: tool_calls, Claude format: tool_use in content) -export function getToolCallIds(msg) { - if (msg.role !== "assistant") return []; - - const ids = []; - - // OpenAI format: tool_calls array - if (msg.tool_calls && Array.isArray(msg.tool_calls)) { - for (const tc of msg.tool_calls) { - if (tc.id) ids.push(tc.id); - } - } - - // Claude format: tool_use blocks in content - if (Array.isArray(msg.content)) { - for (const block of msg.content) { - if (block.type === "tool_use" && block.id) { - ids.push(block.id); - } - } - } - - return ids; -} - -// Check if user message has tool_result for given ids (OpenAI format: role=tool, Claude format: tool_result in content) -export function hasToolResults(msg, toolCallIds) { - if (!msg || !toolCallIds.length) return false; - - // OpenAI format: role = "tool" with tool_call_id - if (msg.role === "tool" && msg.tool_call_id) { - return toolCallIds.includes(msg.tool_call_id); - } - - // Claude format: tool_result blocks in user message content - if (msg.role === "user" && Array.isArray(msg.content)) { - for (const block of msg.content) { - if (block.type === "tool_result" && toolCallIds.includes(block.tool_use_id)) { - return true; - } - } - } - - return false; -} - -// Fix missing tool responses - insert empty tool_result if assistant has tool_use but next message has no tool_result -export function fixMissingToolResponses(body) { - if (!body.messages || !Array.isArray(body.messages)) return body; - - const newMessages = []; - - for (let i = 0; i < body.messages.length; i++) { - const msg = body.messages[i]; - const nextMsg = body.messages[i + 1]; - - newMessages.push(msg); - - // Check if this is assistant with tool_calls/tool_use - const toolCallIds = getToolCallIds(msg); - if (toolCallIds.length === 0) continue; - - // Check if next message has tool_result - if (nextMsg && !hasToolResults(nextMsg, toolCallIds)) { - // Insert tool responses for each tool_call - for (const id of toolCallIds) { - // OpenAI format: role = "tool" - newMessages.push({ - role: "tool", - tool_call_id: id, - content: "" - }); - } - } - } - - body.messages = newMessages; - return body; -} - diff --git a/9router/open-sse/translator/index.js b/9router/open-sse/translator/index.js deleted file mode 100644 index fe622ff9..00000000 --- a/9router/open-sse/translator/index.js +++ /dev/null @@ -1,204 +0,0 @@ -import { FORMATS } from "./formats.js"; -import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.js"; -import { prepareClaudeRequest } from "./helpers/claudeHelper.js"; -import { filterToOpenAIFormat } from "./helpers/openaiHelper.js"; -import { normalizeThinkingConfig } from "../services/provider.js"; - -// Registry for translators -const requestRegistry = new Map(); -const responseRegistry = new Map(); - -// Track initialization state -let initialized = false; - -// Register translator -export function register(from, to, requestFn, responseFn) { - const key = `${from}:${to}`; - if (requestFn) { - requestRegistry.set(key, requestFn); - } - if (responseFn) { - responseRegistry.set(key, responseFn); - } -} - -// Lazy load translators (called once on first use) -function ensureInitialized() { - if (initialized) return; - initialized = true; - - // Request translators - sync require pattern for bundler - require("./request/claude-to-openai.js"); - require("./request/openai-to-claude.js"); - require("./request/gemini-to-openai.js"); - require("./request/openai-to-gemini.js"); - require("./request/antigravity-to-openai.js"); - require("./request/openai-responses.js"); - require("./request/openai-to-kiro.js"); - require("./request/openai-to-cursor.js"); - require("./request/openai-to-ollama.js"); - - // Response translators - require("./response/claude-to-openai.js"); - require("./response/openai-to-claude.js"); - require("./response/gemini-to-openai.js"); - require("./response/openai-to-antigravity.js"); - require("./response/openai-responses.js"); - require("./response/kiro-to-openai.js"); - require("./response/cursor-to-openai.js"); - require("./response/ollama-to-openai.js"); -} - -// Translate request: source -> openai -> target -export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null) { - ensureInitialized(); - let result = body; - - // Normalize thinking config: remove if lastMessage is not user - normalizeThinkingConfig(result); - - // Always ensure tool_calls have id (some providers require it) - ensureToolCallIds(result); - - // Fix missing tool responses (insert empty tool_result if needed) - fixMissingToolResponses(result); - - // If same format, skip translation steps - if (sourceFormat !== targetFormat) { - // Step 1: source -> openai (if source is not openai) - if (sourceFormat !== FORMATS.OPENAI) { - const toOpenAI = requestRegistry.get(`${sourceFormat}:${FORMATS.OPENAI}`); - if (toOpenAI) { - result = toOpenAI(model, result, stream, credentials); - // Log OpenAI intermediate format - reqLogger?.logOpenAIRequest?.(result); - } - } - - // Step 2: openai -> target (if target is not openai) - if (targetFormat !== FORMATS.OPENAI) { - const fromOpenAI = requestRegistry.get(`${FORMATS.OPENAI}:${targetFormat}`); - if (fromOpenAI) { - result = fromOpenAI(model, result, stream, credentials); - } - } - } - - // Always normalize to clean OpenAI format when target is OpenAI - // This handles hybrid requests (e.g., OpenAI messages + Claude tools) - if (targetFormat === FORMATS.OPENAI) { - result = filterToOpenAIFormat(result); - } - - // Final step: prepare request for Claude format endpoints - if (targetFormat === FORMATS.CLAUDE) { - const apiKey = credentials?.accessToken || credentials?.apiKey || null; - result = prepareClaudeRequest(result, provider, apiKey); - } - - return result; -} - -// Translate response chunk: target -> openai -> source -export function translateResponse(targetFormat, sourceFormat, chunk, state) { - ensureInitialized(); - // If same format, return as-is - if (sourceFormat === targetFormat) { - return [chunk]; - } - - let results = [chunk]; - let openaiResults = null; // Store OpenAI intermediate results - - // Step 1: target -> openai (if target is not openai) - if (targetFormat !== FORMATS.OPENAI) { - const toOpenAI = responseRegistry.get(`${targetFormat}:${FORMATS.OPENAI}`); - if (toOpenAI) { - results = []; - const converted = toOpenAI(chunk, state); - if (converted) { - results = Array.isArray(converted) ? converted : [converted]; - openaiResults = results; // Store OpenAI intermediate - } - } - } - - // Step 2: openai -> source (if source is not openai) - if (sourceFormat !== FORMATS.OPENAI) { - const fromOpenAI = responseRegistry.get(`${FORMATS.OPENAI}:${sourceFormat}`); - if (fromOpenAI) { - const finalResults = []; - for (const r of results) { - const converted = fromOpenAI(r, state); - if (converted) { - finalResults.push(...(Array.isArray(converted) ? converted : [converted])); - } - } - results = finalResults; - } - } - - // Attach OpenAI intermediate results for logging - if (openaiResults && sourceFormat !== FORMATS.OPENAI && targetFormat !== FORMATS.OPENAI) { - results._openaiIntermediate = openaiResults; - } - - return results; -} - -// Check if translation needed -export function needsTranslation(sourceFormat, targetFormat) { - return sourceFormat !== targetFormat; -} - -// Initialize state for streaming response based on format -export function initState(sourceFormat) { - // Base state for all formats - const base = { - messageId: null, - model: null, - textBlockStarted: false, - thinkingBlockStarted: false, - inThinkingBlock: false, - currentBlockIndex: null, - toolCalls: new Map(), - finishReason: null, - finishReasonSent: false, - usage: null, - contentBlockIndex: -1 - }; - - // Add openai-responses specific fields - if (sourceFormat === FORMATS.OPENAI_RESPONSES) { - return { - ...base, - seq: 0, - responseId: `resp_${Date.now()}`, - created: Math.floor(Date.now() / 1000), - started: false, - msgTextBuf: {}, - msgItemAdded: {}, - msgContentAdded: {}, - msgItemDone: {}, - reasoningId: "", - reasoningIndex: -1, - reasoningBuf: "", - reasoningPartAdded: false, - reasoningDone: false, - inThinking: false, - funcArgsBuf: {}, - funcNames: {}, - funcCallIds: {}, - funcArgsDone: {}, - funcItemDone: {}, - completedSent: false - }; - } - - return base; -} - -// Initialize all translators (kept for backward compatibility) -export function initTranslators() { - ensureInitialized(); -} diff --git a/9router/open-sse/translator/request/antigravity-to-openai.js b/9router/open-sse/translator/request/antigravity-to-openai.js deleted file mode 100644 index 8d729a59..00000000 --- a/9router/open-sse/translator/request/antigravity-to-openai.js +++ /dev/null @@ -1,223 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; - -// Convert Antigravity request to OpenAI format -// Antigravity body: { project, model, userAgent, requestType, requestId, request: { contents, systemInstruction, tools, toolConfig, generationConfig, sessionId } } -export function antigravityToOpenAIRequest(model, body, stream) { - const req = body.request || body; - const result = { - model: model, - messages: [], - stream: stream - }; - - // Generation config - if (req.generationConfig) { - const config = req.generationConfig; - if (config.maxOutputTokens) { - const tempBody = { max_tokens: config.maxOutputTokens, tools: req.tools }; - result.max_tokens = adjustMaxTokens(tempBody); - } - if (config.temperature !== undefined) { - result.temperature = config.temperature; - } - if (config.topP !== undefined) { - result.top_p = config.topP; - } - if (config.topK !== undefined) { - result.top_k = config.topK; - } - - // Thinking config → reasoning_effort - if (config.thinkingConfig) { - const budget = config.thinkingConfig.thinkingBudget || 0; - if (budget > 0) { - if (budget <= 2048) { - result.reasoning_effort = "low"; - } else if (budget <= 16384) { - result.reasoning_effort = "medium"; - } else { - result.reasoning_effort = "high"; - } - } - } - } - - // System instruction - if (req.systemInstruction) { - const systemText = extractText(req.systemInstruction); - if (systemText) { - result.messages.push({ role: "system", content: systemText }); - } - } - - // Convert contents to messages - if (req.contents && Array.isArray(req.contents)) { - for (const content of req.contents) { - const converted = convertContent(content); - if (converted) { - if (Array.isArray(converted)) { - result.messages.push(...converted); - } else { - result.messages.push(converted); - } - } - } - } - - // Tools - if (req.tools && Array.isArray(req.tools)) { - result.tools = []; - for (const tool of req.tools) { - if (tool.functionDeclarations) { - for (const func of tool.functionDeclarations) { - result.tools.push({ - type: "function", - function: { - name: func.name, - description: func.description || "", - parameters: normalizeSchemaTypes(func.parameters) || { type: "object", properties: {} } - } - }); - } - } - } - } - - return result; -} - -// Recursively convert Antigravity schema types (OBJECT, STRING, etc.) to lowercase -function normalizeSchemaTypes(schema) { - if (!schema || typeof schema !== "object") return schema; - - const result = Array.isArray(schema) ? [...schema] : { ...schema }; - - if (typeof result.type === "string") { - result.type = result.type.toLowerCase(); - } - - if (result.properties) { - const normalized = {}; - for (const [key, val] of Object.entries(result.properties)) { - normalized[key] = normalizeSchemaTypes(val); - } - result.properties = normalized; - } - - if (result.items) { - result.items = normalizeSchemaTypes(result.items); - } - - return result; -} - -// Convert Antigravity content to OpenAI message -// Handles: text, thought, thoughtSignature, functionCall, functionResponse, inlineData -function convertContent(content) { - const role = content.role === "model" ? "assistant" : content.role === "user" ? "user" : content.role; - - if (!content.parts || !Array.isArray(content.parts)) { - return null; - } - - const textParts = []; - const toolCalls = []; - const toolResults = []; - let reasoningContent = ""; - - for (const part of content.parts) { - // Thinking content (thought: true) - if (part.thought === true && part.text) { - reasoningContent += part.text; - continue; - } - - // Text with thoughtSignature = regular text after thinking - if (part.thoughtSignature && part.text !== undefined) { - textParts.push({ type: "text", text: part.text }); - continue; - } - - // Regular text - if (part.text !== undefined) { - textParts.push({ type: "text", text: part.text }); - } - - // Inline data (images) - if (part.inlineData) { - textParts.push({ - type: "image_url", - image_url: { - url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` - } - }); - } - - // Function call - if (part.functionCall) { - toolCalls.push({ - id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - type: "function", - function: { - name: part.functionCall.name, - arguments: JSON.stringify(part.functionCall.args || {}) - } - }); - } - - // Function response → collect all, each becomes a separate tool message - if (part.functionResponse) { - toolResults.push({ - role: "tool", - tool_call_id: part.functionResponse.id || part.functionResponse.name, - content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {}) - }); - } - } - - // Content with only functionResponses → return array of tool messages - if (toolResults.length > 0) { - return toolResults; - } - - // Assistant with tool calls - if (toolCalls.length > 0) { - const msg = { role: "assistant" }; - if (textParts.length > 0) { - msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts; - } - if (reasoningContent) { - msg.reasoning_content = reasoningContent; - } - msg.tool_calls = toolCalls; - return msg; - } - - // Regular message - if (textParts.length > 0 || reasoningContent) { - const msg = { role }; - if (textParts.length > 0) { - msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts; - } - if (reasoningContent) { - msg.reasoning_content = reasoningContent; - } - return msg; - } - - return null; -} - -// Extract text from systemInstruction -function extractText(instruction) { - if (typeof instruction === "string") return instruction; - if (instruction.parts && Array.isArray(instruction.parts)) { - return instruction.parts.map(p => p.text || "").join(""); - } - return ""; -} - -// Register -register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, antigravityToOpenAIRequest, null); diff --git a/9router/open-sse/translator/request/claude-to-openai.js b/9router/open-sse/translator/request/claude-to-openai.js deleted file mode 100644 index 341c2bae..00000000 --- a/9router/open-sse/translator/request/claude-to-openai.js +++ /dev/null @@ -1,241 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; - -// Convert Claude request to OpenAI format -export function claudeToOpenAIRequest(model, body, stream) { - const result = { - model: model, - messages: [], - stream: stream - }; - - // Max tokens - if (body.max_tokens) { - result.max_tokens = adjustMaxTokens(body); - } - - // Temperature - if (body.temperature !== undefined) { - result.temperature = body.temperature; - } - - // System message — strip the claude-agent-sdk's "You are Claude Code..." - // identity sentence before forwarding to non-Anthropic providers. Without - // this, Gemini/Codex/etc receive the Claude Code preset's first line as - // gospel and dutifully introduce themselves as Claude. The rest of the - // preset (tool conventions, scaffolding) stays intact. - if (body.system) { - let systemContent = Array.isArray(body.system) - ? body.system.map(s => s.text || "").join("\n") - : body.system; - - systemContent = systemContent.replace( - /^You are [^.]*?(?:Claude Code|Claude agent)[^.]*?\.\s*/, - "" - ); - - if (systemContent) { - result.messages.push({ - role: "system", - content: systemContent - }); - } - } - - // Convert messages - if (body.messages && Array.isArray(body.messages)) { - for (let i = 0; i < body.messages.length; i++) { - const msg = body.messages[i]; - const converted = convertClaudeMessage(msg); - if (converted) { - // Handle array of messages (multiple tool results) - if (Array.isArray(converted)) { - result.messages.push(...converted); - } else { - result.messages.push(converted); - } - } - } - } - - // Fix missing tool responses - OpenAI requires every tool_call to have a response - fixMissingToolResponses(result.messages); - - // Tools - if (body.tools && Array.isArray(body.tools)) { - result.tools = body.tools.map(tool => ({ - type: "function", - function: { - name: tool.name, - description: tool.description, - parameters: tool.input_schema || { type: "object", properties: {} } - } - })); - } - - // Tool choice - if (body.tool_choice) { - result.tool_choice = convertToolChoice(body.tool_choice); - } - - return result; -} - -// Fix missing tool responses - add empty responses for tool_calls without responses -function fixMissingToolResponses(messages) { - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) { - const toolCallIds = msg.tool_calls.map(tc => tc.id); - - // Collect all tool response IDs that IMMEDIATELY follow this assistant message - const respondedIds = new Set(); - let insertPosition = i + 1; - for (let j = i + 1; j < messages.length; j++) { - const nextMsg = messages[j]; - if (nextMsg.role === "tool" && nextMsg.tool_call_id) { - respondedIds.add(nextMsg.tool_call_id); - insertPosition = j + 1; - } else { - break; - } - } - - // Find missing responses and insert them - const missingIds = toolCallIds.filter(id => !respondedIds.has(id)); - - if (missingIds.length > 0) { - const missingResponses = missingIds.map(id => ({ - role: "tool", - tool_call_id: id, - content: "[No response received]" - })); - messages.splice(insertPosition, 0, ...missingResponses); - i = insertPosition + missingResponses.length - 1; - } - } - } -} - -// Convert single Claude message - returns single message or array of messages -function convertClaudeMessage(msg) { - const role = msg.role === "user" || msg.role === "tool" ? "user" : "assistant"; - - // Simple string content - if (typeof msg.content === "string") { - return { role, content: msg.content }; - } - - // Array content - if (Array.isArray(msg.content)) { - const parts = []; - const toolCalls = []; - const toolResults = []; - - for (const block of msg.content) { - switch (block.type) { - case "text": - parts.push({ type: "text", text: block.text }); - break; - - case "image": - if (block.source?.type === "base64") { - parts.push({ - type: "image_url", - image_url: { - url: `data:${block.source.media_type};base64,${block.source.data}` - } - }); - } - break; - - case "tool_use": - toolCalls.push({ - id: block.id, - type: "function", - function: { - name: block.name, - arguments: JSON.stringify(block.input || {}) - } - }); - break; - - case "tool_result": - let resultContent = ""; - if (typeof block.content === "string") { - resultContent = block.content; - } else if (Array.isArray(block.content)) { - resultContent = block.content - .filter(c => c.type === "text") - .map(c => c.text) - .join("\n") || JSON.stringify(block.content); - } else if (block.content) { - resultContent = JSON.stringify(block.content); - } - - toolResults.push({ - role: "tool", - tool_call_id: block.tool_use_id, - content: resultContent - }); - break; - } - } - - // If has tool results, return array of tool messages - if (toolResults.length > 0) { - if (parts.length > 0) { - const textContent = parts.length === 1 && parts[0].type === "text" - ? parts[0].text - : parts; - return [...toolResults, { role: "user", content: textContent }]; - } - return toolResults; - } - - // If has tool calls, return assistant message with tool_calls - if (toolCalls.length > 0) { - const result = { role: "assistant" }; - if (parts.length > 0) { - result.content = parts.length === 1 && parts[0].type === "text" - ? parts[0].text - : parts; - } - result.tool_calls = toolCalls; - return result; - } - - // Return content - if (parts.length > 0) { - return { - role, - content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts - }; - } - - // Empty content array - if (msg.content.length === 0) { - return { role, content: "" }; - } - } - - return null; -} - -// Convert tool choice -function convertToolChoice(choice) { - if (!choice) return "auto"; - if (typeof choice === "string") return choice; - - switch (choice.type) { - case "auto": return "auto"; - case "any": return "required"; - case "tool": return { type: "function", function: { name: choice.name } }; - default: return "auto"; - } -} - -// Register -register(FORMATS.CLAUDE, FORMATS.OPENAI, claudeToOpenAIRequest, null); - diff --git a/9router/open-sse/translator/request/gemini-to-openai.js b/9router/open-sse/translator/request/gemini-to-openai.js deleted file mode 100644 index 5b3d0f01..00000000 --- a/9router/open-sse/translator/request/gemini-to-openai.js +++ /dev/null @@ -1,147 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; - -// Convert Gemini request to OpenAI format -export function geminiToOpenAIRequest(model, body, stream) { - const result = { - model: model, - messages: [], - stream: stream - }; - - // Generation config - if (body.generationConfig) { - const config = body.generationConfig; - if (config.maxOutputTokens) { - const tempBody = { max_tokens: config.maxOutputTokens, tools: body.tools }; - result.max_tokens = adjustMaxTokens(tempBody); - } - if (config.temperature !== undefined) { - result.temperature = config.temperature; - } - if (config.topP !== undefined) { - result.top_p = config.topP; - } - } - - // System instruction - if (body.systemInstruction) { - const systemText = extractGeminiText(body.systemInstruction); - if (systemText) { - result.messages.push({ - role: "system", - content: systemText - }); - } - } - - // Convert contents to messages - if (body.contents && Array.isArray(body.contents)) { - for (const content of body.contents) { - const converted = convertGeminiContent(content); - if (converted) { - result.messages.push(converted); - } - } - } - - // Tools - if (body.tools && Array.isArray(body.tools)) { - result.tools = []; - for (const tool of body.tools) { - if (tool.functionDeclarations) { - for (const func of tool.functionDeclarations) { - result.tools.push({ - type: "function", - function: { - name: func.name, - description: func.description || "", - parameters: func.parameters || { type: "object", properties: {} } - } - }); - } - } - } - } - - return result; -} - -// Convert Gemini content to OpenAI message -function convertGeminiContent(content) { - const role = content.role === "user" ? "user" : "assistant"; - - if (!content.parts || !Array.isArray(content.parts)) { - return null; - } - - const parts = []; - const toolCalls = []; - - for (const part of content.parts) { - if (part.text !== undefined) { - parts.push({ type: "text", text: part.text }); - } - - if (part.inlineData) { - parts.push({ - type: "image_url", - image_url: { - url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` - } - }); - } - - if (part.functionCall) { - toolCalls.push({ - id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - type: "function", - function: { - name: part.functionCall.name, - arguments: JSON.stringify(part.functionCall.args || {}) - } - }); - } - - if (part.functionResponse) { - return { - role: "tool", - tool_call_id: part.functionResponse.id || part.functionResponse.name, - content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {}) - }; - } - } - - if (toolCalls.length > 0) { - const result = { role: "assistant" }; - if (parts.length > 0) { - result.content = parts.length === 1 ? parts[0].text : parts; - } - result.tool_calls = toolCalls; - return result; - } - - if (parts.length > 0) { - return { - role, - content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts - }; - } - - return null; -} - -// Extract text from Gemini content -function extractGeminiText(content) { - if (typeof content === "string") return content; - if (content.parts && Array.isArray(content.parts)) { - return content.parts.map(p => p.text || "").join(""); - } - return ""; -} - -// Register -register(FORMATS.GEMINI, FORMATS.OPENAI, geminiToOpenAIRequest, null); -register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, geminiToOpenAIRequest, null); - diff --git a/9router/open-sse/translator/request/openai-responses.js b/9router/open-sse/translator/request/openai-responses.js deleted file mode 100644 index 8067c8fc..00000000 --- a/9router/open-sse/translator/request/openai-responses.js +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Translator: OpenAI Responses API → OpenAI Chat Completions - * - * Responses API uses: { input: [...], instructions: "..." } - * Chat API uses: { messages: [...] } - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { normalizeResponsesInput } from "../helpers/responsesApiHelper.js"; - -/** - * Convert OpenAI Responses API request to OpenAI Chat Completions format - */ -export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) { - if (!body.input) return body; - - const result = { ...body }; - result.messages = []; - - // Convert instructions to system message - if (body.instructions) { - result.messages.push({ role: "system", content: body.instructions }); - } - - // Group items by conversation turn - let currentAssistantMsg = null; - let pendingToolResults = []; - - const inputItems = normalizeResponsesInput(body.input); - if (!inputItems) return body; - - for (const item of inputItems) { - // Determine item type - Droid CLI sends role-based items without 'type' field - // Fallback: if no type but has role property, treat as message - const itemType = item.type || (item.role ? "message" : null); - - if (itemType === "message") { - // Flush any pending assistant message with tool calls - if (currentAssistantMsg) { - result.messages.push(currentAssistantMsg); - currentAssistantMsg = null; - } - // Flush pending tool results - if (pendingToolResults.length > 0) { - for (const tr of pendingToolResults) { - result.messages.push(tr); - } - pendingToolResults = []; - } - - // Convert content: input_text → text, output_text → text, input_image → image_url - const content = Array.isArray(item.content) - ? item.content.map(c => { - if (c.type === "input_text") return { type: "text", text: c.text }; - if (c.type === "output_text") return { type: "text", text: c.text }; - if (c.type === "input_image") { - const url = c.image_url || c.file_id || ""; - return { type: "image_url", image_url: { url, detail: c.detail || "auto" } }; - } - return c; - }) - : item.content; - result.messages.push({ role: item.role, content }); - } - else if (itemType === "function_call") { - // Start or append to assistant message with tool_calls - if (!currentAssistantMsg) { - currentAssistantMsg = { - role: "assistant", - content: null, - tool_calls: [] - }; - } - currentAssistantMsg.tool_calls.push({ - id: item.call_id, - type: "function", - function: { - name: item.name, - arguments: item.arguments - } - }); - } - else if (itemType === "function_call_output") { - // Flush assistant message first if exists - if (currentAssistantMsg) { - result.messages.push(currentAssistantMsg); - currentAssistantMsg = null; - } - // Flush any pending tool results first - if (pendingToolResults.length > 0) { - for (const tr of pendingToolResults) { - result.messages.push(tr); - } - pendingToolResults = []; - } - // Add tool result immediately - result.messages.push({ - role: "tool", - tool_call_id: item.call_id, - content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) - }); - } - else if (itemType === "reasoning") { - // Skip reasoning items - they are for display only - continue; - } - } - - // Flush remaining - if (currentAssistantMsg) { - result.messages.push(currentAssistantMsg); - } - if (pendingToolResults.length > 0) { - for (const tr of pendingToolResults) { - result.messages.push(tr); - } - } - - // Convert tools format. - // Responses API supports "hosted" tools (e.g. { type: "request_user_input" }) that carry no - // explicit `name` field and cannot be represented as Chat Completions function declarations. - // Filter them out to avoid sending nameless functionDeclarations to downstream providers - // such as Gemini, which strictly validates function names. - if (body.tools && Array.isArray(body.tools)) { - result.tools = body.tools - .map(tool => { - // Already in Chat Completions format: { type: "function", function: { name, ... } } - if (tool.function) return tool; - // Responses API function tool: { type: "function", name, description, parameters } - // Only convert when a non-empty name is present; skip hosted tools without one. - const name = tool.name; - if (!name || typeof name !== "string" || name.trim() === "") return null; - return { - type: "function", - function: { - name, - description: tool.description, - parameters: tool.parameters, - strict: tool.strict - } - }; - }) - .filter(Boolean); - } - - // Cleanup Responses API specific fields - delete result.input; - delete result.instructions; - delete result.include; - delete result.prompt_cache_key; - delete result.store; - delete result.reasoning; - - return result; -} - -/** - * Convert OpenAI Chat Completions to OpenAI Responses API format - */ -export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) { - // Body already in Responses API format (e.g. Cursor CLI calling /chat/completions with input[]) - if (body.input) return { ...body, model, stream: true }; - - const result = { - model, - input: [], - stream: true, - store: false - }; - - // Extract system message as instructions - let hasSystemMessage = false; - const messages = body.messages || []; - - for (const msg of messages) { - if (msg.role === "system") { - // Use first system message as instructions - if (!hasSystemMessage) { - result.instructions = typeof msg.content === "string" ? msg.content : ""; - hasSystemMessage = true; - } - continue; // Skip system messages in input - } - - // Convert user/assistant messages to input items - if (msg.role === "user" || msg.role === "assistant") { - const contentType = msg.role === "user" ? "input_text" : "output_text"; - const content = typeof msg.content === "string" - ? [{ type: contentType, text: msg.content }] - : Array.isArray(msg.content) - ? msg.content.map(c => { - if (c.type === "text") return { type: contentType, text: c.text }; - // Convert Chat Completions image_url → Responses API input_image - // Responses API expects: { type: "input_image", image_url: "" } - // Chat Completions sends: { type: "image_url", image_url: { url: "...", detail: "..." } } - if (c.type === "image_url") { - const url = typeof c.image_url === "string" ? c.image_url : c.image_url?.url; - return { type: "input_image", image_url: url, detail: c.image_url?.detail || "auto" }; - } - if (c.type === "input_image") return c; - // Serialize any unknown type (tool_use, tool_result, thinking, etc.) as text - const text = c.text || c.content || JSON.stringify(c); - return { type: contentType, text: typeof text === "string" ? text : JSON.stringify(text) }; - }) - : []; - - // Only push a message block if content is non-empty. - // Assistant messages with only tool_calls have content: null — skip the - // message block in that case; the tool_calls are pushed separately below. - if (content.length > 0) { - result.input.push({ - type: "message", - role: msg.role, - content - }); - } - } - - // Convert tool calls - if (msg.role === "assistant" && msg.tool_calls) { - for (const tc of msg.tool_calls) { - result.input.push({ - type: "function_call", - call_id: tc.id, - name: tc.function?.name || "", - arguments: tc.function?.arguments || "{}" - }); - } - } - - // Convert tool results - output must be a string for Responses API - if (msg.role === "tool") { - const output = typeof msg.content === "string" - ? msg.content - : Array.isArray(msg.content) - ? msg.content.map(c => c.text || JSON.stringify(c)).join("") - : JSON.stringify(msg.content); - result.input.push({ - type: "function_call_output", - call_id: msg.tool_call_id, - output - }); - } - } - - // If no system message, leave instructions empty (will be filled by executor) - if (!hasSystemMessage) { - result.instructions = ""; - } - - // Convert tools format - if (body.tools && Array.isArray(body.tools)) { - result.tools = body.tools.map(tool => { - if (tool.type === "function") { - return { - type: "function", - name: tool.function.name, - description: tool.function.description, - parameters: tool.function.parameters, - strict: tool.function.strict - }; - } - return tool; - }); - } - - // Pass through other relevant fields - if (body.temperature !== undefined) result.temperature = body.temperature; - if (body.max_tokens !== undefined) result.max_tokens = body.max_tokens; - if (body.top_p !== undefined) result.top_p = body.top_p; - - return result; -} - -// Register both directions -register(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI, openaiResponsesToOpenAIRequest, null); -register(FORMATS.OPENAI, FORMATS.OPENAI_RESPONSES, openaiToOpenAIResponsesRequest, null); diff --git a/9router/open-sse/translator/request/openai-to-claude.js b/9router/open-sse/translator/request/openai-to-claude.js deleted file mode 100644 index bb2c3153..00000000 --- a/9router/open-sse/translator/request/openai-to-claude.js +++ /dev/null @@ -1,348 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { CLAUDE_SYSTEM_PROMPT } from "../../config/appConstants.js"; -import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; - -// Empty prefix matches real Claude Code behavior (no tool name prefix). -// Previously "proxy_" was used but this is a detectable fingerprint difference. -const CLAUDE_OAUTH_TOOL_PREFIX = ""; - -// Convert OpenAI request to Claude format -export function openaiToClaudeRequest(model, body, stream) { - // Tool name mapping for Claude OAuth (capitalizedName → originalName) - const toolNameMap = new Map(); - const result = { - model: model, - max_tokens: adjustMaxTokens(body), - stream: stream - }; - - // Temperature - if (body.temperature !== undefined) { - result.temperature = body.temperature; - } - - // Messages - result.messages = []; - const systemParts = []; - - if (body.messages && Array.isArray(body.messages)) { - // Extract system messages - for (const msg of body.messages) { - if (msg.role === "system") { - systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content)); - } - } - - // Filter out system messages for separate processing - const nonSystemMessages = body.messages.filter(m => m.role !== "system"); - - // Process messages with merging logic - // CRITICAL: tool_result must be in separate message immediately after tool_use - let currentRole = undefined; - let currentParts = []; - - const flushCurrentMessage = () => { - if (currentRole && currentParts.length > 0) { - result.messages.push({ role: currentRole, content: currentParts }); - currentParts = []; - } - }; - - for (const msg of nonSystemMessages) { - const newRole = (msg.role === "user" || msg.role === "tool") ? "user" : "assistant"; - const blocks = getContentBlocksFromMessage(msg, toolNameMap); - const hasToolUse = blocks.some(b => b.type === "tool_use"); - const hasToolResult = blocks.some(b => b.type === "tool_result"); - - // Separate tool_result from other content - if (hasToolResult) { - const toolResultBlocks = blocks.filter(b => b.type === "tool_result"); - const otherBlocks = blocks.filter(b => b.type !== "tool_result"); - - flushCurrentMessage(); - - if (toolResultBlocks.length > 0) { - result.messages.push({ role: "user", content: toolResultBlocks }); - } - - if (otherBlocks.length > 0) { - currentRole = newRole; - currentParts.push(...otherBlocks); - } - continue; - } - - if (currentRole !== newRole) { - flushCurrentMessage(); - currentRole = newRole; - } - - currentParts.push(...blocks); - - if (hasToolUse) { - flushCurrentMessage(); - } - } - - flushCurrentMessage(); - - // Add cache_control to last assistant message - for (let i = result.messages.length - 1; i >= 0; i--) { - const message = result.messages[i]; - if (message.role === "assistant" && Array.isArray(message.content) && message.content.length > 0) { - const lastBlock = message.content[message.content.length - 1]; - if (lastBlock) { - lastBlock.cache_control = { type: "ephemeral" }; - break; - } - } - } - } - - // Handle response_format for JSON mode - if (body.response_format) { - const responseFormat = body.response_format; - if (responseFormat.type === "json_schema" && responseFormat.json_schema?.schema) { - const schemaJson = JSON.stringify(responseFormat.json_schema.schema, null, 2); - systemParts.push(`You must respond with valid JSON that strictly follows this JSON schema: -\`\`\`json -${schemaJson} -\`\`\` -Respond ONLY with the JSON object, no other text.`); - } else if (responseFormat.type === "json_object") { - systemParts.push("You must respond with valid JSON. Respond ONLY with a JSON object, no other text."); - } - } - - // System with Claude Code prompt and cache_control - const claudeCodePrompt = { type: "text", text: CLAUDE_SYSTEM_PROMPT }; - - if (systemParts.length > 0) { - const systemText = systemParts.join("\n"); - result.system = [ - claudeCodePrompt, - { type: "text", text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } } - ]; - } else { - result.system = [claudeCodePrompt]; - } - - // Tools - convert from OpenAI format to Claude format with prefix for OAuth - if (body.tools && Array.isArray(body.tools)) { - result.tools = []; - for (const tool of body.tools) { - // Pass-through built-in tools (e.g. web_search_20250305) without prefix or conversion - const toolType = tool.type; - if (toolType && toolType !== "function") { - result.tools.push(tool); - continue; - } - - const toolData = toolType === "function" && tool.function ? tool.function : tool; - const originalName = toolData.name; - - // Claude OAuth requires prefixed tool names to avoid conflicts - const toolName = CLAUDE_OAUTH_TOOL_PREFIX + originalName; - - // Store mapping for response translation (prefixed → original) - toolNameMap.set(toolName, originalName); - - result.tools.push({ - name: toolName, - description: toolData.description || "", - input_schema: toolData.parameters || toolData.input_schema || { type: "object", properties: {}, required: [] } - }); - } - - if (result.tools.length > 0) { - result.tools[result.tools.length - 1].cache_control = { type: "ephemeral", ttl: "1h" }; - } - } - - // Tool choice - if (body.tool_choice) { - result.tool_choice = convertOpenAIToolChoice(body.tool_choice); - } - - // Thinking configuration - if (body.thinking) { - result.thinking = { - type: body.thinking.type || "enabled", - ...(body.thinking.budget_tokens && { budget_tokens: body.thinking.budget_tokens }), - ...(body.thinking.max_tokens && { max_tokens: body.thinking.max_tokens }) - }; - } - - // Attach toolNameMap to result for response translation - if (toolNameMap.size > 0) { - result._toolNameMap = toolNameMap; - } - - return result; -} - -// Get content blocks from single message -function getContentBlocksFromMessage(msg, toolNameMap = new Map()) { - const blocks = []; - - if (msg.role === "tool") { - blocks.push({ - type: "tool_result", - tool_use_id: msg.tool_call_id, - content: msg.content - }); - } else if (msg.role === "user") { - if (typeof msg.content === "string") { - if (msg.content) { - blocks.push({ type: "text", text: msg.content }); - } - } else if (Array.isArray(msg.content)) { - for (const part of msg.content) { - if (part.type === "text" && part.text) { - blocks.push({ type: "text", text: part.text }); - } else if (part.type === "tool_result") { - blocks.push({ - type: "tool_result", - tool_use_id: part.tool_use_id, - content: part.content, - ...(part.is_error && { is_error: part.is_error }) - }); - } else if (part.type === "image_url") { - const url = part.image_url.url; - const match = url.match(/^data:([^;]+);base64,(.+)$/); - if (match) { - blocks.push({ - type: "image", - source: { type: "base64", media_type: match[1], data: match[2] } - }); - } - } else if (part.type === "image" && part.source) { - blocks.push({ type: "image", source: part.source }); - } - } - } - } else if (msg.role === "assistant") { - if (Array.isArray(msg.content)) { - for (const part of msg.content) { - if (part.type === "text" && part.text) { - blocks.push({ type: "text", text: part.text }); - } else if (part.type === "tool_use") { - // Tool name already has prefix from tool declarations, keep as-is - blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input }); - } - } - } else if (msg.content) { - const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content); - if (text) { - blocks.push({ type: "text", text }); - } - } - - if (msg.tool_calls && Array.isArray(msg.tool_calls)) { - for (const tc of msg.tool_calls) { - if (tc.type === "function") { - // Apply prefix to tool name - const toolName = CLAUDE_OAUTH_TOOL_PREFIX + tc.function.name; - blocks.push({ - type: "tool_use", - id: tc.id, - name: toolName, - input: tryParseJSON(tc.function.arguments) - }); - } - } - } - } - - return blocks; -} - -// Convert OpenAI tool choice to Claude format -function convertOpenAIToolChoice(choice) { - if (!choice) return { type: "auto" }; - if (typeof choice === "object" && choice.type) return choice; - if (choice === "auto" || choice === "none") return { type: "auto" }; - if (choice === "required") return { type: "any" }; - if (typeof choice === "object" && choice.function) { - return { type: "tool", name: choice.function.name }; - } - return { type: "auto" }; -} - -// Extract text from content -function extractTextContent(content) { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content.filter(c => c.type === "text").map(c => c.text).join("\n"); - } - return ""; -} - -// Try parse JSON -function tryParseJSON(str) { - if (typeof str !== "string") return str; - try { - return JSON.parse(str); - } catch { - return str; - } -} - -// OpenAI -> Claude format for Antigravity (without system prompt modifications) -function openaiToClaudeRequestForAntigravity(model, body, stream) { - const result = openaiToClaudeRequest(model, body, stream); - - // Remove Claude Code system prompt, keep only user's system messages - if (result.system && Array.isArray(result.system)) { - result.system = result.system.filter(block => - !block.text || !block.text.includes("You are Claude Code") - ); - if (result.system.length === 0) { - delete result.system; - } - } - - // Strip prefix from tool names for Antigravity (doesn't use Claude OAuth) - if (result.tools && Array.isArray(result.tools)) { - result.tools = result.tools.map(tool => { - if (tool.name && tool.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) { - return { - ...tool, - name: tool.name.slice(CLAUDE_OAUTH_TOOL_PREFIX.length) - }; - } - return tool; - }); - } - - // Strip prefix from tool_use in messages - if (result.messages && Array.isArray(result.messages)) { - result.messages = result.messages.map(msg => { - if (!msg.content || !Array.isArray(msg.content)) { - return msg; - } - - const updatedContent = msg.content.map(block => { - if (block.type === "tool_use" && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) { - return { - ...block, - name: block.name.slice(CLAUDE_OAUTH_TOOL_PREFIX.length) - }; - } - return block; - }); - - return { ...msg, content: updatedContent }; - }); - } - - return result; -} - -// Export for use in other translators -export { openaiToClaudeRequestForAntigravity }; - -// Register -register(FORMATS.OPENAI, FORMATS.CLAUDE, openaiToClaudeRequest, null); - diff --git a/9router/open-sse/translator/request/openai-to-cursor.js b/9router/open-sse/translator/request/openai-to-cursor.js deleted file mode 100644 index a36f25cb..00000000 --- a/9router/open-sse/translator/request/openai-to-cursor.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * OpenAI to Cursor Request Translator - * Converts OpenAI messages to Cursor ask/agent format. - * - * Important: Cursor can loop when tool outputs are sent via protobuf tool_results - * with partial schema mismatches. For stability, tool outputs are represented as - * structured text blocks in user messages. - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -function extractContent(content) { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content - .filter(part => { - if (!part || typeof part !== "object") return false; - return part.type === "text" && typeof part.text === "string"; - }) - .map(part => part.text || "") - .join(""); - } - return ""; -} - -function sanitizeToolResultText(text) { - // Strip non-printable control chars that can produce backend request errors - return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ""); -} - -function escapeXml(text) { - return text.replace(/&/g, "&").replace(//g, ">"); -} - -function buildToolResultBlock(toolName, toolCallId, resultText) { - const cleanResult = sanitizeToolResultText(resultText || ""); - return [ - "", - `${escapeXml(toolName || "tool")}`, - `${escapeXml(toolCallId || "")}`, - `${escapeXml(cleanResult)}`, - "" - ].join("\n"); -} - -function normalizeToolCallId(id) { - return typeof id === "string" ? id.split("\n")[0] : ""; -} - -function convertMessages(messages) { - const result = []; - - // Build a map of tool_call_id -> tool name from assistant tool calls - const toolCallMetaMap = new Map(); - const rememberToolMeta = (toolCallId, toolName) => { - if (!toolCallId) return; - const name = toolName || "tool"; - toolCallMetaMap.set(toolCallId, { name }); - const normalized = normalizeToolCallId(toolCallId); - if (normalized && normalized !== toolCallId) { - toolCallMetaMap.set(normalized, { name }); - } - }; - - for (const msg of messages) { - if (msg.role === "assistant" && msg.tool_calls) { - for (const tc of msg.tool_calls) { - rememberToolMeta(tc.id || "", tc.function?.name || "tool"); - } - } - if (msg.role === "assistant" && Array.isArray(msg.content)) { - for (const part of msg.content) { - if (part?.type !== "tool_use") continue; - rememberToolMeta(part.id || "", part.name || "tool"); - } - } - } - - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - - if (msg.role === "system") { - result.push({ - role: "user", - content: `[System Instructions]\n${extractContent(msg.content)}` - }); - continue; - } - - if (msg.role === "tool") { - const toolContent = extractContent(msg.content); - const toolCallId = msg.tool_call_id || ""; - const toolMeta = toolCallMetaMap.get(toolCallId) || {}; - const toolName = msg.name || toolMeta.name || "tool"; - result.push({ - role: "user", - content: buildToolResultBlock(toolName, toolCallId, toolContent) - }); - continue; - } - - if (msg.role === "user" || msg.role === "assistant") { - if (msg.role === "user" && Array.isArray(msg.content)) { - const parts = []; - for (const block of msg.content) { - if (!block || typeof block !== "object") continue; - if (block.type === "text") { - if (typeof block.text === "string") { - parts.push(block.text || ""); - } - continue; - } - if (block.type === "tool_result") { - const toolCallId = block.tool_use_id || ""; - const toolMeta = - toolCallMetaMap.get(toolCallId) || - toolCallMetaMap.get(normalizeToolCallId(toolCallId)); - const toolName = toolMeta?.name || "tool"; - const toolContent = extractContent(block.content); - parts.push(buildToolResultBlock(toolName, toolCallId, toolContent)); - } - } - const joined = parts.filter(Boolean).join("\n"); - if (joined) result.push({ role: "user", content: joined }); - continue; - } - - const content = extractContent(msg.content); - - if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) { - const assistantMsg = { role: "assistant", content: content || "" }; - assistantMsg.tool_calls = msg.tool_calls.map(tc => { - const { index, ...rest } = tc || {}; - return rest; - }); - result.push(assistantMsg); - } else if (msg.role === "assistant" && Array.isArray(msg.content)) { - const extractedToolCalls = msg.content - .filter(b => b?.type === "tool_use") - .map(b => ({ - id: b.id || "", - type: "function", - function: { - name: b.name || "tool", - arguments: JSON.stringify(b.input || {}) - } - })) - .filter(tc => tc.id); - - if (extractedToolCalls.length > 0) { - result.push({ - role: "assistant", - content: content || "", - tool_calls: extractedToolCalls - }); - } else if (content) { - result.push({ role: "assistant", content }); - } - } else { - if (content) { - result.push({ role: msg.role, content }); - } - } - } - } - - return result; -} - -export function buildCursorRequest(model, body, stream, credentials) { - const messages = convertMessages(body.messages || []); - // Strip fields irrelevant to Cursor (OpenAI/Anthropic-specific) - const { user, metadata, tool_choice, stream_options, system, ...rest } = body; - return { - ...rest, - messages, - max_tokens: 32000 - }; -} - -register(FORMATS.OPENAI, FORMATS.CURSOR, buildCursorRequest, null); diff --git a/9router/open-sse/translator/request/openai-to-gemini.js b/9router/open-sse/translator/request/openai-to-gemini.js deleted file mode 100644 index 914e6b5a..00000000 --- a/9router/open-sse/translator/request/openai-to-gemini.js +++ /dev/null @@ -1,449 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js"; -import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js"; -import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js"; - -function generateUUID() { - return crypto.randomUUID(); -} - -import { - DEFAULT_SAFETY_SETTINGS, - convertOpenAIContentToParts, - extractTextContent, - tryParseJSON, - generateRequestId, - generateSessionId, - generateProjectId, - cleanJSONSchemaForAntigravity -} from "../helpers/geminiHelper.js"; -import { deriveSessionId } from "../../utils/sessionManager.js"; - -// Core: Convert OpenAI request to Gemini format (base for all variants) -function openaiToGeminiBase(model, body, stream) { - const result = { - model: model, - contents: [], - generationConfig: {}, - safetySettings: DEFAULT_SAFETY_SETTINGS - }; - - // Generation config - if (body.temperature !== undefined) { - result.generationConfig.temperature = body.temperature; - } - if (body.top_p !== undefined) { - result.generationConfig.topP = body.top_p; - } - if (body.top_k !== undefined) { - result.generationConfig.topK = body.top_k; - } - if (body.max_tokens !== undefined) { - result.generationConfig.maxOutputTokens = body.max_tokens; - } - - // Build tool_call_id -> name map - const tcID2Name = {}; - if (body.messages && Array.isArray(body.messages)) { - for (const msg of body.messages) { - if (msg.role === "assistant" && msg.tool_calls) { - for (const tc of msg.tool_calls) { - if (tc.type === "function" && tc.id && tc.function?.name) { - tcID2Name[tc.id] = tc.function.name; - } - } - } - } - } - - // Build tool responses cache - const toolResponses = {}; - if (body.messages && Array.isArray(body.messages)) { - for (const msg of body.messages) { - if (msg.role === "tool" && msg.tool_call_id) { - toolResponses[msg.tool_call_id] = msg.content; - } - } - } - - // Convert messages - if (body.messages && Array.isArray(body.messages)) { - for (let i = 0; i < body.messages.length; i++) { - const msg = body.messages[i]; - const role = msg.role; - const content = msg.content; - - if (role === "system" && body.messages.length > 1) { - result.systemInstruction = { - role: "user", - parts: [{ text: typeof content === "string" ? content : extractTextContent(content) }] - }; - } else if (role === "user" || (role === "system" && body.messages.length === 1)) { - const parts = convertOpenAIContentToParts(content); - if (parts.length > 0) { - result.contents.push({ role: "user", parts }); - } - } else if (role === "assistant") { - const parts = []; - - // Gemini 3 models require a valid per-session `thoughtSignature` - // on thinking blocks and function calls. Real signatures are lost - // during the Gemini→OpenAI→Claude→OpenAI→Gemini translation - // round-trip (no intermediate format has a field for them). - // - // Google's official workaround for proxy/translation layers: - // set thoughtSignature to the literal "skip_thought_signature_validator" - // which bypasses validation. Per Google's docs this is a "last - // resort" that "negatively impacts model performance" because the - // model can't build on its prior reasoning across turns — but it - // lets multi-turn tool use work through any translation layer. - // See: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thought-signatures - const SKIP_SIG = "skip_thought_signature_validator"; - - if (msg.reasoning_content) { - parts.push({ - thought: true, - text: msg.reasoning_content - }); - parts.push({ - thoughtSignature: SKIP_SIG, - text: "" - }); - } - - if (content) { - const text = typeof content === "string" ? content : extractTextContent(content); - if (text) { - parts.push({ text }); - } - } - - if (msg.tool_calls && Array.isArray(msg.tool_calls)) { - const toolCallIds = []; - for (const tc of msg.tool_calls) { - if (tc.type !== "function") continue; - - const args = tryParseJSON(tc.function?.arguments || "{}"); - parts.push({ - thoughtSignature: SKIP_SIG, - functionCall: { - id: tc.id, - name: tc.function.name, - args: args - } - }); - toolCallIds.push(tc.id); - } - - if (parts.length > 0) { - result.contents.push({ role: "model", parts }); - } - - // Check if there are actual tool responses in the next messages - const hasActualResponses = toolCallIds.some(fid => toolResponses[fid]); - - if (hasActualResponses) { - const toolParts = []; - for (const fid of toolCallIds) { - if (!toolResponses[fid]) continue; - - let name = tcID2Name[fid]; - if (!name) { - const idParts = fid.split("-"); - if (idParts.length > 2) { - name = idParts.slice(0, -2).join("-"); - } else { - name = fid; - } - } - - let resp = toolResponses[fid]; - let parsedResp = tryParseJSON(resp); - if (parsedResp === null) { - parsedResp = { result: resp }; - } else if (typeof parsedResp !== "object") { - parsedResp = { result: parsedResp }; - } - - toolParts.push({ - functionResponse: { - id: fid, - name: name, - response: { result: parsedResp } - } - }); - } - if (toolParts.length > 0) { - result.contents.push({ role: "user", parts: toolParts }); - } - } - } else if (parts.length > 0) { - result.contents.push({ role: "model", parts }); - } - } - } - } - - // Convert tools - if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) { - const functionDeclarations = []; - for (const t of body.tools) { - // Check if already in Anthropic/Claude format (no type field, direct name/description/input_schema) - if (t.name && t.input_schema) { - functionDeclarations.push({ - name: t.name, - description: t.description || "", - parameters: t.input_schema || { type: "object", properties: {} } - }); - } - // OpenAI format - else if (t.type === "function" && t.function) { - const fn = t.function; - functionDeclarations.push({ - name: fn.name, - description: fn.description || "", - parameters: fn.parameters || { type: "object", properties: {} } - }); - } - } - - if (functionDeclarations.length > 0) { - result.tools = [{ functionDeclarations }]; - } - } - - return result; -} - -// OpenAI -> Gemini (standard API) -export function openaiToGeminiRequest(model, body, stream) { - return openaiToGeminiBase(model, body, stream); -} - -// OpenAI -> Gemini CLI (Cloud Code Assist) -export function openaiToGeminiCLIRequest(model, body, stream) { - const gemini = openaiToGeminiBase(model, body, stream); - const isClaude = model.toLowerCase().includes("claude"); - - // Pass through thinking config from the incoming request. Gemini 3 - // models have thinking always-on (can't be disabled per Google's docs). - // The thought-signature round-trip issue is handled by using - // "skip_thought_signature_validator" on all function call and thinking - // parts in the conversation history (see assistant message handling - // above at lines ~87-125). This lets thinking work with the trade-off - // that the model can't build on prior reasoning across turns. - if (body.reasoning_effort) { - const budgetMap = { low: 1024, medium: 8192, high: 32768 }; - const budget = budgetMap[body.reasoning_effort] || 8192; - gemini.generationConfig.thinkingConfig = { - thinkingBudget: budget, - includeThoughts: true - }; - } - - if (body.thinking?.type === "enabled" && body.thinking.budget_tokens) { - gemini.generationConfig.thinkingConfig = { - thinkingBudget: body.thinking.budget_tokens, - includeThoughts: true - }; - } - - // Clean schema for tools - if (gemini.tools?.[0]?.functionDeclarations) { - for (const fn of gemini.tools[0].functionDeclarations) { - if (fn.parameters) { - const cleanedSchema = cleanJSONSchemaForAntigravity(fn.parameters); - fn.parameters = cleanedSchema; - // if (isClaude) { - // fn.parameters = cleanedSchema; - // } else { - // fn.parametersJsonSchema = cleanedSchema; - // delete fn.parameters; - // } - } - } - } - - return gemini; -} - -// Wrap Gemini CLI format in Cloud Code wrapper -function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigravity = false) { - const projectId = credentials?.projectId || generateProjectId(); - - const envelope = { - project: projectId, - model: model, - userAgent: isAntigravity ? "antigravity" : "gemini-cli", - requestId: isAntigravity ? `agent-${generateUUID()}` : generateRequestId(), - request: { - sessionId: isAntigravity ? deriveSessionId(credentials?.email || credentials?.connectionId) : generateSessionId(), - contents: geminiCLI.contents, - systemInstruction: geminiCLI.systemInstruction, - generationConfig: geminiCLI.generationConfig, - tools: geminiCLI.tools, - } - }; - - // Antigravity specific fields - if (isAntigravity) { - envelope.requestType = "agent"; - - // Inject required default system prompt for Antigravity - // Inject required default system prompt for Antigravity (double injection) - const systemParts = [ - { text: ANTIGRAVITY_DEFAULT_SYSTEM }, - { text: `Please ignore the following [ignore]${ANTIGRAVITY_DEFAULT_SYSTEM}[/ignore]` } - ]; - - if (envelope.request.systemInstruction?.parts) { - envelope.request.systemInstruction.parts.unshift(...systemParts); - } else { - envelope.request.systemInstruction = { role: "user", parts: systemParts }; - } - - // Add toolConfig for Antigravity - if (geminiCLI.tools?.length > 0) { - envelope.request.toolConfig = { - functionCallingConfig: { mode: "VALIDATED" } - }; - } - } else { - // Keep safetySettings for Gemini CLI - envelope.request.safetySettings = geminiCLI.safetySettings; - } - - return envelope; -} - -// Wrap Claude format in Cloud Code envelope for Antigravity -function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) { - const projectId = credentials?.projectId || generateProjectId(); - - const envelope = { - project: projectId, - model: model, - userAgent: "antigravity", - requestId: `agent-${generateUUID()}`, - requestType: "agent", - request: { - sessionId: deriveSessionId(credentials?.email || credentials?.connectionId), - contents: [], - generationConfig: { - temperature: claudeRequest.temperature || 1, - maxOutputTokens: claudeRequest.max_tokens || 4096 - } - } - }; - - // Convert Claude messages to Gemini contents - if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) { - for (const msg of claudeRequest.messages) { - const parts = []; - - if (Array.isArray(msg.content)) { - for (const block of msg.content) { - if (block.type === "text") { - parts.push({ text: block.text }); - } else if (block.type === "tool_use") { - parts.push({ - functionCall: { - id: block.id, - name: block.name, - args: block.input || {} - } - }); - } else if (block.type === "tool_result") { - let content = block.content; - if (Array.isArray(content)) { - content = content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n"); - } - parts.push({ - functionResponse: { - id: block.tool_use_id, - name: "unknown", - response: { result: tryParseJSON(content) || content } - } - }); - } - } - } else if (typeof msg.content === "string") { - parts.push({ text: msg.content }); - } - - if (parts.length > 0) { - envelope.request.contents.push({ - role: msg.role === "assistant" ? "model" : "user", - parts - }); - } - } - } - - // Convert Claude tools to Gemini functionDeclarations - if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) { - const functionDeclarations = []; - for (const tool of claudeRequest.tools) { - if (tool.name && tool.input_schema) { - const cleanedSchema = cleanJSONSchemaForAntigravity(tool.input_schema); - functionDeclarations.push({ - name: tool.name, - description: tool.description || "", - parameters: cleanedSchema - }); - } - } - if (functionDeclarations.length > 0) { - envelope.request.tools = [{ functionDeclarations }]; - envelope.request.toolConfig = { - functionCallingConfig: { mode: "VALIDATED" } - }; - } - } - - // Add system instruction (Antigravity default - double injection + user system prompt) - const systemParts = [ - { text: ANTIGRAVITY_DEFAULT_SYSTEM }, - { text: `Please ignore the following [ignore]${ANTIGRAVITY_DEFAULT_SYSTEM}[/ignore]` } - ]; - - // Merge user system prompt from claudeRequest - if (claudeRequest.system) { - if (Array.isArray(claudeRequest.system)) { - for (const block of claudeRequest.system) { - if (block.text) systemParts.push({ text: block.text }); - } - } else if (typeof claudeRequest.system === "string") { - systemParts.push({ text: claudeRequest.system }); - } - } - - // Merge existing systemInstruction parts (from contents conversion) - if (envelope.request.systemInstruction?.parts) { - envelope.request.systemInstruction.parts.unshift(...systemParts); - } else { - envelope.request.systemInstruction = { role: "user", parts: systemParts }; - } - - return envelope; -} - -// OpenAI -> Antigravity (Sandbox Cloud Code with wrapper) -export function openaiToAntigravityRequest(model, body, stream, credentials = null) { - const isClaude = model.toLowerCase().includes("claude"); - - if (isClaude) { - const claudeRequest = openaiToClaudeRequestForAntigravity(model, body, stream); - return wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials); - } - - const geminiCLI = openaiToGeminiCLIRequest(model, body, stream); - return wrapInCloudCodeEnvelope(model, geminiCLI, credentials, true); -} - -// Register -register(FORMATS.OPENAI, FORMATS.GEMINI, openaiToGeminiRequest, null); -register(FORMATS.OPENAI, FORMATS.GEMINI_CLI, (model, body, stream, credentials) => wrapInCloudCodeEnvelope(model, openaiToGeminiCLIRequest(model, body, stream), credentials), null); -register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravityRequest, null); - diff --git a/9router/open-sse/translator/request/openai-to-kiro.js b/9router/open-sse/translator/request/openai-to-kiro.js deleted file mode 100644 index c3880020..00000000 --- a/9router/open-sse/translator/request/openai-to-kiro.js +++ /dev/null @@ -1,306 +0,0 @@ -/** - * OpenAI to Kiro Request Translator - * Converts OpenAI Chat Completions format to Kiro/AWS CodeWhisperer format - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { v4 as uuidv4 } from "uuid"; - -/** - * Convert OpenAI messages to Kiro format - * Rules: system/tool/user -> user role, merge consecutive same roles - */ -function convertMessages(messages, tools, model) { - let history = []; - let currentMessage = null; - - let pendingUserContent = []; - let pendingAssistantContent = []; - let pendingToolResults = []; - let pendingImages = []; - let currentRole = null; - - const flushPending = () => { - if (currentRole === "user") { - const content = pendingUserContent.join("\n\n").trim() || "continue"; - const userMsg = { - userInputMessage: { - content: content, - modelId: "" - } - }; - - // Attach images if present (Kiro API supports images field) - if (pendingImages.length > 0) { - userMsg.userInputMessage.images = pendingImages; - } - - if (pendingToolResults.length > 0) { - userMsg.userInputMessage.userInputMessageContext = { - toolResults: pendingToolResults - }; - } - - // Add tools to first user message - if (tools && tools.length > 0 && history.length === 0) { - if (!userMsg.userInputMessage.userInputMessageContext) { - userMsg.userInputMessage.userInputMessageContext = {}; - } - userMsg.userInputMessage.userInputMessageContext.tools = tools.map(t => { - const name = t.function?.name || t.name; - let description = t.function?.description || t.description || ""; - - if (!description.trim()) { - description = `Tool: ${name}`; - } - - const schema = t.function?.parameters || t.parameters || t.input_schema || {}; - // Normalize schema: Kiro requires required[] and proper type/properties - const normalizedSchema = Object.keys(schema).length === 0 - ? { type: "object", properties: {}, required: [] } - : { ...schema, required: schema.required ?? [] }; - - return { - toolSpecification: { - name, - description, - inputSchema: { json: normalizedSchema } - } - }; - }); - } - - history.push(userMsg); - currentMessage = userMsg; - pendingUserContent = []; - pendingToolResults = []; - pendingImages = []; - } else if (currentRole === "assistant") { - const content = pendingAssistantContent.join("\n\n").trim() || "..."; - const assistantMsg = { - assistantResponseMessage: { - content: content - } - }; - history.push(assistantMsg); - pendingAssistantContent = []; - } - }; - - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - let role = msg.role; - - // Normalize: system/tool -> user - if (role === "system" || role === "tool") { - role = "user"; - } - - // If role changes, flush pending - if (role !== currentRole && currentRole !== null) { - flushPending(); - } - currentRole = role; - - if (role === "user") { - // Extract content - let content = ""; - if (typeof msg.content === "string") { - content = msg.content; - } else if (Array.isArray(msg.content)) { - const textParts = []; - for (const c of msg.content) { - if (c.type === "text" || c.text) { - textParts.push(c.text || ""); - } else if (c.type === "image_url") { - const url = c.image_url?.url || ""; - const base64Match = url.match(/^data:([^;]+);base64,(.+)$/); - if (base64Match) { - // Extract format from media type (e.g. "image/png" → "png") - const mediaType = base64Match[1]; - const format = mediaType.split("/")[1] || mediaType; - pendingImages.push({ format, source: { bytes: base64Match[2] } }); - } else if (url.startsWith("http://") || url.startsWith("https://")) { - // Kiro images field only supports base64 — fallback to URL text - textParts.push(`[Image: ${url}]`); - } - } - } - content = textParts.join("\n"); - - // Check for tool_result blocks - const toolResultBlocks = msg.content.filter(c => c.type === "tool_result"); - if (toolResultBlocks.length > 0) { - toolResultBlocks.forEach(block => { - const text = Array.isArray(block.content) - ? block.content.map(c => c.text || "").join("\n") - : (typeof block.content === "string" ? block.content : ""); - - pendingToolResults.push({ - toolUseId: block.tool_use_id, - status: "success", - content: [{ text: text }] - }); - }); - } - } - - // Handle tool role (from normalized) - if (msg.role === "tool") { - const toolContent = typeof msg.content === "string" ? msg.content : ""; - pendingToolResults.push({ - toolUseId: msg.tool_call_id, - status: "success", - content: [{ text: toolContent }] - }); - } else if (content) { - pendingUserContent.push(content); - } - } else if (role === "assistant") { - // Extract text content and tool uses - let textContent = ""; - let toolUses = []; - - if (Array.isArray(msg.content)) { - const textBlocks = msg.content.filter(c => c.type === "text"); - textContent = textBlocks.map(b => b.text).join("\n").trim(); - - const toolUseBlocks = msg.content.filter(c => c.type === "tool_use"); - toolUses = toolUseBlocks; - } else if (typeof msg.content === "string") { - textContent = msg.content.trim(); - } - - if (msg.tool_calls && msg.tool_calls.length > 0) { - toolUses = msg.tool_calls; - } - - if (textContent) { - pendingAssistantContent.push(textContent); - } - - // Store tool uses in last assistant message - if (toolUses.length > 0) { - if (pendingAssistantContent.length === 0) { - // pendingAssistantContent.push("Call tools"); - } - - // Flush to create assistant message with toolUses - flushPending(); - - const lastMsg = history[history.length - 1]; - if (lastMsg?.assistantResponseMessage) { - lastMsg.assistantResponseMessage.toolUses = toolUses.map(tc => { - if (tc.function) { - return { - toolUseId: tc.id || uuidv4(), - name: tc.function.name, - input: typeof tc.function.arguments === "string" - ? JSON.parse(tc.function.arguments) - : (tc.function.arguments || {}) - }; - } else { - return { - toolUseId: tc.id || uuidv4(), - name: tc.name, - input: tc.input || {} - }; - } - }); - } - - currentRole = null; - } - } - } - - // Flush remaining - if (currentRole !== null) { - flushPending(); - } - - // If last message in history is userInputMessage, use it as currentMessage - if (history.length > 0 && history[history.length - 1].userInputMessage) { - currentMessage = history.pop(); - } - - const firstHistoryItem = history[0]; - if (firstHistoryItem?.userInputMessage?.userInputMessageContext?.tools && - !currentMessage?.userInputMessage?.userInputMessageContext?.tools) { - if (!currentMessage.userInputMessage.userInputMessageContext) { - currentMessage.userInputMessage.userInputMessageContext = {}; - } - currentMessage.userInputMessage.userInputMessageContext.tools = - firstHistoryItem.userInputMessage.userInputMessageContext.tools; - } - - // Clean up history for Kiro API compatibility - history.forEach(item => { - if (item.userInputMessage?.userInputMessageContext?.tools) { - delete item.userInputMessage.userInputMessageContext.tools; - } - - if (item.userInputMessage?.userInputMessageContext && - Object.keys(item.userInputMessage.userInputMessageContext).length === 0) { - delete item.userInputMessage.userInputMessageContext; - } - - if (item.userInputMessage && !item.userInputMessage.modelId) { - item.userInputMessage.modelId = model; - } - }); - - return { history, currentMessage }; -} - -/** - * Build Kiro payload from OpenAI format - */ -export function buildKiroPayload(model, body, stream, credentials) { - const messages = body.messages || []; - const tools = body.tools || []; - const maxTokens = 32000; - const temperature = body.temperature; - const topP = body.top_p; - - const { history, currentMessage } = convertMessages(messages, tools, model); - - const profileArn = credentials?.providerSpecificData?.profileArn || ""; - - let finalContent = currentMessage?.userInputMessage?.content || ""; - const timestamp = new Date().toISOString(); - finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`; - - const payload = { - conversationState: { - chatTriggerType: "MANUAL", - conversationId: uuidv4(), - currentMessage: { - userInputMessage: { - content: finalContent, - modelId: model, - origin: "AI_EDITOR", - ...(currentMessage?.userInputMessage?.userInputMessageContext && { - userInputMessageContext: currentMessage.userInputMessage.userInputMessageContext - }) - } - }, - history: history - } - }; - - if (profileArn) { - payload.profileArn = profileArn; - } - - if (maxTokens || temperature !== undefined || topP !== undefined) { - payload.inferenceConfig = {}; - if (maxTokens) payload.inferenceConfig.maxTokens = maxTokens; - if (temperature !== undefined) payload.inferenceConfig.temperature = temperature; - if (topP !== undefined) payload.inferenceConfig.topP = topP; - } - - return payload; -} - -register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null); diff --git a/9router/open-sse/translator/request/openai-to-kiro.old.js b/9router/open-sse/translator/request/openai-to-kiro.old.js deleted file mode 100644 index 2474051f..00000000 --- a/9router/open-sse/translator/request/openai-to-kiro.old.js +++ /dev/null @@ -1,278 +0,0 @@ -/** - * OpenAI to Kiro Request Translator - * Converts OpenAI Chat Completions format to Kiro/AWS CodeWhisperer format - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; -import { v4 as uuidv4 } from "uuid"; - -/** - * Convert OpenAI messages to Kiro format - */ -function convertMessages(messages, tools, model) { - let history = []; - let currentMessage = null; - let systemPrompt = ""; - - const toolResultsMap = new Map(); - - for (const msg of messages) { - if (msg.role === "tool" && msg.tool_call_id) { - const content = typeof msg.content === "string" ? msg.content : - (Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : ""); - toolResultsMap.set(msg.tool_call_id, content); - } - - if (msg.role === "user" && Array.isArray(msg.content)) { - for (const block of msg.content) { - if (block.type === "tool_result" && block.tool_use_id) { - const content = Array.isArray(block.content) - ? block.content.map(c => c.text || "").join("\n") - : (typeof block.content === "string" ? block.content : ""); - toolResultsMap.set(block.tool_use_id, content); - } - } - } - } - - for (const msg of messages) { - const role = msg.role; - - if (role === "tool") continue; - - const content = typeof msg.content === "string" ? msg.content : - (Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : ""); - - if (role === "system") { - systemPrompt += (systemPrompt ? "\n" : "") + content; - continue; - } - - if (role === "user") { - let finalContent = content; - let toolResults = []; - - // Check if this user message contains tool_result blocks - if (Array.isArray(msg.content)) { - const toolResultBlocks = msg.content.filter(c => c.type === "tool_result"); - if (toolResultBlocks.length > 0) { - toolResults = toolResultBlocks.map(block => { - const text = Array.isArray(block.content) - ? block.content.map(c => c.text || "").join("\n") - : (typeof block.content === "string" ? block.content : ""); - - return { - toolUseId: block.tool_use_id, - status: "success", - content: [{ text: text }] - }; - }); - - // Set simple content when tool results exist - finalContent = content || "Continue"; - } - } - - const userMsg = { - userInputMessage: { - content: finalContent, - modelId: "", - } - }; - - // Add tool results to userInputMessageContext - if (toolResults.length > 0) { - if (!userMsg.userInputMessage.userInputMessageContext) { - userMsg.userInputMessage.userInputMessageContext = {}; - } - userMsg.userInputMessage.userInputMessageContext.toolResults = toolResults; - } - - // Add tools to first user message - if (tools && tools.length > 0 && history.length === 0) { - if (!userMsg.userInputMessage.userInputMessageContext) { - userMsg.userInputMessage.userInputMessageContext = {}; - } - userMsg.userInputMessage.userInputMessageContext.tools = tools.map(t => { - const name = t.function?.name || t.name; - let description = t.function?.description || t.description || ""; - - if (!description.trim()) { - description = `Tool: ${name}`; - } - - return { - toolSpecification: { - name, - description, - inputSchema: { - json: t.function?.parameters || t.parameters || t.input_schema || {} - } - } - }; - }); - } - - currentMessage = userMsg; - history.push(userMsg); - } - - if (role === "assistant") { - // Extract text content and tool uses separately from content array - let textContent = ""; - let toolUses = []; - - if (Array.isArray(msg.content)) { - const textBlocks = msg.content.filter(c => c.type === "text"); - textContent = textBlocks.map(b => b.text).join("\n").trim(); - - const toolUseBlocks = msg.content.filter(c => c.type === "tool_use"); - toolUses = toolUseBlocks; - } else if (typeof msg.content === "string") { - textContent = msg.content.trim(); - } - - // Fallback for OpenAI tool_calls format - if (msg.tool_calls && msg.tool_calls.length > 0) { - toolUses = msg.tool_calls; - } - - const assistantMsg = { - assistantResponseMessage: { - content: textContent || "Call tools" - } - }; - - if (toolUses.length > 0) { - assistantMsg.assistantResponseMessage.toolUses = toolUses.map(tc => { - if (tc.function) { - // OpenAI format - return { - toolUseId: tc.id || uuidv4(), - name: tc.function.name, - input: typeof tc.function.arguments === "string" - ? JSON.parse(tc.function.arguments) - : (tc.function.arguments || {}) - }; - } else { - // Anthropic format - return { - toolUseId: tc.id || uuidv4(), - name: tc.name, - input: tc.input || {} - }; - } - }); - } - - history.push(assistantMsg); - } - } - - // If last message in history is userInputMessage, use it as currentMessage - if (history.length > 0 && history[history.length - 1].userInputMessage) { - currentMessage = history.pop(); - } - - const firstHistoryItem = history[0]; - if (firstHistoryItem?.userInputMessage?.userInputMessageContext?.tools && - !currentMessage?.userInputMessage?.userInputMessageContext?.tools) { - if (!currentMessage.userInputMessage.userInputMessageContext) { - currentMessage.userInputMessage.userInputMessageContext = {}; - } - currentMessage.userInputMessage.userInputMessageContext.tools = - firstHistoryItem.userInputMessage.userInputMessageContext.tools; - } - - // Clean up history for Kiro API compatibility - history.forEach(item => { - if (item.userInputMessage?.userInputMessageContext?.tools) { - delete item.userInputMessage.userInputMessageContext.tools; - } - - if (item.userInputMessage?.userInputMessageContext && - Object.keys(item.userInputMessage.userInputMessageContext).length === 0) { - delete item.userInputMessage.userInputMessageContext; - } - - if (item.userInputMessage && !item.userInputMessage.modelId) { - item.userInputMessage.modelId = model; - } - }); - - // Merge consecutive user messages (Kiro requires alternating user/assistant) - const mergedHistory = []; - for (let i = 0; i < history.length; i++) { - const current = history[i]; - - if (current.userInputMessage && - mergedHistory.length > 0 && - mergedHistory[mergedHistory.length - 1].userInputMessage) { - const prev = mergedHistory[mergedHistory.length - 1]; - prev.userInputMessage.content += "\n\n" + current.userInputMessage.content; - } else { - mergedHistory.push(current); - } - } - history = mergedHistory; - - return { history, currentMessage, systemPrompt }; -} - -/** - * Build Kiro payload from OpenAI format - */ -function buildKiroPayload(model, body, stream, credentials) { - const messages = body.messages || []; - const tools = body.tools || []; - const maxTokens = 32000; - const temperature = body.temperature; - const topP = body.top_p; - - const { history, currentMessage, systemPrompt } = convertMessages(messages, tools, model); - - const profileArn = credentials?.providerSpecificData?.profileArn || ""; - - let finalContent = currentMessage?.userInputMessage?.content || ""; - if (systemPrompt) { - finalContent = `[System: ${systemPrompt}]\n\n${finalContent}`; - } - - const timestamp = new Date().toISOString(); - finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`; - - const payload = { - conversationState: { - chatTriggerType: "MANUAL", - conversationId: uuidv4(), - currentMessage: { - userInputMessage: { - content: finalContent, - modelId: model, - origin: "AI_EDITOR", - ...(currentMessage?.userInputMessage?.userInputMessageContext && { - userInputMessageContext: currentMessage.userInputMessage.userInputMessageContext - }) - } - }, - history: history - } - }; - - if (profileArn) { - payload.profileArn = profileArn; - } - - if (maxTokens || temperature !== undefined || topP !== undefined) { - payload.inferenceConfig = {}; - if (maxTokens) payload.inferenceConfig.maxTokens = maxTokens; - if (temperature !== undefined) payload.inferenceConfig.temperature = temperature; - if (topP !== undefined) payload.inferenceConfig.topP = topP; - } - - return payload; -} - -register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null); - -export { buildKiroPayload }; diff --git a/9router/open-sse/translator/request/openai-to-ollama.js b/9router/open-sse/translator/request/openai-to-ollama.js deleted file mode 100644 index 6a316ead..00000000 --- a/9router/open-sse/translator/request/openai-to-ollama.js +++ /dev/null @@ -1,159 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -/** - * Convert OpenAI request to Ollama format - * - * Ollama expects: - * - model: string - * - messages: Array<{role: string, content: string}> - * - stream: boolean - * - options?: {temperature?: number, num_predict?: number} - * - * Key differences from OpenAI: - * - Content must be string, not array - * - No support for tool_calls in request (tools are handled differently) - * - tool role maps to user - */ -export function openaiToOllamaRequest(model, body, stream) { - const result = { - model: model, - messages: normalizeMessages(body.messages), - stream: stream - }; - - // Temperature - if (body.temperature !== undefined) { - result.options = result.options || {}; - result.options.temperature = body.temperature; - } - - // Max tokens (Ollama uses num_predict) - if (body.max_tokens !== undefined) { - result.options = result.options || {}; - result.options.num_predict = body.max_tokens; - } - - // Top_p - if (body.top_p !== undefined) { - result.options = result.options || {}; - result.options.top_p = body.top_p; - } - - // Tools (Ollama supports tools in OpenAI format) - if (body.tools && Array.isArray(body.tools)) { - result.tools = body.tools; - } - - // Tool choice - if (body.tool_choice) { - result.tool_choice = body.tool_choice; - } - - return result; -} - -/** - * Normalize messages to Ollama format - * - Content must be string - * - tool messages: convert tool_call_id to tool_name - * - assistant messages: keep tool_calls as-is - */ -function normalizeMessages(messages) { - if (!Array.isArray(messages)) return messages; - - const result = []; - const toolCallMap = new Map(); // Map tool_call_id -> tool_name - - // First pass: build tool_call_id -> tool_name map from assistant messages - for (const msg of messages) { - if (msg.role === "assistant" && msg.tool_calls) { - for (const tc of msg.tool_calls) { - if (tc.id && tc.function?.name) { - toolCallMap.set(tc.id, tc.function.name); - } - } - } - } - - // Second pass: convert messages - for (const msg of messages) { - // Handle tool result messages (OpenAI format -> Ollama format) - if (msg.role === "tool") { - const toolResult = normalizeContent(msg.content); - if (!toolResult) continue; - - // Get tool_name from map or use msg.name as fallback - const toolName = toolCallMap.get(msg.tool_call_id) || msg.name || "unknown_tool"; - - result.push({ - role: "tool", - tool_name: toolName, - content: toolResult - }); - continue; - } - - // Handle assistant messages with tool_calls - if (msg.role === "assistant" && msg.tool_calls) { - const content = normalizeContent(msg.content) || ""; - - // Convert OpenAI tool_calls format to Ollama format - const ollamaToolCalls = msg.tool_calls.map(tc => ({ - type: "function", - function: { - index: tc.index || 0, - name: tc.function?.name || "", - arguments: typeof tc.function?.arguments === "string" - ? JSON.parse(tc.function.arguments || "{}") - : tc.function?.arguments || {} - } - })); - - result.push({ - role: "assistant", - content: content, - tool_calls: ollamaToolCalls - }); - continue; - } - - // Normal messages - const role = msg.role; - const content = normalizeContent(msg.content); - - // Skip empty messages (except assistant) - if (!content && role !== "assistant") continue; - - result.push({ - role: role, - content: content - }); - } - - return result; -} - -/** - * Normalize content to string - * Ollama only accepts string content - */ -function normalizeContent(content) { - if (typeof content === "string") { - return content; - } - - if (Array.isArray(content)) { - // Extract text from content array - const textParts = content - .filter(block => block && block.type === "text" && block.text) - .map(block => block.text); - - return textParts.join("\n") || ""; - } - - return ""; -} - -// Register translator -register(FORMATS.OPENAI, FORMATS.OLLAMA, openaiToOllamaRequest, null); diff --git a/9router/open-sse/translator/response/claude-to-openai.js b/9router/open-sse/translator/response/claude-to-openai.js deleted file mode 100644 index 2dfe0dbb..00000000 --- a/9router/open-sse/translator/response/claude-to-openai.js +++ /dev/null @@ -1,206 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -// Create OpenAI chunk helper -function createChunk(state, delta, finishReason = null) { - return { - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta, - finish_reason: finishReason - }] - }; -} - -// Convert Claude stream chunk to OpenAI format -export function claudeToOpenAIResponse(chunk, state) { - if (!chunk) return null; - - const results = []; - const event = chunk.type; - - switch (event) { - case "message_start": { - state.messageId = chunk.message?.id || `msg_${Date.now()}`; - state.model = chunk.message?.model; - state.toolCallIndex = 0; - results.push(createChunk(state, { role: "assistant" })); - break; - } - - case "content_block_start": { - const block = chunk.content_block; - if (block?.type === "server_tool_use") { - // Built-in tool (web search) - Claude handles internally, skip - state.serverToolBlockIndex = chunk.index; - break; - } - if (block?.type === "text") { - state.textBlockStarted = true; - } else if (block?.type === "thinking") { - state.inThinkingBlock = true; - state.currentBlockIndex = chunk.index; - results.push(createChunk(state, { content: "" })); - } else if (block?.type === "tool_use") { - const toolCallIndex = state.toolCallIndex++; - // Restore original tool name from mapping (Claude OAuth) - const toolName = state.toolNameMap?.get(block.name) || block.name; - const toolCall = { - index: toolCallIndex, - id: block.id, - type: "function", - function: { - name: toolName, - arguments: "" - } - }; - state.toolCalls.set(chunk.index, toolCall); - results.push(createChunk(state, { tool_calls: [toolCall] })); - } - break; - } - - case "content_block_delta": { - // Skip deltas for built-in server tool blocks (web search) - if (chunk.index === state.serverToolBlockIndex) break; - const delta = chunk.delta; - if (delta?.type === "text_delta" && delta.text) { - results.push(createChunk(state, { content: delta.text })); - } else if (delta?.type === "thinking_delta" && delta.thinking) { - results.push(createChunk(state, { reasoning_content: delta.thinking })); - } else if (delta?.type === "input_json_delta" && delta.partial_json) { - const toolCall = state.toolCalls.get(chunk.index); - if (toolCall) { - toolCall.function.arguments += delta.partial_json; - results.push(createChunk(state, { - tool_calls: [{ - index: toolCall.index, - id: toolCall.id, - function: { arguments: delta.partial_json } - }] - })); - } - } - break; - } - - case "content_block_stop": { - // Skip stop for built-in server tool blocks (web search) - if (chunk.index === state.serverToolBlockIndex) { - state.serverToolBlockIndex = -1; - break; - } - if (state.inThinkingBlock && chunk.index === state.currentBlockIndex) { - results.push(createChunk(state, { reasoning_content: "" })); - state.inThinkingBlock = false; - } - state.textBlockStarted = false; - state.thinkingBlockStarted = false; - break; - } - - case "message_delta": { - // Extract usage from message_delta event (Claude native format) - // Normalize to OpenAI format (prompt_tokens/completion_tokens) for consistent logging - if (chunk.usage && typeof chunk.usage === "object") { - const inputTokens = typeof chunk.usage.input_tokens === "number" ? chunk.usage.input_tokens : 0; - const outputTokens = typeof chunk.usage.output_tokens === "number" ? chunk.usage.output_tokens : 0; - const cacheReadTokens = typeof chunk.usage.cache_read_input_tokens === "number" ? chunk.usage.cache_read_input_tokens : 0; - const cacheCreationTokens = typeof chunk.usage.cache_creation_input_tokens === "number" ? chunk.usage.cache_creation_input_tokens : 0; - - // prompt_tokens = input_tokens + cache_read + cache_creation (all prompt-side tokens) - const promptTokens = inputTokens + cacheReadTokens + cacheCreationTokens; - - state.usage = { - prompt_tokens: promptTokens, - completion_tokens: outputTokens, - total_tokens: promptTokens + outputTokens, - input_tokens: inputTokens, - output_tokens: outputTokens - }; - - if (cacheReadTokens > 0) state.usage.cache_read_input_tokens = cacheReadTokens; - if (cacheCreationTokens > 0) state.usage.cache_creation_input_tokens = cacheCreationTokens; - } - - if (chunk.delta?.stop_reason) { - state.finishReason = convertStopReason(chunk.delta.stop_reason); - const finalChunk = { - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ index: 0, delta: {}, finish_reason: state.finishReason }] - }; - - if (state.usage) { - finalChunk.usage = { - prompt_tokens: state.usage.prompt_tokens, - completion_tokens: state.usage.completion_tokens, - total_tokens: state.usage.total_tokens - }; - - const cacheRead = state.usage.cache_read_input_tokens; - const cacheCreate = state.usage.cache_creation_input_tokens; - if (cacheRead > 0 || cacheCreate > 0) { - finalChunk.usage.prompt_tokens_details = {}; - if (cacheRead > 0) finalChunk.usage.prompt_tokens_details.cached_tokens = cacheRead; - if (cacheCreate > 0) finalChunk.usage.prompt_tokens_details.cache_creation_tokens = cacheCreate; - } - } - - results.push(finalChunk); - state.finishReasonSent = true; - } - break; - } - - case "message_stop": { - if (!state.finishReasonSent) { - const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? "tool_calls" : "stop"); - const usageObj = (state.usage && typeof state.usage === 'object') ? { - usage: { - prompt_tokens: state.usage.input_tokens || 0, - completion_tokens: state.usage.output_tokens || 0, - total_tokens: (state.usage.input_tokens || 0) + (state.usage.output_tokens || 0) - } - } : {}; - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: {}, - finish_reason: finishReason - }], - ...usageObj - }); - state.finishReasonSent = true; - } - break; - } - } - - return results.length > 0 ? results : null; -} - -// Convert Claude stop_reason to OpenAI finish_reason -function convertStopReason(reason) { - switch (reason) { - case "end_turn": return "stop"; - case "max_tokens": return "length"; - case "tool_use": return "tool_calls"; - case "stop_sequence": return "stop"; - default: return "stop"; - } -} - -// Register -register(FORMATS.CLAUDE, FORMATS.OPENAI, null, claudeToOpenAIResponse); - diff --git a/9router/open-sse/translator/response/cursor-to-openai.js b/9router/open-sse/translator/response/cursor-to-openai.js deleted file mode 100644 index b2546918..00000000 --- a/9router/open-sse/translator/response/cursor-to-openai.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Cursor to OpenAI Response Translator - * CursorExecutor already emits OpenAI format - this is a passthrough - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -/** - * Convert Cursor response to OpenAI format - * Since CursorExecutor.transformProtobufToSSE/JSON already emits OpenAI chunks, - * this is a passthrough translator (similar to Kiro pattern) - */ -export function convertCursorToOpenAI(chunk, state) { - if (!chunk) return null; - - // If chunk is already in OpenAI format (from executor transform), return as-is - if (chunk.object === "chat.completion.chunk" && chunk.choices) { - return chunk; - } - - // If chunk is a completion object (non-streaming), return as-is - if (chunk.object === "chat.completion" && chunk.choices) { - return chunk; - } - - // Fallback: return chunk as-is (should not reach here) - return chunk; -} - -register(FORMATS.CURSOR, FORMATS.OPENAI, null, convertCursorToOpenAI); diff --git a/9router/open-sse/translator/response/gemini-to-openai.js b/9router/open-sse/translator/response/gemini-to-openai.js deleted file mode 100644 index 000cc1f7..00000000 --- a/9router/open-sse/translator/response/gemini-to-openai.js +++ /dev/null @@ -1,240 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -// Convert Gemini response chunk to OpenAI format -export function geminiToOpenAIResponse(chunk, state) { - if (!chunk) return null; - - // Handle Antigravity wrapper - const response = chunk.response || chunk; - if (!response || !response.candidates?.[0]) return null; - - const results = []; - const candidate = response.candidates[0]; - const content = candidate.content; - - // Initialize state - if (!state.messageId) { - state.messageId = response.responseId || `msg_${Date.now()}`; - state.model = response.modelVersion || "gemini"; - state.functionIndex = 0; - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: { role: "assistant" }, - finish_reason: null - }] - }); - } - - // Process parts - if (content?.parts) { - for (const part of content.parts) { - const hasThoughtSig = part.thoughtSignature || part.thought_signature; - const isThought = part.thought === true; - - // Handle thought signature (thinking mode) - if (hasThoughtSig) { - const hasTextContent = part.text !== undefined && part.text !== ""; - const hasFunctionCall = !!part.functionCall; - - if (hasTextContent) { - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: isThought - ? { reasoning_content: part.text } - : { content: part.text }, - finish_reason: null - }] - }); - } - - if (hasFunctionCall) { - const fcName = part.functionCall.name; - const fcArgs = part.functionCall.args || {}; - const toolCallIndex = state.functionIndex++; - - const toolCall = { - id: `${fcName}-${Date.now()}-${toolCallIndex}`, - index: toolCallIndex, - type: "function", - function: { - name: fcName, - arguments: JSON.stringify(fcArgs) - } - }; - - state.toolCalls.set(toolCallIndex, toolCall); - - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: { tool_calls: [toolCall] }, - finish_reason: null - }] - }); - } - continue; - } - - // Text content (non-thinking) - if (part.text !== undefined && part.text !== "") { - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: { content: part.text }, - finish_reason: null - }] - }); - } - - // Function call - if (part.functionCall) { - const fcName = part.functionCall.name; - const fcArgs = part.functionCall.args || {}; - const toolCallIndex = state.functionIndex++; - - const toolCall = { - id: `${fcName}-${Date.now()}-${toolCallIndex}`, - index: toolCallIndex, - type: "function", - function: { - name: fcName, - arguments: JSON.stringify(fcArgs) - } - }; - - state.toolCalls.set(toolCallIndex, toolCall); - - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: { tool_calls: [toolCall] }, - finish_reason: null - }] - }); - } - - // Inline data (images) - const inlineData = part.inlineData || part.inline_data; - if (inlineData?.data) { - const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png"; - results.push({ - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: { - images: [{ - type: "image_url", - image_url: { url: `data:${mimeType};base64,${inlineData.data}` } - }] - }, - finish_reason: null - }] - }); - } - } - } - - // Usage metadata - extract before finish reason so we can include it - const usageMeta = response.usageMetadata || chunk.usageMetadata; - if (usageMeta && typeof usageMeta === "object") { - const cachedTokens = typeof usageMeta.cachedContentTokenCount === "number" ? usageMeta.cachedContentTokenCount : 0; - const promptTokenCountRaw = typeof usageMeta.promptTokenCount === "number" ? usageMeta.promptTokenCount : 0; - const thoughtsTokens = typeof usageMeta.thoughtsTokenCount === "number" ? usageMeta.thoughtsTokenCount : 0; - let candidatesTokens = typeof usageMeta.candidatesTokenCount === "number" ? usageMeta.candidatesTokenCount : 0; - const totalTokens = typeof usageMeta.totalTokenCount === "number" ? usageMeta.totalTokenCount : 0; - - // prompt_tokens = promptTokenCount (includes cached tokens, matching claude-to-openai.js behavior) - const promptTokens = promptTokenCountRaw; - - // Fallback calculation if candidatesTokenCount is 0 but totalTokenCount exists - if (candidatesTokens === 0 && totalTokens > 0) { - candidatesTokens = totalTokens - promptTokenCountRaw - thoughtsTokens; - if (candidatesTokens < 0) candidatesTokens = 0; - } - - // completion_tokens = candidatesTokenCount + thoughtsTokenCount (match Go code) - const completionTokens = candidatesTokens + thoughtsTokens; - - state.usage = { - prompt_tokens: promptTokens, - completion_tokens: completionTokens, - total_tokens: totalTokens - }; - - // Add prompt_tokens_details if cached tokens exist - if (cachedTokens > 0) { - state.usage.prompt_tokens_details = { - cached_tokens: cachedTokens - }; - } - - // Add completion_tokens_details if reasoning tokens exist - if (thoughtsTokens > 0) { - state.usage.completion_tokens_details = { - reasoning_tokens: thoughtsTokens - }; - } - } - - // Finish reason - include usage in final chunk - if (candidate.finishReason) { - let finishReason = candidate.finishReason.toLowerCase(); - if (finishReason === "stop" && state.toolCalls.size > 0) { - finishReason = "tool_calls"; - } - - const finalChunk = { - id: `chatcmpl-${state.messageId}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: state.model, - choices: [{ - index: 0, - delta: {}, - finish_reason: finishReason - }] - }; - - // Include usage in final chunk for downstream translators - if (state.usage) { - finalChunk.usage = state.usage; - } - - results.push(finalChunk); - state.finishReason = finishReason; - } - - return results.length > 0 ? results : null; -} - -// Register -register(FORMATS.GEMINI, FORMATS.OPENAI, null, geminiToOpenAIResponse); -register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, null, geminiToOpenAIResponse); -register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, null, geminiToOpenAIResponse); - diff --git a/9router/open-sse/translator/response/kiro-to-openai.js b/9router/open-sse/translator/response/kiro-to-openai.js deleted file mode 100644 index 9454c572..00000000 --- a/9router/open-sse/translator/response/kiro-to-openai.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Kiro to OpenAI Response Translator - * Converts Kiro/AWS CodeWhisperer streaming events to OpenAI SSE format - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -/** - * Parse Kiro SSE event and convert to OpenAI format - * Kiro events: assistantResponseEvent, codeEvent, supplementaryWebLinksEvent, etc. - */ -export function convertKiroToOpenAI(chunk, state) { - - if (!chunk) return null; - - // If chunk is already in OpenAI format (from executor transform), return as-is - if (chunk.object === "chat.completion.chunk" && chunk.choices) { - return chunk; - } - - // Handle string chunk (raw SSE data) - let data = chunk; - if (typeof chunk === "string") { - // Parse SSE format: event:xxx\ndata:xxx - const lines = chunk.split("\n"); - let eventType = ""; - let eventData = ""; - - for (const line of lines) { - if (line.startsWith("event:")) { - eventType = line.slice(6).trim(); - } else if (line.startsWith(":event-type:")) { - eventType = line.slice(12).trim(); - } else if (line.startsWith("data:")) { - eventData = line.slice(5).trim(); - } else if (line.startsWith(":content-type:")) { - // Skip content-type header - } else if (line.trim() && !line.startsWith(":")) { - // Raw JSON data - eventData = line.trim(); - } - } - - if (!eventData) return null; - - try { - data = JSON.parse(eventData); - data._eventType = eventType; - } catch { - // Not JSON, might be raw text - data = { text: eventData, _eventType: eventType }; - } - } - - // Initialize state if needed - if (!state.responseId) { - state.responseId = `chatcmpl-${Date.now()}`; - state.created = Math.floor(Date.now() / 1000); - state.chunkIndex = 0; - } - - const eventType = data._eventType || data.event || ""; - - // Handle different Kiro event types - if (eventType === "assistantResponseEvent" || data.assistantResponseEvent) { - const content = data.assistantResponseEvent?.content || data.content || ""; - if (!content) return null; - - const openaiChunk = { - id: state.responseId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "kiro", - choices: [{ - index: 0, - delta: { - ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), - content: content - }, - finish_reason: null - }] - }; - - state.chunkIndex++; - return openaiChunk; - } - - // Handle reasoning/thinking events - if (eventType === "reasoningContentEvent" || data.reasoningContentEvent) { - const content = data.reasoningContentEvent?.content || data.content || ""; - if (!content) return null; - - // Convert to thinking block format (Claude-style) - const openaiChunk = { - id: state.responseId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "kiro", - choices: [{ - index: 0, - delta: { - ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), - content: `${content}` - }, - finish_reason: null - }] - }; - - state.chunkIndex++; - return openaiChunk; - } - - // Handle tool use events - if (eventType === "toolUseEvent" || data.toolUseEvent) { - const toolUse = data.toolUseEvent || data; - const toolCallId = toolUse.toolUseId || `call_${Date.now()}`; - const toolName = toolUse.name || ""; - const toolInput = toolUse.input || {}; - - const openaiChunk = { - id: state.responseId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "kiro", - choices: [{ - index: 0, - delta: { - ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), - tool_calls: [{ - index: 0, - id: toolCallId, - type: "function", - function: { - name: toolName, - arguments: JSON.stringify(toolInput) - } - }] - }, - finish_reason: null - }] - }; - - state.chunkIndex++; - return openaiChunk; - } - - // Handle completion/done events - if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) { - state.finishReason = "stop"; // Mark for usage injection in stream.js - - const openaiChunk = { - id: state.responseId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "kiro", - choices: [{ - index: 0, - delta: {}, - finish_reason: "stop" - }] - }; - - // Include usage in final chunk if available - if (state.usage && typeof state.usage === "object") { - openaiChunk.usage = state.usage; - } - - return openaiChunk; - } - -// Handle usage events - if (eventType === "usageEvent" || data.usageEvent) { - const usage = data.usageEvent || data; - if (usage && typeof usage === 'object') { - state.usage = { - prompt_tokens: usage.inputTokens || 0, - completion_tokens: usage.outputTokens || 0, - total_tokens: (usage.inputTokens || 0) + (usage.outputTokens || 0) - }; - } - return null; - } - - // Unknown event type - skip - return null; -} - -// Register translator -register(FORMATS.KIRO, FORMATS.OPENAI, null, convertKiroToOpenAI); diff --git a/9router/open-sse/translator/response/ollama-to-openai.js b/9router/open-sse/translator/response/ollama-to-openai.js deleted file mode 100644 index 9cd5b888..00000000 --- a/9router/open-sse/translator/response/ollama-to-openai.js +++ /dev/null @@ -1,152 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -/** - * Convert Ollama NDJSON response to OpenAI SSE format - * - * Ollama response format: - * {"model": "...", "message": {"role": "assistant", "content": "..."}, "done": false} - * {"model": "...", "done": true, "prompt_eval_count": 123, "eval_count": 456} - * - * OpenAI format: - * {"id": "...", "object": "chat.completion.chunk", "created": 123, "model": "...", - * "choices": [{"index": 0, "delta": {"content": "..."}, "finish_reason": null}]} - */ -export function ollamaToOpenAI(chunk, state) { - if (!chunk || typeof chunk !== "object") return null; - - // Initialize state on first chunk - if (!state.ollama) { - state.ollama = { - id: `chatcmpl-${Date.now()}`, - created: Math.floor(Date.now() / 1000), - model: chunk.model || state.model - }; - } - - const { id, created, model } = state.ollama; - - // Final chunk with done=true - if (chunk.done) { - const usage = extractUsage(chunk); - - // Determine finish_reason based on done_reason and previous tool_calls - let finishReason = "stop"; - if (chunk.done_reason === "tool_calls" || state.hadToolCalls) { - finishReason = "tool_calls"; - } - - return { - id: id, - object: "chat.completion.chunk", - created: created, - model: model, - choices: [{ - index: 0, - delta: {}, - finish_reason: finishReason - }], - usage: usage - }; - } - - // Content chunk - const message = chunk.message; - if (!message) return null; - - const content = typeof message.content === "string" ? message.content : ""; - const thinking = typeof message.thinking === "string" ? message.thinking : ""; - const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : null; - - // Skip empty chunks - if (!content && !thinking && !toolCalls) return null; - - // Accumulate content in state - if (content) { - state.accumulatedContent = (state.accumulatedContent || "") + content; - } - if (thinking) { - state.accumulatedThinking = (state.accumulatedThinking || "") + thinking; - } - - const delta = {}; - if (content) delta.content = content; - if (thinking) delta.reasoning_content = thinking; - - // Convert Ollama tool_calls to OpenAI format - if (toolCalls) { - state.hadToolCalls = true; - delta.tool_calls = convertToolCalls(toolCalls); - } - - return { - id: id, - object: "chat.completion.chunk", - created: created, - model: model, - choices: [{ - index: 0, - delta: delta, - finish_reason: null - }] - }; -} - -/** - * Extract usage stats from Ollama response - */ -function extractUsage(ollamaChunk) { - return { - prompt_tokens: ollamaChunk.prompt_eval_count || 0, - completion_tokens: ollamaChunk.eval_count || 0, - total_tokens: (ollamaChunk.prompt_eval_count || 0) + (ollamaChunk.eval_count || 0) - }; -} - -/** - * Convert tool_calls from Ollama format to OpenAI format - */ -function convertToolCalls(toolCalls) { - return toolCalls.map((tc, i) => ({ - index: tc.function?.index ?? i, - id: tc.id || `call_${i}_${Date.now()}`, - type: "function", - function: { - name: tc.function?.name || "", - arguments: typeof tc.function?.arguments === "string" - ? tc.function.arguments - : JSON.stringify(tc.function?.arguments || {}) - } - })); -} - -/** - * Convert Ollama non-streaming response body to OpenAI chat.completion format - */ -export function ollamaBodyToOpenAI(body) { - const msg = body.message || {}; - const content = msg.content || ""; - const thinking = msg.thinking || ""; - const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : []; - - const message = { role: "assistant" }; - if (content) message.content = content; - if (thinking) message.reasoning_content = thinking; - if (toolCalls.length > 0) message.tool_calls = convertToolCalls(toolCalls); - if (!message.content && !message.tool_calls) message.content = ""; - - let finishReason = body.done_reason || "stop"; - if (toolCalls.length > 0) finishReason = "tool_calls"; - - return { - id: `chatcmpl-${Date.now()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: body.model || "ollama", - choices: [{ index: 0, message, finish_reason: finishReason }], - usage: extractUsage(body) - }; -} - -// Register translator -register(FORMATS.OLLAMA, FORMATS.OPENAI, null, ollamaToOpenAI); diff --git a/9router/open-sse/translator/response/openai-responses.js b/9router/open-sse/translator/response/openai-responses.js deleted file mode 100644 index 671de332..00000000 --- a/9router/open-sse/translator/response/openai-responses.js +++ /dev/null @@ -1,571 +0,0 @@ -/** - * Translator: OpenAI Chat Completions → OpenAI Responses API (response) - * Converts streaming chunks from Chat Completions to Responses API events - */ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -/** - * Translate OpenAI chunk to Responses API events - * @returns {Array} Array of events with { event, data } structure - */ -export function openaiToOpenAIResponsesResponse(chunk, state) { - if (!chunk) { - return flushEvents(state); - } - - if (!chunk.choices?.length) return []; - - const events = []; - const nextSeq = () => ++state.seq; - - const emit = (eventType, data) => { - data.sequence_number = nextSeq(); - events.push({ event: eventType, data }); - }; - - const choice = chunk.choices[0]; - const idx = choice.index || 0; - const delta = choice.delta || {}; - - // Emit initial events - if (!state.started) { - state.started = true; - state.responseId = chunk.id ? `resp_${chunk.id}` : state.responseId; - - emit("response.created", { - type: "response.created", - response: { - id: state.responseId, - object: "response", - created_at: state.created, - status: "in_progress", - background: false, - error: null, - output: [] - } - }); - - emit("response.in_progress", { - type: "response.in_progress", - response: { - id: state.responseId, - object: "response", - created_at: state.created, - status: "in_progress" - } - }); - } - - // Handle reasoning_content - if (delta.reasoning_content) { - startReasoning(state, emit, idx); - emitReasoningDelta(state, emit, delta.reasoning_content); - } - - // Handle text content - if (delta.content) { - let content = delta.content; - - if (content.includes("")) { - state.inThinking = true; - content = content.replace("", ""); - startReasoning(state, emit, idx); - } - - if (content.includes("")) { - const parts = content.split(""); - const thinkPart = parts[0]; - const textPart = parts.slice(1).join(""); - if (thinkPart) emitReasoningDelta(state, emit, thinkPart); - closeReasoning(state, emit); - state.inThinking = false; - content = textPart; - } - - if (state.inThinking && content) { - emitReasoningDelta(state, emit, content); - return events; - } - - if (content) { - emitTextContent(state, emit, idx, content); - } - } - - // Handle tool_calls - if (delta.tool_calls) { - closeMessage(state, emit, idx); - for (const tc of delta.tool_calls) { - emitToolCall(state, emit, tc); - } - } - - // Handle finish_reason - if (choice.finish_reason) { - for (const i in state.msgItemAdded) closeMessage(state, emit, i); - closeReasoning(state, emit); - for (const i in state.funcCallIds) closeToolCall(state, emit, i); - sendCompleted(state, emit); - } - - return events; -} - -// Helper functions -function startReasoning(state, emit, idx) { - if (!state.reasoningId) { - state.reasoningId = `rs_${state.responseId}_${idx}`; - state.reasoningIndex = idx; - - emit("response.output_item.added", { - type: "response.output_item.added", - output_index: idx, - item: { id: state.reasoningId, type: "reasoning", summary: [] } - }); - - emit("response.reasoning_summary_part.added", { - type: "response.reasoning_summary_part.added", - item_id: state.reasoningId, - output_index: idx, - summary_index: 0, - part: { type: "summary_text", text: "" } - }); - state.reasoningPartAdded = true; - } -} - -function emitReasoningDelta(state, emit, text) { - if (!text) return; - state.reasoningBuf += text; - emit("response.reasoning_summary_text.delta", { - type: "response.reasoning_summary_text.delta", - item_id: state.reasoningId, - output_index: state.reasoningIndex, - summary_index: 0, - delta: text - }); -} - -function closeReasoning(state, emit) { - if (state.reasoningId && !state.reasoningDone) { - state.reasoningDone = true; - - emit("response.reasoning_summary_text.done", { - type: "response.reasoning_summary_text.done", - item_id: state.reasoningId, - output_index: state.reasoningIndex, - summary_index: 0, - text: state.reasoningBuf - }); - - emit("response.reasoning_summary_part.done", { - type: "response.reasoning_summary_part.done", - item_id: state.reasoningId, - output_index: state.reasoningIndex, - summary_index: 0, - part: { type: "summary_text", text: state.reasoningBuf } - }); - - emit("response.output_item.done", { - type: "response.output_item.done", - output_index: state.reasoningIndex, - item: { - id: state.reasoningId, - type: "reasoning", - summary: [{ type: "summary_text", text: state.reasoningBuf }] - } - }); - } -} - -function emitTextContent(state, emit, idx, content) { - if (!state.msgItemAdded[idx]) { - state.msgItemAdded[idx] = true; - const msgId = `msg_${state.responseId}_${idx}`; - - emit("response.output_item.added", { - type: "response.output_item.added", - output_index: idx, - item: { id: msgId, type: "message", content: [], role: "assistant" } - }); - } - - if (!state.msgContentAdded[idx]) { - state.msgContentAdded[idx] = true; - - emit("response.content_part.added", { - type: "response.content_part.added", - item_id: `msg_${state.responseId}_${idx}`, - output_index: idx, - content_index: 0, - part: { type: "output_text", annotations: [], logprobs: [], text: "" } - }); - } - - emit("response.output_text.delta", { - type: "response.output_text.delta", - item_id: `msg_${state.responseId}_${idx}`, - output_index: idx, - content_index: 0, - delta: content, - logprobs: [] - }); - - if (!state.msgTextBuf[idx]) state.msgTextBuf[idx] = ""; - state.msgTextBuf[idx] += content; -} - -function closeMessage(state, emit, idx) { - if (state.msgItemAdded[idx] && !state.msgItemDone[idx]) { - state.msgItemDone[idx] = true; - const fullText = state.msgTextBuf[idx] || ""; - const msgId = `msg_${state.responseId}_${idx}`; - - emit("response.output_text.done", { - type: "response.output_text.done", - item_id: msgId, - output_index: parseInt(idx), - content_index: 0, - text: fullText, - logprobs: [] - }); - - emit("response.content_part.done", { - type: "response.content_part.done", - item_id: msgId, - output_index: parseInt(idx), - content_index: 0, - part: { type: "output_text", annotations: [], logprobs: [], text: fullText } - }); - - emit("response.output_item.done", { - type: "response.output_item.done", - output_index: parseInt(idx), - item: { - id: msgId, - type: "message", - content: [{ type: "output_text", annotations: [], logprobs: [], text: fullText }], - role: "assistant" - } - }); - } -} - -function emitToolCall(state, emit, tc) { - const tcIdx = tc.index ?? 0; - const newCallId = tc.id; - const funcName = tc.function?.name; - - if (funcName) state.funcNames[tcIdx] = funcName; - - if (!state.funcCallIds[tcIdx] && newCallId) { - state.funcCallIds[tcIdx] = newCallId; - - emit("response.output_item.added", { - type: "response.output_item.added", - output_index: tcIdx, - item: { - id: `fc_${newCallId}`, - type: "function_call", - arguments: "", - call_id: newCallId, - name: state.funcNames[tcIdx] || "" - } - }); - } - - if (!state.funcArgsBuf[tcIdx]) state.funcArgsBuf[tcIdx] = ""; - - if (tc.function?.arguments) { - const refCallId = state.funcCallIds[tcIdx] || newCallId; - if (refCallId) { - emit("response.function_call_arguments.delta", { - type: "response.function_call_arguments.delta", - item_id: `fc_${refCallId}`, - output_index: tcIdx, - delta: tc.function.arguments - }); - } - state.funcArgsBuf[tcIdx] += tc.function.arguments; - } -} - -function closeToolCall(state, emit, idx) { - const callId = state.funcCallIds[idx]; - if (callId && !state.funcItemDone[idx]) { - const args = state.funcArgsBuf[idx] || "{}"; - - emit("response.function_call_arguments.done", { - type: "response.function_call_arguments.done", - item_id: `fc_${callId}`, - output_index: parseInt(idx), - arguments: args - }); - - emit("response.output_item.done", { - type: "response.output_item.done", - output_index: parseInt(idx), - item: { - id: `fc_${callId}`, - type: "function_call", - arguments: args, - call_id: callId, - name: state.funcNames[idx] || "" - } - }); - - state.funcItemDone[idx] = true; - state.funcArgsDone[idx] = true; - } -} - -function sendCompleted(state, emit) { - if (!state.completedSent) { - state.completedSent = true; - emit("response.completed", { - type: "response.completed", - response: { - id: state.responseId, - object: "response", - created_at: state.created, - status: "completed", - background: false, - error: null - } - }); - } -} - -function flushEvents(state) { - if (state.completedSent) return []; - - const events = []; - const nextSeq = () => ++state.seq; - const emit = (eventType, data) => { - data.sequence_number = nextSeq(); - events.push({ event: eventType, data }); - }; - - for (const i in state.msgItemAdded) closeMessage(state, emit, i); - closeReasoning(state, emit); - for (const i in state.funcCallIds) closeToolCall(state, emit, i); - sendCompleted(state, emit); - - return events; -} - -/** - * Translate OpenAI Responses API chunk to OpenAI Chat Completions format - * This is for when Codex returns data and we need to send it to an OpenAI-compatible client - */ -export function openaiResponsesToOpenAIResponse(chunk, state) { - if (!chunk) { - // Flush: send final chunk with finish_reason - if (!state.finishReasonSent && state.started) { - state.finishReasonSent = true; - return { - id: state.chatId || `chatcmpl-${Date.now()}`, - object: "chat.completion.chunk", - created: state.created || Math.floor(Date.now() / 1000), - model: state.model || "unknown", - choices: [{ - index: 0, - delta: {}, - finish_reason: "stop" - }] - }; - } - return null; - } - - // Handle different event types from Responses API - const eventType = chunk.type || chunk.event; - const data = chunk.data || chunk; - - // Initialize state - if (!state.started) { - state.started = true; - state.chatId = `chatcmpl-${Date.now()}`; - state.created = Math.floor(Date.now() / 1000); - state.toolCallIndex = 0; - state.currentToolCallId = null; - } - - // Text content delta - if (eventType === "response.output_text.delta") { - const delta = data.delta || ""; - if (!delta) return null; - - return { - id: state.chatId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "unknown", - choices: [{ - index: 0, - delta: { content: delta }, - finish_reason: null - }] - }; - } - - // Text content done (ignore, we handle via delta) - if (eventType === "response.output_text.done") { - return null; - } - - // Function call started (standard function_call or custom_tool_call) - if (eventType === "response.output_item.added" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) { - const item = data.item; - state.currentToolCallId = item.call_id || `call_${Date.now()}`; - - return { - id: state.chatId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "unknown", - choices: [{ - index: 0, - delta: { - tool_calls: [{ - index: state.toolCallIndex, - id: state.currentToolCallId, - type: "function", - function: { - name: item.name || "", - arguments: "" - } - }] - }, - finish_reason: null - }] - }; - } - - // Function call arguments delta (standard or custom_tool_call variant) - if (eventType === "response.function_call_arguments.delta" || eventType === "response.custom_tool_call_input.delta") { - const argsDelta = data.delta || ""; - if (!argsDelta) return null; - - return { - id: state.chatId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "unknown", - choices: [{ - index: 0, - delta: { - tool_calls: [{ - index: state.toolCallIndex, - function: { arguments: argsDelta } - }] - }, - finish_reason: null - }] - }; - } - - // Function call done (standard or custom_tool_call variant) - if (eventType === "response.output_item.done" && (data.item?.type === "function_call" || data.item?.type === "custom_tool_call")) { - state.toolCallIndex++; - return null; - } - - // Response completed - if (eventType === "response.completed") { - // Extract usage from response.completed event - const responseUsage = data.response?.usage; - if (responseUsage && typeof responseUsage === "object") { - const inputTokens = responseUsage.input_tokens || responseUsage.prompt_tokens || 0; - const outputTokens = responseUsage.output_tokens || responseUsage.completion_tokens || 0; - const cacheReadTokens = responseUsage.cache_read_input_tokens || 0; - const cacheCreationTokens = responseUsage.cache_creation_input_tokens || 0; - - // prompt_tokens = input_tokens + cache_read + cache_creation (all prompt-side tokens) - const promptTokens = inputTokens + cacheReadTokens + cacheCreationTokens; - - state.usage = { - prompt_tokens: promptTokens, - completion_tokens: outputTokens, - total_tokens: promptTokens + outputTokens - }; - - // Add prompt_tokens_details if cache tokens exist - if (cacheReadTokens > 0 || cacheCreationTokens > 0) { - state.usage.prompt_tokens_details = {}; - if (cacheReadTokens > 0) { - state.usage.prompt_tokens_details.cached_tokens = cacheReadTokens; - } - if (cacheCreationTokens > 0) { - state.usage.prompt_tokens_details.cache_creation_tokens = cacheCreationTokens; - } - } - } - - if (!state.finishReasonSent) { - state.finishReasonSent = true; - state.finishReason = "stop"; // Mark for usage injection in stream.js - - const finalChunk = { - id: state.chatId, - object: "chat.completion.chunk", - created: state.created, - model: state.model || "unknown", - choices: [{ - index: 0, - delta: {}, - finish_reason: "stop" - }] - }; - - // Include usage in final chunk if available - if (state.usage && typeof state.usage === "object") { - finalChunk.usage = state.usage; - } - - return finalChunk; - } - return null; - } - - // Error events from Responses API (e.g. model_not_found) - if (eventType === "error" || eventType === "response.failed") { - // Avoid emitting duplicate errors (error + response.failed arrive back-to-back) - if (state.finishReasonSent) return null; - - const error = data.error || data.response?.error; - if (error) { - state.error = error; - state.finishReasonSent = true; - - // Surface the error as an OpenAI-compatible error chunk - return { - id: state.chatId || `chatcmpl-${Date.now()}`, - object: "chat.completion.chunk", - created: state.created || Math.floor(Date.now() / 1000), - model: state.model || "unknown", - choices: [{ - index: 0, - delta: { content: `[Error] ${error.message || JSON.stringify(error)}` }, - finish_reason: "stop" - }] - }; - } - return null; - } - - // Reasoning events (convert to content or skip) - if (eventType === "response.reasoning_summary_text.delta") { - // Optionally include reasoning as content, or skip - return null; - } - - // Ignore other events - return null; -} - -// Register both directions -register(FORMATS.OPENAI, FORMATS.OPENAI_RESPONSES, null, openaiToOpenAIResponsesResponse); -register(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI, null, openaiResponsesToOpenAIResponse); - diff --git a/9router/open-sse/translator/response/openai-to-antigravity.js b/9router/open-sse/translator/response/openai-to-antigravity.js deleted file mode 100644 index b1c3ebfe..00000000 --- a/9router/open-sse/translator/response/openai-to-antigravity.js +++ /dev/null @@ -1,120 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -// Convert OpenAI SSE chunk to Antigravity SSE format -// Real Antigravity format: -// data: {"response":{"candidates":[{"content":{"role":"model","parts":[...]}, "finishReason":"STOP"}], "usageMetadata":{...}, "modelVersion":"...", "responseId":"..."}} -// Tool calls: OpenAI sends incremental args across chunks → accumulate and emit ONCE at finish -export function openaiToAntigravityResponse(chunk, state) { - if (!chunk) return null; - - const choice = chunk.choices?.[0]; - if (!choice) { - if (chunk.usage) { - state._usage = chunk.usage; - } - return null; - } - - const delta = choice.delta || {}; - const finishReason = choice.finish_reason; - - // Init state - if (!state._toolCallAccum) state._toolCallAccum = {}; - if (!state._responseId) state._responseId = chunk.id || `resp_${Date.now()}`; - if (!state._modelVersion) state._modelVersion = chunk.model || ""; - - const parts = []; - - // Thinking/reasoning → thought part - if (delta.reasoning_content) { - parts.push({ thought: true, text: delta.reasoning_content }); - } - - // Text content - if (delta.content) { - parts.push({ text: delta.content }); - } - - // Accumulate tool calls silently (no emit until finish) - if (delta.tool_calls) { - for (const tc of delta.tool_calls) { - const idx = tc.index ?? 0; - if (!state._toolCallAccum[idx]) { - state._toolCallAccum[idx] = { id: "", name: "", arguments: "" }; - } - const accum = state._toolCallAccum[idx]; - if (tc.id) accum.id = tc.id; - if (tc.function?.name) accum.name += tc.function.name; - if (tc.function?.arguments) accum.arguments += tc.function.arguments; - } - // Skip emit — wait for finish_reason - if (parts.length === 0 && !finishReason) return null; - } - - // On finish, emit accumulated tool calls as complete functionCall parts - if (finishReason) { - const indices = Object.keys(state._toolCallAccum); - for (const idx of indices) { - const accum = state._toolCallAccum[idx]; - let args = {}; - try { args = JSON.parse(accum.arguments); } catch { /* empty */ } - parts.push({ - functionCall: { - name: accum.name, - args - } - }); - } - } - - // Skip empty non-finish chunks - if (parts.length === 0 && !finishReason) return null; - - // Ensure at least empty text part on finish with no content - if (parts.length === 0 && finishReason) { - parts.push({ text: "" }); - } - - // Build candidate - const candidate = { content: { role: "model", parts } }; - - // Finish reason mapping - if (finishReason) { - const reasonMap = { - "stop": "STOP", - "length": "MAX_TOKENS", - "tool_calls": "STOP", - "content_filter": "SAFETY" - }; - candidate.finishReason = reasonMap[finishReason] || "STOP"; - } - - // Build response - const response = { - candidates: [candidate], - modelVersion: state._modelVersion, - responseId: state._responseId - }; - - // Usage metadata - const usage = chunk.usage || state._usage; - if (usage) { - response.usageMetadata = { - promptTokenCount: usage.prompt_tokens || 0, - candidatesTokenCount: usage.completion_tokens || 0, - totalTokenCount: usage.total_tokens || 0 - }; - if (usage.completion_tokens_details?.reasoning_tokens) { - response.usageMetadata.thoughtsTokenCount = usage.completion_tokens_details.reasoning_tokens; - } - if (usage.prompt_tokens_details?.cached_tokens) { - response.usageMetadata.cachedContentTokenCount = usage.prompt_tokens_details.cached_tokens; - } - } - - return { response }; -} - -// Register -register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, null, openaiToAntigravityResponse); diff --git a/9router/open-sse/translator/response/openai-to-claude.js b/9router/open-sse/translator/response/openai-to-claude.js deleted file mode 100644 index f3b2e00e..00000000 --- a/9router/open-sse/translator/response/openai-to-claude.js +++ /dev/null @@ -1,223 +0,0 @@ -import { register } from "../index.js"; -import { FORMATS } from "../formats.js"; - -// Prefix for Claude OAuth tool names (must match request translator) -const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; - -// Helper: stop thinking block if started -function stopThinkingBlock(state, results) { - if (!state.thinkingBlockStarted) return; - results.push({ - type: "content_block_stop", - index: state.thinkingBlockIndex - }); - state.thinkingBlockStarted = false; -} - -// Helper: stop text block if started -function stopTextBlock(state, results) { - if (!state.textBlockStarted || state.textBlockClosed) return; - state.textBlockClosed = true; - results.push({ - type: "content_block_stop", - index: state.textBlockIndex - }); - state.textBlockStarted = false; -} - -// Convert OpenAI stream chunk to Claude format -export function openaiToClaudeResponse(chunk, state) { - if (!chunk || !chunk.choices?.[0]) return null; - - const results = []; - const choice = chunk.choices[0]; - const delta = choice.delta; - - // Track usage from OpenAI chunk if available - if (chunk.usage && typeof chunk.usage === "object") { - const promptTokens = typeof chunk.usage.prompt_tokens === "number" ? chunk.usage.prompt_tokens : 0; - const outputTokens = typeof chunk.usage.completion_tokens === "number" ? chunk.usage.completion_tokens : 0; - - // Extract cache tokens from prompt_tokens_details - const cachedTokens = chunk.usage.prompt_tokens_details?.cached_tokens; - const cacheCreationTokens = chunk.usage.prompt_tokens_details?.cache_creation_tokens; - const cacheReadTokens = typeof cachedTokens === "number" ? cachedTokens : 0; - const cacheCreateTokens = typeof cacheCreationTokens === "number" ? cacheCreationTokens : 0; - - // input_tokens = prompt_tokens - cached_tokens - cache_creation_tokens - // Because OpenAI's prompt_tokens includes all prompt-side tokens - const inputTokens = promptTokens - cacheReadTokens - cacheCreateTokens; - - state.usage = { - input_tokens: inputTokens, - output_tokens: outputTokens - }; - - // Add cache_read_input_tokens if present - if (cacheReadTokens > 0) { - state.usage.cache_read_input_tokens = cacheReadTokens; - } - - // Add cache_creation_input_tokens if present - if (cacheCreateTokens > 0) { - state.usage.cache_creation_input_tokens = cacheCreateTokens; - } - - // Note: completion_tokens_details.reasoning_tokens is already included in output_tokens - // No need to add separately as Claude expects total output_tokens - } - - // First chunk - ALWAYS send message_start first - if (!state.messageStartSent) { - state.messageStartSent = true; - state.messageId = chunk.id?.replace("chatcmpl-", "") || `msg_${Date.now()}`; - if (!state.messageId || state.messageId === "chat" || state.messageId.length < 8) { - state.messageId = chunk.extend_fields?.requestId || - chunk.extend_fields?.traceId || - `msg_${Date.now()}`; - } - state.model = chunk.model || "unknown"; - state.nextBlockIndex = 0; - results.push({ - type: "message_start", - message: { - id: state.messageId, - type: "message", - role: "assistant", - model: state.model, - content: [], - stop_reason: null, - stop_sequence: null, - usage: { input_tokens: 0, output_tokens: 0 } - } - }); - } - - // Handle reasoning_content (thinking) - GLM, DeepSeek, etc. - const reasoningContent = delta?.reasoning_content || delta?.reasoning; - if (reasoningContent) { - stopTextBlock(state, results); - - if (!state.thinkingBlockStarted) { - state.thinkingBlockIndex = state.nextBlockIndex++; - state.thinkingBlockStarted = true; - results.push({ - type: "content_block_start", - index: state.thinkingBlockIndex, - content_block: { type: "thinking", thinking: "" } - }); - } - - results.push({ - type: "content_block_delta", - index: state.thinkingBlockIndex, - delta: { type: "thinking_delta", thinking: reasoningContent } - }); - } - - // Handle regular content - if (delta?.content) { - stopThinkingBlock(state, results); - - if (!state.textBlockStarted) { - state.textBlockIndex = state.nextBlockIndex++; - state.textBlockStarted = true; - state.textBlockClosed = false; - results.push({ - type: "content_block_start", - index: state.textBlockIndex, - content_block: { type: "text", text: "" } - }); - } - - results.push({ - type: "content_block_delta", - index: state.textBlockIndex, - delta: { type: "text_delta", text: delta.content } - }); - } - - // Tool calls - if (delta?.tool_calls) { - for (const tc of delta.tool_calls) { - const idx = tc.index ?? 0; - - if (tc.id) { - stopThinkingBlock(state, results); - stopTextBlock(state, results); - - const toolBlockIndex = state.nextBlockIndex++; - state.toolCalls.set(idx, { id: tc.id, name: tc.function?.name || "", blockIndex: toolBlockIndex }); - - // Strip prefix from tool name for response - let toolName = tc.function?.name || ""; - if (toolName.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) { - toolName = toolName.slice(CLAUDE_OAUTH_TOOL_PREFIX.length); - } - - results.push({ - type: "content_block_start", - index: toolBlockIndex, - content_block: { - type: "tool_use", - id: tc.id, - name: toolName, - input: {} - } - }); - } - - if (tc.function?.arguments) { - const toolInfo = state.toolCalls.get(idx); - if (toolInfo) { - results.push({ - type: "content_block_delta", - index: toolInfo.blockIndex, - delta: { type: "input_json_delta", partial_json: tc.function.arguments } - }); - } - } - } - } - - // Finish - if (choice.finish_reason) { - stopThinkingBlock(state, results); - stopTextBlock(state, results); - - for (const [, toolInfo] of state.toolCalls) { - results.push({ - type: "content_block_stop", - index: toolInfo.blockIndex - }); - } - - // Mark finish for later usage injection in stream.js - state.finishReason = choice.finish_reason; - - // Use tracked usage (will be estimated in stream.js if not valid) - const finalUsage = state.usage || { input_tokens: 0, output_tokens: 0 }; - results.push({ - type: "message_delta", - delta: { stop_reason: convertFinishReason(choice.finish_reason) }, - usage: finalUsage - }); - results.push({ type: "message_stop" }); - } - - return results.length > 0 ? results : null; -} - -// Convert OpenAI finish_reason to Claude stop_reason -function convertFinishReason(reason) { - switch (reason) { - case "stop": return "end_turn"; - case "length": return "max_tokens"; - case "tool_calls": return "tool_use"; - default: return "end_turn"; - } -} - -// Register -register(FORMATS.OPENAI, FORMATS.CLAUDE, null, openaiToClaudeResponse); - diff --git a/9router/open-sse/utils/bypassHandler.js b/9router/open-sse/utils/bypassHandler.js deleted file mode 100644 index 57fa2ff2..00000000 --- a/9router/open-sse/utils/bypassHandler.js +++ /dev/null @@ -1,298 +0,0 @@ -import { detectFormat } from "../services/provider.js"; -import { translateResponse, initState } from "../translator/index.js"; -import { FORMATS } from "../translator/formats.js"; -import { SKIP_PATTERNS } from "../config/runtimeConfig.js"; -import { formatSSE } from "./stream.js"; - -/** - * Check for bypass patterns - return fake response without calling provider - * Only works for Claude CLI requests - */ -export function handleBypassRequest(body, model, userAgent = "", ccFilterNaming = false) { - if (!userAgent.includes("claude-cli")) return null; - if (!body.messages?.length) return null; - - const messages = body.messages; - const getText = (content) => { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content.filter(c => c.type === "text").map(c => c.text).join(" "); - } - return ""; - }; - - let shouldBypass = false; - let namingBypass = false; - - // Pattern 1: Title extraction (assistant message = "{") - const lastMsg = messages[messages.length - 1]; - if (lastMsg?.role === "assistant" && lastMsg.content?.[0]?.text === "{") { - shouldBypass = true; - } - - // Pattern 2: Warmup - if (!shouldBypass) { - const firstText = getText(messages[0]?.content); - if (firstText === "Warmup") { - shouldBypass = true; - } - } - - // Pattern 3: Count - if (!shouldBypass && messages.length === 1 && messages[0]?.role === "user") { - const firstText = getText(messages[0]?.content); - if (firstText === "count") { - shouldBypass = true; - } - } - - // Pattern 4: Skip patterns - if (!shouldBypass && SKIP_PATTERNS?.length) { - const userMessages = messages.filter(m => m.role === "user"); - const userText = userMessages.map(m => getText(m.content)).join(" "); - if (SKIP_PATTERNS.some(p => userText.includes(p))) { - shouldBypass = true; - } - } - - // Pattern 5: CC naming request (topic title extraction by Claude Code CLI) - // Claude format: system is top-level body.system field, not inside messages - if (!shouldBypass && ccFilterNaming) { - const systemMsg = messages.find(m => m.role === "system"); - const systemFromMessages = getText(systemMsg?.content); - const systemFromBody = Array.isArray(body.system) - ? body.system.filter(s => s.type === "text").map(s => s.text).join(" ") - : (typeof body.system === "string" ? body.system : ""); - const systemText = systemFromMessages || systemFromBody; - if (systemText.includes("isNewTopic")) { - shouldBypass = true; - namingBypass = true; - } - } - - if (!shouldBypass) return null; - - const sourceFormat = detectFormat(body); - const stream = body.stream !== false; - - // For naming bypass, generate title from user message - if (namingBypass) { - const userMsg = messages.find(m => m.role === "user"); - const userText = getText(userMsg?.content); - const title = userText.trim().split(/\s+/).slice(0, 3).join(" "); - const namingText = JSON.stringify({ isNewTopic: true, title }); - return stream - ? createStreamingResponse(sourceFormat, model, namingText) - : createNonStreamingResponse(sourceFormat, model, namingText); - } - - return stream - ? createStreamingResponse(sourceFormat, model) - : createNonStreamingResponse(sourceFormat, model); -} - -const DEFAULT_BYPASS_TEXT = "CLI Command Execution: Clear Terminal"; - -/** - * Create OpenAI standard format response - */ -function createOpenAIResponse(model, text = DEFAULT_BYPASS_TEXT) { - const id = `chatcmpl-${Date.now()}`; - const created = Math.floor(Date.now() / 1000); - - return { - id, - object: "chat.completion", - created, - model, - choices: [{ - index: 0, - message: { - role: "assistant", - content: text - }, - finish_reason: "stop" - }], - usage: { - prompt_tokens: 1, - completion_tokens: 1, - total_tokens: 2 - } - }; -} - -/** - * Create non-streaming response with translation - * Use translator to convert OpenAI → sourceFormat - */ -function createNonStreamingResponse(sourceFormat, model, text) { - const openaiResponse = createOpenAIResponse(model, text); - - // If sourceFormat is OpenAI, return directly - if (sourceFormat === FORMATS.OPENAI) { - return { - success: true, - response: new Response(JSON.stringify(openaiResponse), { - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }) - }; - } - - // Use translator to convert: simulate streaming then collect all chunks - const state = initState(sourceFormat); - state.model = model; - - const openaiChunks = createOpenAIStreamingChunks(openaiResponse); - const allTranslated = []; - - for (const chunk of openaiChunks) { - const translated = translateResponse(FORMATS.OPENAI, sourceFormat, chunk, state); - if (translated?.length > 0) { - allTranslated.push(...translated); - } - } - - // Flush remaining - const flushed = translateResponse(FORMATS.OPENAI, sourceFormat, null, state); - if (flushed?.length > 0) { - allTranslated.push(...flushed); - } - - // For non-streaming, merge all chunks into final response - const finalResponse = mergeChunksToResponse(allTranslated, sourceFormat); - - return { - success: true, - response: new Response(JSON.stringify(finalResponse), { - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }) - }; -} - -/** - * Create streaming response with translation - * Use translator to convert OpenAI chunks → sourceFormat - */ -function createStreamingResponse(sourceFormat, model, text) { - const openaiResponse = createOpenAIResponse(model, text); - const state = initState(sourceFormat); - state.model = model; - - // Create OpenAI streaming chunks - const openaiChunks = createOpenAIStreamingChunks(openaiResponse); - - // Translate each chunk to sourceFormat using translator - const translatedChunks = []; - - for (const chunk of openaiChunks) { - const translated = translateResponse(FORMATS.OPENAI, sourceFormat, chunk, state); - if (translated?.length > 0) { - for (const item of translated) { - translatedChunks.push(formatSSE(item, sourceFormat)); - } - } - } - - // Flush remaining events - const flushed = translateResponse(FORMATS.OPENAI, sourceFormat, null, state); - if (flushed?.length > 0) { - for (const item of flushed) { - translatedChunks.push(formatSSE(item, sourceFormat)); - } - } - - // Add [DONE] - translatedChunks.push("data: [DONE]\n\n"); - - return { - success: true, - response: new Response(translatedChunks.join(""), { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "Access-Control-Allow-Origin": "*" - } - }) - }; -} - -/** - * Merge translated chunks into final response object (for non-streaming) - * Takes the last complete chunk as the final response - */ -function mergeChunksToResponse(chunks, sourceFormat) { - if (!chunks || chunks.length === 0) { - return createOpenAIResponse("unknown"); - } - - // For most formats, the last chunk before done contains the complete response - // Find the most complete chunk (usually the last one with content) - let finalChunk = chunks[chunks.length - 1]; - - // For Claude format, find the message_stop or final message - if (sourceFormat === FORMATS.CLAUDE) { - const messageStop = chunks.find(c => c.type === "message_stop"); - if (messageStop) { - // Reconstruct complete message from chunks - const contentDelta = chunks.find(c => c.type === "content_block_delta"); - const messageDelta = chunks.find(c => c.type === "message_delta"); - const messageStart = chunks.find(c => c.type === "message_start"); - - if (messageStart?.message) { - finalChunk = messageStart.message; - // Merge usage if available - if (messageDelta?.usage) { - finalChunk.usage = messageDelta.usage; - } - } - } - } - - return finalChunk; -} - -/** - * Create OpenAI streaming chunks from complete response - */ -function createOpenAIStreamingChunks(completeResponse) { - const { id, created, model, choices } = completeResponse; - const content = choices[0].message.content; - - return [ - // Chunk with content - { - id, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: { - role: "assistant", - content - }, - finish_reason: null - }] - }, - // Final chunk with finish_reason - { - id, - object: "chat.completion.chunk", - created, - model, - choices: [{ - index: 0, - delta: {}, - finish_reason: "stop" - }], - usage: completeResponse.usage - } - ]; -} diff --git a/9router/open-sse/utils/claudeCloaking.js b/9router/open-sse/utils/claudeCloaking.js deleted file mode 100644 index 8cb490da..00000000 --- a/9router/open-sse/utils/claudeCloaking.js +++ /dev/null @@ -1,60 +0,0 @@ -import { createHash, randomBytes } from "crypto"; - -const CLAUDE_VERSION = "2.1.63"; - -// Generate billing header matching real Claude Code format: -// x-anthropic-billing-header: cc_version=.; cc_entrypoint=cli; cch=; -function generateBillingHeader(payload) { - const content = JSON.stringify(payload); - const cch = createHash("sha256").update(content).digest("hex").slice(0, 5); - const buildHash = randomBytes(2).toString("hex").slice(0, 3); - return `x-anthropic-billing-header: cc_version=${CLAUDE_VERSION}.${buildHash}; cc_entrypoint=cli; cch=${cch};`; -} - -// Generate a random UUID v4 -function generateFakeUserID() { - const bytes = randomBytes(16); - bytes[6] = (bytes[6] & 0x0f) | 0x40; - bytes[8] = (bytes[8] & 0x3f) | 0x80; - const hex = bytes.toString("hex"); - return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`; -} - -/** - * Apply Claude cloaking to request body: - * 1. Inject billing header as first system block - * 2. Inject fake user ID into metadata - * - * Only applies when using OAuth token (sk-ant-oat). - * @param {object} body - Claude API request body - * @param {string} apiKey - API key or OAuth token - * @returns {object} Modified body - */ -export function applyCloaking(body, apiKey) { - if (!apiKey || !apiKey.includes("sk-ant-oat")) return body; - - const result = { ...body }; - - // Inject billing header as system[0], preserve existing system blocks - const billingText = generateBillingHeader(body); - const billingBlock = { type: "text", text: billingText }; - - if (Array.isArray(result.system)) { - // Skip if already injected - if (!result.system[0]?.text?.startsWith("x-anthropic-billing-header:")) { - result.system = [billingBlock, ...result.system]; - } - } else if (typeof result.system === "string") { - result.system = [billingBlock, { type: "text", text: result.system }]; - } else { - result.system = [billingBlock]; - } - - // Inject fake user ID into metadata - const existingUserId = result.metadata?.user_id; - if (!existingUserId) { - result.metadata = { ...result.metadata, user_id: generateFakeUserID() }; - } - - return result; -} diff --git a/9router/open-sse/utils/cursorChecksum.js b/9router/open-sse/utils/cursorChecksum.js deleted file mode 100644 index c41d5b7a..00000000 --- a/9router/open-sse/utils/cursorChecksum.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Cursor Checksum Utility (Jyh Cipher) - * - * Generates the x-cursor-checksum header required for Cursor API authentication. - * Based on the JavaScript implementation from Cursor IDE. - */ - -import crypto from "crypto"; -import { v5 as uuidv5 } from "uuid"; - -/** - * Generate SHA-256 hash like generateHashed64Hex - * @param {string} input - Input string - * @param {string} salt - Optional salt - * @returns {string} - 64-character hex string - */ -export function generateHashed64Hex(input, salt = "") { - return crypto.createHash("sha256").update(input + salt).digest("hex"); -} - -/** - * Generate session ID using UUID v5 with DNS namespace - * @param {string} authToken - Auth token - * @returns {string} - UUID string - */ -export function generateSessionId(authToken) { - return uuidv5(authToken, uuidv5.DNS); -} - -/** - * Generate cursor checksum (Jyh cipher) - * - * Algorithm: - * 1. Get Unix timestamp in specific format - * 2. XOR each byte with key (starting 165) - * 3. Update key: key = (key + byte) & 0xFF - * 4. URL-safe base64 encode - * 5. Format: {base64_encoded}{machineId} - * - * @param {string} machineId - Machine ID from Cursor storage or generated - * @returns {string} - Checksum string - */ -export function generateCursorChecksum(machineId) { - // Math.floor(Date.now() / 1e6) - same as Python implementation - const timestamp = Math.floor(Date.now() / 1000000); - - // Create byte array from timestamp (6 bytes, big-endian) - const byteArray = new Uint8Array([ - (timestamp >> 40) & 0xFF, - (timestamp >> 32) & 0xFF, - (timestamp >> 24) & 0xFF, - (timestamp >> 16) & 0xFF, - (timestamp >> 8) & 0xFF, - timestamp & 0xFF - ]); - - // Jyh cipher obfuscation - let t = 165; - for (let i = 0; i < byteArray.length; i++) { - byteArray[i] = ((byteArray[i] ^ t) + (i % 256)) & 0xFF; - t = byteArray[i]; - } - - // URL-safe base64 encode (without padding) - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - let encoded = ""; - - for (let i = 0; i < byteArray.length; i += 3) { - const a = byteArray[i]; - const b = i + 1 < byteArray.length ? byteArray[i + 1] : 0; - const c = i + 2 < byteArray.length ? byteArray[i + 2] : 0; - - encoded += alphabet[a >> 2]; - encoded += alphabet[((a & 3) << 4) | (b >> 4)]; - - if (i + 1 < byteArray.length) { - encoded += alphabet[((b & 15) << 2) | (c >> 6)]; - } - if (i + 2 < byteArray.length) { - encoded += alphabet[c & 63]; - } - } - - return `${encoded}${machineId}`; -} - -/** - * Build all Cursor API headers - * - * @param {string} accessToken - Bearer token - * @param {string} machineId - Machine ID (or will be generated from token) - * @param {boolean} ghostMode - Enable ghost mode (privacy) - * @returns {Object} - Headers object - */ -export function buildCursorHeaders(accessToken, machineId = null, ghostMode = true) { - // Clean token if it has prefix - const cleanToken = accessToken.includes("::") - ? accessToken.split("::")[1] - : accessToken; - - // Generate machine ID if not provided - const effectiveMachineId = machineId || generateHashed64Hex(cleanToken, "machineId"); - - // Generate derived values - const sessionId = generateSessionId(cleanToken); - const clientKey = generateHashed64Hex(cleanToken); - const checksum = generateCursorChecksum(effectiveMachineId); - - // Detect OS - let os = "linux"; - if (typeof process !== "undefined") { - if (process.platform === "win32") os = "windows"; - else if (process.platform === "darwin") os = "macos"; - } - - // Detect architecture - let arch = "x64"; - if (typeof process !== "undefined") { - if (process.arch === "arm64") arch = "aarch64"; - } - - return { - "authorization": `Bearer ${cleanToken}`, - "connect-accept-encoding": "gzip", - "connect-protocol-version": "1", - "content-type": "application/connect+proto", - "user-agent": "connect-es/1.6.1", - "x-amzn-trace-id": `Root=${crypto.randomUUID()}`, - "x-client-key": clientKey, - "x-cursor-checksum": checksum, - "x-cursor-client-version": "2.3.41", - "x-cursor-client-type": "ide", - "x-cursor-client-os": os, - "x-cursor-client-arch": arch, - "x-cursor-client-device-type": "desktop", - "x-cursor-config-version": crypto.randomUUID(), - "x-cursor-timezone": Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", - "x-ghost-mode": ghostMode ? "true" : "false", - "x-request-id": crypto.randomUUID(), - "x-session-id": sessionId - }; -} - -export default { - generateCursorChecksum, - buildCursorHeaders, - generateHashed64Hex, - generateSessionId -}; diff --git a/9router/open-sse/utils/cursorProtobuf.js b/9router/open-sse/utils/cursorProtobuf.js deleted file mode 100644 index e6f7f8b5..00000000 --- a/9router/open-sse/utils/cursorProtobuf.js +++ /dev/null @@ -1,904 +0,0 @@ -/** - * Cursor Protobuf Encoder/Decoder - * Implements ConnectRPC protobuf wire format for Cursor API - */ - -import { v4 as uuidv4 } from "uuid"; -import zlib from "zlib"; - -const DEBUG = process.env.CURSOR_PROTOBUF_DEBUG === "1"; -const log = (tag, ...args) => DEBUG && console.log(`[PROTOBUF:${tag}]`, ...args); -const textDecoder = new TextDecoder(); - -const PROTOBUF_SCHEMA_VERSION = "1.1.3"; - -// ==================== SCHEMAS ==================== - -const WIRE_TYPE = { VARINT: 0, FIXED64: 1, LEN: 2, FIXED32: 5 }; - -const ROLE = { USER: 1, ASSISTANT: 2 }; - -const UNIFIED_MODE = { CHAT: 1, AGENT: 2 }; - -const THINKING_LEVEL = { UNSPECIFIED: 0, MEDIUM: 1, HIGH: 2 }; -const CLIENT_SIDE_TOOL_V2 = { MCP: 19 }; -const CLIENT_SIDE_TOOL_V2_MCP = 19; - -const FIELD = { - // StreamUnifiedChatRequestWithTools (top level) - REQUEST: 1, - - // StreamUnifiedChatRequest - MESSAGES: 1, - UNKNOWN_2: 2, - INSTRUCTION: 3, - UNKNOWN_4: 4, - MODEL: 5, - WEB_TOOL: 8, - UNKNOWN_13: 13, - CURSOR_SETTING: 15, - UNKNOWN_19: 19, - CONVERSATION_ID: 23, - METADATA: 26, - IS_AGENTIC: 27, - SUPPORTED_TOOLS: 29, - MESSAGE_IDS: 30, - MCP_TOOLS: 34, - LARGE_CONTEXT: 35, - UNKNOWN_38: 38, - UNIFIED_MODE: 46, - UNKNOWN_47: 47, - SHOULD_DISABLE_TOOLS: 48, - THINKING_LEVEL: 49, - UNKNOWN_51: 51, - UNKNOWN_53: 53, - UNIFIED_MODE_NAME: 54, - - // ConversationMessage - MSG_CONTENT: 1, - MSG_ROLE: 2, - MSG_ID: 13, - MSG_TOOL_RESULTS: 18, - MSG_IS_AGENTIC: 29, - MSG_SERVER_BUBBLE_ID: 32, - MSG_UNIFIED_MODE: 47, - MSG_SUPPORTED_TOOLS: 51, - - // ConversationMessage.ToolResult - TOOL_RESULT_CALL_ID: 1, - TOOL_RESULT_NAME: 2, - TOOL_RESULT_INDEX: 3, - TOOL_RESULT_RAW_ARGS: 5, - TOOL_RESULT_RESULT: 8, - TOOL_RESULT_TOOL_CALL: 11, - TOOL_RESULT_MODEL_CALL_ID: 12, - - // ClientSideToolV2Result (nested inside ToolResult.result) - CLIENT_RESULT_TOOL: 1, - CLIENT_RESULT_MCP_RESULT: 28, - CLIENT_RESULT_TOOL_CALL_ID: 35, - CLIENT_RESULT_MODEL_CALL_ID: 48, - CLIENT_RESULT_TOOL_INDEX: 49, - // Aliases used by encodeClientSideToolV2Result - CV2R_TOOL: 1, - CV2R_MCP_RESULT: 28, - CV2R_CALL_ID: 35, - CV2R_MODEL_CALL_ID: 48, - CV2R_TOOL_INDEX: 49, - - // MCPResult (nested inside ClientSideToolV2Result.mcp_result) - MCP_RESULT_SELECTED_TOOL: 1, - MCP_RESULT_RESULT: 2, - // Aliases used by encodeMcpResult - MCPR_SELECTED_TOOL: 1, - MCPR_RESULT: 2, - - // ClientSideToolV2Call (nested inside ToolResult.tool_call) - CLIENT_CALL_TOOL: 1, - CLIENT_CALL_MCP_PARAMS: 27, - CLIENT_CALL_TOOL_CALL_ID: 3, - CLIENT_CALL_NAME: 9, - CLIENT_CALL_RAW_ARGS: 10, - CLIENT_CALL_TOOL_INDEX: 48, - CLIENT_CALL_MODEL_CALL_ID: 49, - // Aliases used by encodeClientSideToolV2Call - CV2C_TOOL: 1, - CV2C_MCP_PARAMS: 27, - CV2C_CALL_ID: 3, - CV2C_NAME: 9, - CV2C_RAW_ARGS: 10, - CV2C_TOOL_INDEX: 48, - CV2C_MODEL_CALL_ID: 49, - - // Model - MODEL_NAME: 1, - MODEL_EMPTY: 4, - - // Instruction - INSTRUCTION_TEXT: 1, - - // CursorSetting - SETTING_PATH: 1, - SETTING_UNKNOWN_3: 3, - SETTING_UNKNOWN_6: 6, - SETTING_UNKNOWN_8: 8, - SETTING_UNKNOWN_9: 9, - - // CursorSetting.Unknown6 - SETTING6_FIELD_1: 1, - SETTING6_FIELD_2: 2, - - // Metadata - META_PLATFORM: 1, - META_ARCH: 2, - META_VERSION: 3, - META_CWD: 4, - META_TIMESTAMP: 5, - - // MessageId - MSGID_ID: 1, - MSGID_SUMMARY: 2, - MSGID_ROLE: 3, - - // MCPTool - MCP_TOOL_NAME: 1, - MCP_TOOL_DESC: 2, - MCP_TOOL_PARAMS: 3, - MCP_TOOL_SERVER: 4, - - // StreamUnifiedChatResponseWithTools (response) - TOOL_CALL: 1, - RESPONSE: 2, - - // ClientSideToolV2Call - TOOL_ID: 3, - TOOL_NAME: 9, - TOOL_RAW_ARGS: 10, - TOOL_IS_LAST: 11, - TOOL_IS_LAST_ALT: 15, - TOOL_MCP_PARAMS: 27, - - // MCPParams - MCP_TOOLS_LIST: 1, - - // MCPParams.Tool (nested) - MCP_NESTED_NAME: 1, - MCP_NESTED_PARAMS: 3, - - // StreamUnifiedChatResponse - RESPONSE_TEXT: 1, - THINKING: 25, - - // Thinking - THINKING_TEXT: 1 -}; - -// Known response field numbers — used to detect unknown fields from protocol updates -const KNOWN_RESPONSE_FIELDS = new Set([ - FIELD.TOOL_CALL, - FIELD.RESPONSE, - FIELD.TOOL_ID, - FIELD.TOOL_NAME, - FIELD.TOOL_RAW_ARGS, - FIELD.TOOL_IS_LAST, - FIELD.TOOL_MCP_PARAMS, - FIELD.RESPONSE_TEXT, - FIELD.THINKING -]); - -// ==================== PRIMITIVE ENCODING ==================== - -export function encodeVarint(value) { - const bytes = []; - while (value >= 0x80) { - bytes.push((value & 0x7F) | 0x80); - value >>>= 7; - } - bytes.push(value & 0x7F); - return new Uint8Array(bytes); -} - -export function encodeField(fieldNum, wireType, value) { - const tag = (fieldNum << 3) | wireType; - const tagBytes = encodeVarint(tag); - - if (wireType === WIRE_TYPE.VARINT) { - const valueBytes = encodeVarint(value); - return concatArrays(tagBytes, valueBytes); - } - - if (wireType === WIRE_TYPE.LEN) { - const dataBytes = typeof value === "string" - ? new TextEncoder().encode(value) - : value instanceof Uint8Array ? value - : Buffer.isBuffer(value) ? new Uint8Array(value) - : new Uint8Array(0); - - const lengthBytes = encodeVarint(dataBytes.length); - return concatArrays(tagBytes, lengthBytes, dataBytes); - } - - return new Uint8Array(0); -} - -function concatArrays(...arrays) { - const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); - const result = new Uint8Array(totalLength); - let offset = 0; - for (const arr of arrays) { - result.set(arr, offset); - offset += arr.length; - } - return result; -} - -// ==================== MESSAGE ENCODING ==================== - -/** - * Format tool name: "toolName" → "mcp_custom_toolName" - * Also handles: "mcp__server__tool" → "mcp_server_tool" - */ -function formatToolName(name) { - const base = typeof name === "string" && name.length > 0 ? name : "tool"; - - if (base.startsWith("mcp__")) { - const rest = base.slice("mcp__".length); - const splitIdx = rest.indexOf("__"); - if (splitIdx >= 0) { - const server = rest.slice(0, splitIdx) || "custom"; - const toolName = rest.slice(splitIdx + 2) || "tool"; - return `mcp_${server}_${toolName}`; - } - return `mcp_custom_${rest || "tool"}`; - } - - if (base.startsWith("mcp_")) return base; - return `mcp_custom_${base}`; -} - -/** - * Parse formatted tool name: "mcp_server_tool" → { serverName, selectedTool } - */ -function parseToolName(formattedName) { - if (typeof formattedName !== "string" || !formattedName.startsWith("mcp_")) { - return { serverName: "custom", selectedTool: formattedName || "tool" }; - } - - const tail = formattedName.slice("mcp_".length); - const splitIdx = tail.indexOf("_"); - if (splitIdx < 0) { - return { serverName: "custom", selectedTool: tail || "tool" }; - } - - return { - serverName: tail.slice(0, splitIdx) || "custom", - selectedTool: tail.slice(splitIdx + 1) || "tool" - }; -} - -/** - * Parse tool_call_id into { toolCallId, modelCallId } - * Cursor uses "\nmc_" delimiter for model_call_id - */ -function parseToolId(id) { - const delimiter = "\nmc_"; - const idx = id.indexOf(delimiter); - if (idx >= 0) { - return { toolCallId: id.slice(0, idx), modelCallId: id.slice(idx + delimiter.length) }; - } - return { toolCallId: id, modelCallId: null }; -} - -/** - * Encode MCPResult proto: { selected_tool, result } - */ -function encodeMcpResult(selectedTool, resultContent) { - return concatArrays( - encodeField(FIELD.MCPR_SELECTED_TOOL, WIRE_TYPE.LEN, selectedTool), - encodeField(FIELD.MCPR_RESULT, WIRE_TYPE.LEN, resultContent) - ); -} - -/** - * Encode ClientSideToolV2Result proto: { tool, mcp_result, call_id, model_call_id, tool_index } - * Represents the result of executing a tool - */ -function encodeClientSideToolV2Result(toolCallId, modelCallId, selectedTool, resultContent, toolIndex = 1) { - return concatArrays( - encodeField(FIELD.CV2R_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2_MCP), - encodeField(FIELD.CV2R_MCP_RESULT, WIRE_TYPE.LEN, encodeMcpResult(selectedTool, resultContent)), - encodeField(FIELD.CV2R_CALL_ID, WIRE_TYPE.LEN, toolCallId), - ...(modelCallId ? [encodeField(FIELD.CV2R_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []), - encodeField(FIELD.CV2R_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1) - ); -} - -/** - * Encode MCPParams.Tool nested inside ClientSideToolV2Call - */ -function encodeMcpParamsForCall(toolName, rawArgs, serverName) { - const tool = concatArrays( - encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName), - encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, rawArgs), - encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, serverName) - ); - return encodeField(FIELD.MCP_TOOLS_LIST, WIRE_TYPE.LEN, tool); -} - -/** - * Encode ClientSideToolV2Call proto: { tool, mcp_params, call_id, name, raw_args, tool_index, model_call_id } - * Represents a tool call definition - */ -function encodeClientSideToolV2Call(toolCallId, toolName, selectedTool, serverName, rawArgs, modelCallId, toolIndex = 1) { - return concatArrays( - encodeField(FIELD.CV2C_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2_MCP), - encodeField(FIELD.CV2C_MCP_PARAMS, WIRE_TYPE.LEN, encodeMcpParamsForCall(selectedTool, rawArgs, serverName)), - encodeField(FIELD.CV2C_CALL_ID, WIRE_TYPE.LEN, toolCallId), - encodeField(FIELD.CV2C_NAME, WIRE_TYPE.LEN, toolName), - encodeField(FIELD.CV2C_RAW_ARGS, WIRE_TYPE.LEN, rawArgs), - encodeField(FIELD.CV2C_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1), - ...(modelCallId ? [encodeField(FIELD.CV2C_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []) - ); -} - -/** - * Encode ConversationMessage.ToolResult with full structure - * Matches Cursor proto: tool_call_id, tool_name, tool_index, raw_args, result, tool_call - */ -export function encodeToolResult(toolResult) { - const originalName = toolResult.tool_name || toolResult.name || ""; - const toolName = formatToolName(originalName); - const rawArgs = toolResult.raw_args || "{}"; - const resultContent = toolResult.result_content || toolResult.result || ""; - const { toolCallId, modelCallId } = parseToolId(toolResult.tool_call_id || ""); - const toolIndex = toolResult.tool_index || toolResult.index || 1; - - // Parse tool name to extract server and selected tool - const { serverName, selectedTool } = parseToolName(toolName); - - return concatArrays( - encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId), - encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName), - encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1), - ...(modelCallId ? [encodeField(FIELD.TOOL_RESULT_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []), - encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs), - encodeField(FIELD.TOOL_RESULT_RESULT, WIRE_TYPE.LEN, - encodeClientSideToolV2Result(toolCallId, modelCallId, selectedTool, resultContent, toolIndex) - ), - encodeField(FIELD.TOOL_RESULT_TOOL_CALL, WIRE_TYPE.LEN, - encodeClientSideToolV2Call(toolCallId, toolName, selectedTool, serverName, rawArgs, modelCallId, toolIndex) - ) - ); -} - -export function encodeMessage(content, role, messageId, chatModeEnum = null, isLast = false, hasTools = false, toolResults = [], serverBubbleId = null) { - const hasToolResults = toolResults.length > 0; - return concatArrays( - encodeField(FIELD.MSG_CONTENT, WIRE_TYPE.LEN, content), - encodeField(FIELD.MSG_ROLE, WIRE_TYPE.VARINT, role), - encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId), - // Only include server_bubble_id if explicitly provided (last assistant message only) - ...(serverBubbleId ? [encodeField(FIELD.MSG_SERVER_BUBBLE_ID, WIRE_TYPE.LEN, serverBubbleId)] : []), - ...(hasToolResults ? toolResults.map(tr => - encodeField(FIELD.MSG_TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr)) - ) : []), - encodeField(FIELD.MSG_IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0), - encodeField(FIELD.MSG_UNIFIED_MODE, WIRE_TYPE.VARINT, hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT), - ...(isLast && hasTools ? [encodeField(FIELD.MSG_SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))] : []) - ); -} - -export function encodeInstruction(text) { - return text ? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text) : new Uint8Array(0); -} - -export function encodeModel(modelName) { - return concatArrays( - encodeField(FIELD.MODEL_NAME, WIRE_TYPE.LEN, modelName), - encodeField(FIELD.MODEL_EMPTY, WIRE_TYPE.LEN, new Uint8Array(0)) - ); -} - -export function encodeCursorSetting() { - const unknown6 = concatArrays( - encodeField(FIELD.SETTING6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)), - encodeField(FIELD.SETTING6_FIELD_2, WIRE_TYPE.LEN, new Uint8Array(0)) - ); - - return concatArrays( - encodeField(FIELD.SETTING_PATH, WIRE_TYPE.LEN, "cursor\\aisettings"), - encodeField(FIELD.SETTING_UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)), - encodeField(FIELD.SETTING_UNKNOWN_6, WIRE_TYPE.LEN, unknown6), - encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1), - encodeField(FIELD.SETTING_UNKNOWN_9, WIRE_TYPE.VARINT, 1) - ); -} - -export function encodeMetadata() { - return concatArrays( - encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || "linux"), - encodeField(FIELD.META_ARCH, WIRE_TYPE.LEN, process.arch || "x64"), - encodeField(FIELD.META_VERSION, WIRE_TYPE.LEN, process.version || "v20.0.0"), - encodeField(FIELD.META_CWD, WIRE_TYPE.LEN, process.cwd?.() || "/"), - encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString()) - ); -} - -export function encodeMessageId(messageId, role, summaryId = null) { - return concatArrays( - encodeField(FIELD.MSGID_ID, WIRE_TYPE.LEN, messageId), - ...(summaryId ? [encodeField(FIELD.MSGID_SUMMARY, WIRE_TYPE.LEN, summaryId)] : []), - encodeField(FIELD.MSGID_ROLE, WIRE_TYPE.VARINT, role) - ); -} - -export function encodeMcpTool(tool) { - const toolName = tool.function?.name || tool.name || ""; - const toolDesc = tool.function?.description || tool.description || ""; - const inputSchema = tool.function?.parameters || tool.input_schema || {}; - - return concatArrays( - ...(toolName ? [encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName)] : []), - ...(toolDesc ? [encodeField(FIELD.MCP_TOOL_DESC, WIRE_TYPE.LEN, toolDesc)] : []), - ...(Object.keys(inputSchema).length > 0 ? [encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))] : []), - encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, "custom") - ); -} - -// ==================== REQUEST BUILDING ==================== - -export function encodeRequest(messages, modelName, tools = [], reasoningEffort = null) { - const hasTools = tools?.length > 0; - const isAgentic = hasTools; - const formattedMessages = []; - const messageIds = []; - const normalizedMessages = []; - - // Guardrail: split mixed assistant payload into separate assistant messages - // This prevents protobuf encoding errors when tool calls and results are in same message - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - const hasToolCalls = Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0; - const hasToolResults = Array.isArray(msg?.tool_results) && msg.tool_results.length > 0; - - if (msg?.role === "assistant" && hasToolCalls && hasToolResults) { - log( - "ENCODE", - `normalizing mixed assistant tool payload at msg[${i}] (calls=${msg.tool_calls.length}, results=${msg.tool_results.length})` - ); - - // Keep assistant tool call message without embedded results - normalizedMessages.push({ - ...msg, - tool_results: [] - }); - - // Avoid inserting duplicate assistant tool-result message if next one already matches - const nextMsg = messages[i + 1]; - const nextHasToolResults = - nextMsg?.role === "assistant" && - Array.isArray(nextMsg?.tool_results) && - nextMsg.tool_results.length > 0; - const currentIds = new Set( - msg.tool_results.map(tr => tr?.tool_call_id).filter(id => typeof id === "string") - ); - const nextIds = new Set( - (nextMsg?.tool_results || []) - .map(tr => tr?.tool_call_id) - .filter(id => typeof id === "string") - ); - let sameIds = currentIds.size > 0 && currentIds.size === nextIds.size; - if (sameIds) { - for (const id of currentIds) { - if (!nextIds.has(id)) { - sameIds = false; - break; - } - } - } - - if (!(nextHasToolResults && sameIds)) { - normalizedMessages.push({ - role: "assistant", - content: "", - tool_results: msg.tool_results - }); - } - - continue; - } - - normalizedMessages.push(msg); - } - - // Prepare messages - for (let i = 0; i < normalizedMessages.length; i++) { - const msg = normalizedMessages[i]; - const role = msg.role === "user" ? ROLE.USER : ROLE.ASSISTANT; - const msgId = uuidv4(); - const isLast = i === normalizedMessages.length - 1; - - formattedMessages.push({ - content: msg.content, - role, - messageId: msgId, - isLast, - hasTools, - toolResults: msg.tool_results || [] - }); - - messageIds.push({ messageId: msgId, role }); - } - - // Map reasoning effort to thinking level - let thinkingLevel = THINKING_LEVEL.UNSPECIFIED; - if (reasoningEffort === "medium") thinkingLevel = THINKING_LEVEL.MEDIUM; - else if (reasoningEffort === "high") thinkingLevel = THINKING_LEVEL.HIGH; - - // Build request - return concatArrays( - // Messages - ...formattedMessages.map(fm => - encodeField(FIELD.MESSAGES, WIRE_TYPE.LEN, - encodeMessage(fm.content, fm.role, fm.messageId, null, fm.isLast, fm.hasTools, fm.toolResults) - ) - ), - - // Static fields - encodeField(FIELD.UNKNOWN_2, WIRE_TYPE.VARINT, 1), - encodeField(FIELD.INSTRUCTION, WIRE_TYPE.LEN, encodeInstruction("")), - encodeField(FIELD.UNKNOWN_4, WIRE_TYPE.VARINT, 1), - encodeField(FIELD.MODEL, WIRE_TYPE.LEN, encodeModel(modelName)), - encodeField(FIELD.WEB_TOOL, WIRE_TYPE.LEN, ""), - encodeField(FIELD.UNKNOWN_13, WIRE_TYPE.VARINT, 1), - encodeField(FIELD.CURSOR_SETTING, WIRE_TYPE.LEN, encodeCursorSetting()), - encodeField(FIELD.UNKNOWN_19, WIRE_TYPE.VARINT, 1), - encodeField(FIELD.CONVERSATION_ID, WIRE_TYPE.LEN, uuidv4()), - encodeField(FIELD.METADATA, WIRE_TYPE.LEN, encodeMetadata()), - - // Tool-related fields - encodeField(FIELD.IS_AGENTIC, WIRE_TYPE.VARINT, isAgentic ? 1 : 0), - ...(isAgentic ? [encodeField(FIELD.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))] : []), - - // Message IDs - ...messageIds.map(mid => - encodeField(FIELD.MESSAGE_IDS, WIRE_TYPE.LEN, encodeMessageId(mid.messageId, mid.role)) - ), - - // MCP Tools - ...(tools?.length > 0 ? tools.map(tool => - encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool)) - ) : []), - - // Mode fields - encodeField(FIELD.LARGE_CONTEXT, WIRE_TYPE.VARINT, 0), - encodeField(FIELD.UNKNOWN_38, WIRE_TYPE.VARINT, 0), - encodeField(FIELD.UNIFIED_MODE, WIRE_TYPE.VARINT, isAgentic ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT), - encodeField(FIELD.UNKNOWN_47, WIRE_TYPE.LEN, ""), - encodeField(FIELD.SHOULD_DISABLE_TOOLS, WIRE_TYPE.VARINT, isAgentic ? 0 : 1), - encodeField(FIELD.THINKING_LEVEL, WIRE_TYPE.VARINT, thinkingLevel), - encodeField(FIELD.UNKNOWN_51, WIRE_TYPE.VARINT, 0), - encodeField(FIELD.UNKNOWN_53, WIRE_TYPE.VARINT, 1), - encodeField(FIELD.UNIFIED_MODE_NAME, WIRE_TYPE.LEN, isAgentic ? "Agent" : "Ask") - ); -} - -export function buildChatRequest(messages, modelName, tools = [], reasoningEffort = null) { - return encodeField(FIELD.REQUEST, WIRE_TYPE.LEN, encodeRequest(messages, modelName, tools, reasoningEffort)); -} - -/** - * Encode a tool result as ClientSideToolV2Result (field 2 of StreamUnifiedChatRequestWithTools) - * This is sent as a SEPARATE request frame, not inside conversation messages. - * Proto: StreamUnifiedChatRequestWithTools.client_side_tool_v2_result = 2 - */ -export function buildToolResultRequest(toolResult) { - const { toolCallId, modelCallId } = parseToolId(toolResult.tool_call_id || ""); - const rawName = toolResult.tool_name || ""; - const resultContent = toolResult.result_content || ""; - - // selected_tool = raw tool name (e.g. "Write", "Read") per cursor-api Rust source: - // McpResult { selected_tool: tool_name, result } where tool_name is the mcpParams.tools[0].name - // which is the name AFTER server prefix stripping (e.g. "custom_Write" -> name = "Write") - // Actually cursor-api uses: name = tool_name.slice_unchecked(d+1..) → raw name without "custom_" - // So selected_tool = raw tool name without any prefix - const selectedTool = rawName.startsWith("mcp_custom_") - ? rawName.slice("mcp_custom_".length) - : rawName.startsWith("mcp_") - ? rawName.slice(4) - : rawName; - - // ClientSideToolV2Result per proto: - // field 1 (tool): varint = 19 (MCP) - // field 28 (mcp_result): LEN { field 1: selected_tool, field 2: result } - // field 35 (tool_call_id): string - // field 48 (model_call_id): string (optional) - // NO tool_index (None in Rust source: encode_tool_result sets tool_index: None) - const cv2Result = concatArrays( - encodeField(FIELD.CV2R_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2_MCP), - encodeField(FIELD.CV2R_MCP_RESULT, WIRE_TYPE.LEN, encodeMcpResult(selectedTool, resultContent)), - encodeField(FIELD.CV2R_CALL_ID, WIRE_TYPE.LEN, toolCallId), - ...(modelCallId ? [encodeField(FIELD.CV2R_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []) - // tool_index intentionally omitted (None per Rust source) - ); - - // StreamUnifiedChatRequestWithTools: field 2 = client_side_tool_v2_result - return encodeField(2, WIRE_TYPE.LEN, cv2Result); -} - -export function wrapConnectRPCFrame(payload, compress = false) { - let finalPayload = payload; - let flags = 0x00; - - if (compress) { - finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload))); - flags = 0x01; - } - - const frame = new Uint8Array(5 + finalPayload.length); - frame[0] = flags; - frame[1] = (finalPayload.length >> 24) & 0xFF; - frame[2] = (finalPayload.length >> 16) & 0xFF; - frame[3] = (finalPayload.length >> 8) & 0xFF; - frame[4] = finalPayload.length & 0xFF; - frame.set(finalPayload, 5); - - return frame; -} - -export function generateCursorBody(messages, modelName, tools = [], reasoningEffort = null) { - log("BODY", `Generating: ${messages.length} msgs, model=${modelName}, tools=${tools.length}, reasoning=${reasoningEffort || "none"}`); - - const protobuf = buildChatRequest(messages, modelName, tools, reasoningEffort); - const framed = wrapConnectRPCFrame(protobuf, false); // Cursor doesn't support compressed requests - - log("BODY", `Protobuf=${protobuf.length}B, Framed=${framed.length}B`); - return framed; -} - -/** - * Generate a framed tool result body to send as a separate request frame. - * Uses field 2 (client_side_tool_v2_result) of StreamUnifiedChatRequestWithTools. - */ -export function generateToolResultBody(toolResult) { - const protobuf = buildToolResultRequest(toolResult); - return wrapConnectRPCFrame(protobuf, false); -} - -// ==================== PRIMITIVE DECODING ==================== - -export function decodeVarint(buffer, offset) { - let result = 0; - let shift = 0; - let pos = offset; - - while (pos < buffer.length) { - const b = buffer[pos]; - result |= (b & 0x7F) << shift; - pos++; - if (!(b & 0x80)) break; - shift += 7; - } - - return [result, pos]; -} - -export function decodeField(buffer, offset) { - if (offset >= buffer.length) return [null, null, null, offset]; - - const [tag, pos1] = decodeVarint(buffer, offset); - const fieldNum = tag >> 3; - const wireType = tag & 0x07; - - let value; - let pos = pos1; - - if (wireType === WIRE_TYPE.VARINT) { - [value, pos] = decodeVarint(buffer, pos); - } else if (wireType === WIRE_TYPE.LEN) { - const [length, pos2] = decodeVarint(buffer, pos); - value = buffer.slice(pos2, pos2 + length); - pos = pos2 + length; - } else if (wireType === WIRE_TYPE.FIXED64) { - value = buffer.slice(pos, pos + 8); - pos += 8; - } else if (wireType === WIRE_TYPE.FIXED32) { - value = buffer.slice(pos, pos + 4); - pos += 4; - } else { - value = null; - } - - return [fieldNum, wireType, value, pos]; -} - -export function decodeMessage(data) { - const fields = new Map(); - let pos = 0; - - while (pos < data.length) { - const [fieldNum, wireType, value, newPos] = decodeField(data, pos); - if (fieldNum === null) break; - - if (!fields.has(fieldNum)) fields.set(fieldNum, []); - fields.get(fieldNum).push({ wireType, value }); - pos = newPos; - } - - return fields; -} - -// ==================== RESPONSE PARSING ==================== - -export function parseConnectRPCFrame(buffer) { - if (buffer.length < 5) return null; - - const flags = buffer[0]; - const length = (buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4]; - - if (buffer.length < 5 + length) return null; - - let payload = buffer.slice(5, 5 + length); - - // Decompress if gzip - if (flags === 0x01) { - try { - payload = new Uint8Array(zlib.gunzipSync(Buffer.from(payload))); - } catch (err) { - log("PARSE", `Decompression failed: ${err.message}`); - } - } - - return { flags, length, payload, consumed: 5 + length }; -} - -function extractToolCall(toolCallData) { - const toolCall = decodeMessage(toolCallData); - let toolCallId = ""; - let toolName = ""; - let rawArgs = ""; - let isLast = false; - - // Extract tool call ID - if (toolCall.has(FIELD.TOOL_ID)) { - const fullId = new TextDecoder().decode(toolCall.get(FIELD.TOOL_ID)[0].value); - toolCallId = fullId.split("\n")[0]; // Cursor returns multi-line ID, take first line - } - - // Extract tool name - if (toolCall.has(FIELD.TOOL_NAME)) { - toolName = new TextDecoder().decode(toolCall.get(FIELD.TOOL_NAME)[0].value); - } - - // Extract is_last flag - if (toolCall.has(FIELD.TOOL_IS_LAST)) { - isLast = toolCall.get(FIELD.TOOL_IS_LAST)[0].value !== 0; - } - - // Extract MCP params - nested real tool info - if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) { - try { - const mcpParams = decodeMessage(toolCall.get(FIELD.TOOL_MCP_PARAMS)[0].value); - - if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) { - const tool = decodeMessage(mcpParams.get(FIELD.MCP_TOOLS_LIST)[0].value); - - if (tool.has(FIELD.MCP_NESTED_NAME)) { - toolName = new TextDecoder().decode(tool.get(FIELD.MCP_NESTED_NAME)[0].value); - } - - if (tool.has(FIELD.MCP_NESTED_PARAMS)) { - rawArgs = new TextDecoder().decode(tool.get(FIELD.MCP_NESTED_PARAMS)[0].value); - } - } - } catch (err) { - log("EXTRACT", `MCP parse error: ${err.message}`); - } - } - - // Fallback to raw_args - if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) { - rawArgs = new TextDecoder().decode(toolCall.get(FIELD.TOOL_RAW_ARGS)[0].value); - } - - if (toolCallId && toolName) { - return { - id: toolCallId, - type: "function", - function: { - name: toolName, - arguments: rawArgs || "{}" - }, - isLast - }; - } - - return null; -} - -function extractTextAndThinking(responseData) { - const nested = decodeMessage(responseData); - let text = null; - let thinking = null; - - // Extract text - if (nested.has(FIELD.RESPONSE_TEXT)) { - text = new TextDecoder().decode(nested.get(FIELD.RESPONSE_TEXT)[0].value); - } - - // Extract thinking - if (nested.has(FIELD.THINKING)) { - try { - const thinkingMsg = decodeMessage(nested.get(FIELD.THINKING)[0].value); - if (thinkingMsg.has(FIELD.THINKING_TEXT)) { - thinking = new TextDecoder().decode(thinkingMsg.get(FIELD.THINKING_TEXT)[0].value); - } - } catch (err) { - log("EXTRACT", `Thinking parse error: ${err.message}`); - } - } - - return { text, thinking }; -} - -export function extractTextFromResponse(payload) { - try { - const fields = decodeMessage(payload); - - // Warn about unknown field numbers — may indicate a Cursor protocol update - for (const fieldNum of fields.keys()) { - if (!KNOWN_RESPONSE_FIELDS.has(fieldNum)) { - log( - "SCHEMA", - `Unknown response field #${fieldNum} detected. Schema v${PROTOBUF_SCHEMA_VERSION} may be outdated.` - ); - } - } - - // Field 1: ClientSideToolV2Call - if (fields.has(FIELD.TOOL_CALL)) { - const toolCall = extractToolCall(fields.get(FIELD.TOOL_CALL)[0].value); - if (toolCall) { - log("EXTRACT", `Tool call: ${toolCall.function.name}`); - return { text: null, error: null, toolCall, thinking: null }; - } - } - - // Field 2: StreamUnifiedChatResponse - if (fields.has(FIELD.RESPONSE)) { - const { text, thinking } = extractTextAndThinking(fields.get(FIELD.RESPONSE)[0].value); - - if (text || thinking) { - return { text, error: null, toolCall: null, thinking }; - } - } - - return { text: null, error: null, toolCall: null, thinking: null }; - } catch (err) { - log("EXTRACT", `Decode failed (schema v${PROTOBUF_SCHEMA_VERSION}): ${err.message}`); - return { - text: null, - error: null, - toolCall: null, - thinking: null, - raw: Buffer.from(payload).toString("base64"), - decodeError: err.message - }; - } -} - -// ==================== EXPORTS ==================== - -export default { - encodeVarint, - encodeField, - encodeMessage, - buildChatRequest, - wrapConnectRPCFrame, - generateCursorBody, - decodeVarint, - decodeField, - decodeMessage, - parseConnectRPCFrame, - extractTextFromResponse -}; diff --git a/9router/open-sse/utils/error.js b/9router/open-sse/utils/error.js deleted file mode 100644 index cf637873..00000000 --- a/9router/open-sse/utils/error.js +++ /dev/null @@ -1,185 +0,0 @@ -import { ERROR_TYPES, DEFAULT_ERROR_MESSAGES } from "../config/runtimeConfig.js"; - -/** - * Build OpenAI-compatible error response body - * @param {number} statusCode - HTTP status code - * @param {string} message - Error message - * @returns {object} Error response object - */ -export function buildErrorBody(statusCode, message) { - const errorInfo = ERROR_TYPES[statusCode] || - (statusCode >= 500 - ? { type: "server_error", code: "internal_server_error" } - : { type: "invalid_request_error", code: "" }); - - return { - error: { - message: message || DEFAULT_ERROR_MESSAGES[statusCode] || "An error occurred", - type: errorInfo.type, - code: errorInfo.code - } - }; -} - -/** - * Create error Response object (for non-streaming) - * @param {number} statusCode - HTTP status code - * @param {string} message - Error message - * @returns {Response} HTTP Response object - */ -export function errorResponse(statusCode, message) { - return new Response(JSON.stringify(buildErrorBody(statusCode, message)), { - status: statusCode, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }); -} - -/** - * Write error to SSE stream (for streaming) - * @param {WritableStreamDefaultWriter} writer - Stream writer - * @param {number} statusCode - HTTP status code - * @param {string} message - Error message - */ -export async function writeStreamError(writer, statusCode, message) { - const errorBody = buildErrorBody(statusCode, message); - const encoder = new TextEncoder(); - await writer.write(encoder.encode(`data: ${JSON.stringify(errorBody)}\n\n`)); -} - -/** - * Parse Antigravity error message to extract retry time - * Example: "You have exhausted your capacity on this model. Your quota will reset after 2h7m23s." - * @param {string} message - Error message - * @returns {number|null} Retry time in milliseconds, or null if not found - */ -export function parseAntigravityRetryTime(message) { - if (typeof message !== "string") return null; - - // Match patterns like: 2h7m23s, 5m30s, 45s, 1h20m, etc. - const match = message.match(/reset after (\d+h)?(\d+m)?(\d+s)?/i); - if (!match) return null; - - let totalMs = 0; - - // Extract hours - if (match[1]) { - const hours = parseInt(match[1]); - totalMs += hours * 60 * 60 * 1000; - } - - // Extract minutes - if (match[2]) { - const minutes = parseInt(match[2]); - totalMs += minutes * 60 * 1000; - } - - // Extract seconds - if (match[3]) { - const seconds = parseInt(match[3]); - totalMs += seconds * 1000; - } - - return totalMs > 0 ? totalMs : null; -} - -/** - * Parse upstream provider error response - * @param {Response} response - Fetch response from provider - * @param {string} provider - Provider name (for Antigravity-specific parsing) - * @returns {Promise<{statusCode: number, message: string, retryAfterMs: number|null}>} - */ -export async function parseUpstreamError(response, provider = null) { - let message = ""; - let retryAfterMs = null; - - try { - const text = await response.text(); - - // Try parse as JSON - try { - const json = JSON.parse(text); - message = json.error?.message || json.message || json.error || text; - } catch { - message = text; - } - } catch { - message = `Upstream error: ${response.status}`; - } - - const messageStr = typeof message === "string" ? message : JSON.stringify(message); - const finalMessage = messageStr || DEFAULT_ERROR_MESSAGES[response.status] || `Upstream error: ${response.status}`; - - // Parse Antigravity-specific retry time from error message - if (provider === "antigravity" && response.status === 429) { - retryAfterMs = parseAntigravityRetryTime(finalMessage); - } - - return { - statusCode: response.status, - message: finalMessage, - retryAfterMs - }; -} - -/** - * Create error result for chatCore handler - * @param {number} statusCode - HTTP status code - * @param {string} message - Error message - * @param {number|null} retryAfterMs - Optional retry-after time in milliseconds - * @returns {{ success: false, status: number, error: string, response: Response, retryAfterMs?: number }} - */ -export function createErrorResult(statusCode, message, retryAfterMs = null) { - const result = { - success: false, - status: statusCode, - error: message, - response: errorResponse(statusCode, message) - }; - - // Add retryAfterMs if available (for Antigravity quota errors) - if (retryAfterMs) { - result.retryAfterMs = retryAfterMs; - } - - return result; -} - -/** - * Create unavailable response when all accounts are rate limited - * @param {number} statusCode - Original error status code - * @param {string} message - Error message (without retry info) - * @param {string} retryAfter - ISO timestamp when earliest account becomes available - * @param {string} retryAfterHuman - Human-readable retry info e.g. "reset after 30s" - * @returns {Response} - */ -export function unavailableResponse(statusCode, message, retryAfter, retryAfterHuman) { - const retryAfterSec = Math.max(Math.ceil((new Date(retryAfter).getTime() - Date.now()) / 1000), 1); - const msg = `${message} (${retryAfterHuman})`; - return new Response( - JSON.stringify({ error: { message: msg } }), - { - status: statusCode, - headers: { - "Content-Type": "application/json", - "Retry-After": String(retryAfterSec) - } - } - ); -} - -/** - * Format provider error with context - * @param {Error} error - Original error - * @param {string} provider - Provider name - * @param {string} model - Model name - * @param {number|string} statusCode - HTTP status code or error code - * @returns {string} Formatted error message - */ -export function formatProviderError(error, provider, model, statusCode) { - const code = statusCode || error.code || 'FETCH_FAILED'; - const message = error.message || "Unknown error"; - return `[${code}]: ${message}`; -} diff --git a/9router/open-sse/utils/ollamaTransform.js b/9router/open-sse/utils/ollamaTransform.js deleted file mode 100644 index b4fb6a65..00000000 --- a/9router/open-sse/utils/ollamaTransform.js +++ /dev/null @@ -1,85 +0,0 @@ -// Transform OpenAI SSE stream to Ollama JSON lines format -export function transformToOllama(response, model) { - let buffer = ""; - let pendingToolCalls = {}; - - const transform = new TransformStream({ - transform(chunk, controller) { - const text = new TextDecoder().decode(chunk); - buffer += text; - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.startsWith("data:")) continue; - const data = line.slice(5).trim(); - - if (data === "[DONE]") { - const ollamaEnd = JSON.stringify({ model, message: { role: "assistant", content: "" }, done: true }) + "\n"; - controller.enqueue(new TextEncoder().encode(ollamaEnd)); - return; - } - - try { - const parsed = JSON.parse(data); - const delta = parsed.choices?.[0]?.delta || {}; - const content = delta.content || ""; - const toolCalls = delta.tool_calls; - - if (toolCalls) { - for (const tc of toolCalls) { - const idx = tc.index; - if (!pendingToolCalls[idx]) { - pendingToolCalls[idx] = { id: tc.id, function: { name: "", arguments: "" } }; - } - if (tc.function?.name) pendingToolCalls[idx].function.name += tc.function.name; - if (tc.function?.arguments) pendingToolCalls[idx].function.arguments += tc.function.arguments; - } - } - - if (content) { - const ollama = JSON.stringify({ model, message: { role: "assistant", content }, done: false }) + "\n"; - controller.enqueue(new TextEncoder().encode(ollama)); - } - - const finishReason = parsed.choices?.[0]?.finish_reason; - if (finishReason === "tool_calls" || finishReason === "stop") { - const toolCallsArr = Object.values(pendingToolCalls); - if (toolCallsArr.length > 0) { - const formattedCalls = toolCallsArr.map(tc => ({ - function: { - name: tc.function.name, - arguments: (() => { try { return JSON.parse(tc.function.arguments || "{}"); } catch { return {}; } })() - } - })); - const ollama = JSON.stringify({ - model, - message: { role: "assistant", content: "", tool_calls: formattedCalls }, - done: true - }) + "\n"; - controller.enqueue(new TextEncoder().encode(ollama)); - pendingToolCalls = {}; - } else if (finishReason === "stop") { - const ollamaEnd = JSON.stringify({ model, message: { role: "assistant", content: "" }, done: true }) + "\n"; - controller.enqueue(new TextEncoder().encode(ollamaEnd)); - } - } - } catch (e) { - // Silently ignore parse errors - } - } - }, - flush(controller) { - const ollamaEnd = JSON.stringify({ model, message: { role: "assistant", content: "" }, done: true }) + "\n"; - controller.enqueue(new TextEncoder().encode(ollamaEnd)); - } - }); - - if (!response.body) { - return new Response("", { status: response.status, headers: { "Content-Type": "application/x-ndjson" } }); - } - return new Response(response.body.pipeThrough(transform), { - headers: { "Content-Type": "application/x-ndjson", "Access-Control-Allow-Origin": "*" } - }); -} - diff --git a/9router/open-sse/utils/proxyFetch.js b/9router/open-sse/utils/proxyFetch.js deleted file mode 100644 index c97c909b..00000000 --- a/9router/open-sse/utils/proxyFetch.js +++ /dev/null @@ -1,258 +0,0 @@ -import { Readable } from "stream"; -import { MEMORY_CONFIG } from "../config/runtimeConfig.js"; - -const isCloud = typeof caches !== "undefined" && typeof caches === "object"; - -const originalFetch = globalThis.fetch; -const proxyDispatchers = new Map(); - -// DNS cache — use Map to avoid prototype pollution via malformed hostnames -const DNS_CACHE = new Map(); -const MITM_BYPASS_HOSTS = [ - "cloudcode-pa.googleapis.com", - "daily-cloudcode-pa.googleapis.com", - "api.individual.githubcopilot.com", - "q.us-east-1.amazonaws.com", - "codewhisperer.us-east-1.amazonaws.com", - "api2.cursor.sh", -]; -const GOOGLE_DNS_SERVERS = ["8.8.8.8", "8.8.4.4"]; -const HTTPS_PORT = 443; -const HTTP_SUCCESS_MIN = 200; -const HTTP_SUCCESS_MAX = 300; - -function normalizeString(value) { - if (value === undefined || value === null) return ""; - return String(value).trim(); -} - -/** - * Resolve real IP using Google DNS (bypass system DNS) - */ -async function resolveRealIP(hostname) { - const cached = DNS_CACHE.get(hostname); - if (cached && Date.now() < cached.expiry) return cached.ip; - - try { - const dns = await import("dns"); - const { promisify } = await import("util"); - const resolver = new dns.Resolver(); - resolver.setServers(GOOGLE_DNS_SERVERS); - const resolve4 = promisify(resolver.resolve4.bind(resolver)); - const addresses = await resolve4(hostname); - DNS_CACHE.set(hostname, { ip: addresses[0], expiry: Date.now() + MEMORY_CONFIG.dnsCacheTtlMs }); - return addresses[0]; - } catch (error) { - console.warn(`[ProxyFetch] DNS resolve failed for ${hostname}:`, error.message); - return null; - } -} - -/** - * Check if request should bypass MITM DNS redirect - */ -function shouldBypassMitmDns(url) { - try { - const hostname = new URL(url).hostname; - return MITM_BYPASS_HOSTS.some(host => hostname.includes(host)); - } catch { return false; } -} - -function shouldBypassByNoProxy(targetUrl, noProxyValue) { - const noProxy = normalizeString(noProxyValue); - if (!noProxy) return false; - - let hostname; - try { hostname = new URL(targetUrl).hostname.toLowerCase(); } catch { return false; } - const patterns = noProxy.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean); - - return patterns.some((pattern) => { - if (pattern === "*") return true; - if (pattern.startsWith(".")) return hostname.endsWith(pattern) || hostname === pattern.slice(1); - return hostname === pattern || hostname.endsWith(`.${pattern}`); - }); -} - -/** - * Get proxy URL from environment - */ -function getEnvProxyUrl(targetUrl) { - const noProxy = process.env.NO_PROXY || process.env.no_proxy; - if (shouldBypassByNoProxy(targetUrl, noProxy)) return null; - - let protocol; - try { protocol = new URL(targetUrl).protocol; } catch { return null; } - - if (protocol === "https:") { - return process.env.HTTPS_PROXY || process.env.https_proxy || - process.env.ALL_PROXY || process.env.all_proxy; - } - - return process.env.HTTP_PROXY || process.env.http_proxy || - process.env.ALL_PROXY || process.env.all_proxy; -} - -/** - * Normalize proxy URL (allow host:port) - */ -function normalizeProxyUrl(proxyUrl) { - const normalizedInput = normalizeString(proxyUrl); - if (!normalizedInput) return null; - - try { - // eslint-disable-next-line no-new - new URL(normalizedInput); - return normalizedInput; - } catch { - // Allow "127.0.0.1:7890" style values - return `http://${normalizedInput}`; - } -} - -function resolveConnectionProxyUrl(targetUrl, proxyOptions) { - const enabled = proxyOptions?.enabled === true || proxyOptions?.connectionProxyEnabled === true; - if (!enabled) return null; - - const proxyUrlRaw = normalizeString(proxyOptions?.url ?? proxyOptions?.connectionProxyUrl); - if (!proxyUrlRaw) return null; - - const noProxy = normalizeString(proxyOptions?.noProxy ?? proxyOptions?.connectionNoProxy); - if (noProxy && shouldBypassByNoProxy(targetUrl, noProxy)) return null; - - return normalizeProxyUrl(proxyUrlRaw); -} - -/** - * Create proxy dispatcher lazily (undici-compatible) - */ -async function getDispatcher(proxyUrl) { - const normalized = normalizeProxyUrl(proxyUrl); - if (!normalized) return null; - - if (!proxyDispatchers.has(normalized)) { - // Evict oldest entry if max size reached - if (proxyDispatchers.size >= MEMORY_CONFIG.proxyDispatchersMaxSize) { - proxyDispatchers.delete(proxyDispatchers.keys().next().value); - } - const { ProxyAgent } = await import("undici"); - proxyDispatchers.set(normalized, new ProxyAgent({ uri: normalized })); - } - - return proxyDispatchers.get(normalized); -} - -/** - * Create HTTPS request with manual socket connection (bypass DNS) - */ -async function createBypassRequest(parsedUrl, realIP, options) { - const httpsModule = await import("https"); - const netModule = await import("net"); - // CJS modules expose exports via .default in ESM dynamic import context - const https = httpsModule.default ?? httpsModule; - const net = netModule.default ?? netModule; - - return new Promise((resolve, reject) => { - const socket = new net.Socket(); - - socket.connect(HTTPS_PORT, realIP, () => { - const reqOptions = { - socket, - servername: parsedUrl.hostname, - rejectUnauthorized: false, - path: parsedUrl.pathname + parsedUrl.search, - method: options.method || "POST", - headers: { - ...options.headers, - Host: parsedUrl.hostname, - }, - }; - - const req = https.request(reqOptions, (res) => { - const response = { - ok: res.statusCode >= HTTP_SUCCESS_MIN && res.statusCode < HTTP_SUCCESS_MAX, - status: res.statusCode, - statusText: res.statusMessage, - headers: new Map(Object.entries(res.headers)), - body: Readable.toWeb(res), - text: async () => { - const chunks = []; - for await (const chunk of res) chunks.push(chunk); - return Buffer.concat(chunks).toString(); - }, - json: async () => JSON.parse(await response.text()), - }; - resolve(response); - }); - - req.on("error", reject); - if (options.body) { - req.write(typeof options.body === "string" ? options.body : JSON.stringify(options.body)); - } - req.end(); - }); - - socket.on("error", reject); - }); -} - -export async function proxyAwareFetch(url, options = {}, proxyOptions = null) { - const targetUrl = typeof url === "string" ? url : url.toString(); - - const connectionProxyUrl = resolveConnectionProxyUrl(targetUrl, proxyOptions); - const envProxyUrl = connectionProxyUrl ? null : normalizeProxyUrl(getEnvProxyUrl(targetUrl)); - const proxyUrl = connectionProxyUrl || envProxyUrl; - - // MITM DNS bypass: for known MITM-intercepted hosts, resolve real IP to avoid DNS spoof - if (shouldBypassMitmDns(targetUrl)) { - if (proxyUrl) { - // Proxy resolves DNS externally (not affected by /etc/hosts) — use proxy directly - try { - const dispatcher = await getDispatcher(proxyUrl); - return await originalFetch(url, { ...options, dispatcher }); - } catch (proxyError) { - if (proxyOptions?.strictProxy === true) { - throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`); - } - console.warn(`[ProxyFetch] Proxy failed, falling back to direct bypass: ${proxyError.message}`); - } - } - // No proxy — manually resolve real IP to bypass DNS spoof - try { - const parsedUrl = new URL(targetUrl); - const realIP = await resolveRealIP(parsedUrl.hostname); - if (realIP) return await createBypassRequest(parsedUrl, realIP, options); - } catch (error) { - console.warn(`[ProxyFetch] MITM bypass failed: ${error.message}`); - } - } - - if (proxyUrl) { - try { - const dispatcher = await getDispatcher(proxyUrl); - return await originalFetch(url, { ...options, dispatcher }); - } catch (proxyError) { - // If strictProxy is enabled, fail hard instead of falling back to direct - if (proxyOptions?.strictProxy === true) { - throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`); - } - console.warn(`[ProxyFetch] Proxy failed, falling back to direct: ${proxyError.message}`); - return originalFetch(url, options); - } - } - - return originalFetch(url, options); -} - -/** - * Patched global fetch with env-proxy support and MITM DNS bypass - */ -async function patchedFetch(url, options = {}) { - return proxyAwareFetch(url, options, null); -} - -// Idempotency guard — only patch once to avoid wrapping multiple times -if (!isCloud && globalThis.fetch !== patchedFetch) { - globalThis.fetch = patchedFetch; -} - -export default isCloud ? originalFetch : patchedFetch; diff --git a/9router/open-sse/utils/requestLogger.js b/9router/open-sse/utils/requestLogger.js deleted file mode 100644 index 010153d3..00000000 --- a/9router/open-sse/utils/requestLogger.js +++ /dev/null @@ -1,260 +0,0 @@ -// Check if running in Node.js environment (has fs module) -const isNode = typeof process !== "undefined" && process.versions?.node && typeof window === "undefined"; - -// Check if logging is enabled via environment variable (default: false) -const LOGGING_ENABLED = typeof process !== "undefined" && process.env?.ENABLE_REQUEST_LOGS === 'true'; - -let fs = null; -let path = null; -let LOGS_DIR = null; - -// Lazy load Node.js modules (avoid top-level await) -async function ensureNodeModules() { - if (!isNode || !LOGGING_ENABLED || fs) return; - try { - fs = await import("fs"); - path = await import("path"); - LOGS_DIR = path.join(typeof process !== "undefined" && process.cwd ? process.cwd() : ".", "logs"); - } catch { - // Running in non-Node environment (Worker, Browser, etc.) - } -} - -// Format timestamp for folder name: 20251228_143045_123 -function formatTimestamp(date = new Date()) { - const pad = (n) => String(n).padStart(2, "0"); - const y = date.getFullYear(); - const m = pad(date.getMonth() + 1); - const d = pad(date.getDate()); - const h = pad(date.getHours()); - const min = pad(date.getMinutes()); - const s = pad(date.getSeconds()); - const ms = String(date.getMilliseconds()).padStart(3, "0"); - return `${y}${m}${d}_${h}${min}${s}_${ms}`; -} - -// Create log session folder: {sourceFormat}_{targetFormat}_{model}_{timestamp} -async function createLogSession(sourceFormat, targetFormat, model) { - await ensureNodeModules(); - if (!fs || !LOGS_DIR) return null; - - try { - if (!fs.existsSync(LOGS_DIR)) { - fs.mkdirSync(LOGS_DIR, { recursive: true }); - } - - const timestamp = formatTimestamp(); - const safeModel = (model || "unknown").replace(/[/:]/g, "-"); - const folderName = `${sourceFormat}_${targetFormat}_${safeModel}_${timestamp}`; - const sessionPath = path.join(LOGS_DIR, folderName); - - fs.mkdirSync(sessionPath, { recursive: true }); - - return sessionPath; - } catch (err) { - console.log("[LOG] Failed to create log session:", err.message); - return null; - } -} - -// Write JSON file -function writeJsonFile(sessionPath, filename, data) { - if (!fs || !sessionPath) return; - - try { - const filePath = path.join(sessionPath, filename); - fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); - } catch (err) { - console.log(`[LOG] Failed to write ${filename}:`, err.message); - } -} - -// Mask sensitive data in headers (DISABLED - keep full token for testing) -function maskSensitiveHeaders(headers) { - if (!headers) return {}; - return { ...headers }; - - // Old masking code (disabled): - // const masked = { ...headers }; - // const sensitiveKeys = ["authorization", "x-api-key", "cookie", "token"]; - // - // for (const key of Object.keys(masked)) { - // const lowerKey = key.toLowerCase(); - // if (sensitiveKeys.some(sk => lowerKey.includes(sk))) { - // const value = masked[key]; - // if (value && value.length > 20) { - // masked[key] = value.slice(0, 10) + "..." + value.slice(-5); - // } - // } - // } - // return masked; -} - -// No-op logger when logging is disabled -function createNoOpLogger() { - return { - sessionPath: null, - logClientRawRequest() {}, - logRawRequest() {}, - logOpenAIRequest() {}, - logTargetRequest() {}, - logProviderResponse() {}, - appendProviderChunk() {}, - appendOpenAIChunk() {}, - logConvertedResponse() {}, - appendConvertedChunk() {}, - logError() {} - }; -} - -/** - * Create a new log session and return logger functions - * @param {string} sourceFormat - Source format from client (claude, openai, etc.) - * @param {string} targetFormat - Target format to provider (antigravity, gemini-cli, etc.) - * @param {string} model - Model name - * @returns {Promise} Promise that resolves to logger object with methods to log each stage - */ -export async function createRequestLogger(sourceFormat, targetFormat, model) { - // Return no-op logger if logging is disabled - if (!LOGGING_ENABLED) { - return createNoOpLogger(); - } - - // Wait for session to be created before returning logger - const sessionPath = await createLogSession(sourceFormat, targetFormat, model); - - return { - get sessionPath() { return sessionPath; }, - - // 1. Log client raw request (before any conversion) - logClientRawRequest(endpoint, body, headers = {}) { - writeJsonFile(sessionPath, "1_req_client.json", { - timestamp: new Date().toISOString(), - endpoint, - headers: maskSensitiveHeaders(headers), - body - }); - }, - - // 2. Log raw request from client (after initial conversion like responsesApi) - logRawRequest(body, headers = {}) { - writeJsonFile(sessionPath, "2_req_source.json", { - timestamp: new Date().toISOString(), - headers: maskSensitiveHeaders(headers), - body - }); - }, - - // 3. Log OpenAI intermediate format (source → openai) - logOpenAIRequest(body) { - writeJsonFile(sessionPath, "3_req_openai.json", { - timestamp: new Date().toISOString(), - body - }); - }, - - // 4. Log target format request (openai → target) - logTargetRequest(url, headers, body) { - writeJsonFile(sessionPath, "4_req_target.json", { - timestamp: new Date().toISOString(), - url, - headers: maskSensitiveHeaders(headers), - body - }); - }, - - // 5. Log provider response (for non-streaming or error) - logProviderResponse(status, statusText, headers, body) { - const filename = "5_res_provider.json"; - writeJsonFile(sessionPath, filename, { - timestamp: new Date().toISOString(), - status, - statusText, - headers: headers ? (typeof headers.entries === "function" ? Object.fromEntries(headers.entries()) : headers) : {}, - body - }); - }, - - // 5. Append streaming chunk to provider response - appendProviderChunk(chunk) { - if (!fs || !sessionPath) return; - try { - const filePath = path.join(sessionPath, "5_res_provider.txt"); - fs.appendFileSync(filePath, chunk); - } catch (err) { - // Ignore append errors - } - }, - - // 6. Append OpenAI intermediate chunks (target → openai) - appendOpenAIChunk(chunk) { - if (!fs || !sessionPath) return; - try { - const filePath = path.join(sessionPath, "6_res_openai.txt"); - fs.appendFileSync(filePath, chunk); - } catch (err) { - // Ignore append errors - } - }, - - // 7. Log converted response to client (for non-streaming) - logConvertedResponse(body) { - writeJsonFile(sessionPath, "7_res_client.json", { - timestamp: new Date().toISOString(), - body - }); - }, - - // 7. Append streaming chunk to converted response - appendConvertedChunk(chunk) { - if (!fs || !sessionPath) return; - try { - const filePath = path.join(sessionPath, "7_res_client.txt"); - fs.appendFileSync(filePath, chunk); - } catch (err) { - // Ignore append errors - } - }, - - // 6. Log error - logError(error, requestBody = null) { - writeJsonFile(sessionPath, "6_error.json", { - timestamp: new Date().toISOString(), - error: error?.message || String(error), - stack: error?.stack, - requestBody - }); - } - }; -} - -// Legacy functions for backward compatibility -export function logRequest() {} -export function logResponse() {} -export function logError(provider, { error, url, model, requestBody }) { - if (!fs || !LOGS_DIR) return; - - try { - if (!fs.existsSync(LOGS_DIR)) { - fs.mkdirSync(LOGS_DIR, { recursive: true }); - } - - const date = new Date().toISOString().split("T")[0]; - const logPath = path.join(LOGS_DIR, `${provider}-${date}.log`); - - const logEntry = { - timestamp: new Date().toISOString(), - type: "error", - provider, - model, - url, - error: error?.message || String(error), - stack: error?.stack, - requestBody - }; - - fs.appendFileSync(logPath, JSON.stringify(logEntry) + "\n"); - } catch (err) { - console.log("[LOG] Failed to write error log:", err.message); - } -} diff --git a/9router/open-sse/utils/sessionManager.js b/9router/open-sse/utils/sessionManager.js deleted file mode 100644 index 7c08013e..00000000 --- a/9router/open-sse/utils/sessionManager.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Session Manager for Antigravity Cloud Code - * - * Handles session ID generation and caching for prompt caching continuity. - * Mimics the Antigravity binary behavior: generates a session ID at startup - * and keeps it for the process lifetime, scoped per account/connection. - * - * Reference: antigravity-claude-proxy/src/cloudcode/session-manager.js - */ - -import crypto from "crypto"; -import { MEMORY_CONFIG } from "../config/runtimeConfig.js"; - -// Runtime storage: Key = connectionId, Value = { sessionId, lastUsed } -const runtimeSessionStore = new Map(); - -// Periodically evict entries that haven't been used within TTL -const cleanupInterval = setInterval(() => { - const now = Date.now(); - for (const [key, entry] of runtimeSessionStore) { - if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) { - runtimeSessionStore.delete(key); - } - } -}, MEMORY_CONFIG.sessionCleanupIntervalMs); - -// Allow Node.js to exit even if interval is still active -if (cleanupInterval.unref) cleanupInterval.unref(); - -/** - * Get or create a session ID for the given connection. - * - * The binary generates a session ID once at startup: `rs() + Date.now()`. - * Since 9router is long-running, we simulate this "per-launch" behavior by - * storing a generated ID in memory for each connection. - * - * - If 9router restarts, the ID changes (matching binary restart behavior). - * - Within a running instance, the ID is stable for that connection. - * - This enables prompt caching while using the EXACT random logic of the binary. - * - * @param {string} connectionId - The connection identifier (email or unique ID) - * @returns {string} A stable session ID string matching binary format - */ -export function deriveSessionId(connectionId) { - if (!connectionId) { - return generateBinaryStyleId(); - } - - const existing = runtimeSessionStore.get(connectionId); - if (existing) { - existing.lastUsed = Date.now(); - return existing.sessionId; - } - - // Evict oldest entry if store exceeds max size (safety cap between cleanup cycles) - const MAX_SESSIONS = 1000; - if (runtimeSessionStore.size >= MAX_SESSIONS) { - const oldest = runtimeSessionStore.keys().next().value; - runtimeSessionStore.delete(oldest); - } - - const sessionId = generateBinaryStyleId(); - runtimeSessionStore.set(connectionId, { sessionId, lastUsed: Date.now() }); - return sessionId; -} - -/** - * Generate a Session ID using the binary's exact logic. - * Format: `rs() + Date.now()` where `rs()` is randomUUID - * - * @returns {string} A session ID in binary format - */ -export function generateBinaryStyleId() { - return crypto.randomUUID() + Date.now().toString(); -} - -/** - * Clears all session IDs (e.g. useful for testing or explicit reset) - */ -export function clearSessionStore() { - runtimeSessionStore.clear(); -} diff --git a/9router/open-sse/utils/stream.js b/9router/open-sse/utils/stream.js deleted file mode 100644 index 138f2ee3..00000000 --- a/9router/open-sse/utils/stream.js +++ /dev/null @@ -1,392 +0,0 @@ -import { translateResponse, initState } from "../translator/index.js"; -import { FORMATS } from "../translator/formats.js"; -import { trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js"; -import { extractUsage, hasValidUsage, estimateUsage, logUsage, addBufferToUsage, filterUsageForFormat, COLORS } from "./usageTracking.js"; -import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./streamHelpers.js"; - -export { COLORS, formatSSE }; - -// sharedEncoder is stateless — safe to share across streams -const sharedEncoder = new TextEncoder(); - -/** - * Stream modes - */ -const STREAM_MODE = { - TRANSLATE: "translate", // Full translation between formats - PASSTHROUGH: "passthrough" // No translation, normalize output, extract usage -}; - -/** - * Create unified SSE transform stream - * @param {object} options - * @param {string} options.mode - Stream mode: translate, passthrough - * @param {string} options.targetFormat - Provider format (for translate mode) - * @param {string} options.sourceFormat - Client format (for translate mode) - * @param {string} options.provider - Provider name - * @param {object} options.reqLogger - Request logger instance - * @param {string} options.model - Model name - * @param {string} options.connectionId - Connection ID for usage tracking - * @param {object} options.body - Request body (for input token estimation) - * @param {function} options.onStreamComplete - Callback when stream completes (content, usage) - * @param {string} options.apiKey - API key for usage tracking - */ -export function createSSEStream(options = {}) { - const { - mode = STREAM_MODE.TRANSLATE, - targetFormat, - sourceFormat, - provider = null, - reqLogger = null, - toolNameMap = null, - model = null, - connectionId = null, - body = null, - onStreamComplete = null, - apiKey = null - } = options; - - let buffer = ""; - let usage = null; - - // Per-stream decoder with stream:true to correctly handle multi-byte chars split across chunks - const decoder = new TextDecoder("utf-8", { fatal: false }); - - const state = mode === STREAM_MODE.TRANSLATE ? { ...initState(sourceFormat), provider, toolNameMap, model } : null; - - let totalContentLength = 0; - let accumulatedContent = ""; - let accumulatedThinking = ""; - let ttftAt = null; - - return new TransformStream({ - transform(chunk, controller) { - if (!ttftAt) { - ttftAt = Date.now(); - } - const text = decoder.decode(chunk, { stream: true }); - buffer += text; - reqLogger?.appendProviderChunk?.(text); - - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - const trimmed = line.trim(); - - // Passthrough mode: normalize and forward - if (mode === STREAM_MODE.PASSTHROUGH) { - let output; - let injectedUsage = false; - - if (trimmed.startsWith("data:") && trimmed.slice(5).trim() !== "[DONE]") { - try { - const parsed = JSON.parse(trimmed.slice(5).trim()); - - const idFixed = fixInvalidId(parsed); - - // Ensure OpenAI-required fields are present on streaming chunks (Letta compat) - let fieldsInjected = false; - if (parsed.choices !== undefined) { - if (!parsed.object) { parsed.object = "chat.completion.chunk"; fieldsInjected = true; } - if (!parsed.created) { parsed.created = Math.floor(Date.now() / 1000); fieldsInjected = true; } - } - - // Strip Azure-specific non-standard fields from streaming chunks - if (parsed.prompt_filter_results !== undefined) { - delete parsed.prompt_filter_results; - fieldsInjected = true; - } - if (parsed?.choices) { - for (const choice of parsed.choices) { - if (choice.content_filter_results !== undefined) { - delete choice.content_filter_results; - fieldsInjected = true; - } - } - } - - if (!hasValuableContent(parsed, FORMATS.OPENAI)) { - continue; - } - - const delta = parsed.choices?.[0]?.delta; - const content = delta?.content; - const reasoning = delta?.reasoning_content; - if (content && typeof content === "string") { - totalContentLength += content.length; - accumulatedContent += content; - } - if (reasoning && typeof reasoning === "string") { - totalContentLength += reasoning.length; - accumulatedThinking += reasoning; - } - - const extracted = extractUsage(parsed); - if (extracted) { - usage = extracted; - } - - const isFinishChunk = parsed.choices?.[0]?.finish_reason; - if (isFinishChunk && !hasValidUsage(parsed.usage)) { - const estimated = estimateUsage(body, totalContentLength, FORMATS.OPENAI); - parsed.usage = filterUsageForFormat(estimated, FORMATS.OPENAI); - output = `data: ${JSON.stringify(parsed)}\n`; - usage = estimated; - injectedUsage = true; - } else if (isFinishChunk && usage) { - const buffered = addBufferToUsage(usage); - parsed.usage = filterUsageForFormat(buffered, FORMATS.OPENAI); - output = `data: ${JSON.stringify(parsed)}\n`; - injectedUsage = true; - } else if (idFixed || fieldsInjected) { - output = `data: ${JSON.stringify(parsed)}\n`; - injectedUsage = true; - } - } catch { } - } - - if (!injectedUsage) { - if (line.startsWith("data:") && !line.startsWith("data: ")) { - output = "data: " + line.slice(5) + "\n"; - } else { - output = line + "\n"; - } - } - - reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(sharedEncoder.encode(output)); - continue; - } - - // Translate mode - if (!trimmed) continue; - - const parsed = parseSSELine(trimmed, targetFormat); - if (!parsed) continue; - - // For Ollama: done=true is the final chunk with finish_reason/usage, must translate - // For other formats: done=true is the [DONE] sentinel, skip - if (parsed && parsed.done && targetFormat !== FORMATS.OLLAMA) { - const output = "data: [DONE]\n\n"; - reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(sharedEncoder.encode(output)); - continue; - } - - // Claude format - content - if (parsed.delta?.text) { - totalContentLength += parsed.delta.text.length; - accumulatedContent += parsed.delta.text; - } - // Claude format - thinking - if (parsed.delta?.thinking) { - totalContentLength += parsed.delta.thinking.length; - accumulatedThinking += parsed.delta.thinking; - } - - // OpenAI format - content - if (parsed.choices?.[0]?.delta?.content) { - totalContentLength += parsed.choices[0].delta.content.length; - accumulatedContent += parsed.choices[0].delta.content; - } - // OpenAI format - reasoning - if (parsed.choices?.[0]?.delta?.reasoning_content) { - totalContentLength += parsed.choices[0].delta.reasoning_content.length; - accumulatedThinking += parsed.choices[0].delta.reasoning_content; - } - - // Gemini format - if (parsed.candidates?.[0]?.content?.parts) { - for (const part of parsed.candidates[0].content.parts) { - if (part.text && typeof part.text === "string") { - totalContentLength += part.text.length; - // Check if this is thinking content - if (part.thought === true) { - accumulatedThinking += part.text; - } else { - accumulatedContent += part.text; - } - } - } - } - - // Extract usage - const extracted = extractUsage(parsed); - if (extracted) state.usage = extracted; // Keep original usage for logging - - // Translate: targetFormat -> openai -> sourceFormat - const translated = translateResponse(targetFormat, sourceFormat, parsed, state); - - // Log OpenAI intermediate chunks (if available) - if (translated?._openaiIntermediate) { - for (const item of translated._openaiIntermediate) { - const openaiOutput = formatSSE(item, FORMATS.OPENAI); - reqLogger?.appendOpenAIChunk?.(openaiOutput); - } - } - - if (translated?.length > 0) { - for (const item of translated) { - // Filter empty chunks - if (!hasValuableContent(item, sourceFormat)) { - continue; // Skip this empty chunk - } - - // Inject estimated usage if finish chunk has no valid usage - const isFinishChunk = item.type === "message_delta" || item.choices?.[0]?.finish_reason; - if (state.finishReason && isFinishChunk && !hasValidUsage(item.usage) && totalContentLength > 0) { - const estimated = estimateUsage(body, totalContentLength, sourceFormat); - item.usage = filterUsageForFormat(estimated, sourceFormat); // Filter + already has buffer - state.usage = estimated; - } else if (state.finishReason && isFinishChunk && state.usage) { - // Add buffer and filter usage for client (but keep original in state.usage for logging) - const buffered = addBufferToUsage(state.usage); - item.usage = filterUsageForFormat(buffered, sourceFormat); - } - - const output = formatSSE(item, sourceFormat); - reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(sharedEncoder.encode(output)); - } - } - } - }, - - flush(controller) { - trackPendingRequest(model, provider, connectionId, false); - try { - const remaining = decoder.decode(); - if (remaining) buffer += remaining; - - if (mode === STREAM_MODE.PASSTHROUGH) { - if (buffer) { - let output = buffer; - if (buffer.startsWith("data:") && !buffer.startsWith("data: ")) { - output = "data: " + buffer.slice(5); - } - reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(sharedEncoder.encode(output)); - } - - if (!hasValidUsage(usage) && totalContentLength > 0) { - usage = estimateUsage(body, totalContentLength, FORMATS.OPENAI); - } - - if (hasValidUsage(usage)) { - logUsage(provider, usage, model, connectionId, apiKey); - } else { - appendRequestLog({ model, provider, connectionId, tokens: null, status: "200 OK" }).catch(() => { }); - } - - // IMPORTANT: In passthrough mode we still must terminate the SSE stream. - // Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel: - // data: [DONE]\n\n - // Without it they can hang until timeout and trigger failover. - const doneOutput = "data: [DONE]\n\n"; - reqLogger?.appendConvertedChunk?.(doneOutput); - controller.enqueue(sharedEncoder.encode(doneOutput)); - - if (onStreamComplete) { - onStreamComplete({ - content: accumulatedContent, - thinking: accumulatedThinking - }, usage, ttftAt); - } - return; - } - - if (buffer.trim()) { - const parsed = parseSSELine(buffer.trim()); - if (parsed && !parsed.done) { - const translated = translateResponse(targetFormat, sourceFormat, parsed, state); - - if (translated?._openaiIntermediate) { - for (const item of translated._openaiIntermediate) { - const openaiOutput = formatSSE(item, FORMATS.OPENAI); - reqLogger?.appendOpenAIChunk?.(openaiOutput); - } - } - - if (translated?.length > 0) { - for (const item of translated) { - const output = formatSSE(item, sourceFormat); - reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(sharedEncoder.encode(output)); - } - } - } - } - - const flushed = translateResponse(targetFormat, sourceFormat, null, state); - - if (flushed?._openaiIntermediate) { - for (const item of flushed._openaiIntermediate) { - const openaiOutput = formatSSE(item, FORMATS.OPENAI); - reqLogger?.appendOpenAIChunk?.(openaiOutput); - } - } - - if (flushed?.length > 0) { - for (const item of flushed) { - const output = formatSSE(item, sourceFormat); - reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(sharedEncoder.encode(output)); - } - } - - const doneOutput = "data: [DONE]\n\n"; - reqLogger?.appendConvertedChunk?.(doneOutput); - controller.enqueue(sharedEncoder.encode(doneOutput)); - - if (!hasValidUsage(state?.usage) && totalContentLength > 0) { - state.usage = estimateUsage(body, totalContentLength, sourceFormat); - } - - if (hasValidUsage(state?.usage)) { - logUsage(state.provider || targetFormat, state.usage, model, connectionId, apiKey); - } else { - appendRequestLog({ model, provider, connectionId, tokens: null, status: "200 OK" }).catch(() => { }); - } - - if (onStreamComplete) { - onStreamComplete({ - content: accumulatedContent, - thinking: accumulatedThinking - }, state?.usage, ttftAt); - } - } catch (error) { - console.log("Error in flush:", error); - } - } - }); -} - -export function createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider = null, reqLogger = null, toolNameMap = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null) { - return createSSEStream({ - mode: STREAM_MODE.TRANSLATE, - targetFormat, - sourceFormat, - provider, - reqLogger, - toolNameMap, - model, - connectionId, - body, - onStreamComplete, - apiKey - }); -} - -export function createPassthroughStreamWithLogger(provider = null, reqLogger = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null) { - return createSSEStream({ - mode: STREAM_MODE.PASSTHROUGH, - provider, - reqLogger, - model, - connectionId, - body, - onStreamComplete, - apiKey - }); -} diff --git a/9router/open-sse/utils/streamHandler.js b/9router/open-sse/utils/streamHandler.js deleted file mode 100644 index 10ac6535..00000000 --- a/9router/open-sse/utils/streamHandler.js +++ /dev/null @@ -1,138 +0,0 @@ -// Stream handler with disconnect detection - shared for all providers - -// Get HH:MM:SS timestamp -function getTimeString() { - return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); -} - -/** - * Create stream controller with abort and disconnect detection - * @param {object} options - * @param {function} options.onDisconnect - Callback when client disconnects - * @param {object} options.log - Logger instance - * @param {string} options.provider - Provider name - * @param {string} options.model - Model name - */ -export function createStreamController({ onDisconnect, onError, log, provider, model } = {}) { - const abortController = new AbortController(); - const startTime = Date.now(); - let disconnected = false; - let abortTimeout = null; - - const logStream = (status) => { - const duration = Date.now() - startTime; - const p = provider?.toUpperCase() || "UNKNOWN"; - console.log(`[${getTimeString()}] 🌊 [STREAM] ${p} | ${model || "unknown"} | ${duration}ms | ${status}`); - }; - - return { - signal: abortController.signal, - startTime, - - isConnected: () => !disconnected, - - // Call when client disconnects - handleDisconnect: (reason = "client_closed") => { - if (disconnected) return; - disconnected = true; - - logStream(`disconnect: ${reason}`); - - // Delay abort to allow cleanup - abortTimeout = setTimeout(() => { - abortController.abort(); - }, 500); - - onDisconnect?.({ reason, duration: Date.now() - startTime }); - }, - - // Call when stream completes normally - handleComplete: () => { - if (disconnected) return; - disconnected = true; - - logStream("complete"); - - if (abortTimeout) { - clearTimeout(abortTimeout); - abortTimeout = null; - } - }, - - // Call on error - handleError: (error) => { - if (disconnected) return; - disconnected = true; - - if (abortTimeout) { - clearTimeout(abortTimeout); - abortTimeout = null; - } - - if (error.name === "AbortError") { - logStream("aborted"); - return; - } - - logStream(`error: ${error.message}`); - onError?.(error); - }, - - abort: () => abortController.abort() - }; -} - -/** - * Create transform stream with disconnect detection - * Wraps existing transform stream and adds abort capability - */ -export function createDisconnectAwareStream(transformStream, streamController) { - const reader = transformStream.readable.getReader(); - const writer = transformStream.writable.getWriter(); - - return new ReadableStream({ - async pull(controller) { - if (!streamController.isConnected()) { - controller.close(); - return; - } - - try { - const { done, value } = await reader.read(); - if (done) { - streamController.handleComplete(); - controller.close(); - return; - } - controller.enqueue(value); - } catch (error) { - streamController.handleError(error); - // Cleanup reader/writer to avoid orphaned streams - reader.cancel().catch(() => {}); - writer.abort().catch(() => {}); - controller.error(error); - } - }, - - cancel(reason) { - streamController.handleDisconnect(reason || "cancelled"); - reader.cancel(); - writer.abort(); - } - }); -} - -/** - * Pipe provider response through transform with disconnect detection - * @param {Response} providerResponse - Response from provider - * @param {TransformStream} transformStream - Transform stream for SSE - * @param {object} streamController - Stream controller from createStreamController - */ -export function pipeWithDisconnect(providerResponse, transformStream, streamController) { - const transformedBody = providerResponse.body.pipeThrough(transformStream); - return createDisconnectAwareStream( - { readable: transformedBody, writable: { getWriter: () => ({ abort: () => Promise.resolve() }) } }, - streamController - ); -} - diff --git a/9router/open-sse/utils/streamHelpers.js b/9router/open-sse/utils/streamHelpers.js deleted file mode 100644 index a7a19180..00000000 --- a/9router/open-sse/utils/streamHelpers.js +++ /dev/null @@ -1,122 +0,0 @@ -import { FORMATS } from "../translator/formats.js"; - -// Parse SSE data line -export function parseSSELine(line, format = null) { - if (!line) return null; - - // NDJSON format (Ollama): raw JSON lines without "data:" prefix - if (format === FORMATS.OLLAMA) { - const trimmed = line.trim(); - if (trimmed.startsWith("{")) { - try { - return JSON.parse(trimmed); - } catch (error) { - return null; - } - } - return null; - } - - // Standard SSE format: "data: {...}" - if (line.charCodeAt(0) !== 100) return null; // 'd' = 100 - - const data = line.slice(5).trim(); - if (data === "[DONE]") return { done: true }; - - try { - return JSON.parse(data); - } catch (error) { - if (data.length > 0 && data.length < 1000) { - console.log(`[WARN] Failed to parse SSE line (${data.length} chars): ${data.substring(0, 100)}...`); - } - return null; - } -} - -// Check if chunk has valuable content (not empty) -export function hasValuableContent(chunk, format) { - // OpenAI format - if (format === FORMATS.OPENAI && chunk.choices?.[0]?.delta) { - const delta = chunk.choices[0].delta; - return delta.content && delta.content !== "" || - delta.reasoning_content && delta.reasoning_content !== "" || - delta.tool_calls && delta.tool_calls.length > 0 || - chunk.choices[0].finish_reason || - delta.role; - } - - // Claude format - if (format === FORMATS.CLAUDE) { - const isContentBlockDelta = chunk.type === "content_block_delta"; - const hasText = chunk.delta?.text && chunk.delta.text !== ""; - const hasThinking = chunk.delta?.thinking && chunk.delta.thinking !== ""; - const hasInputJson = chunk.delta?.partial_json && chunk.delta.partial_json !== ""; - - if (isContentBlockDelta && !hasText && !hasThinking && !hasInputJson) { - return false; - } - return true; - } - - return true; // Other formats: keep all chunks -} - -// Fix invalid id (generic or too short) -export function fixInvalidId(parsed) { - if (parsed.id && (parsed.id === "chat" || parsed.id === "completion" || parsed.id.length < 8)) { - const fallbackId = parsed.extend_fields?.requestId || - parsed.extend_fields?.traceId || - Date.now().toString(36); - parsed.id = `chatcmpl-${fallbackId}`; - return true; - } - return false; -} - -function cleanUsagePayload(payload) { - if (!payload || typeof payload !== "object" || Array.isArray(payload)) { - return payload; - } - - let cleaned = payload; - - if ("usage" in cleaned) { - if (cleaned.usage === null) { - const { usage, ...payloadWithoutUsage } = cleaned; - cleaned = payloadWithoutUsage; - } else if (typeof cleaned.usage === "object" && cleaned.usage.perf_metrics === null) { - const { perf_metrics, ...usageWithoutPerf } = cleaned.usage; - cleaned = { ...cleaned, usage: usageWithoutPerf }; - } - } - - if (cleaned.response && typeof cleaned.response === "object" && !Array.isArray(cleaned.response)) { - const cleanedResponse = cleanUsagePayload(cleaned.response); - if (cleanedResponse !== cleaned.response) { - cleaned = { ...cleaned, response: cleanedResponse }; - } - } - - return cleaned; -} - -// Format output as SSE -export function formatSSE(data, sourceFormat) { - if (data === null || data === undefined) return "data: null\n\n"; - if (data && data.done) return "data: [DONE]\n\n"; - - // OpenAI Responses API format - if (data && data.event && data.data) { - const cleanedEventData = cleanUsagePayload(data.data); - return `event: ${data.event}\ndata: ${JSON.stringify(cleanedEventData)}\n\n`; - } - - data = cleanUsagePayload(data); - - // Claude format - if (sourceFormat === FORMATS.CLAUDE && data && data.type) { - return `event: ${data.type}\ndata: ${JSON.stringify(data)}\n\n`; - } - - return `data: ${JSON.stringify(data)}\n\n`; -} diff --git a/9router/open-sse/utils/usageTracking.js b/9router/open-sse/utils/usageTracking.js deleted file mode 100644 index 810f6235..00000000 --- a/9router/open-sse/utils/usageTracking.js +++ /dev/null @@ -1,325 +0,0 @@ -/** - * Token Usage Tracking - Extract, normalize, estimate and log token usage - */ - -import { saveRequestUsage, appendRequestLog } from "@/lib/usageDb.js"; -import { FORMATS } from "../translator/formats.js"; - -// ANSI color codes -export const COLORS = { - reset: "\x1b[0m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - cyan: "\x1b[36m" -}; - -// Buffer tokens to prevent context errors -const BUFFER_TOKENS = 2000; - -// Get HH:MM:SS timestamp -function getTimeString() { - return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); -} - -/** - * Add buffer tokens to usage to prevent context errors - * @param {object} usage - Usage object (any format) - * @returns {object} Usage with buffer added - */ -export function addBufferToUsage(usage) { - if (!usage || typeof usage !== "object") return usage; - - const result = { ...usage }; - - // Claude format - if (result.input_tokens !== undefined) { - result.input_tokens += BUFFER_TOKENS; - } - - // OpenAI format - if (result.prompt_tokens !== undefined) { - result.prompt_tokens += BUFFER_TOKENS; - } - - // Calculate or update total_tokens - if (result.total_tokens !== undefined) { - result.total_tokens += BUFFER_TOKENS; - } else if (result.prompt_tokens !== undefined && result.completion_tokens !== undefined) { - // Calculate total_tokens if not exists - result.total_tokens = result.prompt_tokens + result.completion_tokens; - } - - return result; -} - -export function filterUsageForFormat(usage, targetFormat) { - if (!usage || typeof usage !== "object") return usage; - - // Helper to pick only defined fields from usage - const pickFields = (fields) => { - const filtered = {}; - for (const field of fields) { - if (usage[field] !== undefined) { - filtered[field] = usage[field]; - } - } - return filtered; - }; - - // Define allowed fields for each format - const formatFields = { - [FORMATS.CLAUDE]: [ - 'input_tokens', 'output_tokens', - 'cache_read_input_tokens', 'cache_creation_input_tokens', - 'estimated' - ], - [FORMATS.GEMINI]: [ - 'promptTokenCount', 'candidatesTokenCount', 'totalTokenCount', - 'cachedContentTokenCount', 'thoughtsTokenCount', - 'estimated' - ], - [FORMATS.OPENAI_RESPONSES]: [ - 'input_tokens', 'output_tokens', - 'input_tokens_details', 'output_tokens_details', - 'estimated' - ], - // OpenAI format (default for OPENAI, CODEX, KIRO, etc.) - default: [ - 'prompt_tokens', 'completion_tokens', 'total_tokens', - 'cached_tokens', 'reasoning_tokens', - 'prompt_tokens_details', 'completion_tokens_details', - 'estimated' - ] - }; - - // Get fields for target format - let fields = formatFields[targetFormat]; - - // Use same fields for similar formats - if (targetFormat === FORMATS.GEMINI_CLI || targetFormat === FORMATS.ANTIGRAVITY) { - fields = formatFields[FORMATS.GEMINI]; - } else if (targetFormat === FORMATS.OPENAI_RESPONSE) { - fields = formatFields[FORMATS.OPENAI_RESPONSES]; - } else if (!fields) { - fields = formatFields.default; - } - - return pickFields(fields); -} - -/** - * Normalize usage object - ensure all values are valid numbers - */ -export function normalizeUsage(usage) { - if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null; - - const normalized = {}; - const assignNumber = (key, value) => { - if (value === undefined || value === null) return; - const numeric = Number(value); - if (Number.isFinite(numeric)) normalized[key] = numeric; - }; - - assignNumber("prompt_tokens", usage?.prompt_tokens); - assignNumber("completion_tokens", usage?.completion_tokens); - assignNumber("total_tokens", usage?.total_tokens); - assignNumber("cache_read_input_tokens", usage?.cache_read_input_tokens); - assignNumber("cache_creation_input_tokens", usage?.cache_creation_input_tokens); - assignNumber("cached_tokens", usage?.cached_tokens); - assignNumber("reasoning_tokens", usage?.reasoning_tokens); - - if (Object.keys(normalized).length === 0) return null; - return normalized; -} - -/** - * Check if usage has valid token data - * Valid = has at least one token field with value > 0 - * Invalid = empty object {}, null, undefined, no token fields, or all zeros - */ -export function hasValidUsage(usage) { - if (!usage || typeof usage !== "object") return false; - - // Check for any known token field with value > 0 - const tokenFields = [ - "prompt_tokens", "completion_tokens", "total_tokens", // OpenAI - "input_tokens", "output_tokens", // Claude - "promptTokenCount", "candidatesTokenCount" // Gemini - ]; - - for (const field of tokenFields) { - if (typeof usage[field] === "number" && usage[field] > 0) { - return true; - } - } - - return false; -} - -/** - * Extract usage from any format (Claude, OpenAI, Gemini, Responses API) - */ -export function extractUsage(chunk) { - if (!chunk || typeof chunk !== "object") return null; - - // Claude format (message_delta event) - if (chunk.type === "message_delta" && chunk.usage && typeof chunk.usage === "object") { - return normalizeUsage({ - prompt_tokens: chunk.usage.input_tokens || 0, - completion_tokens: chunk.usage.output_tokens || 0, - cache_read_input_tokens: chunk.usage.cache_read_input_tokens, - cache_creation_input_tokens: chunk.usage.cache_creation_input_tokens - }); - } - - // OpenAI Responses API format (response.completed or response.done) - if ((chunk.type === "response.completed" || chunk.type === "response.done") && chunk.response?.usage && typeof chunk.response.usage === "object") { - const usage = chunk.response.usage; - return normalizeUsage({ - prompt_tokens: usage.input_tokens || usage.prompt_tokens || 0, - completion_tokens: usage.output_tokens || usage.completion_tokens || 0, - cached_tokens: usage.input_tokens_details?.cached_tokens, - reasoning_tokens: usage.output_tokens_details?.reasoning_tokens - }); - } - - // OpenAI format - if (chunk.usage && typeof chunk.usage === "object" && chunk.usage.prompt_tokens !== undefined) { - return normalizeUsage({ - prompt_tokens: chunk.usage.prompt_tokens, - completion_tokens: chunk.usage.completion_tokens || 0, - cached_tokens: chunk.usage.prompt_tokens_details?.cached_tokens, - reasoning_tokens: chunk.usage.completion_tokens_details?.reasoning_tokens - }); - } - - // Gemini format (Antigravity) - // Antigravity wraps usageMetadata inside response: { response: { usageMetadata: {...} } } - const usageMeta = chunk.usageMetadata || chunk.response?.usageMetadata; - if (usageMeta && typeof usageMeta === "object") { - return normalizeUsage({ - prompt_tokens: usageMeta.promptTokenCount || 0, - completion_tokens: usageMeta.candidatesTokenCount || 0, - total_tokens: usageMeta.totalTokenCount, - cached_tokens: usageMeta.cachedContentTokenCount, - reasoning_tokens: usageMeta.thoughtsTokenCount - }); - } - - return null; -} - -/** - * Estimate input tokens from request body - * Calculate total body size for more accurate estimation - */ -export function estimateInputTokens(body) { - if (!body || typeof body !== "object") return 0; - - try { - // Calculate total body size (includes messages, tools, system, thinking config, etc.) - const bodyStr = JSON.stringify(body); - const totalChars = bodyStr.length; - - // Estimate: ~4 chars per token (rough average across all tokenizers) - return Math.ceil(totalChars / 4); - } catch (err) { - // Fallback if stringify fails - return 0; - } -} - -/** - * Estimate output tokens from content length - */ -export function estimateOutputTokens(contentLength) { - if (!contentLength || contentLength <= 0) return 0; - return Math.max(1, Math.floor(contentLength / 4)); -} - -/** - * Format usage object based on target format - * @param {number} inputTokens - Input/prompt tokens - * @param {number} outputTokens - Output/completion tokens - * @param {string} targetFormat - Target format from FORMATS - */ -export function formatUsage(inputTokens, outputTokens, targetFormat) { - // Claude format uses input_tokens/output_tokens - if (targetFormat === FORMATS.CLAUDE) { - return addBufferToUsage({ - input_tokens: inputTokens, - output_tokens: outputTokens, - estimated: true - }); - } - - // Default: OpenAI format (works for openai, gemini, responses, etc.) - return addBufferToUsage({ - prompt_tokens: inputTokens, - completion_tokens: outputTokens, - total_tokens: inputTokens + outputTokens, - estimated: true - }); -} - -/** - * Estimate full usage when provider doesn't return it - * @param {object} body - Request body for input token estimation - * @param {number} contentLength - Content length for output token estimation - * @param {string} targetFormat - Target format from FORMATS constant - */ -export function estimateUsage(body, contentLength, targetFormat = FORMATS.OPENAI) { - return formatUsage( - estimateInputTokens(body), - estimateOutputTokens(contentLength), - targetFormat - ); -} - -/** - * Log usage with cache info (green color) - */ -export function logUsage(provider, usage, model = null, connectionId = null, apiKey = null) { - if (!usage || typeof usage !== "object") return; - - const p = provider?.toUpperCase() || "UNKNOWN"; - - // Support both formats: - // - OpenAI: prompt_tokens, completion_tokens - // - Claude: input_tokens, output_tokens - const inTokens = usage?.prompt_tokens || usage?.input_tokens || 0; - const outTokens = usage?.completion_tokens || usage?.output_tokens || 0; - const accountPrefix = connectionId ? connectionId.slice(0, 8) + "..." : "unknown"; - - let msg = `[${getTimeString()}] 📊 ${COLORS.green}[USAGE] ${p} | in=${inTokens} | out=${outTokens} | account=${accountPrefix}${COLORS.reset}`; - - // Add estimated flag if present - if (usage.estimated) { - msg += ` ${COLORS.yellow}(estimated)${COLORS.reset}`; - } - - // Add cache info if present (unified from different formats) - const cacheRead = usage.cache_read_input_tokens || usage.cached_tokens; - if (cacheRead) msg += ` | cache_read=${cacheRead}`; - - const cacheCreation = usage.cache_creation_input_tokens; - if (cacheCreation) msg += ` | cache_create=${cacheCreation}`; - - const reasoning = usage.reasoning_tokens; - if (reasoning) msg += ` | reasoning=${reasoning}`; - - console.log(msg); - - // Save to usage DB - const tokens = { - prompt_tokens: inTokens, - completion_tokens: outTokens, - cache_read_input_tokens: cacheRead || 0, - cache_creation_input_tokens: cacheCreation || 0, - reasoning_tokens: reasoning || 0 - }; - saveRequestUsage({ model, provider, connectionId, tokens, apiKey: apiKey || undefined }).catch(() => { }); - appendRequestLog({ model, provider, connectionId, tokens, status: "200 OK" }).catch(() => { }); -} diff --git a/9router/package.json b/9router/package.json deleted file mode 100644 index ea7e2340..00000000 --- a/9router/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "9router-app", - "version": "0.3.60", - "description": "9Router web dashboard", - "private": true, - "scripts": { - "dev": "next dev --webpack --port 20128", - "build": "cross-env NODE_ENV=production next build --turbopack", - "start": "cross-env NODE_ENV=production next start", - "dev:bun": "bun --bun next dev --webpack --port 20128", - "build:bun": "cross-env NODE_ENV=production bun --bun next build --webpack", - "start:bun": "cross-env NODE_ENV=production bun ./.next/standalone/server.js" - }, - "dependencies": { - "@monaco-editor/react": "^4.7.0", - "@xyflow/react": "^12.10.1", - "bcryptjs": "^3.0.3", - "confbox": "^0.2.4", - "express": "^5.2.1", - "fs": "^0.0.1-security", - "http-proxy-middleware": "^3.0.5", - "jose": "^6.1.3", - "lowdb": "^7.0.1", - "monaco-editor": "^0.55.1", - "next": "^16.1.6", - "node-forge": "^1.3.3", - "node-machine-id": "^1.1.12", - "open": "^11.0.0", - "ora": "^9.1.0", - "react": "19.2.4", - "react-dom": "19.2.4", - "react-is": "^16.13.1", - "recharts": "^3.7.0", - "selfsigned": "^5.5.0", - "socks-proxy-agent": "^8.0.5", - "sql.js": "^1.14.1", - "undici": "^7.19.2", - "uuid": "^13.0.0", - "zustand": "^5.0.10" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.18", - "cross-env": "^7.0.3", - "eslint": "^9", - "eslint-config-next": "16.1.6", - "postcss": "^8.5.6", - "tailwindcss": "^4" - } -} diff --git a/9router/postcss.config.mjs b/9router/postcss.config.mjs deleted file mode 100644 index fa4a1da8..00000000 --- a/9router/postcss.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - diff --git a/9router/public/favicon.svg b/9router/public/favicon.svg deleted file mode 100644 index a72e45be..00000000 --- a/9router/public/favicon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - 9 - - - - - - - - diff --git a/9router/public/file.svg b/9router/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/9router/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/9router/public/globe.svg b/9router/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/9router/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/9router/public/i18n/literals/ar.json b/9router/public/i18n/literals/ar.json deleted file mode 100644 index 21cfab4c..00000000 --- a/9router/public/i18n/literals/ar.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "إلغاء", - "Delete": "حذف", - "Edit": "تحرير", - "Save": "حفظ", - "Close": "إغلاق", - "Add": "إضافة", - "Remove": "إزالة", - "Settings": "الإعدادات", - "Profile": "الملف الشخصي", - "Dashboard": "لوحة التحكم", - "Logout": "تسجيل الخروج", - "Login": "تسجيل الدخول", - "Providers": "الموفرون", - "Usage": "الاستخدام", - "API Key": "مفتاح API", - "Connected": "متصل", - "Disconnected": "غير متصل", - "Active": "نشط", - "Inactive": "غير نشط", - "Success": "نجح", - "Failed": "فشل", - "Error": "خطأ", - "Warning": "تحذير", - "Info": "معلومات", - "Loading": "جاري التحميل", - "Search": "بحث", - "Filter": "تصفية", - "Sort": "ترتيب", - "Export": "تصدير", - "Import": "استيراد", - "Refresh": "تحديث", - "Back": "رجوع", - "Next": "التالي", - "Previous": "السابق", - "Submit": "إرسال", - "Confirm": "تأكيد", - "Yes": "نعم", - "No": "لا", - "OK": "حسنا", - "Apply": "تطبيق", - "Reset": "إعادة تعيين", - "Clear": "مسح", - "Select": "تحديد", - "Upload": "تحميل", - "Download": "تنزيل", - "Copy": "نسخ", - "Paste": "لصق", - "Cut": "قص", - "Undo": "تراجع", - "Redo": "إعادة", - "Name": "الاسم", - "Description": "الوصف", - "Status": "الحالة", - "Type": "النوع", - "Date": "التاريخ", - "Time": "الوقت", - "Created": "تم إنشاء", - "Updated": "تم التحديث", - "Actions": "الإجراءات", - "Details": "التفاصيل", - "View": "عرض", - "New": "جديد", - "Total": "الإجمالي", - "Count": "العدد", - "Price": "السعر", - "Cost": "التكلفة", - "Free": "مجاني", - "Paid": "مدفوع", - "Enable": "تفعيل", - "Disable": "تعطيل", - "Enabled": "مفعل", - "Disabled": "معطل", - "Online": "متصل", - "Offline": "غير متصل", - "Available": "متاح", - "Unavailable": "غير متاح", - "Required": "مطلوب", - "Optional": "اختياري", - "Default": "افتراضي", - "Custom": "مخصص", - "Advanced": "متقدم", - "Basic": "أساسي", - "Help": "مساعدة", - "Support": "دعم", - "Documentation": "التوثيق", - "Version": "الإصدار", - "Language": "اللغة", - "Theme": "المظهر", - "Light": "فاتح", - "Dark": "داكن", - "Auto": "تلقائي", - "Endpoint": "نقطة نهاية", - "Providers": "الموفرون", - "Combos": "تراكيب", - "Usage": "الإحصائيات", - "Quota Tracker": "متتبع الحصة", - "MITM": "MITM", - "CLI Tools": "أدوات CLI", - "Console Log": "سجل وحدة التحكم", - "System": "النظام", - "Debug": "تصحيح", - "Shutdown": "إيقاف", - "Close Proxy": "إغلاق الوكيل", - "Are you sure you want to close the proxy server?": "هل أنت متأكد من أنك تريد إغلاق خادم الوكيل؟", - "Server Disconnected": "خادم غير متصل", - "The proxy server has been stopped.": "تم إيقاف خادم الوكيل.", - "Reload Page": "إعادة تحميل الصفحة", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "الخدمة تعمل في المحطة الطرفية. يمكنك إغلاق صفحة الويب هذه. سيؤدي الإيقاف إلى إيقاف الخدمة.", - "Manage your AI provider connections": "إدارة اتصالات موفر الذكاء الاصطناعي الخاصة بك", - "Model combos with fallback": "تراكيب النموذج مع الخيار البديل", - "Monitor your API usage, token consumption, and request logs": "راقب استخدام API وتناول الرموز وسجلات الطلب", - "Intercept CLI tool traffic and route through 9Router": "اعترض حركة مرور أداة CLI وحجم من خلال 9Router", - "Configure CLI tools": "تكوين أدوات CLI", - "API endpoint configuration": "تكوين نقطة نهاية API", - "Manage your preferences": "إدارة تفضيلاتك", - "Debug translation flow between formats": "تصحيح تدفق الترجمة بين الصيغ", - "Live server console output": "مخرجات وحدة التحكم على الخادم المباشر", - "Create model combos with fallback support": "إنشاء تراكيب نموذج مع دعم الخيار البديل", - "Local Mode": "الوضع المحلي", - "Running on your machine": "يعمل على جهازك", - "Database Location": "موقع قاعدة البيانات", - "Download Backup": "تنزيل النسخة الاحتياطية", - "Import Backup": "استيراد النسخة الاحتياطية", - "Database backup downloaded": "تم تنزيل النسخة الاحتياطية لقاعدة البيانات", - "Database imported successfully": "تم استيراد قاعدة البيانات بنجاح", - "Security": "الأمان", - "Require login": "يتطلب تسجيل الدخول", - "When ON, dashboard requires password. When OFF, access without login.": "عند التشغيل، يتطلب لوحة التحكم كلمة مرور. عند الإيقاف، الوصول بدون تسجيل دخول.", - "Current Password": "كلمة المرور الحالية", - "Enter current password": "أدخل كلمة المرور الحالية", - "New Password": "كلمة مرور جديدة", - "Enter new password": "أدخل كلمة مرور جديدة", - "Confirm New Password": "تأكيد كلمة المرور الجديدة", - "Confirm new password": "تأكيد كلمة المرور الجديدة", - "Update Password": "تحديث كلمة المرور", - "Set Password": "تعيين كلمة المرور", - "Password updated successfully": "تم تحديث كلمة المرور بنجاح", - "Passwords do not match": "كلمات المرور لا تتطابق", - "Routing Strategy": "استراتيجية التوجيه", - "Round Robin": "جولة روبن", - "Cycle through accounts to distribute load": "الدوران عبر الحسابات لتوزيع الحمل", - "Sticky Limit": "حد لزج", - "Calls per account before switching": "المكالمات لكل حساب قبل التبديل", - "Network": "الشبكة", - "Outbound Proxy": "وكيل الخروج", - "Enable proxy for OAuth + provider outbound requests.": "تفعيل الوكيل لطلبات OAuth + الخروج من الموفر.", - "Proxy URL": "عنوان URL الوكيل", - "Leave empty to inherit existing env proxy (if any).": "اترك فارغًا لوراثة وكيل env الموجود (إن وجد).", - "No Proxy": "لا يوجد وكيل", - "Comma-separated hostnames/domains to bypass the proxy.": "أسماء المضيفين/النطاقات المفصولة بفواصل لتجاوز الوكيل.", - "Test proxy URL": "اختبر عنوان URL الوكيل", - "Apply": "تطبيق", - "Proxy settings applied": "تم تطبيق إعدادات الوكيل", - "Proxy enabled": "الوكيل مفعل", - "Proxy disabled": "الوكيل معطل", - "Proxy test OK": "اختبار الوكيل حسنا", - "Proxy test failed": "فشل اختبار الوكيل", - "Please enter a Proxy URL to test": "يرجى إدخال عنوان URL الوكيل للاختبار", - "Observability": "القابلية للمراقبة", - "Enable Observability": "تفعيل القابلية للمراقبة", - "Turn request detail recording on/off globally": "تشغيل/إيقاف تسجيل تفاصيل الطلب بشكل عام", - "Max Records": "أقصى عدد من السجلات", - "Maximum request detail records to keep (older records are auto-deleted)": "الحد الأقصى من سجلات تفاصيل الطلب للاحتفاظ بها (يتم حذف السجلات الأقدم تلقائيًا)", - "Batch Size": "حجم الدفعة", - "Number of items to accumulate before writing to database (higher = better performance)": "عدد العناصر المراد تجميعها قبل الكتابة إلى قاعدة البيانات (أعلى = أداء أفضل)", - "Flush Interval (ms)": "فترة المسح (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "الحد الأقصى للانتظار قبل مسح المخزن المؤقت (يمنع فقدان البيانات أثناء الحركة المنخفضة)", - "Max JSON Size (KB)": "أقصى حجم JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "الحد الأقصى لحجم كل حقل JSON (الطلب/الرد) قبل القطع", - "All data stored on your machine": "جميع البيانات المخزنة على جهازك", - "MITM Server": "خادم MITM", - "Running": "يجري", - "Stopped": "متوقف", - "Cert": "شهادة", - "Server": "الخادم", - "Purpose:": "الغرض:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "استخدم Antigravity IDE و GitHub Copilot → مع أي موفر/نموذج من 9Router", - "How it works:": "كيف يعمل:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "طلب Antigravity/Copilot IDE → إعادة توجيه DNS إلى localhost:443 → يعترض وكيل MITM → 9Router → الرد إلى Antigravity/Copilot", - "API Key": "مفتاح API", - "No API keys — create one in Keys page": "لا توجد مفاتيح API — قم بإنشاء واحدة في صفحة المفاتيح", - "sk_9router (default)": "sk_9router (افتراضي)", - "Server started": "تم بدء الخادم", - "Failed to start server": "فشل في بدء الخادم", - "Server stopped — all DNS cleared": "تم إيقاف الخادم — تم مسح جميع DNS", - "Failed to stop server": "فشل في إيقاف الخادم", - "Sudo password is required": "كلمة مرور sudo مطلوبة", - "Stop Server": "إيقاف الخادم", - "Start Server": "بدء الخادم", - "Enable DNS per tool below to activate interception": "قم بتفعيل DNS لكل أداة أدناه لتفعيل الاعتراض", - "Sudo Password Required": "كلمة مرور Sudo مطلوبة", - "Enter your sudo password to start/stop MITM server": "أدخل كلمة مرور sudo للبدء/الإيقاف من خادم MITM", - "Sudo Password": "كلمة مرور Sudo", - "Confirm": "تأكيد" -} diff --git a/9router/public/i18n/literals/bn.json b/9router/public/i18n/literals/bn.json deleted file mode 100644 index a18aaa3e..00000000 --- a/9router/public/i18n/literals/bn.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "বাতিল করুন", - "Delete": "মুছুন", - "Edit": "সম্পাদনা করুন", - "Save": "সংরক্ষণ করুন", - "Close": "বন্ধ করুন", - "Add": "যোগ করুন", - "Remove": "সরান", - "Settings": "সেটিংস", - "Profile": "প্রোফাইল", - "Dashboard": "ড্যাশবোর্ড", - "Logout": "লগ আউট", - "Login": "লগ ইন", - "Providers": "সরবরাহকারী", - "Usage": "ব্যবহার", - "API Key": "API কী", - "Connected": "সংযুক্ত", - "Disconnected": "বিচ্ছিন্ন", - "Active": "সক্রিয়", - "Inactive": "নিষ্ক্রিয়", - "Success": "সফল", - "Failed": "ব্যর্থ", - "Error": "ত্রুটি", - "Warning": "সতর্কতা", - "Info": "তথ্য", - "Loading": "লোড হচ্ছে", - "Search": "অনুসন্ধান করুন", - "Filter": "ফিল্টার", - "Sort": "সাজান", - "Export": "রপ্তানি করুন", - "Import": "আমদানি করুন", - "Refresh": "রিফ্রেশ করুন", - "Back": "ফিরে যান", - "Next": "পরবর্তী", - "Previous": "পূর্ববর্তী", - "Submit": "জমা দিন", - "Confirm": "নিশ্চিত করুন", - "Yes": "হ্যাঁ", - "No": "না", - "OK": "ঠিক আছে", - "Apply": "প্রয়োগ করুন", - "Reset": "পুনরায় সেট করুন", - "Clear": "সাফ করুন", - "Select": "নির্বাচন করুন", - "Upload": "আপলোড করুন", - "Download": "ডাউনলোড করুন", - "Copy": "অনুলিপি করুন", - "Paste": "পেস্ট করুন", - "Cut": "কাটুন", - "Undo": "পূর্বাবস্থায় ফিরিয়ে আনুন", - "Redo": "পুনরায় করুন", - "Name": "নাম", - "Description": "বর্ণনা", - "Status": "অবস্থা", - "Type": "ধরন", - "Date": "তারিখ", - "Time": "সময়", - "Created": "তৈরি করা হয়েছে", - "Updated": "আপডেট করা হয়েছে", - "Actions": "পদক্ষেপ", - "Details": "বিশদ", - "View": "দেখুন", - "New": "নতুন", - "Total": "মোট", - "Count": "গণনা", - "Price": "দাম", - "Cost": "খরচ", - "Free": "বিনামূল্যে", - "Paid": "পেইড", - "Enable": "সক্ষম করুন", - "Disable": "অক্ষম করুন", - "Enabled": "সক্ষম", - "Disabled": "অক্ষম", - "Online": "অনলাইন", - "Offline": "অফলাইন", - "Available": "উপলব্ধ", - "Unavailable": "অনুপলব্ধ", - "Required": "প্রয়োজনীয়", - "Optional": "ঐচ্ছিক", - "Default": "ডিফল্ট", - "Custom": "কাস্টম", - "Advanced": "উন্নত", - "Basic": "মৌলিক", - "Help": "সহায়তা", - "Support": "সহায়তা", - "Documentation": "ডকুমেন্টেশন", - "Version": "সংস্করণ", - "Language": "ভাষা", - "Theme": "থিম", - "Light": "হালকা", - "Dark": "গাঢ়", - "Auto": "স্বয়ংক্রিয়", - "Endpoint": "এন্ডপয়েন্ট", - "Providers": "সরবরাহকারী", - "Combos": "কম্বো", - "Usage": "ব্যবহারের পরিসংখ্যান", - "Quota Tracker": "কোটা ট্র্যাকার", - "MITM": "MITM", - "CLI Tools": "সরঞ্জাম", - "Console Log": "কনসোল লগ", - "System": "সিস্টেম", - "Debug": "ডিবাগ", - "Shutdown": "বন্ধ করুন", - "Close Proxy": "প্রক্সি বন্ধ করুন", - "Are you sure you want to close the proxy server?": "আপনি কি নিশ্চিত যে আপনি প্রক্সি সার্ভার বন্ধ করতে চান?", - "Server Disconnected": "সার্ভার সংযোগ বিচ্ছিন্ন", - "The proxy server has been stopped.": "প্রক্সি সার্ভার বন্ধ করা হয়েছে।", - "Reload Page": "পৃষ্ঠা পুনরায় লোড করুন", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "সেবা টার্মিনালে চলছে। আপনি এই ওয়েব পৃষ্ঠাটি বন্ধ করতে পারেন। শাটডাউন সেবা বন্ধ করবে।", - "Manage your AI provider connections": "আপনার AI সরবরাহকারী সংযোগ পরিচালনা করুন", - "Model combos with fallback": "ফলব্যাক সহ মডেল কম্বো", - "Monitor your API usage, token consumption, and request logs": "আপনার API ব্যবহার, টোকেন খরচ এবং অনুরোধ লগ পর্যবেক্ষণ করুন", - "Intercept CLI tool traffic and route through 9Router": "CLI টুল ট্রাফিক ইন্টারসেপ্ট করুন এবং 9Router এর মাধ্যমে রুট করুন", - "Configure CLI tools": "CLI সরঞ্জাম কনফিগার করুন", - "API endpoint configuration": "API এন্ডপয়েন্ট কনফিগারেশন", - "Manage your preferences": "আপনার পছন্দগুলি পরিচালনা করুন", - "Debug translation flow between formats": "ফর্ম্যাটগুলির মধ্যে অনুবাদ প্রবাহ ডিবাগ করুন", - "Live server console output": "লাইভ সার্ভার কনসোল আউটপুট", - "Create model combos with fallback support": "ফলব্যাক সমর্থন সহ মডেল কম্বো তৈরি করুন", - "Local Mode": "স্থানীয় মোড", - "Running on your machine": "আপনার মেশিনে চলছে", - "Database Location": "ডাটাবেস অবস্থান", - "Download Backup": "ব্যাকআপ ডাউনলোড করুন", - "Import Backup": "ব্যাকআপ আমদানি করুন", - "Database backup downloaded": "ডাটাবেস ব্যাকআপ ডাউনলোড করা হয়েছে", - "Database imported successfully": "ডাটাবেস সফলভাবে আমদানি করা হয়েছে", - "Security": "নিরাপত্তা", - "Require login": "লগইন প্রয়োজন", - "When ON, dashboard requires password. When OFF, access without login.": "চালু থাকলে, ড্যাশবোর্ড পাসওয়ার্ড প্রয়োজন। বন্ধ থাকলে, লগইন ছাড়াই অ্যাক্সেস করুন।", - "Current Password": "বর্তমান পাসওয়ার্ড", - "Enter current password": "বর্তমান পাসওয়ার্ড প্রবেশ করুন", - "New Password": "নতুন পাসওয়ার্ড", - "Enter new password": "নতুন পাসওয়ার্ড প্রবেশ করুন", - "Confirm New Password": "নতুন পাসওয়ার্ড নিশ্চিত করুন", - "Confirm new password": "নতুন পাসওয়ার্ড নিশ্চিত করুন", - "Update Password": "পাসওয়ার্ড আপডেট করুন", - "Set Password": "পাসওয়ার্ড সেট করুন", - "Password updated successfully": "পাসওয়ার্ড সফলভাবে আপডেট করা হয়েছে", - "Passwords do not match": "পাসওয়ার্ড মেলে না", - "Routing Strategy": "রাউটিং কৌশল", - "Round Robin": "রাউন্ড রবিন", - "Cycle through accounts to distribute load": "লোড বিতরণের জন্য অ্যাকাউন্টগুলির মধ্য দিয়ে চক্র", - "Sticky Limit": "স্টিকি সীমা", - "Calls per account before switching": "স্যুইচিংয়ের আগে অ্যাকাউন্ট প্রতি কল", - "Network": "নেটওয়ার্ক", - "Outbound Proxy": "আউটবাউন্ড প্রক্সি", - "Enable proxy for OAuth + provider outbound requests.": "OAuth + সরবরাহকারী আউটবাউন্ড অনুরোধের জন্য প্রক্সি সক্ষম করুন।", - "Proxy URL": "প্রক্সি URL", - "Leave empty to inherit existing env proxy (if any).": "বিদ্যমান env প্রক্সি উত্তরাধিকার করতে খালি রেখে দিন (থাকলে)।", - "No Proxy": "কোন প্রক্সি নেই", - "Comma-separated hostnames/domains to bypass the proxy.": "প্রক্সি বাইপাস করার জন্য কমা-পৃথক হোস্টনাম/ডোমেইন।", - "Test proxy URL": "প্রক্সি URL পরীক্ষা করুন", - "Apply": "প্রয়োগ করুন", - "Proxy settings applied": "প্রক্সি সেটিংস প্রয়োগ করা হয়েছে", - "Proxy enabled": "প্রক্সি সক্ষম", - "Proxy disabled": "প্রক্সি অক্ষম", - "Proxy test OK": "প্রক্সি পরীক্ষা ঠিক আছে", - "Proxy test failed": "প্রক্সি পরীক্ষা ব্যর্থ", - "Please enter a Proxy URL to test": "পরীক্ষার জন্য দয়া করে একটি প্রক্সি URL প্রবেশ করুন", - "Observability": "পর্যবেক্ষণযোগ্যতা", - "Enable Observability": "পর্যবেক্ষণযোগ্যতা সক্ষম করুন", - "Turn request detail recording on/off globally": "অনুরোধ বিস্তারিত রেকর্ডিং বিশ্বব্যাপী চালু/বন্ধ করুন", - "Max Records": "সর্বাধিক রেকর্ড", - "Maximum request detail records to keep (older records are auto-deleted)": "রাখার জন্য সর্বাধিক অনুরোধ বিস্তারিত রেকর্ড (পুরানো রেকর্ড স্বয়ংক্রিয়ভাবে মুছে যায়)", - "Batch Size": "ব্যাচ আকার", - "Number of items to accumulate before writing to database (higher = better performance)": "ডাটাবেসে লেখার আগে জমা করার জন্য আইটেমের সংখ্যা (উচ্চতর = ভাল কর্মক্ষমতা)", - "Flush Interval (ms)": "ফ্লাশ ইন্টারভাল (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "বাফার ফ্লাশ করার আগে অপেক্ষা করার সর্বাধিক সময় (কম ট্রাফিক সময় ডেটা হারানো প্রতিরোধ করে)", - "Max JSON Size (KB)": "সর্বাধিক JSON আকার (KB)", - "Maximum size for each JSON field (request/response) before truncation": "ট্রাঙ্কেশনের আগে প্রতিটি JSON ফিল্ডের সর্বাধিক আকার (অনুরোধ/প্রতিক্রিয়া)", - "All data stored on your machine": "সমস্ত ডেটা আপনার মেশিনে সংরক্ষিত", - "MITM Server": "MITM সার্ভার", - "Running": "চলছে", - "Stopped": "বন্ধ", - "Cert": "সার্টিফিকেট", - "Server": "সার্ভার", - "Purpose:": "উদ্দেশ্য:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Antigravity IDE এবং GitHub Copilot ব্যবহার করুন → 9Router থেকে যেকোনো সরবরাহকারী/মডেলের সাথে", - "How it works:": "এটি কীভাবে কাজ করে:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE অনুরোধ → DNS কে localhost:443 তে রিডিরেক্ট করুন → MITM প্রক্সি ইন্টারসেপ্ট করে → 9Router → Antigravity/Copilot এ প্রতিক্রিয়া", - "API Key": "API কী", - "No API keys — create one in Keys page": "কোন API কী নেই — Keys পৃষ্ঠায় একটি তৈরি করুন", - "sk_9router (default)": "sk_9router (ডিফল্ট)", - "Server started": "সার্ভার শুরু হয়েছে", - "Failed to start server": "সার্ভার শুরু করতে ব্যর্থ", - "Server stopped — all DNS cleared": "সার্ভার বন্ধ — সমস্ত DNS সাফ করা হয়েছে", - "Failed to stop server": "সার্ভার বন্ধ করতে ব্যর্থ", - "Sudo password is required": "Sudo পাসওয়ার্ড প্রয়োজন", - "Stop Server": "সার্ভার বন্ধ করুন", - "Start Server": "সার্ভার শুরু করুন", - "Enable DNS per tool below to activate interception": "ইন্টারসেপশন সক্রিয় করতে নীচে প্রতিটি সরঞ্জামের জন্য DNS সক্ষম করুন", - "Sudo Password Required": "Sudo পাসওয়ার্ড প্রয়োজন", - "Enter your sudo password to start/stop MITM server": "MITM সার্ভার শুরু/বন্ধ করতে আপনার sudo পাসওয়ার্ড প্রবেশ করুন", - "Sudo Password": "Sudo পাসওয়ার্ড", - "Confirm": "নিশ্চিত করুন" -} diff --git a/9router/public/i18n/literals/cs.json b/9router/public/i18n/literals/cs.json deleted file mode 100644 index 5feab11a..00000000 --- a/9router/public/i18n/literals/cs.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Zrušit", - "Delete": "Smazat", - "Edit": "Upravit", - "Save": "Uložit", - "Close": "Zavřít", - "Add": "Přidat", - "Remove": "Odebrat", - "Settings": "Nastavení", - "Profile": "Profil", - "Dashboard": "Řídicí panel", - "Logout": "Odhlásit se", - "Login": "Přihlásit se", - "Providers": "Poskytovatelé", - "Usage": "Použití", - "API Key": "Klíč API", - "Connected": "Připojeno", - "Disconnected": "Odpojeno", - "Active": "Aktivní", - "Inactive": "Neaktivní", - "Success": "Úspěch", - "Failed": "Selhalo", - "Error": "Chyba", - "Warning": "Upozornění", - "Info": "Informace", - "Loading": "Načítání", - "Search": "Hledání", - "Filter": "Filtr", - "Sort": "Řazení", - "Export": "Exportovat", - "Import": "Importovat", - "Refresh": "Aktualizovat", - "Back": "Zpět", - "Next": "Další", - "Previous": "Předchozí", - "Submit": "Odeslat", - "Confirm": "Potvrdit", - "Yes": "Ano", - "No": "Ne", - "OK": "OK", - "Apply": "Aplikovat", - "Reset": "Obnovit", - "Clear": "Vymazat", - "Select": "Vybrat", - "Upload": "Nahrát", - "Download": "Stáhnout", - "Copy": "Kopírovat", - "Paste": "Vložit", - "Cut": "Vyjmout", - "Undo": "Vrátit zpět", - "Redo": "Znovu", - "Name": "Název", - "Description": "Popis", - "Status": "Stav", - "Type": "Typ", - "Date": "Datum", - "Time": "Čas", - "Created": "Vytvořeno", - "Updated": "Aktualizováno", - "Actions": "Akce", - "Details": "Podrobnosti", - "View": "Zobrazit", - "New": "Nový", - "Total": "Celkem", - "Count": "Počet", - "Price": "Cena", - "Cost": "Náklady", - "Free": "Zdarma", - "Paid": "Placené", - "Enable": "Povolit", - "Disable": "Zakázat", - "Enabled": "Povoleno", - "Disabled": "Zakázáno", - "Online": "Online", - "Offline": "Offline", - "Available": "K dispozici", - "Unavailable": "Není k dispozici", - "Required": "Povinné", - "Optional": "Volitelné", - "Default": "Výchozí", - "Custom": "Vlastní", - "Advanced": "Pokročilé", - "Basic": "Základní", - "Help": "Pomoc", - "Support": "Podpora", - "Documentation": "Dokumentace", - "Version": "Verze", - "Language": "Jazyk", - "Theme": "Motiv", - "Light": "Světlý", - "Dark": "Tmavý", - "Auto": "Automaticky", - "Endpoint": "Koncový bod", - "Providers": "Poskytovatelé", - "Combos": "Kombinace", - "Usage": "Statistika", - "Quota Tracker": "Sledování kvót", - "MITM": "MITM", - "CLI Tools": "Nástroje CLI", - "Console Log": "Protokol konzole", - "System": "Systém", - "Debug": "Ladění", - "Shutdown": "Vypnutí", - "Close Proxy": "Zavřít proxy", - "Are you sure you want to close the proxy server?": "Opravdu chcete zavřít proxy server?", - "Server Disconnected": "Server odpojen", - "The proxy server has been stopped.": "Proxy server byl zastaven.", - "Reload Page": "Obnovit stránku", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Služba běží v terminálu. Tuto webovou stránku si můžete zavřít. Vypnutí zastaví službu.", - "Manage your AI provider connections": "Spravujte svá připojení poskytovatele AI", - "Model combos with fallback": "Kombinace modelů s zálohou", - "Monitor your API usage, token consumption, and request logs": "Monitorujte použití API, spotřebu tokenů a protokoly žádostí", - "Intercept CLI tool traffic and route through 9Router": "Zachycujte provoz nástrojů CLI a směrujte jej přes 9Router", - "Configure CLI tools": "Konfigurace nástrojů CLI", - "API endpoint configuration": "Konfigurace koncového bodu API", - "Manage your preferences": "Spravujte své preferences", - "Debug translation flow between formats": "Ladění toku překladu mezi formáty", - "Live server console output": "Výstup konzole serveru v přímém čase", - "Create model combos with fallback support": "Vytvářejte kombinace modelů s podporou zálohy", - "Local Mode": "Místní režim", - "Running on your machine": "Běží na vašem počítači", - "Database Location": "Umístění databáze", - "Download Backup": "Stáhnout zálohu", - "Import Backup": "Importovat zálohu", - "Database backup downloaded": "Záloha databáze stažena", - "Database imported successfully": "Databáze byla úspěšně importována", - "Security": "Bezpečnost", - "Require login": "Vyžadovat přihlášení", - "When ON, dashboard requires password. When OFF, access without login.": "Když je ZAPNUTO, řídicí panel vyžaduje heslo. Když je VYPNUTO, přístup bez přihlášení.", - "Current Password": "Aktuální heslo", - "Enter current password": "Zadejte aktuální heslo", - "New Password": "Nové heslo", - "Enter new password": "Zadejte nové heslo", - "Confirm New Password": "Potvrzení nového hesla", - "Confirm new password": "Potvrďte nové heslo", - "Update Password": "Aktualizovat heslo", - "Set Password": "Nastavit heslo", - "Password updated successfully": "Heslo bylo úspěšně aktualizováno", - "Passwords do not match": "Hesla se neshodují", - "Routing Strategy": "Strategie směrování", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Procházejte účty pro distribuci zátěže", - "Sticky Limit": "Lepkavý limit", - "Calls per account before switching": "Volání na účet před přepnutím", - "Network": "Síť", - "Outbound Proxy": "Odchozí proxy", - "Enable proxy for OAuth + provider outbound requests.": "Povolte proxy pro OAuth + odchozí požadavky poskytovatele.", - "Proxy URL": "URL proxy", - "Leave empty to inherit existing env proxy (if any).": "Ponechte prázdné pro dědění existujícího env proxy (pokud existuje).", - "No Proxy": "Bez proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Čárkou oddělené názvy hostitelů/domény pro obejití proxy.", - "Test proxy URL": "Testovat URL proxy", - "Apply": "Aplikovat", - "Proxy settings applied": "Nastavení proxy aplikováno", - "Proxy enabled": "Proxy povolena", - "Proxy disabled": "Proxy zakázána", - "Proxy test OK": "Test proxy OK", - "Proxy test failed": "Test proxy se nezdařil", - "Please enter a Proxy URL to test": "Zadejte prosím URL proxy k testování", - "Observability": "Pozorovatelnost", - "Enable Observability": "Povolit pozorovatelnost", - "Turn request detail recording on/off globally": "Zapnutí/vypnutí záznamů podrobností požadavku globálně", - "Max Records": "Maximální počet záznamů", - "Maximum request detail records to keep (older records are auto-deleted)": "Maximální počet záznamů o podrobnostech požadavku k uchování (starší záznamy se automaticky odstraňují)", - "Batch Size": "Velikost dávky", - "Number of items to accumulate before writing to database (higher = better performance)": "Počet položek k hromadění před zápisem do databáze (vyšší = lepší výkon)", - "Flush Interval (ms)": "Interval vyprazdňování (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maximální čas čekání před vyprazdněním vyrovnávací paměti (zabraňuje ztrátě dat při nízkém provozu)", - "Max JSON Size (KB)": "Maximální velikost JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maximální velikost každého pole JSON (požadavek/odpověď) před zkrácením", - "All data stored on your machine": "Všechna data jsou uložena na vašem počítači", - "MITM Server": "Server MITM", - "Running": "Běžící", - "Stopped": "Zastaveno", - "Cert": "Certifikát", - "Server": "Server", - "Purpose:": "Účel:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Použijte Antigravity IDE a GitHub Copilot → s JAKÝMKOLIV poskytovatelem/modelem z 9Router", - "How it works:": "Jak to funguje:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Požadavek Antigravity/Copilot IDE → Přesměrování DNS na localhost:443 → Proxy MITM zachycuje → 9Router → odpověď na Antigravity/Copilot", - "API Key": "Klíč API", - "No API keys — create one in Keys page": "Žádné klíče API — vytvořte jeden na stránce Klíče", - "sk_9router (default)": "sk_9router (výchozí)", - "Server started": "Server spuštěn", - "Failed to start server": "Spuštění serveru se nezdařilo", - "Server stopped — all DNS cleared": "Server zastaven — veškerý DNS vymazán", - "Failed to stop server": "Zastavení serveru se nezdařilo", - "Sudo password is required": "Je vyžadováno heslo sudo", - "Stop Server": "Zastavit server", - "Start Server": "Spustit server", - "Enable DNS per tool below to activate interception": "Povolte DNS pro každý nástroj níže a aktivujte zachycování", - "Sudo Password Required": "Je vyžadováno heslo Sudo", - "Enter your sudo password to start/stop MITM server": "Zadejte heslo sudo pro spuštění/zastavení serveru MITM", - "Sudo Password": "Heslo sudo", - "Confirm": "Potvrdit" -} diff --git a/9router/public/i18n/literals/da.json b/9router/public/i18n/literals/da.json deleted file mode 100644 index 3c04bf7f..00000000 --- a/9router/public/i18n/literals/da.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Annuller", - "Delete": "Slet", - "Edit": "Rediger", - "Save": "Gem", - "Close": "Luk", - "Add": "Tilføj", - "Remove": "Fjern", - "Settings": "Indstillinger", - "Profile": "Profil", - "Dashboard": "Kontrolpanel", - "Logout": "Log ud", - "Login": "Log ind", - "Providers": "Udbydere", - "Usage": "Forbrug", - "API Key": "API-nøgle", - "Connected": "Forbundet", - "Disconnected": "Afbrudt", - "Active": "Aktiv", - "Inactive": "Inaktiv", - "Success": "Succes", - "Failed": "Mislykket", - "Error": "Fejl", - "Warning": "Advarsel", - "Info": "Info", - "Loading": "Indlæser", - "Search": "Søg", - "Filter": "Filter", - "Sort": "Sorter", - "Export": "Eksporter", - "Import": "Importer", - "Refresh": "Opdater", - "Back": "Tilbage", - "Next": "Næste", - "Previous": "Forrige", - "Submit": "Indsend", - "Confirm": "Bekræft", - "Yes": "Ja", - "No": "Nej", - "OK": "OK", - "Apply": "Anvend", - "Reset": "Nulstil", - "Clear": "Ryd", - "Select": "Vælg", - "Upload": "Upload", - "Download": "Download", - "Copy": "Kopier", - "Paste": "Indsæt", - "Cut": "Klip", - "Undo": "Fortryd", - "Redo": "Gentag", - "Name": "Navn", - "Description": "Beskrivelse", - "Status": "Status", - "Type": "Type", - "Date": "Dato", - "Time": "Tid", - "Created": "Oprettet", - "Updated": "Opdateret", - "Actions": "Handlinger", - "Details": "Detaljer", - "View": "Vis", - "New": "Ny", - "Total": "Total", - "Count": "Antal", - "Price": "Pris", - "Cost": "Omkostning", - "Free": "Gratis", - "Paid": "Betalt", - "Enable": "Aktivér", - "Disable": "Deaktivér", - "Enabled": "Aktiveret", - "Disabled": "Deaktiveret", - "Online": "Online", - "Offline": "Offline", - "Available": "Tilgængelig", - "Unavailable": "Ikke tilgængelig", - "Required": "Påkrævet", - "Optional": "Valgfrit", - "Default": "Standard", - "Custom": "Brugerdefineret", - "Advanced": "Avanceret", - "Basic": "Grundlæggende", - "Help": "Hjælp", - "Support": "Support", - "Documentation": "Dokumentation", - "Version": "Version", - "Language": "Sprog", - "Theme": "Tema", - "Light": "Lys", - "Dark": "Mørk", - "Auto": "Automatisk", - "Endpoint": "Slutpunkt", - "Providers": "Udbydere", - "Combos": "Kombinationer", - "Usage": "Forbrugsstatistik", - "Quota Tracker": "Kvotetracker", - "MITM": "MITM", - "CLI Tools": "Værktøjer", - "Console Log": "Konsollog", - "System": "System", - "Debug": "Debug", - "Shutdown": "Luk af", - "Close Proxy": "Luk proxy", - "Are you sure you want to close the proxy server?": "Er du sikker på, at du vil lukke proxyserveren?", - "Server Disconnected": "Server afbrudt", - "The proxy server has been stopped.": "Proxyserveren er blevet stoppet.", - "Reload Page": "Genindlæs siden", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Tjenesten kører i terminalen. Du kan lukke denne webside. Aflukning stopper tjenesten.", - "Manage your AI provider connections": "Administrer dine AI-udbyderforbindelser", - "Model combos with fallback": "Modelkombinationer med fallback", - "Monitor your API usage, token consumption, and request logs": "Overvåg dit API-forbrug, tokenforbrugning og anmodningslogger", - "Intercept CLI tool traffic and route through 9Router": "Aflyt CLI-værktøj trafikk og rute gennem 9Router", - "Configure CLI tools": "Konfigurer CLI-værktøjer", - "API endpoint configuration": "API-slutpunktkonfiguration", - "Manage your preferences": "Administrer dine præferencer", - "Debug translation flow between formats": "Debug oversættelsesflow mellem formater", - "Live server console output": "Live serverkonsoloutput", - "Create model combos with fallback support": "Opret modelkombinationer med fallback-understøttelse", - "Local Mode": "Lokalt tilstand", - "Running on your machine": "Kørende på din maskine", - "Database Location": "Databaseplacering", - "Download Backup": "Download sikkerhedskopi", - "Import Backup": "Importer sikkerhedskopi", - "Database backup downloaded": "Databasesikkerhedskopi downloadet", - "Database imported successfully": "Database importeret med succes", - "Security": "Sikkerhed", - "Require login": "Kræv login", - "When ON, dashboard requires password. When OFF, access without login.": "Når TIL kræves adgangskode på kontrolpanelet. Når FRA tillades adgang uden login.", - "Current Password": "Nuværende adgangskode", - "Enter current password": "Indtast nuværende adgangskode", - "New Password": "Ny adgangskode", - "Enter new password": "Indtast ny adgangskode", - "Confirm New Password": "Bekræft ny adgangskode", - "Confirm new password": "Bekræft ny adgangskode", - "Update Password": "Opdater adgangskode", - "Set Password": "Indstil adgangskode", - "Password updated successfully": "Adgangskode opdateret med succes", - "Passwords do not match": "Adgangskoderne stemmer ikke overens", - "Routing Strategy": "Rutestrategi", - "Round Robin": "Rundetabel", - "Cycle through accounts to distribute load": "Cyklus gennem konti for at distribuere belastningen", - "Sticky Limit": "Klæbrig grænse", - "Calls per account before switching": "Opkald pr. konto før skift", - "Network": "Netværk", - "Outbound Proxy": "Udgående proxy", - "Enable proxy for OAuth + provider outbound requests.": "Aktivér proxy til OAuth + udbyder udgående anmodninger.", - "Proxy URL": "Proxy-URL", - "Leave empty to inherit existing env proxy (if any).": "Lad være tomt for at nedarve eksisterende env-proxy (hvis nogen).", - "No Proxy": "Ingen proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Kommaseparerede værtsnavne/domæner for at omgå proxyen.", - "Test proxy URL": "Test proxy-URL", - "Apply": "Anvend", - "Proxy settings applied": "Proxyindstillinger anvendt", - "Proxy enabled": "Proxy aktiveret", - "Proxy disabled": "Proxy deaktiveret", - "Proxy test OK": "Proxy-test OK", - "Proxy test failed": "Proxy-test mislykket", - "Please enter a Proxy URL to test": "Indtast en proxy-URL til test", - "Observability": "Observerbarhed", - "Enable Observability": "Aktivér observerbarhed", - "Turn request detail recording on/off globally": "Slå anmodningsdetaljeoptagelse til/fra globalt", - "Max Records": "Max-poster", - "Maximum request detail records to keep (older records are auto-deleted)": "Maksimum anmodningsdetaljeposter at beholde (ældre poster bliver automatisk slettet)", - "Batch Size": "Batch-størrelse", - "Number of items to accumulate before writing to database (higher = better performance)": "Antal elementer der skal akkumuleres før skrivning til database (højere = bedre ydeevne)", - "Flush Interval (ms)": "Flush-interval (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maksimal ventetid før bufferrensing (forhindrer datatab ved lavt trafikk)", - "Max JSON Size (KB)": "Max JSON-størrelse (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maksimal størrelse for hvert JSON-felt (anmodning/svar) før afkortning", - "All data stored on your machine": "Alle data lagret på din maskine", - "MITM Server": "MITM-server", - "Running": "Kørende", - "Stopped": "Stoppet", - "Cert": "Certifikat", - "Server": "Server", - "Purpose:": "Formål:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Brug Antigravity IDE & GitHub Copilot → med ENHVER udbyder/model fra 9Router", - "How it works:": "Sådan virker det:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-anmodning → DNS-omdirigering til localhost:443 → MITM-proxy aflytter → 9Router → svar til Antigravity/Copilot", - "API Key": "API-nøgle", - "No API keys — create one in Keys page": "Ingen API-nøgler — opret en på Keys-siden", - "sk_9router (default)": "sk_9router (standard)", - "Server started": "Server startet", - "Failed to start server": "Fejl ved start af server", - "Server stopped — all DNS cleared": "Server stoppet — alle DNS slettet", - "Failed to stop server": "Fejl ved stopning af server", - "Sudo password is required": "Sudo-adgangskode er påkrævet", - "Stop Server": "Stop server", - "Start Server": "Start server", - "Enable DNS per tool below to activate interception": "Aktivér DNS for hvert værktøj nedenfor for at aktivere aflytning", - "Sudo Password Required": "Sudo-adgangskode påkrævet", - "Enter your sudo password to start/stop MITM server": "Indtast din sudo-adgangskode for at starte/stoppe MITM-server", - "Sudo Password": "Sudo-adgangskode", - "Confirm": "Bekræft" -} diff --git a/9router/public/i18n/literals/de.json b/9router/public/i18n/literals/de.json deleted file mode 100644 index 30a0baee..00000000 --- a/9router/public/i18n/literals/de.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Abbrechen", - "Delete": "Löschen", - "Edit": "Bearbeiten", - "Save": "Speichern", - "Close": "Schließen", - "Add": "Hinzufügen", - "Remove": "Entfernen", - "Settings": "Einstellungen", - "Profile": "Profil", - "Dashboard": "Dashboard", - "Logout": "Abmelden", - "Login": "Anmelden", - "Providers": "Anbieter", - "Usage": "Verwendung", - "API Key": "API-Schlüssel", - "Connected": "Verbunden", - "Disconnected": "Getrennt", - "Active": "Aktiv", - "Inactive": "Inaktiv", - "Success": "Erfolg", - "Failed": "Fehlgeschlagen", - "Error": "Fehler", - "Warning": "Warnung", - "Info": "Info", - "Loading": "Wird geladen", - "Search": "Suche", - "Filter": "Filtern", - "Sort": "Sortieren", - "Export": "Exportieren", - "Import": "Importieren", - "Refresh": "Aktualisieren", - "Back": "Zurück", - "Next": "Weiter", - "Previous": "Zurück", - "Submit": "Absenden", - "Confirm": "Bestätigen", - "Yes": "Ja", - "No": "Nein", - "OK": "OK", - "Apply": "Anwenden", - "Reset": "Zurücksetzen", - "Clear": "Löschen", - "Select": "Wählen", - "Upload": "Hochladen", - "Download": "Herunterladen", - "Copy": "Kopieren", - "Paste": "Einfügen", - "Cut": "Ausschneiden", - "Undo": "Rückgängig", - "Redo": "Wiederherstellen", - "Name": "Name", - "Description": "Beschreibung", - "Status": "Status", - "Type": "Typ", - "Date": "Datum", - "Time": "Uhrzeit", - "Created": "Erstellt", - "Updated": "Aktualisiert", - "Actions": "Aktionen", - "Details": "Details", - "View": "Anzeigen", - "New": "Neu", - "Total": "Gesamt", - "Count": "Anzahl", - "Price": "Preis", - "Cost": "Kosten", - "Free": "Kostenlos", - "Paid": "Bezahlt", - "Enable": "Aktivieren", - "Disable": "Deaktivieren", - "Enabled": "Aktiviert", - "Disabled": "Deaktiviert", - "Online": "Online", - "Offline": "Offline", - "Available": "Verfügbar", - "Unavailable": "Nicht verfügbar", - "Required": "Erforderlich", - "Optional": "Optional", - "Default": "Standard", - "Custom": "Benutzerdefiniert", - "Advanced": "Erweitert", - "Basic": "Grundlagen", - "Help": "Hilfe", - "Support": "Unterstützung", - "Documentation": "Dokumentation", - "Version": "Version", - "Language": "Sprache", - "Theme": "Design", - "Light": "Hell", - "Dark": "Dunkel", - "Auto": "Automatisch", - "Endpoint": "Endpunkt", - "Providers": "Anbieter", - "Combos": "Kombinationen", - "Usage": "Statistiken", - "Quota Tracker": "Kontingenttracker", - "MITM": "MITM", - "CLI Tools": "CLI-Tools", - "Console Log": "Konsolenprotokoll", - "System": "System", - "Debug": "Debuggen", - "Shutdown": "Herunterfahren", - "Close Proxy": "Proxy schließen", - "Are you sure you want to close the proxy server?": "Möchten Sie den Proxy-Server wirklich schließen?", - "Server Disconnected": "Server getrennt", - "The proxy server has been stopped.": "Der Proxy-Server wurde gestoppt.", - "Reload Page": "Seite neu laden", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Der Service läuft im Terminal. Sie können diese Webseite schließen. Das Herunterfahren stoppt den Service.", - "Manage your AI provider connections": "Verwalten Sie Ihre KI-Anbieterverbindungen", - "Model combos with fallback": "Modellkombinationen mit Fallback", - "Monitor your API usage, token consumption, and request logs": "Überwachen Sie Ihre API-Nutzung, den Token-Verbrauch und die Anforderungsprotokolle", - "Intercept CLI tool traffic and route through 9Router": "CLI-Tool-Verkehr abfangen und über 9Router leiten", - "Configure CLI tools": "CLI-Tools konfigurieren", - "API endpoint configuration": "API-Endpunkt-Konfiguration", - "Manage your preferences": "Verwalten Sie Ihre Vorlieben", - "Debug translation flow between formats": "Übersetzungsfluss zwischen Formaten debuggen", - "Live server console output": "Ausgabe der Live-Server-Konsole", - "Create model combos with fallback support": "Erstellen Sie Modellkombinationen mit Fallback-Unterstützung", - "Local Mode": "Lokaler Modus", - "Running on your machine": "Wird auf Ihrem Computer ausgeführt", - "Database Location": "Datenbankort", - "Download Backup": "Sicherung herunterladen", - "Import Backup": "Sicherung importieren", - "Database backup downloaded": "Datenbanksicherung heruntergeladen", - "Database imported successfully": "Datenbank erfolgreich importiert", - "Security": "Sicherheit", - "Require login": "Login erforderlich", - "When ON, dashboard requires password. When OFF, access without login.": "Wenn AN, erfordert das Dashboard ein Passwort. Wenn AUS, Zugriff ohne Login.", - "Current Password": "Aktuelles Passwort", - "Enter current password": "Aktuelles Passwort eingeben", - "New Password": "Neues Passwort", - "Enter new password": "Neues Passwort eingeben", - "Confirm New Password": "Neues Passwort bestätigen", - "Confirm new password": "Neues Passwort bestätigen", - "Update Password": "Passwort aktualisieren", - "Set Password": "Passwort festlegen", - "Password updated successfully": "Passwort erfolgreich aktualisiert", - "Passwords do not match": "Passwörter stimmen nicht überein", - "Routing Strategy": "Routing-Strategie", - "Round Robin": "Round-Robin", - "Cycle through accounts to distribute load": "Konten durchlaufen, um die Last zu verteilen", - "Sticky Limit": "Klebrige Grenze", - "Calls per account before switching": "Anrufe pro Konto vor dem Wechsel", - "Network": "Netzwerk", - "Outbound Proxy": "Ausgehender Proxy", - "Enable proxy for OAuth + provider outbound requests.": "Aktivieren Sie den Proxy für OAuth + Anfragen der ausgehenden Anbieter.", - "Proxy URL": "Proxy-URL", - "Leave empty to inherit existing env proxy (if any).": "Lassen Sie leer, um einen vorhandenen Umgebungs-Proxy zu erben (falls vorhanden).", - "No Proxy": "Kein Proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Kommagetrennte Hostnamen/Domänen zum Umgehen des Proxys.", - "Test proxy URL": "Proxy-URL testen", - "Apply": "Anwenden", - "Proxy settings applied": "Proxy-Einstellungen angewendet", - "Proxy enabled": "Proxy aktiviert", - "Proxy disabled": "Proxy deaktiviert", - "Proxy test OK": "Proxy-Test OK", - "Proxy test failed": "Proxy-Test fehlgeschlagen", - "Please enter a Proxy URL to test": "Bitte geben Sie eine Proxy-URL zum Testen ein", - "Observability": "Beobachtbarkeit", - "Enable Observability": "Beobachtbarkeit aktivieren", - "Turn request detail recording on/off globally": "Aufzeichnung von Anforderungsdetails global ein-/ausschalten", - "Max Records": "Maximale Datensätze", - "Maximum request detail records to keep (older records are auto-deleted)": "Maximale Anzahl der zu speichernden Anforderungsdetaildatensätze (ältere Datensätze werden automatisch gelöscht)", - "Batch Size": "Batch-Größe", - "Number of items to accumulate before writing to database (higher = better performance)": "Anzahl der Elemente, die sich ansammeln, bevor in die Datenbank geschrieben wird (höher = bessere Leistung)", - "Flush Interval (ms)": "Leerungsintervall (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maximale Wartezeit vor dem Leeren des Puffers (verhindert Datenverlust bei niedrigem Datenverkehr)", - "Max JSON Size (KB)": "Maximale JSON-Größe (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maximale Größe für jedes JSON-Feld (Anforderung/Antwort) vor dem Kürzen", - "All data stored on your machine": "Alle Daten auf Ihrem Computer gespeichert", - "MITM Server": "MITM-Server", - "Running": "Läuft", - "Stopped": "Gestoppt", - "Cert": "Zertifikat", - "Server": "Server", - "Purpose:": "Zweck:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Verwenden Sie Antigravity IDE und GitHub Copilot → mit JEDEM Anbieter/Modell von 9Router", - "How it works:": "So funktioniert es:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-Anforderung → DNS-Umleitung auf localhost:443 → MITM-Proxy abfangen → 9Router → Antwort auf Antigravity/Copilot", - "API Key": "API-Schlüssel", - "No API keys — create one in Keys page": "Keine API-Schlüssel — erstellen Sie einen auf der Seite Schlüssel", - "sk_9router (default)": "sk_9router (Standard)", - "Server started": "Server gestartet", - "Failed to start server": "Server konnte nicht gestartet werden", - "Server stopped — all DNS cleared": "Server gestoppt — alle DNS gelöscht", - "Failed to stop server": "Server konnte nicht gestoppt werden", - "Sudo password is required": "Sudo-Passwort erforderlich", - "Stop Server": "Server stoppen", - "Start Server": "Server starten", - "Enable DNS per tool below to activate interception": "Aktivieren Sie DNS für jedes Tool unten, um die Abfangung zu aktivieren", - "Sudo Password Required": "Sudo-Passwort erforderlich", - "Enter your sudo password to start/stop MITM server": "Geben Sie Ihr Sudo-Passwort ein, um den MITM-Server zu starten/stoppen", - "Sudo Password": "Sudo-Passwort", - "Confirm": "Bestätigen" -} diff --git a/9router/public/i18n/literals/el.json b/9router/public/i18n/literals/el.json deleted file mode 100644 index 0e15daae..00000000 --- a/9router/public/i18n/literals/el.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Ακύρωση", - "Delete": "Διαγραφή", - "Edit": "Επεξεργασία", - "Save": "Αποθήκευση", - "Close": "Κλείσιμο", - "Add": "Προσθήκη", - "Remove": "Αφαίρεση", - "Settings": "Ρυθμίσεις", - "Profile": "Προφίλ", - "Dashboard": "Πίνακας ελέγχου", - "Logout": "Έξοδος", - "Login": "Σύνδεση", - "Providers": "Παρόχοι", - "Usage": "Χρήση", - "API Key": "Κλειδί API", - "Connected": "Συνδεδεμένο", - "Disconnected": "Αποσυνδεδεμένο", - "Active": "Ενεργό", - "Inactive": "Ανενεργό", - "Success": "Επιτυχία", - "Failed": "Απέτυχε", - "Error": "Σφάλμα", - "Warning": "Προειδοποίηση", - "Info": "Πληροφορίες", - "Loading": "Φόρτωση", - "Search": "Αναζήτηση", - "Filter": "Φίλτρο", - "Sort": "Ταξινόμηση", - "Export": "Εξαγωγή", - "Import": "Εισαγωγή", - "Refresh": "Ανανέωση", - "Back": "Πίσω", - "Next": "Επόμενο", - "Previous": "Προηγούμενο", - "Submit": "Υποβολή", - "Confirm": "Επιβεβαίωση", - "Yes": "Ναι", - "No": "Όχι", - "OK": "ΟΚ", - "Apply": "Εφαρμογή", - "Reset": "Επαναφορά", - "Clear": "Εκκαθάριση", - "Select": "Επιλογή", - "Upload": "Μεταφόρτωση", - "Download": "Λήψη", - "Copy": "Αντιγραφή", - "Paste": "Επικόλληση", - "Cut": "Αποκοπή", - "Undo": "Αναίρεση", - "Redo": "Επανάληψη", - "Name": "Όνομα", - "Description": "Περιγραφή", - "Status": "Κατάσταση", - "Type": "Τύπος", - "Date": "Ημερομηνία", - "Time": "Ώρα", - "Created": "Δημιουργήθηκε", - "Updated": "Ενημερώθηκε", - "Actions": "Ενέργειες", - "Details": "Λεπτομέρειες", - "View": "Προβολή", - "New": "Νέο", - "Total": "Σύνολο", - "Count": "Μέτρηση", - "Price": "Τιμή", - "Cost": "Κόστος", - "Free": "Δωρεάν", - "Paid": "Πληρωμένο", - "Enable": "Ενεργοποίηση", - "Disable": "Απενεργοποίηση", - "Enabled": "Ενεργοποιημένο", - "Disabled": "Απενεργοποιημένο", - "Online": "Σε σύνδεση", - "Offline": "Χωρίς σύνδεση", - "Available": "Διαθέσιμο", - "Unavailable": "Μη διαθέσιμο", - "Required": "Απαιτούμενο", - "Optional": "Προαιρετικό", - "Default": "Προεπιλεγμένο", - "Custom": "Προσαρμοσμένο", - "Advanced": "Προχωρημένο", - "Basic": "Βασικό", - "Help": "Βοήθεια", - "Support": "Υποστήριξη", - "Documentation": "Τεκμηρίωση", - "Version": "Έκδοση", - "Language": "Γλώσσα", - "Theme": "Θέμα", - "Light": "Ανοιχτό", - "Dark": "Σκοτεινό", - "Auto": "Αυτόματο", - "Endpoint": "Τελικό σημείο", - "Providers": "Παρόχοι", - "Combos": "Συνδυασμοί", - "Usage": "Στατιστικά χρήσης", - "Quota Tracker": "Παρακολούθηση ποσόστωσης", - "MITM": "MITM", - "CLI Tools": "Εργαλεία", - "Console Log": "Αρχείο καταγραφής κονσόλας", - "System": "Σύστημα", - "Debug": "Αποσφαλμάτωση", - "Shutdown": "Τερματισμός", - "Close Proxy": "Κλείσιμο διακομιστή μεσολάβησης", - "Are you sure you want to close the proxy server?": "Είστε σίγουρος ότι θέλετε να κλείσετε τον διακομιστή μεσολάβησης;", - "Server Disconnected": "Διακομιστής αποσυνδεδεμένος", - "The proxy server has been stopped.": "Ο διακομιστής μεσολάβησης έχει σταματήσει.", - "Reload Page": "Επαναφόρτωση σελίδας", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Η υπηρεσία εκτελείται στο τερματικό. Μπορείτε να κλείσετε αυτή τη σελίδα ιστού. Ο τερματισμός θα σταματήσει την υπηρεσία.", - "Manage your AI provider connections": "Διαχειριστείτε τις συνδέσεις του παρόχου AI σας", - "Model combos with fallback": "Συνδυασμοί μοντέλων με αποπροσπέλαση", - "Monitor your API usage, token consumption, and request logs": "Παρακολουθήστε τη χρήση API, την κατανάλωση token και τα αρχεία καταγραφής αιτημάτων", - "Intercept CLI tool traffic and route through 9Router": "Αναχαίτηση κίνησης εργαλείου CLI και δρομολόγηση μέσω 9Router", - "Configure CLI tools": "Ρύθμιση εργαλείων CLI", - "API endpoint configuration": "Διαμόρφωση τελικού σημείου API", - "Manage your preferences": "Διαχείριση προτιμήσεών σας", - "Debug translation flow between formats": "Αποσφαλμάτωση ροής μετάφρασης μεταξύ μορφών", - "Live server console output": "Έξοδος κονσόλας διακομιστή σε πραγματικό χρόνο", - "Create model combos with fallback support": "Δημιουργήστε συνδυασμούς μοντέλων με υποστήριξη αποπροσπέλασης", - "Local Mode": "Τοπική λειτουργία", - "Running on your machine": "Εκτελείται στον υπολογιστή σας", - "Database Location": "Τοποθεσία βάσης δεδομένων", - "Download Backup": "Λήψη ασφαλείας", - "Import Backup": "Εισαγωγή ασφαλείας", - "Database backup downloaded": "Ασφάλεια βάσης δεδομένων λήφθηκε", - "Database imported successfully": "Η βάση δεδομένων εισήχθη με επιτυχία", - "Security": "Ασφάλεια", - "Require login": "Απαιτείται σύνδεση", - "When ON, dashboard requires password. When OFF, access without login.": "Όταν είναι ΕΝ, ο πίνακας ελέγχου απαιτεί κωδικό πρόσβασης. Όταν είναι ΑΠΕΝΕΡΓΟΠΟΙΗΜΕΝΟ, πρόσβαση χωρίς σύνδεση.", - "Current Password": "Τρέχων κωδικός πρόσβασης", - "Enter current password": "Εισαγάγετε τον τρέχοντα κωδικό πρόσβασης", - "New Password": "Νέος κωδικός πρόσβασης", - "Enter new password": "Εισαγάγετε τον νέο κωδικό πρόσβασης", - "Confirm New Password": "Επιβεβαίωση νέου κωδικού πρόσβασης", - "Confirm new password": "Επιβεβαίωση νέου κωδικού πρόσβασης", - "Update Password": "Ενημέρωση κωδικού πρόσβασης", - "Set Password": "Ορισμός κωδικού πρόσβασης", - "Password updated successfully": "Ο κωδικός πρόσβασης ενημερώθηκε με επιτυχία", - "Passwords do not match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", - "Routing Strategy": "Στρατηγική δρομολόγησης", - "Round Robin": "Κυκλική δρομολόγηση", - "Cycle through accounts to distribute load": "Κύκλος μέσω λογαριασμών για κατανομή φορτίου", - "Sticky Limit": "Περιορισμός κόλλησης", - "Calls per account before switching": "Κλήσεις ανά λογαριασμό πριν από την εναλλαγή", - "Network": "Δίκτυο", - "Outbound Proxy": "Εξερχόμενος διακομιστής μεσολάβησης", - "Enable proxy for OAuth + provider outbound requests.": "Ενεργοποιήστε τον διακομιστή μεσολάβησης για αιτήματα εξόδου OAuth + παρόχου.", - "Proxy URL": "URL διακομιστή μεσολάβησης", - "Leave empty to inherit existing env proxy (if any).": "Αφήστε κενό για να κληρονομήσετε υπάρχοντα env proxy (εάν υπάρχει).", - "No Proxy": "Χωρίς διακομιστή μεσολάβησης", - "Comma-separated hostnames/domains to bypass the proxy.": "Ονόματα κεντρικών υπολογιστών/τομείς χωρισμένοι με κόμμα για να παραστούν τον διακομιστή μεσολάβησης.", - "Test proxy URL": "Δοκιμή URL διακομιστή μεσολάβησης", - "Apply": "Εφαρμογή", - "Proxy settings applied": "Ρυθμίσεις διακομιστή μεσολάβησης εφαρμόστηκαν", - "Proxy enabled": "Διακομιστής μεσολάβησης ενεργοποιημένος", - "Proxy disabled": "Διακομιστής μεσολάβησης απενεργοποιημένος", - "Proxy test OK": "Δοκιμή διακομιστή μεσολάβησης OK", - "Proxy test failed": "Η δοκιμή διακομιστή μεσολάβησης απέτυχε", - "Please enter a Proxy URL to test": "Παρακαλώ εισάγετε ένα URL διακομιστή μεσολάβησης για δοκιμή", - "Observability": "Δυνατότητα παρατήρησης", - "Enable Observability": "Ενεργοποίηση δυνατότητας παρατήρησης", - "Turn request detail recording on/off globally": "Ενεργοποιήστε/απενεργοποιήστε την καταγραφή λεπτομερειών αιτήματος σε παγκόσμιο επίπεδο", - "Max Records": "Μέγιστα αρχεία", - "Maximum request detail records to keep (older records are auto-deleted)": "Μέγιστα αρχεία λεπτομερειών αιτήματος για διατήρηση (τα παλαιότερα αρχεία διαγράφονται αυτόματα)", - "Batch Size": "Μέγεθος δέσμης", - "Number of items to accumulate before writing to database (higher = better performance)": "Αριθμός στοιχείων που πρέπει να συσσωρευθούν πριν από τη σύνταξη στη βάση δεδομένων (υψηλότερο = καλύτερη απόδοση)", - "Flush Interval (ms)": "Διάστημα ξεπλύματος (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Μέγιστος χρόνος αναμονής πριν από το ξέπλυμα του buffer (αποτρέπει την απώλεια δεδομένων κατά την χαμηλή κίνηση)", - "Max JSON Size (KB)": "Μέγιστο μέγεθος JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Μέγιστο μέγεθος για κάθε πεδίο JSON (αίτημα/απάντηση) πριν από την περικοπή", - "All data stored on your machine": "Όλα τα δεδομένα αποθηκεύονται στον υπολογιστή σας", - "MITM Server": "Διακομιστής MITM", - "Running": "Εκτελείται", - "Stopped": "Διακοπή", - "Cert": "Πιστοποιητικό", - "Server": "Διακομιστής", - "Purpose:": "Σκοπός:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Χρησιμοποιήστε Antigravity IDE & GitHub Copilot → με ΟΠΟΙΟΝΔΗΠΟΤΕ πάροχο/μοντέλο από 9Router", - "How it works:": "Πώς λειτουργεί:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Αίτημα Antigravity/Copilot IDE → Ανακατεύθυνση DNS στο localhost:443 → Ο διακομιστής μεσολάβησης MITM παρεμβαίνει → 9Router → απάντηση στο Antigravity/Copilot", - "API Key": "Κλειδί API", - "No API keys — create one in Keys page": "Δεν υπάρχουν κλειδιά API — δημιουργήστε ένα στη σελίδα Keys", - "sk_9router (default)": "sk_9router (προεπιλεγμένο)", - "Server started": "Ο διακομιστής ξεκίνησε", - "Failed to start server": "Αποτυχία εκκίνησης διακομιστή", - "Server stopped — all DNS cleared": "Ο διακομιστής σταμάτησε — όλα τα DNS διαγράφηκαν", - "Failed to stop server": "Αποτυχία διακοπής διακομιστή", - "Sudo password is required": "Απαιτείται κωδικός πρόσβασης sudo", - "Stop Server": "Διακοπή διακομιστή", - "Start Server": "Διακομιστή ξεκινήματος", - "Enable DNS per tool below to activate interception": "Ενεργοποιήστε το DNS για κάθε εργαλείο παρακάτω για ενεργοποίηση παρεμβολής", - "Sudo Password Required": "Απαιτείται κωδικός πρόσβασης Sudo", - "Enter your sudo password to start/stop MITM server": "Εισαγάγετε τον κωδικό πρόσβασης sudo για να ξεκινήσετε/διακόψετε τον διακομιστή MITM", - "Sudo Password": "Κωδικός πρόσβασης Sudo", - "Confirm": "Επιβεβαίωση" -} diff --git a/9router/public/i18n/literals/es.json b/9router/public/i18n/literals/es.json deleted file mode 100644 index ea7cf45b..00000000 --- a/9router/public/i18n/literals/es.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Cancelar", - "Delete": "Eliminar", - "Edit": "Editar", - "Save": "Guardar", - "Close": "Cerrar", - "Add": "Añadir", - "Remove": "Quitar", - "Settings": "Configuración", - "Profile": "Perfil", - "Dashboard": "Panel de control", - "Logout": "Cerrar sesión", - "Login": "Iniciar sesión", - "Providers": "Proveedores", - "Usage": "Uso", - "API Key": "Clave API", - "Connected": "Conectado", - "Disconnected": "Desconectado", - "Active": "Activo", - "Inactive": "Inactivo", - "Success": "Éxito", - "Failed": "Fallido", - "Error": "Error", - "Warning": "Advertencia", - "Info": "Información", - "Loading": "Cargando", - "Search": "Buscar", - "Filter": "Filtrar", - "Sort": "Ordenar", - "Export": "Exportar", - "Import": "Importar", - "Refresh": "Actualizar", - "Back": "Atrás", - "Next": "Siguiente", - "Previous": "Anterior", - "Submit": "Enviar", - "Confirm": "Confirmar", - "Yes": "Sí", - "No": "No", - "OK": "OK", - "Apply": "Aplicar", - "Reset": "Restablecer", - "Clear": "Limpiar", - "Select": "Seleccionar", - "Upload": "Cargar", - "Download": "Descargar", - "Copy": "Copiar", - "Paste": "Pegar", - "Cut": "Cortar", - "Undo": "Deshacer", - "Redo": "Rehacer", - "Name": "Nombre", - "Description": "Descripción", - "Status": "Estado", - "Type": "Tipo", - "Date": "Fecha", - "Time": "Hora", - "Created": "Creado", - "Updated": "Actualizado", - "Actions": "Acciones", - "Details": "Detalles", - "View": "Ver", - "New": "Nuevo", - "Total": "Total", - "Count": "Cantidad", - "Price": "Precio", - "Cost": "Costo", - "Free": "Gratuito", - "Paid": "Pagado", - "Enable": "Habilitar", - "Disable": "Deshabilitar", - "Enabled": "Habilitado", - "Disabled": "Deshabilitado", - "Online": "En línea", - "Offline": "Desconectado", - "Available": "Disponible", - "Unavailable": "No disponible", - "Required": "Requerido", - "Optional": "Opcional", - "Default": "Predeterminado", - "Custom": "Personalizado", - "Advanced": "Avanzado", - "Basic": "Básico", - "Help": "Ayuda", - "Support": "Soporte", - "Documentation": "Documentación", - "Version": "Versión", - "Language": "Idioma", - "Theme": "Tema", - "Light": "Claro", - "Dark": "Oscuro", - "Auto": "Automático", - "Endpoint": "Punto final", - "Providers": "Proveedores", - "Combos": "Combinaciones", - "Usage": "Estadísticas", - "Quota Tracker": "Rastreador de cuota", - "MITM": "MITM", - "CLI Tools": "Herramientas CLI", - "Console Log": "Registro de consola", - "System": "Sistema", - "Debug": "Depuración", - "Shutdown": "Apagar", - "Close Proxy": "Cerrar proxy", - "Are you sure you want to close the proxy server?": "¿Está seguro de que desea cerrar el servidor proxy?", - "Server Disconnected": "Servidor desconectado", - "The proxy server has been stopped.": "El servidor proxy se ha detenido.", - "Reload Page": "Recargar página", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "El servicio se está ejecutando en la terminal. Puede cerrar esta página web. El apagado detendrá el servicio.", - "Manage your AI provider connections": "Administre sus conexiones de proveedor de IA", - "Model combos with fallback": "Combinaciones de modelos con respaldo", - "Monitor your API usage, token consumption, and request logs": "Monitoree su uso de API, consumo de tokens y registros de solicitudes", - "Intercept CLI tool traffic and route through 9Router": "Interceptar el tráfico de herramientas CLI y enrutar a través de 9Router", - "Configure CLI tools": "Configurar herramientas CLI", - "API endpoint configuration": "Configuración del punto final de API", - "Manage your preferences": "Administrar sus preferencias", - "Debug translation flow between formats": "Depurar el flujo de traducción entre formatos", - "Live server console output": "Salida de consola del servidor en vivo", - "Create model combos with fallback support": "Crear combinaciones de modelos con soporte de respaldo", - "Local Mode": "Modo local", - "Running on your machine": "En ejecución en su máquina", - "Database Location": "Ubicación de la base de datos", - "Download Backup": "Descargar respaldo", - "Import Backup": "Importar respaldo", - "Database backup downloaded": "Respaldo de la base de datos descargado", - "Database imported successfully": "Base de datos importada correctamente", - "Security": "Seguridad", - "Require login": "Requerir inicio de sesión", - "When ON, dashboard requires password. When OFF, access without login.": "Cuando está ACTIVADO, el panel requiere contraseña. Cuando está DESACTIVADO, acceso sin iniciar sesión.", - "Current Password": "Contraseña actual", - "Enter current password": "Ingrese la contraseña actual", - "New Password": "Nueva contraseña", - "Enter new password": "Ingrese la nueva contraseña", - "Confirm New Password": "Confirmar nueva contraseña", - "Confirm new password": "Confirme la nueva contraseña", - "Update Password": "Actualizar contraseña", - "Set Password": "Establecer contraseña", - "Password updated successfully": "Contraseña actualizada correctamente", - "Passwords do not match": "Las contraseñas no coinciden", - "Routing Strategy": "Estrategia de enrutamiento", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Ciclo a través de cuentas para distribuir la carga", - "Sticky Limit": "Límite pegajoso", - "Calls per account before switching": "Llamadas por cuenta antes de cambiar", - "Network": "Red", - "Outbound Proxy": "Proxy de salida", - "Enable proxy for OAuth + provider outbound requests.": "Habilite el proxy para OAuth + solicitudes de salida del proveedor.", - "Proxy URL": "URL del proxy", - "Leave empty to inherit existing env proxy (if any).": "Deje en blanco para heredar el proxy env existente (si lo hay).", - "No Proxy": "Sin proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Nombres de host/dominios separados por comas para omitir el proxy.", - "Test proxy URL": "Prueba URL del proxy", - "Apply": "Aplicar", - "Proxy settings applied": "Configuración de proxy aplicada", - "Proxy enabled": "Proxy habilitado", - "Proxy disabled": "Proxy deshabilitado", - "Proxy test OK": "Prueba de proxy OK", - "Proxy test failed": "Falha en la prueba de proxy", - "Please enter a Proxy URL to test": "Por favor ingrese una URL de proxy para probar", - "Observability": "Observabilidad", - "Enable Observability": "Habilitar observabilidad", - "Turn request detail recording on/off globally": "Activar/desactivar globalmente el registro de detalles de solicitud", - "Max Records": "Número máximo de registros", - "Maximum request detail records to keep (older records are auto-deleted)": "Número máximo de registros de detalle de solicitud a mantener (los registros más antiguos se eliminan automáticamente)", - "Batch Size": "Tamaño del lote", - "Number of items to accumulate before writing to database (higher = better performance)": "Número de elementos a acumular antes de escribir en la base de datos (más alto = mejor rendimiento)", - "Flush Interval (ms)": "Intervalo de vaciado (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Tiempo máximo de espera antes de vaciar el búfer (evita pérdida de datos durante tráfico bajo)", - "Max JSON Size (KB)": "Tamaño máximo de JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Tamaño máximo para cada campo JSON (solicitud/respuesta) antes del truncamiento", - "All data stored on your machine": "Todos los datos almacenados en su máquina", - "MITM Server": "Servidor MITM", - "Running": "En ejecución", - "Stopped": "Detenido", - "Cert": "Certificado", - "Server": "Servidor", - "Purpose:": "Propósito:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Use Antigravity IDE y GitHub Copilot → con CUALQUIER proveedor/modelo de 9Router", - "How it works:": "Cómo funciona:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Solicitud de Antigravity/Copilot IDE → Redireccionamiento DNS a localhost:443 → El proxy MITM intercepta → 9Router → respuesta a Antigravity/Copilot", - "API Key": "Clave API", - "No API keys — create one in Keys page": "Sin claves API — cree una en la página Claves", - "sk_9router (default)": "sk_9router (predeterminado)", - "Server started": "Servidor iniciado", - "Failed to start server": "Error al iniciar el servidor", - "Server stopped — all DNS cleared": "Servidor detenido — todo DNS borrado", - "Failed to stop server": "Error al detener el servidor", - "Sudo password is required": "Se requiere contraseña de sudo", - "Stop Server": "Detener servidor", - "Start Server": "Iniciar servidor", - "Enable DNS per tool below to activate interception": "Habilite DNS para cada herramienta a continuación para activar la intercepción", - "Sudo Password Required": "Contraseña de Sudo requerida", - "Enter your sudo password to start/stop MITM server": "Ingrese su contraseña de sudo para iniciar/detener el servidor MITM", - "Sudo Password": "Contraseña de sudo", - "Confirm": "Confirmar" -} diff --git a/9router/public/i18n/literals/fi.json b/9router/public/i18n/literals/fi.json deleted file mode 100644 index 54af1a42..00000000 --- a/9router/public/i18n/literals/fi.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Peruuta", - "Delete": "Poista", - "Edit": "Muokkaa", - "Save": "Tallenna", - "Close": "Sulje", - "Add": "Lisää", - "Remove": "Poista", - "Settings": "Asetukset", - "Profile": "Profiili", - "Dashboard": "Kojelauta", - "Logout": "Kirjaudu ulos", - "Login": "Kirjaudu sisään", - "Providers": "Palveluntarjoajat", - "Usage": "Käyttö", - "API Key": "API-avain", - "Connected": "Yhdistetty", - "Disconnected": "Yhteys katkaistu", - "Active": "Aktiivinen", - "Inactive": "Passiivinen", - "Success": "Onnistui", - "Failed": "Epäonnistui", - "Error": "Virhe", - "Warning": "Varoitus", - "Info": "Tiedot", - "Loading": "Ladataan", - "Search": "Hae", - "Filter": "Suodin", - "Sort": "Lajittele", - "Export": "Vie", - "Import": "Tuo", - "Refresh": "Päivitä", - "Back": "Takaisin", - "Next": "Seuraava", - "Previous": "Edellinen", - "Submit": "Lähetä", - "Confirm": "Vahvista", - "Yes": "Kyllä", - "No": "Ei", - "OK": "OK", - "Apply": "Käytä", - "Reset": "Nollaa", - "Clear": "Tyhjennä", - "Select": "Valitse", - "Upload": "Lataa", - "Download": "Lataa", - "Copy": "Kopioi", - "Paste": "Liitä", - "Cut": "Leikkaa", - "Undo": "Kumoa", - "Redo": "Tee uudelleen", - "Name": "Nimi", - "Description": "Kuvaus", - "Status": "Tila", - "Type": "Tyyppi", - "Date": "Päivämäärä", - "Time": "Aika", - "Created": "Luotu", - "Updated": "Päivitetty", - "Actions": "Toiminnot", - "Details": "Tiedot", - "View": "Näytä", - "New": "Uusi", - "Total": "Yhteensä", - "Count": "Määrä", - "Price": "Hinta", - "Cost": "Kustannus", - "Free": "Ilmainen", - "Paid": "Maksettu", - "Enable": "Ota käyttöön", - "Disable": "Poista käytöstä", - "Enabled": "Käytössä", - "Disabled": "Pois käytöstä", - "Online": "Online", - "Offline": "Offline", - "Available": "Saatavilla", - "Unavailable": "Ei saatavilla", - "Required": "Pakollinen", - "Optional": "Valinnainen", - "Default": "Oletus", - "Custom": "Mukautettu", - "Advanced": "Lisäasetukset", - "Basic": "Perus", - "Help": "Apua", - "Support": "Tuki", - "Documentation": "Dokumentaatio", - "Version": "Versio", - "Language": "Kieli", - "Theme": "Teema", - "Light": "Vaalea", - "Dark": "Tumma", - "Auto": "Automaattinen", - "Endpoint": "Pääteeksi", - "Providers": "Palveluntarjoajat", - "Combos": "Yhdistelmät", - "Usage": "Käyttötilastot", - "Quota Tracker": "Kiintiyden seuranta", - "MITM": "MITM", - "CLI Tools": "Työkalut", - "Console Log": "Konsolilokit", - "System": "Järjestelmä", - "Debug": "Virheenkorjaus", - "Shutdown": "Sammuta", - "Close Proxy": "Sulje välityspalvelin", - "Are you sure you want to close the proxy server?": "Oletko varma, että haluat sulkea välityspalvelimen?", - "Server Disconnected": "Palvelin katkaistiin", - "The proxy server has been stopped.": "Välityspalvelin on pysäytetty.", - "Reload Page": "Päivitä sivu", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Palvelu on käynnissä terminaalissa. Voit sulkea tämän verkkosivun. Sammutus pysäyttää palvelun.", - "Manage your AI provider connections": "Hallitse AI-palveluntarjoajayhteyksääsi", - "Model combos with fallback": "Mallien yhdistelmät palautuksella", - "Monitor your API usage, token consumption, and request logs": "Valvo API-käyttöä, tunnusmerkkien kulutusta ja pyyntölokeja", - "Intercept CLI tool traffic and route through 9Router": "Sieppaa CLI-työkaluliikenteen ja reitit 9Routerin kautta", - "Configure CLI tools": "Konfiguroi CLI-työkalut", - "API endpoint configuration": "API-päätepisteen konfiguraatio", - "Manage your preferences": "Hallitse asetuksiasi", - "Debug translation flow between formats": "Virheenkorjaus kääntämisvirta formaattien välillä", - "Live server console output": "Palvelimen konsolin tulostus reaaliajassa", - "Create model combos with fallback support": "Luo malliyhdistelmiä palautustuen kanssa", - "Local Mode": "Paikallinen tila", - "Running on your machine": "Käynnissä koneellasi", - "Database Location": "Tietokannan sijainti", - "Download Backup": "Lataa varmuuskopio", - "Import Backup": "Tuo varmuuskopio", - "Database backup downloaded": "Tietokannan varmuuskopio ladattiin", - "Database imported successfully": "Tietokanta tuotiin onnistuneesti", - "Security": "Turvallisuus", - "Require login": "Vaadi kirjautumista", - "When ON, dashboard requires password. When OFF, access without login.": "Kun ON, kojelauta vaatii salasanaa. Kun OFF, pääsy ilman kirjautumista.", - "Current Password": "Nykyinen salasana", - "Enter current password": "Kirjoita nykyinen salasana", - "New Password": "Uusi salasana", - "Enter new password": "Kirjoita uusi salasana", - "Confirm New Password": "Vahvista uusi salasana", - "Confirm new password": "Vahvista uusi salasana", - "Update Password": "Päivitä salasana", - "Set Password": "Aseta salasana", - "Password updated successfully": "Salasana päivitettiin onnistuneesti", - "Passwords do not match": "Salasanat eivät täsmää", - "Routing Strategy": "Reititysstrategia", - "Round Robin": "Kiertelevä robotti", - "Cycle through accounts to distribute load": "Kierrä tileillä kuormituksen jakamiseksi", - "Sticky Limit": "Kiinteä raja", - "Calls per account before switching": "Puhelut tiliä kohti ennen vaihtamista", - "Network": "Verkko", - "Outbound Proxy": "Lähtevä välityspalvelin", - "Enable proxy for OAuth + provider outbound requests.": "Ota käyttöön välityspalvelin OAuth + palveluntarjoajan lähteviin pyyntöihin.", - "Proxy URL": "Välityspalvelimen URL", - "Leave empty to inherit existing env proxy (if any).": "Jätä tyhjäksi periä olemassa olevaa env-välityspalvelinta (jos sellainen on).", - "No Proxy": "Ei välityspalvelinta", - "Comma-separated hostnames/domains to bypass the proxy.": "Pilkulla erotetut isäntänimet/etäisyydet välityspalvelimen ohittamiseksi.", - "Test proxy URL": "Testaa välityspalvelimen URL", - "Apply": "Käytä", - "Proxy settings applied": "Välityspalvelimen asetukset käytössä", - "Proxy enabled": "Välityspalvelin otettu käyttöön", - "Proxy disabled": "Välityspalvelin poistettu käytöstä", - "Proxy test OK": "Välityspalvelimen testi OK", - "Proxy test failed": "Välityspalvelimen testi epäonnistui", - "Please enter a Proxy URL to test": "Kirjoita testattava välityspalvelimen URL", - "Observability": "Havainnointitarkkuus", - "Enable Observability": "Ota havainnointitarkkuus käyttöön", - "Turn request detail recording on/off globally": "Poista pyynnön yksityiskohtien tallentaminen käyttöön/pois käytöstä maailmanlaajuisesti", - "Max Records": "Max-tietueet", - "Maximum request detail records to keep (older records are auto-deleted)": "Maksimaalinen pyynnön yksityiskohtitietueet säilytettäväksi (vanhemmat tietueet poistetaan automaattisesti)", - "Batch Size": "Erän koko", - "Number of items to accumulate before writing to database (higher = better performance)": "Tietokantaan kirjoittamista edeltävien kerättävien kohteiden lukumäärä (korkeampi = parempi suorituskyky)", - "Flush Interval (ms)": "Tyhjennysväli (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Enimmäisaika odottaa ennen puskurin tyhjentämistä (estää tietojen menetyksen matalan liikenteen aikana)", - "Max JSON Size (KB)": "Maksimaalinen JSON-koko (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Kunkin JSON-kentän (pyynnön/vastauksen) enimmäiskoko ennen katkaisua", - "All data stored on your machine": "Kaikki tiedot tallennettu koneellasi", - "MITM Server": "MITM-palvelin", - "Running": "Käynnissä", - "Stopped": "Pysäytetty", - "Cert": "Sertifikaatti", - "Server": "Palvelin", - "Purpose:": "Tarkoitus:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Käytä Antigravity IDE:tä ja GitHub Copilot:ia → minkä tahansa palveluntarjoajan/mallin kanssa 9Routerista", - "How it works:": "Kuinka se toimii:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-pyyntö → DNS-uudelleenohjaus localhost:443:iin → MITM-välityspalvelin sieppaa → 9Router → vastaus Antigravity/Copilot:ille", - "API Key": "API-avain", - "No API keys — create one in Keys page": "Ei API-avaimia — luo yksi Keys-sivulla", - "sk_9router (default)": "sk_9router (oletus)", - "Server started": "Palvelin käynnistetty", - "Failed to start server": "Palvelimen käynnistäminen epäonnistui", - "Server stopped — all DNS cleared": "Palvelin pysäytetty — kaikki DNS poistettu", - "Failed to stop server": "Palvelimen pysäyttäminen epäonnistui", - "Sudo password is required": "Sudo-salasana vaaditaan", - "Stop Server": "Pysäytä palvelin", - "Start Server": "Käynnistä palvelin", - "Enable DNS per tool below to activate interception": "Ota DNS käyttöön kunkin alla olevan työkalun osalta aktivoidaksesi sieppauksen", - "Sudo Password Required": "Sudo-salasana vaaditaan", - "Enter your sudo password to start/stop MITM server": "Kirjoita sudo-salasanasi MITM-palvelimen käynnistämiseksi/pysäyttämiseksi", - "Sudo Password": "Sudo-salasana", - "Confirm": "Vahvista" -} diff --git a/9router/public/i18n/literals/fr.json b/9router/public/i18n/literals/fr.json deleted file mode 100644 index 5a75f5c0..00000000 --- a/9router/public/i18n/literals/fr.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Annuler", - "Delete": "Supprimer", - "Edit": "Modifier", - "Save": "Enregistrer", - "Close": "Fermer", - "Add": "Ajouter", - "Remove": "Retirer", - "Settings": "Paramètres", - "Profile": "Profil", - "Dashboard": "Tableau de bord", - "Logout": "Déconnexion", - "Login": "Connexion", - "Providers": "Fournisseurs", - "Usage": "Utilisation", - "API Key": "Clé API", - "Connected": "Connecté", - "Disconnected": "Déconnecté", - "Active": "Actif", - "Inactive": "Inactif", - "Success": "Succès", - "Failed": "Échoué", - "Error": "Erreur", - "Warning": "Avertissement", - "Info": "Informations", - "Loading": "Chargement", - "Search": "Rechercher", - "Filter": "Filtrer", - "Sort": "Trier", - "Export": "Exporter", - "Import": "Importer", - "Refresh": "Actualiser", - "Back": "Retour", - "Next": "Suivant", - "Previous": "Précédent", - "Submit": "Soumettre", - "Confirm": "Confirmer", - "Yes": "Oui", - "No": "Non", - "OK": "OK", - "Apply": "Appliquer", - "Reset": "Réinitialiser", - "Clear": "Effacer", - "Select": "Sélectionner", - "Upload": "Télécharger", - "Download": "Télécharger", - "Copy": "Copier", - "Paste": "Coller", - "Cut": "Couper", - "Undo": "Annuler", - "Redo": "Refaire", - "Name": "Nom", - "Description": "Description", - "Status": "Statut", - "Type": "Type", - "Date": "Date", - "Time": "Heure", - "Created": "Créé", - "Updated": "Modifié", - "Actions": "Actions", - "Details": "Détails", - "View": "Afficher", - "New": "Nouveau", - "Total": "Total", - "Count": "Nombre", - "Price": "Prix", - "Cost": "Coût", - "Free": "Gratuit", - "Paid": "Payant", - "Enable": "Activer", - "Disable": "Désactiver", - "Enabled": "Activé", - "Disabled": "Désactivé", - "Online": "En ligne", - "Offline": "Hors ligne", - "Available": "Disponible", - "Unavailable": "Indisponible", - "Required": "Requis", - "Optional": "Facultatif", - "Default": "Par défaut", - "Custom": "Personnalisé", - "Advanced": "Avancé", - "Basic": "Basique", - "Help": "Aide", - "Support": "Support", - "Documentation": "Documentation", - "Version": "Version", - "Language": "Langue", - "Theme": "Thème", - "Light": "Clair", - "Dark": "Sombre", - "Auto": "Automatique", - "Endpoint": "Point final", - "Providers": "Fournisseurs", - "Combos": "Combinaisons", - "Usage": "Statistiques", - "Quota Tracker": "Suivi des quotas", - "MITM": "MITM", - "CLI Tools": "Outils CLI", - "Console Log": "Journaux de console", - "System": "Système", - "Debug": "Débogage", - "Shutdown": "Arrêt", - "Close Proxy": "Fermer le proxy", - "Are you sure you want to close the proxy server?": "Êtes-vous sûr de vouloir fermer le serveur proxy ?", - "Server Disconnected": "Serveur déconnecté", - "The proxy server has been stopped.": "Le serveur proxy a été arrêté.", - "Reload Page": "Recharger la page", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Le service s'exécute dans le terminal. Vous pouvez fermer cette page web. L'arrêt arrêtera le service.", - "Manage your AI provider connections": "Gérez vos connexions de fournisseur IA", - "Model combos with fallback": "Combinaisons de modèles avec secours", - "Monitor your API usage, token consumption, and request logs": "Surveillez votre utilisation d'API, la consommation de jetons et les journaux de requête", - "Intercept CLI tool traffic and route through 9Router": "Interceptez le trafic des outils CLI et acheminez via 9Router", - "Configure CLI tools": "Configurer les outils CLI", - "API endpoint configuration": "Configuration du point final API", - "Manage your preferences": "Gérez vos préférences", - "Debug translation flow between formats": "Déboguer le flux de traduction entre les formats", - "Live server console output": "Sortie de la console du serveur en direct", - "Create model combos with fallback support": "Créer des combinaisons de modèles avec support de secours", - "Local Mode": "Mode local", - "Running on your machine": "S'exécute sur votre machine", - "Database Location": "Emplacement de la base de données", - "Download Backup": "Télécharger la sauvegarde", - "Import Backup": "Importer la sauvegarde", - "Database backup downloaded": "Sauvegarde de la base de données téléchargée", - "Database imported successfully": "Base de données importée avec succès", - "Security": "Sécurité", - "Require login": "Exiger une connexion", - "When ON, dashboard requires password. When OFF, access without login.": "Lorsqu'il est ACTIVÉ, le tableau de bord nécessite un mot de passe. Lorsqu'il est DÉSACTIVÉ, accès sans connexion.", - "Current Password": "Mot de passe actuel", - "Enter current password": "Entrez le mot de passe actuel", - "New Password": "Nouveau mot de passe", - "Enter new password": "Entrez le nouveau mot de passe", - "Confirm New Password": "Confirmer le nouveau mot de passe", - "Confirm new password": "Confirmez le nouveau mot de passe", - "Update Password": "Mettre à jour le mot de passe", - "Set Password": "Définir le mot de passe", - "Password updated successfully": "Mot de passe mis à jour avec succès", - "Passwords do not match": "Les mots de passe ne correspondent pas", - "Routing Strategy": "Stratégie d'acheminement", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Parcourir les comptes pour distribuer la charge", - "Sticky Limit": "Limite collante", - "Calls per account before switching": "Appels par compte avant de changer", - "Network": "Réseau", - "Outbound Proxy": "Proxy sortant", - "Enable proxy for OAuth + provider outbound requests.": "Activez le proxy pour OAuth + les demandes sortantes du fournisseur.", - "Proxy URL": "URL du proxy", - "Leave empty to inherit existing env proxy (if any).": "Laissez vide pour hériter du proxy env existant (le cas échéant).", - "No Proxy": "Pas de proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Noms d'hôtes/domaines séparés par des virgules pour contourner le proxy.", - "Test proxy URL": "Tester l'URL du proxy", - "Apply": "Appliquer", - "Proxy settings applied": "Paramètres de proxy appliqués", - "Proxy enabled": "Proxy activé", - "Proxy disabled": "Proxy désactivé", - "Proxy test OK": "Test de proxy OK", - "Proxy test failed": "Échec du test de proxy", - "Please enter a Proxy URL to test": "Veuillez entrer une URL de proxy à tester", - "Observability": "Observabilité", - "Enable Observability": "Activer l'observabilité", - "Turn request detail recording on/off globally": "Activer/désactiver l'enregistrement des détails de la requête globalement", - "Max Records": "Nombre maximum d'enregistrements", - "Maximum request detail records to keep (older records are auto-deleted)": "Nombre maximum d'enregistrements de détails de requête à conserver (les anciens enregistrements sont supprimés automatiquement)", - "Batch Size": "Taille du lot", - "Number of items to accumulate before writing to database (higher = better performance)": "Nombre d'éléments à accumuler avant d'écrire dans la base de données (plus élevé = meilleures performances)", - "Flush Interval (ms)": "Intervalle de vidage (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Temps maximum d'attente avant de vider le tampon (évite la perte de données en cas de faible trafic)", - "Max JSON Size (KB)": "Taille JSON maximale (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Taille maximale pour chaque champ JSON (demande/réponse) avant la troncature", - "All data stored on your machine": "Toutes les données stockées sur votre machine", - "MITM Server": "Serveur MITM", - "Running": "En cours d'exécution", - "Stopped": "Arrêté", - "Cert": "Certificat", - "Server": "Serveur", - "Purpose:": "Objectif :", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Utilisez Antigravity IDE et GitHub Copilot → avec N'IMPORTE QUEL fournisseur/modèle de 9Router", - "How it works:": "Comment ça marche :", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Demande Antigravity/Copilot IDE → Redirection DNS vers localhost:443 → Le proxy MITM intercepte → 9Router → réponse à Antigravity/Copilot", - "API Key": "Clé API", - "No API keys — create one in Keys page": "Aucune clé API — créez-en une dans la page Clés", - "sk_9router (default)": "sk_9router (par défaut)", - "Server started": "Serveur démarré", - "Failed to start server": "Impossible de démarrer le serveur", - "Server stopped — all DNS cleared": "Serveur arrêté — tout DNS effacé", - "Failed to stop server": "Impossible d'arrêter le serveur", - "Sudo password is required": "Le mot de passe sudo est requis", - "Stop Server": "Arrêter le serveur", - "Start Server": "Démarrer le serveur", - "Enable DNS per tool below to activate interception": "Activez le DNS pour chaque outil ci-dessous pour activer l'interception", - "Sudo Password Required": "Mot de passe Sudo requis", - "Enter your sudo password to start/stop MITM server": "Entrez votre mot de passe sudo pour démarrer/arrêter le serveur MITM", - "Sudo Password": "Mot de passe sudo", - "Confirm": "Confirmer" -} diff --git a/9router/public/i18n/literals/he.json b/9router/public/i18n/literals/he.json deleted file mode 100644 index bb519b3c..00000000 --- a/9router/public/i18n/literals/he.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "ביטול", - "Delete": "מחק", - "Edit": "עריכה", - "Save": "שמור", - "Close": "סגור", - "Add": "הוספה", - "Remove": "הסרה", - "Settings": "הגדרות", - "Profile": "פרופיל", - "Dashboard": "לוח בקרה", - "Logout": "התנתקות", - "Login": "כניסה", - "Providers": "ספקים", - "Usage": "שימוש", - "API Key": "מפתח API", - "Connected": "מחובר", - "Disconnected": "מנותק", - "Active": "פעיל", - "Inactive": "לא פעיל", - "Success": "הצלחה", - "Failed": "נכשל", - "Error": "שגיאה", - "Warning": "אזהרה", - "Info": "מידע", - "Loading": "טוען", - "Search": "חיפוש", - "Filter": "סינון", - "Sort": "מיון", - "Export": "ייצוא", - "Import": "ייבוא", - "Refresh": "רענן", - "Back": "חזור", - "Next": "הבא", - "Previous": "הקודם", - "Submit": "שלח", - "Confirm": "אישור", - "Yes": "כן", - "No": "לא", - "OK": "אישור", - "Apply": "החל", - "Reset": "אפס", - "Clear": "נקה", - "Select": "בחר", - "Upload": "העלאה", - "Download": "הורדה", - "Copy": "העתק", - "Paste": "הדבק", - "Cut": "גזור", - "Undo": "ביטול", - "Redo": "חזור על", - "Name": "שם", - "Description": "תיאור", - "Status": "סטטוס", - "Type": "סוג", - "Date": "תאריך", - "Time": "זמן", - "Created": "נוצר", - "Updated": "עודכן", - "Actions": "פעולות", - "Details": "פרטים", - "View": "צפה", - "New": "חדש", - "Total": "סה\"כ", - "Count": "ספירה", - "Price": "מחיר", - "Cost": "עלות", - "Free": "חינם", - "Paid": "בתשלום", - "Enable": "הפעל", - "Disable": "כבה", - "Enabled": "הופעל", - "Disabled": "מבוטל", - "Online": "מחובר", - "Offline": "לא מחובר", - "Available": "זמין", - "Unavailable": "לא זמין", - "Required": "נדרש", - "Optional": "אופציונלי", - "Default": "ברירת מחדל", - "Custom": "מותאם", - "Advanced": "מתקדם", - "Basic": "בסיסי", - "Help": "עזרה", - "Support": "תמיכה", - "Documentation": "תיעוד", - "Version": "גרסה", - "Language": "שפה", - "Theme": "עיצוב", - "Light": "בהיר", - "Dark": "אפל", - "Auto": "אוטומטי", - "Endpoint": "נקודת קצה", - "Providers": "ספקים", - "Combos": "שילובים", - "Usage": "סטטיסטיקה", - "Quota Tracker": "עוקב הקצאה", - "MITM": "MITM", - "CLI Tools": "כלים CLI", - "Console Log": "יומן קונסול", - "System": "מערכת", - "Debug": "ניפוי שגיאות", - "Shutdown": "כיבוי", - "Close Proxy": "סגור פרוקסי", - "Are you sure you want to close the proxy server?": "האם אתה בטוח שברצונך לסגור את שרת הפרוקסי?", - "Server Disconnected": "השרת מנותק", - "The proxy server has been stopped.": "שרת הפרוקסי הופסק.", - "Reload Page": "טען מחדש את הדף", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "השירות פועל בטרמינל. אתה יכול לסגור את דף אינטרנט זה. כיבוי יעצור את השירות.", - "Manage your AI provider connections": "נהל את חיבורי ספק ה-AI שלך", - "Model combos with fallback": "שילובי מודל עם התחזוקה", - "Monitor your API usage, token consumption, and request logs": "עקוב אחרי השימוש ב-API, צריכת אסימונים ויומני בקשה", - "Intercept CLI tool traffic and route through 9Router": "תקוף את תנועת כלי CLI וניתוב דרך 9Router", - "Configure CLI tools": "הגדר כלים CLI", - "API endpoint configuration": "הגדרת נקודת קצה של API", - "Manage your preferences": "נהל את העדפותיך", - "Debug translation flow between formats": "ניפוי זרם התרגום בין פורמטים", - "Live server console output": "פלט קונסול שרת חי", - "Create model combos with fallback support": "יצור שילובי מודל עם תמיכה בהתחזוקה", - "Local Mode": "מצב מקומי", - "Running on your machine": "רץ על המחשב שלך", - "Database Location": "מיקום מסד הנתונים", - "Download Backup": "הורד גיבוי", - "Import Backup": "ייבא גיבוי", - "Database backup downloaded": "גיבוי מסד הנתונים הורד", - "Database imported successfully": "מסד הנתונים יובא בהצלחה", - "Security": "אבטחה", - "Require login": "דרוש כניסה", - "When ON, dashboard requires password. When OFF, access without login.": "כאשר כבוי, לוח הבקרה דורש סיסמה. כאשר מכובה, גישה ללא כניסה.", - "Current Password": "סיסמה נוכחית", - "Enter current password": "הזן את הסיסמה הנוכחית", - "New Password": "סיסמה חדשה", - "Enter new password": "הזן סיסמה חדשה", - "Confirm New Password": "אשר סיסמה חדשה", - "Confirm new password": "אשר סיסמה חדשה", - "Update Password": "עדכן סיסמה", - "Set Password": "הגדר סיסמה", - "Password updated successfully": "הסיסמה עודכנה בהצלחה", - "Passwords do not match": "הסיסמאות אינן תואמות", - "Routing Strategy": "אסטרטגיית ניתוב", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "מחזור בחשבונות לחלוקת עומס", - "Sticky Limit": "גבול דבוק", - "Calls per account before switching": "קריאות לפי חשבון לפני המעבר", - "Network": "רשת", - "Outbound Proxy": "פרוקסי יוצא", - "Enable proxy for OAuth + provider outbound requests.": "הפעל פרוקסי עבור בקשות יוצאות של OAuth + ספק.", - "Proxy URL": "URL פרוקסי", - "Leave empty to inherit existing env proxy (if any).": "השאר ריק כדי לרשת פרוקסי env קיים (אם יש).", - "No Proxy": "ללא פרוקסי", - "Comma-separated hostnames/domains to bypass the proxy.": "שמות משדר/תחומים מופרדים בפסיקים לעקיפת הפרוקסי.", - "Test proxy URL": "בדוק URL פרוקסי", - "Apply": "החל", - "Proxy settings applied": "הגדרות פרוקסי הופעלו", - "Proxy enabled": "פרוקסי הופעל", - "Proxy disabled": "פרוקסי מבוטל", - "Proxy test OK": "בדיקת פרוקסי בסדר", - "Proxy test failed": "בדיקת פרוקסי נכשלה", - "Please enter a Proxy URL to test": "אנא הזן URL פרוקסי לבדיקה", - "Observability": "יכולת תצפית", - "Enable Observability": "הפעל יכולת תצפית", - "Turn request detail recording on/off globally": "הפעל/כבה הקלטת פרטי בקשה בעולם", - "Max Records": "מרבי רשומות", - "Maximum request detail records to keep (older records are auto-deleted)": "מרבי רשומות פרטי בקשה לשמור (רשומות ישנות יותר נמחקות באופן אוטומטי)", - "Batch Size": "גודל אצווה", - "Number of items to accumulate before writing to database (higher = better performance)": "מספר הפריטים להצטברות לפני הכתיבה למסד הנתונים (גבוה יותר = ביצועים טובים יותר)", - "Flush Interval (ms)": "מרווח שטיפה (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "זמן מקסימום להמתנה לפני שטיפת ביפר (מונע הפסד נתונים תחת תנועה נמוכה)", - "Max JSON Size (KB)": "גודל JSON מקסימלי (KB)", - "Maximum size for each JSON field (request/response) before truncation": "גודל מקסימלי לכל שדה JSON (בקשה/תגובה) לפני חיתוך", - "All data stored on your machine": "כל הנתונים מאוחסנים במחשב שלך", - "MITM Server": "שרת MITM", - "Running": "רץ", - "Stopped": "עצור", - "Cert": "תעודה", - "Server": "שרת", - "Purpose:": "מטרה:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "השתמש ב-Antigravity IDE ו-GitHub Copilot → עם כל ספק/מודל מ-9Router", - "How it works:": "איך זה עובד:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "בקשת Antigravity/Copilot IDE → הפניה DNS ל-localhost:443 → פרוקסי MITM חוטף → 9Router → תגובה ל-Antigravity/Copilot", - "API Key": "מפתח API", - "No API keys — create one in Keys page": "אין מפתחות API — צור אחד בעמוד Keys", - "sk_9router (default)": "sk_9router (ברירת מחדל)", - "Server started": "השרת התחיל", - "Failed to start server": "הפעלת השרת נכשלה", - "Server stopped — all DNS cleared": "השרת הופסק — כל ה-DNS נוקה", - "Failed to stop server": "עצירת השרת נכשלה", - "Sudo password is required": "נדרשת סיסמת sudo", - "Stop Server": "עצור שרת", - "Start Server": "הפעל שרת", - "Enable DNS per tool below to activate interception": "הפעל DNS לכל כלי למטה להפעלת היירוט", - "Sudo Password Required": "סיסמת Sudo נדרשת", - "Enter your sudo password to start/stop MITM server": "הזן את סיסמת sudo שלך כדי להתחיל/עצור שרת MITM", - "Sudo Password": "סיסמת Sudo", - "Confirm": "אישור" -} diff --git a/9router/public/i18n/literals/hi.json b/9router/public/i18n/literals/hi.json deleted file mode 100644 index 25bf5f04..00000000 --- a/9router/public/i18n/literals/hi.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "रद्द करें", - "Delete": "हटाएं", - "Edit": "संपादित करें", - "Save": "सहेजें", - "Close": "बंद करें", - "Add": "जोड़ें", - "Remove": "निकालें", - "Settings": "सेटिंग्स", - "Profile": "प्रोफ़ाइल", - "Dashboard": "डैशबोर्ड", - "Logout": "लॉग आउट", - "Login": "लॉगिन", - "Providers": "प्रदाता", - "Usage": "उपयोग", - "API Key": "API कुंजी", - "Connected": "जुड़ा हुआ", - "Disconnected": "डिस्कनेक्ट किया गया", - "Active": "सक्रिय", - "Inactive": "निष्क्रिय", - "Success": "सफल", - "Failed": "विफल", - "Error": "त्रुटि", - "Warning": "चेतावनी", - "Info": "जानकारी", - "Loading": "लोड हो रहा है", - "Search": "खोज", - "Filter": "फिल्टर", - "Sort": "सॉर्ट करें", - "Export": "निर्यात", - "Import": "आयात", - "Refresh": "रीफ्रेश करें", - "Back": "वापस", - "Next": "आगे", - "Previous": "पिछला", - "Submit": "जमा करें", - "Confirm": "पुष्टि करें", - "Yes": "हां", - "No": "नहीं", - "OK": "ठीक है", - "Apply": "लागू करें", - "Reset": "रीसेट करें", - "Clear": "साफ़ करें", - "Select": "चुनें", - "Upload": "अपलोड करें", - "Download": "डाउनलोड करें", - "Copy": "कॉपी करें", - "Paste": "पेस्ट करें", - "Cut": "काटें", - "Undo": "पूर्ववत करें", - "Redo": "फिर से करें", - "Name": "नाम", - "Description": "विवरण", - "Status": "स्थिति", - "Type": "प्रकार", - "Date": "तारीख", - "Time": "समय", - "Created": "बनाया गया", - "Updated": "अपडेट किया गया", - "Actions": "कार्य", - "Details": "विवरण", - "View": "देखें", - "New": "नया", - "Total": "कुल", - "Count": "गिनती", - "Price": "कीमत", - "Cost": "लागत", - "Free": "मुक्त", - "Paid": "भुगतान किया गया", - "Enable": "सक्षम करें", - "Disable": "अक्षम करें", - "Enabled": "सक्षम", - "Disabled": "अक्षम", - "Online": "ऑनलाइन", - "Offline": "ऑफ़लाइन", - "Available": "उपलब्ध", - "Unavailable": "अनुपलब्ध", - "Required": "आवश्यक", - "Optional": "वैकल्पिक", - "Default": "डिफ़ॉल्ट", - "Custom": "कस्टम", - "Advanced": "उन्नत", - "Basic": "बुनियादी", - "Help": "मदद", - "Support": "समर्थन", - "Documentation": "दस्तावेज़", - "Version": "संस्करण", - "Language": "भाषा", - "Theme": "थीम", - "Light": "हल्का", - "Dark": "अंधेरा", - "Auto": "स्वचालित", - "Endpoint": "एंडपॉइंट", - "Providers": "प्रदाता", - "Combos": "कॉम्बो", - "Usage": "उपयोग के आंकड़े", - "Quota Tracker": "कोटा ट्रैकर", - "MITM": "MITM", - "CLI Tools": "उपकरण", - "Console Log": "कंसोल लॉग", - "System": "प्रणाली", - "Debug": "डीबग", - "Shutdown": "बंद करें", - "Close Proxy": "प्रॉक्सी बंद करें", - "Are you sure you want to close the proxy server?": "क्या आप वाकई प्रॉक्सी सर्वर को बंद करना चाहते हैं?", - "Server Disconnected": "सर्वर डिस्कनेक्ट किया गया", - "The proxy server has been stopped.": "प्रॉक्सी सर्वर को बंद कर दिया गया है।", - "Reload Page": "पृष्ठ को पुनः लोड करें", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "सेवा टर्मिनल में चल रही है। आप इस वेब पृष्ठ को बंद कर सकते हैं। शटडाउन सेवा को बंद कर देगा।", - "Manage your AI provider connections": "अपने AI प्रदाता कनेक्शन प्रबंधित करें", - "Model combos with fallback": "फॉलबैक के साथ मॉडल कॉम्बो", - "Monitor your API usage, token consumption, and request logs": "अपने API उपयोग, टोकन खपत और अनुरोध लॉग की निगरानी करें", - "Intercept CLI tool traffic and route through 9Router": "CLI टूल ट्रैफिक को इंटरसेप्ट करें और 9Router के माध्यम से रूट करें", - "Configure CLI tools": "CLI उपकरण कॉन्फ़िगर करें", - "API endpoint configuration": "API एंडपॉइंट कॉन्फ़िगरेशन", - "Manage your preferences": "अपनी प्राथमिकताएं प्रबंधित करें", - "Debug translation flow between formats": "फॉर्मेट के बीच अनुवाद प्रवाह डीबग करें", - "Live server console output": "लाइव सर्वर कंसोल आउटपुट", - "Create model combos with fallback support": "फॉलबैक समर्थन के साथ मॉडल कॉम्बो बनाएं", - "Local Mode": "स्थानीय मोड", - "Running on your machine": "आपकी मशीन पर चल रहा है", - "Database Location": "डेटाबेस स्थान", - "Download Backup": "बैकअप डाउनलोड करें", - "Import Backup": "बैकअप आयात करें", - "Database backup downloaded": "डेटाबेस बैकअप डाउनलोड किया गया", - "Database imported successfully": "डेटाबेस सफलतापूर्वक आयात किया गया", - "Security": "सुरक्षा", - "Require login": "लॉगिन की आवश्यकता है", - "When ON, dashboard requires password. When OFF, access without login.": "चालू होने पर, डैशबोर्ड को पासवर्ड की आवश्यकता होती है। बंद होने पर, लॉगिन के बिना एक्सेस करें।", - "Current Password": "वर्तमान पासवर्ड", - "Enter current password": "वर्तमान पासवर्ड दर्ज करें", - "New Password": "नया पासवर्ड", - "Enter new password": "नया पासवर्ड दर्ज करें", - "Confirm New Password": "नए पासवर्ड की पुष्टि करें", - "Confirm new password": "नए पासवर्ड की पुष्टि करें", - "Update Password": "पासवर्ड अपडेट करें", - "Set Password": "पासवर्ड सेट करें", - "Password updated successfully": "पासवर्ड सफलतापूर्वक अपडेट किया गया", - "Passwords do not match": "पासवर्ड मेल नहीं खाते", - "Routing Strategy": "रूटिंग रणनीति", - "Round Robin": "राउंड रॉबिन", - "Cycle through accounts to distribute load": "लोड वितरित करने के लिए खातों के माध्यम से साइकिल चलाएं", - "Sticky Limit": "स्टिकी सीमा", - "Calls per account before switching": "स्विच करने से पहले प्रति खाते कॉल", - "Network": "नेटवर्क", - "Outbound Proxy": "आउटबाउंड प्रॉक्सी", - "Enable proxy for OAuth + provider outbound requests.": "OAuth + प्रदाता आउटबाउंड अनुरोधों के लिए प्रॉक्सी सक्षम करें।", - "Proxy URL": "प्रॉक्सी URL", - "Leave empty to inherit existing env proxy (if any).": "मौजूदा env प्रॉक्सी को इनहेरिट करने के लिए खाली छोड़ें (यदि कोई हो)।", - "No Proxy": "कोई प्रॉक्सी नहीं", - "Comma-separated hostnames/domains to bypass the proxy.": "प्रॉक्सी को बायपास करने के लिए अल्पविराम से अलग की गई होस्टनाम/डोमेन।", - "Test proxy URL": "प्रॉक्सी URL का परीक्षण करें", - "Apply": "लागू करें", - "Proxy settings applied": "प्रॉक्सी सेटिंग्स लागू की गई", - "Proxy enabled": "प्रॉक्सी सक्षम", - "Proxy disabled": "प्रॉक्सी अक्षम", - "Proxy test OK": "प्रॉक्सी परीक्षण ठीक है", - "Proxy test failed": "प्रॉक्सी परीक्षण विफल", - "Please enter a Proxy URL to test": "परीक्षण के लिए कृपया एक प्रॉक्सी URL दर्ज करें", - "Observability": "पर्यवेक्षणीयता", - "Enable Observability": "पर्यवेक्षणीयता सक्षम करें", - "Turn request detail recording on/off globally": "अनुरोध विवरण रिकॉर्डिंग को विश्व स्तर पर चालू/बंद करें", - "Max Records": "अधिकतम रिकॉर्ड", - "Maximum request detail records to keep (older records are auto-deleted)": "रखने के लिए अधिकतम अनुरोध विवरण रिकॉर्ड (पुराने रिकॉर्ड स्वचालित रूप से हटाए जाते हैं)", - "Batch Size": "बैच आकार", - "Number of items to accumulate before writing to database (higher = better performance)": "डेटाबेस में लिखने से पहले जमा करने के लिए आइटम की संख्या (अधिक = बेहतर प्रदर्शन)", - "Flush Interval (ms)": "फ्लश अंतराल (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "बफर को फ्लश करने से पहले प्रतीक्षा करने का अधिकतम समय (कम ट्रैफिक के दौरान डेटा नुकसान को रोकता है)", - "Max JSON Size (KB)": "अधिकतम JSON आकार (KB)", - "Maximum size for each JSON field (request/response) before truncation": "ट्रंकेशन से पहले प्रत्येक JSON फ़ील्ड (अनुरोध/प्रतिक्रिया) के लिए अधिकतम आकार", - "All data stored on your machine": "आपकी मशीन पर सभी डेटा संग्रहीत है", - "MITM Server": "MITM सर्वर", - "Running": "चल रहा है", - "Stopped": "रुका हुआ", - "Cert": "प्रमाणपत्र", - "Server": "सर्वर", - "Purpose:": "उद्देश्य:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Antigravity IDE और GitHub Copilot का उपयोग करें → 9Router से किसी भी प्रदाता/मॉडल के साथ", - "How it works:": "यह कैसे काम करता है:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE अनुरोध → DNS को localhost:443 में पुनर्निर्देशित करें → MITM प्रॉक्सी इंटरसेप्ट करता है → 9Router → Antigravity/Copilot को प्रतिक्रिया", - "API Key": "API कुंजी", - "No API keys — create one in Keys page": "कोई API कुंजी नहीं — Keys पृष्ठ में एक बनाएं", - "sk_9router (default)": "sk_9router (डिफ़ॉल्ट)", - "Server started": "सर्वर शुरू किया गया", - "Failed to start server": "सर्वर शुरू करने में विफल", - "Server stopped — all DNS cleared": "सर्वर बंद — सभी DNS साफ़ किए गए", - "Failed to stop server": "सर्वर को रोकने में विफल", - "Sudo password is required": "Sudo पासवर्ड की आवश्यकता है", - "Stop Server": "सर्वर बंद करें", - "Start Server": "सर्वर शुरू करें", - "Enable DNS per tool below to activate interception": "इंटरसेप्शन को सक्रिय करने के लिए नीचे प्रत्येक उपकरण के लिए DNS सक्षम करें", - "Sudo Password Required": "Sudo पासवर्ड आवश्यक है", - "Enter your sudo password to start/stop MITM server": "MITM सर्वर शुरू/रोकने के लिए अपना sudo पासवर्ड दर्ज करें", - "Sudo Password": "Sudo पासवर्ड", - "Confirm": "पुष्टि करें" -} diff --git a/9router/public/i18n/literals/hu.json b/9router/public/i18n/literals/hu.json deleted file mode 100644 index 05954a0e..00000000 --- a/9router/public/i18n/literals/hu.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Mégse", - "Delete": "Törlés", - "Edit": "Szerkesztés", - "Save": "Mentés", - "Close": "Bezárás", - "Add": "Hozzáadás", - "Remove": "Eltávolítás", - "Settings": "Beállítások", - "Profile": "Profil", - "Dashboard": "Irányítópult", - "Logout": "Kijelentkezés", - "Login": "Bejelentkezés", - "Providers": "Szolgáltatók", - "Usage": "Használat", - "API Key": "API-kulcs", - "Connected": "Csatlakoztatva", - "Disconnected": "Leválasztva", - "Active": "Aktív", - "Inactive": "Inaktív", - "Success": "Siker", - "Failed": "Sikertelen", - "Error": "Hiba", - "Warning": "Figyelmeztetés", - "Info": "Információ", - "Loading": "Betöltés", - "Search": "Keresés", - "Filter": "Szűrő", - "Sort": "Rendezés", - "Export": "Exportálás", - "Import": "Importálás", - "Refresh": "Frissítés", - "Back": "Vissza", - "Next": "Következő", - "Previous": "Előző", - "Submit": "Küldés", - "Confirm": "Megerősítés", - "Yes": "Igen", - "No": "Nem", - "OK": "OK", - "Apply": "Alkalmazás", - "Reset": "Visszaállítás", - "Clear": "Törlés", - "Select": "Kiválasztás", - "Upload": "Feltöltés", - "Download": "Letöltés", - "Copy": "Másolás", - "Paste": "Beillesztés", - "Cut": "Kivágás", - "Undo": "Visszavonás", - "Redo": "Ismét", - "Name": "Név", - "Description": "Leírás", - "Status": "Állapot", - "Type": "Típus", - "Date": "Dátum", - "Time": "Idő", - "Created": "Létrehozva", - "Updated": "Frissítve", - "Actions": "Műveletek", - "Details": "Részletek", - "View": "Megtekintés", - "New": "Új", - "Total": "Összes", - "Count": "Darabszám", - "Price": "Ár", - "Cost": "Költség", - "Free": "Ingyenes", - "Paid": "Fizetős", - "Enable": "Engedélyezés", - "Disable": "Letiltás", - "Enabled": "Engedélyezve", - "Disabled": "Letiltva", - "Online": "Online", - "Offline": "Offline", - "Available": "Elérhető", - "Unavailable": "Nem elérhető", - "Required": "Kötelező", - "Optional": "Opcionális", - "Default": "Alapértelmezett", - "Custom": "Egyéni", - "Advanced": "Haladó", - "Basic": "Alapvető", - "Help": "Súgó", - "Support": "Támogatás", - "Documentation": "Dokumentáció", - "Version": "Verzió", - "Language": "Nyelv", - "Theme": "Téma", - "Light": "Világos", - "Dark": "Sötét", - "Auto": "Automatikus", - "Endpoint": "Végpont", - "Providers": "Szolgáltatók", - "Combos": "Kombinációk", - "Usage": "Használati statisztika", - "Quota Tracker": "Kvóta nyomkövetés", - "MITM": "MITM", - "CLI Tools": "Eszközök", - "Console Log": "Konzol napló", - "System": "Rendszer", - "Debug": "Hibakeresés", - "Shutdown": "Leállítás", - "Close Proxy": "Proxy bezárása", - "Are you sure you want to close the proxy server?": "Biztosan le akarja zárni a proxy szervert?", - "Server Disconnected": "Szerver leválasztva", - "The proxy server has been stopped.": "A proxy szerver leállt.", - "Reload Page": "Oldal újratöltése", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "A szolgáltatás a terminálon fut. Bezárhatja ezt a weboldalt. A leállítás megállítja a szolgáltatást.", - "Manage your AI provider connections": "Az AI-szolgáltatók kapcsolatainak kezelése", - "Model combos with fallback": "Modell kombinációk tartalékkal", - "Monitor your API usage, token consumption, and request logs": "Az API-használat, a token-fogyasztás és a kérési naplók figyelése", - "Intercept CLI tool traffic and route through 9Router": "CLI-eszköz forgalmának elfogása és az 9Router-en keresztüli irányítása", - "Configure CLI tools": "CLI-eszközök konfigurálása", - "API endpoint configuration": "API-végpont konfigurálása", - "Manage your preferences": "Előnyzeteinek kezelése", - "Debug translation flow between formats": "A fordítási folyamat hibakeresése a formátumok között", - "Live server console output": "Élő kiszolgáló konzol kimenete", - "Create model combos with fallback support": "Modell kombinációk létrehozása tartalék támogatással", - "Local Mode": "Helyi mód", - "Running on your machine": "A gépén futó", - "Database Location": "Adatbázis helye", - "Download Backup": "Biztonsági másolat letöltése", - "Import Backup": "Biztonsági másolat importálása", - "Database backup downloaded": "Adatbázis biztonsági másolat letöltve", - "Database imported successfully": "Az adatbázis sikeresen importálva", - "Security": "Biztonság", - "Require login": "Bejelentkezés szükséges", - "When ON, dashboard requires password. When OFF, access without login.": "Ha BEKAPCSOLT, az irányítópulthoz jelszó szükséges. Ha KIKAPCSOLT, bejelentkezés nélkül is hozzáférhet.", - "Current Password": "Jelenlegi jelszó", - "Enter current password": "Adja meg a jelenlegi jelszót", - "New Password": "Új jelszó", - "Enter new password": "Adja meg az új jelszót", - "Confirm New Password": "Új jelszó megerősítése", - "Confirm new password": "Erősítse meg az új jelszót", - "Update Password": "Jelszó frissítése", - "Set Password": "Jelszó beállítása", - "Password updated successfully": "A jelszó sikeresen frissítve", - "Passwords do not match": "A jelszavak nem egyeznek", - "Routing Strategy": "Útválasztási stratégia", - "Round Robin": "Fordított körforgalom", - "Cycle through accounts to distribute load": "Ciklikus váltakozás a fiókok között a terhelés elosztásához", - "Sticky Limit": "Ragadós korlát", - "Calls per account before switching": "Hívások fiókonként a váltás előtt", - "Network": "Hálózat", - "Outbound Proxy": "Kimenő proxy", - "Enable proxy for OAuth + provider outbound requests.": "Engedélyezze a proxy-t OAuth + szolgáltató kimenő kérésekhez.", - "Proxy URL": "Proxy URL", - "Leave empty to inherit existing env proxy (if any).": "Hagyja üresen a meglévő env proxy örökléséhez (ha van).", - "No Proxy": "Nincs proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Vesszővel elválasztott gazdanév/tartomány a proxy megkerüléséhez.", - "Test proxy URL": "Proxy URL-cím tesztelése", - "Apply": "Alkalmazás", - "Proxy settings applied": "Proxy beállítások alkalmazva", - "Proxy enabled": "Proxy engedélyezve", - "Proxy disabled": "Proxy letiltva", - "Proxy test OK": "Proxy teszt OK", - "Proxy test failed": "Proxy teszt sikertelen", - "Please enter a Proxy URL to test": "Kérjük, adjon meg egy Proxy URL-t teszteléshez", - "Observability": "Megfigyelhetőség", - "Enable Observability": "Megfigyelhetőség engedélyezése", - "Turn request detail recording on/off globally": "Kérés részleteinak rögzítésének be/kikapcsolása globálisan", - "Max Records": "Maximális rekordok", - "Maximum request detail records to keep (older records are auto-deleted)": "Maximális kérés részleteit tartalmaz (a régebbi rekordok automatikusan törlődnek)", - "Batch Size": "Köteg mérete", - "Number of items to accumulate before writing to database (higher = better performance)": "Az adatbázisba írás előtt felhalmozandó elemek száma (magasabb = jobb teljesítmény)", - "Flush Interval (ms)": "Kiürítési intervallum (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maximális várakozási idő a puffer kiürítése előtt (megelőzi az adatvesztést alacsony forgalom alatt)", - "Max JSON Size (KB)": "Maximális JSON méret (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maximális méret az egyes JSON-mezőkhöz (kérés/válasz) a csonkítás előtt", - "All data stored on your machine": "Az összes adat a gépén tárolt", - "MITM Server": "MITM szerver", - "Running": "Futó", - "Stopped": "Leállítva", - "Cert": "Tanúsítvány", - "Server": "Kiszolgáló", - "Purpose:": "Cél:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Antigravity IDE és GitHub Copilot használata → az 9Router bármelyik szolgáltatójával/modelljével", - "How it works:": "Hogyan működik:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE kérés → DNS átirányítás a localhost:443-ra → MITM proxy elfogja → 9Router → válasz Antigravity/Copilot-nak", - "API Key": "API-kulcs", - "No API keys — create one in Keys page": "Nincsenek API-kulcsok — hozzon létre egyet a Keys oldalon", - "sk_9router (default)": "sk_9router (alapértelmezett)", - "Server started": "Szerver elindult", - "Failed to start server": "Nem sikerült elindítani a szervert", - "Server stopped — all DNS cleared": "A szerver leállt — az összes DNS törlésre kerül", - "Failed to stop server": "Nem sikerült leállítani a szervert", - "Sudo password is required": "Sudo jelszó szükséges", - "Stop Server": "Szerver leállítása", - "Start Server": "Szerver indítása", - "Enable DNS per tool below to activate interception": "Engedélyezze az alábbi DNS-t az elfogás aktiválásához", - "Sudo Password Required": "Sudo jelszó szükséges", - "Enter your sudo password to start/stop MITM server": "Adja meg sudo jelszavát a MITM szerver indításához/leállításához", - "Sudo Password": "Sudo jelszó", - "Confirm": "Megerősítés" -} diff --git a/9router/public/i18n/literals/id.json b/9router/public/i18n/literals/id.json deleted file mode 100644 index 9d8e26e1..00000000 --- a/9router/public/i18n/literals/id.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Batalkan", - "Delete": "Hapus", - "Edit": "Sunting", - "Save": "Simpan", - "Close": "Tutup", - "Add": "Tambah", - "Remove": "Hapus", - "Settings": "Pengaturan", - "Profile": "Profil", - "Dashboard": "Dasbor", - "Logout": "Keluar", - "Login": "Masuk", - "Providers": "Penyedia", - "Usage": "Penggunaan", - "API Key": "Kunci API", - "Connected": "Terhubung", - "Disconnected": "Terputus", - "Active": "Aktif", - "Inactive": "Nonaktif", - "Success": "Berhasil", - "Failed": "Gagal", - "Error": "Kesalahan", - "Warning": "Peringatan", - "Info": "Informasi", - "Loading": "Memuat", - "Search": "Cari", - "Filter": "Saring", - "Sort": "Urutkan", - "Export": "Ekspor", - "Import": "Impor", - "Refresh": "Segarkan", - "Back": "Kembali", - "Next": "Berikutnya", - "Previous": "Sebelumnya", - "Submit": "Kirim", - "Confirm": "Konfirmasi", - "Yes": "Ya", - "No": "Tidak", - "OK": "OK", - "Apply": "Terapkan", - "Reset": "Atur Ulang", - "Clear": "Hapus", - "Select": "Pilih", - "Upload": "Unggah", - "Download": "Unduh", - "Copy": "Salin", - "Paste": "Tempel", - "Cut": "Potong", - "Undo": "Batalkan", - "Redo": "Ulangi", - "Name": "Nama", - "Description": "Deskripsi", - "Status": "Status", - "Type": "Jenis", - "Date": "Tanggal", - "Time": "Waktu", - "Created": "Dibuat", - "Updated": "Diperbarui", - "Actions": "Tindakan", - "Details": "Detail", - "View": "Lihat", - "New": "Baru", - "Total": "Total", - "Count": "Jumlah", - "Price": "Harga", - "Cost": "Biaya", - "Free": "Gratis", - "Paid": "Berbayar", - "Enable": "Aktifkan", - "Disable": "Nonaktifkan", - "Enabled": "Diaktifkan", - "Disabled": "Dinonaktifkan", - "Online": "Daring", - "Offline": "Luring", - "Available": "Tersedia", - "Unavailable": "Tidak Tersedia", - "Required": "Diperlukan", - "Optional": "Opsional", - "Default": "Bawaan", - "Custom": "Kustom", - "Advanced": "Lanjutan", - "Basic": "Dasar", - "Help": "Bantuan", - "Support": "Dukungan", - "Documentation": "Dokumentasi", - "Version": "Versi", - "Language": "Bahasa", - "Theme": "Tema", - "Light": "Terang", - "Dark": "Gelap", - "Auto": "Otomatis", - "Endpoint": "Titik Akhir", - "Providers": "Penyedia", - "Combos": "Kombinasi", - "Usage": "Statistik Penggunaan", - "Quota Tracker": "Pelacak Kuota", - "MITM": "MITM", - "CLI Tools": "Alat", - "Console Log": "Log Konsol", - "System": "Sistem", - "Debug": "Debug", - "Shutdown": "Matikan", - "Close Proxy": "Tutup Proxy", - "Are you sure you want to close the proxy server?": "Apakah Anda yakin ingin menutup server proxy?", - "Server Disconnected": "Server Terputus", - "The proxy server has been stopped.": "Server proxy telah dihentikan.", - "Reload Page": "Muat Ulang Halaman", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Layanan sedang berjalan di terminal. Anda dapat menutup halaman web ini. Shutdown akan menghentikan layanan.", - "Manage your AI provider connections": "Kelola koneksi penyedia AI Anda", - "Model combos with fallback": "Kombinasi model dengan fallback", - "Monitor your API usage, token consumption, and request logs": "Pantau penggunaan API, konsumsi token, dan log permintaan Anda", - "Intercept CLI tool traffic and route through 9Router": "Intersep lalu lintas alat CLI dan rute melalui 9Router", - "Configure CLI tools": "Konfigurasi alat CLI", - "API endpoint configuration": "Konfigurasi titik akhir API", - "Manage your preferences": "Kelola preferensi Anda", - "Debug translation flow between formats": "Debug alur terjemahan antara format", - "Live server console output": "Output konsol server langsung", - "Create model combos with fallback support": "Buat kombinasi model dengan dukungan fallback", - "Local Mode": "Mode Lokal", - "Running on your machine": "Berjalan di mesin Anda", - "Database Location": "Lokasi Database", - "Download Backup": "Unduh Cadangan", - "Import Backup": "Impor Cadangan", - "Database backup downloaded": "Cadangan database telah diunduh", - "Database imported successfully": "Database berhasil diimpor", - "Security": "Keamanan", - "Require login": "Memerlukan Login", - "When ON, dashboard requires password. When OFF, access without login.": "Ketika AKTIF, dasbor memerlukan kata sandi. Ketika NONAKTIF, akses tanpa login.", - "Current Password": "Kata Sandi Saat Ini", - "Enter current password": "Masukkan kata sandi saat ini", - "New Password": "Kata Sandi Baru", - "Enter new password": "Masukkan kata sandi baru", - "Confirm New Password": "Konfirmasi Kata Sandi Baru", - "Confirm new password": "Konfirmasi kata sandi baru", - "Update Password": "Perbarui Kata Sandi", - "Set Password": "Atur Kata Sandi", - "Password updated successfully": "Kata sandi berhasil diperbarui", - "Passwords do not match": "Kata sandi tidak cocok", - "Routing Strategy": "Strategi Rute", - "Round Robin": "Putaran Bulat", - "Cycle through accounts to distribute load": "Siklus melalui akun untuk mendistribusikan beban", - "Sticky Limit": "Batas Lengket", - "Calls per account before switching": "Panggilan per akun sebelum beralih", - "Network": "Jaringan", - "Outbound Proxy": "Proxy Keluar", - "Enable proxy for OAuth + provider outbound requests.": "Aktifkan proxy untuk permintaan keluar OAuth + penyedia.", - "Proxy URL": "URL Proxy", - "Leave empty to inherit existing env proxy (if any).": "Biarkan kosong untuk mewarisi proxy env yang ada (jika ada).", - "No Proxy": "Tidak Ada Proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Nama host/domain yang dipisahkan koma untuk melewati proxy.", - "Test proxy URL": "Uji URL Proxy", - "Apply": "Terapkan", - "Proxy settings applied": "Pengaturan proxy diterapkan", - "Proxy enabled": "Proxy diaktifkan", - "Proxy disabled": "Proxy dinonaktifkan", - "Proxy test OK": "Tes proxy OK", - "Proxy test failed": "Tes proxy gagal", - "Please enter a Proxy URL to test": "Masukkan URL Proxy untuk diuji", - "Observability": "Observabilitas", - "Enable Observability": "Aktifkan Observabilitas", - "Turn request detail recording on/off globally": "Aktifkan/nonaktifkan pencatatan detail permintaan secara global", - "Max Records": "Rekam Maksimal", - "Maximum request detail records to keep (older records are auto-deleted)": "Rekam detail permintaan maksimal untuk disimpan (rekam lama otomatis dihapus)", - "Batch Size": "Ukuran Batch", - "Number of items to accumulate before writing to database (higher = better performance)": "Jumlah item yang diakumulasikan sebelum menulis ke database (lebih tinggi = performa lebih baik)", - "Flush Interval (ms)": "Interval Flush (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Waktu maksimum untuk menunggu sebelum mem-flush buffer (mencegah kehilangan data saat lalu lintas rendah)", - "Max JSON Size (KB)": "Ukuran JSON Maks (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Ukuran maksimum untuk setiap bidang JSON (permintaan/respons) sebelum pemotongan", - "All data stored on your machine": "Semua data disimpan di mesin Anda", - "MITM Server": "Server MITM", - "Running": "Berjalan", - "Stopped": "Dihentikan", - "Cert": "Sertifikat", - "Server": "Server", - "Purpose:": "Tujuan:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Gunakan Antigravity IDE & GitHub Copilot → dengan PENYEDIA/model APA PUN dari 9Router", - "How it works:": "Cara kerjanya:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Permintaan Antigravity/Copilot IDE → Pengalihan DNS ke localhost:443 → Proxy MITM mengintersep → 9Router → respons ke Antigravity/Copilot", - "API Key": "Kunci API", - "No API keys — create one in Keys page": "Tidak ada kunci API — buat satu di halaman Keys", - "sk_9router (default)": "sk_9router (bawaan)", - "Server started": "Server dimulai", - "Failed to start server": "Gagal memulai server", - "Server stopped — all DNS cleared": "Server dihentikan — semua DNS dihapus", - "Failed to stop server": "Gagal menghentikan server", - "Sudo password is required": "Kata sandi sudo diperlukan", - "Stop Server": "Hentikan Server", - "Start Server": "Mulai Server", - "Enable DNS per tool below to activate interception": "Aktifkan DNS untuk setiap alat di bawah untuk mengaktifkan intersepsi", - "Sudo Password Required": "Kata Sandi Sudo Diperlukan", - "Enter your sudo password to start/stop MITM server": "Masukkan kata sandi sudo Anda untuk memulai/menghentikan server MITM", - "Sudo Password": "Kata Sandi Sudo", - "Confirm": "Konfirmasi" -} diff --git a/9router/public/i18n/literals/it.json b/9router/public/i18n/literals/it.json deleted file mode 100644 index 18264329..00000000 --- a/9router/public/i18n/literals/it.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Annulla", - "Delete": "Elimina", - "Edit": "Modifica", - "Save": "Salva", - "Close": "Chiudi", - "Add": "Aggiungi", - "Remove": "Rimuovi", - "Settings": "Impostazioni", - "Profile": "Profilo", - "Dashboard": "Pannello di controllo", - "Logout": "Esci", - "Login": "Accedi", - "Providers": "Provider", - "Usage": "Utilizzo", - "API Key": "Chiave API", - "Connected": "Connesso", - "Disconnected": "Disconnesso", - "Active": "Attivo", - "Inactive": "Inattivo", - "Success": "Successo", - "Failed": "Non riuscito", - "Error": "Errore", - "Warning": "Avvertenza", - "Info": "Informazioni", - "Loading": "Caricamento", - "Search": "Cerca", - "Filter": "Filtro", - "Sort": "Ordina", - "Export": "Esporta", - "Import": "Importa", - "Refresh": "Aggiorna", - "Back": "Indietro", - "Next": "Avanti", - "Previous": "Precedente", - "Submit": "Invia", - "Confirm": "Conferma", - "Yes": "Sì", - "No": "No", - "OK": "OK", - "Apply": "Applica", - "Reset": "Ripristina", - "Clear": "Cancella", - "Select": "Seleziona", - "Upload": "Carica", - "Download": "Scarica", - "Copy": "Copia", - "Paste": "Incolla", - "Cut": "Taglia", - "Undo": "Annulla", - "Redo": "Ripeti", - "Name": "Nome", - "Description": "Descrizione", - "Status": "Stato", - "Type": "Tipo", - "Date": "Data", - "Time": "Ora", - "Created": "Creato", - "Updated": "Aggiornato", - "Actions": "Azioni", - "Details": "Dettagli", - "View": "Visualizza", - "New": "Nuovo", - "Total": "Totale", - "Count": "Conteggio", - "Price": "Prezzo", - "Cost": "Costo", - "Free": "Gratuito", - "Paid": "A pagamento", - "Enable": "Abilita", - "Disable": "Disabilita", - "Enabled": "Abilitato", - "Disabled": "Disabilitato", - "Online": "Online", - "Offline": "Offline", - "Available": "Disponibile", - "Unavailable": "Non disponibile", - "Required": "Obbligatorio", - "Optional": "Opzionale", - "Default": "Predefinito", - "Custom": "Personalizzato", - "Advanced": "Avanzate", - "Basic": "Di base", - "Help": "Aiuto", - "Support": "Supporto", - "Documentation": "Documentazione", - "Version": "Versione", - "Language": "Lingua", - "Theme": "Tema", - "Light": "Chiaro", - "Dark": "Scuro", - "Auto": "Automatico", - "Endpoint": "Endpoint", - "Providers": "Provider", - "Combos": "Combinazioni", - "Usage": "Statistiche di utilizzo", - "Quota Tracker": "Tracker quota", - "MITM": "MITM", - "CLI Tools": "Strumenti", - "Console Log": "Log della console", - "System": "Sistema", - "Debug": "Debug", - "Shutdown": "Spegni", - "Close Proxy": "Chiudi proxy", - "Are you sure you want to close the proxy server?": "Sei sicuro di voler chiudere il server proxy?", - "Server Disconnected": "Server disconnesso", - "The proxy server has been stopped.": "Il server proxy è stato arrestato.", - "Reload Page": "Ricarica pagina", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Il servizio è in esecuzione nel terminale. Puoi chiudere questa pagina web. Lo spegnimento fermerà il servizio.", - "Manage your AI provider connections": "Gestisci le connessioni del tuo provider AI", - "Model combos with fallback": "Combinazioni di modelli con fallback", - "Monitor your API usage, token consumption, and request logs": "Monitora l'utilizzo dell'API, il consumo di token e i log delle richieste", - "Intercept CLI tool traffic and route through 9Router": "Intercetta il traffico dello strumento CLI e instradalo attraverso 9Router", - "Configure CLI tools": "Configura gli strumenti CLI", - "API endpoint configuration": "Configurazione dell'endpoint API", - "Manage your preferences": "Gestisci le tue preferenze", - "Debug translation flow between formats": "Debug del flusso di traduzione tra i formati", - "Live server console output": "Output della console del server in tempo reale", - "Create model combos with fallback support": "Crea combinazioni di modelli con supporto fallback", - "Local Mode": "Modalità locale", - "Running on your machine": "In esecuzione sulla tua macchina", - "Database Location": "Posizione del database", - "Download Backup": "Scarica backup", - "Import Backup": "Importa backup", - "Database backup downloaded": "Backup del database scaricato", - "Database imported successfully": "Database importato con successo", - "Security": "Sicurezza", - "Require login": "Richiedi accesso", - "When ON, dashboard requires password. When OFF, access without login.": "Quando ATTIVO, il pannello di controllo richiede la password. Quando SPENTO, accedi senza login.", - "Current Password": "Password attuale", - "Enter current password": "Inserisci la password attuale", - "New Password": "Nuova password", - "Enter new password": "Inserisci la nuova password", - "Confirm New Password": "Conferma nuova password", - "Confirm new password": "Conferma la nuova password", - "Update Password": "Aggiorna password", - "Set Password": "Imposta password", - "Password updated successfully": "Password aggiornata con successo", - "Passwords do not match": "Le password non corrispondono", - "Routing Strategy": "Strategia di routing", - "Round Robin": "Round robin", - "Cycle through accounts to distribute load": "Scorri gli account per distribuire il carico", - "Sticky Limit": "Limite appiccicoso", - "Calls per account before switching": "Chiamate per account prima del passaggio", - "Network": "Rete", - "Outbound Proxy": "Proxy in uscita", - "Enable proxy for OAuth + provider outbound requests.": "Abilita il proxy per le richieste in uscita OAuth + provider.", - "Proxy URL": "URL proxy", - "Leave empty to inherit existing env proxy (if any).": "Lascia vuoto per ereditare il proxy env esistente (se presente).", - "No Proxy": "Nessun proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Nomi host/domini separati da virgole per ignorare il proxy.", - "Test proxy URL": "Test URL proxy", - "Apply": "Applica", - "Proxy settings applied": "Impostazioni proxy applicate", - "Proxy enabled": "Proxy abilitato", - "Proxy disabled": "Proxy disabilitato", - "Proxy test OK": "Test proxy OK", - "Proxy test failed": "Test proxy non riuscito", - "Please enter a Proxy URL to test": "Inserisci un URL proxy da testare", - "Observability": "Osservabilità", - "Enable Observability": "Abilita osservabilità", - "Turn request detail recording on/off globally": "Attiva/disattiva la registrazione dei dettagli della richiesta globalmente", - "Max Records": "Record massimi", - "Maximum request detail records to keep (older records are auto-deleted)": "Record di dettagli della richiesta massimi da mantenere (i record più vecchi vengono eliminati automaticamente)", - "Batch Size": "Dimensione batch", - "Number of items to accumulate before writing to database (higher = better performance)": "Numero di elementi da accumulare prima di scrivere nel database (più alto = migliori prestazioni)", - "Flush Interval (ms)": "Intervallo di scaricamento (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Tempo massimo di attesa prima dello scaricamento del buffer (previene la perdita di dati durante il traffico basso)", - "Max JSON Size (KB)": "Dimensione JSON massima (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Dimensione massima per ogni campo JSON (richiesta/risposta) prima del troncamento", - "All data stored on your machine": "Tutti i dati memorizzati sulla tua macchina", - "MITM Server": "Server MITM", - "Running": "In esecuzione", - "Stopped": "Arrestato", - "Cert": "Certificato", - "Server": "Server", - "Purpose:": "Scopo:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Usa Antigravity IDE & GitHub Copilot → con QUALSIASI provider/modello da 9Router", - "How it works:": "Come funziona:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Richiesta Antigravity/Copilot IDE → Reindirizzamento DNS a localhost:443 → Il proxy MITM intercetta → 9Router → Risposta a Antigravity/Copilot", - "API Key": "Chiave API", - "No API keys — create one in Keys page": "Nessuna chiave API — crearne una nella pagina Chiavi", - "sk_9router (default)": "sk_9router (predefinito)", - "Server started": "Server avviato", - "Failed to start server": "Impossibile avviare il server", - "Server stopped — all DNS cleared": "Server arrestato — tutti i DNS cancellati", - "Failed to stop server": "Impossibile arrestare il server", - "Sudo password is required": "La password sudo è obbligatoria", - "Stop Server": "Arresta server", - "Start Server": "Avvia server", - "Enable DNS per tool below to activate interception": "Abilita DNS per ogni strumento sottostante per attivare l'intercettazione", - "Sudo Password Required": "Password Sudo richiesta", - "Enter your sudo password to start/stop MITM server": "Inserisci la tua password sudo per avviare/arrestare il server MITM", - "Sudo Password": "Password Sudo", - "Confirm": "Conferma" -} diff --git a/9router/public/i18n/literals/ja.json b/9router/public/i18n/literals/ja.json deleted file mode 100644 index bfb63aa3..00000000 --- a/9router/public/i18n/literals/ja.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "キャンセル", - "Delete": "削除", - "Edit": "編集", - "Save": "保存", - "Close": "閉じる", - "Add": "追加", - "Remove": "削除", - "Settings": "設定", - "Profile": "プロフィール", - "Dashboard": "ダッシュボード", - "Logout": "ログアウト", - "Login": "ログイン", - "Providers": "プロバイダー", - "Usage": "使用状況", - "API Key": "APIキー", - "Connected": "接続済み", - "Disconnected": "未接続", - "Active": "アクティブ", - "Inactive": "非アクティブ", - "Success": "成功", - "Failed": "失敗", - "Error": "エラー", - "Warning": "警告", - "Info": "情報", - "Loading": "読み込み中", - "Search": "検索", - "Filter": "フィルター", - "Sort": "並べ替え", - "Export": "エクスポート", - "Import": "インポート", - "Refresh": "更新", - "Back": "戻る", - "Next": "次へ", - "Previous": "前へ", - "Submit": "送信", - "Confirm": "確認", - "Yes": "はい", - "No": "いいえ", - "OK": "OK", - "Apply": "適用", - "Reset": "リセット", - "Clear": "クリア", - "Select": "選択", - "Upload": "アップロード", - "Download": "ダウンロード", - "Copy": "コピー", - "Paste": "貼り付け", - "Cut": "切り取り", - "Undo": "元に戻す", - "Redo": "やり直す", - "Name": "名前", - "Description": "説明", - "Status": "ステータス", - "Type": "タイプ", - "Date": "日付", - "Time": "時間", - "Created": "作成済み", - "Updated": "更新済み", - "Actions": "アクション", - "Details": "詳細", - "View": "表示", - "New": "新規", - "Total": "合計", - "Count": "カウント", - "Price": "価格", - "Cost": "コスト", - "Free": "無料", - "Paid": "有料", - "Enable": "有効", - "Disable": "無効", - "Enabled": "有効化済み", - "Disabled": "無効化済み", - "Online": "オンライン", - "Offline": "オフライン", - "Available": "利用可能", - "Unavailable": "利用不可", - "Required": "必須", - "Optional": "オプション", - "Default": "デフォルト", - "Custom": "カスタム", - "Advanced": "詳細", - "Basic": "基本", - "Help": "ヘルプ", - "Support": "サポート", - "Documentation": "ドキュメント", - "Version": "バージョン", - "Language": "言語", - "Theme": "テーマ", - "Light": "ライト", - "Dark": "ダーク", - "Auto": "自動", - "Endpoint": "エンドポイント", - "Providers": "プロバイダー", - "Combos": "コンボ", - "Usage": "統計", - "Quota Tracker": "クォータトラッカー", - "MITM": "MITM", - "CLI Tools": "CLIツール", - "Console Log": "コンソールログ", - "System": "システム", - "Debug": "デバッグ", - "Shutdown": "シャットダウン", - "Close Proxy": "プロキシを閉じる", - "Are you sure you want to close the proxy server?": "プロキシサーバーを閉じてもよろしいですか?", - "Server Disconnected": "サーバーが切断されました", - "The proxy server has been stopped.": "プロキシサーバーが停止しました。", - "Reload Page": "ページを再読み込み", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "サービスはターミナルで実行されています。このウェブページを閉じることができます。シャットダウンはサービスを停止します。", - "Manage your AI provider connections": "AIプロバイダーの接続を管理する", - "Model combos with fallback": "フォールバック付きモデルコンボ", - "Monitor your API usage, token consumption, and request logs": "APIの使用状況、トークン消費量、およびリクエストログを監視する", - "Intercept CLI tool traffic and route through 9Router": "CLIツールのトラフィックをインターセプトし、9Routerを通じてルーティングする", - "Configure CLI tools": "CLIツールを構成", - "API endpoint configuration": "APIエンドポイント構成", - "Manage your preferences": "設定を管理する", - "Debug translation flow between formats": "形式間の翻訳フローをデバッグ", - "Live server console output": "ライブサーバーコンソール出力", - "Create model combos with fallback support": "フォールバックサポート付きモデルコンボを作成", - "Local Mode": "ローカルモード", - "Running on your machine": "お使いのマシンで実行中", - "Database Location": "データベースの場所", - "Download Backup": "バックアップをダウンロード", - "Import Backup": "バックアップをインポート", - "Database backup downloaded": "データベースバックアップがダウンロードされました", - "Database imported successfully": "データベースが正常にインポートされました", - "Security": "セキュリティ", - "Require login": "ログインが必要", - "When ON, dashboard requires password. When OFF, access without login.": "ON の場合、ダッシュボードはパスワードが必要です。OFF の場合、ログインなしでアクセスできます。", - "Current Password": "現在のパスワード", - "Enter current password": "現在のパスワードを入力", - "New Password": "新しいパスワード", - "Enter new password": "新しいパスワードを入力", - "Confirm New Password": "新しいパスワードを確認", - "Confirm new password": "新しいパスワードを確認", - "Update Password": "パスワードを更新", - "Set Password": "パスワードを設定", - "Password updated successfully": "パスワードが正常に更新されました", - "Passwords do not match": "パスワードが一致しません", - "Routing Strategy": "ルーティング戦略", - "Round Robin": "ラウンドロビン", - "Cycle through accounts to distribute load": "アカウント間を循環してロードを分散", - "Sticky Limit": "スティッキーリミット", - "Calls per account before switching": "切り替え前のアカウントごとのコール数", - "Network": "ネットワーク", - "Outbound Proxy": "アウトバウンドプロキシ", - "Enable proxy for OAuth + provider outbound requests.": "OAuth + プロバイダーアウトバウンドリクエストのプロキシを有効にします。", - "Proxy URL": "プロキシURL", - "Leave empty to inherit existing env proxy (if any).": "既存の環境プロキシを継承するには空のままにしてください。", - "No Proxy": "プロキシなし", - "Comma-separated hostnames/domains to bypass the proxy.": "プロキシをバイパスするためのコンマ区切りのホスト名/ドメイン。", - "Test proxy URL": "プロキシURLをテスト", - "Apply": "適用", - "Proxy settings applied": "プロキシ設定が適用されました", - "Proxy enabled": "プロキシが有効になりました", - "Proxy disabled": "プロキシが無効になりました", - "Proxy test OK": "プロキシテストOK", - "Proxy test failed": "プロキシテストが失敗しました", - "Please enter a Proxy URL to test": "テストするプロキシURLを入力してください", - "Observability": "可観測性", - "Enable Observability": "可観測性を有効にする", - "Turn request detail recording on/off globally": "リクエスト詳細記録をグローバルにオン/オフにします", - "Max Records": "最大レコード数", - "Maximum request detail records to keep (older records are auto-deleted)": "保持するリクエスト詳細レコードの最大数(古いレコードは自動削除されます)", - "Batch Size": "バッチサイズ", - "Number of items to accumulate before writing to database (higher = better performance)": "データベースに書き込む前に蓄積するアイテム数(高い = パフォーマンス向上)", - "Flush Interval (ms)": "フラッシュ間隔 (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "バッファをフラッシュする前の最大待機時間(低トラフィック中のデータ損失を防ぎます)", - "Max JSON Size (KB)": "最大JSON サイズ (KB)", - "Maximum size for each JSON field (request/response) before truncation": "切り詰め前の各JSONフィールド(リクエスト/レスポンス)の最大サイズ", - "All data stored on your machine": "すべてのデータがお使いのマシンに保存されます", - "MITM Server": "MITMサーバー", - "Running": "実行中", - "Stopped": "停止済み", - "Cert": "証明書", - "Server": "サーバー", - "Purpose:": "目的:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Antigravity IDE & GitHub Copilot → 9Router の任意のプロバイダー/モデルを使用", - "How it works:": "しくみ:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE リクエスト → localhost:443 への DNS リダイレクト → MITM プロキシが傍受 → 9Router → Antigravity/Copilot への応答", - "API Key": "APIキー", - "No API keys — create one in Keys page": "APIキーがありません — キーページで1つ作成してください", - "sk_9router (default)": "sk_9router(デフォルト)", - "Server started": "サーバーが開始されました", - "Failed to start server": "サーバーの開始に失敗しました", - "Server stopped — all DNS cleared": "サーバーが停止しました — すべてのDNSがクリアされました", - "Failed to stop server": "サーバーの停止に失敗しました", - "Sudo password is required": "Sudoパスワードが必要です", - "Stop Server": "サーバーを停止", - "Start Server": "サーバーを開始", - "Enable DNS per tool below to activate interception": "以下の各ツールに対してDNSを有効にして、傍受をアクティブにします", - "Sudo Password Required": "Sudoパスワードが必要です", - "Enter your sudo password to start/stop MITM server": "MITMサーバーを開始/停止するには、sudoパスワードを入力してください", - "Sudo Password": "Sudoパスワード", - "Confirm": "確認" -} diff --git a/9router/public/i18n/literals/ko.json b/9router/public/i18n/literals/ko.json deleted file mode 100644 index 85deabe4..00000000 --- a/9router/public/i18n/literals/ko.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "취소", - "Delete": "삭제", - "Edit": "편집", - "Save": "저장", - "Close": "닫기", - "Add": "추가", - "Remove": "제거", - "Settings": "설정", - "Profile": "프로필", - "Dashboard": "대시보드", - "Logout": "로그아웃", - "Login": "로그인", - "Providers": "제공자", - "Usage": "사용 현황", - "API Key": "API 키", - "Connected": "연결됨", - "Disconnected": "연결 해제됨", - "Active": "활성", - "Inactive": "비활성", - "Success": "성공", - "Failed": "실패", - "Error": "오류", - "Warning": "경고", - "Info": "정보", - "Loading": "로딩 중", - "Search": "검색", - "Filter": "필터", - "Sort": "정렬", - "Export": "내보내기", - "Import": "가져오기", - "Refresh": "새로고침", - "Back": "뒤로", - "Next": "다음", - "Previous": "이전", - "Submit": "제출", - "Confirm": "확인", - "Yes": "예", - "No": "아니오", - "OK": "확인", - "Apply": "적용", - "Reset": "재설정", - "Clear": "지우기", - "Select": "선택", - "Upload": "업로드", - "Download": "다운로드", - "Copy": "복사", - "Paste": "붙여넣기", - "Cut": "잘라내기", - "Undo": "실행 취소", - "Redo": "다시 실행", - "Name": "이름", - "Description": "설명", - "Status": "상태", - "Type": "유형", - "Date": "날짜", - "Time": "시간", - "Created": "생성됨", - "Updated": "업데이트됨", - "Actions": "작업", - "Details": "세부정보", - "View": "보기", - "New": "새로 만들기", - "Total": "합계", - "Count": "개수", - "Price": "가격", - "Cost": "비용", - "Free": "무료", - "Paid": "유료", - "Enable": "활성화", - "Disable": "비활성화", - "Enabled": "활성화됨", - "Disabled": "비활성화됨", - "Online": "온라인", - "Offline": "오프라인", - "Available": "사용 가능", - "Unavailable": "사용 불가", - "Required": "필수", - "Optional": "선택사항", - "Default": "기본값", - "Custom": "사용자 지정", - "Advanced": "고급", - "Basic": "기본", - "Help": "도움말", - "Support": "지원", - "Documentation": "설명서", - "Version": "버전", - "Language": "언어", - "Theme": "테마", - "Light": "밝음", - "Dark": "어두움", - "Auto": "자동", - "Endpoint": "엔드포인트", - "Providers": "제공자", - "Combos": "조합", - "Usage": "통계", - "Quota Tracker": "할당량 추적", - "MITM": "MITM", - "CLI Tools": "CLI 도구", - "Console Log": "콘솔 로그", - "System": "시스템", - "Debug": "디버깅", - "Shutdown": "종료", - "Close Proxy": "프록시 닫기", - "Are you sure you want to close the proxy server?": "프록시 서버를 닫으시겠습니까?", - "Server Disconnected": "서버 연결 해제됨", - "The proxy server has been stopped.": "프록시 서버가 중지되었습니다.", - "Reload Page": "페이지 다시 로드", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "서비스가 터미널에서 실행 중입니다. 이 웹 페이지를 닫을 수 있습니다. 종료하면 서비스가 중지됩니다.", - "Manage your AI provider connections": "AI 제공자 연결 관리", - "Model combos with fallback": "폴백 기능이 있는 모델 조합", - "Monitor your API usage, token consumption, and request logs": "API 사용, 토큰 소비 및 요청 로그 모니터링", - "Intercept CLI tool traffic and route through 9Router": "CLI 도구 트래픽을 가로채고 9Router를 통해 라우팅", - "Configure CLI tools": "CLI 도구 구성", - "API endpoint configuration": "API 엔드포인트 구성", - "Manage your preferences": "기본 설정 관리", - "Debug translation flow between formats": "형식 간 변환 흐름 디버깅", - "Live server console output": "라이브 서버 콘솔 출력", - "Create model combos with fallback support": "폴백 지원이 있는 모델 조합 만들기", - "Local Mode": "로컬 모드", - "Running on your machine": "컴퓨터에서 실행 중", - "Database Location": "데이터베이스 위치", - "Download Backup": "백업 다운로드", - "Import Backup": "백업 가져오기", - "Database backup downloaded": "데이터베이스 백업 다운로드됨", - "Database imported successfully": "데이터베이스를 성공적으로 가져왔습니다", - "Security": "보안", - "Require login": "로그인 필요", - "When ON, dashboard requires password. When OFF, access without login.": "ON일 때 대시보드에서 암호가 필요합니다. OFF일 때 로그인 없이 접근할 수 있습니다.", - "Current Password": "현재 암호", - "Enter current password": "현재 암호 입력", - "New Password": "새 암호", - "Enter new password": "새 암호 입력", - "Confirm New Password": "새 암호 확인", - "Confirm new password": "새 암호 확인", - "Update Password": "암호 업데이트", - "Set Password": "암호 설정", - "Password updated successfully": "암호가 성공적으로 업데이트되었습니다", - "Passwords do not match": "암호가 일치하지 않습니다", - "Routing Strategy": "라우팅 전략", - "Round Robin": "라운드 로빈", - "Cycle through accounts to distribute load": "계정을 순환하여 로드 분산", - "Sticky Limit": "고정 제한", - "Calls per account before switching": "전환 전 계정당 호출 수", - "Network": "네트워크", - "Outbound Proxy": "아웃바운드 프록시", - "Enable proxy for OAuth + provider outbound requests.": "OAuth + 제공자 아웃바운드 요청에 대한 프록시를 활성화합니다.", - "Proxy URL": "프록시 URL", - "Leave empty to inherit existing env proxy (if any).": "기존 환경 프록시를 상속받으려면 비워두세요.", - "No Proxy": "프록시 없음", - "Comma-separated hostnames/domains to bypass the proxy.": "프록시를 우회하기 위한 쉼표로 구분된 호스트명/도메인입니다.", - "Test proxy URL": "프록시 URL 테스트", - "Apply": "적용", - "Proxy settings applied": "프록시 설정이 적용되었습니다", - "Proxy enabled": "프록시 활성화됨", - "Proxy disabled": "프록시 비활성화됨", - "Proxy test OK": "프록시 테스트 성공", - "Proxy test failed": "프록시 테스트 실패", - "Please enter a Proxy URL to test": "테스트할 프록시 URL을 입력하세요", - "Observability": "관찰 가능성", - "Enable Observability": "관찰 가능성 활성화", - "Turn request detail recording on/off globally": "요청 세부 정보 기록을 전역적으로 켜기/끄기", - "Max Records": "최대 레코드", - "Maximum request detail records to keep (older records are auto-deleted)": "보관할 최대 요청 세부 정보 레코드(오래된 레코드는 자동 삭제됨)", - "Batch Size": "배치 크기", - "Number of items to accumulate before writing to database (higher = better performance)": "데이터베이스에 쓰기 전에 누적할 항목 수(높을수록 더 나은 성능)", - "Flush Interval (ms)": "플러시 간격 (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "버퍼를 플러시하기 전 최대 대기 시간(낮은 트래픽 중 데이터 손실 방지)", - "Max JSON Size (KB)": "최대 JSON 크기 (KB)", - "Maximum size for each JSON field (request/response) before truncation": "자르기 전 각 JSON 필드(요청/응답)의 최대 크기", - "All data stored on your machine": "모든 데이터가 컴퓨터에 저장됨", - "MITM Server": "MITM 서버", - "Running": "실행 중", - "Stopped": "중지됨", - "Cert": "인증서", - "Server": "서버", - "Purpose:": "목적:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Antigravity IDE 및 GitHub Copilot 사용 → 9Router의 모든 제공자/모델과 함께", - "How it works:": "작동 방식:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE 요청 → localhost:443로 DNS 리디렉션 → MITM 프록시가 가로챔 → 9Router → Antigravity/Copilot으로 응답", - "API Key": "API 키", - "No API keys — create one in Keys page": "API 키 없음 — 키 페이지에서 만들기", - "sk_9router (default)": "sk_9router (기본값)", - "Server started": "서버 시작됨", - "Failed to start server": "서버 시작 실패", - "Server stopped — all DNS cleared": "서버 중지됨 — 모든 DNS 지워짐", - "Failed to stop server": "서버 중지 실패", - "Sudo password is required": "Sudo 암호가 필요합니다", - "Stop Server": "서버 중지", - "Start Server": "서버 시작", - "Enable DNS per tool below to activate interception": "아래 각 도구에 대해 DNS를 활성화하여 가로채기 활성화", - "Sudo Password Required": "Sudo 암호 필요", - "Enter your sudo password to start/stop MITM server": "MITM 서버를 시작/중지하려면 sudo 암호를 입력하세요", - "Sudo Password": "Sudo 암호", - "Confirm": "확인" -} diff --git a/9router/public/i18n/literals/nl.json b/9router/public/i18n/literals/nl.json deleted file mode 100644 index 38ca28e8..00000000 --- a/9router/public/i18n/literals/nl.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Annuleren", - "Delete": "Verwijderen", - "Edit": "Bewerken", - "Save": "Opslaan", - "Close": "Sluiten", - "Add": "Toevoegen", - "Remove": "Verwijderen", - "Settings": "Instellingen", - "Profile": "Profiel", - "Dashboard": "Dashboard", - "Logout": "Afmelden", - "Login": "Aanmelden", - "Providers": "Providers", - "Usage": "Gebruik", - "API Key": "API-sleutel", - "Connected": "Verbonden", - "Disconnected": "Verbroken", - "Active": "Actief", - "Inactive": "Inactief", - "Success": "Succes", - "Failed": "Mislukt", - "Error": "Fout", - "Warning": "Waarschuwing", - "Info": "Info", - "Loading": "Laden", - "Search": "Zoeken", - "Filter": "Filteren", - "Sort": "Sorteren", - "Export": "Exporteren", - "Import": "Importeren", - "Refresh": "Vernieuwen", - "Back": "Terug", - "Next": "Volgende", - "Previous": "Vorige", - "Submit": "Verzenden", - "Confirm": "Bevestigen", - "Yes": "Ja", - "No": "Nee", - "OK": "OK", - "Apply": "Toepassen", - "Reset": "Opnieuw instellen", - "Clear": "Wissen", - "Select": "Selecteren", - "Upload": "Uploaden", - "Download": "Downloaden", - "Copy": "Kopieëren", - "Paste": "Plakken", - "Cut": "Knippen", - "Undo": "Ongedaan maken", - "Redo": "Opnieuw uitvoeren", - "Name": "Naam", - "Description": "Beschrijving", - "Status": "Status", - "Type": "Type", - "Date": "Datum", - "Time": "Tijd", - "Created": "Gemaakt", - "Updated": "Bijgewerkt", - "Actions": "Acties", - "Details": "Details", - "View": "Weergeven", - "New": "Nieuw", - "Total": "Totaal", - "Count": "Aantal", - "Price": "Prijs", - "Cost": "Kosten", - "Free": "Gratis", - "Paid": "Betaald", - "Enable": "Inschakelen", - "Disable": "Uitschakelen", - "Enabled": "Ingeschakeld", - "Disabled": "Uitgeschakeld", - "Online": "Online", - "Offline": "Offline", - "Available": "Beschikbaar", - "Unavailable": "Niet beschikbaar", - "Required": "Verplicht", - "Optional": "Optioneel", - "Default": "Standaard", - "Custom": "Aangepast", - "Advanced": "Geavanceerd", - "Basic": "Basis", - "Help": "Help", - "Support": "Ondersteuning", - "Documentation": "Documentatie", - "Version": "Versie", - "Language": "Taal", - "Theme": "Thema", - "Light": "Licht", - "Dark": "Donker", - "Auto": "Automatisch", - "Endpoint": "Eindpunt", - "Providers": "Providers", - "Combos": "Combinaties", - "Usage": "Statistieken", - "Quota Tracker": "Quotabijhouder", - "MITM": "MITM", - "CLI Tools": "CLI-tools", - "Console Log": "Consolenlogboek", - "System": "Systeem", - "Debug": "Foutopsporing", - "Shutdown": "Afsluiten", - "Close Proxy": "Proxy sluiten", - "Are you sure you want to close the proxy server?": "Weet u zeker dat u de proxyserver wilt sluiten?", - "Server Disconnected": "Server verbroken", - "The proxy server has been stopped.": "De proxyserver is gestopt.", - "Reload Page": "Pagina vernieuwen", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Service wordt uitgevoerd in terminal. U kunt deze webpagina sluiten. Afsluiten stopt de service.", - "Manage your AI provider connections": "Beheer uw AI-providerverbindingen", - "Model combos with fallback": "Modelcombinaties met fallback", - "Monitor your API usage, token consumption, and request logs": "Controleer uw API-gebruik, tokenverbruik en aanvraaglogboeken", - "Intercept CLI tool traffic and route through 9Router": "Onderschep CLI-toolverkeer en stuur het via 9Router", - "Configure CLI tools": "CLI-tools configureren", - "API endpoint configuration": "Configuratie van API-eindpunt", - "Manage your preferences": "Beheer uw voorkeuren", - "Debug translation flow between formats": "Debug de vertaalstroom tussen indelingen", - "Live server console output": "Live-serverconsoluitvoer", - "Create model combos with fallback support": "Maak modelcombinaties met fallback-ondersteuning", - "Local Mode": "Lokale modus", - "Running on your machine": "Actief op uw machine", - "Database Location": "Databaselocatie", - "Download Backup": "Backup downloaden", - "Import Backup": "Backup importeren", - "Database backup downloaded": "Databaseback-up gedownload", - "Database imported successfully": "Database succesvol geïmporteerd", - "Security": "Beveiliging", - "Require login": "Aanmelden vereist", - "When ON, dashboard requires password. When OFF, access without login.": "Wanneer AAN, vereist dashboard een wachtwoord. Wanneer UIT, toegang zonder aanmelden.", - "Current Password": "Huidig wachtwoord", - "Enter current password": "Voer het huidige wachtwoord in", - "New Password": "Nieuw wachtwoord", - "Enter new password": "Voer een nieuw wachtwoord in", - "Confirm New Password": "Nieuw wachtwoord bevestigen", - "Confirm new password": "Bevestig het nieuwe wachtwoord", - "Update Password": "Wachtwoord bijwerken", - "Set Password": "Wachtwoord instellen", - "Password updated successfully": "Wachtwoord succesvol bijgewerkt", - "Passwords do not match": "Wachtwoorden komen niet overeen", - "Routing Strategy": "Routeringsstrategie", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Wisselen tussen accounts om belasting te verdelen", - "Sticky Limit": "Plakkerige limiet", - "Calls per account before switching": "Oproepen per account voordat u overschakelt", - "Network": "Netwerk", - "Outbound Proxy": "Uitgaande proxy", - "Enable proxy for OAuth + provider outbound requests.": "Schakel proxy in voor OAuth + uitgaande verzoeken van provider.", - "Proxy URL": "Proxy-URL", - "Leave empty to inherit existing env proxy (if any).": "Laat leeg om bestaande env-proxy over te nemen (indien aanwezig).", - "No Proxy": "Geen proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Door komma's gescheiden hostnamen/domeinen om de proxy te omzeilen.", - "Test proxy URL": "Proxy-URL testen", - "Apply": "Toepassen", - "Proxy settings applied": "Proxy-instellingen toegepast", - "Proxy enabled": "Proxy ingeschakeld", - "Proxy disabled": "Proxy uitgeschakeld", - "Proxy test OK": "Proxy-test OK", - "Proxy test failed": "Proxy-test mislukt", - "Please enter a Proxy URL to test": "Voer een proxy-URL in om te testen", - "Observability": "Waarneembaarheid", - "Enable Observability": "Waarneembaarheid inschakelen", - "Turn request detail recording on/off globally": "Recordering van aanvraagdetails globaal in-/uitschakelen", - "Max Records": "Maximale records", - "Maximum request detail records to keep (older records are auto-deleted)": "Maximale aantal aanvraagdetailrecords om te behouden (oudere records worden automatisch verwijderd)", - "Batch Size": "Batchgrootte", - "Number of items to accumulate before writing to database (higher = better performance)": "Aantal items dat moet worden verzameld voordat naar database wordt geschreven (hoger = beter prestaties)", - "Flush Interval (ms)": "Spoelinterval (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maximale wachttijd voordat buffer wordt leeggemaakt (voorkomt gegevensverlies bij laag verkeer)", - "Max JSON Size (KB)": "Maximale JSON-grootte (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maximale grootte voor elk JSON-veld (aanvraag/antwoord) voordat afkappen", - "All data stored on your machine": "Alle gegevens opgeslagen op uw machine", - "MITM Server": "MITM-server", - "Running": "Actief", - "Stopped": "Gestopt", - "Cert": "Certificaat", - "Server": "Server", - "Purpose:": "Doel:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Gebruik Antigravity IDE en GitHub Copilot → met ELKE provider/model van 9Router", - "How it works:": "Hoe het werkt:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-aanvraag → DNS-omleiding naar localhost:443 → MITM-proxy onderschept → 9Router → antwoord naar Antigravity/Copilot", - "API Key": "API-sleutel", - "No API keys — create one in Keys page": "Geen API-sleutels — maak er één aan op de pagina Sleutels", - "sk_9router (default)": "sk_9router (standaard)", - "Server started": "Server gestart", - "Failed to start server": "Server starten mislukt", - "Server stopped — all DNS cleared": "Server gestopt — alle DNS gewist", - "Failed to stop server": "Server stoppen mislukt", - "Sudo password is required": "Sudo-wachtwoord vereist", - "Stop Server": "Server stoppen", - "Start Server": "Server starten", - "Enable DNS per tool below to activate interception": "Schakel DNS in voor elk hulpmiddel hieronder om onderschepping te activeren", - "Sudo Password Required": "Sudo-wachtwoord vereist", - "Enter your sudo password to start/stop MITM server": "Voer uw sudo-wachtwoord in om de MITM-server te starten/stoppen", - "Sudo Password": "Sudo-wachtwoord", - "Confirm": "Bevestigen" -} diff --git a/9router/public/i18n/literals/no.json b/9router/public/i18n/literals/no.json deleted file mode 100644 index e9af007c..00000000 --- a/9router/public/i18n/literals/no.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Avbryt", - "Delete": "Slett", - "Edit": "Rediger", - "Save": "Lagre", - "Close": "Lukk", - "Add": "Legg til", - "Remove": "Fjern", - "Settings": "Innstillinger", - "Profile": "Profil", - "Dashboard": "Kontrollpanel", - "Logout": "Logg ut", - "Login": "Logg inn", - "Providers": "Leverandører", - "Usage": "Bruk", - "API Key": "API-nøkkel", - "Connected": "Tilkoblet", - "Disconnected": "Frakoblet", - "Active": "Aktiv", - "Inactive": "Inaktiv", - "Success": "Suksess", - "Failed": "Mislyktes", - "Error": "Feil", - "Warning": "Advarsel", - "Info": "Informasjon", - "Loading": "Laster", - "Search": "Søk", - "Filter": "Filter", - "Sort": "Sorter", - "Export": "Eksporter", - "Import": "Importer", - "Refresh": "Oppdater", - "Back": "Tilbake", - "Next": "Neste", - "Previous": "Forrige", - "Submit": "Send inn", - "Confirm": "Bekreft", - "Yes": "Ja", - "No": "Nei", - "OK": "OK", - "Apply": "Bruk", - "Reset": "Tilbakestill", - "Clear": "Tøm", - "Select": "Velg", - "Upload": "Last opp", - "Download": "Last ned", - "Copy": "Kopier", - "Paste": "Lim inn", - "Cut": "Klipp", - "Undo": "Angre", - "Redo": "Gjør på nytt", - "Name": "Navn", - "Description": "Beskrivelse", - "Status": "Status", - "Type": "Type", - "Date": "Dato", - "Time": "Tid", - "Created": "Opprettet", - "Updated": "Oppdatert", - "Actions": "Handlinger", - "Details": "Detaljer", - "View": "Vis", - "New": "Ny", - "Total": "Total", - "Count": "Antall", - "Price": "Pris", - "Cost": "Kostnad", - "Free": "Gratis", - "Paid": "Betalt", - "Enable": "Aktiver", - "Disable": "Deaktiver", - "Enabled": "Aktivert", - "Disabled": "Deaktivert", - "Online": "Pålogget", - "Offline": "Frakoblet", - "Available": "Tilgjengelig", - "Unavailable": "Utilgjengelig", - "Required": "Obligatorisk", - "Optional": "Valgfritt", - "Default": "Standard", - "Custom": "Tilpasset", - "Advanced": "Avansert", - "Basic": "Grunnleggende", - "Help": "Hjelp", - "Support": "Støtte", - "Documentation": "Dokumentasjon", - "Version": "Versjon", - "Language": "Språk", - "Theme": "Tema", - "Light": "Lys", - "Dark": "Mørk", - "Auto": "Automatisk", - "Endpoint": "Endepunkt", - "Providers": "Leverandører", - "Combos": "Kombinasjoner", - "Usage": "Bruksstatistikk", - "Quota Tracker": "Kvotasporer", - "MITM": "MITM", - "CLI Tools": "Verktøy", - "Console Log": "Konsollogg", - "System": "System", - "Debug": "Feilsøking", - "Shutdown": "Slå av", - "Close Proxy": "Lukk proxy", - "Are you sure you want to close the proxy server?": "Er du sikker på at du vil lukke proxyserveren?", - "Server Disconnected": "Server frakoblet", - "The proxy server has been stopped.": "Proxyserveren har blitt stoppet.", - "Reload Page": "Last inn siden på nytt", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Tjenesten kjører i terminalen. Du kan lukke denne nettsiden. Slå av vil stoppe tjenesten.", - "Manage your AI provider connections": "Administrer AI-leverandørforbindelsene dine", - "Model combos with fallback": "Modellkombinasjoner med fallback", - "Monitor your API usage, token consumption, and request logs": "Overvåk API-bruken, tokenforbruk og anmodningslogger", - "Intercept CLI tool traffic and route through 9Router": "Avlytting av CLI-verktøy trafikk og rute gjennom 9Router", - "Configure CLI tools": "Konfigurer CLI-verktøy", - "API endpoint configuration": "Konfiguration av API-endepunkt", - "Manage your preferences": "Administrer dine preferanser", - "Debug translation flow between formats": "Feilsøking av oversettelsesflyt mellom formater", - "Live server console output": "Direkte serverkonsolresultat", - "Create model combos with fallback support": "Opprett modellkombinasjoner med fallback-støtte", - "Local Mode": "Lokalt modus", - "Running on your machine": "Kjørende på maskinen din", - "Database Location": "Databaseplassering", - "Download Backup": "Last ned sikkerhetskopi", - "Import Backup": "Importer sikkerhetskopi", - "Database backup downloaded": "Databasesikkerhetskopi lastet ned", - "Database imported successfully": "Database importert med suksess", - "Security": "Sikkerhet", - "Require login": "Krev innlogging", - "When ON, dashboard requires password. When OFF, access without login.": "Når PÅ krever kontrollpanelet passord. Når AV, tilgang uten innlogging.", - "Current Password": "Gjeldende passord", - "Enter current password": "Skriv inn gjeldende passord", - "New Password": "Nytt passord", - "Enter new password": "Skriv inn nytt passord", - "Confirm New Password": "Bekreft nytt passord", - "Confirm new password": "Bekreft nytt passord", - "Update Password": "Oppdater passord", - "Set Password": "Angi passord", - "Password updated successfully": "Passord oppdatert med suksess", - "Passwords do not match": "Passordene samsvarer ikke", - "Routing Strategy": "Rutestrategi", - "Round Robin": "Runderobin", - "Cycle through accounts to distribute load": "Syklus gjennom kontoer for å distribuere belastning", - "Sticky Limit": "Klebrig grense", - "Calls per account before switching": "Anrop per konto før bytte", - "Network": "Nettverk", - "Outbound Proxy": "Utgående proxy", - "Enable proxy for OAuth + provider outbound requests.": "Aktiver proxy for OAuth + leverandør utgående forespørsler.", - "Proxy URL": "Proxy-URL", - "Leave empty to inherit existing env proxy (if any).": "La være tom for å arve eksisterende env-proxy (hvis noen).", - "No Proxy": "Ingen proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Kommaseparerte vertsnavner/domener for å omgå proxyen.", - "Test proxy URL": "Test proxy-URL", - "Apply": "Bruk", - "Proxy settings applied": "Proxyinnstillinger brukt", - "Proxy enabled": "Proxy aktivert", - "Proxy disabled": "Proxy deaktivert", - "Proxy test OK": "Proxytest OK", - "Proxy test failed": "Proxytest mislyktes", - "Please enter a Proxy URL to test": "Vennligst skriv inn en proxy-URL å teste", - "Observability": "Observerbarhet", - "Enable Observability": "Aktiver observerbarhet", - "Turn request detail recording on/off globally": "Slå detaljregistrering av forespørsel på/av globalt", - "Max Records": "Max-poster", - "Maximum request detail records to keep (older records are auto-deleted)": "Maksimum anmodningsdetaljposter å beholde (eldre poster blir automatisk slettet)", - "Batch Size": "Batch-størrelse", - "Number of items to accumulate before writing to database (higher = better performance)": "Antall elementer som skal akkumuleres før skriving til database (høyere = bedre ytelse)", - "Flush Interval (ms)": "Spylt intervall (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maksimal ventetid før spyling av buffer (forhindrer tap av data under lavt trafikk)", - "Max JSON Size (KB)": "Maks JSON-størrelse (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maksimal størrelse for hvert JSON-felt (forespørsel/svar) før avkutting", - "All data stored on your machine": "Alle data lagret på maskinen din", - "MITM Server": "MITM-server", - "Running": "Kjørende", - "Stopped": "Stoppet", - "Cert": "Sertifikat", - "Server": "Server", - "Purpose:": "Formål:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Bruk Antigravity IDE & GitHub Copilot → med ENHVER leverandør/modell fra 9Router", - "How it works:": "Slik fungerer det:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-forespørsel → DNS-omdirigering til localhost:443 → MITM-proxy avlytt → 9Router → svar til Antigravity/Copilot", - "API Key": "API-nøkkel", - "No API keys — create one in Keys page": "Ingen API-nøkler — lag en på Keys-siden", - "sk_9router (default)": "sk_9router (standard)", - "Server started": "Server startet", - "Failed to start server": "Klarte ikke å starte server", - "Server stopped — all DNS cleared": "Server stoppet — alle DNS ryddet", - "Failed to stop server": "Klarte ikke å stoppe server", - "Sudo password is required": "Sudo-passord er påkrevd", - "Stop Server": "Stopp server", - "Start Server": "Start server", - "Enable DNS per tool below to activate interception": "Aktiver DNS for hvert verktøy nedenfor for å aktivere avlytting", - "Sudo Password Required": "Sudo-passord påkrevd", - "Enter your sudo password to start/stop MITM server": "Skriv inn sudo-passordet ditt for å starte/stoppe MITM-server", - "Sudo Password": "Sudo-passord", - "Confirm": "Bekreft" -} diff --git a/9router/public/i18n/literals/pl.json b/9router/public/i18n/literals/pl.json deleted file mode 100644 index 4c857330..00000000 --- a/9router/public/i18n/literals/pl.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Anuluj", - "Delete": "Usuń", - "Edit": "Edytuj", - "Save": "Zapisz", - "Close": "Zamknij", - "Add": "Dodaj", - "Remove": "Usuń", - "Settings": "Ustawienia", - "Profile": "Profil", - "Dashboard": "Panel kontrolny", - "Logout": "Wyloguj się", - "Login": "Zaloguj się", - "Providers": "Dostawcy", - "Usage": "Użycie", - "API Key": "Klucz API", - "Connected": "Połączony", - "Disconnected": "Rozłączony", - "Active": "Aktywny", - "Inactive": "Nieaktywny", - "Success": "Sukces", - "Failed": "Niepowodzenie", - "Error": "Błąd", - "Warning": "Ostrzeżenie", - "Info": "Informacja", - "Loading": "Ładowanie", - "Search": "Szukaj", - "Filter": "Filtruj", - "Sort": "Sortuj", - "Export": "Eksportuj", - "Import": "Importuj", - "Refresh": "Odśwież", - "Back": "Wstecz", - "Next": "Dalej", - "Previous": "Wstecz", - "Submit": "Prześlij", - "Confirm": "Potwierdź", - "Yes": "Tak", - "No": "Nie", - "OK": "OK", - "Apply": "Zastosuj", - "Reset": "Resetuj", - "Clear": "Wyczyść", - "Select": "Wybierz", - "Upload": "Prześlij", - "Download": "Pobierz", - "Copy": "Skopiuj", - "Paste": "Wklej", - "Cut": "Wytnij", - "Undo": "Cofnij", - "Redo": "Powtórz", - "Name": "Nazwa", - "Description": "Opis", - "Status": "Stan", - "Type": "Typ", - "Date": "Data", - "Time": "Czas", - "Created": "Utworzono", - "Updated": "Zaktualizowano", - "Actions": "Akcje", - "Details": "Szczegóły", - "View": "Wyświetl", - "New": "Nowy", - "Total": "Razem", - "Count": "Liczba", - "Price": "Cena", - "Cost": "Koszt", - "Free": "Bezpłatny", - "Paid": "Płatny", - "Enable": "Włącz", - "Disable": "Wyłącz", - "Enabled": "Włączony", - "Disabled": "Wyłączony", - "Online": "Online", - "Offline": "Offline", - "Available": "Dostępny", - "Unavailable": "Niedostępny", - "Required": "Wymagane", - "Optional": "Opcjonalne", - "Default": "Domyślnie", - "Custom": "Niestandardowy", - "Advanced": "Zaawansowane", - "Basic": "Podstawowy", - "Help": "Pomoc", - "Support": "Pomoc techniczna", - "Documentation": "Dokumentacja", - "Version": "Wersja", - "Language": "Język", - "Theme": "Motyw", - "Light": "Jasny", - "Dark": "Ciemny", - "Auto": "Automatycznie", - "Endpoint": "Punkt końcowy", - "Providers": "Dostawcy", - "Combos": "Kombinacje", - "Usage": "Statystyka", - "Quota Tracker": "Śledzenie limitów", - "MITM": "MITM", - "CLI Tools": "Narzędzia CLI", - "Console Log": "Dziennik konsoli", - "System": "System", - "Debug": "Debugowanie", - "Shutdown": "Wyłączenie", - "Close Proxy": "Zamknij serwer proxy", - "Are you sure you want to close the proxy server?": "Czy na pewno chcesz zamknąć serwer proxy?", - "Server Disconnected": "Serwer rozłączony", - "The proxy server has been stopped.": "Serwer proxy został zatrzymany.", - "Reload Page": "Odśwież stronę", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Usługa działa w terminalu. Możesz zamknąć tę stronę internetową. Wyłączenie zatrzyma usługę.", - "Manage your AI provider connections": "Zarządzaj połączeniami dostawcy sztucznej inteligencji", - "Model combos with fallback": "Kombinacje modeli z rezerwą", - "Monitor your API usage, token consumption, and request logs": "Monitoruj użycie API, zużycie tokenów i dzienniki żądań", - "Intercept CLI tool traffic and route through 9Router": "Przechwytuj ruch narzędzi CLI i kieruj przez 9Router", - "Configure CLI tools": "Konfiguruj narzędzia CLI", - "API endpoint configuration": "Konfiguracja punktu końcowego API", - "Manage your preferences": "Zarządzaj swoimi preferencjami", - "Debug translation flow between formats": "Debuguj przepływ tłumaczenia między formatami", - "Live server console output": "Wyjście konsoli serwera na żywo", - "Create model combos with fallback support": "Twórz kombinacje modeli z obsługą rezerwową", - "Local Mode": "Tryb lokalny", - "Running on your machine": "Działające na twoim komputerze", - "Database Location": "Lokalizacja bazy danych", - "Download Backup": "Pobierz kopię zapasową", - "Import Backup": "Importuj kopię zapasową", - "Database backup downloaded": "Kopia zapasowa bazy danych pobrana", - "Database imported successfully": "Baza danych pomyślnie zaimportowana", - "Security": "Bezpieczeństwo", - "Require login": "Wymagaj logowania", - "When ON, dashboard requires password. When OFF, access without login.": "Gdy jest WŁĄCZONY, panel wymaga hasła. Gdy jest WYŁĄCZONY, dostęp bez logowania.", - "Current Password": "Obecne hasło", - "Enter current password": "Wprowadź obecne hasło", - "New Password": "Nowe hasło", - "Enter new password": "Wprowadź nowe hasło", - "Confirm New Password": "Potwierdź nowe hasło", - "Confirm new password": "Potwierdź nowe hasło", - "Update Password": "Aktualizuj hasło", - "Set Password": "Ustaw hasło", - "Password updated successfully": "Hasło zaktualizowane pomyślnie", - "Passwords do not match": "Hasła się nie zgadzają", - "Routing Strategy": "Strategia routingu", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Obracaj kontem w celu rozłożenia obciążenia", - "Sticky Limit": "Limit lepki", - "Calls per account before switching": "Wywołań na konto przed przełączeniem", - "Network": "Sieć", - "Outbound Proxy": "Serwer proxy wychodzący", - "Enable proxy for OAuth + provider outbound requests.": "Włącz serwer proxy dla żądań wychodzących OAuth + dostawcy.", - "Proxy URL": "URL serwera proxy", - "Leave empty to inherit existing env proxy (if any).": "Zostaw puste, aby odziedziczyć istniejący serwer proxy env (jeśli istnieje).", - "No Proxy": "Bez serwera proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Nazwy hostów/domeny oddzielone przecinkami do pominięcia serwera proxy.", - "Test proxy URL": "Przetestuj URL serwera proxy", - "Apply": "Zastosuj", - "Proxy settings applied": "Ustawienia serwera proxy zastosowane", - "Proxy enabled": "Serwer proxy włączony", - "Proxy disabled": "Serwer proxy wyłączony", - "Proxy test OK": "Test serwera proxy OK", - "Proxy test failed": "Test serwera proxy nie powiódł się", - "Please enter a Proxy URL to test": "Proszę wprowadzić URL serwera proxy do testowania", - "Observability": "Obserwacyjność", - "Enable Observability": "Włącz obserwacyjność", - "Turn request detail recording on/off globally": "Włącz/wyłącz globalnie rejestrowanie szczegółów żądania", - "Max Records": "Maksymalna liczba rekordów", - "Maximum request detail records to keep (older records are auto-deleted)": "Maksymalna liczba rekordów szczegółów żądania do przechowywania (starsze rekordy są automatycznie usuwane)", - "Batch Size": "Rozmiar partii", - "Number of items to accumulate before writing to database (higher = better performance)": "Liczba elementów do gromadzenia przed zapisaniem w bazie danych (wyższa = lepsza wydajność)", - "Flush Interval (ms)": "Interwał opróżniania (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maksymalny czas oczekiwania przed opróżnieniem buforu (zapobiega utracie danych podczas małego ruchu)", - "Max JSON Size (KB)": "Maksymalny rozmiar JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maksymalny rozmiar każdego pola JSON (żądanie/odpowiedź) przed obcięciem", - "All data stored on your machine": "Wszystkie dane przechowywane na twoim komputerze", - "MITM Server": "Serwer MITM", - "Running": "Działający", - "Stopped": "Zatrzymany", - "Cert": "Certyfikat", - "Server": "Serwer", - "Purpose:": "Cel:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Użyj Antigravity IDE i GitHub Copilot → z DOWOLNYM dostawcą/modelem z 9Router", - "How it works:": "Jak to działa:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Żądanie Antigravity/Copilot IDE → Przekierowanie DNS na localhost:443 → Serwer proxy MITM przechwytuje → 9Router → odpowiedź do Antigravity/Copilot", - "API Key": "Klucz API", - "No API keys — create one in Keys page": "Brak kluczy API — utwórz jeden na stronie Klucze", - "sk_9router (default)": "sk_9router (domyślnie)", - "Server started": "Serwer uruchomiony", - "Failed to start server": "Nie udało się uruchomić serwera", - "Server stopped — all DNS cleared": "Serwer zatrzymany — cały DNS usunięty", - "Failed to stop server": "Nie udało się zatrzymać serwera", - "Sudo password is required": "Wymagane hasło sudo", - "Stop Server": "Zatrzymaj serwer", - "Start Server": "Uruchom serwer", - "Enable DNS per tool below to activate interception": "Włącz DNS dla każdego narzędzia poniżej, aby aktywować przechwytywanie", - "Sudo Password Required": "Wymagane hasło Sudo", - "Enter your sudo password to start/stop MITM server": "Wprowadź hasło sudo, aby uruchomić/zatrzymać serwer MITM", - "Sudo Password": "Hasło sudo", - "Confirm": "Potwierdź" -} diff --git a/9router/public/i18n/literals/pt-BR.json b/9router/public/i18n/literals/pt-BR.json deleted file mode 100644 index e487d74b..00000000 --- a/9router/public/i18n/literals/pt-BR.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Cancelar", - "Delete": "Excluir", - "Edit": "Editar", - "Save": "Salvar", - "Close": "Fechar", - "Add": "Adicionar", - "Remove": "Remover", - "Settings": "Configurações", - "Profile": "Perfil", - "Dashboard": "Painel de controle", - "Logout": "Sair", - "Login": "Conectar", - "Providers": "Provedores", - "Usage": "Uso", - "API Key": "Chave API", - "Connected": "Conectado", - "Disconnected": "Desconectado", - "Active": "Ativo", - "Inactive": "Inativo", - "Success": "Sucesso", - "Failed": "Falha", - "Error": "Erro", - "Warning": "Aviso", - "Info": "Informações", - "Loading": "Carregando", - "Search": "Pesquisar", - "Filter": "Filtrar", - "Sort": "Classificar", - "Export": "Exportar", - "Import": "Importar", - "Refresh": "Atualizar", - "Back": "Voltar", - "Next": "Próximo", - "Previous": "Anterior", - "Submit": "Enviar", - "Confirm": "Confirmar", - "Yes": "Sim", - "No": "Não", - "OK": "OK", - "Apply": "Aplicar", - "Reset": "Redefinir", - "Clear": "Limpar", - "Select": "Selecionar", - "Upload": "Enviar", - "Download": "Baixar", - "Copy": "Copiar", - "Paste": "Colar", - "Cut": "Cortar", - "Undo": "Desfazer", - "Redo": "Refazer", - "Name": "Nome", - "Description": "Descrição", - "Status": "Status", - "Type": "Tipo", - "Date": "Data", - "Time": "Hora", - "Created": "Criado", - "Updated": "Atualizado", - "Actions": "Ações", - "Details": "Detalhes", - "View": "Visualizar", - "New": "Novo", - "Total": "Total", - "Count": "Contagem", - "Price": "Preço", - "Cost": "Custo", - "Free": "Gratuito", - "Paid": "Pago", - "Enable": "Ativar", - "Disable": "Desativar", - "Enabled": "Ativado", - "Disabled": "Desativado", - "Online": "Online", - "Offline": "Offline", - "Available": "Disponível", - "Unavailable": "Indisponível", - "Required": "Obrigatório", - "Optional": "Opcional", - "Default": "Padrão", - "Custom": "Personalizado", - "Advanced": "Avançado", - "Basic": "Básico", - "Help": "Ajuda", - "Support": "Suporte", - "Documentation": "Documentação", - "Version": "Versão", - "Language": "Idioma", - "Theme": "Tema", - "Light": "Claro", - "Dark": "Escuro", - "Auto": "Automático", - "Endpoint": "Ponto de extremidade", - "Providers": "Provedores", - "Combos": "Combinações", - "Usage": "Estatísticas", - "Quota Tracker": "Rastreador de cota", - "MITM": "MITM", - "CLI Tools": "Ferramentas CLI", - "Console Log": "Log do console", - "System": "Sistema", - "Debug": "Depuração", - "Shutdown": "Desligar", - "Close Proxy": "Fechar proxy", - "Are you sure you want to close the proxy server?": "Tem certeza de que deseja fechar o servidor proxy?", - "Server Disconnected": "Servidor desconectado", - "The proxy server has been stopped.": "O servidor proxy foi parado.", - "Reload Page": "Recarregar página", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "O serviço está em execução no terminal. Você pode fechar esta página da web. O desligamento interromperá o serviço.", - "Manage your AI provider connections": "Gerencie suas conexões de provedor de IA", - "Model combos with fallback": "Combinações de modelos com fallback", - "Monitor your API usage, token consumption, and request logs": "Monitore seu uso de API, consumo de tokens e logs de solicitação", - "Intercept CLI tool traffic and route through 9Router": "Intercepte o tráfego da ferramenta CLI e roteie através do 9Router", - "Configure CLI tools": "Configurar ferramentas CLI", - "API endpoint configuration": "Configuração do ponto de extremidade da API", - "Manage your preferences": "Gerenciar suas preferências", - "Debug translation flow between formats": "Depurar fluxo de tradução entre formatos", - "Live server console output": "Saída do console do servidor ao vivo", - "Create model combos with fallback support": "Crie combinações de modelos com suporte a fallback", - "Local Mode": "Modo local", - "Running on your machine": "Executando em sua máquina", - "Database Location": "Localização do banco de dados", - "Download Backup": "Baixar backup", - "Import Backup": "Importar backup", - "Database backup downloaded": "Backup do banco de dados baixado", - "Database imported successfully": "Banco de dados importado com sucesso", - "Security": "Segurança", - "Require login": "Exigir login", - "When ON, dashboard requires password. When OFF, access without login.": "Quando ATIVO, o painel requer senha. Quando DESATIVO, acesso sem login.", - "Current Password": "Senha atual", - "Enter current password": "Digite a senha atual", - "New Password": "Nova senha", - "Enter new password": "Digite a nova senha", - "Confirm New Password": "Confirmar nova senha", - "Confirm new password": "Confirme a nova senha", - "Update Password": "Atualizar senha", - "Set Password": "Definir senha", - "Password updated successfully": "Senha atualizada com sucesso", - "Passwords do not match": "As senhas não correspondem", - "Routing Strategy": "Estratégia de roteamento", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Percorrer contas para distribuir carga", - "Sticky Limit": "Limite pegajoso", - "Calls per account before switching": "Chamadas por conta antes de alternar", - "Network": "Rede", - "Outbound Proxy": "Proxy de saída", - "Enable proxy for OAuth + provider outbound requests.": "Ativar proxy para OAuth + solicitações de saída do provedor.", - "Proxy URL": "URL do proxy", - "Leave empty to inherit existing env proxy (if any).": "Deixe em branco para herdar o proxy env existente (se houver).", - "No Proxy": "Sem proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Nomes de host/domínios separados por vírgula para contornar o proxy.", - "Test proxy URL": "Testar URL do proxy", - "Apply": "Aplicar", - "Proxy settings applied": "Configurações de proxy aplicadas", - "Proxy enabled": "Proxy ativado", - "Proxy disabled": "Proxy desativado", - "Proxy test OK": "Teste de proxy OK", - "Proxy test failed": "Falha no teste de proxy", - "Please enter a Proxy URL to test": "Por favor, digite uma URL de proxy para testar", - "Observability": "Observabilidade", - "Enable Observability": "Ativar observabilidade", - "Turn request detail recording on/off globally": "Ativar/desativar globalmente o registro de detalhes da solicitação", - "Max Records": "Número máximo de registros", - "Maximum request detail records to keep (older records are auto-deleted)": "Número máximo de registros de detalhes de solicitação a manter (registros antigos são excluídos automaticamente)", - "Batch Size": "Tamanho do lote", - "Number of items to accumulate before writing to database (higher = better performance)": "Número de itens a acumular antes de gravar no banco de dados (maior = melhor desempenho)", - "Flush Interval (ms)": "Intervalo de liberação (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Tempo máximo de espera antes de liberar o buffer (evita perda de dados durante baixo tráfego)", - "Max JSON Size (KB)": "Tamanho máximo de JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Tamanho máximo para cada campo JSON (solicitação/resposta) antes do truncamento", - "All data stored on your machine": "Todos os dados armazenados em sua máquina", - "MITM Server": "Servidor MITM", - "Running": "Executando", - "Stopped": "Parado", - "Cert": "Certificado", - "Server": "Servidor", - "Purpose:": "Propósito:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Use Antigravity IDE e GitHub Copilot → com QUALQUER provedor/modelo do 9Router", - "How it works:": "Como funciona:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Solicitação do Antigravity/Copilot IDE → Redirecionamento DNS para localhost:443 → Proxy MITM intercepta → 9Router → resposta para Antigravity/Copilot", - "API Key": "Chave API", - "No API keys — create one in Keys page": "Sem chaves de API — crie uma na página Chaves", - "sk_9router (default)": "sk_9router (padrão)", - "Server started": "Servidor iniciado", - "Failed to start server": "Falha ao iniciar o servidor", - "Server stopped — all DNS cleared": "Servidor parado — todo DNS foi limpo", - "Failed to stop server": "Falha ao parar o servidor", - "Sudo password is required": "Senha sudo é necessária", - "Stop Server": "Parar servidor", - "Start Server": "Iniciar servidor", - "Enable DNS per tool below to activate interception": "Ativar DNS para cada ferramenta abaixo para ativar a interceptação", - "Sudo Password Required": "Senha Sudo necessária", - "Enter your sudo password to start/stop MITM server": "Digite sua senha sudo para iniciar/parar o servidor MITM", - "Sudo Password": "Senha sudo", - "Confirm": "Confirmar" -} diff --git a/9router/public/i18n/literals/pt-PT.json b/9router/public/i18n/literals/pt-PT.json deleted file mode 100644 index 0d3719f8..00000000 --- a/9router/public/i18n/literals/pt-PT.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Cancelar", - "Delete": "Eliminar", - "Edit": "Editar", - "Save": "Guardar", - "Close": "Fechar", - "Add": "Adicionar", - "Remove": "Remover", - "Settings": "Definições", - "Profile": "Perfil", - "Dashboard": "Painel de controlo", - "Logout": "Terminar sessão", - "Login": "Iniciar sessão", - "Providers": "Fornecedores", - "Usage": "Utilização", - "API Key": "Chave API", - "Connected": "Ligado", - "Disconnected": "Desligado", - "Active": "Ativo", - "Inactive": "Inativo", - "Success": "Sucesso", - "Failed": "Falha", - "Error": "Erro", - "Warning": "Aviso", - "Info": "Informações", - "Loading": "A carregar", - "Search": "Pesquisar", - "Filter": "Filtrar", - "Sort": "Ordenar", - "Export": "Exportar", - "Import": "Importar", - "Refresh": "Atualizar", - "Back": "Voltar", - "Next": "Seguinte", - "Previous": "Anterior", - "Submit": "Enviar", - "Confirm": "Confirmar", - "Yes": "Sim", - "No": "Não", - "OK": "OK", - "Apply": "Aplicar", - "Reset": "Repor", - "Clear": "Limpar", - "Select": "Selecionar", - "Upload": "Carregar", - "Download": "Descarregar", - "Copy": "Copiar", - "Paste": "Colar", - "Cut": "Cortar", - "Undo": "Desfazer", - "Redo": "Refazer", - "Name": "Nome", - "Description": "Descrição", - "Status": "Estado", - "Type": "Tipo", - "Date": "Data", - "Time": "Hora", - "Created": "Criado", - "Updated": "Atualizado", - "Actions": "Ações", - "Details": "Detalhes", - "View": "Ver", - "New": "Novo", - "Total": "Total", - "Count": "Contagem", - "Price": "Preço", - "Cost": "Custo", - "Free": "Gratuito", - "Paid": "Pago", - "Enable": "Ativar", - "Disable": "Desativar", - "Enabled": "Ativado", - "Disabled": "Desativado", - "Online": "Online", - "Offline": "Offline", - "Available": "Disponível", - "Unavailable": "Indisponível", - "Required": "Obrigatório", - "Optional": "Opcional", - "Default": "Predefinição", - "Custom": "Personalizado", - "Advanced": "Avançado", - "Basic": "Básico", - "Help": "Ajuda", - "Support": "Suporte", - "Documentation": "Documentação", - "Version": "Versão", - "Language": "Idioma", - "Theme": "Tema", - "Light": "Claro", - "Dark": "Escuro", - "Auto": "Automático", - "Endpoint": "Ponto final", - "Providers": "Fornecedores", - "Combos": "Combinações", - "Usage": "Estatísticas", - "Quota Tracker": "Rastreador de quota", - "MITM": "MITM", - "CLI Tools": "Ferramentas CLI", - "Console Log": "Registo da consola", - "System": "Sistema", - "Debug": "Depuração", - "Shutdown": "Encerramento", - "Close Proxy": "Fechar proxy", - "Are you sure you want to close the proxy server?": "Tem a certeza de que deseja fechar o servidor proxy?", - "Server Disconnected": "Servidor desligado", - "The proxy server has been stopped.": "O servidor proxy foi parado.", - "Reload Page": "Recarregar página", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "O serviço está em execução no terminal. Pode fechar esta página da web. O encerramento irá parar o serviço.", - "Manage your AI provider connections": "Gerir as suas ligações de fornecedor de IA", - "Model combos with fallback": "Combinações de modelos com contingência", - "Monitor your API usage, token consumption, and request logs": "Monitorize a sua utilização de API, consumo de fichas e registos de pedidos", - "Intercept CLI tool traffic and route through 9Router": "Intercete o tráfego da ferramenta CLI e encaminhe através do 9Router", - "Configure CLI tools": "Configurar ferramentas CLI", - "API endpoint configuration": "Configuração do ponto final da API", - "Manage your preferences": "Gerir as suas preferências", - "Debug translation flow between formats": "Depurar fluxo de tradução entre formatos", - "Live server console output": "Saída da consola do servidor em direto", - "Create model combos with fallback support": "Criar combinações de modelos com suporte a contingência", - "Local Mode": "Modo local", - "Running on your machine": "Em execução na sua máquina", - "Database Location": "Localização da base de dados", - "Download Backup": "Descarregar cópia de segurança", - "Import Backup": "Importar cópia de segurança", - "Database backup downloaded": "Cópia de segurança da base de dados descarregada", - "Database imported successfully": "Base de dados importada com sucesso", - "Security": "Segurança", - "Require login": "Requer início de sessão", - "When ON, dashboard requires password. When OFF, access without login.": "Quando ATIVO, o painel requer palavra-passe. Quando DESATIVO, acesso sem iniciar sessão.", - "Current Password": "Palavra-passe atual", - "Enter current password": "Introduza a palavra-passe atual", - "New Password": "Nova palavra-passe", - "Enter new password": "Introduza a nova palavra-passe", - "Confirm New Password": "Confirmar nova palavra-passe", - "Confirm new password": "Confirme a nova palavra-passe", - "Update Password": "Atualizar palavra-passe", - "Set Password": "Definir palavra-passe", - "Password updated successfully": "Palavra-passe atualizada com sucesso", - "Passwords do not match": "As palavras-passe não coincidem", - "Routing Strategy": "Estratégia de encaminhamento", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Passar por contas para distribuir carga", - "Sticky Limit": "Limite aderente", - "Calls per account before switching": "Chamadas por conta antes de mudar", - "Network": "Rede", - "Outbound Proxy": "Proxy de saída", - "Enable proxy for OAuth + provider outbound requests.": "Ativar proxy para OAuth + pedidos de saída do fornecedor.", - "Proxy URL": "URL do proxy", - "Leave empty to inherit existing env proxy (if any).": "Deixe em branco para herdar o proxy env existente (se houver).", - "No Proxy": "Sem proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Nomes de host/domínios separados por vírgulas para contornar o proxy.", - "Test proxy URL": "Testar URL do proxy", - "Apply": "Aplicar", - "Proxy settings applied": "Definições de proxy aplicadas", - "Proxy enabled": "Proxy ativado", - "Proxy disabled": "Proxy desativado", - "Proxy test OK": "Teste de proxy OK", - "Proxy test failed": "Falha no teste de proxy", - "Please enter a Proxy URL to test": "Introduza um URL de proxy para testar", - "Observability": "Observabilidade", - "Enable Observability": "Ativar observabilidade", - "Turn request detail recording on/off globally": "Ativar/desativar globalmente o registo de detalhes de pedidos", - "Max Records": "Número máximo de registos", - "Maximum request detail records to keep (older records are auto-deleted)": "Número máximo de registos de detalhes de pedidos a manter (registos mais antigos são eliminados automaticamente)", - "Batch Size": "Tamanho do lote", - "Number of items to accumulate before writing to database (higher = better performance)": "Número de itens a acumular antes de gravar na base de dados (mais alto = melhor desempenho)", - "Flush Interval (ms)": "Intervalo de limpeza (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Tempo máximo de espera antes de limpar o buffer (evita perda de dados durante tráfego baixo)", - "Max JSON Size (KB)": "Tamanho máximo de JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Tamanho máximo para cada campo JSON (pedido/resposta) antes do truncamento", - "All data stored on your machine": "Todos os dados armazenados na sua máquina", - "MITM Server": "Servidor MITM", - "Running": "Em execução", - "Stopped": "Parado", - "Cert": "Certificado", - "Server": "Servidor", - "Purpose:": "Finalidade:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Utilize o Antigravity IDE e GitHub Copilot → com QUALQUER fornecedor/modelo do 9Router", - "How it works:": "Como funciona:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Pedido do Antigravity/Copilot IDE → Redirecionamento DNS para localhost:443 → Proxy MITM interceta → 9Router → resposta para Antigravity/Copilot", - "API Key": "Chave API", - "No API keys — create one in Keys page": "Sem chaves de API — crie uma na página Chaves", - "sk_9router (default)": "sk_9router (predefinição)", - "Server started": "Servidor iniciado", - "Failed to start server": "Falha ao iniciar o servidor", - "Server stopped — all DNS cleared": "Servidor parado — todo DNS foi limpo", - "Failed to stop server": "Falha ao parar o servidor", - "Sudo password is required": "Palavra-passe sudo é necessária", - "Stop Server": "Parar servidor", - "Start Server": "Iniciar servidor", - "Enable DNS per tool below to activate interception": "Ativar DNS para cada ferramenta abaixo para ativar a interceção", - "Sudo Password Required": "Palavra-passe Sudo necessária", - "Enter your sudo password to start/stop MITM server": "Introduza a sua palavra-passe sudo para iniciar/parar o servidor MITM", - "Sudo Password": "Palavra-passe sudo", - "Confirm": "Confirmar" -} diff --git a/9router/public/i18n/literals/ro.json b/9router/public/i18n/literals/ro.json deleted file mode 100644 index bd599f8f..00000000 --- a/9router/public/i18n/literals/ro.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Anulare", - "Delete": "Ștergere", - "Edit": "Editare", - "Save": "Salvare", - "Close": "Închidere", - "Add": "Adăugare", - "Remove": "Eliminare", - "Settings": "Setări", - "Profile": "Profil", - "Dashboard": "Tablou de bord", - "Logout": "Ieșire", - "Login": "Conectare", - "Providers": "Furnizori", - "Usage": "Utilizare", - "API Key": "Cheie API", - "Connected": "Conectat", - "Disconnected": "Deconectat", - "Active": "Activ", - "Inactive": "Inactiv", - "Success": "Succes", - "Failed": "Eşuat", - "Error": "Eroare", - "Warning": "Avertisment", - "Info": "Informații", - "Loading": "Se încarcă", - "Search": "Căutare", - "Filter": "Filtru", - "Sort": "Sortare", - "Export": "Exportare", - "Import": "Importare", - "Refresh": "Reîmprospătare", - "Back": "Înapoi", - "Next": "Următorul", - "Previous": "Anterior", - "Submit": "Trimitere", - "Confirm": "Confirmare", - "Yes": "Da", - "No": "Nu", - "OK": "OK", - "Apply": "Aplicare", - "Reset": "Resetare", - "Clear": "Curățare", - "Select": "Selectare", - "Upload": "Încărcare", - "Download": "Descărcare", - "Copy": "Copiere", - "Paste": "Lipire", - "Cut": "Tăiere", - "Undo": "Anulare", - "Redo": "Refacere", - "Name": "Nume", - "Description": "Descriere", - "Status": "Status", - "Type": "Tip", - "Date": "Data", - "Time": "Oră", - "Created": "Creat", - "Updated": "Actualizat", - "Actions": "Acțiuni", - "Details": "Detalii", - "View": "Vizualizare", - "New": "Nou", - "Total": "Total", - "Count": "Număr", - "Price": "Preț", - "Cost": "Cost", - "Free": "Gratuit", - "Paid": "Plătit", - "Enable": "Activare", - "Disable": "Dezactivare", - "Enabled": "Activat", - "Disabled": "Dezactivat", - "Online": "Online", - "Offline": "Offline", - "Available": "Disponibil", - "Unavailable": "Indisponibil", - "Required": "Necesar", - "Optional": "Opțional", - "Default": "Implicit", - "Custom": "Personalizat", - "Advanced": "Avansat", - "Basic": "De bază", - "Help": "Ajutor", - "Support": "Suport", - "Documentation": "Documentație", - "Version": "Versiune", - "Language": "Limbă", - "Theme": "Temă", - "Light": "Lumină", - "Dark": "Întunecat", - "Auto": "Automat", - "Endpoint": "Punct final", - "Providers": "Furnizori", - "Combos": "Combinații", - "Usage": "Statistici de utilizare", - "Quota Tracker": "Urmăritor cote", - "MITM": "MITM", - "CLI Tools": "Instrumente", - "Console Log": "Jurnal consolă", - "System": "Sistem", - "Debug": "Depanare", - "Shutdown": "Oprire", - "Close Proxy": "Închidere proxy", - "Are you sure you want to close the proxy server?": "Sunteti sigur că doriți să închideți serverul proxy?", - "Server Disconnected": "Server deconectat", - "The proxy server has been stopped.": "Serverul proxy a fost oprit.", - "Reload Page": "Reîncărcare pagină", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Serviciul rulează în terminal. Puteți închide această pagină web. Oprirea va opri serviciul.", - "Manage your AI provider connections": "Gestionați conexiunile furnizorului dvs. de IA", - "Model combos with fallback": "Combinații de modele cu fallback", - "Monitor your API usage, token consumption, and request logs": "Monitorizați utilizarea API-ului, consumul de token și jurnalele de solicitare", - "Intercept CLI tool traffic and route through 9Router": "Interceptați traficul instrumentului CLI și rutați prin 9Router", - "Configure CLI tools": "Configurați instrumentele CLI", - "API endpoint configuration": "Configurația punctului final API", - "Manage your preferences": "Gestionați preferințele dvs.", - "Debug translation flow between formats": "Depanare fluxului de traducere între formate", - "Live server console output": "Ieșire consolă server în direct", - "Create model combos with fallback support": "Creați combinații de modele cu suport fallback", - "Local Mode": "Mod local", - "Running on your machine": "Rulează pe mașina dvs.", - "Database Location": "Locația bazei de date", - "Download Backup": "Descărcare copie de rezervă", - "Import Backup": "Importare copie de rezervă", - "Database backup downloaded": "Copia de rezervă a bazei de date a fost descărcată", - "Database imported successfully": "Baza de date a fost importată cu succes", - "Security": "Securitate", - "Require login": "Necesită conectare", - "When ON, dashboard requires password. When OFF, access without login.": "Când este PORNIT, tabloul de bord necesită parolă. Când este OPRIT, accesați fără conectare.", - "Current Password": "Parola actuală", - "Enter current password": "Introduceți parola actuală", - "New Password": "Parola nouă", - "Enter new password": "Introduceți parola nouă", - "Confirm New Password": "Confirmați parola nouă", - "Confirm new password": "Confirmați parola nouă", - "Update Password": "Actualizare parolă", - "Set Password": "Setare parolă", - "Password updated successfully": "Parola a fost actualizată cu succes", - "Passwords do not match": "Parolele nu coincid", - "Routing Strategy": "Strategie de rutare", - "Round Robin": "Tur în jurul", - "Cycle through accounts to distribute load": "Ciclați prin conturi pentru a distribui sarcina", - "Sticky Limit": "Limită lipicioasă", - "Calls per account before switching": "Apeluri pe cont înainte de comutare", - "Network": "Rețea", - "Outbound Proxy": "Proxy de ieșire", - "Enable proxy for OAuth + provider outbound requests.": "Activați proxy-ul pentru solicitări de ieșire OAuth + furnizor.", - "Proxy URL": "URL proxy", - "Leave empty to inherit existing env proxy (if any).": "Lăsați gol pentru a moșteni proxy-ul env existent (dacă există).", - "No Proxy": "Nicio delegare", - "Comma-separated hostnames/domains to bypass the proxy.": "Nume de gazdă/domenii separate prin virgulă pentru a ocoli proxy-ul.", - "Test proxy URL": "Testare URL proxy", - "Apply": "Aplicare", - "Proxy settings applied": "Setările proxy au fost aplicate", - "Proxy enabled": "Proxy activat", - "Proxy disabled": "Proxy dezactivat", - "Proxy test OK": "Testul proxy OK", - "Proxy test failed": "Testul proxy a eșuat", - "Please enter a Proxy URL to test": "Vă rugăm să introduceți un URL proxy pentru a testa", - "Observability": "Observabilitate", - "Enable Observability": "Activare observabilitate", - "Turn request detail recording on/off globally": "Porniți/opriți înregistrarea detaliilor solicitării la nivel global", - "Max Records": "Înregistrări maxime", - "Maximum request detail records to keep (older records are auto-deleted)": "Înregistrări de detalii de solicitare maxime de păstrat (înregistrările mai vechi sunt șterse automat)", - "Batch Size": "Dimensiune lot", - "Number of items to accumulate before writing to database (higher = better performance)": "Numărul de articole de acumulat înainte de a scrie în baza de date (mai mare = performanță mai bună)", - "Flush Interval (ms)": "Interval de golire (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Timp maxim de așteptare înainte de golirea bufferului (previne pierderea datelor în condiții de trafic redus)", - "Max JSON Size (KB)": "Dimensiune JSON maximă (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Dimensiune maximă pentru fiecare câmp JSON (solicitare/răspuns) înainte de trunchiere", - "All data stored on your machine": "Toate datele sunt stocate pe mașina dvs.", - "MITM Server": "Server MITM", - "Running": "Se execută", - "Stopped": "Oprit", - "Cert": "Certificat", - "Server": "Server", - "Purpose:": "Scop:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Folosiți Antigravity IDE & GitHub Copilot → cu ORICE furnizor/model din 9Router", - "How it works:": "Cum funcționează:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Solicitare Antigravity/Copilot IDE → Redirecționare DNS la localhost:443 → Proxy MITM interceptează → 9Router → răspuns la Antigravity/Copilot", - "API Key": "Cheie API", - "No API keys — create one in Keys page": "Nicio cheie API — creați una în pagina Chei", - "sk_9router (default)": "sk_9router (implicit)", - "Server started": "Server pornit", - "Failed to start server": "Nu s-a putut porni serverul", - "Server stopped — all DNS cleared": "Server oprit — toate DNS-urile șterse", - "Failed to stop server": "Nu s-a putut opri serverul", - "Sudo password is required": "Parola sudo este necesară", - "Stop Server": "Oprire server", - "Start Server": "Server de pornire", - "Enable DNS per tool below to activate interception": "Activați DNS pentru fiecare instrument de mai jos pentru a activa interceptarea", - "Sudo Password Required": "Parola Sudo este necesară", - "Enter your sudo password to start/stop MITM server": "Introduceți parola sudo pentru a porni/opri serverul MITM", - "Sudo Password": "Parola Sudo", - "Confirm": "Confirmare" -} diff --git a/9router/public/i18n/literals/ru.json b/9router/public/i18n/literals/ru.json deleted file mode 100644 index 779a8bd0..00000000 --- a/9router/public/i18n/literals/ru.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Отменить", - "Delete": "Удалить", - "Edit": "Редактировать", - "Save": "Сохранить", - "Close": "Закрыть", - "Add": "Добавить", - "Remove": "Удалить", - "Settings": "Настройки", - "Profile": "Профиль", - "Dashboard": "Панель управления", - "Logout": "Выход", - "Login": "Вход", - "Providers": "Провайдеры", - "Usage": "Использование", - "API Key": "Ключ API", - "Connected": "Подключено", - "Disconnected": "Отключено", - "Active": "Активно", - "Inactive": "Неактивно", - "Success": "Успех", - "Failed": "Ошибка", - "Error": "Ошибка", - "Warning": "Предупреждение", - "Info": "Информация", - "Loading": "Загрузка", - "Search": "Поиск", - "Filter": "Фильтр", - "Sort": "Сортировка", - "Export": "Экспорт", - "Import": "Импорт", - "Refresh": "Обновить", - "Back": "Назад", - "Next": "Далее", - "Previous": "Назад", - "Submit": "Отправить", - "Confirm": "Подтвердить", - "Yes": "Да", - "No": "Нет", - "OK": "OK", - "Apply": "Применить", - "Reset": "Сбросить", - "Clear": "Очистить", - "Select": "Выбрать", - "Upload": "Загрузить", - "Download": "Скачать", - "Copy": "Копировать", - "Paste": "Вставить", - "Cut": "Вырезать", - "Undo": "Отменить", - "Redo": "Повторить", - "Name": "Имя", - "Description": "Описание", - "Status": "Статус", - "Type": "Тип", - "Date": "Дата", - "Time": "Время", - "Created": "Создано", - "Updated": "Обновлено", - "Actions": "Действия", - "Details": "Подробности", - "View": "Просмотр", - "New": "Новый", - "Total": "Всего", - "Count": "Количество", - "Price": "Цена", - "Cost": "Стоимость", - "Free": "Бесплатно", - "Paid": "Платно", - "Enable": "Включить", - "Disable": "Отключить", - "Enabled": "Включено", - "Disabled": "Отключено", - "Online": "Онлайн", - "Offline": "Офлайн", - "Available": "Доступно", - "Unavailable": "Недоступно", - "Required": "Обязательно", - "Optional": "Опционально", - "Default": "По умолчанию", - "Custom": "Пользовательский", - "Advanced": "Дополнительно", - "Basic": "Основной", - "Help": "Справка", - "Support": "Поддержка", - "Documentation": "Документация", - "Version": "Версия", - "Language": "Язык", - "Theme": "Тема", - "Light": "Светлая", - "Dark": "Темная", - "Auto": "Автоматически", - "Endpoint": "Конечная точка", - "Providers": "Провайдеры", - "Combos": "Комбинации", - "Usage": "Статистика", - "Quota Tracker": "Отслеживание квоты", - "MITM": "MITM", - "CLI Tools": "Инструменты CLI", - "Console Log": "Журнал консоли", - "System": "Система", - "Debug": "Отладка", - "Shutdown": "Завершение", - "Close Proxy": "Закрыть прокси", - "Are you sure you want to close the proxy server?": "Вы уверены, что хотите закрыть прокси-сервер?", - "Server Disconnected": "Сервер отключен", - "The proxy server has been stopped.": "Прокси-сервер был остановлен.", - "Reload Page": "Перезагрузить страницу", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Сервис работает в терминале. Вы можете закрыть эту веб-страницу. Завершение остановит сервис.", - "Manage your AI provider connections": "Управляйте своими подключениями провайдера ИИ", - "Model combos with fallback": "Комбинации моделей с резервным вариантом", - "Monitor your API usage, token consumption, and request logs": "Мониторьте использование API, потребление токенов и журналы запросов", - "Intercept CLI tool traffic and route through 9Router": "Перехватите трафик инструмента CLI и маршрутизируйте через 9Router", - "Configure CLI tools": "Настройка инструментов CLI", - "API endpoint configuration": "Конфигурация конечной точки API", - "Manage your preferences": "Управляйте своими предпочтениями", - "Debug translation flow between formats": "Отладка потока трансляции между форматами", - "Live server console output": "Вывод консоли сервера в реальном времени", - "Create model combos with fallback support": "Создание комбинаций моделей с поддержкой резервного варианта", - "Local Mode": "Локальный режим", - "Running on your machine": "Работает на вашем компьютере", - "Database Location": "Расположение базы данных", - "Download Backup": "Загрузить резервную копию", - "Import Backup": "Импортировать резервную копию", - "Database backup downloaded": "Резервная копия базы данных загружена", - "Database imported successfully": "База данных успешно импортирована", - "Security": "Безопасность", - "Require login": "Требовать вход", - "When ON, dashboard requires password. When OFF, access without login.": "Когда ВКЛЮЧЕНО, панель управления требует пароль. Когда ОТКЛЮЧЕНО, доступ без входа.", - "Current Password": "Текущий пароль", - "Enter current password": "Введите текущий пароль", - "New Password": "Новый пароль", - "Enter new password": "Введите новый пароль", - "Confirm New Password": "Подтвердить новый пароль", - "Confirm new password": "Подтвердите новый пароль", - "Update Password": "Обновить пароль", - "Set Password": "Установить пароль", - "Password updated successfully": "Пароль успешно обновлен", - "Passwords do not match": "Пароли не совпадают", - "Routing Strategy": "Стратегия маршрутизации", - "Round Robin": "Циклическая выборка", - "Cycle through accounts to distribute load": "Чередование аккаунтов для распределения нагрузки", - "Sticky Limit": "Липкий предел", - "Calls per account before switching": "Вызовов на аккаунт перед переключением", - "Network": "Сеть", - "Outbound Proxy": "Исходящий прокси", - "Enable proxy for OAuth + provider outbound requests.": "Включить прокси для OAuth + исходящих запросов провайдера.", - "Proxy URL": "URL прокси", - "Leave empty to inherit existing env proxy (if any).": "Оставьте пусто, чтобы унаследовать существующий прокси env (если есть).", - "No Proxy": "Без прокси", - "Comma-separated hostnames/domains to bypass the proxy.": "Разделенные запятыми имена хостов/домены для обхода прокси.", - "Test proxy URL": "Протестировать URL прокси", - "Apply": "Применить", - "Proxy settings applied": "Параметры прокси применены", - "Proxy enabled": "Прокси включен", - "Proxy disabled": "Прокси отключен", - "Proxy test OK": "Тест прокси OK", - "Proxy test failed": "Тест прокси не пройден", - "Please enter a Proxy URL to test": "Пожалуйста, введите URL прокси для тестирования", - "Observability": "Наблюдаемость", - "Enable Observability": "Включить наблюдаемость", - "Turn request detail recording on/off globally": "Включить/отключить запись деталей запроса глобально", - "Max Records": "Максимум записей", - "Maximum request detail records to keep (older records are auto-deleted)": "Максимальное количество записей деталей запроса для сохранения (старые записи автоматически удаляются)", - "Batch Size": "Размер пакета", - "Number of items to accumulate before writing to database (higher = better performance)": "Количество элементов для накопления перед записью в базу данных (выше = лучшая производительность)", - "Flush Interval (ms)": "Интервал очистки (мс)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Максимальное время ожидания перед очисткой буфера (предотвращает потерю данных при низком трафике)", - "Max JSON Size (KB)": "Максимальный размер JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Максимальный размер каждого поля JSON (запрос/ответ) перед усечением", - "All data stored on your machine": "Все данные хранятся на вашем компьютере", - "MITM Server": "Сервер MITM", - "Running": "Работает", - "Stopped": "Остановлено", - "Cert": "Сертификат", - "Server": "Сервер", - "Purpose:": "Назначение:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Используйте Antigravity IDE и GitHub Copilot → с ЛЮБЫМ провайдером/моделью от 9Router", - "How it works:": "Как это работает:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Запрос Antigravity/Copilot IDE → Перенаправление DNS на localhost:443 → Прокси MITM перехватывает → 9Router → ответ для Antigravity/Copilot", - "API Key": "Ключ API", - "No API keys — create one in Keys page": "Нет ключей API — создайте один на странице ключей", - "sk_9router (default)": "sk_9router (по умолчанию)", - "Server started": "Сервер запущен", - "Failed to start server": "Ошибка при запуске сервера", - "Server stopped — all DNS cleared": "Сервер остановлен — все DNS очищено", - "Failed to stop server": "Ошибка при остановке сервера", - "Sudo password is required": "Требуется пароль sudo", - "Stop Server": "Остановить сервер", - "Start Server": "Запустить сервер", - "Enable DNS per tool below to activate interception": "Включите DNS для каждого инструмента ниже, чтобы активировать перехват", - "Sudo Password Required": "Требуется пароль Sudo", - "Enter your sudo password to start/stop MITM server": "Введите пароль sudo для запуска/остановки сервера MITM", - "Sudo Password": "Пароль sudo", - "Confirm": "Подтвердить" -} diff --git a/9router/public/i18n/literals/sv.json b/9router/public/i18n/literals/sv.json deleted file mode 100644 index e77b89b0..00000000 --- a/9router/public/i18n/literals/sv.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Avbryt", - "Delete": "Ta bort", - "Edit": "Redigera", - "Save": "Spara", - "Close": "Stäng", - "Add": "Lägg till", - "Remove": "Ta bort", - "Settings": "Inställningar", - "Profile": "Profil", - "Dashboard": "Instrumentpanel", - "Logout": "Logga ut", - "Login": "Logga in", - "Providers": "Leverantörer", - "Usage": "Användning", - "API Key": "API-nyckel", - "Connected": "Ansluten", - "Disconnected": "Frånkopplad", - "Active": "Aktiv", - "Inactive": "Inaktiv", - "Success": "Framgång", - "Failed": "Misslyckad", - "Error": "Fel", - "Warning": "Varning", - "Info": "Information", - "Loading": "Laddar", - "Search": "Sök", - "Filter": "Filter", - "Sort": "Sortera", - "Export": "Exportera", - "Import": "Importera", - "Refresh": "Uppdatera", - "Back": "Tillbaka", - "Next": "Nästa", - "Previous": "Föregående", - "Submit": "Skicka", - "Confirm": "Bekräfta", - "Yes": "Ja", - "No": "Nej", - "OK": "OK", - "Apply": "Verkställ", - "Reset": "Återställ", - "Clear": "Rensa", - "Select": "Välj", - "Upload": "Ladda upp", - "Download": "Ladda ner", - "Copy": "Kopiera", - "Paste": "Klistra in", - "Cut": "Klipp ut", - "Undo": "Ångra", - "Redo": "Gör om", - "Name": "Namn", - "Description": "Beskrivning", - "Status": "Status", - "Type": "Typ", - "Date": "Datum", - "Time": "Tid", - "Created": "Skapad", - "Updated": "Uppdaterad", - "Actions": "Åtgärder", - "Details": "Detaljer", - "View": "Visa", - "New": "Ny", - "Total": "Totalt", - "Count": "Antal", - "Price": "Pris", - "Cost": "Kostnad", - "Free": "Gratis", - "Paid": "Betald", - "Enable": "Aktivera", - "Disable": "Inaktivera", - "Enabled": "Aktiverad", - "Disabled": "Inaktiverad", - "Online": "Online", - "Offline": "Offline", - "Available": "Tillgänglig", - "Unavailable": "Inte tillgänglig", - "Required": "Krävs", - "Optional": "Valfritt", - "Default": "Standard", - "Custom": "Anpassad", - "Advanced": "Avancerat", - "Basic": "Grundläggande", - "Help": "Hjälp", - "Support": "Support", - "Documentation": "Dokumentation", - "Version": "Version", - "Language": "Språk", - "Theme": "Tema", - "Light": "Ljus", - "Dark": "Mörk", - "Auto": "Automatisk", - "Endpoint": "Slutpunkt", - "Providers": "Leverantörer", - "Combos": "Kombinationer", - "Usage": "Användarstatistik", - "Quota Tracker": "Kvotspårare", - "MITM": "MITM", - "CLI Tools": "Verktyg", - "Console Log": "Konsollogg", - "System": "System", - "Debug": "Felsökning", - "Shutdown": "Stänga av", - "Close Proxy": "Stäng proxy", - "Are you sure you want to close the proxy server?": "Är du säker på att du vill stänga proxyservern?", - "Server Disconnected": "Server frånkopplad", - "The proxy server has been stopped.": "Proxyservern har stoppats.", - "Reload Page": "Läs in sidan på nytt", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Tjänsten körs i terminalen. Du kan stänga denna webbsida. Avstängning stoppar tjänsten.", - "Manage your AI provider connections": "Hantera dina AI-leverantörsanslutningar", - "Model combos with fallback": "Modellkombinationer med fallback", - "Monitor your API usage, token consumption, and request logs": "Övervaka din API-användning, tokenförbrukning och begärandeloggar", - "Intercept CLI tool traffic and route through 9Router": "Avlyssna CLI-verktygstrafik och dirigera genom 9Router", - "Configure CLI tools": "Konfigurera CLI-verktyg", - "API endpoint configuration": "Konfiguration av API-slutpunkt", - "Manage your preferences": "Hantera dina inställningar", - "Debug translation flow between formats": "Felsöka översättningsflöde mellan format", - "Live server console output": "Live-serverkonsoloutdata", - "Create model combos with fallback support": "Skapa modellkombinationer med fallback-stöd", - "Local Mode": "Lokalt läge", - "Running on your machine": "Körs på din maskin", - "Database Location": "Databasplats", - "Download Backup": "Ladda ner säkerhetskopia", - "Import Backup": "Importera säkerhetskopia", - "Database backup downloaded": "Databassäkerhetskopia nedladdad", - "Database imported successfully": "Databasen importerades framgångsrikt", - "Security": "Säkerhet", - "Require login": "Kräv inloggning", - "When ON, dashboard requires password. When OFF, access without login.": "När ON krävs lösenord för instrumentpanelen. När OFF, åtkomst utan inloggning.", - "Current Password": "Aktuellt lösenord", - "Enter current password": "Ange aktuellt lösenord", - "New Password": "Nytt lösenord", - "Enter new password": "Ange nytt lösenord", - "Confirm New Password": "Bekräfta nytt lösenord", - "Confirm new password": "Bekräfta nytt lösenord", - "Update Password": "Uppdatera lösenord", - "Set Password": "Ange lösenord", - "Password updated successfully": "Lösenord uppdaterades framgångsrikt", - "Passwords do not match": "Lösenorden matchar inte", - "Routing Strategy": "Routningsstrategi", - "Round Robin": "Omväxling", - "Cycle through accounts to distribute load": "Cykla genom konton för att distribuera belastningen", - "Sticky Limit": "Klibbig gräns", - "Calls per account before switching": "Anrop per konto innan byte", - "Network": "Nätverk", - "Outbound Proxy": "Utgående proxy", - "Enable proxy for OAuth + provider outbound requests.": "Aktivera proxy för utgående OAuth + leverantörsförfrågningar.", - "Proxy URL": "Proxy-URL", - "Leave empty to inherit existing env proxy (if any).": "Lämna tomt för att ärva befintlig env-proxy (om sådan finns).", - "No Proxy": "Ingen proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Kommaseparerade värdnamn/domäner för att kringgå proxyn.", - "Test proxy URL": "Testa proxy-URL", - "Apply": "Verkställ", - "Proxy settings applied": "Proxyinställningar tillämpade", - "Proxy enabled": "Proxy aktiverad", - "Proxy disabled": "Proxy inaktiverad", - "Proxy test OK": "Proxy-test OK", - "Proxy test failed": "Proxy-test misslyckades", - "Please enter a Proxy URL to test": "Ange en proxy-URL att testa", - "Observability": "Observerbarhet", - "Enable Observability": "Aktivera observerbarhet", - "Turn request detail recording on/off globally": "Slå på/av inspelning av förfrågningsdetaljer globalt", - "Max Records": "Maximala poster", - "Maximum request detail records to keep (older records are auto-deleted)": "Maximala begärandedetaljposter att behålla (äldre poster raderas automatiskt)", - "Batch Size": "Batchstorlek", - "Number of items to accumulate before writing to database (higher = better performance)": "Antal objekt att ackumulera innan skrivning till databas (högre = bättre prestanda)", - "Flush Interval (ms)": "Spölningsintervall (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Maximal väntetid innan buffern spolas (förhindrar dataförlust vid låg trafik)", - "Max JSON Size (KB)": "Maximal JSON-storlek (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Maximal storlek för varje JSON-fält (begäran/svar) före trunkering", - "All data stored on your machine": "Alla data lagras på din maskin", - "MITM Server": "MITM-server", - "Running": "Körs", - "Stopped": "Stoppad", - "Cert": "Certifikat", - "Server": "Server", - "Purpose:": "Syfte:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Använd Antigravity IDE & GitHub Copilot → med VALFRI leverantör/modell från 9Router", - "How it works:": "Hur det fungerar:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE-begäran → DNS-omdirigering till localhost:443 → MITM-proxy avlyssnar → 9Router → svar till Antigravity/Copilot", - "API Key": "API-nyckel", - "No API keys — create one in Keys page": "Inga API-nycklar — skapa en på nyckelsidan", - "sk_9router (default)": "sk_9router (standard)", - "Server started": "Servern startad", - "Failed to start server": "Misslyckades att starta servern", - "Server stopped — all DNS cleared": "Server stoppad — all DNS rensad", - "Failed to stop server": "Misslyckades att stoppa servern", - "Sudo password is required": "Sudo-lösenord krävs", - "Stop Server": "Stoppa server", - "Start Server": "Starta server", - "Enable DNS per tool below to activate interception": "Aktivera DNS för varje verktyg nedan för att aktivera avlyssning", - "Sudo Password Required": "Sudo-lösenord krävs", - "Enter your sudo password to start/stop MITM server": "Ange ditt sudo-lösenord för att starta/stoppa MITM-servern", - "Sudo Password": "Sudo-lösenord", - "Confirm": "Bekräfta" -} diff --git a/9router/public/i18n/literals/th.json b/9router/public/i18n/literals/th.json deleted file mode 100644 index d0a7248e..00000000 --- a/9router/public/i18n/literals/th.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "ยกเลิก", - "Delete": "ลบ", - "Edit": "แก้ไข", - "Save": "บันทึก", - "Close": "ปิด", - "Add": "เพิ่ม", - "Remove": "นำออก", - "Settings": "การตั้งค่า", - "Profile": "โปรไฟล์", - "Dashboard": "แดชบอร์ด", - "Logout": "ออกจากระบบ", - "Login": "เข้าสู่ระบบ", - "Providers": "ผู้ให้บริการ", - "Usage": "การใช้งาน", - "API Key": "คีย์ API", - "Connected": "เชื่อมต่อแล้ว", - "Disconnected": "ตัดการเชื่อมต่อ", - "Active": "ใช้งาน", - "Inactive": "ไม่ใช้งาน", - "Success": "สำเร็จ", - "Failed": "ล้มเหลว", - "Error": "ข้อผิดพลาด", - "Warning": "คำเตือน", - "Info": "ข้อมูล", - "Loading": "กำลังโหลด", - "Search": "ค้นหา", - "Filter": "ตัวกรอง", - "Sort": "เรียงลำดับ", - "Export": "ส่งออก", - "Import": "นำเข้า", - "Refresh": "รีเฟรช", - "Back": "ย้อนกลับ", - "Next": "ถัดไป", - "Previous": "ก่อนหน้า", - "Submit": "ส่ง", - "Confirm": "ยืนยัน", - "Yes": "ใช่", - "No": "ไม่", - "OK": "ตกลง", - "Apply": "ใช้", - "Reset": "รีเซ็ต", - "Clear": "ล้าง", - "Select": "เลือก", - "Upload": "อัพโหลด", - "Download": "ดาวน์โหลด", - "Copy": "คัดลอก", - "Paste": "วาง", - "Cut": "ตัด", - "Undo": "ยกเลิก", - "Redo": "ทำซ้ำ", - "Name": "ชื่อ", - "Description": "คำอธิบาย", - "Status": "สถานะ", - "Type": "ประเภท", - "Date": "วันที่", - "Time": "เวลา", - "Created": "สร้างแล้ว", - "Updated": "อัพเดตแล้ว", - "Actions": "การกระทำ", - "Details": "รายละเอียด", - "View": "ดู", - "New": "ใหม่", - "Total": "ทั้งหมด", - "Count": "จำนวน", - "Price": "ราคา", - "Cost": "ต้นทุน", - "Free": "ฟรี", - "Paid": "จ่ายเงิน", - "Enable": "เปิดใช้งาน", - "Disable": "ปิดใช้งาน", - "Enabled": "เปิดใช้งานแล้ว", - "Disabled": "ปิดใช้งานแล้ว", - "Online": "ออนไลน์", - "Offline": "ออฟไลน์", - "Available": "พร้อมใช้งาน", - "Unavailable": "ไม่พร้อมใช้งาน", - "Required": "จำเป็น", - "Optional": "ไม่บังคับ", - "Default": "ค่าเริ่มต้น", - "Custom": "กำหนดเอง", - "Advanced": "ขั้นสูง", - "Basic": "พื้นฐาน", - "Help": "ช่วยเหลือ", - "Support": "สนับสนุน", - "Documentation": "เอกสาร", - "Version": "เวอร์ชัน", - "Language": "ภาษา", - "Theme": "ธีม", - "Light": "สว่าง", - "Dark": "มืด", - "Auto": "อัตโนมัติ", - "Endpoint": "จุดสิ้นสุด", - "Providers": "ผู้ให้บริการ", - "Combos": "ชุดรวม", - "Usage": "สถิติการใช้งาน", - "Quota Tracker": "ตัวติดตามโควต้า", - "MITM": "MITM", - "CLI Tools": "เครื่องมือ", - "Console Log": "บันทึกคอนโซล", - "System": "ระบบ", - "Debug": "ดีบัก", - "Shutdown": "ปิดระบบ", - "Close Proxy": "ปิด Proxy", - "Are you sure you want to close the proxy server?": "คุณแน่ใจหรือว่าต้องการปิดเซิร์ฟเวอร์ proxy?", - "Server Disconnected": "เซิร์ฟเวอร์ตัดการเชื่อมต่อ", - "The proxy server has been stopped.": "เซิร์ฟเวอร์ proxy ถูกหยุดแล้ว", - "Reload Page": "โหลดหน้าใหม่", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "บริการกำลังทำงานในเทอร์มินัล คุณสามารถปิดหน้าเว็บนี้ได้ การปิดระบบจะหยุดบริการ", - "Manage your AI provider connections": "จัดการการเชื่อมต่อผู้ให้บริการ AI ของคุณ", - "Model combos with fallback": "ชุดรวมโมเดลที่มี fallback", - "Monitor your API usage, token consumption, and request logs": "ติดตามการใช้งาน API การใช้งาน token และบันทึกคำขอของคุณ", - "Intercept CLI tool traffic and route through 9Router": "สกัดปะท่อ CLI และเส้นทางผ่าน 9Router", - "Configure CLI tools": "กำหนดค่าเครื่องมือ CLI", - "API endpoint configuration": "การตั้งค่าจุดสิ้นสุด API", - "Manage your preferences": "จัดการการตั้งค่าของคุณ", - "Debug translation flow between formats": "ดีบักการไหลของการแปลระหว่างรูปแบบ", - "Live server console output": "ผลลัพธ์คอนโซลเซิร์ฟเวอร์สด", - "Create model combos with fallback support": "สร้างชุดรวมโมเดลที่มีการสนับสนุน fallback", - "Local Mode": "โหมดท้องถิ่น", - "Running on your machine": "ทำงานบนเครื่องของคุณ", - "Database Location": "ตำแหน่งของฐานข้อมูล", - "Download Backup": "ดาวน์โหลดการสำรองข้อมูล", - "Import Backup": "นำเข้าการสำรองข้อมูล", - "Database backup downloaded": "ดาวน์โหลดการสำรองข้อมูลฐานข้อมูลแล้ว", - "Database imported successfully": "นำเข้าฐานข้อมูลเสร็จสิ้น", - "Security": "ความปลอดภัย", - "Require login": "ต้องการการเข้าสู่ระบบ", - "When ON, dashboard requires password. When OFF, access without login.": "เมื่อเปิด แดชบอร์ดต้องการรหัสผ่าน เมื่อปิด เข้าถึงโดยไม่ต้องเข้าสู่ระบบ", - "Current Password": "รหัสผ่านปัจจุบัน", - "Enter current password": "ป้อนรหัสผ่านปัจจุบัน", - "New Password": "รหัสผ่านใหม่", - "Enter new password": "ป้อนรหัสผ่านใหม่", - "Confirm New Password": "ยืนยันรหัสผ่านใหม่", - "Confirm new password": "ยืนยันรหัสผ่านใหม่", - "Update Password": "อัพเดตรหัสผ่าน", - "Set Password": "ตั้งรหัสผ่าน", - "Password updated successfully": "อัพเดตรหัสผ่านเสร็จสิ้น", - "Passwords do not match": "รหัสผ่านไม่ตรงกัน", - "Routing Strategy": "กลยุทธ์การเส้นทาง", - "Round Robin": "โรบินรอบ", - "Cycle through accounts to distribute load": "วนรอบบัญชีเพื่อกระจายการโหลด", - "Sticky Limit": "ขีดจำกัดที่เหนียว", - "Calls per account before switching": "การโทรต่อบัญชีก่อนการสลับ", - "Network": "เครือข่าย", - "Outbound Proxy": "Proxy ขาออก", - "Enable proxy for OAuth + provider outbound requests.": "เปิดใช้งาน proxy สำหรับคำขอขาออก OAuth + ผู้ให้บริการ", - "Proxy URL": "URL Proxy", - "Leave empty to inherit existing env proxy (if any).": "ปล่อยว่างไว้เพื่อสืบทอด proxy env ที่มีอยู่ (หากมี)", - "No Proxy": "ไม่มี Proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "ชื่อโฮสต์/โดเมนคั่นด้วยเครื่องหมายจุลภาค เพื่อข้าม proxy", - "Test proxy URL": "ทดสอบ URL Proxy", - "Apply": "ใช้", - "Proxy settings applied": "ใช้การตั้งค่า proxy แล้ว", - "Proxy enabled": "เปิดใช้งาน proxy", - "Proxy disabled": "ปิดใช้งาน proxy", - "Proxy test OK": "ทดสอบ proxy ตกลง", - "Proxy test failed": "ทดสอบ proxy ล้มเหลว", - "Please enter a Proxy URL to test": "กรุณาป้อน URL Proxy เพื่อทดสอบ", - "Observability": "ความสามารถในการสังเกต", - "Enable Observability": "เปิดใช้งานความสามารถในการสังเกต", - "Turn request detail recording on/off globally": "เปิด/ปิดการบันทึกรายละเอียดคำขอทั่วโลก", - "Max Records": "บันทึกสูงสุด", - "Maximum request detail records to keep (older records are auto-deleted)": "บันทึกรายละเอียดคำขอสูงสุดที่จะเก็บ (บันทึกเก่าจะลบโดยอัตโนมัติ)", - "Batch Size": "ขนาดแบตช์", - "Number of items to accumulate before writing to database (higher = better performance)": "จำนวนรายการที่จะรวบรวมก่อนเขียนลงฐานข้อมูล (สูงกว่า = ประสิทธิภาพดีกว่า)", - "Flush Interval (ms)": "ช่วงเวลาล้าง (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "เวลารอสูงสุดก่อนล้างบัฟเฟอร์ (ป้องกันการสูญหายข้อมูลในช่วงจราจรต่ำ)", - "Max JSON Size (KB)": "ขนาด JSON สูงสุด (KB)", - "Maximum size for each JSON field (request/response) before truncation": "ขนาดสูงสุดสำหรับแต่ละช่อง JSON (คำขอ/การตอบสนอง) ก่อนการตัดทอน", - "All data stored on your machine": "ข้อมูลทั้งหมดจัดเก็บไว้บนเครื่องของคุณ", - "MITM Server": "เซิร์ฟเวอร์ MITM", - "Running": "กำลังทำงาน", - "Stopped": "หยุดแล้ว", - "Cert": "ใบรับรอง", - "Server": "เซิร์ฟเวอร์", - "Purpose:": "วัตถุประสงค์:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "ใช้ Antigravity IDE & GitHub Copilot → ที่มีผู้ให้บริการ/โมเดลใด ๆ จาก 9Router", - "How it works:": "วิธีการทำงาน:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "คำขอ Antigravity/Copilot IDE → เปลี่ยนเส้นทาง DNS เป็น localhost:443 → MITM proxy สกัดปะท่อ → 9Router → ตอบสนอง Antigravity/Copilot", - "API Key": "คีย์ API", - "No API keys — create one in Keys page": "ไม่มีคีย์ API — สร้างคีย์ในหน้า Keys", - "sk_9router (default)": "sk_9router (ค่าเริ่มต้น)", - "Server started": "เซิร์ฟเวอร์เริ่มต้นแล้ว", - "Failed to start server": "ไม่สามารถเริ่มเซิร์ฟเวอร์", - "Server stopped — all DNS cleared": "หยุดเซิร์ฟเวอร์ — ล้าง DNS ทั้งหมด", - "Failed to stop server": "ไม่สามารถหยุดเซิร์ฟเวอร์", - "Sudo password is required": "ต้องการรหัสผ่าน sudo", - "Stop Server": "หยุดเซิร์ฟเวอร์", - "Start Server": "เริ่มเซิร์ฟเวอร์", - "Enable DNS per tool below to activate interception": "เปิดใช้งาน DNS สำหรับแต่ละเครื่องมือด้านล่างเพื่อเปิดใช้งานการสกัดปะท่อ", - "Sudo Password Required": "ต้องการรหัสผ่าน Sudo", - "Enter your sudo password to start/stop MITM server": "ป้อนรหัสผ่าน sudo ของคุณเพื่อเริ่ม/หยุดเซิร์ฟเวอร์ MITM", - "Sudo Password": "รหัสผ่าน Sudo", - "Confirm": "ยืนยัน" -} diff --git a/9router/public/i18n/literals/tl.json b/9router/public/i18n/literals/tl.json deleted file mode 100644 index 8c436767..00000000 --- a/9router/public/i18n/literals/tl.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Kanselahin", - "Delete": "Tanggalin", - "Edit": "Baguhin", - "Save": "Salin", - "Close": "Isara", - "Add": "Magdagdag", - "Remove": "Alisin", - "Settings": "Mga Setting", - "Profile": "Propesyal", - "Dashboard": "Dashboard", - "Logout": "Maglog out", - "Login": "Magsimula ng sesyon", - "Providers": "Mga Provider", - "Usage": "Paggamit", - "API Key": "Susi ng API", - "Connected": "Konektado", - "Disconnected": "Hindi Konektado", - "Active": "Aktibo", - "Inactive": "Hindi Aktibo", - "Success": "Matagumpay", - "Failed": "Nabigo", - "Error": "Kamalian", - "Warning": "Babala", - "Info": "Impormasyon", - "Loading": "Naglo-load", - "Search": "Maghanap", - "Filter": "Salain", - "Sort": "I-sort", - "Export": "I-export", - "Import": "I-import", - "Refresh": "I-refresh", - "Back": "Bumalik", - "Next": "Susunod", - "Previous": "Nakaraan", - "Submit": "Ipadala", - "Confirm": "Kumpirmahin", - "Yes": "Oo", - "No": "Hindi", - "OK": "OK", - "Apply": "Ilapat", - "Reset": "I-reset", - "Clear": "I-clear", - "Select": "Pumili", - "Upload": "Mag-upload", - "Download": "I-download", - "Copy": "Kopyahin", - "Paste": "I-paste", - "Cut": "Gupitin", - "Undo": "Undo", - "Redo": "I-redo", - "Name": "Pangalan", - "Description": "Paglalarawan", - "Status": "Kalagayan", - "Type": "Uri", - "Date": "Petsa", - "Time": "Oras", - "Created": "Ginawa", - "Updated": "Ina-update", - "Actions": "Mga Aksyon", - "Details": "Mga Detalye", - "View": "Tingnan", - "New": "Bago", - "Total": "Kabuuan", - "Count": "Bilang", - "Price": "Presyo", - "Cost": "Gastos", - "Free": "Libre", - "Paid": "Bayad", - "Enable": "Paganahin", - "Disable": "Huwag paganahin", - "Enabled": "Pinagana", - "Disabled": "Hindi pinagana", - "Online": "Online", - "Offline": "Offline", - "Available": "Available", - "Unavailable": "Hindi Available", - "Required": "Kinakailangan", - "Optional": "Opsyonal", - "Default": "Default", - "Custom": "Custom", - "Advanced": "Advanced", - "Basic": "Basic", - "Help": "Tulong", - "Support": "Suporta", - "Documentation": "Dokumentasyon", - "Version": "Bersyon", - "Language": "Wika", - "Theme": "Tema", - "Light": "Liwanag", - "Dark": "Madilim", - "Auto": "Awtomatiko", - "Endpoint": "Endpoint", - "Providers": "Mga Provider", - "Combos": "Mga Combo", - "Usage": "Mga Istatistika ng Paggamit", - "Quota Tracker": "Quota Tracker", - "MITM": "MITM", - "CLI Tools": "Mga Tool", - "Console Log": "Console Log", - "System": "Sistema", - "Debug": "I-debug", - "Shutdown": "Patugharin", - "Close Proxy": "Isara ang Proxy", - "Are you sure you want to close the proxy server?": "Sigurado ka ba na gusto mong isara ang proxy server?", - "Server Disconnected": "Ang Server ay Naka-disconnect", - "The proxy server has been stopped.": "Ang proxy server ay tumigil na.", - "Reload Page": "I-reload ang Pahina", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Ang serbisyo ay tumatakbo sa terminal. Maaari mong isara ang web page na ito. Ang Shutdown ay titigil ng serbisyo.", - "Manage your AI provider connections": "Pamahalaan ang iyong mga koneksyon ng AI provider", - "Model combos with fallback": "Mga model combo na may fallback", - "Monitor your API usage, token consumption, and request logs": "Subaybayan ang iyong paggamit ng API, pagkonsumo ng token, at mga log ng request", - "Intercept CLI tool traffic and route through 9Router": "Harangin ang lalu ng CLI tool at i-route sa pamamagitan ng 9Router", - "Configure CLI tools": "I-configure ang CLI tools", - "API endpoint configuration": "Konfiguration ng API endpoint", - "Manage your preferences": "Pamahalaan ang iyong mga kagustuhan", - "Debug translation flow between formats": "I-debug ang translation flow sa pagitan ng mga format", - "Live server console output": "Live server console output", - "Create model combos with fallback support": "Lumikha ng mga model combo na may fallback support", - "Local Mode": "Local Mode", - "Running on your machine": "Tumatakbo sa iyong machine", - "Database Location": "Lokasyon ng Database", - "Download Backup": "I-download ang Backup", - "Import Backup": "I-import ang Backup", - "Database backup downloaded": "Ang database backup ay na-download", - "Database imported successfully": "Ang database ay matagumpay na nai-import", - "Security": "Seguridad", - "Require login": "Kailangan ng login", - "When ON, dashboard requires password. When OFF, access without login.": "Kapag ON, ang dashboard ay nangangailangan ng password. Kapag OFF, access nang walang login.", - "Current Password": "Kasalukuyang Password", - "Enter current password": "Ipasok ang kasalukuyang password", - "New Password": "Bagong Password", - "Enter new password": "Ipasok ang bagong password", - "Confirm New Password": "Kumpirmahin ang Bagong Password", - "Confirm new password": "Kumpirmahin ang bagong password", - "Update Password": "I-update ang Password", - "Set Password": "I-set ang Password", - "Password updated successfully": "Ang password ay matagumpay na na-update", - "Passwords do not match": "Ang mga password ay hindi tumutugma", - "Routing Strategy": "Routing Strategy", - "Round Robin": "Round Robin", - "Cycle through accounts to distribute load": "Umiikot sa mga account upang ipamahagi ang load", - "Sticky Limit": "Sticky Limit", - "Calls per account before switching": "Mga tawag bawat account bago mag-switch", - "Network": "Network", - "Outbound Proxy": "Outbound Proxy", - "Enable proxy for OAuth + provider outbound requests.": "Paganahin ang proxy para sa OAuth + provider outbound requests.", - "Proxy URL": "Proxy URL", - "Leave empty to inherit existing env proxy (if any).": "Iwanan kung walang laman upang mamanin ang umiiral na env proxy (kung mayroon).", - "No Proxy": "Walang Proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Mga hostname/domain na pinagseparang komma upang lampasan ang proxy.", - "Test proxy URL": "Subukan ang Proxy URL", - "Apply": "Ilapat", - "Proxy settings applied": "Ang mga setting ng proxy ay inilapat", - "Proxy enabled": "Ang proxy ay pinagana", - "Proxy disabled": "Ang proxy ay hindi pinagana", - "Proxy test OK": "Proxy test OK", - "Proxy test failed": "Ang proxy test ay nabigo", - "Please enter a Proxy URL to test": "Pakiusap na ipasok ang Proxy URL upang subukan", - "Observability": "Observability", - "Enable Observability": "Paganahin ang Observability", - "Turn request detail recording on/off globally": "I-turn on/off ang request detail recording nang pandaigdig", - "Max Records": "Max Records", - "Maximum request detail records to keep (older records are auto-deleted)": "Pinakamataas na request detail records na papanatilihin (ang mga lumang record ay awtomatikong tatanggalin)", - "Batch Size": "Batch Size", - "Number of items to accumulate before writing to database (higher = better performance)": "Bilang ng mga item na mag-ipon bago magsulat sa database (mas mataas = mas magandang performance)", - "Flush Interval (ms)": "Flush Interval (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Pinakamataas na oras na paghihintay bago mag-flush ng buffer (pumipigil sa pagkawala ng data sa panahon ng mababang traffic)", - "Max JSON Size (KB)": "Max JSON Size (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Pinakamataas na sukat para sa bawat JSON field (request/response) bago ang truncation", - "All data stored on your machine": "Lahat ng data ay nakaimbak sa iyong machine", - "MITM Server": "MITM Server", - "Running": "Tumatakbo", - "Stopped": "Tumigil", - "Cert": "Cert", - "Server": "Server", - "Purpose:": "Layunin:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Gamitin ang Antigravity IDE & GitHub Copilot → sa ANUMANG provider/model mula sa 9Router", - "How it works:": "Paano ito gumagana:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE request → DNS redirect sa localhost:443 → MITM proxy intercepts → 9Router → response sa Antigravity/Copilot", - "API Key": "Susi ng API", - "No API keys — create one in Keys page": "Walang API keys — lumikha ng isa sa Keys page", - "sk_9router (default)": "sk_9router (default)", - "Server started": "Ang server ay nagsimula", - "Failed to start server": "Nabigo na magsimula ang server", - "Server stopped — all DNS cleared": "Ang server ay tumigil — lahat ng DNS ay naka-clear", - "Failed to stop server": "Nabigo na ihinto ang server", - "Sudo password is required": "Kailangan ng sudo password", - "Stop Server": "Ihinto ang Server", - "Start Server": "Simulan ang Server", - "Enable DNS per tool below to activate interception": "Paganahin ang DNS para sa bawat tool sa ibaba upang aktivahin ang interception", - "Sudo Password Required": "Kailangan ng Sudo Password", - "Enter your sudo password to start/stop MITM server": "Ipasok ang iyong sudo password upang simulan/ihinto ang MITM server", - "Sudo Password": "Sudo Password", - "Confirm": "Kumpirmahin" -} diff --git a/9router/public/i18n/literals/tr.json b/9router/public/i18n/literals/tr.json deleted file mode 100644 index aac02428..00000000 --- a/9router/public/i18n/literals/tr.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "İptal", - "Delete": "Sil", - "Edit": "Düzenle", - "Save": "Kaydet", - "Close": "Kapat", - "Add": "Ekle", - "Remove": "Kaldır", - "Settings": "Ayarlar", - "Profile": "Profil", - "Dashboard": "Kontrol Paneli", - "Logout": "Çıkış Yap", - "Login": "Giriş Yap", - "Providers": "Sağlayıcılar", - "Usage": "Kullanım", - "API Key": "API Anahtarı", - "Connected": "Bağlı", - "Disconnected": "Bağlantısı Kesildi", - "Active": "Etkin", - "Inactive": "Etkin Değil", - "Success": "Başarılı", - "Failed": "Başarısız", - "Error": "Hata", - "Warning": "Uyarı", - "Info": "Bilgi", - "Loading": "Yükleniyor", - "Search": "Ara", - "Filter": "Filtre", - "Sort": "Sırala", - "Export": "Dışa Aktar", - "Import": "İçe Aktar", - "Refresh": "Yenile", - "Back": "Geri", - "Next": "İleri", - "Previous": "Önceki", - "Submit": "Gönder", - "Confirm": "Onayla", - "Yes": "Evet", - "No": "Hayır", - "OK": "Tamam", - "Apply": "Uygula", - "Reset": "Sıfırla", - "Clear": "Temizle", - "Select": "Seç", - "Upload": "Yükle", - "Download": "İndir", - "Copy": "Kopyala", - "Paste": "Yapıştır", - "Cut": "Kes", - "Undo": "Geri Al", - "Redo": "Yinele", - "Name": "Ad", - "Description": "Açıklama", - "Status": "Durum", - "Type": "Tür", - "Date": "Tarih", - "Time": "Saat", - "Created": "Oluşturuldu", - "Updated": "Güncellendi", - "Actions": "İşlemler", - "Details": "Ayrıntılar", - "View": "Görüntüle", - "New": "Yeni", - "Total": "Toplam", - "Count": "Sayı", - "Price": "Fiyat", - "Cost": "Maliyet", - "Free": "Ücretsiz", - "Paid": "Ücretli", - "Enable": "Etkinleştir", - "Disable": "Devre Dışı Bırak", - "Enabled": "Etkinleştirildi", - "Disabled": "Devre Dışı Bırakıldı", - "Online": "Çevrimiçi", - "Offline": "Çevrimdışı", - "Available": "Kullanılabilir", - "Unavailable": "Kullanılamıyor", - "Required": "Gerekli", - "Optional": "İsteğe Bağlı", - "Default": "Varsayılan", - "Custom": "Özel", - "Advanced": "Gelişmiş", - "Basic": "Temel", - "Help": "Yardım", - "Support": "Destek", - "Documentation": "Belgeler", - "Version": "Sürüm", - "Language": "Dil", - "Theme": "Tema", - "Light": "Açık", - "Dark": "Koyu", - "Auto": "Otomatik", - "Endpoint": "Uç Nokta", - "Providers": "Sağlayıcılar", - "Combos": "Kombinasyonlar", - "Usage": "Kullanım İstatistikleri", - "Quota Tracker": "Kota İzleyici", - "MITM": "MITM", - "CLI Tools": "Araçlar", - "Console Log": "Konsol Günlüğü", - "System": "Sistem", - "Debug": "Hata Ayıkla", - "Shutdown": "Kapat", - "Close Proxy": "Proxy'yi Kapat", - "Are you sure you want to close the proxy server?": "Proxy sunucusunu kapatmak istediğinizden emin misiniz?", - "Server Disconnected": "Sunucu Bağlantısı Kesildi", - "The proxy server has been stopped.": "Proxy sunucusu durduruldu.", - "Reload Page": "Sayfayı Yenile", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Hizmet terminalde çalışıyor. Bu web sayfasını kapatabilirsiniz. Kapatma, hizmeti durduracaktır.", - "Manage your AI provider connections": "AI sağlayıcı bağlantılarınızı yönetin", - "Model combos with fallback": "Yedek desteğiyle model kombinasyonları", - "Monitor your API usage, token consumption, and request logs": "API kullanımınızı, token tüketimini ve istek günlüklerini izleyin", - "Intercept CLI tool traffic and route through 9Router": "CLI araç trafiğini yakalayın ve 9Router üzerinden yönlendirin", - "Configure CLI tools": "CLI araçlarını yapılandırın", - "API endpoint configuration": "API uç noktası yapılandırması", - "Manage your preferences": "Tercihlerinizi yönetin", - "Debug translation flow between formats": "Formatlar arasındaki çeviri akışında hata ayıklayın", - "Live server console output": "Canlı sunucu konsol çıkışı", - "Create model combos with fallback support": "Yedek destek ile model kombinasyonları oluşturun", - "Local Mode": "Yerel Mod", - "Running on your machine": "Makinenizde çalışıyor", - "Database Location": "Veritabanı Konumu", - "Download Backup": "Yedeklemeyi İndir", - "Import Backup": "Yedeklemeyi İçe Aktar", - "Database backup downloaded": "Veritabanı yedeklemesi indirildi", - "Database imported successfully": "Veritabanı başarıyla içe aktarıldı", - "Security": "Güvenlik", - "Require login": "Giriş Gerekli", - "When ON, dashboard requires password. When OFF, access without login.": "AÇIK olduğunda, kontrol paneli parola gerektirir. KAPAL olduğunda, oturum açmadan erişin.", - "Current Password": "Mevcut Parola", - "Enter current password": "Mevcut parolayı girin", - "New Password": "Yeni Parola", - "Enter new password": "Yeni parolayı girin", - "Confirm New Password": "Yeni Parolayı Onayla", - "Confirm new password": "Yeni parolayı onayla", - "Update Password": "Parolayı Güncelle", - "Set Password": "Parola Ayarla", - "Password updated successfully": "Parola başarıyla güncellendi", - "Passwords do not match": "Parolalar eşleşmiyor", - "Routing Strategy": "Yönlendirme Stratejisi", - "Round Robin": "Dönerektir", - "Cycle through accounts to distribute load": "Yükü dağıtmak için hesaplar arasında döngü yapın", - "Sticky Limit": "Yapışkan Sınır", - "Calls per account before switching": "Geçiş öncesi hesap başına çağrılar", - "Network": "Ağ", - "Outbound Proxy": "Giden Proxy", - "Enable proxy for OAuth + provider outbound requests.": "OAuth + sağlayıcı giden istekleri için proxy'yi etkinleştirin.", - "Proxy URL": "Proxy URL'si", - "Leave empty to inherit existing env proxy (if any).": "Mevcut ortam proxy'sini (varsa) devralması için boş bırakın.", - "No Proxy": "Proxy Yok", - "Comma-separated hostnames/domains to bypass the proxy.": "Proxy'yi atlamak için virgülle ayrılmış ana bilgisayar adları/etki alanları.", - "Test proxy URL": "Test Proxy URL'si", - "Apply": "Uygula", - "Proxy settings applied": "Proxy ayarları uygulandı", - "Proxy enabled": "Proxy etkinleştirildi", - "Proxy disabled": "Proxy devre dışı bırakıldı", - "Proxy test OK": "Proxy testi Tamam", - "Proxy test failed": "Proxy testi başarısız", - "Please enter a Proxy URL to test": "Test etmek için lütfen bir Proxy URL'si girin", - "Observability": "Gözlemlenebilirlik", - "Enable Observability": "Gözlemlenebilirliği Etkinleştir", - "Turn request detail recording on/off globally": "İstek ayrıntıları kaydını genel olarak aç/kapat", - "Max Records": "Maksimum Kayıtlar", - "Maximum request detail records to keep (older records are auto-deleted)": "Tutulacak maksimum istek ayrıntı kayıtları (eski kayıtlar otomatik olarak silinir)", - "Batch Size": "Toplu İşlem Boyutu", - "Number of items to accumulate before writing to database (higher = better performance)": "Veritabanına yazmadan önce biriktirilecek öğe sayısı (daha yüksek = daha iyi performans)", - "Flush Interval (ms)": "Temizleme Aralığı (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Arabelleği temizlemeden önce beklenecek maksimum süre (düşük trafikte veri kaybını engeller)", - "Max JSON Size (KB)": "Maksimum JSON Boyutu (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Kesilmeden önce her JSON alanı (istek/yanıt) için maksimum boyut", - "All data stored on your machine": "Tüm veriler makinenizde depolanır", - "MITM Server": "MITM Sunucusu", - "Running": "Çalışıyor", - "Stopped": "Durduruldu", - "Cert": "Sertifika", - "Server": "Sunucu", - "Purpose:": "Amaç:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Antigravity IDE & GitHub Copilot kullanın → 9Router'dan HERHANGİ bir sağlayıcı/model ile", - "How it works:": "Nasıl çalışır:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE isteği → DNS'i localhost:443'e yönlendir → MITM proxy yakalar → 9Router → Antigravity/Copilot'a yanıt", - "API Key": "API Anahtarı", - "No API keys — create one in Keys page": "API anahtarı yok — Keys sayfasında bir tane oluşturun", - "sk_9router (default)": "sk_9router (varsayılan)", - "Server started": "Sunucu başlatıldı", - "Failed to start server": "Sunucu başlatılamadı", - "Server stopped — all DNS cleared": "Sunucu durduruldu — tüm DNS temizlendi", - "Failed to stop server": "Sunucu durdurulamadı", - "Sudo password is required": "Sudo parolası gereklidir", - "Stop Server": "Sunucuyu Durdur", - "Start Server": "Sunucuyu Başlat", - "Enable DNS per tool below to activate interception": "Yakalamayı etkinleştirmek için aşağıdaki her araç için DNS'i etkinleştirin", - "Sudo Password Required": "Sudo Parolası Gerekli", - "Enter your sudo password to start/stop MITM server": "MITM sunucusunu başlatmak/durdurmak için sudo parolanızı girin", - "Sudo Password": "Sudo Parolası", - "Confirm": "Onayla" -} diff --git a/9router/public/i18n/literals/uk.json b/9router/public/i18n/literals/uk.json deleted file mode 100644 index 2b509536..00000000 --- a/9router/public/i18n/literals/uk.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Скасувати", - "Delete": "Видалити", - "Edit": "Редагувати", - "Save": "Зберегти", - "Close": "Закрити", - "Add": "Додати", - "Remove": "Видалити", - "Settings": "Налаштування", - "Profile": "Профіль", - "Dashboard": "Панель керування", - "Logout": "Вийти", - "Login": "Увійти", - "Providers": "Постачальники", - "Usage": "Використання", - "API Key": "Ключ API", - "Connected": "Підключено", - "Disconnected": "Відключено", - "Active": "Активний", - "Inactive": "Неактивний", - "Success": "Успіх", - "Failed": "Помилка", - "Error": "Помилка", - "Warning": "Попередження", - "Info": "Інформація", - "Loading": "Завантаження", - "Search": "Пошук", - "Filter": "Фільтр", - "Sort": "Сортування", - "Export": "Експорт", - "Import": "Імпорт", - "Refresh": "Оновити", - "Back": "Назад", - "Next": "Далі", - "Previous": "Попередній", - "Submit": "Надіслати", - "Confirm": "Підтвердити", - "Yes": "Так", - "No": "Ні", - "OK": "ОК", - "Apply": "Застосувати", - "Reset": "Скинути", - "Clear": "Очистити", - "Select": "Вибрати", - "Upload": "Завантажити", - "Download": "Завантажити", - "Copy": "Копіювати", - "Paste": "Вставити", - "Cut": "Вирізати", - "Undo": "Відмінити", - "Redo": "Повторити", - "Name": "Назва", - "Description": "Опис", - "Status": "Статус", - "Type": "Тип", - "Date": "Дата", - "Time": "Час", - "Created": "Створено", - "Updated": "Оновлено", - "Actions": "Дії", - "Details": "Деталі", - "View": "Перегляд", - "New": "Новий", - "Total": "Всього", - "Count": "Кількість", - "Price": "Ціна", - "Cost": "Вартість", - "Free": "Безплатно", - "Paid": "Платно", - "Enable": "Увімкнути", - "Disable": "Вимкнути", - "Enabled": "Увімкнено", - "Disabled": "Вимкнено", - "Online": "Онлайн", - "Offline": "Офлайн", - "Available": "Доступно", - "Unavailable": "Недоступно", - "Required": "Обов'язково", - "Optional": "Опційно", - "Default": "За замовчуванням", - "Custom": "Користувацький", - "Advanced": "Розширені", - "Basic": "Базовий", - "Help": "Допомога", - "Support": "Підтримка", - "Documentation": "Документація", - "Version": "Версія", - "Language": "Мова", - "Theme": "Тема", - "Light": "Світла", - "Dark": "Темна", - "Auto": "Авто", - "Endpoint": "Кінцева точка", - "Providers": "Постачальники", - "Combos": "Комбо", - "Usage": "Статистика використання", - "Quota Tracker": "Відстеження квоти", - "MITM": "MITM", - "CLI Tools": "Інструменти", - "Console Log": "Журнал консолі", - "System": "Система", - "Debug": "Налагодження", - "Shutdown": "Вимкнення", - "Close Proxy": "Закрити проксі", - "Are you sure you want to close the proxy server?": "Ви впевнені, що хочете закрити сервер проксі?", - "Server Disconnected": "Сервер відключено", - "The proxy server has been stopped.": "Сервер проксі було зупинено.", - "Reload Page": "Перезавантажити сторінку", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Служба працює в терміналі. Ви можете закрити цю веб-сторінку. Вимкнення призупинить службу.", - "Manage your AI provider connections": "Керуйте своїми з'єднаннями постачальника AI", - "Model combos with fallback": "Комбо моделей з резервуванням", - "Monitor your API usage, token consumption, and request logs": "Відстежуйте використання API, споживання токенів та журнали запитів", - "Intercept CLI tool traffic and route through 9Router": "Перехопіть трафік інструменту CLI та маршрутизуйте через 9Router", - "Configure CLI tools": "Налаштуйте інструменти CLI", - "API endpoint configuration": "Конфігурація кінцевої точки API", - "Manage your preferences": "Керуйте своїми уподобаннями", - "Debug translation flow between formats": "Налагодити потік перекладу між форматами", - "Live server console output": "Вихід консолі живого сервера", - "Create model combos with fallback support": "Створіть комбо моделей з підтримкою резервування", - "Local Mode": "Локальний режим", - "Running on your machine": "Запуск на вашій машині", - "Database Location": "Розташування бази даних", - "Download Backup": "Завантажити резервну копію", - "Import Backup": "Імпортувати резервну копію", - "Database backup downloaded": "Резервну копію бази даних завантажено", - "Database imported successfully": "Базу даних успішно імпортовано", - "Security": "Безпека", - "Require login": "Потрібне входження", - "When ON, dashboard requires password. When OFF, access without login.": "Коли ВІД, панель керування потребує пароля. Коли ВИМКНЕНО, доступ без входження.", - "Current Password": "Поточний пароль", - "Enter current password": "Введіть поточний пароль", - "New Password": "Новий пароль", - "Enter new password": "Введіть новий пароль", - "Confirm New Password": "Підтвердіть новий пароль", - "Confirm new password": "Підтвердіть новий пароль", - "Update Password": "Оновити пароль", - "Set Password": "Встановити пароль", - "Password updated successfully": "Пароль успішно оновлено", - "Passwords do not match": "Паролі не збігаються", - "Routing Strategy": "Стратегія маршрутизації", - "Round Robin": "Циклічний розподіл", - "Cycle through accounts to distribute load": "Циклічне переключення облікових записів для розподілу навантаження", - "Sticky Limit": "Обмеження липкості", - "Calls per account before switching": "Виклики на обліковий запис перед перемиканням", - "Network": "Мережа", - "Outbound Proxy": "Вихідний проксі", - "Enable proxy for OAuth + provider outbound requests.": "Увімкніть проксі для запитів OAuth + постачальника на виході.", - "Proxy URL": "URL проксі", - "Leave empty to inherit existing env proxy (if any).": "Залиште порожнім, щоб успадкувати існуючий проксі env (якщо є).", - "No Proxy": "Без проксі", - "Comma-separated hostnames/domains to bypass the proxy.": "Розділені комами імена хостів/домени для обходу проксі.", - "Test proxy URL": "Протестувати URL проксі", - "Apply": "Застосувати", - "Proxy settings applied": "Параметри проксі застосовані", - "Proxy enabled": "Проксі увімкнено", - "Proxy disabled": "Проксі вимкнено", - "Proxy test OK": "Тест проксі OK", - "Proxy test failed": "Тест проксі не вдався", - "Please enter a Proxy URL to test": "Будь ласка, введіть URL проксі для тестування", - "Observability": "Спостережуваність", - "Enable Observability": "Увімкнути спостережуваність", - "Turn request detail recording on/off globally": "Увімкніть/вимкніть запис деталей запиту глобально", - "Max Records": "Максимальна кількість записів", - "Maximum request detail records to keep (older records are auto-deleted)": "Максимальна кількість записів деталей запиту для зберігання (старші записи автоматично видаляються)", - "Batch Size": "Розмір пакету", - "Number of items to accumulate before writing to database (higher = better performance)": "Кількість елементів для накопичення перед записом у базу даних (вище = краща продуктивність)", - "Flush Interval (ms)": "Інтервал промивки (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Максимальний час очікування перед промиванням буфера (запобігає втраті даних під час низького трафіку)", - "Max JSON Size (KB)": "Максимальний розмір JSON (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Максимальний розмір для кожного поля JSON (запит/відповідь) перед усіканням", - "All data stored on your machine": "Усі дані зберігаються на вашій машині", - "MITM Server": "MITM сервер", - "Running": "Запущено", - "Stopped": "Зупинено", - "Cert": "Сертифікат", - "Server": "Сервер", - "Purpose:": "Мета:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Використовуйте Antigravity IDE & GitHub Copilot → з БУДЬ-ЯКИМ постачальником/моделлю від 9Router", - "How it works:": "Як це працює:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Запит Antigravity/Copilot IDE → Перенаправлення DNS на localhost:443 → MITM проксі перехопити → 9Router → відповідь на Antigravity/Copilot", - "API Key": "Ключ API", - "No API keys — create one in Keys page": "Немає ключів API — створіть один на сторінці ключів", - "sk_9router (default)": "sk_9router (за замовчуванням)", - "Server started": "Сервер запущено", - "Failed to start server": "Не вдалося запустити сервер", - "Server stopped — all DNS cleared": "Сервер зупинено — усі DNS очищено", - "Failed to stop server": "Не вдалося зупинити сервер", - "Sudo password is required": "Потрібен пароль sudo", - "Stop Server": "Зупинити сервер", - "Start Server": "Запустити сервер", - "Enable DNS per tool below to activate interception": "Увімкніть DNS для кожного інструменту нижче, щоб активувати перехоплення", - "Sudo Password Required": "Потрібен пароль Sudo", - "Enter your sudo password to start/stop MITM server": "Введіть пароль sudo для запуску/зупинення MITM сервера", - "Sudo Password": "Пароль Sudo", - "Confirm": "Підтвердити" -} diff --git a/9router/public/i18n/literals/ur.json b/9router/public/i18n/literals/ur.json deleted file mode 100644 index a7bcc5ff..00000000 --- a/9router/public/i18n/literals/ur.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "منسوخ کریں", - "Delete": "حذف کریں", - "Edit": "ترمیم کریں", - "Save": "محفوظ کریں", - "Close": "بند کریں", - "Add": "شامل کریں", - "Remove": "ہٹائیں", - "Settings": "ترتیبات", - "Profile": "پروفائل", - "Dashboard": "ڈیش بورڈ", - "Logout": "لاگ آؤٹ", - "Login": "لاگ ان", - "Providers": "فراہم کنندگان", - "Usage": "استعمال", - "API Key": "API کلید", - "Connected": "منسلک", - "Disconnected": "منقطع", - "Active": "فعال", - "Inactive": "غیر فعال", - "Success": "کامیاب", - "Failed": "ناکام", - "Error": "خرابی", - "Warning": "انتباہ", - "Info": "معلومات", - "Loading": "لوڈ ہو رہا ہے", - "Search": "تلاش کریں", - "Filter": "فلٹر کریں", - "Sort": "ترتیب دیں", - "Export": "برآمد کریں", - "Import": "درآمد کریں", - "Refresh": "تازہ کریں", - "Back": "واپس", - "Next": "آگے", - "Previous": "پچھلا", - "Submit": "جمع کریں", - "Confirm": "تصدیق کریں", - "Yes": "جی", - "No": "نہیں", - "OK": "ٹھیک ہے", - "Apply": "لاگو کریں", - "Reset": "دوبارہ سیٹ کریں", - "Clear": "صاف کریں", - "Select": "منتخب کریں", - "Upload": "اپ لوڈ کریں", - "Download": "ڈاؤن لوڈ کریں", - "Copy": "کاپی کریں", - "Paste": "پیسٹ کریں", - "Cut": "کاٹ دیں", - "Undo": "واپسی", - "Redo": "دوبارہ کریں", - "Name": "نام", - "Description": "تفصیل", - "Status": "حالت", - "Type": "قسم", - "Date": "تاریخ", - "Time": "وقت", - "Created": "بنایا گیا", - "Updated": "اپڈیٹ شدہ", - "Actions": "اقدامات", - "Details": "تفصیلات", - "View": "دیکھیں", - "New": "نیا", - "Total": "کل", - "Count": "شمار", - "Price": "قیمت", - "Cost": "لاگت", - "Free": "مفت", - "Paid": "ادا شدہ", - "Enable": "فعال کریں", - "Disable": "غیر فعال کریں", - "Enabled": "فعال", - "Disabled": "غیر فعال", - "Online": "آن لائن", - "Offline": "آف لائن", - "Available": "دستیاب", - "Unavailable": "دستیاب نہیں", - "Required": "ضروری", - "Optional": "اختیاری", - "Default": "ڈیفالٹ", - "Custom": "حسب ضرورت", - "Advanced": "اعلیٰ", - "Basic": "بنیادی", - "Help": "مدد", - "Support": "معاونت", - "Documentation": "دستاویزات", - "Version": "ورژن", - "Language": "زبان", - "Theme": "تھیم", - "Light": "روشن", - "Dark": "تاریک", - "Auto": "خودکار", - "Endpoint": "اختتام نقطہ", - "Providers": "فراہم کنندگان", - "Combos": "امتزاجات", - "Usage": "استعمال کے اعدادوشمار", - "Quota Tracker": "کوٹہ ٹریکر", - "MITM": "MITM", - "CLI Tools": "آلات", - "Console Log": "کنسول لاگ", - "System": "نظام", - "Debug": "ڈیبگ", - "Shutdown": "بند کریں", - "Close Proxy": "پروکسی بند کریں", - "Are you sure you want to close the proxy server?": "کیا آپ یقیناً پروکسی سرور کو بند کرنا چاہتے ہیں؟", - "Server Disconnected": "سرور منقطع", - "The proxy server has been stopped.": "پروکسی سرور بند کر دیا گیا ہے۔", - "Reload Page": "صفحہ دوبارہ لوڈ کریں", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "خدمت ٹرمینل میں چل رہی ہے۔ آپ یہ ویب صفحہ بند کر سکتے ہیں۔ شٹ ڈاؤن خدمت کو بند کر دے گا۔", - "Manage your AI provider connections": "اپنے AI فراہم کنندہ کنکشن کو منیج کریں", - "Model combos with fallback": "Fallback کے ساتھ ماڈل امتزاجات", - "Monitor your API usage, token consumption, and request logs": "اپنے API استعمال، ٹوکن کھپت، اور درخواست لاگز کی نگرانی کریں", - "Intercept CLI tool traffic and route through 9Router": "CLI ٹول ٹریفک کو روکیں اور 9Router کے ذریعے منتقل کریں", - "Configure CLI tools": "CLI آلات ترتیب دیں", - "API endpoint configuration": "API اختتام نقطہ کی ترتیب", - "Manage your preferences": "اپنی ترجیحات منیج کریں", - "Debug translation flow between formats": "شکلوں کے درمیان ترجمہ کے بہاؤ کو ڈیبگ کریں", - "Live server console output": "لائیو سرور کنسول آؤٹ پٹ", - "Create model combos with fallback support": "Fallback معاونت کے ساتھ ماڈل امتزاجات بنائیں", - "Local Mode": "مقامی موڈ", - "Running on your machine": "آپ کی مشین پر چل رہا ہے", - "Database Location": "ڈیٹابیس کی جگہ", - "Download Backup": "بیک اپ ڈاؤن لوڈ کریں", - "Import Backup": "بیک اپ درآمد کریں", - "Database backup downloaded": "ڈیٹابیس بیک اپ ڈاؤن لوڈ کیا گیا", - "Database imported successfully": "ڈیٹابیس کامیابی سے درآمد کیا گیا", - "Security": "سیکیورٹی", - "Require login": "لاگ ان کی ضرورت ہے", - "When ON, dashboard requires password. When OFF, access without login.": "جب آن ہو، ڈیش بورڈ کے لیے پاس ورڈ درکار ہے۔ جب آف ہو، بغیر لاگ ان کے رسائی حاصل کریں۔", - "Current Password": "موجودہ پاس ورڈ", - "Enter current password": "موجودہ پاس ورڈ داخل کریں", - "New Password": "نیا پاس ورڈ", - "Enter new password": "نیا پاس ورڈ داخل کریں", - "Confirm New Password": "نئے پاس ورڈ کی تصدیق کریں", - "Confirm new password": "نئے پاس ورڈ کی تصدیق کریں", - "Update Password": "پاس ورڈ اپڈیٹ کریں", - "Set Password": "پاس ورڈ سیٹ کریں", - "Password updated successfully": "پاس ورڈ کامیابی سے اپڈیٹ ہو گیا", - "Passwords do not match": "پاس ورڈ مماثل نہیں ہیں", - "Routing Strategy": "روٹنگ حکمت عملی", - "Round Robin": "دوری رابن", - "Cycle through accounts to distribute load": "بوجھ تقسیم کرنے کے لیے اکاؤنٹس کے ذریعے سائیکل کریں", - "Sticky Limit": "چپکنے والی حد", - "Calls per account before switching": "سوئچنگ سے پہلے فی اکاؤنٹ کالیں", - "Network": "نیٹ ورک", - "Outbound Proxy": "آؤٹ بائونڈ پروکسی", - "Enable proxy for OAuth + provider outbound requests.": "OAuth + فراہم کنندہ آؤٹ بائونڈ درخواستوں کے لیے پروکسی فعال کریں۔", - "Proxy URL": "پروکسی URL", - "Leave empty to inherit existing env proxy (if any).": "موجودہ env پروکسی کو وراثت میں دینے کے لیے خالی رکھیں (اگر کوئی ہو)۔", - "No Proxy": "کوئی پروکسی نہیں", - "Comma-separated hostnames/domains to bypass the proxy.": "پروکسی کو نظر انداز کرنے کے لیے کوما سے الگ شدہ ہوسٹ ناموں/ڈومین۔", - "Test proxy URL": "پروکسی URL کو ٹیسٹ کریں", - "Apply": "لاگو کریں", - "Proxy settings applied": "پروکسی ترتیبات لاگو کی گئیں", - "Proxy enabled": "پروکسی فعال", - "Proxy disabled": "پروکسی غیر فعال", - "Proxy test OK": "پروکسی ٹیسٹ ٹھیک ہے", - "Proxy test failed": "پروکسی ٹیسٹ ناکام", - "Please enter a Proxy URL to test": "براہ کرم ٹیسٹ کے لیے ایک پروکسی URL داخل کریں", - "Observability": "نقطہ نظر کی صلاحیت", - "Enable Observability": "نقطہ نظر کی صلاحیت فعال کریں", - "Turn request detail recording on/off globally": "درخواست کی تفصیلات ریکارڈنگ کو عالمی طور پر آن/آف کریں", - "Max Records": "زیادہ سے زیادہ ریکارڈ", - "Maximum request detail records to keep (older records are auto-deleted)": "رکھنے کے لیے زیادہ سے زیادہ درخواست کی تفصیلات ریکارڈ (پرانے ریکارڈ خود بخود حذف ہو جاتے ہیں)", - "Batch Size": "بیچ سائز", - "Number of items to accumulate before writing to database (higher = better performance)": "ڈیٹابیس میں لکھنے سے پہلے جمع کرنے کے لیے اشیاء کی تعداد (زیادہ = بہتر کارکردگی)", - "Flush Interval (ms)": "فلش وقفہ (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "بفر کو فلش کرنے سے پہلے انتظار کرنے کا زیادہ سے زیادہ وقت (کم ٹریفک کے دوران ڈیٹا کے نقصان سے بچاتا ہے)", - "Max JSON Size (KB)": "زیادہ سے زیادہ JSON سائز (KB)", - "Maximum size for each JSON field (request/response) before truncation": "تشکیل سے پہلے ہر JSON فیلڈ کے لیے زیادہ سے زیادہ سائز (درخواست/جواب)", - "All data stored on your machine": "تمام ڈیٹا آپ کی مشین پر محفوظ ہے", - "MITM Server": "MITM سرور", - "Running": "چل رہا ہے", - "Stopped": "رکا ہوا", - "Cert": "سرٹیفکیٹ", - "Server": "سرور", - "Purpose:": "مقصد:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Antigravity IDE اور GitHub Copilot استعمال کریں → 9Router سے کسی بھی فراہم کنندہ/ماڈل کے ساتھ", - "How it works:": "یہ کیسے کام کرتا ہے:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE درخواست → DNS کو localhost:443 کی طرف ری ڈائریکٹ کریں → MITM پروکسی روکے → 9Router → Antigravity/Copilot کو جواب", - "API Key": "API کلید", - "No API keys — create one in Keys page": "کوئی API کلید نہیں — Keys صفحہ میں ایک بنائیں", - "sk_9router (default)": "sk_9router (ڈیفالٹ)", - "Server started": "سرور شروع ہوگیا", - "Failed to start server": "سرور شروع کرنے میں ناکام", - "Server stopped — all DNS cleared": "سرور بند — تمام DNS صاف کیے گئے", - "Failed to stop server": "سرور بند کرنے میں ناکام", - "Sudo password is required": "Sudo پاس ورڈ درکار ہے", - "Stop Server": "سرور بند کریں", - "Start Server": "سرور شروع کریں", - "Enable DNS per tool below to activate interception": "روک تھام کو فعال کرنے کے لیے نیچے ہر ٹول کے لیے DNS فعال کریں", - "Sudo Password Required": "Sudo پاس ورڈ درکار ہے", - "Enter your sudo password to start/stop MITM server": "MITM سرور شروع/بند کرنے کے لیے اپنا sudo پاس ورڈ داخل کریں", - "Sudo Password": "Sudo پاس ورڈ", - "Confirm": "تصدیق کریں" -} diff --git a/9router/public/i18n/literals/vi.json b/9router/public/i18n/literals/vi.json deleted file mode 100644 index a876310b..00000000 --- a/9router/public/i18n/literals/vi.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "Hủy", - "Delete": "Xóa", - "Edit": "Sửa", - "Save": "Lưu", - "Close": "Đóng", - "Add": "Thêm", - "Remove": "Xóa bỏ", - "Settings": "Cài đặt", - "Profile": "Hồ sơ", - "Dashboard": "Bảng điều khiển", - "Logout": "Đăng xuất", - "Login": "Đăng nhập", - "Providers": "Nhà cung cấp", - "Usage": "Sử dụng", - "API Key": "Khóa API", - "Connected": " Đã kết nối", - "Disconnected": "Chưa kết nối", - "Active": "Hoạt động", - "Inactive": "Không hoạt động", - "Success": "Thành công", - "Failed": "Thất bại", - "Error": "Lỗi", - "Warning": "Cảnh báo", - "Info": "Thông tin", - "Loading": "Đang tải", - "Search": "Tìm kiếm", - "Filter": "Lọc", - "Sort": "Sắp xếp", - "Export": "Xuất", - "Import": "Nhập", - "Refresh": "Làm mới", - "Back": "Quay lại", - "Next": "Tiếp theo", - "Previous": "Trước", - "Submit": "Gửi", - "Confirm": "Xác nhận", - "Yes": "Có", - "No": "Không", - "OK": "OK", - "Apply": "Áp dụng", - "Reset": "Đặt lại", - "Clear": "Xóa", - "Select": "Chọn", - "Upload": "Tải lên", - "Download": "Tải xuống", - "Copy": "Sao chép", - "Paste": "Dán", - "Cut": "Cắt", - "Undo": "Hoàn tác", - "Redo": "Làm lại", - "Name": "Tên", - "Description": "Mô tả", - "Status": "Trạng thái", - "Type": "Loại", - "Date": "Ngày", - "Time": "Thời gian", - "Created": "Đã tạo", - "Updated": "Đã cập nhật", - "Actions": "Hành động", - "Details": "Chi tiết", - "View": "Xem", - "New": "Mới", - "Total": "Tổng", - "Count": "Số lượng", - "Price": "Giá", - "Cost": "Chi phí", - "Free": "Miễn phí", - "Paid": "Trả phí", - "Enable": "Bật", - "Disable": "Tắt", - "Enabled": "Đã bật", - "Disabled": "Đã tắt", - "Online": "Trực tuyến", - "Offline": "Ngoại tuyến", - "Available": "Có sẵn", - "Unavailable": "Không có sẵn", - "Required": "Bắt buộc", - "Optional": "Tùy chọn", - "Default": "Mặc định", - "Custom": "Tùy chỉnh", - "Advanced": "Nâng cao", - "Basic": "Cơ bản", - "Help": "Trợ giúp", - "Support": "Hỗ trợ", - "Documentation": "Tài liệu", - "Version": "Phiên bản", - "Language": "Ngôn ngữ", - "Theme": "Giao diện", - "Light": "Sáng", - "Dark": "Tối", - "Auto": "Tự động", - "Endpoint": "Điểm cuối", - "Providers": "Nhà cung cấp", - "Combos": "Kết hợp", - "Usage": "Thống kê", - "Quota Tracker": "Theo dõi hạn mức", - "MITM": "MITM", - "CLI Tools": "Công cụ", - "Console Log": "Nhật ký Console", - "System": "Hệ thống", - "Debug": "Gỡ lỗi", - "Shutdown": "Tắt máy", - "Close Proxy": "Đóng Proxy", - "Are you sure you want to close the proxy server?": "Bạn có chắc chắn muốn đóng máy chủ proxy không?", - "Server Disconnected": "Máy chủ đã ngắt kết nối", - "The proxy server has been stopped.": "Máy chủ proxy đã bị dừng.", - "Reload Page": "Tải lại trang", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "Dịch vụ đang chạy trong terminal. Bạn có thể đóng trang web này. Tắt máy sẽ dừng dịch vụ.", - "Manage your AI provider connections": "Quản lý kết nối nhà cung cấp AI của bạn", - "Model combos with fallback": "Kết hợp mô hình với dự phòng", - "Monitor your API usage, token consumption, and request logs": "Theo dõi việc sử dụng API, tiêu thụ token và nhật ký yêu cầu", - "Intercept CLI tool traffic and route through 9Router": "Chặn lưu lượng công cụ CLI và định tuyến qua 9Router", - "Configure CLI tools": "Cấu hình công cụ CLI", - "API endpoint configuration": "Cấu hình điểm cuối API", - "Manage your preferences": "Quản lý tùy chọn của bạn", - "Debug translation flow between formats": "Gỡ lỗi luồng dịch giữa các định dạng", - "Live server console output": "Đầu ra console máy chủ trực tiếp", - "Create model combos with fallback support": "Tạo kết hợp mô hình với hỗ trợ dự phòng", - "Local Mode": "Chế độ cục bộ", - "Running on your machine": "Chạy trên máy của bạn", - "Database Location": "Vị trí cơ sở dữ liệu", - "Download Backup": "Tải xuống bản sao lưu", - "Import Backup": "Nhập bản sao lưu", - "Database backup downloaded": "Đã tải xuống bản sao lưu cơ sở dữ liệu", - "Database imported successfully": "Đã nhập cơ sở dữ liệu thành công", - "Security": "Bảo mật", - "Require login": "Yêu cầu đăng nhập", - "When ON, dashboard requires password. When OFF, access without login.": "Khi BẬT, bảng điều khiển yêu cầu mật khẩu. Khi TẮT, truy cập không cần đăng nhập.", - "Current Password": "Mật khẩu hiện tại", - "Enter current password": "Nhập mật khẩu hiện tại", - "New Password": "Mật khẩu mới", - "Enter new password": "Nhập mật khẩu mới", - "Confirm New Password": "Xác nhận mật khẩu mới", - "Confirm new password": "Xác nhận mật khẩu mới", - "Update Password": "Cập nhật mật khẩu", - "Set Password": "Đặt mật khẩu", - "Password updated successfully": "Đã cập nhật mật khẩu thành công", - "Passwords do not match": "Mật khẩu không khớp", - "Routing Strategy": "Chiến lược định tuyến", - "Round Robin": "Vòng tròn", - "Cycle through accounts to distribute load": "Luân phiên qua các tài khoản để phân phối tải", - "Sticky Limit": "Giới hạn dính", - "Calls per account before switching": "Số lần gọi mỗi tài khoản trước khi chuyển", - "Network": "Mạng", - "Outbound Proxy": "Proxy đi ra", - "Enable proxy for OAuth + provider outbound requests.": "Bật proxy cho OAuth + yêu cầu đi ra của nhà cung cấp.", - "Proxy URL": "URL Proxy", - "Leave empty to inherit existing env proxy (if any).": "Để trống để kế thừa proxy môi trường hiện có (nếu có).", - "No Proxy": "Không có Proxy", - "Comma-separated hostnames/domains to bypass the proxy.": "Tên máy chủ/tên miền được phân tách bằng dấu phẩy để bỏ qua proxy.", - "Test proxy URL": "Kiểm tra URL proxy", - "Apply": "Áp dụng", - "Proxy settings applied": "Đã áp dụng cài đặt proxy", - "Proxy enabled": "Đã bật proxy", - "Proxy disabled": "Đã tắt proxy", - "Proxy test OK": "Kiểm tra proxy OK", - "Proxy test failed": "Kiểm tra proxy thất bại", - "Please enter a Proxy URL to test": "Vui lòng nhập URL Proxy để kiểm tra", - "Observability": "Khả năng quan sát", - "Enable Observability": "Bật khả năng quan sát", - "Turn request detail recording on/off globally": "Bật/tắt ghi chi tiết yêu cầu toàn cục", - "Max Records": "Số bản ghi tối đa", - "Maximum request detail records to keep (older records are auto-deleted)": "Số bản ghi chi tiết yêu cầu tối đa để giữ (bản ghi cũ hơn sẽ tự động xóa)", - "Batch Size": "Kích thước lô", - "Number of items to accumulate before writing to database (higher = better performance)": "Số mục tích lũy trước khi ghi vào cơ sở dữ liệu (cao hơn = hiệu suất tốt hơn)", - "Flush Interval (ms)": "Khoảng thời gian xả (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "Thời gian tối đa để chờ trước khi xả bộ đệm (ngăn mất dữ liệu trong lưu lượng thấp)", - "Max JSON Size (KB)": "Kích thước JSON tối đa (KB)", - "Maximum size for each JSON field (request/response) before truncation": "Kích thước tối đa cho mỗi trường JSON (yêu cầu/phản hồi) trước khi cắt bớt", - "All data stored on your machine": "Tất cả dữ liệu được lưu trữ trên máy của bạn", - "MITM Server": "Máy chủ MITM", - "Running": "Đang chạy", - "Stopped": "Đã dừng", - "Cert": "Chứng chỉ", - "Server": "Máy chủ", - "Purpose:": "Mục đích:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "Sử dụng Antigravity IDE & GitHub Copilot → với BẤT KỲ nhà cung cấp/mô hình nào từ 9Router", - "How it works:": "Cách hoạt động:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Yêu cầu Antigravity/Copilot IDE → Chuyển hướng DNS đến localhost:443 → MITM proxy chặn → 9Router → phản hồi đến Antigravity/Copilot", - "API Key": "Khóa API", - "No API keys — create one in Keys page": "Không có khóa API — tạo một khóa trong trang Keys", - "sk_9router (default)": "sk_9router (mặc định)", - "Server started": "Đã khởi động máy chủ", - "Failed to start server": "Không thể khởi động máy chủ", - "Server stopped — all DNS cleared": "Đã dừng máy chủ — đã xóa tất cả DNS", - "Failed to stop server": "Không thể dừng máy chủ", - "Sudo password is required": "Yêu cầu mật khẩu sudo", - "Stop Server": "Dừng máy chủ", - "Start Server": "Khởi động máy chủ", - "Enable DNS per tool below to activate interception": "Bật DNS cho từng công cụ bên dưới để kích hoạt chặn", - "Sudo Password Required": "Yêu cầu mật khẩu Sudo", - "Enter your sudo password to start/stop MITM server": "Nhập mật khẩu sudo của bạn để khởi động/dừng máy chủ MITM", - "Sudo Password": "Mật khẩu Sudo", - "Confirm": "Xác nhận" -} diff --git a/9router/public/i18n/literals/zh-CN.json b/9router/public/i18n/literals/zh-CN.json deleted file mode 100644 index b14e06b5..00000000 --- a/9router/public/i18n/literals/zh-CN.json +++ /dev/null @@ -1,515 +0,0 @@ -{ - "-compatible models manually or import them from the /models endpoint.": "- 手动兼容模型或从 /models 端点导入它们。", - ". Click \"Apply\" to auto-configure.": "。单击“应用”进行自动配置。", - "($/1M tokens). Example: An input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "($/100 万代币)。示例:输入率为 2.50 意味着每 1,000,000 个输入代币 2.50 美元。", - "($/1M tokens). Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "($/100 万代币)。示例:输入率 2.50 表示每 1,000,000 个输入代币 2.50 美元。", - "1. Client Request (Input)": "1. 客户端请求(输入)", - "1. Generates SSL cert & adds to system keychain": "1. 生成 SSL 证书并添加到系统钥匙串", - "2. Provider Request (Translated)": "2. 提供商请求(已​​翻译)", - "2. Redirects": "2. 重定向", - "24h": "24小时", - "3. Maps Antigravity models to any provider via 9Router": "3. 通过 9Router 将Antigravity模型映射到任何提供商", - "3. Provider Response (Raw)": "3. 提供商响应(原始)", - "4. Client Response (Final)": "4. 客户回应(最终)", - "About": "关于", - "Access Anywhere": "随处访问", - "Access Token": "访问令牌", - "Access token will be auto-filled...": "访问令牌将自动填充...", - "Account": "帐户", - "account has been connected.": "帐户已连接。", - "Action": "行动", - "Active": "积极的", - "Add": "添加", - "Add a connection to enable importing models.": "添加连接以启用导入模型。", - "Add Anthropic Compatible": "添加Anthropic兼容", - "Add Connection": "添加连接", - "Add connection using browser cookie": "使用浏览器 cookie 添加连接", - "Add Custom Model": "添加自定义模型", - "Add model": "添加型号", - "Add Model": "添加型号", - "Add Model to Combo": "将模型添加到组合", - "Add New Provider": "添加新提供商", - "Add OpenAI Compatible": "添加 OpenAI 兼容", - "Add your first connection to get started": "添加您的第一个连接以开始使用", - "added)": "额外)", - "After authorization, copy the full URL from your browser address bar.": "授权后,从浏览器地址栏中复制完整的 URL。", - "After authorization, copy the full URL from your browser.": "授权后,从浏览器复制完整的 URL。", - "After installation, run": "安装后,运行", - "After login, you'll need to copy the callback URL from your browser and paste it back here.": "登录后,您需要从浏览器复制回调 URL 并将其粘贴回此处。", - "All models are responding normally.": "所有型号均响应正常。", - "All Providers": "所有提供商", - "All rates are in": "所有费率均在", - "An error occurred": "发生错误", - "Anthropic Compatible (Prod)": "Anthropic兼容(产品)", - "API Endpoint": "API端点", - "API Key": "API密钥", - "API Key (for Check)": "API 密钥(用于检查)", - "API Key Compatible Providers": "API 密钥兼容提供商", - "API Key Created": "API 密钥已创建", - "API Key Name": "API 密钥名称", - "API Key Providers": "API 密钥提供商", - "API Keys": "API 密钥", - "API Reference": "API参考", - "API Type": "API类型", - "Apply": "申请", - "Are you sure you want to disable the tunnel?": "您确定要禁用隧道吗?", - "Authenticate": "认证", - "Authentication Method": "认证方式", - "Authentication Successful!": "认证成功!", - "Authorization Successful!": "授权成功!", - "Auto Refresh (3s)": "自动刷新(3秒)", - "Auto-detecting token...": "自动检测令牌...", - "Auto-detecting tokens...": "自动检测令牌...", - "Auto:": "汽车:", - "Available": "可用的", - "AWS Builder ID": "AWS 构建器 ID", - "AWS IAM Identity Center": "AWS IAM 身份中心", - "AWS Region": "AWS 区域", - "AWS region for your Identity Center (default: us-east-1)": "您的身份中心的 AWS 区域(默认值:us-east-1)", - "Back": "后退", - "Back to Providers": "返回提供商", - "Base URL": "基本网址", - "Batch Size": "批量大小", - "Blog": "博客", - "Cache Creation": "缓存创建", - "Cache Creation:": "缓存创建:", - "Cached": "缓存", - "Cached input tokens (typically 50% of input rate)": "缓存输入令牌(通常为输入速率的 50%)", - "Cached:": "缓存:", - "Calls per account before switching": "切换前每个账户的通话次数", - "Cancel": "取消", - "Cert": "证书", - "Changelog": "变更日志", - "chars)": "字符)", - "Chat Completions": "聊天完成", - "Checking Claude CLI...": "检查克劳德 CLI...", - "Checking Codex CLI...": "正在检查 Codex CLI...", - "Checking Copilot config...": "正在检查Copilot配置...", - "Checking Factory Droid CLI...": "检查工厂 Droid CLI...", - "Checking Open Claw CLI...": "正在检查 Open Claw CLI...", - "Checking OpenCode CLI...": "检查 OpenCode CLI...", - "Choose your authentication method:": "选择您的身份验证方法:", - "Claude": "克洛德", - "Claude CLI - Manual Configuration": "Claude CLI - 手动配置", - "Claude CLI not installed": "克劳德 CLI 未安装", - "Clear": "清除", - "Clear Filters": "清除过滤器", - "Clear search": "清除搜索", - "Click to edit": "点击编辑", - "Close test results": "关闭测试结果", - "Cloudflare Tunnel": "Cloudflare 隧道", - "Codex CLI - Manual Configuration": "Codex CLI - 手动配置", - "Codex CLI not installed": "Codex CLI 未安装", - "Codex uses": "Codex用途", - "Combo Name": "组合名称", - "Combos": "组合", - "Coming soon...": "即将推出...", - "Comma-separated hostnames/domains to bypass the proxy.": "以逗号分隔的主机名/域以绕过代理。", - "Company": "公司", - "Complete the authorization in the popup window.": "在弹出的窗口中完成授权。", - "Completion/response tokens": "完成/响应标记", - "Configure a new AI provider to use with your applications.": "配置新的 AI 提供程序以与您的应用程序一起使用。", - "Configure pricing rates for cost tracking and calculations": "配置定价以进行成本跟踪和计算", - "Confirm": "确认", - "Confirm new password": "确认新密码", - "Confirm New Password": "确认新密码", - "Connect": "连接", - "Connect AI tools remotely": "远程连接AI工具", - "Connect Cursor IDE": "连接CursorIDE", - "Connect Kiro": "连接Kiro", - "Connect to providers with OAuth to track your API quota limits and usage.": "使用 OAuth 连接到提供商以跟踪您的 API 配额限制和使用情况。", - "Connect with OAuth2": "使用 OAuth2 连接", - "Connect your account using OAuth2 authentication.": "使用 OAuth2 身份验证连接您的帐户。", - "Connected": "已连接", - "Connected Successfully!": "连接成功!", - "Connection Failed": "连接失败", - "Connections": "连接", - "Contact": "接触", - "Content": "内容", - "Continue": "继续", - "Continue with GitHub": "继续使用 GitHub", - "Continue with Google": "继续使用谷歌", - "Cookie": "曲奇饼", - "Cookie Auth": "Cookie 验证", - "Cookie String": "饼干字符串", - "Cooldown": "冷却", - "Copy": "复制", - "Copy combo name": "复制组合名称", - "Copy model": "复制模型", - "Copy the entire cookie string (must include BXAuth)": "复制整个 cookie 字符串(必须包括 BXAuth)", - "Copy This URL": "复制此网址", - "Cost": "成本", - "Cost Calculation:": "成本计算:", - "Costs are calculated based on token usage and pricing rates. Each request's cost is determined by: (input_tokens × input_rate) + (output_tokens × output_rate) + (cached_tokens × cached_rate)": "成本是根据代币使用和定价率计算的。每个请求的成本由以下因素决定:(input_tokens × input_rate) + (output_tokens × output_rate) + (cached_tokens × cached_rate)", - "Could not read Cursor database automatically.": "无法自动读取游标数据库。", - "Create": "创造", - "Create API Key": "创建 API 密钥", - "Create Combo": "创建组合", - "Create Key": "创建密钥", - "Create model combos with fallback support": "创建具有后备支持的模型组合", - "Create Provider": "创建提供者", - "Create your first API key to get started": "创建您的第一个 API 密钥以开始使用", - "Created": "已创建", - "Current": "当前的", - "Current Password": "当前密码", - "Current Pricing Overview": "当前定价概述", - "Current: Keeps": "当前: 保留", - "Cursor IDE not detected. Please paste your tokens manually.": "未检测到Cursor IDE。请手动粘贴您的令牌。", - "Custom Pricing:": "定制定价:", - "Cycle through accounts to distribute load": "循环访问帐户以分配负载", - "Database backup downloaded": "数据库备份已下载", - "Database imported successfully": "数据库导入成功", - "Database Location": "数据库位置", - "DateTime": "日期时间", - "Debug translation flow between formats": "调试格式之间的翻译流程", - "Delete": "删除", - "Detail": "细节", - "Disable Tunnel": "禁用隧道", - "Disabled": "残疾人", - "Display Name": "显示名称", - "DNS off": "DNS 关闭", - "Documentation": "文档", - "dollars per million tokens": "每百万代币美元", - "Domain:": "领域:", - "Done": "完毕", - "Download Backup": "下载备份", - "e.g. claude-opus-4-5": "例如克劳德作品 4-5", - "e.g., Production API, Dev Environment": "例如,生产 API、开发环境", - "Edit": "编辑", - "Edit Connection": "编辑连接", - "Edit Pricing": "编辑定价", - "Email": "电子邮件", - "Enable DNS per tool below to activate interception": "启用下面每个工具的 DNS 以激活拦截", - "Enable Observability": "启用可观察性", - "Enable proxy for OAuth + provider outbound requests.": "为 OAuth + 提供商出站请求启用代理。", - "Enable Tunnel": "启用隧道", - "Encrypted": "加密的", - "End Date": "结束日期", - "End-to-end TLS via Cloudflare": "通过 Cloudflare 的端到端 TLS", - "Endpoint": "端点", - "Enter current password": "输入当前密码", - "Enter new API key": "输入新的 API 密钥", - "Enter new password": "输入新密码", - "Enter sudo password": "输入sudo密码", - "Enter your API key": "输入您的 API 密钥", - "Est. Cost": "预计。成本", - "Estimated, not actual billing": "估计的,不是实际的账单", - "Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.": "将您本地的 9Router 暴露到互联网。无需端口转发,无需静态 IP。与您的团队共享端点 URL 或从任何地方在 Cursor、Cline 和其他 AI 工具中使用它。", - "Factory Droid - Manual Configuration": "Factory Droid - 手动配置", - "Factory Droid CLI not installed": "Factory Droid CLI 未安装", - "Failed to load usage statistics.": "无法加载使用情况统计信息。", - "Features": "特征", - "Flush Interval (ms)": "冲洗间隔(毫秒)", - "For enterprise users with custom AWS IAM Identity Center.": "适用于具有自定义 AWS IAM Identity Center 的企业用户。", - "Free Providers": "免费提供商", - "Fresh API key obtained": "获得新的 API 密钥", - "GitHub Account": "GitHub 帐户", - "GitHub Copilot - Manual Configuration": "GitHub Copilot - 手动配置", - "Google Account": "谷歌帐户", - "has been connected.": "已连接。", - "Help Center": "帮助中心", - "How it works:": "工作原理:", - "How Pricing Works": "定价如何运作", - "How to get cookie:": "如何获取cookie:", - "IDC Start URL": "IDC 起始网址", - "iFlow AI": "iFlow人工智能", - "iFlow Cookie Authentication": "iFlow Cookie 身份验证", - "Import Backup": "导入备份", - "Import Token": "导入令牌", - "In": "在", - "In / Out": "输入/输出", - "Inactive": "不活跃", - "Inc. All rights reserved.": "公司保留所有权利。", - "Input": "输入", - "Input Tokens": "输入令牌", - "Input Tokens:": "输入令牌:", - "Input:": "输入:", - "Installation Guide": "安装指南", - "Interactive diagram visible on desktop": "桌面上可见的交互式图表", - "Intercepts Antigravity traffic via DNS redirect, letting you reroute models through 9Router.": "通过 DNS 重定向拦截Antigravity流量,让您可以通过 9Router 重新路由模型。", - "KB per field": "每个字段的 KB", - "Key Name": "按键名称", - "Kimi": "基米", - "Kiro AI": "Kiro艾", - "Kiro IDE not detected. Please paste your refresh token manually.": "未检测到 Kiro IDE。请手动粘贴您的刷新令牌。", - "Last updated:": "最后更新:", - "Last Used": "最后使用", - "Latency": "延迟", - "Latency:": "延迟:", - "Lean": "倾斜", - "Leave empty to inherit existing env proxy (if any).": "留空以继承现有的 env 代理(如果有)。", - "Load": "加载", - "Loading logs...": "正在加载日志...", - "Loading models from provider...": "正在从提供商处加载模型...", - "Loading pricing data...": "正在加载定价数据...", - "Local Mode": "本地模式", - "Local Mode - All data stored on your machine": "本地模式 - 所有数据都存储在您的计算机上", - "Login to your account": "登录您的帐户", - "Login with your GitHub account (manual callback).": "使用您的 GitHub 帐户登录(手动回调)。", - "Login with your Google account (manual callback).": "使用您的 Google 帐户登录(手动回拨)。", - "Logs are saved to log.txt in the application data directory.": "日志保存在应用程序数据目录下的log.txt中。", - "Machine ID": "机器ID", - "Machine ID will be auto-filled...": "机器 ID 将自动填充...", - "macOS / Linux / Windows:": "macOS / Linux / Windows:", - "macOS / Linux:": "macOS / Linux:", - "Manual Callback Required": "需要手动回拨", - "Manual Config": "手动配置", - "Max JSON Size (KB)": "最大 JSON 大小 (KB)", - "Max Records": "最大记录数", - "Maximum request detail records to keep (older records are auto-deleted)": "要保留的最大请求详细记录(较旧的记录会自动删除)", - "Maximum size for each JSON field (request/response) before truncation": "截断前每个 JSON 字段(请求/响应)的最大大小", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "刷新缓冲区之前等待的最长时间(防止低流量期间数据丢失)", - "Messages": "留言", - "MiniMax": "最小最大", - "MITM Server": "中间人服务器", - "Model": "模型", - "Model ID": "型号编号", - "Model ID (from OpenRouter)": "型号 ID(来自 OpenRouter)", - "Model is reachable": "模型可达", - "Model mappings will be available soon.": "模型映射即将推出。", - "Model Status": "型号状态", - "Model:": "模型:", - "Models": "型号", - "more providers": "更多提供商", - "Move down": "下移", - "Move up": "向上移动", - "ms / Total": "毫秒/总计", - "Name": "姓名", - "Network": "网络", - "New Password": "新密码", - "No active connections found for this group.": "未找到该组的活动连接。", - "No active providers": "没有活跃的提供商", - "No API keys yet": "还没有 API 密钥", - "No combos yet": "还没有组合", - "No compatible providers added yet": "尚未添加兼容的提供商", - "No connections": "无连接", - "No connections yet": "还没有连接", - "No console logs yet.": "还没有控制台日志。", - "No data for this period": "此期间没有数据", - "No logs recorded yet.": "尚未记录任何日志。", - "No models": "没有型号", - "No models added yet": "尚未添加型号", - "No models configured": "没有配置型号", - "No models found": "没有找到型号", - "No models match your filter.": "没有型号与您的过滤器匹配。", - "No pricing data available": "无可用定价数据", - "No Providers Connected": "没有连接提供商", - "No Proxy": "无代理", - "No quota data available": "无可用配额数据", - "No request details found": "未找到请求详细信息", - "No requests yet.": "还没有请求。", - "Not configured": "未配置", - "Number of items to accumulate before writing to database (higher = better performance)": "写入数据库之前要累积的项目数(越高=性能越好)", - "OAuth Providers": "OAuth 提供商", - "Observability": "可观察性", - "Only letters, numbers, - and _ allowed": "只允许使用字母、数字、- 和 _", - "Only one connection is allowed per compatible node. Add another node if you need more connections.": "每个兼容节点仅允许一个连接。如果需要更多连接,请添加另一个节点。", - "Open Claw - Manual Configuration": "Open Claw - 手动配置", - "Open Claw CLI not installed": "未安装 Open Claw CLI", - "Open DevTools (F12) → Application/Storage → Cookies": "打开 DevTools (F12) → 应用程序/存储 → Cookie", - "Open platform.iflow.cn in your browser": "在浏览器中打开platform.iflow.cn", - "OpenAI Compatible (Prod)": "OpenAI 兼容(产品)", - "OpenCode - Manual Configuration": "OpenCode - 手动配置", - "OpenCode CLI not installed": "未安装 OpenCode CLI", - "OpenRouter": "开放路由器", - "OpenRouter supports any model. Add models and create aliases for quick access.": "OpenRouter 支持任何型号。添加模型并创建别名以便快速访问。", - "Other": "其他", - "Out": "出去", - "Outbound Proxy": "出站代理", - "Output": "输出", - "Output Tokens": "输出代币", - "Output Tokens:": "输出代币:", - "Output:": "输出:", - "Password updated successfully": "密码更新成功", - "Passwords do not match": "密码不匹配", - "Paste it below": "粘贴在下面", - "Paste refresh token from Kiro IDE.": "从 Kiro IDE 粘贴刷新令牌。", - "Paused": "已暂停", - "Please add and connect providers first to configure CLI tools.": "请先添加并连接提供商以配置 CLI 工具。", - "Please copy the URL from the address bar and paste it in the application.": "请复制地址栏中的 URL 并将其粘贴到应用程序中。", - "Please enter a Proxy URL to test": "请输入代理 URL 进行测试", - "Please install Claude CLI to use this feature.": "请安装 Claude CLI 才能使用此功能。", - "Please install Codex CLI to use auto-apply feature.": "请安装 Codex CLI 以使用自动应用功能。", - "Please install Factory Droid CLI to use this feature.": "请安装 Factory Droid CLI 才能使用此功能。", - "Please install Open Claw CLI to use this feature.": "请安装 Open Claw CLI 才能使用此功能。", - "Please install OpenCode CLI to use auto-apply feature.": "请安装 OpenCode CLI 以使用自动应用功能。", - "Please wait while we complete the authorization.": "我们正在完成授权,请稍候。", - "Popup blocked? Enter URL manually": "弹出窗口被阻止?手动输入网址", - "Prefix": "前缀", - "Pricing": "定价", - "Pricing Configuration": "定价配置", - "Pricing Format:": "定价格式:", - "Pricing Rates Format": "定价格式", - "Pricing Settings": "定价设置", - "Priority": "优先事项", - "Privacy Policy": "隐私政策", - "Product": "产品", - "Production Key": "生产密钥", - "Provider": "提供者", - "Provider Limits": "提供商限制", - "Provider not found": "找不到提供者", - "Provider:": "提供商:", - "Providers": "供应商", - "Proxy settings applied": "已应用代理设置", - "Proxy URL": "代理网址", - "Purpose:": "目的:", - "Qwen": "奎文", - "Reading from AWS SSO cache": "从 AWS SSO 缓存中读取", - "Reading from Cursor IDE database": "从 Cursor IDE 数据库读取", - "Reasoning": "推理", - "Reasoning:": "推理:", - "Recent Requests": "最近的请求", - "Recommended for most users. Free AWS account required.": "推荐给大多数用户。需要免费 AWS 帐户。", - "records, batches every": "记录,每批", - "Refresh": "刷新", - "Refresh All": "全部刷新", - "Refresh quota": "刷新配额", - "Refresh Token": "刷新令牌", - "Reload VS Code after applying for changes to take effect.": "申请更改生效后重新加载 VS Code。", - "Remove": "消除", - "Remove custom model": "删除自定义模型", - "Remove model": "删除模型", - "Request Details": "请求详情", - "Request Logs": "请求日志", - "Requests": "要求", - "Requests without a valid key will be rejected": "没有有效密钥的请求将被拒绝", - "requests, max": "请求数,最大", - "Require API key": "需要 API 密钥", - "Require login": "需要登录", - "Required for SSL certificate and DNS configuration": "SSL 证书和 DNS 配置所需", - "Required for SSL certificate and server startup": "SSL 证书和服务器启动所需", - "Required to modify /etc/hosts and flush DNS cache": "需要修改 /etc/hosts 并刷新 DNS 缓存", - "Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.": "需要出站端口 7844 (TCP/UDP)。连接可能需要 10-30 秒。", - "Reset": "重置", - "Reset to default": "重置为默认值", - "Reset to Defaults": "重置为默认值", - "Resources": "资源", - "Responses API": "响应API", - "Retry": "重试", - "Round Robin": "循环赛", - "Routing Strategy": "路由策略", - "Rows:": "行:", - "Run this command in your terminal, then click": "在终端中运行此命令,然后单击", - "Running": "跑步", - "Running on your machine": "在你的机器上运行", - "s)": ")", - "Save Mappings": "保存映射", - "Save this key now!": "立即保存此密钥!", - "Search model id": "搜索型号 ID", - "Security": "安全", - "Select": "选择", - "Select a provider": "选择提供商", - "Select all": "选择全部", - "Select Model": "选择型号", - "Select Model for Codex": "选择 Codex 型号", - "Select Model for Factory Droid": "选择工厂 Droid 的型号", - "Select Model for GitHub Copilot": "选择 GitHub Copilot 的型号", - "Select Model for Open Claw": "选择Open Claw型号", - "Select Model for OpenCode": "选择 OpenCode 型号", - "Selected only": "仅选定", - "Selected provider": "选定的提供商", - "Send to Provider": "发送给提供商", - "Sent to provider as:": "发送给提供商:", - "Server": "服务器", - "Server off": "服务器关闭", - "Setting up": "设置", - "Share Endpoint": "共享端点", - "Share URL with team members": "与团队成员共享 URL", - "Show only selected models": "仅显示选定的型号", - "Showing": "显示中", - "Special reasoning/thinking tokens (fallback to output rate)": "特殊推理/思维标记(回退到输出率)", - "Standard prompt tokens": "标准提示标记", - "Start Date": "开始日期", - "Start DNS": "启动 DNS", - "Start MITM": "启动中间人", - "Start Server": "启动服务器", - "Start Tunnel": "开始隧道", - "Status": "地位", - "Status:": "地位:", - "Step 1: Open this URL in your browser": "第 1 步:在浏览器中打开此 URL", - "Step 2: Paste the callback URL here": "第 2 步:将回调 URL 粘贴到此处", - "Sticky Limit": "粘性限制", - "Stop DNS": "停止 DNS", - "Stop MITM": "停止中间人", - "Stop Server": "停止服务器", - "Stopped": "已停止", - "Sudo Password Required": "需要 sudo 密码", - "Terms of Service": "服务条款", - "Test": "测试", - "Test all API Key connections": "测试所有 API 密钥连接", - "Test all Compatible connections": "测试所有兼容连接", - "Test all Free connections": "测试所有免费连接", - "Test all Free provider connections": "测试所有免费提供商连接", - "Test all OAuth connections": "测试所有 OAuth 连接", - "Test model": "测试模型", - "Test proxy URL": "测试代理 URL", - "Test Results": "测试结果", - "The tunnel will be disconnected. Remote access will stop working.": "隧道将被断开。远程访问将停止工作。", - "The unified interface for modern AI infrastructure. Secure, observable, and scalable.": "现代人工智能基础设施的统一接口。安全、可观察且可扩展。", - "Thinking Process": "思考过程", - "This is the only time you will see this key. Store it securely.": "这是您唯一一次看到这把钥匙。安全存放。", - "Timestamp": "时间戳", - "Timestamp:": "时间戳:", - "To get a fresh API key, paste your browser cookie from": "要获取新的 API 密钥,请粘贴您的浏览器 cookie", - "to verify.": "来验证。", - "Toggle DNS to redirect": "切换 DNS 重定向", - "Token auto-detected from Kiro IDE successfully!": "已成功从 Kiro IDE 自动检测到令牌!", - "Token Types:": "代币类型:", - "Token will be auto-filled...": "令牌将自动填充...", - "Tokens": "代币", - "Tokens auto-detected from Cursor IDE successfully!": "已成功从 Cursor IDE 自动检测到令牌!", - "Tokens used to create cache entries (fallback to input rate)": "用于创建缓存条目的令牌(回退到输入速率)", - "Total Input Tokens": "总输入代币", - "Total Models": "型号总数", - "Total Requests": "请求总数", - "Total:": "全部的:", - "traffic through 9Router via MITM.": "通过 MITM 通过 9Router 的流量。", - "Translator Debug": "翻译器调试", - "Try Again": "再试一次", - "Tunnel connected!": "隧道连通!", - "Tunnel disabled": "隧道已禁用", - "Turn request detail recording on/off globally": "全局打开/关闭请求详细信息记录", - "Twitter": "叽叽喳喳", - "Unavailable": "不可用", - "Unknown": "未知", - "Unselect all": "取消选择全部", - "Update": "更新", - "Usage by Account": "按帐户使用情况", - "Usage by API Key": "按 API 密钥使用", - "Usage by Endpoint": "按端点使用", - "Usage by Model": "按型号使用", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "使用 Antigravity IDE 和 GitHub Copilot → 与 9Router 的任何提供商/模型", - "Use in Cursor/Cline": "在Cursor/Cline中使用", - "Use the buttons above to add OpenAI or Anthropic compatible endpoints": "使用上面的按钮添加 OpenAI 或 Anthropic 兼容端点", - "Use your API from any network": "从任何网络使用您的 API", - "Verification URL": "验证网址", - "View Full Details": "查看完整详情", - "Visit the URL below and enter the code:": "访问以下网址并输入代码:", - "Waiting for Authorization": "等待授权", - "Waiting for authorization...": "等待授权...", - "Warning": "警告", - "When": "什么时候", - "When ON, dashboard requires password. When OFF, access without login.": "当打开时,仪表板需要密码。当关闭时,无需登录即可访问。", - "Windows: Run 9Router terminal as Administrator": "Windows:以管理员身份运行 9Router 终端", - "Windows: Run terminal (9Router) as Administrator to enable MITM": "Windows:以管理员身份运行终端 (9Router) 以启用 MITM", - "Writes to": "写入到", - "You can override default pricing for specific models. Reset to defaults anytime to restore standard rates.": "您可以覆盖特定型号的默认定价。随时重置为默认值以恢复标准费率。", - "Your": "你的", - "Your Code": "你的代码", - "Your Kiro account via": "您的 Kiro 帐户通过", - "Your organization's AWS IAM Identity Center URL": "您组织的 AWS IAM Identity Center URL", - "Quota Tracker": "配额跟踪器", - "CLI Tools": "命令行工具", - "Console Log": "控制台日志", - "System": "系统", - "Debug": "调试", - "Settings": "设置", - "Usage": "使用情况", - "Shutdown": "关闭", - "Close Proxy": "关闭代理", - "Are you sure you want to close the proxy server?": "您确定要关闭代理服务器吗?", - "Server Disconnected": "服务器已断开", - "The proxy server has been stopped.": "代理服务器已停止。", - "Reload Page": "重新加载页面", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "服务正在终端中运行。您可以关闭此网页。关闭将停止服务。" -} diff --git a/9router/public/i18n/literals/zh-TW.json b/9router/public/i18n/literals/zh-TW.json deleted file mode 100644 index 0fb9c424..00000000 --- a/9router/public/i18n/literals/zh-TW.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "Cancel": "取消", - "Delete": "刪除", - "Edit": "編輯", - "Save": "保存", - "Close": "關閉", - "Add": "添加", - "Remove": "移除", - "Settings": "設置", - "Profile": "個人資料", - "Dashboard": "儀表板", - "Logout": "登出", - "Login": "登錄", - "Providers": "提供者", - "Usage": "使用情況", - "API Key": "API 金鑰", - "Connected": "已連接", - "Disconnected": "未連接", - "Active": "活躍", - "Inactive": "非活躍", - "Success": "成功", - "Failed": "失敗", - "Error": "錯誤", - "Warning": "警告", - "Info": "信息", - "Loading": "載入中", - "Search": "搜尋", - "Filter": "篩選", - "Sort": "排序", - "Export": "導出", - "Import": "導入", - "Refresh": "刷新", - "Back": "返回", - "Next": "下一個", - "Previous": "上一個", - "Submit": "提交", - "Confirm": "確認", - "Yes": "是", - "No": "否", - "OK": "確定", - "Apply": "應用", - "Reset": "重置", - "Clear": "清除", - "Select": "選擇", - "Upload": "上傳", - "Download": "下載", - "Copy": "複製", - "Paste": "粘貼", - "Cut": "剪切", - "Undo": "撤銷", - "Redo": "重做", - "Name": "名稱", - "Description": "描述", - "Status": "狀態", - "Type": "類型", - "Date": "日期", - "Time": "時間", - "Created": "已建立", - "Updated": "已更新", - "Actions": "操作", - "Details": "詳細信息", - "View": "查看", - "New": "新建", - "Total": "總計", - "Count": "計數", - "Price": "價格", - "Cost": "成本", - "Free": "免費", - "Paid": "付費", - "Enable": "啟用", - "Disable": "禁用", - "Enabled": "已啟用", - "Disabled": "已禁用", - "Online": "在線", - "Offline": "離線", - "Available": "可用", - "Unavailable": "不可用", - "Required": "必需", - "Optional": "可選", - "Default": "默認", - "Custom": "自定義", - "Advanced": "進階", - "Basic": "基本", - "Help": "幫助", - "Support": "支持", - "Documentation": "文檔", - "Version": "版本", - "Language": "語言", - "Theme": "主題", - "Light": "淺色", - "Dark": "深色", - "Auto": "自動", - "Endpoint": "端點", - "Providers": "提供者", - "Combos": "組合", - "Usage": "統計", - "Quota Tracker": "配額跟蹤", - "MITM": "MITM", - "CLI Tools": "CLI 工具", - "Console Log": "控制台日誌", - "System": "系統", - "Debug": "調試", - "Shutdown": "關機", - "Close Proxy": "關閉代理", - "Are you sure you want to close the proxy server?": "您確定要關閉代理服務器嗎?", - "Server Disconnected": "服務器已斷開連接", - "The proxy server has been stopped.": "代理服務器已停止。", - "Reload Page": "重新加載頁面", - "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "服務在終端中運行。您可以關閉此網頁。關機將停止服務。", - "Manage your AI provider connections": "管理您的 AI 提供者連接", - "Model combos with fallback": "具有備用的模型組合", - "Monitor your API usage, token consumption, and request logs": "監控您的 API 使用、令牌消耗和請求日誌", - "Intercept CLI tool traffic and route through 9Router": "攔截 CLI 工具流量並通過 9Router 路由", - "Configure CLI tools": "配置 CLI 工具", - "API endpoint configuration": "API 端點配置", - "Manage your preferences": "管理您的首選項", - "Debug translation flow between formats": "調試格式之間的轉換流", - "Live server console output": "實時服務器控制台輸出", - "Create model combos with fallback support": "創建具有備用支持的模型組合", - "Local Mode": "本地模式", - "Running on your machine": "在您的機器上運行", - "Database Location": "數據庫位置", - "Download Backup": "下載備份", - "Import Backup": "導入備份", - "Database backup downloaded": "已下載數據庫備份", - "Database imported successfully": "已成功導入數據庫", - "Security": "安全性", - "Require login": "需要登錄", - "When ON, dashboard requires password. When OFF, access without login.": "打開時,儀表板需要密碼。關閉時,無需登錄即可訪問。", - "Current Password": "當前密碼", - "Enter current password": "輸入當前密碼", - "New Password": "新密碼", - "Enter new password": "輸入新密碼", - "Confirm New Password": "確認新密碼", - "Confirm new password": "確認新密碼", - "Update Password": "更新密碼", - "Set Password": "設置密碼", - "Password updated successfully": "已成功更新密碼", - "Passwords do not match": "密碼不匹配", - "Routing Strategy": "路由策略", - "Round Robin": "輪詢", - "Cycle through accounts to distribute load": "循環循環帳戶以分配負載", - "Sticky Limit": "粘性限制", - "Calls per account before switching": "切換前每個帳戶的調用次數", - "Network": "網絡", - "Outbound Proxy": "出站代理", - "Enable proxy for OAuth + provider outbound requests.": "為 OAuth + 提供者出站請求啟用代理。", - "Proxy URL": "代理 URL", - "Leave empty to inherit existing env proxy (if any).": "留空以繼承現有的環境代理(如果有)。", - "No Proxy": "無代理", - "Comma-separated hostnames/domains to bypass the proxy.": "逗號分隔的主機名/域以繞過代理。", - "Test proxy URL": "測試代理 URL", - "Apply": "應用", - "Proxy settings applied": "已應用代理設置", - "Proxy enabled": "已啟用代理", - "Proxy disabled": "已禁用代理", - "Proxy test OK": "代理測試成功", - "Proxy test failed": "代理測試失敗", - "Please enter a Proxy URL to test": "請輸入要測試的代理 URL", - "Observability": "可觀測性", - "Enable Observability": "啟用可觀測性", - "Turn request detail recording on/off globally": "全局打開/關閉請求詳細記錄", - "Max Records": "最大記錄數", - "Maximum request detail records to keep (older records are auto-deleted)": "要保留的最大請求詳細記錄數(舊記錄自動刪除)", - "Batch Size": "批量大小", - "Number of items to accumulate before writing to database (higher = better performance)": "寫入數據庫前要積累的項目數(更高 = 更好的性能)", - "Flush Interval (ms)": "刷新間隔 (ms)", - "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "刷新緩衝區之前的最大等待時間(防止低流量期間的數據丟失)", - "Max JSON Size (KB)": "最大 JSON 大小 (KB)", - "Maximum size for each JSON field (request/response) before truncation": "截斷前每個 JSON 字段(請求/響應)的最大大小", - "All data stored on your machine": "所有數據存儲在您的機器上", - "MITM Server": "MITM 服務器", - "Running": "運行中", - "Stopped": "已停止", - "Cert": "證書", - "Server": "服務器", - "Purpose:": "目的:", - "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "使用 Antigravity IDE 和 GitHub Copilot → 與 9Router 中的任何提供者/模型一起", - "How it works:": "工作原理:", - "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "Antigravity/Copilot IDE 請求 → DNS 重定向到 localhost:443 → MITM 代理攔截 → 9Router → 响應到 Antigravity/Copilot", - "API Key": "API 金鑰", - "No API keys — create one in Keys page": "沒有 API 金鑰 — 在金鑰頁面中創建一個", - "sk_9router (default)": "sk_9router(默認)", - "Server started": "服務器已啟動", - "Failed to start server": "啟動服務器失敗", - "Server stopped — all DNS cleared": "服務器已停止 — 已清除所有 DNS", - "Failed to stop server": "停止服務器失敗", - "Sudo password is required": "需要 Sudo 密碼", - "Stop Server": "停止服務器", - "Start Server": "啟動服務器", - "Enable DNS per tool below to activate interception": "為下面的每個工具啟用 DNS 以激活攔截", - "Sudo Password Required": "需要 Sudo 密碼", - "Enter your sudo password to start/stop MITM server": "輸入您的 Sudo 密碼以啟動/停止 MITM 服務器", - "Sudo Password": "Sudo 密碼", - "Confirm": "確認" -} diff --git a/9router/public/icons/icon-192.svg b/9router/public/icons/icon-192.svg deleted file mode 100644 index e7978910..00000000 --- a/9router/public/icons/icon-192.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - 9R - diff --git a/9router/public/icons/icon-512.svg b/9router/public/icons/icon-512.svg deleted file mode 100644 index 6fef45af..00000000 --- a/9router/public/icons/icon-512.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - 9R - diff --git a/9router/public/next.svg b/9router/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/9router/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/9router/public/providers/alicode-intl.png b/9router/public/providers/alicode-intl.png deleted file mode 100644 index 21c3ef3df575a3dd0a19ef8d384dee023f8421d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4859 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 ZtX1pk;TUF2%HMx7RYeVj%4g=G{{x|fb3*_C diff --git a/9router/public/providers/anthropic.png b/9router/public/providers/anthropic.png deleted file mode 100644 index dc58142bd80af59ed94e51da6a1055d81d160646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2736 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 BvyuP+ diff --git a/9router/public/providers/antigravity.png b/9router/public/providers/antigravity.png deleted file mode 100644 index 6fe0feaee7ae4fb09679f470df7e89553469e8b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11588 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 a$G8j*DBB-Of&BBsv6_;WVx_!w`2PY0re2W% diff --git a/9router/public/providers/cerebras.png b/9router/public/providers/cerebras.png deleted file mode 100644 index a4ace23a6c6f1ef74437d81d5faad91f89fe0375..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2295 zcmb`J`#;nD8^_-}7-q96W+8_Zl4C*R3xV`_U%NJ4asELt;gg0CwzZ+U9ao${NeS(^~39NWjGzN$D)x9DN=Zt^d#;f$=EGq#X# zJIkBS__c1xWR^?AEMnMRp>nxWggUqu#8WO&*C17tTwu*Jgrh7Ssfu(V>9ao>35!D$ z)u2HeXY`qUv}mv3Vv{rMhgLDsZyZ~ndYs}XO-A&NH!O!|z;({uUZj>QVicYgpCFDR zZObC3)iLD#6?9&_2lL0k(i17;mjS)!y9}240tQrNv5aB`B_l_2t4b=0MyOUyxb-Hj zc)QUfzw_GOjoy1ybnRkha$9+e6<8G|blOi(0mJPM87DlnN6F`FDYZSOTt?*=IrDBL zG}WiuG5vtrW_~U|X?FXSQJJ(4`q-z2n9UD(?d69L7O*zRVh@{&V4-^h(Xr=`hOFih zFvkPo0?zB5LyrUx%S|wO6)U-(g86sexW@5)B9+j44-;RWtp3y)7Xeh10-ve7X;rIY zB6f@B2DUepl)}_MMo2`>jP!LxTv9-|7Ax%i&$(|euGW0lv=QVDkC7_h59X-mzI;?7 zDXTDWM%-9-dJBqW+cPfsI3Sl=`CPKpW(VCVQ*VZ3svWlrY;?q(-2`osL6z);tL}Dn zFr9j}4#_wbW-kbnQ%gtNsD_;YYyCi{56gZV3mtOt|L0rMh1DPu%yRyxkt~3_# zpdz>Eeg_AvkrEuZCf+$j(r7_L2M~yi^n;fFw`g^RjVM2f<{#>UZddAidk*tC=L?@P zSOlBg3gNWC#zlynZ^~zGJj=WjWz73(2|1f1Y->;pZ}2#iMN86L2>^=!$c-bI?MHClK2F3sjo42# zIU%y>x^-72eJ3(X|H(nk>9p-iW2DtfPtGwaOX5$4n7ON6$D7?(@*@r+fk znvUJ#VeJIq;r30OoaTqCS{+S(HRj$!S4M-@*q+I*%%1TTGOeb&cNTZ6$Qoc^R+K2U zJuQ`C>}K{5qK~cV6xY|;;X^8-^)F6j5a1l9)x@fjTi(K&7mjoDHqZMV>JGnUy&X7< zak8|*nuB0%qvQBjvzN`}f#BecNpmAEzeoxXlD-s~$i)f| z#`)PIIBmyTu)k^E^`N}wtYVd~8f=`_*vu{lymy->Qs;hcI}TL1*usXR6MnO>u2OH# z+vnHV6s`e3Dp;Gb_ubVZ>Tm${O*e`EbMDT-j%&bjxScIDJN5nf3yf3zdVHKUqCd0>1po2mNX(-%oR^hrb-p@0AmTYxbJ-OOSjm zW0rbrzqF|H?+=2y7n^o@HS?_l(#7*eYi!3BsCFWn@-Y(IqGbw_L@M>mRRPbr>B%Uw zMdO&Dy8H1T%;MG$4>fTvH3YlN?`e9yZEjZasdR$nSV735oFG-#4W#H4DwooYcW44C zH^!iD>KM}31-QC3+IN;EVzEHacV)+KebG9B=g@mrH(3CW6-C5N_X~;;*Ag=J`J9N+ z2aivG+uL1P&{gKu^n@j z))4iuUR=$69slxTRl~1JE1l%+Qmvo_UdWX1*s4MltP!iO2(_`_s3oq!+0NX(&_XfKDi|K8J*ekJF0J%Am@2R(~Mx2}&YDTamH~p+FuBHZwg8MOy*4A}1PlDdlI0 zazP9Rn9>;81dLN)|ID*GR67NpML9zaE=widK>JGveUm0_NYR_Y*ai|X?t$`6EQG zfwg*>V3veGWNqP)OjKZgC3`*`Yt=?_zq%w^u#CtaZ8CDU{6SicgfP-uO%x`>%g8z;XW( JyBZs6%D=o<2ZI0r diff --git a/9router/public/providers/chutes.png b/9router/public/providers/chutes.png deleted file mode 100644 index f39a6c21c2181413d41b3ecc67b09690aa545bff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4730 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 diff --git a/9router/public/providers/claude.png b/9router/public/providers/claude.png deleted file mode 100644 index c223ad464bd3837ce7758ee54bbb035f7a00aa08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11797 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 P&sSDbNuox~DER*YOT_ga diff --git a/9router/public/providers/codex.png b/9router/public/providers/codex.png deleted file mode 100644 index 41a9dd775e7363fd2f4e63939b96904334b2090b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5819 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 U3er5db$!pPrKYD^qih}VFF!nb5&!@I diff --git a/9router/public/providers/cohere.png b/9router/public/providers/cohere.png deleted file mode 100644 index 60a0fafe80f9805ecf2d4abd7edb9fb8719a85c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2791 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 RZMJd+4D<{!MLIUY{{a%MG|m73 diff --git a/9router/public/providers/continue.png b/9router/public/providers/continue.png deleted file mode 100644 index f54685be8de738c2df92cd94cb4b8a2cacea4304..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33371 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 QSpWb407*qoM6N<$f@{sHO8@`> diff --git a/9router/public/providers/copilot.png b/9router/public/providers/copilot.png deleted file mode 100644 index 9907963e408e15a0d4c96018f692001bfd4f7e6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27346 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 diff --git a/9router/public/providers/cursor.png b/9router/public/providers/cursor.png deleted file mode 100644 index ec02b070bad87b6412334450a80ae435ffbff049..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7736 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 diff --git a/9router/public/providers/deepseek.png b/9router/public/providers/deepseek.png deleted file mode 100644 index 5df2f5040f93f48f5c90c03b2a10c6b078012bcb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2393 zcmb_e`B&139{nPUg1e+3ZiMB&o@r7iQ!?Cgqq0}N1lN+u4XsQ?5g95qGs;{>Np!T_ zUTTY`xi?6T`(&vh3ZuD{yQPAtJ^ShX1Mi3Xx#!$_&gZAkx%ZQf_jXl;pZT2qg7A4?Nzv) znWS{Mef6Sc&PD6f_hniSMEuOD%0px5)+kP#6B?(DXCb_*$0iyuNLM@#0+us5%_-@s z)HgZ^lB#?`d9=l#a`}=ch3}JV2mHcQdYkQ{R|w@*-XD@rFtPq+F^MEAcg^w2%q7^} ziN~1}2owdUNvHM9qTdGPdd$8w?S00@&DxVSHz4HtqtXl%2)=8+c~AqG1# z{i2YHCCsHonw-z)iYVUZZC+evVa89cULvjw>q3~7`H@Z9S4%T2&KZg44m1^O8XB5V z3Gjh)yeRlo<*B}~S40xo8#8qqlGjsFUUiZbA+Y`JRtD!h>g>Y?S3-&=C?}j=GE`jI za=+I4Tv|EGTCbqjSaNb1j;JUzIRV+|%H4hOut9P&smd6<{)A&ZKcC0bX1k=rtp0eVE3p2?j656{P_dw!%A4Qs2eZ%3+HN zhBntV$V15(sTn#rQd!o?At4fLO^dOlBhfNABcFrn9gT<+3QQ@?!*V7pkbegaaRaIL zoOP42T6R_em(k>AjLW2uTbMM^87yP{z!^{`MU?{>qn#39l>oO@E|_*erNsWGFECA% zw#LHJ@I_80|JU_Fhx%APjdRbY$Zyr0#&;PDD+hNHM#h+S)L()Ieq+qks!V={G;9Fu zWSOLw@?(uU@yW+m=V5OamEJraZALVGG3j`1ZgfmZ;gfX2wt&(1=˜&4VMRsitr zjKR4fbRE^SPFQ)Zpk!9T%07+?;!A8xEYA9Q-}&xhdx5^H@BICQ@Z$&aK<#;mt;X>H zV0hWWl3h+L1%iQs*ZGw3P80r630oNT*RKyE7+VWanX&nh5N^T)zeQe{ z@~^+p!m8UQ2pt9A_g2ISHBCkg=te(-P0i9+ zpKF_MN>~^WLEw(#^|YKno|Mx_5U#m7d7R2qXDDBi6$9d51Skj^k@7qiQBZ>!;tPr{?*VgIdQ3IP-! zm6dU$*@H6MC&SQT=Uk=-Y}1TiHO`HAfLG~@q(1Gv;64kqQj%2T)f;OD!b*ckStlY= zcq?H|B6dCRn;UKHBGeBV-AN3iz1>rdrhSw*9sJ$(s6bvwNr~wc^#9yGsVS_EE@j)q zOMeWXbf2M4@I^&nfmLk({&5KOoV_tMel8~_So%j423FbYgPGb_iCVI|?l*45D- z5&iRIe$@0cAtHcqhxkFg_?_sRN8r1@H+C<(ki~o>-|HNplmrwF2Ke1s9CzqanaHgJ zwMi_W%X^H{DSn_vuVdAmsDO)fi14N$0M^35K-|WFo!P-cE#~h1Te01(Uo?8!Mjb2{%&hGF#+V?RxdjV^+%37ej@J9A}j+W8+mr|B{}bNmi~ImI6QcIb3~=E z3iGosaDXt%zbNQfIm3An!A!Z!?@`)N3FWdwQoA`#UBlFmr;Q;Jwd@z z4}PZQ(?V5^4A38C(x7e)sr!<{zEq}q$N>FWIs@wV@6KSUo>+7kh`X}+4Eet-m2--e zB^Vw=+h~(0ws_vMI;i_BoJXty*v&q;Vr{Yts@W9JjY|hUl|$tx1Ter{QCgV zUL$k4$s3O??M>xoqD@d*F~Jo}8mg&RhSPX)FSz$1-TwmJ{n2}m3{9~Qv>9C8itsm0 z&!+SOl#xdmT%~k5U${%C`GUfi3^m8-<3sbm#DETWq&^-J7Chh#7Aie4ioBPh;#;rp z_&y)ys}=;=Xt_?_y(I06lI1O1HZ8h#HEOWTo55AO@GIl}cf{zwY}<7V-)nkiU%?vR z(%G+aO+i*R{qKxhF07JFf)U~5so}Jh^E`{R#nrnmpOlzP=-mnQ7PPyIWa#itQL&Hx zWLi13mqdC~83cigu*OH?u9G5^1xIsD=W^s9%`lseG#K`pUuAVl&)P$m_GDuhnpvlZ S$IAA9CBWU;8&~5LNdGSqX=~E} diff --git a/9router/public/providers/droid.png b/9router/public/providers/droid.png deleted file mode 100644 index 28b8350a78a73f17a3949cb29e280aa688e9ae37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6875 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 H>2A&40Dk7FH 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 diff --git a/9router/public/providers/github.png b/9router/public/providers/github.png deleted file mode 100644 index 9907963e408e15a0d4c96018f692001bfd4f7e6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27346 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 diff --git a/9router/public/providers/glm-cn.png b/9router/public/providers/glm-cn.png deleted file mode 100644 index cee2b24be2c9f03697cdf0cc2fed04b7116a7cb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2548 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 X=EL{!GuID{0D&B{w6}PAlpOm%q=9T+ diff --git a/9router/public/providers/glm.png b/9router/public/providers/glm.png deleted file mode 100644 index cee2b24be2c9f03697cdf0cc2fed04b7116a7cb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2548 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 X=EL{!GuID{0D&B{w6}PAlpOm%q=9T+ diff --git a/9router/public/providers/groq.png b/9router/public/providers/groq.png deleted file mode 100644 index b6bb68a13035d58202bba9d452f1552190b5dfac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2882 zcmb`J`#;l<7sub*7{=t1`!%^s$-U+>m&~2qQej95g-DdkHY1nhe#s^BaVvLS+|4Fr zDf6w5ORg)ch4iuITAO`+{($dq-ydG*{BRzRbNS&sUe63yXA~SJ4FdoGZf9%lc9^mM ziWhnqUz-JtA11y?+l$cvz%Tl*KtNfABmls+?5r(3;wm|5{ph#Qj_k<(Gad~MYy3v_@B=%e1^6vKPALCfJXVNL3hFZze zH(rKp8iurM*ZZSt{ppT)dL0!>wu+t{IfkG|XD&$hzZXe38ddp0N-o(~Mem2R`{sbXXJ=)(cvypHpw{`qsMLJL@FMyh9|D8nmGQzq#j*ettUP2 z<#8nAvlQz{;zTDl;gUN8Q3mzVg5Swar<;x>@W_$~o}!Gpyzu&l%3Wg6OKs!LSNh%R4hel7)m2BE z@niIXw^-wxb=hgoygj4(Yp+&FCnY}m=bNtyPe6{OZ=-;6-P2$d>Wh<{C55?3iP%D0 zU}+uAeXSl-OTYXvXyc{ zfyflc>u%gazt1T`hOxY*leI9#tp`YGI!IWz+D;7lT>>W{1W^jIEYSS_4p7H)#oP1K zyxyg;dJU+AUaZ#OG~XY*5d(K;cl4 zec9Q;-nu~9on|j}dCZ`y(82(MC~aho=;|;+wr(VC28oACEd?v!Wv5+*vsq<^K$q)f zSvfkf)Z7#ChM|vCh3{{NJ(17D+>5#Tr7f}lq;W5i*87d8cR#xvw5DQql`7#vmcU8JDdOd=Ttv4dCmo0!TpK3Nb>^1SQPt-mmLcz zTwVL!fw>ffVVVy0iG!aVS}-9|Wg6Ijg*l(^o8SiraWFpkx{KcSLUdtN^M0^`QzZyiJ`L3iyo{^8T$4mqAbXWfZ@dO~ z#5K4;Ej5xh{kB)+&UlAlhrSJ}O#<^q+yzN%DbG(SIzBb@?WdUYk49a`Y_GhpEmA8_ z#YEUwf)-5Y@pk5Sj4wfg@Fm!vGJT=CelBIC<&NNZFuXn5p&evqBHaR-sNxr=QgDX=lWIg2Px)@waTrkBt4C~^;bKuJuu$s#0x|=I<`SaI@WIbSKhug+X;u$FO zjUdm#<6Q>IGe_)5S|N0H@5W*4Z?V<6)&|z+eeqE{KDy9x5$$`hO^oA zXPK>J8+R82U?RyA=WBXi68U?c^ry(Xom=~FN%UURy{z?V*c~4Xe9${119aJTaA{_6zMPnS5#G`VqK%QLj}bM)$=)pt zt8r0I$(6z;5rO`m+9J#bRK4cPXpZGl1l;}3*Pssv8|9Nwp@n}=oD_>Yo6d$<*t9nY zrm^lf*rPRX`L+jVsX}g>_HZ7=e4;kYksGlZZ{NOPS=yyWS;O$BbuX}+yE%DgKnH4Ml5bVNn{?}|X5;yJ_%t!1x zy%Qx#1v77@Vjo-86$hg$>#pP|_!*9jy5Bg~B<=7^F;bkuj5u9%Y0h^Vh)|eJf;^>5 zQFR-zag-y7>kMdHm6k-zHp^7!BcQrXVmpH(m$@77I@>=b_X0No2a z%O70GYO(4+o9*D^D;Xl9zUH8l`9Q+A=;HfRrTL&O^VR1(YX95wtcT$K(k2ZuTq-j| zIF0Utd?>;JyjfeeDd2^zISp89-1Y$OaA^_6MX+igvC!odu_sOlA^S-WP;tWv5-JZc z$y0sFB~$st%#ga*82Wh;ob2 z??OK`-6IREsTn#-p%2SqWr!F!Z>Nuuj=j7X&k?jTfg>$BH(AmMxM2A%HXMRh&oJOy zU}1MsPYM=`gpzR~?Ke;# zv0+7DVktaj-lO`D9F#Ww0<6*F^|#$Geu*td)~c;RoykoS=tzqGY>LMt9r$!8%uDfD z93_znH`6HGTPz|{!6kydJXT~a8aBqmX9qtdReYe@A{a+GT}_(1rCZf_l}II_^OQ^< zH-d5^$PzyGcx`O)daL7<*1Lb9#((_!Ozs=IxR3gKABb{bJ=-ew0`k9 a_7U3X;nUyrwmXDk!0w#0b*rU+@_zvlO;hXu diff --git a/9router/public/providers/iflow.png b/9router/public/providers/iflow.png deleted file mode 100644 index 1bddeae625daf5f1a972f3ebe2a0fb736804dc91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10327 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 diff --git a/9router/public/providers/kilocode.png b/9router/public/providers/kilocode.png deleted file mode 100644 index b1272cfee4db1a99a3523541230a98b6a7f3527d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3334 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 - ); - } - - const availableModels = getAllAvailableModels(); - const hasActiveProviders = availableModels.length > 0; - - const renderToolCard = (toolId, tool) => { - const commonProps = { - tool, - isExpanded: expandedTool === toolId, - onToggle: () => setExpandedTool(expandedTool === toolId ? null : toolId), - baseUrl: getBaseUrl(), - apiKeys, - }; - - switch (toolId) { - case "claude": - return ( - handleModelMappingChange(toolId, alias, target)} - hasActiveProviders={hasActiveProviders} - cloudEnabled={cloudEnabled} - initialStatus={toolStatuses.claude} - /> - ); - case "codex": - return ; - case "opencode": - return ; - case "droid": - return ; - case "openclaw": - return ; - default: - return ; - } - }; - - const regularTools = Object.entries(CLI_TOOLS); - - return ( -
    - {!hasActiveProviders && ( - -
    - warning -
    -

    No active providers

    -

    Please add and connect providers first to configure CLI tools.

    -
    -
    -
    - )} -
    - {regularTools.map(([toolId, tool]) => renderToolCard(toolId, tool))} -
    -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/AntigravityToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/AntigravityToolCard.js deleted file mode 100644 index db6b1855..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/AntigravityToolCard.js +++ /dev/null @@ -1,478 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Card, Button, Badge, Modal, Input, ModelSelectModal } from "@/shared/components"; -import Image from "next/image"; - -export default function AntigravityToolCard({ - tool, - isExpanded, - onToggle, - baseUrl, - apiKeys, - activeProviders, - hasActiveProviders, - cloudEnabled, - initialStatus, -}) { - const [status, setStatus] = useState(initialStatus || null); - const [loading, setLoading] = useState(false); - const [startingStep, setStartingStep] = useState(null); // "cert" | "server" | "dns" | null - const [showPasswordModal, setShowPasswordModal] = useState(false); - const [sudoPassword, setSudoPassword] = useState(""); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [message, setMessage] = useState(null); - const [modelMappings, setModelMappings] = useState({}); - const [modalOpen, setModalOpen] = useState(false); - const [currentEditingAlias, setCurrentEditingAlias] = useState(null); - const [modelAliases, setModelAliases] = useState({}); - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - if (initialStatus) setStatus(initialStatus); - }, [initialStatus]); - - useEffect(() => { - if (isExpanded && !status) { - fetchStatus(); - loadSavedMappings(); - fetchModelAliases(); - } - if (isExpanded) { - loadSavedMappings(); - fetchModelAliases(); - } - }, [isExpanded]); - - const loadSavedMappings = async () => { - try { - const res = await fetch("/api/cli-tools/antigravity-mitm/alias?tool=antigravity"); - if (res.ok) { - const data = await res.json(); - const aliases = data.aliases || {}; - - if (Object.keys(aliases).length > 0) { - setModelMappings(aliases); - } - } - } catch (error) { - console.log("Error loading saved mappings:", error); - } - }; - - const fetchModelAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) setModelAliases(data.aliases || {}); - } catch (error) { - console.log("Error fetching model aliases:", error); - } - }; - - const fetchStatus = async () => { - try { - const res = await fetch("/api/cli-tools/antigravity-mitm"); - if (res.ok) { - const data = await res.json(); - setStatus(data); - } - } catch (error) { - console.log("Error fetching status:", error); - setStatus({ running: false }); - } - }; - - // Windows uses UAC dialog, no sudo needed - const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); - - const handleStart = () => { - if (isWindows || status?.hasCachedPassword) { - doStart(""); - } else { - setShowPasswordModal(true); - setMessage(null); - } - }; - - const handleStop = () => { - if (isWindows || status?.hasCachedPassword) { - doStop(""); - } else { - setShowPasswordModal(true); - setMessage(null); - } - }; - - const doStart = async (password) => { - setLoading(true); - setMessage(null); - // Show steps progressing in order - setStartingStep("cert"); - try { - const keyToUse = selectedApiKey?.trim() - || (apiKeys?.length > 0 ? apiKeys[0].key : null) - || (!cloudEnabled ? "sk_9router" : null); - - const res = await fetch("/api/cli-tools/antigravity-mitm", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ apiKey: keyToUse, sudoPassword: password }), - }); - - const data = await res.json(); - if (res.ok) { - setStartingStep(null); - setMessage({ type: "success", text: "MITM started" }); - setShowPasswordModal(false); - setSudoPassword(""); - fetchStatus(); - } else { - setStartingStep(null); - setMessage({ type: "error", text: data.error || "Failed to start" }); - } - } catch (error) { - setStartingStep(null); - setMessage({ type: "error", text: error.message }); - } finally { - setLoading(false); - } - }; - - const doStop = async (password) => { - setLoading(true); - setMessage(null); - try { - const res = await fetch("/api/cli-tools/antigravity-mitm", { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ sudoPassword: password }), - }); - - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "MITM stopped" }); - setShowPasswordModal(false); - setSudoPassword(""); - fetchStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to stop" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setLoading(false); - } - }; - - const handleConfirmPassword = () => { - if (!sudoPassword.trim()) { - setMessage({ type: "error", text: "Sudo password is required" }); - return; - } - if (status?.running) { - doStop(sudoPassword); - } else { - doStart(sudoPassword); - } - }; - - const openModelSelector = (alias) => { - setCurrentEditingAlias(alias); - setModalOpen(true); - }; - - const handleModelSelect = (model) => { - if (currentEditingAlias) { - setModelMappings(prev => ({ - ...prev, - [currentEditingAlias]: model.value, - })); - } - }; - - const handleModelMappingChange = (alias, value) => { - setModelMappings(prev => ({ - ...prev, - [alias]: value, - })); - }; - - const handleSaveMappings = async () => { - setLoading(true); - setMessage(null); - - try { - const res = await fetch("/api/cli-tools/antigravity-mitm/alias", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "antigravity", mappings: modelMappings }), - }); - - if (!res.ok) { - const data = await res.json(); - throw new Error(data.error || "Failed to save mappings"); - } - - setMessage({ type: "success", text: "Mappings saved!" }); - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setLoading(false); - } - }; - - const isRunning = status?.running; - - return ( - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} - /> -
    -
    -
    -

    {tool.name}

    - {isRunning ? ( - Active - ) : ( - Inactive - )} -
    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {/* Status indicators — ordered: Cert → Server → DNS */} -
    - {[ - { key: "cert", label: "Cert", ok: status?.certExists }, - { key: "server", label: "Server", ok: status?.running }, - { key: "dns", label: "DNS", ok: status?.dnsConfigured }, - ].map(({ key, label, ok }, i) => { - const isLoading = startingStep === key; - return ( -
    -
    - {isLoading ? ( - progress_activity - ) : ( - - {ok ? "check_circle" : "radio_button_unchecked"} - - )} - - {label} - -
    - {i < 2 && arrow_forward} -
    - ); - })} -
    - - {/* Start/Stop Button */} -
    - {isRunning ? ( - - ) : ( - - )} -
    - - {message?.type === "error" && ( -
    - error - {message.text} -
    - )} - - {/* When running: API Key + Model Mappings */} - {isRunning && ( - <> -
    - API Key - arrow_forward - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} - - )} -
    - - {tool.defaultModels.map((model) => ( -
    - {model.name} - arrow_forward - handleModelMappingChange(model.alias, e.target.value)} - placeholder="provider/model-id" - className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - - {modelMappings[model.alias] && ( - - )} -
    - ))} - -
    - -
    - - )} - - {/* Windows admin warning */} - {!isRunning && isWindows && ( -
    - warning - Windows: Run terminal (9Router) as Administrator to enable MITM -
    - )} - - {/* When stopped: how it works */} - {!isRunning && ( -
    -

    - How it works: Intercepts Antigravity traffic via DNS redirect, letting you reroute models through 9Router. -

    -
    - 1. Generates SSL cert & adds to system keychain - 2. Redirects daily-cloudcode-pa.googleapis.com → localhost - 3. Maps Antigravity models to any provider via 9Router -
    -
    - )} -
    - )} - - {/* Password Modal */} - { - setShowPasswordModal(false); - setSudoPassword(""); - setMessage(null); - }} - title="Sudo Password Required" - size="sm" - > -
    -
    - warning -

    Required for SSL certificate and DNS configuration

    -
    - - setSudoPassword(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !loading) handleConfirmPassword(); - }} - /> - - {message && ( -
    - {message.type === "success" ? "check_circle" : "error"} - {message.text} -
    - )} - -
    - - -
    -
    -
    - - {/* Model Select Modal */} - setModalOpen(false)} - onSelect={handleModelSelect} - selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} - activeProviders={activeProviders} - modelAliases={modelAliases} - title={`Select model for ${currentEditingAlias}`} - /> -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js deleted file mode 100644 index e518b9b7..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +++ /dev/null @@ -1,388 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from "react"; -import { Card, Button, ModelSelectModal, ManualConfigModal, Tooltip } from "@/shared/components"; -import Image from "next/image"; - -const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; - -export default function ClaudeToolCard({ - tool, - isExpanded, - onToggle, - activeProviders, - modelMappings, - onModelMappingChange, - baseUrl, - hasActiveProviders, - apiKeys, - cloudEnabled, - initialStatus, -}) { - const [claudeStatus, setClaudeStatus] = useState(initialStatus || null); - const [checkingClaude, setCheckingClaude] = useState(false); - const [applying, setApplying] = useState(false); - const [restoring, setRestoring] = useState(false); - const [message, setMessage] = useState(null); - const [showInstallGuide, setShowInstallGuide] = useState(false); - const [modalOpen, setModalOpen] = useState(false); - const [currentEditingAlias, setCurrentEditingAlias] = useState(null); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [modelAliases, setModelAliases] = useState({}); - const [showManualConfigModal, setShowManualConfigModal] = useState(false); - const [customBaseUrl, setCustomBaseUrl] = useState(""); - const [ccFilterNaming, setCcFilterNaming] = useState(false); - const hasInitializedModels = useRef(false); - - const getConfigStatus = () => { - if (!claudeStatus?.installed) return null; - const currentUrl = claudeStatus.settings?.env?.ANTHROPIC_BASE_URL; - if (!currentUrl) return "not_configured"; - const localMatch = currentUrl.includes("localhost") || currentUrl.includes("127.0.0.1"); - const cloudMatch = cloudEnabled && CLOUD_URL && currentUrl.startsWith(CLOUD_URL); - const tunnelMatch = baseUrl && currentUrl.startsWith(baseUrl); - if (localMatch || cloudMatch || tunnelMatch) return "configured"; - return "other"; - }; - - const configStatus = getConfigStatus(); - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - if (initialStatus) setClaudeStatus(initialStatus); - }, [initialStatus]); - - useEffect(() => { - if (isExpanded && !claudeStatus) { - checkClaudeStatus(); - fetchModelAliases(); - } - if (isExpanded) fetchModelAliases(); - }, [isExpanded]); - - useEffect(() => { - fetch("/api/settings").then(r => r.json()).then(data => { - setCcFilterNaming(!!data.ccFilterNaming); - }).catch(() => {}); - }, []); - - const handleCcFilterNamingToggle = async (e) => { - const value = e.target.checked; - setCcFilterNaming(value); - await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ccFilterNaming: value }), - }).catch(() => {}); - }; - - const fetchModelAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) setModelAliases(data.aliases || {}); - } catch (error) { - console.log("Error fetching model aliases:", error); - } - }; - - useEffect(() => { - if (claudeStatus?.installed && !hasInitializedModels.current) { - hasInitializedModels.current = true; - const env = claudeStatus.settings?.env || {}; - - tool.defaultModels.forEach((model) => { - if (model.envKey) { - const value = env[model.envKey] || model.defaultValue || ""; - // Only sync initial values from file once - if (value) { - onModelMappingChange(model.alias, value); - } - } - }); - // Only set selectedApiKey if it exists in apiKeys list - const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN; - if (tokenFromFile && apiKeys?.some(k => k.key === tokenFromFile)) { - setSelectedApiKey(tokenFromFile); - } - } - }, [claudeStatus, apiKeys, tool.defaultModels, onModelMappingChange]); - - const checkClaudeStatus = async () => { - setCheckingClaude(true); - try { - const res = await fetch("/api/cli-tools/claude-settings"); - const data = await res.json(); - setClaudeStatus(data); - } catch (error) { - setClaudeStatus({ installed: false, error: error.message }); - } finally { - setCheckingClaude(false); - } - }; - - const getEffectiveBaseUrl = () => { - const url = customBaseUrl || baseUrl; - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const getDisplayUrl = () => { - const url = customBaseUrl || baseUrl; - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const handleApplySettings = async () => { - setApplying(true); - setMessage(null); - try { - const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() }; - - // Get key from dropdown, fallback to first key or sk_9router for localhost - const keyToUse = selectedApiKey?.trim() - || (apiKeys?.length > 0 ? apiKeys[0].key : null) - || (!cloudEnabled ? "sk_9router" : null); - - if (keyToUse) { - env.ANTHROPIC_AUTH_TOKEN = keyToUse; - } - - tool.defaultModels.forEach((model) => { - const targetModel = modelMappings[model.alias]; - if (targetModel && model.envKey) env[model.envKey] = targetModel; - }); - const res = await fetch("/api/cli-tools/claude-settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ env }), - }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings applied successfully!" }); - setClaudeStatus(prev => ({ ...prev, hasBackup: true, settings: { ...prev?.settings, env } })); - } else { - setMessage({ type: "error", text: data.error || "Failed to apply settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setApplying(false); - } - }; - - const handleResetSettings = async () => { - setRestoring(true); - setMessage(null); - try { - const res = await fetch("/api/cli-tools/claude-settings", { method: "DELETE" }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings reset successfully!" }); - tool.defaultModels.forEach((model) => onModelMappingChange(model.alias, model.defaultValue || "")); - setSelectedApiKey(""); - } else { - setMessage({ type: "error", text: data.error || "Failed to reset settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setRestoring(false); - } - }; - - const openModelSelector = (alias) => { - setCurrentEditingAlias(alias); - setModalOpen(true); - }; - - const handleModelSelect = (model) => { - if (currentEditingAlias) onModelMappingChange(currentEditingAlias, model.value); - }; - - // Generate settings.json content for manual copy - const getManualConfigs = () => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : ""); - const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl(), ANTHROPIC_AUTH_TOKEN: keyToUse }; - tool.defaultModels.forEach((model) => { - const targetModel = modelMappings[model.alias]; - if (targetModel && model.envKey) env[model.envKey] = targetModel; - }); - - return [ - { - filename: "~/.claude/settings.json", - content: JSON.stringify({ env }, null, 2), - }, - ]; - }; - - return ( - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} /> -
    -
    -
    -

    {tool.name}

    - {configStatus === "configured" && Connected} - {configStatus === "not_configured" && Not configured} - {configStatus === "other" && Other} -
    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {checkingClaude && ( -
    - progress_activity - Checking Claude CLI... -
    - )} - - {!checkingClaude && claudeStatus && !claudeStatus.installed && ( -
    -
    - warning -
    -

    Claude CLI not installed

    -

    Please install Claude CLI to use this feature.

    -
    - -
    - {showInstallGuide && ( -
    -

    Installation Guide

    -
    -
    -

    macOS / Linux / Windows:

    - npm install -g @anthropic-ai/claude-code -
    -

    After installation, run claude to verify.

    -
    -
    - )} -
    - )} - - {!checkingClaude && claudeStatus?.installed && ( - <> -
    - {/* Current Base URL */} - {claudeStatus?.settings?.env?.ANTHROPIC_BASE_URL && ( -
    - Current - arrow_forward - - {claudeStatus.settings.env.ANTHROPIC_BASE_URL} - -
    - )} - - {/* Base URL */} -
    - Base URL - arrow_forward - setCustomBaseUrl(e.target.value)} - placeholder="https://.../v1" - className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - {customBaseUrl && customBaseUrl !== baseUrl && ( - - )} -
    - - {/* API Key */} -
    - API Key - arrow_forward - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} - - )} -
    - - {/* Model Mappings */} - {tool.defaultModels.map((model) => ( -
    - {model.name} - arrow_forward - onModelMappingChange(model.alias, e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> - - {modelMappings[model.alias] && } -
    - ))} - - {/* CC Filter Naming */} -
    - Filter naming - arrow_forward - - - info - -
    -
    - - {message && ( -
    - {message.type === "success" ? "check_circle" : "error"} - {message.text} -
    - )} - -
    - - - -
    - - )} -
    - )} - - setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} /> - - setShowManualConfigModal(false)} - title="Claude CLI - Manual Configuration" - configs={getManualConfigs()} - /> -
    - ); -} - diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js deleted file mode 100644 index edfe98c2..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js +++ /dev/null @@ -1,336 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; -import Image from "next/image"; - -export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) { - const [codexStatus, setCodexStatus] = useState(initialStatus || null); - const [checkingCodex, setCheckingCodex] = useState(false); - const [applying, setApplying] = useState(false); - const [restoring, setRestoring] = useState(false); - const [message, setMessage] = useState(null); - const [showInstallGuide, setShowInstallGuide] = useState(false); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); - const [modalOpen, setModalOpen] = useState(false); - const [modelAliases, setModelAliases] = useState({}); - const [showManualConfigModal, setShowManualConfigModal] = useState(false); - const [customBaseUrl, setCustomBaseUrl] = useState(""); - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - if (initialStatus) setCodexStatus(initialStatus); - }, [initialStatus]); - - useEffect(() => { - if (isExpanded && !codexStatus) { - checkCodexStatus(); - fetchModelAliases(); - } - if (isExpanded) fetchModelAliases(); - }, [isExpanded]); - - const fetchModelAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) setModelAliases(data.aliases || {}); - } catch (error) { - console.log("Error fetching model aliases:", error); - } - }; - - // Parse model from config content (don't sync URL - always use baseUrl from props) - useEffect(() => { - if (codexStatus?.config) { - const modelMatch = codexStatus.config.match(/^model\s*=\s*"([^"]+)"/m); - if (modelMatch) setSelectedModel(modelMatch[1]); - } - }, [codexStatus]); - - const getConfigStatus = () => { - if (!codexStatus?.installed) return null; - if (!codexStatus.config) return "not_configured"; - const hasBaseUrl = codexStatus.config.includes(baseUrl) || codexStatus.config.includes("localhost") || codexStatus.config.includes("127.0.0.1"); - return hasBaseUrl ? "configured" : "other"; - }; - - const configStatus = getConfigStatus(); - - const getEffectiveBaseUrl = () => { - const url = customBaseUrl || `${baseUrl}/v1`; - // Ensure URL ends with /v1 - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`; - - const checkCodexStatus = async () => { - setCheckingCodex(true); - try { - const res = await fetch("/api/cli-tools/codex-settings"); - const data = await res.json(); - setCodexStatus(data); - } catch (error) { - setCodexStatus({ installed: false, error: error.message }); - } finally { - setCheckingCodex(false); - } - }; - - const handleApplySettings = async () => { - setApplying(true); - setMessage(null); - try { - // Use sk_9router for localhost if no key, otherwise use selected key - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : selectedApiKey); - - const res = await fetch("/api/cli-tools/codex-settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, model: selectedModel }), - }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings applied successfully!" }); - checkCodexStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to apply settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setApplying(false); - } - }; - - const handleResetSettings = async () => { - setRestoring(true); - setMessage(null); - try { - const res = await fetch("/api/cli-tools/codex-settings", { method: "DELETE" }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings reset successfully!" }); - setSelectedModel(""); - checkCodexStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to reset settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setRestoring(false); - } - }; - - const handleModelSelect = (model) => { - setSelectedModel(model.value); - setModalOpen(false); - }; - - const getManualConfigs = () => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : ""); - - const configContent = `# 9Router Configuration for Codex CLI -model = "${selectedModel}" -model_provider = "9router" - -[model_providers.9router] -name = "9Router" -base_url = "${getEffectiveBaseUrl()}" -wire_api = "responses" -`; - - const authContent = JSON.stringify({ - OPENAI_API_KEY: keyToUse - }, null, 2); - - return [ - { - filename: "~/.codex/config.toml", - content: configContent, - }, - { - filename: "~/.codex/auth.json", - content: authContent, - }, - ]; - }; - - return ( - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} /> -
    -
    -
    -

    {tool.name}

    - {configStatus === "configured" && Connected} - {configStatus === "not_configured" && Not configured} - {configStatus === "other" && Other} -
    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {checkingCodex && ( -
    - progress_activity - Checking Codex CLI... -
    - )} - - {!checkingCodex && codexStatus && !codexStatus.installed && ( -
    -
    - warning -
    -

    Codex CLI not installed

    -

    Please install Codex CLI to use auto-apply feature.

    -
    - -
    - {showInstallGuide && ( -
    -

    Installation Guide

    -
    -
    -

    macOS / Linux / Windows:

    - npm install -g @openai/codex -
    -

    After installation, run codex to verify.

    -
    -

    - Codex uses ~/.codex/auth.json with OPENAI_API_KEY. - Click "Apply" to auto-configure. -

    -
    -
    -
    - )} -
    - )} - - {!checkingCodex && codexStatus?.installed && ( - <> -
    - {/* Current Base URL */} - {codexStatus?.config && (() => { - const parsed = codexStatus.config.match(/base_url\s*=\s*"([^"]+)"/); - const currentBaseUrl = parsed ? parsed[1] : null; - return currentBaseUrl ? ( -
    - Current - arrow_forward - - {currentBaseUrl} - -
    - ) : null; - })()} - - {/* Base URL */} -
    - Base URL - arrow_forward - setCustomBaseUrl(e.target.value)} - placeholder="https://.../v1" - className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - {customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && ( - - )} -
    - - {/* API Key */} -
    - API Key - arrow_forward - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} - - )} -
    - - {/* Model */} -
    - Model - arrow_forward - setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> - - {selectedModel && } -
    -
    - - {message && ( -
    - {message.type === "success" ? "check_circle" : "error"} - {message.text} -
    - )} - -
    - - - -
    - - )} -
    - )} - - setModalOpen(false)} - onSelect={handleModelSelect} - selectedModel={selectedModel} - activeProviders={activeProviders} - modelAliases={modelAliases} - title="Select Model for Codex" - /> - - setShowManualConfigModal(false)} - title="Codex CLI - Manual Configuration" - configs={getManualConfigs()} - /> -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js deleted file mode 100644 index 8e8f0d38..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js +++ /dev/null @@ -1,295 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; -import Image from "next/image"; - -export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) { - const [status, setStatus] = useState(initialStatus || null); - const [checking, setChecking] = useState(false); - const [applying, setApplying] = useState(false); - const [restoring, setRestoring] = useState(false); - const [message, setMessage] = useState(null); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [modelAliases, setModelAliases] = useState({}); - const [showManualConfigModal, setShowManualConfigModal] = useState(false); - - // Model list management - const [modelInput, setModelInput] = useState(""); - const [modelList, setModelList] = useState([]); - const [modalOpen, setModalOpen] = useState(false); - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - if (initialStatus) setStatus(initialStatus); - }, [initialStatus]); - - useEffect(() => { - if (isExpanded && !status) { - checkStatus(); - fetchModelAliases(); - } - if (isExpanded) fetchModelAliases(); - }, [isExpanded]); - - // Pre-fill model list from existing config - useEffect(() => { - if (status?.config && Array.isArray(status.config) && modelList.length === 0) { - const entry = status.config.find((e) => e.name === "9Router"); - if (entry?.models?.length > 0) { - setModelList(entry.models.map((m) => m.id)); - } - } - }, [status]); - - const fetchModelAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) setModelAliases(data.aliases || {}); - } catch (error) { - console.log("Error fetching model aliases:", error); - } - }; - - const getConfigStatus = () => { - if (!status) return null; - if (!status.has9Router) return "not_configured"; - const url = status.currentUrl || ""; - return url.includes("localhost") || url.includes("127.0.0.1") || url.includes(baseUrl) - ? "configured" : "other"; - }; - - const configStatus = getConfigStatus(); - const getEffectiveBaseUrl = () => baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`; - - const addModel = () => { - const val = modelInput.trim(); - if (!val || modelList.includes(val)) return; - setModelList((prev) => [...prev, val]); - setModelInput(""); - }; - - const removeModel = (id) => setModelList((prev) => prev.filter((m) => m !== id)); - - const checkStatus = async () => { - setChecking(true); - try { - const res = await fetch("/api/cli-tools/copilot-settings"); - const data = await res.json(); - setStatus(data); - } catch (error) { - setStatus({ error: error.message }); - } finally { - setChecking(false); - } - }; - - const handleApply = async () => { - setApplying(true); - setMessage(null); - try { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : selectedApiKey); - - const res = await fetch("/api/cli-tools/copilot-settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, models: modelList }), - }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: data.message || "Settings applied successfully!" }); - checkStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to apply settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setApplying(false); - } - }; - - const handleReset = async () => { - setRestoring(true); - setMessage(null); - try { - const res = await fetch("/api/cli-tools/copilot-settings", { method: "DELETE" }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings reset successfully!" }); - setModelList([]); - checkStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to reset settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setRestoring(false); - } - }; - - const getManualConfigs = () => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : ""); - const effectiveBaseUrl = getEffectiveBaseUrl(); - - return [{ - filename: "~/Library/Application Support/Code/User/chatLanguageModels.json", - content: JSON.stringify([{ - name: "9Router", - vendor: "azure", - apiKey: keyToUse, - models: modelList.map((id) => ({ - id, name: id, - url: `${effectiveBaseUrl}/chat/completions#models.ai.azure.com`, - toolCalling: true, vision: false, - maxInputTokens: 128000, maxOutputTokens: 16000, - })), - }], null, 2), - }]; - }; - - return ( - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} /> -
    -
    -
    -

    {tool.name}

    - {configStatus === "configured" && Connected} - {configStatus === "not_configured" && Not configured} - {configStatus === "other" && Other} -
    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {checking && ( -
    - progress_activity - Checking Copilot config... -
    - )} - - {!checking && ( - <> - {/* Info */} -
    - info -
    -

    Writes to chatLanguageModels.json

    -

    Reload VS Code after applying for changes to take effect.

    -
    -
    - -
    - {/* API Key */} -
    - - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} - - )} -
    - - {/* Model input + Add */} -
    - - - {/* Model list */} - {modelList.length > 0 && ( -
    - {modelList.map((id) => ( -
    - {id} - -
    - ))} -
    - )} - -
    - setModelInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && addModel()} - placeholder="provider/model-id" - className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - - -
    -
    -
    - - {message && ( -
    - {message.type === "success" ? "check_circle" : "error"} - {message.text} -
    - )} - -
    - - - -
    - - )} -
    - )} - - setModalOpen(false)} - onSelect={(model) => { setModelInput(model.value); setModalOpen(false); }} - selectedModel={modelInput} - activeProviders={activeProviders} - modelAliases={modelAliases} - title="Select Model for GitHub Copilot" - /> - - setShowManualConfigModal(false)} - title="GitHub Copilot - Manual Configuration" - configs={getManualConfigs()} - /> -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js deleted file mode 100644 index 5f00e3f0..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js +++ /dev/null @@ -1,293 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Card, ModelSelectModal } from "@/shared/components"; -import Image from "next/image"; - -export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders = [], cloudEnabled = false, tunnelEnabled = false }) { - const [copiedField, setCopiedField] = useState(null); - const [showModelModal, setShowModelModal] = useState(false); - const [modelValue, setModelValue] = useState(""); - - // Initialize state directly with computed value - no need for useEffect - const [selectedApiKey, setSelectedApiKey] = useState(() => - apiKeys?.length > 0 ? apiKeys[0].key : "" - ); - - const replaceVars = (text) => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : "your-api-key"); - - // Add /v1 suffix only if not already present (DRY - avoid duplicate) - const normalizedBaseUrl = baseUrl || "http://localhost:20128"; - const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1") - ? normalizedBaseUrl - : `${normalizedBaseUrl}/v1`; - - return text - .replace(/\{\{baseUrl\}\}/g, baseUrlWithV1) - .replace(/\{\{apiKey\}\}/g, keyToUse) - .replace(/\{\{model\}\}/g, modelValue || "provider/model-id"); - }; - - const handleCopy = async (text, field) => { - await navigator.clipboard.writeText(replaceVars(text)); - setCopiedField(field); - setTimeout(() => setCopiedField(null), 2000); - }; - - const handleSelectModel = (model) => { - setModelValue(model.value); - }; - - const hasActiveProviders = activeProviders.length > 0; - - const renderApiKeySelector = () => { - return ( -
    - {apiKeys && apiKeys.length > 0 ? ( - <> - - - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router"} - - )} -
    - ); - }; - - const renderModelSelector = () => { - return ( -
    - setModelValue(e.target.value)} - placeholder="provider/model-id" - className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - - {modelValue && ( - <> - - - - )} -
    - ); - }; - - const renderNotes = () => { - if (!tool.notes || tool.notes.length === 0) return null; - - return ( -
    - {tool.notes.map((note, index) => { - // Skip cloudCheck note if tunnel or cloud is enabled - if (note.type === "cloudCheck" && (cloudEnabled || tunnelEnabled)) return null; - - const isWarning = note.type === "warning"; - const isError = note.type === "cloudCheck" && !cloudEnabled && !tunnelEnabled; - - let bgClass = "bg-blue-500/10 border-blue-500/30"; - let textClass = "text-blue-600 dark:text-blue-400"; - let iconClass = "text-blue-500"; - let icon = "info"; - - if (isWarning) { - bgClass = "bg-yellow-500/10 border-yellow-500/30"; - textClass = "text-yellow-600 dark:text-yellow-400"; - iconClass = "text-yellow-500"; - icon = "warning"; - } else if (isError) { - bgClass = "bg-red-500/10 border-red-500/30"; - textClass = "text-red-600 dark:text-red-400"; - iconClass = "text-red-500"; - icon = "error"; - } - - return ( -
    - {icon} -

    {note.text}

    -
    - ); - })} -
    - ); - }; - - const canShowGuide = () => { - if (tool.requiresExternalUrl && !cloudEnabled && !tunnelEnabled) return false; - if (tool.requiresCloud && !cloudEnabled) return false; - return true; - }; - - const renderGuideSteps = () => { - if (!tool.guideSteps) return

    Coming soon...

    ; - - return ( -
    - {renderNotes()} - {canShowGuide() && tool.guideSteps.map((item) => ( -
    -
    - {item.step} -
    -
    -

    {item.title}

    - {item.desc &&

    {item.desc}

    } - {item.type === "apiKeySelector" && renderApiKeySelector()} - {item.type === "modelSelector" && renderModelSelector()} - {item.value && ( -
    - - {replaceVars(item.value)} - - {item.copyable && ( - - )} -
    - )} -
    -
    - ))} - - {canShowGuide() && tool.codeBlock && ( -
    -
    - {tool.codeBlock.language} - -
    -
    -              {replaceVars(tool.codeBlock.code)}
    -            
    -
    - )} -
    - ); - }; - - const renderIcon = () => { - if (tool.image) { - return ( - {tool.name} { e.target.style.display = "none"; }} - /> - ); - } - if (tool.icon) { - return {tool.icon}; - } - return ( - {tool.name} { e.target.style.display = "none"; }} - /> - ); - }; - - return ( - -
    -
    -
    - {renderIcon()} -
    -
    -

    {tool.name}

    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {renderGuideSteps()} -
    - )} - - setShowModelModal(false)} - onSelect={handleSelectModel} - selectedModel={modelValue} - activeProviders={activeProviders} - title="Select Model" - /> -
    - ); -} - diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js deleted file mode 100644 index 6282ba2c..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js +++ /dev/null @@ -1,339 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from "react"; -import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; -import Image from "next/image"; - -const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; - -export default function DroidToolCard({ - tool, - isExpanded, - onToggle, - baseUrl, - hasActiveProviders, - apiKeys, - activeProviders, - cloudEnabled, - initialStatus, -}) { - const [droidStatus, setDroidStatus] = useState(initialStatus || null); - const [checkingDroid, setCheckingDroid] = useState(false); - const [applying, setApplying] = useState(false); - const [restoring, setRestoring] = useState(false); - const [message, setMessage] = useState(null); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); - const [modalOpen, setModalOpen] = useState(false); - const [modelAliases, setModelAliases] = useState({}); - const [showManualConfigModal, setShowManualConfigModal] = useState(false); - const [customBaseUrl, setCustomBaseUrl] = useState(""); - const hasInitializedModel = useRef(false); - - const getConfigStatus = () => { - if (!droidStatus?.installed) return null; - const currentConfig = droidStatus.settings?.customModels?.find(m => m.id === "custom:9Router-0"); - if (!currentConfig) return "not_configured"; - const localMatch = currentConfig.baseUrl?.includes("localhost") || currentConfig.baseUrl?.includes("127.0.0.1"); - const cloudMatch = cloudEnabled && CLOUD_URL && currentConfig.baseUrl?.startsWith(CLOUD_URL); - const tunnelMatch = baseUrl && currentConfig.baseUrl?.startsWith(baseUrl); - if (localMatch || cloudMatch || tunnelMatch) return "configured"; - return "other"; - }; - - const configStatus = getConfigStatus(); - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - if (initialStatus) setDroidStatus(initialStatus); - }, [initialStatus]); - - useEffect(() => { - if (isExpanded && !droidStatus) { - checkDroidStatus(); - fetchModelAliases(); - } - if (isExpanded) fetchModelAliases(); - }, [isExpanded]); - - const fetchModelAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) setModelAliases(data.aliases || {}); - } catch (error) { - console.log("Error fetching model aliases:", error); - } - }; - - useEffect(() => { - if (droidStatus?.installed && !hasInitializedModel.current) { - hasInitializedModel.current = true; - const customModel = droidStatus.settings?.customModels?.find(m => m.id === "custom:9Router-0"); - if (customModel) { - if (customModel.model) setSelectedModel(customModel.model); - if (customModel.apiKey && apiKeys?.some(k => k.key === customModel.apiKey)) { - setSelectedApiKey(customModel.apiKey); - } - } - } - }, [droidStatus, apiKeys]); - - const checkDroidStatus = async () => { - setCheckingDroid(true); - try { - const res = await fetch("/api/cli-tools/droid-settings"); - const data = await res.json(); - setDroidStatus(data); - } catch (error) { - setDroidStatus({ installed: false, error: error.message }); - } finally { - setCheckingDroid(false); - } - }; - - const getEffectiveBaseUrl = () => { - const url = customBaseUrl || baseUrl; - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const getDisplayUrl = () => { - const url = customBaseUrl || baseUrl; - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const handleApplySettings = async () => { - setApplying(true); - setMessage(null); - try { - const keyToUse = selectedApiKey?.trim() - || (apiKeys?.length > 0 ? apiKeys[0].key : null) - || (!cloudEnabled ? "sk_9router" : null); - - const res = await fetch("/api/cli-tools/droid-settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: getEffectiveBaseUrl(), - apiKey: keyToUse, - model: selectedModel - }), - }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings applied successfully!" }); - checkDroidStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to apply settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setApplying(false); - } - }; - - const handleResetSettings = async () => { - setRestoring(true); - setMessage(null); - try { - const res = await fetch("/api/cli-tools/droid-settings", { method: "DELETE" }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings reset successfully!" }); - setSelectedModel(""); - setSelectedApiKey(""); - checkDroidStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to reset settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setRestoring(false); - } - }; - - const handleModelSelect = (model) => { - setSelectedModel(model.value); - setModalOpen(false); - }; - - const getManualConfigs = () => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : ""); - - const settingsContent = { - customModels: [ - { - model: selectedModel || "provider/model-id", - id: "custom:9Router-0", - index: 0, - baseUrl: getEffectiveBaseUrl(), - apiKey: keyToUse, - displayName: selectedModel || "provider/model-id", - maxOutputTokens: 131072, - noImageSupport: false, - provider: "openai", - }, - ], - }; - - const platform = typeof navigator !== "undefined" && navigator.platform; - const isWindows = platform?.toLowerCase().includes("win"); - const settingsPath = isWindows - ? "%USERPROFILE%\\.factory\\settings.json" - : "~/.factory/settings.json"; - - return [ - { - filename: settingsPath, - content: JSON.stringify(settingsContent, null, 2), - }, - ]; - }; - - return ( - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} /> -
    -
    -
    -

    {tool.name}

    - {configStatus === "configured" && Connected} - {configStatus === "not_configured" && Not configured} - {configStatus === "other" && Other} -
    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {checkingDroid && ( -
    - progress_activity - Checking Factory Droid CLI... -
    - )} - - {!checkingDroid && droidStatus && !droidStatus.installed && ( -
    - warning -
    -

    Factory Droid CLI not installed

    -

    Please install Factory Droid CLI to use this feature.

    -
    -
    - )} - - {!checkingDroid && droidStatus?.installed && ( - <> -
    - {/* Current Base URL */} - {droidStatus?.settings?.customModels?.find(m => m.id === "custom:9Router-0")?.baseUrl && ( -
    - Current - arrow_forward - - {droidStatus.settings.customModels.find(m => m.id === "custom:9Router-0").baseUrl} - -
    - )} - - {/* Base URL */} -
    - Base URL - arrow_forward - setCustomBaseUrl(e.target.value)} - placeholder="https://.../v1" - className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - {customBaseUrl && customBaseUrl !== baseUrl && ( - - )} -
    - - {/* API Key */} -
    - API Key - arrow_forward - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} - - )} -
    - - {/* Model */} -
    - Model - arrow_forward - setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> - - {selectedModel && } -
    -
    - - {message && ( -
    - {message.type === "success" ? "check_circle" : "error"} - {message.text} -
    - )} - -
    - - - -
    - - )} -
    - )} - - setModalOpen(false)} - onSelect={handleModelSelect} - selectedModel={selectedModel} - activeProviders={activeProviders} - modelAliases={modelAliases} - title="Select Model for Factory Droid" - /> - - setShowManualConfigModal(false)} - title="Factory Droid - Manual Configuration" - configs={getManualConfigs()} - /> -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js deleted file mode 100644 index 466fe0c8..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +++ /dev/null @@ -1,241 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Card, Button, Badge, Input } from "@/shared/components"; - -/** - * Shared MITM infrastructure card — manages SSL cert + server start/stop. - * DNS per-tool is handled separately in MitmToolCard. - */ -export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }) { - const [status, setStatus] = useState(null); - const [loading, setLoading] = useState(false); - const [showPasswordModal, setShowPasswordModal] = useState(false); - const [sudoPassword, setSudoPassword] = useState(""); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [pendingAction, setPendingAction] = useState(null); - const [modalError, setModalError] = useState(null); - - const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); - const isAdmin = status?.isAdmin !== false; - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - fetchStatus(); - }, []); - - const fetchStatus = async () => { - try { - const res = await fetch("/api/cli-tools/antigravity-mitm"); - if (res.ok) { - const data = await res.json(); - setStatus(data); - onStatusChange?.(data); - } - } catch { - setStatus({ running: false, certExists: false, dnsStatus: {} }); - } - }; - - const handleAction = (action) => { - if (isWindows || status?.hasCachedPassword) { - doAction(action, ""); - } else { - setPendingAction(action); - setShowPasswordModal(true); - setModalError(null); - } - }; - - const doAction = async (action, password) => { - setLoading(true); - try { - if (action === "trust-cert") { - await fetch("/api/cli-tools/antigravity-mitm", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "trust-cert", sudoPassword: password }), - }); - } else if (action === "start") { - const keyToUse = selectedApiKey?.trim() - || (apiKeys?.length > 0 ? apiKeys[0].key : null) - || (!cloudEnabled ? "sk_9router" : null); - await fetch("/api/cli-tools/antigravity-mitm", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ apiKey: keyToUse, sudoPassword: password }), - }); - } else { - await fetch("/api/cli-tools/antigravity-mitm", { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ sudoPassword: password }), - }); - } - setShowPasswordModal(false); - setSudoPassword(""); - await fetchStatus(); - } catch { /* ignore */ } finally { - setLoading(false); - setPendingAction(null); - } - }; - - const handleConfirmPassword = () => { - if (!sudoPassword.trim()) { - setModalError("Sudo password is required"); - return; - } - doAction(pendingAction, sudoPassword); - }; - - const isRunning = status?.running; - - return ( - <> - -
    - {/* Header */} -
    -
    - security - MITM Server - {isRunning ? ( - Running - ) : ( - Stopped - )} -
    -
    - {[ - { label: "Cert", ok: status?.certExists }, - { label: "Trusted", ok: status?.certTrusted }, - { label: "Server", ok: isRunning }, - ].map(({ label, ok }) => ( - - - {ok ? "check_circle" : "cancel"} - - {label} - - ))} -
    -
    - - {/* Purpose & How it works */} -
    -

    - Purpose: Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router -

    -

    - How it works: Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot -

    -
    - - {/* API Key selector (only when stopped) */} - {!isRunning && ( -
    - API Key - {apiKeys?.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys — create one in Keys page" : "sk_9router (default)"} - - )} -
    - )} - - {/* Action buttons */} -
    - {status?.certExists && !status?.certTrusted && ( - - )} - {isRunning ? ( - - ) : ( - - )} - {isRunning && ( -

    Enable DNS per tool below to activate interception

    - )} -
    - - {/* Windows admin warning */} - {isWindows && !isAdmin && ( -
    - shield_lock - Administrator required — restart 9Router as Administrator to use MITM -
    - )} -
    -
    - - {/* Password Modal */} - {showPasswordModal && ( -
    -
    -

    Sudo Password Required

    -
    - warning -

    Required for SSL certificate and server startup

    -
    - setSudoPassword(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter" && !loading) handleConfirmPassword(); }} - /> - {modalError && ( -
    - error - {modalError} -
    - )} -
    - - -
    -
    -
    - )} - - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js deleted file mode 100644 index f6b18f46..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js +++ /dev/null @@ -1,299 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { Card, Button, Badge, Input, ModelSelectModal } from "@/shared/components"; -import Image from "next/image"; - -/** - * Per-tool MITM card — shows DNS status + model mappings. - * - Auto-saves model mapping on blur or modal select - * - Skips sudo modal if password is already cached - * - Model mappings can only be edited when DNS is active - */ -export default function MitmToolCard({ - tool, - isExpanded, - onToggle, - serverRunning, - dnsActive, - hasCachedPassword, - apiKeys, - activeProviders, - hasActiveProviders, - modelAliases = {}, - cloudEnabled, - onDnsChange, -}) { - const [loading, setLoading] = useState(false); - const [warning, setWarning] = useState(null); - const [showPasswordModal, setShowPasswordModal] = useState(false); - const [sudoPassword, setSudoPassword] = useState(""); - const [pendingDnsAction, setPendingDnsAction] = useState(null); - const [modalError, setModalError] = useState(null); - const [modelMappings, setModelMappings] = useState({}); - const [modalOpen, setModalOpen] = useState(false); - const [currentEditingAlias, setCurrentEditingAlias] = useState(null); - - const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); - - useEffect(() => { - if (isExpanded) loadSavedMappings(); - }, [isExpanded]); - - const loadSavedMappings = async () => { - try { - const res = await fetch(`/api/cli-tools/antigravity-mitm/alias?tool=${tool.id}`); - if (res.ok) { - const data = await res.json(); - if (Object.keys(data.aliases || {}).length > 0) setModelMappings(data.aliases); - } - } catch { /* ignore */ } - }; - - const saveMappings = useCallback(async (mappings) => { - try { - await fetch("/api/cli-tools/antigravity-mitm/alias", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: tool.id, mappings }), - }); - } catch { /* ignore */ } - }, [tool.id]); - - const handleMappingBlur = (alias, value) => { - saveMappings({ ...modelMappings, [alias]: value }); - }; - - const handleModelMappingChange = (alias, value) => { - setModelMappings(prev => ({ ...prev, [alias]: value })); - }; - - const openModelSelector = (alias) => { - setCurrentEditingAlias(alias); - setModalOpen(true); - }; - - const handleModelSelect = (model) => { - if (!currentEditingAlias || model.isPlaceholder) return; - const updated = { ...modelMappings, [currentEditingAlias]: model.value }; - setModelMappings(updated); - saveMappings(updated); - }; - - const handleDnsToggle = () => { - if (!serverRunning) return; - const action = dnsActive ? "disable" : "enable"; - if (isWindows || hasCachedPassword) { - doDnsAction(action, ""); - } else { - setPendingDnsAction(action); - setShowPasswordModal(true); - setModalError(null); - } - }; - - const doDnsAction = async (action, password) => { - setLoading(true); - setWarning(null); - try { - const res = await fetch("/api/cli-tools/antigravity-mitm", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: tool.id, action, sudoPassword: password }), - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || "Failed to toggle DNS"); - - if (action === "enable") { - setWarning(`Restart ${tool.name} to apply changes`); - } - - setShowPasswordModal(false); - setSudoPassword(""); - onDnsChange?.(data); - } catch { /* ignore */ } finally { - setLoading(false); - setPendingDnsAction(null); - } - }; - - const handleConfirmPassword = () => { - if (!sudoPassword.trim()) { - setModalError("Sudo password is required"); - return; - } - doDnsAction(pendingDnsAction, sudoPassword); - }; - - return ( - <> - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} - /> -
    -
    -
    -

    {tool.name}

    - {!serverRunning ? ( - Server off - ) : dnsActive ? ( - Active - ) : ( - DNS off - )} -
    -

    Intercept {tool.name} requests via MITM proxy

    -
    -
    - - expand_more - -
    - - {isExpanded && ( -
    - {/* Info */} -
    -

    Toggle DNS to redirect {tool.name} traffic through 9Router via MITM.

    - {!dnsActive && ( -

    - ⚠️ Enable DNS to edit model mappings -

    - )} -
    - - {/* Model Mappings */} - {tool.defaultModels?.length > 0 && ( -
    - {tool.defaultModels.map((model) => ( -
    - {model.name} - arrow_forward - handleModelMappingChange(model.alias, e.target.value)} - onBlur={(e) => handleMappingBlur(model.alias, e.target.value)} - placeholder="provider/model-id" - disabled={!dnsActive} - className={`flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 ${!dnsActive ? "opacity-50 cursor-not-allowed" : ""}`} - /> - - {modelMappings[model.alias] && ( - - )} -
    - ))} -
    - )} - - {tool.defaultModels?.length === 0 && ( -

    Model mappings will be available soon.

    - )} - - {/* Start / Stop DNS button */} -
    - {dnsActive ? ( - - ) : ( - - )} - - {/* Warning below button */} - {warning && ( -
    - warning - {warning} -
    - )} -
    -
    - )} -
    - - {/* Password Modal */} - {showPasswordModal && ( -
    -
    -

    Sudo Password Required

    -
    - warning -

    Required to modify /etc/hosts and flush DNS cache

    -
    - setSudoPassword(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter" && !loading) handleConfirmPassword(); }} - /> - {modalError && ( -
    - error - {modalError} -
    - )} -
    - - -
    -
    -
    - )} - - {/* Model Select Modal */} - setModalOpen(false)} - onSelect={handleModelSelect} - selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} - activeProviders={activeProviders} - modelAliases={modelAliases} - title={`Select model for ${currentEditingAlias}`} - /> - - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js deleted file mode 100644 index ac08e747..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js +++ /dev/null @@ -1,352 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from "react"; -import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; -import Image from "next/image"; - -export default function OpenClawToolCard({ - tool, - isExpanded, - onToggle, - baseUrl, - hasActiveProviders, - apiKeys, - activeProviders, - cloudEnabled, - initialStatus, -}) { - const [openclawStatus, setOpenclawStatus] = useState(initialStatus || null); - const [checkingOpenclaw, setCheckingOpenclaw] = useState(false); - const [applying, setApplying] = useState(false); - const [restoring, setRestoring] = useState(false); - const [message, setMessage] = useState(null); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); - const [modalOpen, setModalOpen] = useState(false); - const [modelAliases, setModelAliases] = useState({}); - const [showManualConfigModal, setShowManualConfigModal] = useState(false); - const [customBaseUrl, setCustomBaseUrl] = useState(""); - const hasInitializedModel = useRef(false); - - const getConfigStatus = () => { - if (!openclawStatus?.installed) return null; - const currentProvider = openclawStatus.settings?.models?.providers?.["9router"]; - if (!currentProvider) return "not_configured"; - const localMatch = currentProvider.baseUrl?.includes("localhost") || currentProvider.baseUrl?.includes("127.0.0.1") || currentProvider.baseUrl?.includes("0.0.0.0"); - const tunnelMatch = baseUrl && currentProvider.baseUrl?.startsWith(baseUrl); - if (localMatch || tunnelMatch) return "configured"; - return "other"; - }; - - const configStatus = getConfigStatus(); - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - if (initialStatus) setOpenclawStatus(initialStatus); - }, [initialStatus]); - - useEffect(() => { - if (isExpanded && !openclawStatus) { - checkOpenclawStatus(); - fetchModelAliases(); - } - if (isExpanded) fetchModelAliases(); - }, [isExpanded]); - - const fetchModelAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) setModelAliases(data.aliases || {}); - } catch (error) { - console.log("Error fetching model aliases:", error); - } - }; - - useEffect(() => { - if (openclawStatus?.installed && !hasInitializedModel.current) { - hasInitializedModel.current = true; - const provider = openclawStatus.settings?.models?.providers?.["9router"]; - if (provider) { - const primaryModel = openclawStatus.settings?.agents?.defaults?.model?.primary; - if (primaryModel) { - const modelId = primaryModel.replace("9router/", ""); - setSelectedModel(modelId); - } - if (provider.apiKey && apiKeys?.some(k => k.key === provider.apiKey)) { - setSelectedApiKey(provider.apiKey); - } - } - } - }, [openclawStatus, apiKeys]); - - const checkOpenclawStatus = async () => { - setCheckingOpenclaw(true); - try { - const res = await fetch("/api/cli-tools/openclaw-settings"); - const data = await res.json(); - setOpenclawStatus(data); - } catch (error) { - setOpenclawStatus({ installed: false, error: error.message }); - } finally { - setCheckingOpenclaw(false); - } - }; - - const normalizeLocalhost = (url) => url.replace("://localhost", "://127.0.0.1"); - - const getLocalBaseUrl = () => { - if (typeof window !== "undefined") { - return normalizeLocalhost(window.location.origin); - } - return "http://127.0.0.1:20128"; - }; - - const getEffectiveBaseUrl = () => { - const url = customBaseUrl || getLocalBaseUrl(); - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const getDisplayUrl = () => { - const url = customBaseUrl || getLocalBaseUrl(); - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const handleApplySettings = async () => { - setApplying(true); - setMessage(null); - try { - const keyToUse = selectedApiKey?.trim() - || (apiKeys?.length > 0 ? apiKeys[0].key : null) - || (!cloudEnabled ? "sk_9router" : null); - - const res = await fetch("/api/cli-tools/openclaw-settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: getEffectiveBaseUrl(), - apiKey: keyToUse, - model: selectedModel - }), - }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings applied successfully!" }); - checkOpenclawStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to apply settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setApplying(false); - } - }; - - const handleResetSettings = async () => { - setRestoring(true); - setMessage(null); - try { - const res = await fetch("/api/cli-tools/openclaw-settings", { method: "DELETE" }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings reset successfully!" }); - setSelectedModel(""); - setSelectedApiKey(""); - checkOpenclawStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to reset settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setRestoring(false); - } - }; - - const handleModelSelect = (model) => { - setSelectedModel(model.value); - setModalOpen(false); - }; - - const getManualConfigs = () => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : ""); - - const settingsContent = { - agents: { - defaults: { - model: { - primary: `9router/${selectedModel || "provider/model-id"}`, - }, - }, - }, - models: { - providers: { - "9router": { - baseUrl: getEffectiveBaseUrl(), - apiKey: keyToUse, - api: "openai-completions", - models: [ - { - id: selectedModel || "provider/model-id", - name: (selectedModel || "provider/model-id").split("/").pop(), - }, - ], - }, - }, - }, - }; - - return [ - { - filename: "~/.openclaw/openclaw.json", - content: JSON.stringify(settingsContent, null, 2), - }, - ]; - }; - - return ( - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} /> -
    -
    -
    -

    {tool.name}

    - {configStatus === "configured" && Connected} - {configStatus === "not_configured" && Not configured} - {configStatus === "other" && Other} -
    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {checkingOpenclaw && ( -
    - progress_activity - Checking Open Claw CLI... -
    - )} - - {!checkingOpenclaw && openclawStatus && !openclawStatus.installed && ( -
    - warning -
    -

    Open Claw CLI not installed

    -

    Please install Open Claw CLI to use this feature.

    -
    -
    - )} - - {!checkingOpenclaw && openclawStatus?.installed && ( - <> -
    - {/* Current Base URL */} - {openclawStatus?.settings?.models?.providers?.["9router"]?.baseUrl && ( -
    - Current - arrow_forward - - {openclawStatus.settings.models.providers["9router"].baseUrl} - -
    - )} - - {/* Base URL */} -
    - Base URL - arrow_forward - setCustomBaseUrl(e.target.value)} - placeholder="https://.../v1" - className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - {customBaseUrl && customBaseUrl !== baseUrl && ( - - )} -
    - - {/* API Key */} -
    - API Key - arrow_forward - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} - - )} -
    - - {/* Model */} -
    - Model - arrow_forward - setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> - - {selectedModel && } -
    -
    - - {message && ( -
    - {message.type === "success" ? "check_circle" : "error"} - {message.text} -
    - )} - -
    - - - -
    - - )} -
    - )} - - setModalOpen(false)} - onSelect={handleModelSelect} - selectedModel={selectedModel} - activeProviders={activeProviders} - modelAliases={modelAliases} - title="Select Model for Open Claw" - /> - - setShowManualConfigModal(false)} - title="Open Claw - Manual Configuration" - configs={getManualConfigs()} - /> -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js deleted file mode 100644 index 4ab16783..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js +++ /dev/null @@ -1,308 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; -import Image from "next/image"; - -export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) { - const [status, setStatus] = useState(initialStatus || null); - const [checking, setChecking] = useState(false); - const [applying, setApplying] = useState(false); - const [restoring, setRestoring] = useState(false); - const [message, setMessage] = useState(null); - const [showInstallGuide, setShowInstallGuide] = useState(false); - const [selectedApiKey, setSelectedApiKey] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); - const [modalOpen, setModalOpen] = useState(false); - const [modelAliases, setModelAliases] = useState({}); - const [showManualConfigModal, setShowManualConfigModal] = useState(false); - const [customBaseUrl, setCustomBaseUrl] = useState(""); - - useEffect(() => { - if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); - } - }, [apiKeys, selectedApiKey]); - - useEffect(() => { - if (initialStatus) setStatus(initialStatus); - }, [initialStatus]); - - useEffect(() => { - if (isExpanded && !status) { - checkStatus(); - fetchModelAliases(); - } - if (isExpanded) fetchModelAliases(); - }, [isExpanded]); - - // Sync model from existing config - useEffect(() => { - if (status?.config?.model?.startsWith("9router/")) { - setSelectedModel(status.config.model.replace("9router/", "")); - } - }, [status]); - - const fetchModelAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) setModelAliases(data.aliases || {}); - } catch (error) { - console.log("Error fetching model aliases:", error); - } - }; - - const getConfigStatus = () => { - if (!status?.installed) return null; - if (!status.config) return "not_configured"; - const url = status.config?.provider?.["9router"]?.options?.baseURL || ""; - const isLocal = url.includes("localhost") || url.includes("127.0.0.1"); - return status.has9Router && (isLocal || url.includes(baseUrl)) ? "configured" : status.has9Router ? "other" : "not_configured"; - }; - - const configStatus = getConfigStatus(); - - const getEffectiveBaseUrl = () => { - const url = customBaseUrl || baseUrl; - return url.endsWith("/v1") ? url : `${url}/v1`; - }; - - const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`; - - const checkStatus = async () => { - setChecking(true); - try { - const res = await fetch("/api/cli-tools/opencode-settings"); - const data = await res.json(); - setStatus(data); - } catch (error) { - setStatus({ installed: false, error: error.message }); - } finally { - setChecking(false); - } - }; - - const handleApply = async () => { - setApplying(true); - setMessage(null); - try { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : selectedApiKey); - - const res = await fetch("/api/cli-tools/opencode-settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, model: selectedModel }), - }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings applied successfully!" }); - checkStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to apply settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setApplying(false); - } - }; - - const handleReset = async () => { - setRestoring(true); - setMessage(null); - try { - const res = await fetch("/api/cli-tools/opencode-settings", { method: "DELETE" }); - const data = await res.json(); - if (res.ok) { - setMessage({ type: "success", text: "Settings reset successfully!" }); - setSelectedModel(""); - checkStatus(); - } else { - setMessage({ type: "error", text: data.error || "Failed to reset settings" }); - } - } catch (error) { - setMessage({ type: "error", text: error.message }); - } finally { - setRestoring(false); - } - }; - - const getManualConfigs = () => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey - : (!cloudEnabled ? "sk_9router" : ""); - - return [{ - filename: "~/.config/opencode/opencode.json", - content: JSON.stringify({ - provider: { - "9router": { - npm: "@ai-sdk/openai-compatible", - options: { baseURL: getEffectiveBaseUrl(), apiKey: keyToUse }, - models: { [selectedModel || "provider/model-id"]: { name: selectedModel || "provider/model-id" } }, - }, - }, - model: `9router/${selectedModel || "provider/model-id"}`, - }, null, 2), - }]; - }; - - return ( - -
    -
    -
    - {tool.name} { e.target.style.display = "none"; }} /> -
    -
    -
    -

    {tool.name}

    - {configStatus === "configured" && Connected} - {configStatus === "not_configured" && Not configured} - {configStatus === "other" && Other} -
    -

    {tool.description}

    -
    -
    - expand_more -
    - - {isExpanded && ( -
    - {checking && ( -
    - progress_activity - Checking OpenCode CLI... -
    - )} - - {!checking && status && !status.installed && ( -
    -
    - warning -
    -

    OpenCode CLI not installed

    -

    Please install OpenCode CLI to use auto-apply feature.

    -
    - -
    - {showInstallGuide && ( -
    -

    Installation Guide

    -
    -
    -

    macOS / Linux:

    - npm install -g opencode-ai -
    -

    After installation, run opencode to verify.

    -
    -
    - )} -
    - )} - - {!checking && status?.installed && ( - <> -
    - {/* Current base URL */} - {status?.config?.provider?.["9router"]?.options?.baseURL && ( -
    - Current - arrow_forward - - {status.config.provider["9router"].options.baseURL} - -
    - )} - - {/* Base URL */} -
    - Base URL - arrow_forward - setCustomBaseUrl(e.target.value)} - placeholder="https://.../v1" - className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - {customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && ( - - )} -
    - - {/* API Key */} -
    - API Key - arrow_forward - {apiKeys.length > 0 ? ( - - ) : ( - - {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"} - - )} -
    - - {/* Model */} -
    - Model - arrow_forward - setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> - - {selectedModel && } -
    -
    - - {message && ( -
    - {message.type === "success" ? "check_circle" : "error"} - {message.text} -
    - )} - -
    - - - -
    - - )} -
    - )} - - setModalOpen(false)} - onSelect={(model) => { setSelectedModel(model.value); setModalOpen(false); }} - selectedModel={selectedModel} - activeProviders={activeProviders} - modelAliases={modelAliases} - title="Select Model for OpenCode" - /> - - setShowManualConfigModal(false)} - title="OpenCode - Manual Configuration" - configs={getManualConfigs()} - /> -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/components/index.js b/9router/src/app/(dashboard)/dashboard/cli-tools/components/index.js deleted file mode 100644 index ee0ecc13..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/components/index.js +++ /dev/null @@ -1,11 +0,0 @@ -export { default as ClaudeToolCard } from "./ClaudeToolCard"; -export { default as CodexToolCard } from "./CodexToolCard"; -export { default as DroidToolCard } from "./DroidToolCard"; -export { default as OpenClawToolCard } from "./OpenClawToolCard"; -export { default as DefaultToolCard } from "./DefaultToolCard"; -export { default as AntigravityToolCard } from "./AntigravityToolCard"; -export { default as OpenCodeToolCard } from "./OpenCodeToolCard"; -export { default as CopilotToolCard } from "./CopilotToolCard"; -export { default as MitmServerCard } from "./MitmServerCard"; -export { default as MitmToolCard } from "./MitmToolCard"; - diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/page.js b/9router/src/app/(dashboard)/dashboard/cli-tools/page.js deleted file mode 100644 index 24f6030a..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/page.js +++ /dev/null @@ -1,7 +0,0 @@ -import { getMachineId } from "@/shared/utils/machine"; -import CLIToolsPageClient from "./CLIToolsPageClient"; - -export default async function CLIToolsPage() { - const machineId = await getMachineId(); - return ; -} diff --git a/9router/src/app/(dashboard)/dashboard/combos/page.js b/9router/src/app/(dashboard)/dashboard/combos/page.js deleted file mode 100644 index 2730717f..00000000 --- a/9router/src/app/(dashboard)/dashboard/combos/page.js +++ /dev/null @@ -1,470 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal } from "@/shared/components"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; -import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; - -// Validate combo name: only a-z, A-Z, 0-9, -, _ -const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/; - -export default function CombosPage() { - const [combos, setCombos] = useState([]); - const [loading, setLoading] = useState(true); - const [showCreateModal, setShowCreateModal] = useState(false); - const [editingCombo, setEditingCombo] = useState(null); - const [activeProviders, setActiveProviders] = useState([]); - const { copied, copy } = useCopyToClipboard(); - - useEffect(() => { - fetchData(); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - const fetchData = async () => { - try { - const [combosRes, providersRes] = await Promise.all([ - fetch("/api/combos"), - fetch("/api/providers"), - ]); - const combosData = await combosRes.json(); - const providersData = await providersRes.json(); - - if (combosRes.ok) setCombos(combosData.combos || []); - if (providersRes.ok) { - setActiveProviders(providersData.connections || []); - } - } catch (error) { - console.log("Error fetching data:", error); - } finally { - setLoading(false); - } - }; - - const handleCreate = async (data) => { - try { - const res = await fetch("/api/combos", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }); - if (res.ok) { - await fetchData(); - setShowCreateModal(false); - } else { - const err = await res.json(); - alert(err.error || "Failed to create combo"); - } - } catch (error) { - console.log("Error creating combo:", error); - } - }; - - const handleUpdate = async (id, data) => { - try { - const res = await fetch(`/api/combos/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }); - if (res.ok) { - await fetchData(); - setEditingCombo(null); - } else { - const err = await res.json(); - alert(err.error || "Failed to update combo"); - } - } catch (error) { - console.log("Error updating combo:", error); - } - }; - - const handleDelete = async (id) => { - if (!confirm("Delete this combo?")) return; - try { - const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); - if (res.ok) { - setCombos(combos.filter(c => c.id !== id)); - } - } catch (error) { - console.log("Error deleting combo:", error); - } - }; - - if (loading) { - return ( -
    - - -
    - ); - } - - return ( -
    - {/* Header */} -
    -
    -

    Combos

    -

    - Create model combos with fallback support -

    -
    - -
    - - {/* Combos List */} - {combos.length === 0 ? ( - -
    -
    - layers -
    -

    No combos yet

    -

    Create model combos with fallback support

    - -
    -
    - ) : ( -
    - {combos.map((combo) => ( - setEditingCombo(combo)} - onDelete={() => handleDelete(combo.id)} - /> - ))} -
    - )} - - {/* Create Modal - Use key to force remount and reset state */} - setShowCreateModal(false)} - onSave={handleCreate} - activeProviders={activeProviders} - /> - - {/* Edit Modal - Use key to force remount and reset state */} - setEditingCombo(null)} - onSave={(data) => handleUpdate(editingCombo.id, data)} - activeProviders={activeProviders} - /> -
    - ); -} - -function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) { - return ( - -
    -
    -
    - layers -
    -
    -
    - {combo.name} - -
    -
    - {combo.models.length === 0 ? ( - No models - ) : ( - combo.models.slice(0, 3).map((model, index) => ( - - {model} - - )) - )} - {combo.models.length > 3 && ( - +{combo.models.length - 3} more - )} -
    -
    -
    - - {/* Actions */} -
    - - -
    -
    -
    - ); -} - -// Inline editable model item -function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown, onRemove }) { - const [editing, setEditing] = useState(false); - const [draft, setDraft] = useState(model); - - const commit = () => { - const trimmed = draft.trim(); - if (trimmed && trimmed !== model) onEdit(trimmed); - else setDraft(model); // revert if empty or unchanged - setEditing(false); - }; - - const handleKeyDown = (e) => { - if (e.key === "Enter") commit(); - if (e.key === "Escape") { setDraft(model); setEditing(false); } - }; - - return ( -
    - {/* Index badge */} - {index + 1} - - {/* Inline editable model value */} - {editing ? ( - setDraft(e.target.value)} - onBlur={commit} - onKeyDown={handleKeyDown} - className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-white dark:bg-black/20 border border-primary/40 rounded outline-none text-text-main" - /> - ) : ( -
    setEditing(true)} - title="Click to edit" - > - {model} -
    - )} - - {/* Priority arrows */} -
    - - -
    - - {/* Remove */} - -
    - ); -} - -function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { - // Initialize state with combo values - key prop on parent handles reset on remount - const [name, setName] = useState(combo?.name || ""); - const [models, setModels] = useState(combo?.models || []); - const [showModelSelect, setShowModelSelect] = useState(false); - const [saving, setSaving] = useState(false); - const [nameError, setNameError] = useState(""); - const [modelAliases, setModelAliases] = useState({}); - - const fetchModalData = async () => { - try { - const aliasesRes = await fetch("/api/models/alias"); - if (!aliasesRes.ok) return; - const aliasesData = await aliasesRes.json(); - setModelAliases(aliasesData.aliases || {}); - } catch (error) { - console.error("Error fetching modal data:", error); - } - }; - - useEffect(() => { - if (isOpen) fetchModalData(); - }, [isOpen]); - - const validateName = (value) => { - if (!value.trim()) { - setNameError("Name is required"); - return false; - } - if (!VALID_NAME_REGEX.test(value)) { - setNameError("Only letters, numbers, -, _ and . allowed"); - return false; - } - setNameError(""); - return true; - }; - - const handleNameChange = (e) => { - const value = e.target.value; - setName(value); - if (value) validateName(value); - else setNameError(""); - }; - - const handleAddModel = (model) => { - if (!models.includes(model.value)) { - setModels([...models, model.value]); - } - }; - - const handleRemoveModel = (index) => { - setModels(models.filter((_, i) => i !== index)); - }; - - const handleMoveUp = (index) => { - if (index === 0) return; - const newModels = [...models]; - [newModels[index - 1], newModels[index]] = [newModels[index], newModels[index - 1]]; - setModels(newModels); - }; - - const handleMoveDown = (index) => { - if (index === models.length - 1) return; - const newModels = [...models]; - [newModels[index], newModels[index + 1]] = [newModels[index + 1], newModels[index]]; - setModels(newModels); - }; - - const handleSave = async () => { - if (!validateName(name)) return; - setSaving(true); - await onSave({ name: name.trim(), models }); - setSaving(false); - }; - - const isEdit = !!combo; - - return ( - <> - -
    - {/* Name */} -
    - -

    - Only letters, numbers, -, _ and . allowed -

    -
    - - {/* Models */} -
    - - - {models.length === 0 ? ( -
    - layers -

    No models added yet

    -
    - ) : ( -
    - {models.map((model, index) => ( - { - const updated = [...models]; - updated[index] = newVal; - setModels(updated); - }} - onMoveUp={() => handleMoveUp(index)} - onMoveDown={() => handleMoveDown(index)} - onRemove={() => handleRemoveModel(index)} - /> - ))} -
    - )} - - {/* Add Model button */} - -
    - - {/* Actions */} -
    - - -
    -
    -
    - - {/* Model Select Modal */} - setShowModelSelect(false)} - onSelect={handleAddModel} - activeProviders={activeProviders} - modelAliases={modelAliases} - title="Add Model to Combo" - /> - - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/console-log/ConsoleLogClient.js b/9router/src/app/(dashboard)/dashboard/console-log/ConsoleLogClient.js deleted file mode 100644 index 6373015d..00000000 --- a/9router/src/app/(dashboard)/dashboard/console-log/ConsoleLogClient.js +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from "react"; -import { Card, Button } from "@/shared/components"; -import { CONSOLE_LOG_CONFIG } from "@/shared/constants/config"; - -const LOG_LEVEL_COLORS = { - LOG: "text-green-400", - INFO: "text-blue-400", - WARN: "text-yellow-400", - ERROR: "text-red-400", - DEBUG: "text-purple-400", -}; - -function colorLine(line) { - const match = line.match(/\[(\w+)\]/g); - const levelTag = match ? match[1]?.replace(/\[|\]/g, "") : null; - const color = LOG_LEVEL_COLORS[levelTag] || "text-green-400"; - return {line}; -} - -export default function ConsoleLogClient() { - const [logs, setLogs] = useState([]); - const [connected, setConnected] = useState(false); - const logRef = useRef(null); - - const handleClear = async () => { - try { - await fetch("/api/translator/console-logs", { method: "DELETE" }); - // UI cleared via SSE "clear" event - } catch (err) { - console.error("Failed to clear console logs:", err); - } - }; - - useEffect(() => { - const es = new EventSource("/api/translator/console-logs/stream"); - - es.onopen = () => setConnected(true); - - es.onmessage = (e) => { - const msg = JSON.parse(e.data); - if (msg.type === "init") { - setLogs(msg.logs.slice(-CONSOLE_LOG_CONFIG.maxLines)); - } else if (msg.type === "line") { - setLogs((prev) => { - const next = [...prev, msg.line]; - return next.length > CONSOLE_LOG_CONFIG.maxLines ? next.slice(-CONSOLE_LOG_CONFIG.maxLines) : next; - }); - } else if (msg.type === "clear") { - setLogs([]); - } - }; - - es.onerror = () => setConnected(false); - - return () => es.close(); - }, []); - - // Auto-scroll to bottom on new logs - useEffect(() => { - if (!logRef.current) return; - logRef.current.scrollTop = logRef.current.scrollHeight; - }, [logs]); - - return ( -
    - -
    - -
    -
    - {logs.length === 0 ? ( - No console logs yet. - ) : ( -
    - {logs.map((line, i) => ( -
    {colorLine(line)}
    - ))} -
    - )} -
    -
    -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/console-log/page.js b/9router/src/app/(dashboard)/dashboard/console-log/page.js deleted file mode 100644 index 863cef86..00000000 --- a/9router/src/app/(dashboard)/dashboard/console-log/page.js +++ /dev/null @@ -1,8 +0,0 @@ -import ConsoleLogClient from "./ConsoleLogClient"; - -// Force dynamic so Next.js standalone build includes the server-side JS file -export const dynamic = "force-dynamic"; - -export default function ConsoleLogPage() { - return ; -} diff --git a/9router/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/9router/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js deleted file mode 100644 index a48eaa14..00000000 --- a/9router/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ /dev/null @@ -1,772 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import PropTypes from "prop-types"; -import { Card, Button, Input, Modal, CardSkeleton, Toggle } from "@/shared/components"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; - -/* ========== CLOUD CODE — COMMENTED OUT (replaced by Tunnel) ========== -const DEFAULT_CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || ""; -const CLOUD_ACTION_TIMEOUT_MS = 15000; -========== END CLOUD CODE ========== */ - -const TUNNEL_BENEFITS = [ - { icon: "public", title: "Access Anywhere", desc: "Use your API from any network" }, - { icon: "group", title: "Share Endpoint", desc: "Share URL with team members" }, - { icon: "code", title: "Use in Cursor/Cline", desc: "Connect AI tools remotely" }, - { icon: "lock", title: "Encrypted", desc: "End-to-end TLS via Cloudflare" }, -]; - -const TUNNEL_ACTION_TIMEOUT_MS = 90000; - -export default function APIPageClient({ machineId }) { - const [keys, setKeys] = useState([]); - const [loading, setLoading] = useState(true); - const [showAddModal, setShowAddModal] = useState(false); - const [newKeyName, setNewKeyName] = useState(""); - const [createdKey, setCreatedKey] = useState(null); - - /* ========== CLOUD STATE — COMMENTED OUT (replaced by Tunnel) ========== - const [cloudEnabled, setCloudEnabled] = useState(false); - const [cloudUrl, setCloudUrl] = useState(DEFAULT_CLOUD_URL); - const [cloudUrlInput, setCloudUrlInput] = useState(DEFAULT_CLOUD_URL); - const [cloudUrlSaving, setCloudUrlSaving] = useState(false); - const [showCloudModal, setShowCloudModal] = useState(false); - const [showDisableModal, setShowDisableModal] = useState(false); - const [showSetupModal, setShowSetupModal] = useState(false); - const [setupStatus, setSetupStatus] = useState(null); - const [cloudSyncing, setCloudSyncing] = useState(false); - const [cloudStatus, setCloudStatus] = useState(null); - const [syncStep, setSyncStep] = useState(""); - ========== END CLOUD STATE ========== */ - - // Tunnel state - const [requireApiKey, setRequireApiKey] = useState(false); - const [tunnelEnabled, setTunnelEnabled] = useState(false); - const [tunnelUrl, setTunnelUrl] = useState(""); - const [tunnelPublicUrl, setTunnelPublicUrl] = useState(""); - const [tunnelShortId, setTunnelShortId] = useState(""); - const [tunnelLoading, setTunnelLoading] = useState(false); - const [tunnelProgress, setTunnelProgress] = useState(""); - const [tunnelStatus, setTunnelStatus] = useState(null); - const [showDisableModal, setShowDisableModal] = useState(false); - const [showEnableModal, setShowEnableModal] = useState(false); - // API key visibility toggle state - const [visibleKeys, setVisibleKeys] = useState(new Set()); - - const { copied, copy } = useCopyToClipboard(); - - useEffect(() => { - fetchData(); - loadSettings(); - }, []); - - /* ========== CLOUD FUNCTIONS — COMMENTED OUT (replaced by Tunnel) ========== - const postCloudAction = async (action, timeoutMs = CLOUD_ACTION_TIMEOUT_MS) => { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - try { - const res = await fetch("/api/sync/cloud", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action }), - signal: controller.signal, - }); - const data = await res.json().catch(() => ({})); - return { ok: res.ok, status: res.status, data }; - } catch (error) { - if (error?.name === "AbortError") { - return { ok: false, status: 408, data: { error: "Cloud request timeout" } }; - } - return { ok: false, status: 500, data: { error: error.message || "Cloud request failed" } }; - } finally { - clearTimeout(timeoutId); - } - }; - - const loadCloudSettings = async () => { - try { - const res = await fetch("/api/settings"); - if (res.ok) { - const data = await res.json(); - setCloudEnabled(data.cloudEnabled || false); - setRequireApiKey(data.requireApiKey || false); - const url = data.cloudUrl || DEFAULT_CLOUD_URL; - setCloudUrl(url); - setCloudUrlInput(url); - } - } catch (error) { - console.log("Error loading cloud settings:", error); - } - }; - - const handleCloudToggle = (checked) => { - if (checked) { - setShowCloudModal(true); - } else { - setShowDisableModal(true); - } - }; - - const handleEnableCloud = async () => { - setCloudSyncing(true); - setSyncStep("syncing"); - try { - const { ok, data } = await postCloudAction("enable"); - if (ok) { - setSyncStep("verifying"); - if (data.verified) { - setCloudEnabled(true); - setCloudStatus({ type: "success", message: "Cloud Proxy connected and verified!" }); - setShowCloudModal(false); - } else { - setCloudEnabled(true); - setCloudStatus({ type: "warning", message: data.verifyError || "Connected but verification failed" }); - setShowCloudModal(false); - } - if (data.createdKey) await fetchData(); - } else { - setCloudStatus({ type: "error", message: data.error || "Failed to enable cloud" }); - } - } catch (error) { - setCloudStatus({ type: "error", message: error.message }); - } finally { - setCloudSyncing(false); - setSyncStep(""); - } - }; - - const handleConfirmDisable = async () => { - setCloudSyncing(true); - setSyncStep("syncing"); - try { - await postCloudAction("sync"); - setSyncStep("disabling"); - const { ok, data } = await postCloudAction("disable"); - if (ok) { - setCloudEnabled(false); - setCloudStatus({ type: "success", message: "Cloud disabled" }); - setShowDisableModal(false); - } else { - setCloudStatus({ type: "error", message: data.error || "Failed to disable cloud" }); - } - } catch (error) { - setCloudStatus({ type: "error", message: "Failed to disable cloud" }); - } finally { - setCloudSyncing(false); - setSyncStep(""); - } - }; - - const handleSyncCloud = async () => { - if (!cloudEnabled) return; - setCloudSyncing(true); - try { - const { ok, data } = await postCloudAction("sync"); - if (ok) setCloudStatus({ type: "success", message: "Synced successfully" }); - else setCloudStatus({ type: "error", message: data.error }); - } catch (error) { - setCloudStatus({ type: "error", message: error.message }); - } finally { - setCloudSyncing(false); - } - }; - - const handleSaveCloudUrl = async () => { - const trimmed = cloudUrlInput.trim().replace(/\/v1\/?$/, "").replace(/\/+$/, ""); - if (!trimmed) return; - setCloudUrlSaving(true); - setSetupStatus(null); - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ cloudUrl: trimmed }), - }); - if (res.ok) { - setCloudUrl(trimmed); - setCloudUrlInput(trimmed); - setSetupStatus({ type: "success", message: "Worker URL saved" }); - } else { - setSetupStatus({ type: "error", message: "Failed to save Worker URL" }); - } - } catch (error) { - setSetupStatus({ type: "error", message: error.message }); - } finally { - setCloudUrlSaving(false); - } - }; - - const handleCheckCloud = async () => { - if (!cloudUrl) return; - setCloudSyncing(true); - setSetupStatus(null); - try { - const { ok, data } = await postCloudAction("check", 8000); - if (ok) setSetupStatus({ type: "success", message: data.message || "Worker is running" }); - else setSetupStatus({ type: "error", message: data.error || "Check failed" }); - } catch { - setSetupStatus({ type: "error", message: "Cannot reach worker" }); - } finally { - setCloudSyncing(false); - } - }; - ========== END CLOUD FUNCTIONS ========== */ - - const loadSettings = async () => { - try { - const [settingsRes, tunnelRes] = await Promise.all([ - fetch("/api/settings"), - fetch("/api/tunnel/status") - ]); - if (settingsRes.ok) { - const data = await settingsRes.json(); - setRequireApiKey(data.requireApiKey || false); - } - if (tunnelRes.ok) { - const data = await tunnelRes.json(); - setTunnelEnabled(data.enabled || false); - setTunnelUrl(data.tunnelUrl || ""); - setTunnelPublicUrl(data.publicUrl || ""); - setTunnelShortId(data.shortId || ""); - } - } catch (error) { - console.log("Error loading settings:", error); - } - }; - - const handleRequireApiKey = async (value) => { - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ requireApiKey: value }), - }); - if (res.ok) setRequireApiKey(value); - } catch (error) { - console.log("Error updating requireApiKey:", error); - } - }; - - const fetchData = async () => { - try { - const keysRes = await fetch("/api/keys"); - const keysData = await keysRes.json(); - if (keysRes.ok) { - setKeys(keysData.keys || []); - } - } catch (error) { - console.log("Error fetching data:", error); - } finally { - setLoading(false); - } - }; - - const handleEnableTunnel = async () => { - setShowEnableModal(false); - setTunnelLoading(true); - setTunnelStatus(null); - setTunnelProgress("Connecting to server..."); - - const progressSteps = [ - { delay: 2000, msg: "Creating tunnel..." }, - { delay: 5000, msg: "Starting cloudflared..." }, - { delay: 15000, msg: "Establishing connections..." }, - { delay: 30000, msg: "Waiting for tunnel ready..." }, - ]; - const timers = progressSteps.map(({ delay, msg }) => - setTimeout(() => setTunnelProgress(msg), delay) - ); - - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), TUNNEL_ACTION_TIMEOUT_MS); - const res = await fetch("/api/tunnel/enable", { - method: "POST", - signal: controller.signal, - }); - clearTimeout(timeoutId); - timers.forEach(clearTimeout); - const data = await res.json(); - if (res.ok) { - setTunnelEnabled(true); - setTunnelUrl(data.tunnelUrl || ""); - setTunnelPublicUrl(data.publicUrl || ""); - setTunnelShortId(data.shortId || ""); - setTunnelStatus({ type: "success", message: "Tunnel connected!" }); - } else { - setTunnelStatus({ type: "error", message: data.error || "Failed to enable tunnel" }); - } - } catch (error) { - timers.forEach(clearTimeout); - const msg = error?.name === "AbortError" ? "Tunnel creation timed out" : error.message; - setTunnelStatus({ type: "error", message: msg }); - } finally { - setTunnelLoading(false); - setTunnelProgress(""); - } - }; - - const handleDisableTunnel = async () => { - setTunnelLoading(true); - setTunnelStatus(null); - try { - const res = await fetch("/api/tunnel/disable", { method: "POST" }); - const data = await res.json(); - if (res.ok) { - setTunnelEnabled(false); - setTunnelUrl(""); - setTunnelPublicUrl(""); - setTunnelStatus({ type: "success", message: "Tunnel disabled" }); - setShowDisableModal(false); - } else { - setTunnelStatus({ type: "error", message: data.error || "Failed to disable tunnel" }); - } - } catch (error) { - setTunnelStatus({ type: "error", message: error.message }); - } finally { - setTunnelLoading(false); - } - }; - - const handleCreateKey = async () => { - if (!newKeyName.trim()) return; - - try { - const res = await fetch("/api/keys", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: newKeyName }), - }); - const data = await res.json(); - - if (res.ok) { - setCreatedKey(data.key); - await fetchData(); - setNewKeyName(""); - setShowAddModal(false); - } - } catch (error) { - console.log("Error creating key:", error); - } - }; - - const handleDeleteKey = async (id) => { - if (!confirm("Delete this API key?")) return; - - try { - const res = await fetch(`/api/keys/${id}`, { method: "DELETE" }); - if (res.ok) { - setKeys(keys.filter((k) => k.id !== id)); - // Clean up visibility state - setVisibleKeys(prev => { - const next = new Set(prev); - next.delete(id); - return next; - }); - } - } catch (error) { - console.log("Error deleting key:", error); - } - }; - - const handleToggleKey = async (id, isActive) => { - try { - const res = await fetch(`/api/keys/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ isActive }), - }); - if (res.ok) { - setKeys(prev => prev.map(k => k.id === id ? { ...k, isActive } : k)); - } - } catch (error) { - console.log("Error toggling key:", error); - } - }; - - const maskKey = (fullKey) => { - if (!fullKey) return ""; - return fullKey.length > 8 ? fullKey.slice(0, 8) + "..." : fullKey; - }; - - const toggleKeyVisibility = (keyId) => { - setVisibleKeys(prev => { - const next = new Set(prev); - if (next.has(keyId)) next.delete(keyId); - else next.add(keyId); - return next; - }); - }; - - const [baseUrl, setBaseUrl] = useState("/v1"); - - // Hydration fix: Only access window on client side - useEffect(() => { - if (typeof window !== "undefined") { - setBaseUrl(`${window.location.origin}/v1`); - } - }, []); - - if (loading) { - return ( -
    - - -
    - ); - } - - const currentEndpoint = tunnelEnabled && tunnelPublicUrl ? `${tunnelPublicUrl}/v1` : baseUrl; - - return ( -
    - {/* Endpoint Card */} - -
    -
    -

    API Endpoint

    -

    - {tunnelEnabled ? "Using Tunnel" : "Using Local Server"} -

    -
    -
    - {tunnelEnabled ? ( - - ) : ( - - )} -
    -
    - - {/* Endpoint URL */} -
    - - -
    - - {/* Tunnel Status */} - {tunnelStatus && ( -
    - {tunnelStatus.message} -
    - )} -
    - - {/* API Keys */} - -
    -

    API Keys

    - -
    - -
    -
    -

    Require API key

    -

    - Requests without a valid key will be rejected -

    -
    - handleRequireApiKey(!requireApiKey)} - /> -
    - - {keys.length === 0 ? ( -
    -
    - vpn_key -
    -

    No API keys yet

    -

    Create your first API key to get started

    - -
    - ) : ( -
    - {keys.map((key) => ( -
    -
    -

    {key.name}

    -
    - - {visibleKeys.has(key.id) ? key.key : maskKey(key.key)} - - - -
    -

    - Created {new Date(key.createdAt).toLocaleDateString()} -

    - {key.isActive === false && ( -

    Paused

    - )} -
    -
    - { - if (key.isActive && !checked) { - if (confirm(`Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`)) { - handleToggleKey(key.id, checked); - } - } else { - handleToggleKey(key.id, checked); - } - }} - title={key.isActive ? "Pause key" : "Resume key"} - /> - -
    -
    - ))} -
    - )} -
    - - {/* CLOUD MODALS — COMMENTED OUT (replaced by Tunnel) */} - {/* Setup Cloud Modal — removed */} - {/* Cloud Enable Modal — removed */} - - {/* Add Key Modal */} - { - setShowAddModal(false); - setNewKeyName(""); - }} - > -
    - setNewKeyName(e.target.value)} - placeholder="Production Key" - /> -
    - - -
    -
    -
    - - {/* Created Key Modal */} - setCreatedKey(null)} - > -
    -
    -

    - Save this key now! -

    -

    - This is the only time you will see this key. Store it securely. -

    -
    -
    - - -
    - -
    -
    - - {/* Enable Tunnel Modal */} - setShowEnableModal(false)} - > -
    -
    -
    - cloud_upload -
    -

    - Cloudflare Tunnel -

    -

    - Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere. -

    -
    -
    -
    - -
    - {TUNNEL_BENEFITS.map((benefit) => ( -
    - {benefit.icon} -

    {benefit.title}

    -

    {benefit.desc}

    -
    - ))} -
    - -

    - Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s. -

    - -
    - - -
    -
    -
    - - {/* Disable Tunnel Modal */} - !tunnelLoading && setShowDisableModal(false)} - > -
    -
    -
    - warning -
    -

    - Warning -

    -

    - The tunnel will be disconnected. Remote access will stop working. -

    -
    -
    -
    - -

    Are you sure you want to disable the tunnel?

    - -
    - - -
    -
    -
    -
    - ); -} - -APIPageClient.propTypes = { - machineId: PropTypes.string.isRequired, -}; diff --git a/9router/src/app/(dashboard)/dashboard/endpoint/page.js b/9router/src/app/(dashboard)/dashboard/endpoint/page.js deleted file mode 100644 index 96a3e31e..00000000 --- a/9router/src/app/(dashboard)/dashboard/endpoint/page.js +++ /dev/null @@ -1,7 +0,0 @@ -import { getMachineId } from "@/shared/utils/machine"; -import EndpointPageClient from "./EndpointPageClient"; - -export default async function EndpointPage() { - const machineId = await getMachineId(); - return ; -} diff --git a/9router/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js b/9router/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js deleted file mode 100644 index 6849359f..00000000 --- a/9router/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { MITM_TOOLS } from "@/shared/constants/cliTools"; -import { getModelsByProviderId } from "@/shared/constants/models"; -import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; -import { MitmServerCard, MitmToolCard } from "@/app/(dashboard)/dashboard/cli-tools/components"; - -export default function MitmPageClient() { - const [connections, setConnections] = useState([]); - const [apiKeys, setApiKeys] = useState([]); - const [modelAliases, setModelAliases] = useState({}); - const [cloudEnabled, setCloudEnabled] = useState(false); - const [expandedTool, setExpandedTool] = useState(null); - const [mitmStatus, setMitmStatus] = useState({ running: false, certExists: false, dnsStatus: {}, hasCachedPassword: false }); - - useEffect(() => { - fetchConnections(); - fetchApiKeys(); - fetchAliases(); - fetchCloudSettings(); - }, []); - - const fetchConnections = async () => { - try { - const res = await fetch("/api/providers"); - if (res.ok) { - const data = await res.json(); - setConnections(data.connections || []); - } - } catch { /* ignore */ } - }; - - const fetchApiKeys = async () => { - try { - const res = await fetch("/api/keys"); - if (res.ok) { - const data = await res.json(); - setApiKeys(data.keys || []); - } - } catch { /* ignore */ } - }; - - const fetchAliases = async () => { - try { - const res = await fetch("/api/models/alias"); - if (res.ok) { - const data = await res.json(); - setModelAliases(data.aliases || {}); - } - } catch { /* ignore */ } - }; - - const fetchCloudSettings = async () => { - try { - const res = await fetch("/api/settings"); - if (res.ok) { - const data = await res.json(); - setCloudEnabled(data.cloudEnabled || false); - } - } catch { /* ignore */ } - }; - - const getActiveProviders = () => connections.filter(c => c.isActive !== false); - - const hasActiveProviders = () => { - const active = getActiveProviders(); - return active.some(conn => - getModelsByProviderId(conn.provider).length > 0 || - isOpenAICompatibleProvider(conn.provider) || - isAnthropicCompatibleProvider(conn.provider) - ); - }; - - const mitmTools = Object.entries(MITM_TOOLS); - - return ( -
    - {/* MITM Server Card */} - - - {/* Tool Cards */} -
    - {mitmTools.map(([toolId, tool]) => ( - setExpandedTool(expandedTool === toolId ? null : toolId)} - serverRunning={mitmStatus.running} - dnsActive={mitmStatus.dnsStatus?.[toolId] || false} - hasCachedPassword={mitmStatus.hasCachedPassword || false} - apiKeys={apiKeys} - activeProviders={getActiveProviders()} - hasActiveProviders={hasActiveProviders()} - modelAliases={modelAliases} - cloudEnabled={cloudEnabled} - onDnsChange={(data) => setMitmStatus(prev => ({ ...prev, dnsStatus: data.dnsStatus ?? prev.dnsStatus }))} - /> - ))} -
    -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/mitm/page.js b/9router/src/app/(dashboard)/dashboard/mitm/page.js deleted file mode 100644 index 03d5f394..00000000 --- a/9router/src/app/(dashboard)/dashboard/mitm/page.js +++ /dev/null @@ -1,5 +0,0 @@ -import MitmPageClient from "./MitmPageClient"; - -export default function MitmPage() { - return ; -} diff --git a/9router/src/app/(dashboard)/dashboard/page.js b/9router/src/app/(dashboard)/dashboard/page.js deleted file mode 100644 index 6a94a1fe..00000000 --- a/9router/src/app/(dashboard)/dashboard/page.js +++ /dev/null @@ -1,7 +0,0 @@ -import { getMachineId } from "@/shared/utils/machine"; -import EndpointPageClient from "./endpoint/EndpointPageClient"; - -export default async function DashboardPage() { - const machineId = await getMachineId(); - return ; -} diff --git a/9router/src/app/(dashboard)/dashboard/profile/page.js b/9router/src/app/(dashboard)/dashboard/profile/page.js deleted file mode 100644 index 63d985b2..00000000 --- a/9router/src/app/(dashboard)/dashboard/profile/page.js +++ /dev/null @@ -1,631 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from "react"; -import { Card, Button, Toggle, Input } from "@/shared/components"; -import { useTheme } from "@/shared/hooks/useTheme"; -import { cn } from "@/shared/utils/cn"; -import { APP_CONFIG } from "@/shared/constants/config"; - -export default function ProfilePage() { - const { theme, setTheme, isDark } = useTheme(); - const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" }); - const [loading, setLoading] = useState(true); - const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" }); - const [passStatus, setPassStatus] = useState({ type: "", message: "" }); - const [passLoading, setPassLoading] = useState(false); - const [dbLoading, setDbLoading] = useState(false); - const [dbStatus, setDbStatus] = useState({ type: "", message: "" }); - const importFileRef = useRef(null); - const [proxyForm, setProxyForm] = useState({ - outboundProxyEnabled: false, - outboundProxyUrl: "", - outboundNoProxy: "", - }); - const [proxyStatus, setProxyStatus] = useState({ type: "", message: "" }); - const [proxyLoading, setProxyLoading] = useState(false); - const [proxyTestLoading, setProxyTestLoading] = useState(false); - - useEffect(() => { - fetch("/api/settings") - .then((res) => res.json()) - .then((data) => { - setSettings(data); - setProxyForm({ - outboundProxyEnabled: data?.outboundProxyEnabled === true, - outboundProxyUrl: data?.outboundProxyUrl || "", - outboundNoProxy: data?.outboundNoProxy || "", - }); - setLoading(false); - }) - .catch((err) => { - console.error("Failed to fetch settings:", err); - setLoading(false); - }); - }, []); - - const updateOutboundProxy = async (e) => { - e.preventDefault(); - if (settings.outboundProxyEnabled !== true) return; - setProxyLoading(true); - setProxyStatus({ type: "", message: "" }); - - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - outboundProxyUrl: proxyForm.outboundProxyUrl, - outboundNoProxy: proxyForm.outboundNoProxy, - }), - }); - - const data = await res.json(); - if (res.ok) { - setSettings((prev) => ({ ...prev, ...data })); - setProxyStatus({ type: "success", message: "Proxy settings applied" }); - } else { - setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" }); - } - } catch (err) { - setProxyStatus({ type: "error", message: "An error occurred" }); - } finally { - setProxyLoading(false); - } - }; - - const testOutboundProxy = async () => { - if (settings.outboundProxyEnabled !== true) return; - - const proxyUrl = (proxyForm.outboundProxyUrl || "").trim(); - if (!proxyUrl) { - setProxyStatus({ type: "error", message: "Please enter a Proxy URL to test" }); - return; - } - - setProxyTestLoading(true); - setProxyStatus({ type: "", message: "" }); - - try { - const res = await fetch("/api/settings/proxy-test", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ proxyUrl }), - }); - - const data = await res.json(); - if (res.ok && data?.ok) { - setProxyStatus({ - type: "success", - message: `Proxy test OK (${data.status}) in ${data.elapsedMs}ms`, - }); - } else { - setProxyStatus({ - type: "error", - message: data?.error || "Proxy test failed", - }); - } - } catch (err) { - setProxyStatus({ type: "error", message: "An error occurred" }); - } finally { - setProxyTestLoading(false); - } - }; - - const updateOutboundProxyEnabled = async (outboundProxyEnabled) => { - setProxyLoading(true); - setProxyStatus({ type: "", message: "" }); - - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ outboundProxyEnabled }), - }); - - const data = await res.json(); - if (res.ok) { - setSettings((prev) => ({ ...prev, ...data })); - setProxyForm((prev) => ({ ...prev, outboundProxyEnabled: data?.outboundProxyEnabled === true })); - setProxyStatus({ - type: "success", - message: outboundProxyEnabled ? "Proxy enabled" : "Proxy disabled", - }); - } else { - setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" }); - } - } catch (err) { - setProxyStatus({ type: "error", message: "An error occurred" }); - } finally { - setProxyLoading(false); - } - }; - - const handlePasswordChange = async (e) => { - e.preventDefault(); - if (passwords.new !== passwords.confirm) { - setPassStatus({ type: "error", message: "Passwords do not match" }); - return; - } - - setPassLoading(true); - setPassStatus({ type: "", message: "" }); - - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - currentPassword: passwords.current, - newPassword: passwords.new, - }), - }); - - const data = await res.json(); - - if (res.ok) { - setPassStatus({ type: "success", message: "Password updated successfully" }); - setPasswords({ current: "", new: "", confirm: "" }); - } else { - setPassStatus({ type: "error", message: data.error || "Failed to update password" }); - } - } catch (err) { - setPassStatus({ type: "error", message: "An error occurred" }); - } finally { - setPassLoading(false); - } - }; - - const updateFallbackStrategy = async (strategy) => { - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ fallbackStrategy: strategy }), - }); - if (res.ok) { - setSettings(prev => ({ ...prev, fallbackStrategy: strategy })); - } - } catch (err) { - console.error("Failed to update settings:", err); - } - }; - - const updateStickyLimit = async (limit) => { - const numLimit = parseInt(limit); - if (isNaN(numLimit) || numLimit < 1) return; - - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ stickyRoundRobinLimit: numLimit }), - }); - if (res.ok) { - setSettings(prev => ({ ...prev, stickyRoundRobinLimit: numLimit })); - } - } catch (err) { - console.error("Failed to update sticky limit:", err); - } - }; - - const updateRequireLogin = async (requireLogin) => { - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ requireLogin }), - }); - if (res.ok) { - setSettings(prev => ({ ...prev, requireLogin })); - } - } catch (err) { - console.error("Failed to update require login:", err); - } - }; - - const updateObservabilityEnabled = async (enabled) => { - try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ observabilityEnabled: enabled }), - }); - if (res.ok) { - setSettings(prev => ({ ...prev, observabilityEnabled: enabled })); - } - } catch (err) { - console.error("Failed to update observabilityEnabled:", err); - } - }; - - const reloadSettings = async () => { - try { - const res = await fetch("/api/settings"); - if (!res.ok) return; - const data = await res.json(); - setSettings(data); - } catch (err) { - console.error("Failed to reload settings:", err); - } - }; - - const handleExportDatabase = async () => { - setDbLoading(true); - setDbStatus({ type: "", message: "" }); - try { - const res = await fetch("/api/settings/database"); - if (!res.ok) { - const data = await res.json().catch(() => ({})); - throw new Error(data.error || "Failed to export database"); - } - - const payload = await res.json(); - const content = JSON.stringify(payload, null, 2); - const blob = new Blob([content], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const anchor = document.createElement("a"); - const stamp = new Date().toISOString().replace(/[.:]/g, "-"); - anchor.href = url; - anchor.download = `9router-backup-${stamp}.json`; - document.body.appendChild(anchor); - anchor.click(); - document.body.removeChild(anchor); - URL.revokeObjectURL(url); - - setDbStatus({ type: "success", message: "Database backup downloaded" }); - } catch (err) { - setDbStatus({ type: "error", message: err.message || "Failed to export database" }); - } finally { - setDbLoading(false); - } - }; - - const handleImportDatabase = async (event) => { - const file = event.target.files?.[0]; - if (!file) return; - - setDbLoading(true); - setDbStatus({ type: "", message: "" }); - - try { - const raw = await file.text(); - const payload = JSON.parse(raw); - - const res = await fetch("/api/settings/database", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - const data = await res.json().catch(() => ({})); - if (!res.ok) { - throw new Error(data.error || "Failed to import database"); - } - - await reloadSettings(); - setDbStatus({ type: "success", message: "Database imported successfully" }); - } catch (err) { - setDbStatus({ type: "error", message: err.message || "Invalid backup file" }); - } finally { - if (importFileRef.current) { - importFileRef.current.value = ""; - } - setDbLoading(false); - } - }; - - const observabilityEnabled = settings.observabilityEnabled === true; - - return ( -
    -
    - {/* Local Mode Info */} - -
    -
    -
    - computer -
    -
    -

    Local Mode

    -

    Running on your machine

    -
    -
    -
    - {["light", "dark", "system"].map((option) => ( - - ))} -
    -
    -
    -
    -
    -

    Database Location

    -

    ~/.9router/db.json

    -
    -
    -
    - - - -
    - {dbStatus.message && ( -

    - {dbStatus.message} -

    - )} -
    -
    - - {/* Security */} - -
    -
    - shield -
    -

    Security

    -
    -
    -
    -
    -

    Require login

    -

    - When ON, dashboard requires password. When OFF, access without login. -

    -
    - updateRequireLogin(!settings.requireLogin)} - disabled={loading} - /> -
    - {settings.requireLogin === true && ( -
    - {settings.hasPassword && ( -
    - - setPasswords({ ...passwords, current: e.target.value })} - required - /> -
    - )} - {/* {!settings.hasPassword && ( -
    -

    - Setting password for the first time. Leave current password empty or use default: 123456 -

    -
    - )} */} -
    -
    - - setPasswords({ ...passwords, new: e.target.value })} - required - /> -
    -
    - - setPasswords({ ...passwords, confirm: e.target.value })} - required - /> -
    -
    - - {passStatus.message && ( -

    - {passStatus.message} -

    - )} - -
    - -
    -
    - )} -
    -
    - - {/* Routing Preferences */} - -
    -
    - route -
    -

    Routing Strategy

    -
    -
    -
    -
    -

    Round Robin

    -

    - Cycle through accounts to distribute load -

    -
    - updateFallbackStrategy(settings.fallbackStrategy === "round-robin" ? "fill-first" : "round-robin")} - disabled={loading} - /> -
    - - {/* Sticky Round Robin Limit */} - {settings.fallbackStrategy === "round-robin" && ( -
    -
    -

    Sticky Limit

    -

    - Calls per account before switching -

    -
    - updateStickyLimit(e.target.value)} - disabled={loading} - className="w-20 text-center" - /> -
    - )} - -

    - {settings.fallbackStrategy === "round-robin" - ? `Currently distributing requests across all available accounts with ${settings.stickyRoundRobinLimit || 3} calls per account.` - : "Currently using accounts in priority order (Fill First)."} -

    -
    -
    - - {/* Network */} - -
    -
    - wifi -
    -

    Network

    -
    - -
    -
    -
    -

    Outbound Proxy

    -

    Enable proxy for OAuth + provider outbound requests.

    -
    - updateOutboundProxyEnabled(!(settings.outboundProxyEnabled === true))} - disabled={loading || proxyLoading} - /> -
    - - {settings.outboundProxyEnabled === true && ( -
    -
    - - setProxyForm((prev) => ({ ...prev, outboundProxyUrl: e.target.value }))} - disabled={loading || proxyLoading} - /> -

    Leave empty to inherit existing env proxy (if any).

    -
    - -
    - - setProxyForm((prev) => ({ ...prev, outboundNoProxy: e.target.value }))} - disabled={loading || proxyLoading} - /> -

    Comma-separated hostnames/domains to bypass the proxy.

    -
    - -
    - - -
    -
    - )} - - {proxyStatus.message && ( -

    - {proxyStatus.message} -

    - )} -
    -
    - - {/* Observability Settings */} - -
    -
    - monitoring -
    -

    Observability

    -
    -
    -
    -

    Enable Observability

    -

    - Record request details for inspection in the logs view -

    -
    - -
    -
    - - {/* App Info */} -
    -

    {APP_CONFIG.name} v{APP_CONFIG.version}

    -

    Local Mode - All data stored on your machine

    -
    -
    -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/providers/[id]/page.js b/9router/src/app/(dashboard)/dashboard/providers/[id]/page.js deleted file mode 100644 index 70a12ee7..00000000 --- a/9router/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ /dev/null @@ -1,2235 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback, useRef } from "react"; -import PropTypes from "prop-types"; -import { useParams, useRouter } from "next/navigation"; -import Link from "next/link"; -import Image from "next/image"; -import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, Toggle, Select } from "@/shared/components"; -import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; -import { getModelsByProviderId } from "@/shared/constants/models"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; - -export default function ProviderDetailPage() { - const params = useParams(); - const router = useRouter(); - const providerId = params.id; - const [connections, setConnections] = useState([]); - const [loading, setLoading] = useState(true); - const [providerNode, setProviderNode] = useState(null); - const [proxyPools, setProxyPools] = useState([]); - const [showOAuthModal, setShowOAuthModal] = useState(false); - const [showIFlowCookieModal, setShowIFlowCookieModal] = useState(false); - const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [showEditNodeModal, setShowEditNodeModal] = useState(false); - const [showBulkProxyModal, setShowBulkProxyModal] = useState(false); - const [selectedConnection, setSelectedConnection] = useState(null); - const [modelAliases, setModelAliases] = useState({}); - const [headerImgError, setHeaderImgError] = useState(false); - const [modelTestResults, setModelTestResults] = useState({}); - const [modelsTestError, setModelsTestError] = useState(""); - const [testingModelId, setTestingModelId] = useState(null); - const [showAddCustomModel, setShowAddCustomModel] = useState(false); - const [selectedConnectionIds, setSelectedConnectionIds] = useState([]); - const [bulkProxyPoolId, setBulkProxyPoolId] = useState("__none__"); - const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false); - const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override - const [providerStickyLimit, setProviderStickyLimit] = useState(""); - const { copied, copy } = useCopyToClipboard(); - - const providerInfo = providerNode - ? { - id: providerNode.id, - name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"), - color: providerNode.type === "anthropic-compatible" ? "#D97757" : "#10A37F", - textIcon: providerNode.type === "anthropic-compatible" ? "AC" : "OC", - apiType: providerNode.apiType, - baseUrl: providerNode.baseUrl, - type: providerNode.type, - } - : (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId]); - const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId]; - const models = getModelsByProviderId(providerId); - const providerAlias = getProviderAlias(providerId); - - const isOpenAICompatible = isOpenAICompatibleProvider(providerId); - const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId); - const isCompatible = isOpenAICompatible || isAnthropicCompatible; - - const providerStorageAlias = isCompatible ? providerId : providerAlias; - const providerDisplayAlias = isCompatible - ? (providerNode?.prefix || providerId) - : providerAlias; - - // Define callbacks BEFORE the useEffect that uses them - const fetchAliases = useCallback(async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) { - setModelAliases(data.aliases || {}); - } - } catch (error) { - console.log("Error fetching aliases:", error); - } - }, []); - - const fetchConnections = useCallback(async () => { - try { - const [connectionsRes, nodesRes, proxyPoolsRes, settingsRes] = await Promise.all([ - fetch("/api/providers", { cache: "no-store" }), - fetch("/api/provider-nodes", { cache: "no-store" }), - fetch("/api/proxy-pools?isActive=true", { cache: "no-store" }), - fetch("/api/settings", { cache: "no-store" }), - ]); - const connectionsData = await connectionsRes.json(); - const nodesData = await nodesRes.json(); - const proxyPoolsData = await proxyPoolsRes.json(); - const settingsData = settingsRes.ok ? await settingsRes.json() : {}; - if (connectionsRes.ok) { - const filtered = (connectionsData.connections || []).filter(c => c.provider === providerId); - setConnections(filtered); - } - if (proxyPoolsRes.ok) { - setProxyPools(proxyPoolsData.proxyPools || []); - } - // Load per-provider strategy override - const override = (settingsData.providerStrategies || {})[providerId] || {}; - setProviderStrategy(override.fallbackStrategy || null); - setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1"); - if (nodesRes.ok) { - let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null; - - // Newly created compatible nodes can be briefly unavailable on one worker. - // Retry a few times before showing "Provider not found". - if (!node && isCompatible) { - for (let attempt = 0; attempt < 3; attempt += 1) { - await new Promise((resolve) => setTimeout(resolve, 150)); - const retryRes = await fetch("/api/provider-nodes", { cache: "no-store" }); - if (!retryRes.ok) continue; - const retryData = await retryRes.json(); - node = (retryData.nodes || []).find((entry) => entry.id === providerId) || null; - if (node) break; - } - } - - setProviderNode(node); - } - } catch (error) { - console.log("Error fetching connections:", error); - } finally { - setLoading(false); - } - }, [providerId, isCompatible]); - - const handleUpdateNode = async (formData) => { - try { - const res = await fetch(`/api/provider-nodes/${providerId}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - const data = await res.json(); - if (res.ok) { - setProviderNode(data.node); - await fetchConnections(); - setShowEditNodeModal(false); - } - } catch (error) { - console.log("Error updating provider node:", error); - } - }; - - const saveProviderStrategy = async (strategy, stickyLimit) => { - try { - const settingsRes = await fetch("/api/settings", { cache: "no-store" }); - const settingsData = settingsRes.ok ? await settingsRes.json() : {}; - const current = settingsData.providerStrategies || {}; - - // Build override: null strategy means remove override, use global - const override = {}; - if (strategy) override.fallbackStrategy = strategy; - if (strategy === "round-robin" && stickyLimit !== "") { - override.stickyRoundRobinLimit = Number(stickyLimit) || 3; - } - - const updated = { ...current }; - if (Object.keys(override).length === 0) { - delete updated[providerId]; - } else { - updated[providerId] = override; - } - - await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ providerStrategies: updated }), - }); - } catch (error) { - console.log("Error saving provider strategy:", error); - } - }; - - const handleRoundRobinToggle = (enabled) => { - const strategy = enabled ? "round-robin" : null; - const sticky = enabled ? (providerStickyLimit || "1") : providerStickyLimit; - if (enabled && !providerStickyLimit) setProviderStickyLimit("1"); - setProviderStrategy(strategy); - saveProviderStrategy(strategy, sticky); - }; - - const handleStickyLimitChange = (value) => { - setProviderStickyLimit(value); - saveProviderStrategy("round-robin", value); - }; - - useEffect(() => { - fetchConnections(); - fetchAliases(); - }, [fetchConnections, fetchAliases]); - - const handleSetAlias = async (modelId, alias, providerAliasOverride = providerAlias) => { - const fullModel = `${providerAliasOverride}/${modelId}`; - try { - const res = await fetch("/api/models/alias", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: fullModel, alias }), - }); - if (res.ok) { - await fetchAliases(); - } else { - const data = await res.json(); - alert(data.error || "Failed to set alias"); - } - } catch (error) { - console.log("Error setting alias:", error); - } - }; - - const handleDeleteAlias = async (alias) => { - try { - const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, { - method: "DELETE", - }); - if (res.ok) { - await fetchAliases(); - } - } catch (error) { - console.log("Error deleting alias:", error); - } - }; - - const handleDelete = async (id) => { - if (!confirm("Delete this connection?")) return; - try { - const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); - if (res.ok) { - setConnections(connections.filter(c => c.id !== id)); - } - } catch (error) { - console.log("Error deleting connection:", error); - } - }; - - const handleOAuthSuccess = () => { - fetchConnections(); - setShowOAuthModal(false); - }; - - const handleIFlowCookieSuccess = () => { - fetchConnections(); - setShowIFlowCookieModal(false); - }; - - const handleSaveApiKey = async (formData) => { - try { - const res = await fetch("/api/providers", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: providerId, ...formData }), - }); - if (res.ok) { - await fetchConnections(); - setShowAddApiKeyModal(false); - } - } catch (error) { - console.log("Error saving connection:", error); - } - }; - - const handleUpdateConnection = async (formData) => { - try { - const res = await fetch(`/api/providers/${selectedConnection.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - if (res.ok) { - await fetchConnections(); - setShowEditModal(false); - } - } catch (error) { - console.log("Error updating connection:", error); - } - }; - - const handleUpdateConnectionStatus = async (id, isActive) => { - try { - const res = await fetch(`/api/providers/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ isActive }), - }); - if (res.ok) { - setConnections(prev => prev.map(c => c.id === id ? { ...c, isActive } : c)); - } - } catch (error) { - console.log("Error updating connection status:", error); - } - }; - - const handleSwapPriority = async (index1, index2) => { - // Optimistic update state - const newConnections = [...connections]; - [newConnections[index1], newConnections[index2]] = [newConnections[index2], newConnections[index1]]; - setConnections(newConnections); - - try { - await Promise.all([ - fetch(`/api/providers/${newConnections[index1].id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: index1 }), - }), - fetch(`/api/providers/${newConnections[index2].id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: index2 }), - }), - ]); - } catch (error) { - console.log("Error swapping priority:", error); - await fetchConnections(); - } - }; - - const selectedConnections = connections.filter((conn) => selectedConnectionIds.includes(conn.id)); - const allSelected = connections.length > 0 && selectedConnectionIds.length === connections.length; - - const toggleSelectConnection = (connectionId) => { - setSelectedConnectionIds((prev) => ( - prev.includes(connectionId) - ? prev.filter((id) => id !== connectionId) - : [...prev, connectionId] - )); - }; - - const toggleSelectAllConnections = () => { - if (allSelected) { - setSelectedConnectionIds([]); - return; - } - setSelectedConnectionIds(connections.map((conn) => conn.id)); - }; - - const clearSelection = () => { - setSelectedConnectionIds([]); - setBulkProxyPoolId("__none__"); - }; - - useEffect(() => { - setSelectedConnectionIds((prev) => prev.filter((id) => connections.some((conn) => conn.id === id))); - }, [connections]); - - const selectedProxySummary = (() => { - if (selectedConnections.length === 0) return ""; - const poolIds = new Set(selectedConnections.map((conn) => conn.providerSpecificData?.proxyPoolId || "__none__")); - if (poolIds.size === 1) { - const onlyId = [...poolIds][0]; - if (onlyId === "__none__") return "All selected currently unbound"; - const pool = proxyPools.find((p) => p.id === onlyId); - return `All selected currently bound to ${pool?.name || onlyId}`; - } - return "Selected connections have mixed proxy bindings"; - })(); - - const openBulkProxyModal = () => { - if (selectedConnections.length === 0) return; - const uniquePoolIds = [...new Set(selectedConnections.map((conn) => conn.providerSpecificData?.proxyPoolId || "__none__"))]; - setBulkProxyPoolId(uniquePoolIds.length === 1 ? uniquePoolIds[0] : "__none__"); - setShowBulkProxyModal(true); - }; - - const closeBulkProxyModal = () => { - if (bulkUpdatingProxy) return; - setShowBulkProxyModal(false); - }; - - const handleBulkApplyProxyPool = async () => { - if (selectedConnectionIds.length === 0) return; - - const proxyPoolId = bulkProxyPoolId === "__none__" ? null : bulkProxyPoolId; - setBulkUpdatingProxy(true); - try { - const results = []; - for (const connectionId of selectedConnectionIds) { - try { - const res = await fetch(`/api/providers/${connectionId}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ proxyPoolId }), - }); - results.push(res.ok); - } catch (e) { - console.log("Error applying bulk proxy pool for", connectionId, e); - results.push(false); - } - } - - const failedCount = results.filter((ok) => !ok).length; - if (failedCount > 0) { - alert(`Updated with ${failedCount} failed request(s).`); - } - - await fetchConnections(); - clearSelection(); - setShowBulkProxyModal(false); - } catch (error) { - console.log("Error applying bulk proxy pool:", error); - } finally { - setBulkUpdatingProxy(false); - } - }; - - - const isSelected = (connectionId) => selectedConnectionIds.includes(connectionId); - - const connectionsList = ( -
    - {connections - .map((conn, index) => ( -
    -
    - handleSwapPriority(index, index - 1)} - onMoveDown={() => handleSwapPriority(index, index + 1)} - onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)} - onUpdateProxy={async (proxyPoolId) => { - try { - const res = await fetch(`/api/providers/${conn.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ proxyPoolId: proxyPoolId || null }), - }); - if (res.ok) { - setConnections(prev => prev.map(c => - c.id === conn.id - ? { ...c, providerSpecificData: { ...c.providerSpecificData, proxyPoolId: proxyPoolId || null } } - : c - )); - } - } catch (error) { - console.log("Error updating proxy:", error); - } - }} - onEdit={() => { - setSelectedConnection(conn); - setShowEditModal(true); - }} - onDelete={() => handleDelete(conn.id)} - /> -
    -
    - ))} -
    - ); - - const bulkProxyOptions = [ - { value: "__none__", label: "None" }, - ...proxyPools.map((pool) => ({ value: pool.id, label: pool.name })), - ]; - - const bulkHint = selectedConnectionIds.length === 0 - ? "Select one or more connections, then click Proxy Action." - : selectedProxySummary; - - const canApplyBulkProxy = selectedConnectionIds.length > 0 && !bulkUpdatingProxy; - - const bulkActionModal = ( - -
    - handleStickyLimitChange(e.target.value)} - placeholder="1" - className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary" - /> -
    - )} - - - - {connections.length === 0 ? ( -
    -
    - {isOAuth ? "lock" : "key"} -
    -

    No connections yet

    -

    Add your first connection to get started

    - {!isCompatible && ( -
    - {providerId === "iflow" && ( - - )} - -
    - )} -
    - ) : ( - <> - {connectionsList} - {!isCompatible && ( -
    - {providerId === "iflow" && ( - - )} - -
    - )} - - )} - - - {/* Models */} - -
    -

    - {providerInfo.passthroughModels ? "Model Aliases" : "Available Models"} -

    -
    - {!!modelsTestError && ( -

    {modelsTestError}

    - )} - {renderModelsSection()} -
    - - {bulkActionModal} - - {/* Modals */} - {providerId === "kiro" ? ( - setShowOAuthModal(false)} - /> - ) : providerId === "cursor" ? ( - setShowOAuthModal(false)} - /> - ) : ( - setShowOAuthModal(false)} - /> - )} - {providerId === "iflow" && ( - setShowIFlowCookieModal(false)} - /> - )} - setShowAddApiKeyModal(false)} - /> - setShowEditModal(false)} - /> - {isCompatible && ( - setShowEditNodeModal(false)} - isAnthropic={isAnthropicCompatible} - /> - )} - {!isCompatible && !providerInfo?.passthroughModels && ( - { - await handleSetAlias(modelId, modelId, providerStorageAlias); - setShowAddCustomModel(false); - }} - onClose={() => setShowAddCustomModel(false)} - /> - )} - - ); -} - -function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, onDeleteAlias, onTest, isTesting }) { - const borderColor = testStatus === "ok" - ? "border-green-500/40" - : testStatus === "error" - ? "border-red-500/40" - : "border-border"; - - const iconColor = testStatus === "ok" - ? "#22c55e" - : testStatus === "error" - ? "#ef4444" - : undefined; - - return ( -
    -
    - - {testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"} - - {fullModel} - {onTest && ( - - )} - - {isCustom && ( - - )} -
    -
    - ); -} - -ModelRow.propTypes = { - model: PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired, - fullModel: PropTypes.string.isRequired, - alias: PropTypes.string, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - testStatus: PropTypes.oneOf(["ok", "error"]), - isCustom: PropTypes.bool, - onDeleteAlias: PropTypes.func, - onTest: PropTypes.func, - isTesting: PropTypes.bool, -}; - -function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) { - const [newModel, setNewModel] = useState(""); - const [adding, setAdding] = useState(false); - - // Filter aliases for this provider - models are persisted via alias - const providerAliases = Object.entries(modelAliases).filter( - ([, model]) => model.startsWith(`${providerAlias}/`) - ); - - const allModels = providerAliases.map(([alias, fullModel]) => ({ - modelId: fullModel.replace(`${providerAlias}/`, ""), - fullModel, - alias, - })); - - // Generate default alias from modelId (last part after /) - const generateDefaultAlias = (modelId) => { - const parts = modelId.split("/"); - return parts[parts.length - 1]; - }; - - const handleAdd = async () => { - if (!newModel.trim() || adding) return; - const modelId = newModel.trim(); - const defaultAlias = generateDefaultAlias(modelId); - - // Check if alias already exists - if (modelAliases[defaultAlias]) { - alert(`Alias "${defaultAlias}" already exists. Please use a different model or edit existing alias.`); - return; - } - - setAdding(true); - try { - await onSetAlias(modelId, defaultAlias); - setNewModel(""); - } catch (error) { - console.log("Error adding model:", error); - } finally { - setAdding(false); - } - }; - - return ( -
    -

    - OpenRouter supports any model. Add models and create aliases for quick access. -

    - - {/* Add new model */} -
    -
    - - setNewModel(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder="anthropic/claude-3-opus" - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> -
    - -
    - - {/* Models list */} - {allModels.length > 0 && ( -
    - {allModels.map(({ modelId, fullModel, alias }) => ( - onDeleteAlias(alias)} - /> - ))} -
    - )} -
    - ); -} - -PassthroughModelsSection.propTypes = { - providerAlias: PropTypes.string.isRequired, - modelAliases: PropTypes.object.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onSetAlias: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, -}; - -function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias, onTest, testStatus, isTesting }) { - const borderColor = testStatus === "ok" - ? "border-green-500/40" - : testStatus === "error" - ? "border-red-500/40" - : "border-border"; - - const iconColor = testStatus === "ok" - ? "#22c55e" - : testStatus === "error" - ? "#ef4444" - : undefined; - - return ( -
    - - {testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"} - - -
    -

    {modelId}

    - -
    - {fullModel} - - {onTest && ( - - )} -
    -
    - - {/* Delete button */} - -
    - ); -} - -PassthroughModelRow.propTypes = { - modelId: PropTypes.string.isRequired, - fullModel: PropTypes.string.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, - onTest: PropTypes.func, - testStatus: PropTypes.oneOf(["ok", "error"]), - isTesting: PropTypes.bool, -}; - -function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) { - const [newModel, setNewModel] = useState(""); - const [adding, setAdding] = useState(false); - const [importing, setImporting] = useState(false); - const [testingModelId, setTestingModelId] = useState(null); - const [modelTestResults, setModelTestResults] = useState({}); - - const handleTestModel = async (modelId) => { - if (testingModelId) return; - setTestingModelId(modelId); - try { - const res = await fetch("/api/models/test", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: `${providerStorageAlias}/${modelId}` }), - }); - const data = await res.json(); - setModelTestResults((prev) => ({ ...prev, [modelId]: data.ok ? "ok" : "error" })); - } catch { - setModelTestResults((prev) => ({ ...prev, [modelId]: "error" })); - } finally { - setTestingModelId(null); - } - }; - - const providerAliases = Object.entries(modelAliases).filter( - ([, model]) => model.startsWith(`${providerStorageAlias}/`) - ); - - const allModels = providerAliases.map(([alias, fullModel]) => ({ - modelId: fullModel.replace(`${providerStorageAlias}/`, ""), - fullModel, - alias, - })); - - const generateDefaultAlias = (modelId) => { - const parts = modelId.split("/"); - return parts[parts.length - 1]; - }; - - const resolveAlias = (modelId) => { - const baseAlias = generateDefaultAlias(modelId); - if (!modelAliases[baseAlias]) return baseAlias; - const prefixedAlias = `${providerDisplayAlias}-${baseAlias}`; - if (!modelAliases[prefixedAlias]) return prefixedAlias; - return null; - }; - - const handleAdd = async () => { - if (!newModel.trim() || adding) return; - const modelId = newModel.trim(); - const resolvedAlias = resolveAlias(modelId); - if (!resolvedAlias) { - alert("All suggested aliases already exist. Please choose a different model or remove conflicting aliases."); - return; - } - - setAdding(true); - try { - await onSetAlias(modelId, resolvedAlias, providerStorageAlias); - setNewModel(""); - } catch (error) { - console.log("Error adding model:", error); - } finally { - setAdding(false); - } - }; - - const handleImport = async () => { - if (importing) return; - const activeConnection = connections.find((conn) => conn.isActive !== false); - if (!activeConnection) return; - - setImporting(true); - try { - const res = await fetch(`/api/providers/${activeConnection.id}/models`); - const data = await res.json(); - if (!res.ok) { - alert(data.error || "Failed to import models"); - return; - } - const models = data.models || []; - if (models.length === 0) { - alert("No models returned from /models."); - return; - } - let importedCount = 0; - for (const model of models) { - const modelId = model.id || model.name || model.model; - if (!modelId) continue; - const resolvedAlias = resolveAlias(modelId); - if (!resolvedAlias) continue; - await onSetAlias(modelId, resolvedAlias, providerStorageAlias); - importedCount += 1; - } - if (importedCount === 0) { - alert("No new models were added."); - } - } catch (error) { - console.log("Error importing models:", error); - } finally { - setImporting(false); - } - }; - - const canImport = connections.some((conn) => conn.isActive !== false); - - return ( -
    -

    - Add {isAnthropic ? "Anthropic" : "OpenAI"}-compatible models manually or import them from the /models endpoint. -

    - -
    -
    - - setNewModel(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder={isAnthropic ? "claude-3-opus-20240229" : "gpt-4o"} - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> -
    - - -
    - - {!canImport && ( -

    - Add a connection to enable importing models. -

    - )} - - {allModels.length > 0 && ( -
    - {allModels.map(({ modelId, fullModel, alias }) => ( - onDeleteAlias(alias)} - onTest={connections.length > 0 ? () => handleTestModel(modelId) : undefined} - testStatus={modelTestResults[modelId]} - isTesting={testingModelId === modelId} - /> - ))} -
    - )} -
    - ); -} - -CompatibleModelsSection.propTypes = { - providerStorageAlias: PropTypes.string.isRequired, - providerDisplayAlias: PropTypes.string.isRequired, - modelAliases: PropTypes.object.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onSetAlias: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, - connections: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - isActive: PropTypes.bool, - })).isRequired, - isAnthropic: PropTypes.bool, -}; - -function CooldownTimer({ until }) { - const [remaining, setRemaining] = useState(""); - - useEffect(() => { - const updateRemaining = () => { - const diff = new Date(until).getTime() - Date.now(); - if (diff <= 0) { - setRemaining(""); - return; - } - const secs = Math.floor(diff / 1000); - if (secs < 60) { - setRemaining(`${secs}s`); - } else if (secs < 3600) { - setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`); - } else { - const hrs = Math.floor(secs / 3600); - const mins = Math.floor((secs % 3600) / 60); - setRemaining(`${hrs}h ${mins}m`); - } - }; - - updateRemaining(); - const interval = setInterval(updateRemaining, 1000); - return () => clearInterval(interval); - }, [until]); - - if (!remaining) return null; - - return ( - - ⏱ {remaining} - - ); -} - -CooldownTimer.propTypes = { - until: PropTypes.string.isRequired, -}; - -function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete }) { - const [showProxyDropdown, setShowProxyDropdown] = useState(false); - const [updatingProxy, setUpdatingProxy] = useState(false); - const proxyDropdownRef = useRef(null); - - const proxyPoolMap = new Map((proxyPools || []).map((pool) => [pool.id, pool])); - const boundProxyPoolId = connection.providerSpecificData?.proxyPoolId || null; - const boundProxyPool = boundProxyPoolId ? proxyPoolMap.get(boundProxyPoolId) : null; - const hasLegacyProxy = connection.providerSpecificData?.connectionProxyEnabled === true && !!connection.providerSpecificData?.connectionProxyUrl; - const hasAnyProxy = !!boundProxyPoolId || hasLegacyProxy; - const proxyDisplayText = boundProxyPool - ? `Pool: ${boundProxyPool.name}` - : boundProxyPoolId - ? `Pool: ${boundProxyPoolId} (inactive/missing)` - : hasLegacyProxy - ? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}` - : ""; - - let maskedProxyUrl = ""; - if (boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl) { - const rawProxyUrl = boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl; - try { - const parsed = new URL(rawProxyUrl); - maskedProxyUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`; - } catch { - maskedProxyUrl = rawProxyUrl; - } - } - - const noProxyText = boundProxyPool?.noProxy || connection.providerSpecificData?.connectionNoProxy || ""; - - let proxyBadgeVariant = "default"; - if (boundProxyPool?.isActive === true) { - proxyBadgeVariant = "success"; - } else if (boundProxyPoolId || hasLegacyProxy) { - proxyBadgeVariant = "error"; - } - - // Close dropdown when clicking outside - useEffect(() => { - if (!showProxyDropdown) return; - const handler = (e) => { - if (proxyDropdownRef.current && !proxyDropdownRef.current.contains(e.target)) { - setShowProxyDropdown(false); - } - }; - document.addEventListener("mousedown", handler); - return () => document.removeEventListener("mousedown", handler); - }, [showProxyDropdown]); - - const handleSelectProxy = async (poolId) => { - setUpdatingProxy(true); - try { - await onUpdateProxy(poolId === "__none__" ? null : poolId); - } finally { - setUpdatingProxy(false); - setShowProxyDropdown(false); - } - }; - - const displayName = isOAuth - ? connection.name || connection.email || connection.displayName || "OAuth Account" - : connection.name; - - // Use useState + useEffect for impure Date.now() to avoid calling during render - const [isCooldown, setIsCooldown] = useState(false); - - // Get earliest model lock timestamp (useEffect handles the Date.now() comparison) - const modelLockUntil = Object.entries(connection) - .filter(([k]) => k.startsWith("modelLock_")) - .map(([, v]) => v) - .filter(v => !!v) - .sort()[0] || null; - - useEffect(() => { - const checkCooldown = () => { - const until = Object.entries(connection) - .filter(([k]) => k.startsWith("modelLock_")) - .map(([, v]) => v) - .filter(v => v && new Date(v).getTime() > Date.now()) - .sort()[0] || null; - setIsCooldown(!!until); - }; - - checkCooldown(); - const interval = modelLockUntil ? setInterval(checkCooldown, 1000) : null; - return () => { - if (interval) clearInterval(interval); - }; - }, [modelLockUntil]); - - // Determine effective status (override unavailable if cooldown expired) - const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown) - ? "active" // Cooldown expired → treat as active - : connection.testStatus; - - const getStatusVariant = () => { - if (connection.isActive === false) return "default"; - if (effectiveStatus === "active" || effectiveStatus === "success") return "success"; - if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error"; - return "default"; - }; - - return ( -
    -
    - {/* Priority arrows */} -
    - - -
    - - {isOAuth ? "lock" : "key"} - -
    -

    {displayName}

    -
    - - {connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")} - - {hasAnyProxy && ( - - Proxy - - )} - {isCooldown && connection.isActive !== false && } - {connection.lastError && connection.isActive !== false && ( - - {connection.lastError} - - )} - #{connection.priority} - {connection.globalPriority && ( - Auto: {connection.globalPriority} - )} -
    - {hasAnyProxy && ( -
    - - {proxyDisplayText} - - {maskedProxyUrl && ( - - {maskedProxyUrl} - - )} - {noProxyText && ( - - no_proxy: {noProxyText} - - )} -
    - )} -
    -
    -
    -
    - {/* Proxy button with inline dropdown */} - {(proxyPools || []).length > 0 && ( -
    - - {showProxyDropdown && ( -
    - - {(proxyPools || []).map((pool) => ( - - ))} -
    - )} -
    - )} - - -
    - -
    -
    - ); -} - -ConnectionRow.propTypes = { - connection: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - email: PropTypes.string, - displayName: PropTypes.string, - modelLockUntil: PropTypes.string, - testStatus: PropTypes.string, - isActive: PropTypes.bool, - lastError: PropTypes.string, - priority: PropTypes.number, - globalPriority: PropTypes.number, - }).isRequired, - proxyPools: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - proxyUrl: PropTypes.string, - noProxy: PropTypes.string, - isActive: PropTypes.bool, - })), - isOAuth: PropTypes.bool.isRequired, - isFirst: PropTypes.bool.isRequired, - isLast: PropTypes.bool.isRequired, - onMoveUp: PropTypes.func.isRequired, - onMoveDown: PropTypes.func.isRequired, - onToggleActive: PropTypes.func.isRequired, - onUpdateProxy: PropTypes.func, - onEdit: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, -}; - -function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, proxyPools, onSave, onClose }) { - const NONE_PROXY_POOL_VALUE = "__none__"; - - const [formData, setFormData] = useState({ - name: "", - apiKey: "", - priority: 1, - proxyPoolId: NONE_PROXY_POOL_VALUE, - }); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - const [saving, setSaving] = useState(false); - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - const handleSubmit = async () => { - if (!provider || !formData.apiKey) return; - - setSaving(true); - try { - let isValid = false; - try { - setValidating(true); - setValidationResult(null); - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - isValid = !!data.valid; - setValidationResult(isValid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - - await onSave({ - name: formData.name, - apiKey: formData.apiKey, - priority: formData.priority, - proxyPoolId: formData.proxyPoolId === NONE_PROXY_POOL_VALUE ? null : formData.proxyPoolId, - testStatus: isValid ? "active" : "unknown", - providerSpecificData: undefined - }); - } finally { - setSaving(false); - } - }; - - if (!provider) return null; - - return ( - -
    - setFormData({ ...formData, name: e.target.value })} - placeholder="Production Key" - /> -
    - setFormData({ ...formData, apiKey: e.target.value })} - className="flex-1" - /> -
    - -
    -
    - {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} - {isCompatible && ( -

    - {isAnthropic - ? `Validation checks ${providerName || "Anthropic Compatible"} by verifying the API key.` - : `Validation checks ${providerName || "OpenAI Compatible"} via /models on your base URL.` - } -

    - )} - setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} - /> - - setFormData({ ...formData, name: e.target.value })} - placeholder={isOAuth ? "Account name" : "Production Key"} - /> - {isOAuth && connection.email && ( -
    -

    Email

    -

    {connection.email}

    -
    - )} - setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} - /> - - {!isOAuth && ( - <> -
    - setFormData({ ...formData, apiKey: e.target.value })} - placeholder="Enter new API key" - hint="Leave blank to keep the current API key." - className="flex-1" - /> -
    - -
    -
    - {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} - - )} - - {/* Test Connection */} - {!isCompatible && ( -
    - - {testResult && ( - - {testResult === "success" ? "Valid" : "Failed"} - - )} -
    - )} - -
    - - -
    -
    -
    - ); -} - -EditConnectionModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - connection: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - email: PropTypes.string, - priority: PropTypes.number, - authType: PropTypes.string, - provider: PropTypes.string, - providerSpecificData: PropTypes.object, - }), - proxyPools: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - })), - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - -function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) { - const [formData, setFormData] = useState({ - name: "", - prefix: "", - apiType: "chat", - baseUrl: "https://api.openai.com/v1", - }); - const [saving, setSaving] = useState(false); - const [checkKey, setCheckKey] = useState(""); - const [checkModelId, setCheckModelId] = useState(""); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - - useEffect(() => { - if (node) { - setFormData({ - name: node.name || "", - prefix: node.prefix || "", - apiType: node.apiType || "chat", - baseUrl: node.baseUrl || (isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"), - }); - } - }, [node, isAnthropic]); - - const apiTypeOptions = [ - { value: "chat", label: "Chat Completions" }, - { value: "responses", label: "Responses API" }, - ]; - - const handleSubmit = async () => { - if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return; - setSaving(true); - try { - const payload = { - name: formData.name, - prefix: formData.prefix, - baseUrl: formData.baseUrl, - }; - if (!isAnthropic) { - payload.apiType = formData.apiType; - } - await onSave(payload); - } finally { - setSaving(false); - } - }; - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/provider-nodes/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: formData.baseUrl, - apiKey: checkKey, - type: isAnthropic ? "anthropic-compatible" : "openai-compatible", - modelId: checkModelId.trim() || undefined - }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - if (!node) return null; - - return ( - -
    - setFormData({ ...formData, name: e.target.value })} - placeholder={`${isAnthropic ? "Anthropic" : "OpenAI"} Compatible (Prod)`} - hint="Required. A friendly label for this node." - /> - setFormData({ ...formData, prefix: e.target.value })} - placeholder={isAnthropic ? "ac-prod" : "oc-prod"} - hint="Required. Used as the provider prefix for model IDs." - /> - {!isAnthropic && ( - setFormData({ ...formData, baseUrl: e.target.value })} - placeholder={isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"} - hint={`Use the base URL (ending in /v1) for your ${isAnthropic ? "Anthropic" : "OpenAI"}-compatible API.`} - /> -
    - setCheckKey(e.target.value)} - className="flex-1" - /> -
    - -
    -
    - setCheckModelId(e.target.value)} - placeholder="e.g. my-model-id" - hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead." - /> - {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} -
    - - -
    -
    -
    - ); -} - -EditCompatibleNodeModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - node: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - prefix: PropTypes.string, - apiType: PropTypes.string, - baseUrl: PropTypes.string, - }), - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - isAnthropic: PropTypes.bool, -}; - -function AddCustomModelModal({ isOpen, providerAlias, providerDisplayAlias, onSave, onClose }) { - const [modelId, setModelId] = useState(""); - const [testStatus, setTestStatus] = useState(null); // null | "testing" | "ok" | "error" - const [testError, setTestError] = useState(""); - const [saving, setSaving] = useState(false); - - // Reset state when modal opens - useEffect(() => { - if (isOpen) { setModelId(""); setTestStatus(null); setTestError(""); } - }, [isOpen]); - - const handleTest = async () => { - if (!modelId.trim()) return; - setTestStatus("testing"); - setTestError(""); - try { - const res = await fetch("/api/models/test", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: `${providerAlias}/${modelId.trim()}` }), - }); - const data = await res.json(); - setTestStatus(data.ok ? "ok" : "error"); - setTestError(data.error || ""); - } catch (err) { - setTestStatus("error"); - setTestError(err.message); - } - }; - - const handleSave = async () => { - if (!modelId.trim() || saving) return; - setSaving(true); - try { - await onSave(modelId.trim()); - } finally { - setSaving(false); - } - }; - - const handleKeyDown = (e) => { - if (e.key === "Enter") handleTest(); - }; - - return ( - -
    -
    - -
    - { setModelId(e.target.value); setTestStatus(null); setTestError(""); }} - onKeyDown={handleKeyDown} - placeholder="e.g. claude-opus-4-5" - className="flex-1 px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - autoFocus - /> - -
    -

    - Sent to provider as: {modelId.trim() || "model-id"} -

    -
    - - {/* Test result */} - {testStatus === "ok" && ( -
    - check_circle - Model is reachable -
    - )} - {testStatus === "error" && ( -
    - cancel - {testError || "Model not reachable"} -
    - )} - -
    - - -
    -
    -
    - ); -} - -AddCustomModelModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - providerAlias: PropTypes.string.isRequired, - providerDisplayAlias: PropTypes.string.isRequired, - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - diff --git a/9router/src/app/(dashboard)/dashboard/providers/[id]/page.new.js b/9router/src/app/(dashboard)/dashboard/providers/[id]/page.new.js deleted file mode 100644 index 16f4a368..00000000 --- a/9router/src/app/(dashboard)/dashboard/providers/[id]/page.new.js +++ /dev/null @@ -1,1724 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback, useMemo } from "react"; -import PropTypes from "prop-types"; -import { useParams, useRouter } from "next/navigation"; -import Link from "next/link"; -import Image from "next/image"; -import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, Toggle, Select } from "@/shared/components"; -import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; -import { getModelsByProviderId } from "@/shared/constants/models"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; - -export default function ProviderDetailPage() { - const params = useParams(); - const router = useRouter(); - const providerId = params.id; - const [connections, setConnections] = useState([]); - const [loading, setLoading] = useState(true); - const [providerNode, setProviderNode] = useState(null); - const [showOAuthModal, setShowOAuthModal] = useState(false); - const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [showEditNodeModal, setShowEditNodeModal] = useState(false); - const [selectedConnection, setSelectedConnection] = useState(null); - const [modelAliases, setModelAliases] = useState({}); - const [remoteModels, setRemoteModels] = useState([]); - const [loadingRemoteModels, setLoadingRemoteModels] = useState(false); - const [selectedModelIds, setSelectedModelIds] = useState([]); - const [savingSelectedModels, setSavingSelectedModels] = useState(false); - const [modelSearchQuery, setModelSearchQuery] = useState(""); - const [showSelectedOnly, setShowSelectedOnly] = useState(false); - const [headerImgError, setHeaderImgError] = useState(false); - const { copied, copy } = useCopyToClipboard(); - - const providerInfo = providerNode - ? { - id: providerNode.id, - name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"), - color: providerNode.type === "anthropic-compatible" ? "#D97757" : "#10A37F", - textIcon: providerNode.type === "anthropic-compatible" ? "AC" : "OC", - apiType: providerNode.apiType, - baseUrl: providerNode.baseUrl, - type: providerNode.type, - } - : (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId]); - const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId]; - const models = useMemo(() => getModelsByProviderId(providerId), [providerId]); - const providerAlias = getProviderAlias(providerId); - - const isOpenAICompatible = isOpenAICompatibleProvider(providerId); - const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId); - const isCompatible = isOpenAICompatible || isAnthropicCompatible; - - const providerStorageAlias = isCompatible ? providerId : providerAlias; - const providerDisplayAlias = isCompatible - ? (providerNode?.prefix || providerId) - : providerAlias; - const activeConnection = connections.find((conn) => conn.isActive !== false) || null; - const allProviderModels = models.length > 0 ? models : remoteModels; - const allProviderModelIds = useMemo( - () => allProviderModels.map((model) => model.id), - [allProviderModels] - ); - const savedEnabledModels = useMemo(() => { - const enabled = activeConnection?.providerSpecificData?.enabledModels; - return Array.isArray(enabled) - ? enabled.filter((modelId) => allProviderModelIds.includes(modelId)) - : []; - }, [activeConnection?.providerSpecificData?.enabledModels, allProviderModelIds]); - const savedEnabledModelsKey = useMemo( - () => savedEnabledModels.join("|"), - [savedEnabledModels] - ); - - // Define callbacks BEFORE the useEffect that uses them - const fetchAliases = useCallback(async () => { - try { - const res = await fetch("/api/models/alias"); - const data = await res.json(); - if (res.ok) { - setModelAliases(data.aliases || {}); - } - } catch (error) { - console.log("Error fetching aliases:", error); - } - }, []); - - const fetchConnections = useCallback(async () => { - try { - const [connectionsRes, nodesRes] = await Promise.all([ - fetch("/api/providers", { cache: "no-store" }), - fetch("/api/provider-nodes", { cache: "no-store" }), - ]); - const connectionsData = await connectionsRes.json(); - const nodesData = await nodesRes.json(); - if (connectionsRes.ok) { - const filtered = (connectionsData.connections || []).filter(c => c.provider === providerId); - setConnections(filtered); - } - if (nodesRes.ok) { - let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null; - - // Newly created compatible nodes can be briefly unavailable on one worker. - // Retry a few times before showing "Provider not found". - if (!node && isCompatible) { - for (let attempt = 0; attempt < 3; attempt += 1) { - await new Promise((resolve) => setTimeout(resolve, 150)); - const retryRes = await fetch("/api/provider-nodes", { cache: "no-store" }); - if (!retryRes.ok) continue; - const retryData = await retryRes.json(); - node = (retryData.nodes || []).find((entry) => entry.id === providerId) || null; - if (node) break; - } - } - - setProviderNode(node); - } - } catch (error) { - console.log("Error fetching connections:", error); - } finally { - setLoading(false); - } - }, [providerId, isCompatible]); - - const handleUpdateNode = async (formData) => { - try { - const res = await fetch(`/api/provider-nodes/${providerId}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - const data = await res.json(); - if (res.ok) { - setProviderNode(data.node); - await fetchConnections(); - setShowEditNodeModal(false); - } - } catch (error) { - console.log("Error updating provider node:", error); - } - }; - - const handleToggleModelSelected = (modelId) => { - setSelectedModelIds((prev) => ( - prev.includes(modelId) - ? prev.filter((id) => id !== modelId) - : [...prev, modelId] - )); - }; - - const handleSaveSelectedModels = async () => { - if (!activeConnection || savingSelectedModels) return; - setSavingSelectedModels(true); - try { - const res = await fetch(`/api/providers/${activeConnection.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - providerSpecificData: { - enabledModels: selectedModelIds, - }, - }), - }); - - if (!res.ok) { - const data = await res.json(); - alert(data.error || "Failed to save selected models"); - return; - } - - await fetchConnections(); - } catch (error) { - console.log("Error saving selected models:", error); - alert("Failed to save selected models"); - } finally { - setSavingSelectedModels(false); - } - }; - - useEffect(() => { - fetchConnections(); - fetchAliases(); - }, [fetchConnections, fetchAliases]); - - useEffect(() => { - const nextSelectedModelIds = (isCompatible || providerInfo?.passthroughModels) - ? [] - : savedEnabledModels; - - setSelectedModelIds((prev) => { - if ( - prev.length === nextSelectedModelIds.length - && prev.every((modelId, index) => modelId === nextSelectedModelIds[index]) - ) { - return prev; - } - return nextSelectedModelIds; - }); - }, [ - isCompatible, - providerInfo?.passthroughModels, - activeConnection?.id, - savedEnabledModels - ]); - - const fetchRemoteModels = useCallback(async () => { - if (isCompatible || providerInfo?.passthroughModels || models.length > 0) { - setRemoteModels([]); - return; - } - - if (!activeConnection) { - setRemoteModels([]); - return; - } - - setLoadingRemoteModels(true); - try { - const res = await fetch(`/api/providers/${activeConnection.id}/models`); - const data = await res.json(); - if (!res.ok) { - setRemoteModels([]); - return; - } - - const parsed = (data.models || []) - .map((item) => { - if (typeof item === "string") return { id: item, name: item }; - const modelId = item?.id || item?.name || item?.model; - if (!modelId) return null; - return { id: modelId, name: item?.name || modelId }; - }) - .filter(Boolean); - - const deduped = Array.from( - new Map(parsed.map((item) => [item.id, item])).values() - ); - - setRemoteModels(deduped); - } catch (error) { - console.log("Error fetching remote models:", error); - setRemoteModels([]); - } finally { - setLoadingRemoteModels(false); - } - }, [activeConnection, isCompatible, models.length, providerInfo?.passthroughModels]); - - useEffect(() => { - fetchRemoteModels(); - }, [fetchRemoteModels]); - - const handleSetAlias = async (modelId, alias, providerAliasOverride = providerAlias) => { - const fullModel = `${providerAliasOverride}/${modelId}`; - try { - const res = await fetch("/api/models/alias", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: fullModel, alias }), - }); - if (res.ok) { - await fetchAliases(); - } else { - const data = await res.json(); - alert(data.error || "Failed to set alias"); - } - } catch (error) { - console.log("Error setting alias:", error); - } - }; - - const handleDeleteAlias = async (alias) => { - try { - const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, { - method: "DELETE", - }); - if (res.ok) { - await fetchAliases(); - } - } catch (error) { - console.log("Error deleting alias:", error); - } - }; - - const handleDelete = async (id) => { - if (!confirm("Delete this connection?")) return; - try { - const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); - if (res.ok) { - setConnections(connections.filter(c => c.id !== id)); - } - } catch (error) { - console.log("Error deleting connection:", error); - } - }; - - const handleOAuthSuccess = () => { - fetchConnections(); - setShowOAuthModal(false); - }; - - const handleSaveApiKey = async (formData) => { - try { - const res = await fetch("/api/providers", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: providerId, ...formData }), - }); - if (res.ok) { - await fetchConnections(); - setShowAddApiKeyModal(false); - } - } catch (error) { - console.log("Error saving connection:", error); - } - }; - - const handleUpdateConnection = async (formData) => { - try { - const res = await fetch(`/api/providers/${selectedConnection.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - if (res.ok) { - await fetchConnections(); - setShowEditModal(false); - } - } catch (error) { - console.log("Error updating connection:", error); - } - }; - - const handleUpdateConnectionStatus = async (id, isActive) => { - try { - const res = await fetch(`/api/providers/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ isActive }), - }); - if (res.ok) { - setConnections(prev => prev.map(c => c.id === id ? { ...c, isActive } : c)); - } - } catch (error) { - console.log("Error updating connection status:", error); - } - }; - - const handleSwapPriority = async (conn1, conn2) => { - if (!conn1 || !conn2) return; - try { - // If they have the same priority, we need to ensure the one moving up - // gets a lower value than the one moving down. - // We use a small offset which the backend re-indexing will fix. - let p1 = conn2.priority; - let p2 = conn1.priority; - - if (p1 === p2) { - // If moving conn1 "up" (index decreases) - const isConn1MovingUp = connections.indexOf(conn1) > connections.indexOf(conn2); - if (isConn1MovingUp) { - p1 = conn2.priority - 0.5; - } else { - p1 = conn2.priority + 0.5; - } - } - - await Promise.all([ - fetch(`/api/providers/${conn1.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: p1 }), - }), - fetch(`/api/providers/${conn2.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: p2 }), - }), - ]); - await fetchConnections(); - } catch (error) { - console.log("Error swapping priority:", error); - } - }; - - const renderModelsSection = () => { - if (isCompatible) { - return ( - - ); - } - if (providerInfo.passthroughModels) { - return ( - - ); - } - - const availableModels = allProviderModels; - if (availableModels.length === 0) { - if (loadingRemoteModels) { - return

    Loading models from provider...

    ; - } - return

    No models configured

    ; - } - - const selectedSet = new Set(selectedModelIds); - const filteredBySelection = showSelectedOnly - ? availableModels.filter((model) => selectedSet.has(model.id)) - : availableModels; - const query = modelSearchQuery.trim().toLowerCase(); - const visibleModels = query - ? filteredBySelection.filter((model) => - model.id.toLowerCase().includes(query) || - (model.name || "").toLowerCase().includes(query) - ) - : filteredBySelection; - const hasSelectionChanges = - savedEnabledModels.length !== selectedModelIds.length || - savedEnabledModels.some((modelId) => !selectedSet.has(modelId)); - - return ( -
    -
    -
    - setModelSearchQuery(e.target.value)} - placeholder="Search model id" - className="pr-8" - /> - {modelSearchQuery && ( - - )} -
    - - - -
    - Selected only - -
    -
    - -

    - {selectedModelIds.length > 0 - ? `${selectedModelIds.length} selected` - : "All models enabled"} -

    - - {visibleModels.length === 0 ? ( -

    No models match your filter.

    - ) : ( -
    - {visibleModels.map((model) => { - const fullModel = `${providerStorageAlias}/${model.id}`; - const oldFormatModel = `${providerId}/${model.id}`; - const existingAlias = Object.entries(modelAliases).find( - ([, m]) => m === fullModel || m === oldFormatModel - )?.[0]; - return ( - handleToggleModelSelected(model.id)} - onSetAlias={(alias) => handleSetAlias(model.id, alias, providerStorageAlias)} - onDeleteAlias={() => handleDeleteAlias(existingAlias)} - /> - ); - })} -
    - )} -
    - ); - }; - - if (loading) { - return ( -
    - - -
    - ); - } - - if (!providerInfo) { - return ( -
    -

    Provider not found

    - - Back to Providers - -
    - ); - } - - // Determine icon path: OpenAI Compatible providers use specialized icons - const getHeaderIconPath = () => { - if (isOpenAICompatible && providerInfo.apiType) { - return providerInfo.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png"; - } - if (isAnthropicCompatible) { - return "/providers/anthropic-m.png"; - } - return `/providers/${providerInfo.id}.png`; - }; - - return ( -
    - {/* Header */} -
    - - arrow_back - Back to Providers - -
    -
    - {headerImgError ? ( - - {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()} - - ) : ( - {providerInfo.name} setHeaderImgError(true)} - /> - )} -
    -
    -

    {providerInfo.name}

    -

    - {connections.length} connection{connections.length === 1 ? "" : "s"} -

    -
    -
    -
    - - {isCompatible && providerNode && ( - -
    -
    -

    {isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}

    -

    - {isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/ - {isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")} -

    -
    -
    - - - -
    -
    - {connections.length > 0 && ( -

    - Only one connection is allowed per compatible node. Add another node if you need more connections. -

    - )} -
    - )} - - {/* Connections */} - -
    -

    Connections

    - {!isCompatible && ( - - )} -
    - - {connections.length === 0 ? ( -
    -
    - {isOAuth ? "lock" : "key"} -
    -

    No connections yet

    -

    Add your first connection to get started

    - {!isCompatible && ( - - )} -
    - ) : ( -
    - {connections - .sort((a, b) => (a.priority || 0) - (b.priority || 0)) - .map((conn, index) => ( - handleSwapPriority(conn, connections[index - 1])} - onMoveDown={() => handleSwapPriority(conn, connections[index + 1])} - onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)} - onEdit={() => { - setSelectedConnection(conn); - setShowEditModal(true); - }} - onDelete={() => handleDelete(conn.id)} - /> - ))} -
    - )} -
    - - {/* Models */} - -

    - {providerInfo.passthroughModels ? "Model Aliases" : "Available Models"} -

    - {renderModelsSection()} - -
    - - {/* Modals */} - {providerId === "kiro" ? ( - setShowOAuthModal(false)} - /> - ) : providerId === "cursor" ? ( - setShowOAuthModal(false)} - /> - ) : ( - setShowOAuthModal(false)} - /> - )} - setShowAddApiKeyModal(false)} - /> - setShowEditModal(false)} - /> - {isCompatible && ( - setShowEditNodeModal(false)} - isAnthropic={isAnthropicCompatible} - /> - )} -
    - ); -} - -function ModelRow({ model, fullModel, alias, selected, onToggleSelect, copied, onCopy }) { - return ( -
    - - smart_toy - {fullModel} - -
    - ); -} - -ModelRow.propTypes = { - model: PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired, - fullModel: PropTypes.string.isRequired, - alias: PropTypes.string, - selected: PropTypes.bool, - onToggleSelect: PropTypes.func, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, -}; - -function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) { - const [newModel, setNewModel] = useState(""); - const [adding, setAdding] = useState(false); - - // Filter aliases for this provider - models are persisted via alias - const providerAliases = Object.entries(modelAliases).filter( - ([, model]) => model.startsWith(`${providerAlias}/`) - ); - - const allModels = providerAliases.map(([alias, fullModel]) => ({ - modelId: fullModel.replace(`${providerAlias}/`, ""), - fullModel, - alias, - })); - - // Generate default alias from modelId (last part after /) - const generateDefaultAlias = (modelId) => { - const parts = modelId.split("/"); - return parts[parts.length - 1]; - }; - - const handleAdd = async () => { - if (!newModel.trim() || adding) return; - const modelId = newModel.trim(); - const defaultAlias = generateDefaultAlias(modelId); - - // Check if alias already exists - if (modelAliases[defaultAlias]) { - alert(`Alias "${defaultAlias}" already exists. Please use a different model or edit existing alias.`); - return; - } - - setAdding(true); - try { - await onSetAlias(modelId, defaultAlias); - setNewModel(""); - } catch (error) { - console.log("Error adding model:", error); - } finally { - setAdding(false); - } - }; - - return ( -
    -

    - OpenRouter supports any model. Add models and create aliases for quick access. -

    - - {/* Add new model */} -
    -
    - - setNewModel(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder="anthropic/claude-3-opus" - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> -
    - -
    - - {/* Models list */} - {allModels.length > 0 && ( -
    - {allModels.map(({ modelId, fullModel, alias }) => ( - onDeleteAlias(alias)} - /> - ))} -
    - )} -
    - ); -} - -PassthroughModelsSection.propTypes = { - providerAlias: PropTypes.string.isRequired, - modelAliases: PropTypes.object.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onSetAlias: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, -}; - -function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias }) { - return ( -
    - smart_toy - -
    -

    {modelId}

    - -
    - {fullModel} - -
    -
    - - {/* Delete button */} - -
    - ); -} - -PassthroughModelRow.propTypes = { - modelId: PropTypes.string.isRequired, - fullModel: PropTypes.string.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, -}; - -function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) { - const [newModel, setNewModel] = useState(""); - const [adding, setAdding] = useState(false); - const [importing, setImporting] = useState(false); - - const providerAliases = Object.entries(modelAliases).filter( - ([, model]) => model.startsWith(`${providerStorageAlias}/`) - ); - - const allModels = providerAliases.map(([alias, fullModel]) => ({ - modelId: fullModel.replace(`${providerStorageAlias}/`, ""), - fullModel, - alias, - })); - - const generateDefaultAlias = (modelId) => { - const parts = modelId.split("/"); - return parts[parts.length - 1]; - }; - - const resolveAlias = (modelId) => { - const baseAlias = generateDefaultAlias(modelId); - if (!modelAliases[baseAlias]) return baseAlias; - const prefixedAlias = `${providerDisplayAlias}-${baseAlias}`; - if (!modelAliases[prefixedAlias]) return prefixedAlias; - return null; - }; - - const handleAdd = async () => { - if (!newModel.trim() || adding) return; - const modelId = newModel.trim(); - const resolvedAlias = resolveAlias(modelId); - if (!resolvedAlias) { - alert("All suggested aliases already exist. Please choose a different model or remove conflicting aliases."); - return; - } - - setAdding(true); - try { - await onSetAlias(modelId, resolvedAlias, providerStorageAlias); - setNewModel(""); - } catch (error) { - console.log("Error adding model:", error); - } finally { - setAdding(false); - } - }; - - const handleImport = async () => { - if (importing) return; - const activeConnection = connections.find((conn) => conn.isActive !== false); - if (!activeConnection) return; - - setImporting(true); - try { - const res = await fetch(`/api/providers/${activeConnection.id}/models`); - const data = await res.json(); - if (!res.ok) { - alert(data.error || "Failed to import models"); - return; - } - const models = data.models || []; - if (models.length === 0) { - alert("No models returned from /models."); - return; - } - let importedCount = 0; - for (const model of models) { - const modelId = model.id || model.name || model.model; - if (!modelId) continue; - const resolvedAlias = resolveAlias(modelId); - if (!resolvedAlias) continue; - await onSetAlias(modelId, resolvedAlias, providerStorageAlias); - importedCount += 1; - } - if (importedCount === 0) { - alert("No new models were added."); - } - } catch (error) { - console.log("Error importing models:", error); - } finally { - setImporting(false); - } - }; - - const canImport = connections.some((conn) => conn.isActive !== false); - - return ( -
    -

    - Add {isAnthropic ? "Anthropic" : "OpenAI"}-compatible models manually or import them from the /models endpoint. -

    - -
    -
    - - setNewModel(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder={isAnthropic ? "claude-3-opus-20240229" : "gpt-4o"} - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> -
    - - -
    - - {!canImport && ( -

    - Add a connection to enable importing models. -

    - )} - - {allModels.length > 0 && ( -
    - {allModels.map(({ modelId, fullModel, alias }) => ( - onDeleteAlias(alias)} - /> - ))} -
    - )} -
    - ); -} - -CompatibleModelsSection.propTypes = { - providerStorageAlias: PropTypes.string.isRequired, - providerDisplayAlias: PropTypes.string.isRequired, - modelAliases: PropTypes.object.isRequired, - copied: PropTypes.string, - onCopy: PropTypes.func.isRequired, - onSetAlias: PropTypes.func.isRequired, - onDeleteAlias: PropTypes.func.isRequired, - connections: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - isActive: PropTypes.bool, - })).isRequired, - isAnthropic: PropTypes.bool, -}; - -function CooldownTimer({ until }) { - const [remaining, setRemaining] = useState(""); - - useEffect(() => { - const updateRemaining = () => { - const diff = new Date(until).getTime() - Date.now(); - if (diff <= 0) { - setRemaining(""); - return; - } - const secs = Math.floor(diff / 1000); - if (secs < 60) { - setRemaining(`${secs}s`); - } else if (secs < 3600) { - setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`); - } else { - const hrs = Math.floor(secs / 3600); - const mins = Math.floor((secs % 3600) / 60); - setRemaining(`${hrs}h ${mins}m`); - } - }; - - updateRemaining(); - const interval = setInterval(updateRemaining, 1000); - return () => clearInterval(interval); - }, [until]); - - if (!remaining) return null; - - return ( - - ⏱ {remaining} - - ); -} - -CooldownTimer.propTypes = { - until: PropTypes.string.isRequired, -}; - -function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onEdit, onDelete }) { - const displayName = isOAuth - ? connection.name || connection.email || connection.displayName || "OAuth Account" - : connection.name; - - // Use useState + useEffect for impure Date.now() to avoid calling during render - const [isCooldown, setIsCooldown] = useState(false); - - const modelLockUntil = Object.entries(connection) - .filter(([k]) => k.startsWith("modelLock_")) - .map(([, v]) => v) - .filter(v => v && new Date(v).getTime() > Date.now()) - .sort()[0] || null; - - useEffect(() => { - const checkCooldown = () => { - const until = Object.entries(connection) - .filter(([k]) => k.startsWith("modelLock_")) - .map(([, v]) => v) - .filter(v => v && new Date(v).getTime() > Date.now()) - .sort()[0] || null; - setIsCooldown(!!until); - }; - - checkCooldown(); - const interval = modelLockUntil ? setInterval(checkCooldown, 1000) : null; - return () => { - if (interval) clearInterval(interval); - }; - }, [modelLockUntil]); - - // Determine effective status (override unavailable if cooldown expired) - const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown) - ? "active" // Cooldown expired → treat as active - : connection.testStatus; - - const getStatusVariant = () => { - if (connection.isActive === false) return "default"; - if (effectiveStatus === "active" || effectiveStatus === "success") return "success"; - if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error"; - return "default"; - }; - - return ( -
    -
    - {/* Priority arrows */} -
    - - -
    - - {isOAuth ? "lock" : "key"} - -
    -

    {displayName}

    -
    - - {connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")} - - {isCooldown && connection.isActive !== false && } - {connection.lastError && connection.isActive !== false && ( - - {connection.lastError} - - )} - #{connection.priority} - {connection.globalPriority && ( - Auto: {connection.globalPriority} - )} -
    -
    -
    -
    - -
    - - -
    -
    -
    - ); -} - -ConnectionRow.propTypes = { - connection: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - email: PropTypes.string, - displayName: PropTypes.string, - modelLockUntil: PropTypes.string, - testStatus: PropTypes.string, - isActive: PropTypes.bool, - lastError: PropTypes.string, - priority: PropTypes.number, - globalPriority: PropTypes.number, - }).isRequired, - isOAuth: PropTypes.bool.isRequired, - isFirst: PropTypes.bool.isRequired, - isLast: PropTypes.bool.isRequired, - onMoveUp: PropTypes.func.isRequired, - onMoveDown: PropTypes.func.isRequired, - onToggleActive: PropTypes.func.isRequired, - onEdit: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, -}; - -function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, onSave, onClose }) { - const [formData, setFormData] = useState({ - name: "", - apiKey: "", - priority: 1, - }); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - const [saving, setSaving] = useState(false); - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - const handleSubmit = async () => { - if (!provider || !formData.apiKey) return; - - setSaving(true); - try { - let isValid = false; - try { - setValidating(true); - setValidationResult(null); - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - isValid = !!data.valid; - setValidationResult(isValid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - - await onSave({ - name: formData.name, - apiKey: formData.apiKey, - priority: formData.priority, - testStatus: isValid ? "active" : "unknown", - }); - } finally { - setSaving(false); - } - }; - - if (!provider) return null; - - return ( - -
    - setFormData({ ...formData, name: e.target.value })} - placeholder="Production Key" - /> -
    - setFormData({ ...formData, apiKey: e.target.value })} - className="flex-1" - /> -
    - -
    -
    - {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} - {isCompatible && ( -

    - {isAnthropic - ? `Validation checks ${providerName || "Anthropic Compatible"} by verifying the API key.` - : `Validation checks ${providerName || "OpenAI Compatible"} via /models on your base URL.` - } -

    - )} - setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} - /> -
    - - -
    -
    -
    - ); -} - -AddApiKeyModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - provider: PropTypes.string, - providerName: PropTypes.string, - isCompatible: PropTypes.bool, - isAnthropic: PropTypes.bool, - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - -function EditConnectionModal({ isOpen, connection, onSave, onClose }) { - const [formData, setFormData] = useState({ - name: "", - priority: 1, - apiKey: "", - }); - const [testing, setTesting] = useState(false); - const [testResult, setTestResult] = useState(null); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - const [saving, setSaving] = useState(false); - - useEffect(() => { - if (connection) { - setFormData({ - name: connection.name || "", - priority: connection.priority || 1, - apiKey: "", - }); - setTestResult(null); - setValidationResult(null); - } - }, [connection]); - - const handleTest = async () => { - if (!connection?.provider) return; - setTesting(true); - setTestResult(null); - try { - const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" }); - const data = await res.json(); - setTestResult(data.valid ? "success" : "failed"); - } catch { - setTestResult("failed"); - } finally { - setTesting(false); - } - }; - - const handleValidate = async () => { - if (!connection?.provider || !formData.apiKey) return; - setValidating(true); - setValidationResult(null); - try { - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - const handleSubmit = async () => { - setSaving(true); - try { - const updates = { name: formData.name, priority: formData.priority }; - if (!isOAuth && formData.apiKey) { - updates.apiKey = formData.apiKey; - let isValid = validationResult === "success"; - if (!isValid) { - try { - setValidating(true); - setValidationResult(null); - const res = await fetch("/api/providers/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }), - }); - const data = await res.json(); - isValid = !!data.valid; - setValidationResult(isValid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - } - if (isValid) { - updates.testStatus = "active"; - updates.lastError = null; - updates.lastErrorAt = null; - } - } - await onSave(updates); - } finally { - setSaving(false); - } - }; - - if (!connection) return null; - - const isOAuth = connection.authType === "oauth"; - const isCompatible = isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider); - - return ( - -
    - setFormData({ ...formData, name: e.target.value })} - placeholder={isOAuth ? "Account name" : "Production Key"} - /> - {isOAuth && connection.email && ( -
    -

    Email

    -

    {connection.email}

    -
    - )} - setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} - /> - {!isOAuth && ( - <> -
    - setFormData({ ...formData, apiKey: e.target.value })} - placeholder="Enter new API key" - hint="Leave blank to keep the current API key." - className="flex-1" - /> -
    - -
    -
    - {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} - - )} - - {/* Test Connection */} - {!isCompatible && ( -
    - - {testResult && ( - - {testResult === "success" ? "Valid" : "Failed"} - - )} -
    - )} - -
    - - -
    -
    -
    - ); -} - -EditConnectionModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - connection: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - email: PropTypes.string, - priority: PropTypes.number, - authType: PropTypes.string, - provider: PropTypes.string, - }), - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - -function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) { - const [formData, setFormData] = useState({ - name: "", - prefix: "", - apiType: "chat", - baseUrl: "https://api.openai.com/v1", - }); - const [saving, setSaving] = useState(false); - const [checkKey, setCheckKey] = useState(""); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - - useEffect(() => { - if (node) { - setFormData({ - name: node.name || "", - prefix: node.prefix || "", - apiType: node.apiType || "chat", - baseUrl: node.baseUrl || (isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"), - }); - } - }, [node, isAnthropic]); - - const apiTypeOptions = [ - { value: "chat", label: "Chat Completions" }, - { value: "responses", label: "Responses API" }, - ]; - - const handleSubmit = async () => { - if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return; - setSaving(true); - try { - const payload = { - name: formData.name, - prefix: formData.prefix, - baseUrl: formData.baseUrl, - }; - if (!isAnthropic) { - payload.apiType = formData.apiType; - } - await onSave(payload); - } finally { - setSaving(false); - } - }; - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/provider-nodes/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: formData.baseUrl, - apiKey: checkKey, - type: isAnthropic ? "anthropic-compatible" : "openai-compatible" - }), - }); - const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); - } catch { - setValidationResult("failed"); - } finally { - setValidating(false); - } - }; - - if (!node) return null; - - return ( - -
    - setFormData({ ...formData, name: e.target.value })} - placeholder={`${isAnthropic ? "Anthropic" : "OpenAI"} Compatible (Prod)`} - hint="Required. A friendly label for this node." - /> - setFormData({ ...formData, prefix: e.target.value })} - placeholder={isAnthropic ? "ac-prod" : "oc-prod"} - hint="Required. Used as the provider prefix for model IDs." - /> - {!isAnthropic && ( - setFormData({ ...formData, baseUrl: e.target.value })} - placeholder={isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"} - hint={`Use the base URL (ending in /v1) for your ${isAnthropic ? "Anthropic" : "OpenAI"}-compatible API.`} - /> -
    - setCheckKey(e.target.value)} - className="flex-1" - /> -
    - -
    -
    - {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )} -
    - - -
    -
    -
    - ); -} - -EditCompatibleNodeModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - node: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - prefix: PropTypes.string, - apiType: PropTypes.string, - baseUrl: PropTypes.string, - }), - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - isAnthropic: PropTypes.bool, -}; diff --git a/9router/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js b/9router/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js deleted file mode 100644 index 9150bef1..00000000 --- a/9router/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js +++ /dev/null @@ -1,185 +0,0 @@ -"use client"; - -/** - * ModelAvailabilityBadge — compact inline status indicator - * - * Shows green when all models are operational, or amber/red when there are - * issues, with a hover popover for details and cooldown clearing. - */ - -import { useState, useEffect, useCallback, useRef } from "react"; -import { Button } from "@/shared/components"; -import { useNotificationStore } from "@/store/notificationStore"; - -const STATUS_CONFIG = { - available: { icon: "check_circle", color: "#22c55e", label: "Available" }, - cooldown: { icon: "schedule", color: "#f59e0b", label: "Cooldown" }, - unavailable: { icon: "error", color: "#ef4444", label: "Unavailable" }, - unknown: { icon: "help", color: "#6b7280", label: "Unknown" }, -}; - -export default function ModelAvailabilityBadge() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [expanded, setExpanded] = useState(false); - const [clearing, setClearing] = useState(null); - const ref = useRef(null); - const notify = useNotificationStore(); - - const fetchStatus = useCallback(async () => { - try { - const res = await fetch("/api/models/availability"); - if (res.ok) { - const json = await res.json(); - setData(json); - } - } catch { - // silent fail — will retry - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchStatus(); - const interval = setInterval(fetchStatus, 30000); - return () => clearInterval(interval); - }, [fetchStatus]); - - // Close popover on outside click - useEffect(() => { - const handleClick = (e) => { - if (ref.current && !ref.current.contains(e.target)) setExpanded(false); - }; - if (expanded) document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [expanded]); - - const handleClearCooldown = async (provider, model) => { - setClearing(`${provider}:${model}`); - try { - const res = await fetch("/api/models/availability", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "clearCooldown", provider, model }), - }); - if (res.ok) { - notify.success(`Cooldown cleared for ${model}`); - await fetchStatus(); - } else { - notify.error("Failed to clear cooldown"); - } - } catch { - notify.error("Failed to clear cooldown"); - } finally { - setClearing(null); - } - }; - - if (loading) return null; - - const models = data?.models || []; - const unavailableCount = data?.unavailableCount || models.filter((m) => m.status !== "available").length; - const isHealthy = unavailableCount === 0; - - // Group unhealthy models by provider - const byProvider = {}; - models.forEach((m) => { - if (m.status === "available") return; - const key = m.provider || "unknown"; - if (!byProvider[key]) byProvider[key] = []; - byProvider[key].push(m); - }); - - return ( -
    - {/* */} - - {expanded && ( -
    -
    -
    - - {isHealthy ? "verified" : "warning"} - - Model Status -
    - -
    - -
    - {isHealthy ? ( -

    - All models are responding normally. -

    - ) : ( -
    - {Object.entries(byProvider).map(([provider, provModels]) => ( -
    -

    {provider}

    -
    - {provModels.map((m) => { - const status = STATUS_CONFIG[m.status] || STATUS_CONFIG.unknown; - const isClearing = clearing === `${m.provider}:${m.model}`; - return ( -
    -
    - - {status.icon} - - {m.model} -
    - {m.status === "cooldown" && ( - - )} -
    - ); - })} -
    -
    - ))} -
    - )} -
    -
    - )} -
    - ); -} diff --git a/9router/src/app/(dashboard)/dashboard/providers/new/page.js b/9router/src/app/(dashboard)/dashboard/providers/new/page.js deleted file mode 100644 index 57d0a7f5..00000000 --- a/9router/src/app/(dashboard)/dashboard/providers/new/page.js +++ /dev/null @@ -1,220 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { Card, Button, Input, Select, Toggle } from "@/shared/components"; -import { AI_PROVIDERS, AUTH_METHODS } from "@/shared/constants/config"; - -const providerOptions = Object.values(AI_PROVIDERS).map((p) => ({ - value: p.id, - label: p.name, -})); - -const authMethodOptions = Object.values(AUTH_METHODS).map((m) => ({ - value: m.id, - label: m.name, -})); - -export default function NewProviderPage() { - const router = useRouter(); - const [loading, setLoading] = useState(false); - const [formData, setFormData] = useState({ - provider: "", - authMethod: "api_key", - apiKey: "", - displayName: "", - isActive: true, - }); - const [errors, setErrors] = useState({}); - - const handleChange = (field, value) => { - setFormData((prev) => ({ ...prev, [field]: value })); - if (errors[field]) { - setErrors((prev) => ({ ...prev, [field]: null })); - } - }; - - const validate = () => { - const newErrors = {}; - if (!formData.provider) newErrors.provider = "Please select a provider"; - if (formData.authMethod === "api_key" && !formData.apiKey) { - newErrors.apiKey = "API Key is required"; - } - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - if (!validate()) return; - - setLoading(true); - try { - const response = await fetch("/api/providers", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - - if (response.ok) { - router.push("/dashboard/providers"); - } else { - const data = await response.json(); - setErrors({ submit: data.error || "Failed to create provider" }); - } - } catch (error) { - setErrors({ submit: "An error occurred. Please try again." }); - } finally { - setLoading(false); - } - }; - - const selectedProvider = AI_PROVIDERS[formData.provider]; - - return ( -
    - {/* Header */} -
    - - arrow_back - Back to Providers - -

    Add New Provider

    -

    - Configure a new AI provider to use with your applications. -

    -
    - - {/* Form */} - -
    - {/* Provider Selection */} - handleChange("apiKey", e.target.value)} - error={errors.apiKey} - hint="Your API key will be encrypted and stored securely." - required - /> - )} - - {/* OAuth2 Button */} - {formData.authMethod === "oauth2" && ( - -

    - Connect your account using OAuth2 authentication. -

    - -
    - )} - - {/* Display Name */} - handleChange("displayName", e.target.value)} - hint="Optional. A friendly name to identify this configuration." - /> - - {/* Active Toggle */} - handleChange("isActive", checked)} - label="Active" - description="Enable this provider for use in your applications" - /> - - {/* Error Message */} - {errors.submit && ( -
    - {errors.submit} -
    - )} - - {/* Actions */} -
    - - - - -
    - -
    -
    - ); -} - diff --git a/9router/src/app/(dashboard)/dashboard/providers/page.js b/9router/src/app/(dashboard)/dashboard/providers/page.js deleted file mode 100644 index 8b0bb12e..00000000 --- a/9router/src/app/(dashboard)/dashboard/providers/page.js +++ /dev/null @@ -1,1036 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import Image from "next/image"; -import PropTypes from "prop-types"; -import { Card, CardSkeleton, Badge, Button, Input, Modal, Select, Toggle } from "@/shared/components"; -import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; -import { FREE_PROVIDERS, OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX } from "@/shared/constants/providers"; -import Link from "next/link"; -import { getErrorCode, getRelativeTime } from "@/shared/utils"; -import { useNotificationStore } from "@/store/notificationStore"; -import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge"; - -function getStatusDisplay(connected, error, errorCode) { - const parts = []; - if (connected > 0) { - parts.push( - - {connected} Connected - - ); - } - if (error > 0) { - const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`; - parts.push( - - {errText} - - ); - } - if (parts.length === 0) { - return No connections; - } - return parts; -} - -function getConnectionErrorTag(connection) { - if (!connection) return null; - - const explicitType = connection.lastErrorType; - if (explicitType === "runtime_error") return "RUNTIME"; - if ( - explicitType === "upstream_auth_error" || - explicitType === "auth_missing" || - explicitType === "token_refresh_failed" || - explicitType === "token_expired" - ) return "AUTH"; - if (explicitType === "upstream_rate_limited") return "429"; - if (explicitType === "upstream_unavailable") return "5XX"; - if (explicitType === "network_error") return "NET"; - - const numericCode = Number(connection.errorCode); - if (Number.isFinite(numericCode) && numericCode >= 400) return String(numericCode); - - const fromMessage = getErrorCode(connection.lastError); - if (fromMessage === "401" || fromMessage === "403") return "AUTH"; - if (fromMessage && fromMessage !== "ERR") return fromMessage; - - const msg = (connection.lastError || "").toLowerCase(); - if (msg.includes("runtime") || msg.includes("not runnable") || msg.includes("not installed")) return "RUNTIME"; - if (msg.includes("invalid api key") || msg.includes("token invalid") || msg.includes("revoked") || msg.includes("unauthorized")) return "AUTH"; - - return "ERR"; -} - -export default function ProvidersPage() { - const [connections, setConnections] = useState([]); - const [providerNodes, setProviderNodes] = useState([]); - const [loading, setLoading] = useState(true); - const [showAddCompatibleModal, setShowAddCompatibleModal] = useState(false); - const [showAddAnthropicCompatibleModal, setShowAddAnthropicCompatibleModal] = useState(false); - const [testingMode, setTestingMode] = useState(null); - const [testResults, setTestResults] = useState(null); - const notify = useNotificationStore(); - - useEffect(() => { - const fetchData = async () => { - try { - const [connectionsRes, nodesRes] = await Promise.all([ - fetch("/api/providers"), - fetch("/api/provider-nodes"), - ]); - const connectionsData = await connectionsRes.json(); - const nodesData = await nodesRes.json(); - if (connectionsRes.ok) setConnections(connectionsData.connections || []); - if (nodesRes.ok) setProviderNodes(nodesData.nodes || []); - } catch (error) { - console.log("Error fetching data:", error); - } finally { - setLoading(false); - } - }; - fetchData(); - }, []); - - const getProviderStats = (providerId, authType) => { - const providerConnections = connections.filter( - (c) => c.provider === providerId && c.authType === authType - ); - - const getEffectiveStatus = (conn) => { - const isCooldown = Object.entries(conn) - .some(([k, v]) => k.startsWith("modelLock_") && v && new Date(v).getTime() > Date.now()); - return conn.testStatus === "unavailable" && !isCooldown ? "active" : conn.testStatus; - }; - - const connected = providerConnections.filter((c) => { - const status = getEffectiveStatus(c); - return status === "active" || status === "success"; - }).length; - - const errorConns = providerConnections.filter((c) => { - const status = getEffectiveStatus(c); - return status === "error" || status === "expired" || status === "unavailable"; - }); - - const error = errorConns.length; - const total = providerConnections.length; - const allDisabled = total > 0 && providerConnections.every((c) => c.isActive === false); - - const latestError = errorConns.sort( - (a, b) => new Date(b.lastErrorAt || 0) - new Date(a.lastErrorAt || 0) - )[0]; - const errorCode = latestError ? getConnectionErrorTag(latestError) : null; - const errorTime = latestError?.lastErrorAt ? getRelativeTime(latestError.lastErrorAt) : null; - - return { connected, error, total, errorCode, errorTime, allDisabled }; - }; - - // Toggle all connections for a provider on/off - const handleToggleProvider = async (providerId, authType, newActive) => { - const providerConns = connections.filter( - (c) => c.provider === providerId && c.authType === authType - ); - setConnections((prev) => - prev.map((c) => - c.provider === providerId && c.authType === authType ? { ...c, isActive: newActive } : c - ) - ); - await Promise.allSettled( - providerConns.map((c) => - fetch(`/api/providers/${c.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ isActive: newActive }), - }) - ) - ); - }; - - const handleBatchTest = async (mode, providerId = null) => { - if (testingMode) return; - setTestingMode(mode === "provider" ? providerId : mode); - setTestResults(null); - try { - const res = await fetch("/api/providers/test-batch", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ mode, providerId }), - }); - const data = await res.json(); - setTestResults(data); - if (data.summary) { - const { passed, failed, total } = data.summary; - if (failed === 0) notify.success(`All ${total} tests passed`); - else notify.warning(`${passed}/${total} passed, ${failed} failed`); - } - } catch (error) { - setTestResults({ error: "Test request failed" }); - notify.error("Provider test failed"); - } finally { - setTestingMode(null); - } - }; - - const compatibleProviders = providerNodes - .filter((node) => node.type === "openai-compatible") - .map((node) => ({ - id: node.id, - name: node.name || "OpenAI Compatible", - color: "#10A37F", - textIcon: "OC", - apiType: node.apiType, - })); - - const anthropicCompatibleProviders = providerNodes - .filter((node) => node.type === "anthropic-compatible") - .map((node) => ({ - id: node.id, - name: node.name || "Anthropic Compatible", - color: "#D97757", - textIcon: "AC", - })); - - if (loading) { - return ( -
    - - -
    - ); - } - - return ( -
    - {/* OAuth Providers */} -
    -
    -

    - OAuth Providers -

    -
    - - -
    -
    -
    - {Object.entries(OAUTH_PROVIDERS).map(([key, info]) => ( - handleToggleProvider(key, "oauth", active)} - /> - ))} -
    -
    - - {/* Free Providers */} -
    -
    -

    - Free Providers -

    - -
    -
    - {Object.entries(FREE_PROVIDERS).map(([key, info]) => ( - handleToggleProvider(key, "oauth", active)} - /> - ))} -
    -
    - - {/* API Key Providers — fixed list */} -
    -
    -

    - API Key Providers{" "} -

    - -
    -
    - {Object.entries(APIKEY_PROVIDERS).map(([key, info]) => ( - handleToggleProvider(key, "apikey", active)} - /> - ))} -
    -
    - - {/* API Key Compatible Providers — dynamic (OpenAI/Anthropic compatible) */} -
    -
    -

    - API Key Compatible Providers{" "} -

    -
    - {/* {(compatibleProviders.length > 0 || anthropicCompatibleProviders.length > 0) && ( - - )} */} - - -
    -
    - {compatibleProviders.length === 0 && anthropicCompatibleProviders.length === 0 ? ( -
    - extension -

    No compatible providers added yet

    -

    - Use the buttons above to add OpenAI or Anthropic compatible endpoints -

    -
    - ) : ( -
    - {[...compatibleProviders, ...anthropicCompatibleProviders].map((info) => ( - handleToggleProvider(info.id, "apikey", active)} - /> - ))} -
    - )} -
    - - setShowAddCompatibleModal(false)} - onCreated={(node) => { - setProviderNodes((prev) => [...prev, node]); - setShowAddCompatibleModal(false); - }} - /> - setShowAddAnthropicCompatibleModal(false)} - onCreated={(node) => { - setProviderNodes((prev) => [...prev, node]); - setShowAddAnthropicCompatibleModal(false); - }} - /> - - {/* Test Results Modal */} - {testResults && ( -
    setTestResults(null)} - > -
    -
    e.stopPropagation()} - > -
    -

    Test Results

    - -
    -
    - -
    -
    -
    - )} -
    - ); -} - -function ProviderCard({ providerId, provider, stats, authType, onToggle }) { - const { connected, error, errorCode, errorTime, allDisabled } = stats; - const [imgError, setImgError] = useState(false); - - const dotColors = { - free: "bg-green-500", - oauth: "bg-blue-500", - apikey: "bg-amber-500", - compatible: "bg-orange-500", - }; - const dotLabels = { free: "Free", oauth: "OAuth", apikey: "API Key", compatible: "Compatible" }; - - return ( - - -
    -
    -
    7 ? provider.color : provider.color + "15"}` }} - > - {imgError ? ( - - {provider.textIcon || provider.id.slice(0, 2).toUpperCase()} - - ) : ( - {provider.name} setImgError(true)} - /> - )} -
    -
    -

    - {provider.name} -

    -
    - {allDisabled ? ( - - - pause_circle - Disabled - - - ) : ( - <> - {getStatusDisplay(connected, error, errorCode)} - {errorTime && {errorTime}} - - )} -
    -
    -
    -
    - {stats.total > 0 && ( -
    { - e.preventDefault(); - e.stopPropagation(); - onToggle(!allDisabled ? false : true); - }} - > - { }} - title={allDisabled ? "Enable provider" : "Disable provider"} - /> -
    - )} -
    -
    -
    - - ); -} - -ProviderCard.propTypes = { - providerId: PropTypes.string.isRequired, - provider: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - color: PropTypes.string, - textIcon: PropTypes.string, - }).isRequired, - stats: PropTypes.shape({ - connected: PropTypes.number, - error: PropTypes.number, - errorCode: PropTypes.string, - errorTime: PropTypes.string, - }).isRequired, - authType: PropTypes.string, - onToggle: PropTypes.func, -}; - -function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle }) { - const { connected, error, errorCode, errorTime, allDisabled } = stats; - const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX); - const isAnthropicCompatible = providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX); - const [imgError, setImgError] = useState(false); - - const dotColors = { - free: "bg-green-500", - oauth: "bg-blue-500", - apikey: "bg-amber-500", - compatible: "bg-orange-500", - }; - const dotLabels = { free: "Free", oauth: "OAuth", apikey: "API Key", compatible: "Compatible" }; - - const getIconPath = () => { - if (isCompatible) return provider.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png"; - if (isAnthropicCompatible) return "/providers/anthropic-m.png"; - return `/providers/${provider.id}.png`; - }; - - return ( - - -
    -
    -
    7 ? provider.color : provider.color + "15"}` }} - > - {imgError ? ( - - {provider.textIcon || provider.id.slice(0, 2).toUpperCase()} - - ) : ( - {provider.name} setImgError(true)} - /> - )} -
    -
    -

    - {provider.name} -

    -
    - {allDisabled ? ( - - - pause_circle - Disabled - - - ) : ( - <> - {getStatusDisplay(connected, error, errorCode)} - {isCompatible && ( - - {provider.apiType === "responses" ? "Responses" : "Chat"} - - )} - {isAnthropicCompatible && ( - Messages - )} - {errorTime && {errorTime}} - - )} -
    -
    -
    -
    - {stats.total > 0 && ( -
    { - e.preventDefault(); - e.stopPropagation(); - onToggle(!allDisabled ? false : true); - }} - > - { }} - title={allDisabled ? "Enable provider" : "Disable provider"} - /> -
    - )} -
    -
    -
    - - ); -} - -ApiKeyProviderCard.propTypes = { - providerId: PropTypes.string.isRequired, - provider: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - color: PropTypes.string, - textIcon: PropTypes.string, - apiType: PropTypes.string, - }).isRequired, - stats: PropTypes.shape({ - connected: PropTypes.number, - error: PropTypes.number, - errorCode: PropTypes.string, - errorTime: PropTypes.string, - }).isRequired, - authType: PropTypes.string, - onToggle: PropTypes.func, -}; - -function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { - const [formData, setFormData] = useState({ - name: "", - prefix: "", - apiType: "chat", - baseUrl: "https://api.openai.com/v1", - }); - const [submitting, setSubmitting] = useState(false); - const [checkKey, setCheckKey] = useState(""); - const [checkModelId, setCheckModelId] = useState(""); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); - - const apiTypeOptions = [ - { value: "chat", label: "Chat Completions" }, - { value: "responses", label: "Responses API" }, - ]; - - useEffect(() => { - const defaultBaseUrl = "https://api.openai.com/v1"; - setFormData((prev) => ({ ...prev, baseUrl: defaultBaseUrl })); - }, [formData.apiType]); - - const handleSubmit = async () => { - if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return; - setSubmitting(true); - try { - const res = await fetch("/api/provider-nodes", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: formData.name, - prefix: formData.prefix, - apiType: formData.apiType, - baseUrl: formData.baseUrl, - type: "openai-compatible", - }), - }); - const data = await res.json(); - if (res.ok) { - onCreated(data.node); - setFormData({ name: "", prefix: "", apiType: "chat", baseUrl: "https://api.openai.com/v1" }); - setCheckKey(""); - setValidationResult(null); - } - } catch (error) { - console.log("Error creating OpenAI Compatible node:", error); - } finally { - setSubmitting(false); - } - }; - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/provider-nodes/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: formData.baseUrl, - apiKey: checkKey, - type: "openai-compatible", - modelId: checkModelId.trim() || undefined - }), - }); - const data = await res.json(); - setValidationResult(data); - } catch { - setValidationResult({ valid: false, error: "Network error" }); - } finally { - setValidating(false); - } - }; - - // Helper to render validation result - const renderValidationResult = () => { - if (!validationResult) return null; - const { valid, error, method } = validationResult; - - if (valid) { - return ( - <> - Valid - {method === "chat" && (via inference test)} - - ); - } - return ( -
    - Invalid - {error && {error}} -
    - ); - }; - - return ( - -
    - setFormData({ ...formData, name: e.target.value })} - placeholder="OpenAI Compatible (Prod)" - hint="Required. A friendly label for this node." - /> - setFormData({ ...formData, prefix: e.target.value })} - placeholder="oc-prod" - hint="Required. Used as the provider prefix for model IDs." - /> - setFormData({ ...formData, baseUrl: e.target.value })} - placeholder="https://api.openai.com/v1" - hint="Use the base URL (ending in /v1) for your OpenAI-compatible API." - /> - setCheckKey(e.target.value)} - /> - setCheckModelId(e.target.value)} - placeholder="e.g. gpt-4, claude-3-opus" - hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead." - /> -
    - - {renderValidationResult()} -
    -
    - - -
    -
    -
    - ); -} - -AddOpenAICompatibleModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - onCreated: PropTypes.func.isRequired, -}; - -function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) { - const [formData, setFormData] = useState({ - name: "", - prefix: "", - baseUrl: "https://api.anthropic.com/v1", - }); - const [submitting, setSubmitting] = useState(false); - const [checkKey, setCheckKey] = useState(""); - const [checkModelId, setCheckModelId] = useState(""); - const [validating, setValidating] = useState(false); - const [validationResult, setValidationResult] = useState(null); // { valid, error, method } - - useEffect(() => { - if (isOpen) { - setValidationResult(null); - setCheckKey(""); - setCheckModelId(""); - } - }, [isOpen]); - - const handleSubmit = async () => { - if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return; - setSubmitting(true); - try { - const res = await fetch("/api/provider-nodes", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: formData.name, - prefix: formData.prefix, - baseUrl: formData.baseUrl, - type: "anthropic-compatible", - }), - }); - const data = await res.json(); - if (res.ok) { - onCreated(data.node); - setFormData({ name: "", prefix: "", baseUrl: "https://api.anthropic.com/v1" }); - setCheckKey(""); - setValidationResult(null); - } - } catch (error) { - console.log("Error creating Anthropic Compatible node:", error); - } finally { - setSubmitting(false); - } - }; - - const handleValidate = async () => { - setValidating(true); - try { - const res = await fetch("/api/provider-nodes/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: formData.baseUrl, - apiKey: checkKey, - type: "anthropic-compatible", - modelId: checkModelId.trim() || undefined - }), - }); - const data = await res.json(); - setValidationResult(data); - } catch { - setValidationResult({ valid: false, error: "Network error" }); - } finally { - setValidating(false); - } - }; - - // Helper to render validation result - const renderValidationResult = () => { - if (!validationResult) return null; - const { valid, error, method } = validationResult; - - if (valid) { - return ( - <> - Valid - {method === "chat" && (via inference test)} - - ); - } - return ( -
    - Invalid - {error && {error}} -
    - ); - }; - - return ( - -
    - setFormData({ ...formData, name: e.target.value })} - placeholder="Anthropic Compatible (Prod)" - hint="Required. A friendly label for this node." - /> - setFormData({ ...formData, prefix: e.target.value })} - placeholder="ac-prod" - hint="Required. Used as the provider prefix for model IDs." - /> - setFormData({ ...formData, baseUrl: e.target.value })} - placeholder="https://api.anthropic.com/v1" - hint="Use the base URL (ending in /v1) for your Anthropic-compatible API. The system will append /messages." - /> - setCheckKey(e.target.value)} - /> - setCheckModelId(e.target.value)} - placeholder="e.g. claude-3-opus" - hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead." - /> -
    - - {renderValidationResult()} -
    -
    - - -
    -
    -
    - ); -} - -AddAnthropicCompatibleModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - onCreated: PropTypes.func.isRequired, -}; - -function ProviderTestResultsView({ results }) { - if (results.error && !results.results) { - return ( -
    - error -

    {results.error}

    -
    - ); - } - - const { summary, mode } = results; - const items = results.results || []; - const modeLabel = { oauth: "OAuth", free: "Free", apikey: "API Key", provider: "Provider", all: "All" }[mode] || mode; - - return ( -
    - {summary && ( -
    - {modeLabel} Test - - {summary.passed} passed - - {summary.failed > 0 && ( - - {summary.failed} failed - - )} - {summary.total} tested -
    - )} - {items.map((r, i) => ( -
    - - {r.valid ? "check_circle" : "error"} - -
    - {r.connectionName} - ({r.provider}) -
    - {r.latencyMs !== undefined && ( - {r.latencyMs}ms - )} - - {r.valid ? "OK" : r.diagnosis?.type || "ERROR"} - -
    - ))} - {items.length === 0 && ( -
    - No active connections found for this group. -
    - )} -
    - ); -} - -ProviderTestResultsView.propTypes = { - results: PropTypes.shape({ - mode: PropTypes.string, - results: PropTypes.array, - summary: PropTypes.shape({ - total: PropTypes.number, - passed: PropTypes.number, - failed: PropTypes.number, - }), - error: PropTypes.string, - }).isRequired, -}; diff --git a/9router/src/app/(dashboard)/dashboard/proxy-pools/page.js b/9router/src/app/(dashboard)/dashboard/proxy-pools/page.js deleted file mode 100644 index 36db1eca..00000000 --- a/9router/src/app/(dashboard)/dashboard/proxy-pools/page.js +++ /dev/null @@ -1,489 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useMemo, useState } from "react"; -import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle } from "@/shared/components"; -import { useNotificationStore } from "@/store/notificationStore"; - -function getStatusVariant(status) { - if (status === "active") return "success"; - if (status === "error") return "error"; - return "default"; -} - -function formatDateTime(value) { - if (!value) return "Never"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return "Never"; - return date.toLocaleString(); -} - -function normalizeFormData(data = {}) { - return { - name: data.name || "", - proxyUrl: data.proxyUrl || "", - noProxy: data.noProxy || "", - isActive: data.isActive !== false, - strictProxy: data.strictProxy === true, - }; -} - -export default function ProxyPoolsPage() { - const [proxyPools, setProxyPools] = useState([]); - const [loading, setLoading] = useState(true); - const [showFormModal, setShowFormModal] = useState(false); - const [showBatchImportModal, setShowBatchImportModal] = useState(false); - const [editingProxyPool, setEditingProxyPool] = useState(null); - const [formData, setFormData] = useState(normalizeFormData()); - const [batchImportText, setBatchImportText] = useState(""); - const [saving, setSaving] = useState(false); - const [importing, setImporting] = useState(false); - const [testingId, setTestingId] = useState(null); - const notify = useNotificationStore(); - - const fetchProxyPools = useCallback(async () => { - try { - const res = await fetch("/api/proxy-pools?includeUsage=true", { cache: "no-store" }); - const data = await res.json(); - if (res.ok) { - setProxyPools(data.proxyPools || []); - } - } catch (error) { - console.log("Error fetching proxy pools:", error); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchProxyPools(); - }, [fetchProxyPools]); - - const resetForm = () => { - setEditingProxyPool(null); - setFormData(normalizeFormData()); - }; - - const openCreateModal = () => { - resetForm(); - setShowFormModal(true); - }; - - const openEditModal = (proxyPool) => { - setEditingProxyPool(proxyPool); - setFormData(normalizeFormData(proxyPool)); - setShowFormModal(true); - }; - - const closeFormModal = () => { - setShowFormModal(false); - resetForm(); - }; - - const handleSave = async () => { - const payload = { - name: formData.name.trim(), - proxyUrl: formData.proxyUrl.trim(), - noProxy: formData.noProxy.trim(), - isActive: formData.isActive === true, - strictProxy: formData.strictProxy === true, - }; - - if (!payload.name || !payload.proxyUrl) return; - - setSaving(true); - try { - const isEdit = !!editingProxyPool; - const res = await fetch(isEdit ? `/api/proxy-pools/${editingProxyPool.id}` : "/api/proxy-pools", { - method: isEdit ? "PUT" : "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (res.ok) { - await fetchProxyPools(); - closeFormModal(); - notify.success(editingProxyPool ? "Proxy pool updated" : "Proxy pool created"); - } else { - const data = await res.json(); - notify.error(data.error || "Failed to save proxy pool"); - } - } catch (error) { - console.log("Error saving proxy pool:", error); - } finally { - setSaving(false); - } - }; - - const handleDelete = async (proxyPool) => { - const deleting = confirm(`Delete proxy pool \"${proxyPool.name}\"?`); - if (!deleting) return; - - try { - const res = await fetch(`/api/proxy-pools/${proxyPool.id}`, { method: "DELETE" }); - if (res.ok) { - setProxyPools((prev) => prev.filter((item) => item.id !== proxyPool.id)); - notify.success("Proxy pool deleted"); - return; - } - - const data = await res.json(); - if (res.status === 409) { - notify.warning(`Cannot delete: ${data.boundConnectionCount || 0} connection(s) are still using this pool.`); - } else { - notify.error(data.error || "Failed to delete proxy pool"); - } - } catch (error) { - console.log("Error deleting proxy pool:", error); - notify.error("Failed to delete proxy pool"); - } - }; - - const handleTest = async (proxyPoolId) => { - setTestingId(proxyPoolId); - try { - const res = await fetch(`/api/proxy-pools/${proxyPoolId}/test`, { method: "POST" }); - const data = await res.json(); - - if (!res.ok) { - notify.error(data.error || "Failed to test proxy"); - return; - } - - await fetchProxyPools(); - notify.success(data.ok ? "Proxy test passed" : "Proxy test failed"); - } catch (error) { - console.log("Error testing proxy pool:", error); - notify.error("Failed to test proxy"); - } finally { - setTestingId(null); - } - }; - - const openBatchImportModal = () => { - setBatchImportText(""); - setShowBatchImportModal(true); - }; - - const closeBatchImportModal = () => { - if (importing) return; - setShowBatchImportModal(false); - }; - - const parseProxyLine = (line) => { - const trimmed = line.trim(); - if (!trimmed) return null; - - if (trimmed.includes("://")) { - const parsed = new URL(trimmed); - const hostLabel = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname; - return { - proxyUrl: parsed.toString(), - name: `Imported ${hostLabel}`, - }; - } - - const parts = trimmed.split(":"); - if (parts.length === 4) { - const [host, port, username, password] = parts; - if (!host || !port || !username || !password) { - throw new Error("Invalid host:port:user:pass format"); - } - - const proxyUrl = `http://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`; - const parsed = new URL(proxyUrl); - return { - proxyUrl: parsed.toString(), - name: `Imported ${host}:${port}`, - }; - } - - throw new Error("Unsupported format"); - }; - - const handleBatchImport = async () => { - const lines = batchImportText - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean); - - if (lines.length === 0) { - notify.warning("Please paste at least one proxy line."); - return; - } - - const parsedEntries = []; - const invalidLines = []; - - lines.forEach((line, index) => { - try { - const parsed = parseProxyLine(line); - if (parsed) { - parsedEntries.push({ - ...parsed, - lineNumber: index + 1, - }); - } - } catch (error) { - invalidLines.push(`Line ${index + 1}: ${error.message}`); - } - }); - - if (invalidLines.length > 0) { - notify.error(`Invalid proxy format:\n${invalidLines.join("\n")}`); - return; - } - - setImporting(true); - try { - const existingKeys = new Set( - proxyPools.map((pool) => `${(pool.proxyUrl || "").trim()}|||${(pool.noProxy || "").trim()}`) - ); - - let created = 0; - let skipped = 0; - let failed = 0; - - for (const entry of parsedEntries) { - const dedupeKey = `${entry.proxyUrl}|||`; - if (existingKeys.has(dedupeKey)) { - skipped += 1; - continue; - } - - const res = await fetch("/api/proxy-pools", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: entry.name, - proxyUrl: entry.proxyUrl, - noProxy: "", - isActive: true, - }), - }); - - if (res.ok) { - created += 1; - existingKeys.add(dedupeKey); - } else { - failed += 1; - } - } - - await fetchProxyPools(); - setShowBatchImportModal(false); - notify.success(`Batch import completed: Created ${created}, Skipped ${skipped}, Failed ${failed}`); - } catch (error) { - console.log("Error batch importing proxies:", error); - notify.error("Batch import failed"); - } finally { - setImporting(false); - } - }; - - const activeCount = useMemo( - () => proxyPools.filter((pool) => pool.isActive === true).length, - [proxyPools] - ); - - if (loading) { - return ( -
    - - -
    - ); - } - - return ( -
    -
    -
    -

    Proxy Pools

    -

    - Manage reusable per-connection proxies and bind them to provider connections. -

    -
    - -
    - - -
    -
    - - -
    -
    - Total: {proxyPools.length} - Active: {activeCount} -
    -
    - - {proxyPools.length === 0 ? ( -
    -

    No proxy pool entries yet

    -

    - Create a proxy pool entry, then assign it to connections. -

    - -
    - ) : ( -
    - {proxyPools.map((pool) => ( -
    -
    -
    -

    {pool.name}

    - - {pool.testStatus || "unknown"} - - - {pool.isActive ? "active" : "inactive"} - - - {pool.boundConnectionCount || 0} bound - -
    -

    {pool.proxyUrl}

    - {pool.noProxy ? ( -

    No proxy: {pool.noProxy}

    - ) : null} -

    - Last tested: {formatDateTime(pool.lastTestedAt)} - {pool.lastError ? ` · ${pool.lastError}` : ""} -

    -
    - -
    - - - -
    -
    - ))} -
    - )} -
    - - -
    -
    - -

    FI5=y0b?W44;|oOYCMgo`eAw0wMf0a&SnV#z(mbKYSu7$$epm z<&MC5>dnCd-qB(A)B@pdBPd3f&!guWlu%wEY^zjv1W2)DD97k-N_>mye!=e<|58}p z@3fgei~tjdef%+|v&zMlg`a_-rtKCLiTwdAxH#;!q~phM1poj5 diff --git a/9router/public/providers/kimi-coding.png b/9router/public/providers/kimi-coding.png deleted file mode 100644 index 422b7f96289368f94a5e85a25dd31e36288acaed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18477 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 diff --git a/9router/public/providers/kimi.png b/9router/public/providers/kimi.png deleted file mode 100644 index 422b7f96289368f94a5e85a25dd31e36288acaed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18477 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 diff --git a/9router/public/providers/kiro.png b/9router/public/providers/kiro.png deleted file mode 100644 index 166c7c72c9a680992ce46ff35f616469fb19ee8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8905 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 VRN6TN|MS;}f~<;6jg(2~{{g0vJAMEF diff --git a/9router/public/providers/minimax-cn.png b/9router/public/providers/minimax-cn.png deleted file mode 100644 index a8b9bf7ea6080912467af2906da7026b08f84649..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15349 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 z 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
    noGX4}hdxD{46J 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 z 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
    noGX4}hdxD{46J zbYhwZ8vr54P5(56pJad#t>ACzfvOr!?{!&WMVO3&W5Efv_jAKoV!`ED6M~S6W0ZUb4nA)XCb%IZ3YE<0Ea3%yhPtisRY_T} zJTL5U#HbSWeE`$_!YGPY{cTwf+8sTVZj}9(dMNQ zfG#2NRRQ8yYXJtdgR9x{wB|Iv{AR??2DYp41Y#Ra;vUhgHnLE)BP8O-A!=WKh7zB2 zzSw4PqS{qf1@3Fdv!lv&*Ind|dbO{vZ(ZaGDrdvbges~5Yk$y>rW7;@%@`DykDeLI z&?8w$ij~;-f5y&#NH}RrkSWdS#IJ)2%&B;!zvecA`!kgF0au411v)0*P)e`R(JsbB zesJozH4JIj_C;S<2$|zE0g z3X6U@N?74n4=Lh*qmk}!0Z@tiKkegu@!k*^xW{qn*c zb2Jz6y)GGzR9ZlP5W(yPvQpBtM0O!AqtT#AtvhO#^h%d~kL6&zslrrX6kXw548dtAIiZ7M=Y6}JEI)>9GCwRJL!AcwLMafuc^S%>R1w5VR zf*ubXD*&ACRKH)}M@o5ppfqjAk{hhSELLdb$J>dd5PdL@hqcXDL>RmwUdmuN(l;+Z z5YFokJ#}8!C&6*?#CPZ1{IgD1g!_GdI_48PclJ!Gkna=z`3~~FOC%JHz~sC4);xv9 zQ@!4H@9!p1ze`x8~S<6i=K2^0{K6TWh_~`le6Xd-ef+14H`_eb0MxR3PBlseTs8znD-n-0T;Y{B8Xj z&6QRB3)=aQKK~84@^u@g&S+JLUGaNEs}ILO-oxx?X2 zFd%bF3_=4qV*1b{l=Ec=%gIVP;EezC=_F->N4hDDqSJhj{DInE%H-br-w(KY2nB0{ z-*odnvhas1c%~dVg4>WdE_t*-Ht`FNQF=?CVwQ-hl`A6eQjW@`3lmn8%9q$r_@?pw zr999u3ye9jw}y?Ked8YxF0yw21%qyoYP&{!A=&lRc<>MWY^a3bl&wFlGF2j8ptCxGqNoy&oj#gFSaia?s(*WJb*lwjZpgT* z>ae+6=6>`uw!I`-ef_}tAS8^k*XZK^FFYUsW!XT&*sspy`50sE-%qf-7qUo!aOcR* zZIz?`cc-aux6`^VY$NnwqJPD9fh)7FuxHABl$Yg#0_d&b9-fS_w!;!oyot<=VT#eI zX`Ez?d9{P)o!B;d0vxAl`zrlH;7enG!KxSX^M>?bI8BQ~rQ*1#Y`26@rdN+lK=(bFi(2v?%hu%Kr0O1?$+L%*NL~#iBUW>S+ zVHoJ?Qj@U0u*ZMvXT_#^n9FC>J%0XP8LWb=J5+0>19@R$>|yVl>@kF3lJO9x-!>W) z*(pTV__iWqmnpV)Bt_RWQ9<;qZ7aQa69uKaMuVcTusb--@*#ma;hQRcArDqJzc79F zSba3NahEvK<{vwJEGIal(dIH55yUhUtD94#k}0k!D-J`pE?UP|@5F2sL`12VJwzZs zk=*3j=eu$%uo{rG^Ymr#$MCYy;|qdDou+VhcR&6DZPE!dtqP0m$CEK0KAh-eaZ7%m z1p5qGZneE@E)N>b?*-q_{ONQ1q?Y=;M~GvBk|y}^&SD3&Rs=Ds8dHW=gF%SKT#JC& zsQB@}NRn^OTsP7UUIAF*7OOSgmT7NRFagCmtqv0QKRAe>5q+xn@abVn!LvQB6jq1j zE}NUtRE1b~k_!&edaJW%&^Qu!j09Z{DdJJlb zR)4%t;vy!wehx#44A4hreH7_ik2M1sarq7?ZVe3Ez$zit8a#g2HLOd1;L-F}D#Fso z1?GUBCKAbYI;+!cTl1wgk!M@UJ%VC_Xp?M(yX1+|#V7aW3Ik*uuSvuue+hvsW+D<= zrYag=F+yKO1_3H>m3p2YK`v@)Rfi&3Ng~b9Z_yV$Z7gdRg8QG8N*{ zG@UH0W0eHqQ+mGN0fqgCuD~xLjRZnaaY79NJ~NDsE!ShV=?fGi3A`951aIKx1*;0U z{Fe*xr2U;?!zCYEFV^{F-lqrjxOyns(dkCSZ7LT<5#2@!S4~$cjBUCKId@ zO$n_0EJE{!lkNSKWt+P8_f|R4jj95DqVZT9$Tsnst;r*Hy?!t6M1k-T3#vM-7ExpN z&Sb$|NK7(1vYl32K`qHMdUXU@k~Z+@xPB%JW}%wFXEs+Y9g10Kw8+$fB^qg=%6+LS zrU*OTZ0YQaEY#4$`r;3nS?LYnt82J6>Q@7$Nr$`L2>rP<()GSpuRE=IExU{k&!9|L zwb(8V_}djc(8P{rAk1g04s6o8SYMjT37b4Hw~VG%Ey6@ktBDO{wFVpPflz{RxQ6Ju z!_3zJIH>&(4B~Z^KwP416Y#z%W(1E+NZt1% zuVZiSGk=Vd)}~hUKGc*|@)tWoLPmR5SnaUd|I91a25U#&YeMbcqe(SqW-;5*a65n> zS^Mi+9qFt$5e2a%91&h#(#x$8FxC%n(rWHLIEk4&96!$?xcgqvesaZygs_Ai?x-;Y zNB_(-dAz8m3Z$m{_m4@l249P4;I6 zXmEXxWIa}3l(c=s$B=uK68-d>GW6zOlZrfCN5L`i$pQaF61he=l2dW9&wG9}U7!L% z`?br@%lDPbityTV4$pY-S#YJ#Z(}q%zrvSE{B6%?cK2P-A`05lzkz<+5=4n(JmMuJ z^;X@JcX);D%7xtB(%}ehE&lTCGJA{fKVzEjo&l^Dm)@imo}Ug-sdcKNGC_;n%6)>hA1Dw}mV6G}m4D=wRiT zJ3W_kSL`F{1p>HGA$%O^n2Uv&T5NQU6a%9qM??%gU(o+oXyu0|sx$Pg97h>*{xUSv>+%?q;F&O*`vh5E zYxlRvRAq@33p*6}SGudX9x1{xCb+WWcRD77()+!f3sKQsk?7a*-$C8|IAIFSE9{n= zUXZ4XKHw)=!Pf(<|LO_$io79W)tJ0*f2>9~cdqY{yRf=l9@+tiH<`p_KIyT;<1TDQ z@$4+(cJuPi_PSP%*YXc?N_67|G+9{wYTf= zyc#D`Y`7~PzrJ?c9Yz+`PuaCt3h63oAjl0aVS9*D4${6SpNQYT2$N@mT9Z=yu3j(T zgK2D3299Nc6Fo@FvdnJbKeDz*r&1hljc%SYK*$5IV?RfLlRzFq>^MY{2hhn#oDrL|bR6jh7xsbd0DYT)Y z6KYZ_bh#aAHs}MUy=!W)ud?$j8F`Mq{B#K^C<#@4V%m?SQvC9d!@=GW`2EjM-d=YE z9b`obTq>Y-|~HWuHo1D&GAFecfDT^b4h4xD64mao&) z;Bv6IZ4nj+fqG>Y0nv+VLn*$C@8xWtm?|FellDm3;b}d7 z2Ut~zXJ<%M&~(c1#@&J??>1gaU(hIu$XK9@@sL9F!S2ukfp59Lq)=+Z1`3@}Cv%p5rPRAAA({cG0@YP3X_(@OF8kM6 zj20|xb`2EB4HWwpjyUF#)^H&-iuHS&-;**L zctckGs2oM9t+;BOXgQy)enss5?sZ~d@eF3Kk$2>2-e@p4k}>_1Gu%rzQ3od z<~YA=oL(RiGARTcbb(laE=5>Zk)*oFE2pQ_vWX;!l_uW`&j#T0Ngp|d#BRVRfO z0)mf?WS7umuug*(=vlR_GE&tFEi)aQts>@@J-rp?@=-0No*V};?)0P~8yvNFl^Uh8 ztP`j2=K|$JJJkhT2^#Z*!Lu>>A4{4`Z!F!Q7yrUtsZcz(&B2aTzlWsSj8l}4IMB=5P1Gy&|Z zYqTvM9b;$fg*TkZ^YnS#N7ci{pB_-#P|2^9>mY$yzwy0Ya?K`*!P}@FHo>O9AeR72&*rN{DQvO*uBV%^ecmlIE+|J9I)Y&0%yH-gsf&}1epFpE!$Rru#e zn!8ae;@bOZl`!r{Y|B_s9Yr|a!=%Xvst^e(W>pE-?ZQ06WM+1_W3qAlYili3F|R3+ z#hH*7rG?def1jQ*1v8kiUL1N7ZcJ8E6sN~a^69#%Hbo31+qXxm2bj4)R%q~irDhuC zl}Z=#gEI~J(`D4qzy5ca^!QbSvgE;7^48OREGs-rDvOhGPSvi%iB9axR7{GkpAmUA ziOf44X~|>dKA{AeOAB57mzO2lyBI~FB8W6w-syw?9yhLe48F6?r2XT4b1K=ARhn^H zUaNu^#iiFcLtrS|+#SF2rP(9IQkdsmV2V6Jjm1S8kSdYEPP+c$7OoHGq7AR7*L8Kw zbM(g+2J#4R=gQz*PaSg|%jka93!NKxsO^U;iu&$7QWL~&7kR9}_GV=A{9e7a3=_g> zo#0)!i$Q>a+P!>tdrs5GIaz;7H55kHN0;QIL=JaaKKCE&d_odnXG5S9Pl9Z!JPzCK zqk%b%P)A<78kD?xZ60L%7=(CHfC|=N7R2b($c!_}IK~Nd)Yr7+3y^Liba*4OZ8Cy& zGTn4TRe}lKAA~Uy)AiK7z2>ACz?zs@tNI%KQJ4obJLWsSJoeB1*-E?Ns$C5*X(KjW zD0%ZTw_^15E2(^+>GD2E@-6O$HGI11eHi?Fh2i5PC7Zgz1j4^8TzWs6d0yd&Oe>{( zy^H|9IZ_(+lwa>)78Q`JZF7ksNh&N$8=kOQSU{DQLZVZR_0%Mh)mJk$D(C)Kc?q&B zoC=J@d8b_-4u9?CL#u9#fSDPs9oLu2H%3Y1=U6Q$T)_pf9eEMmWj*tb;r)NbL;nga zubM3NpM7ejA~BLthe6-sgqFoF5A%{+p0JfcssV;^%Am6moi z6hhi*#f}regv-f=Qmac|X=3`zMSz6obD@Ty{(>hid9*^%eE{Z+xYy2JHkq6b30Jwm z&vN<2B?b^~VaffXq~=%q2n_OOjr(Z4!e>Et*dChg2s0*ql6H0nJ4P0F&3LG37Ki{_ z@s}Q-MB7-%zLSPGT_e~yyJ_Y$aBq#CppXSX%z=`N>;13u38`MPgVn_g=40^4o(_0@ z`qSi`+Q*P*L{XkEt&hMSQcG#LXDNtRBU>KZ`2WgBXex1rvid&GrG09E%J@I^~Gt@ehWAa(eP zBS*VdYbYOIVJU~g6kwn$SgZ;_abq`4{xb6hF-9w-Hzjl-pxwfy$;-A^&<9-eh3E&n zw1Ts{nUr^JylEa~_WQJaAhs0xCWl6C=lyrccbzSe^z~_- zcc1opkk59Lr`sfM4bfQsrQn}8AbbzbXiQ@*;!F%h=>=4Fm#-wD2CwmTl6VOmp{|hqLbRAt`e|glj%CS2*{>-&^y5FQsV3N zP|fJ2y4C8jl5KwzuCP;wn+PX1U^PK5NdMdyXi4efL)-D0&^lTGX}4^6zO6&{{FwGw zgYZ10xKY(?$;nzacHlf%z$!2&2Ri%FRO4%wpkyT%e<9r#I`f!qMC>q|GBkL#j7FpC zGuskh6)o;y74y*eP&B@h+`z{lTmN35hc8IQZ=tEtaCz39BknVkK4#^hmrpZqJS=Zd zk#7ppIE9VqF5{jmC1%Vc{fgvvT4GQ&w%VLZS{re zhAQf=jT#wqG(jlAc*_{Zw8}i{Ih|{NbXjaS20p5~Y*$TusP@3`uD+r3XC#ubIT@49 z8V~L?a7AHPUPB<-yTh8=0oAL_r1it4NNn8PvamrN<6q5#cP*bo+4a~1t*%8 z1B+?4;eGwtG3wVrE5Va@yo|)bIW0W7a~oNL5e5^ATcfZ9ZGPv(uyUtPSN8v&>#X)j zrpa?9-*%V|cIJNWNZc@vdAD8X@*1aNLj~^Ev_pY zyjYJb33Y$MWCHSO{!D7^1}F1jgg2UI-kJOK{dJHh?8+!ql6TGl9U|G1*q6Uz+rH#U54|uGqDM zc5)ZEgq3QsyYa@}oR)OacjOu47X8ewJb}40IV$Wn!9(f*CVWmKf_c_5k>DbAEV5 zyD&&HJ2>Wy_LFE4)%rZswXDugB%9 zr#se#8Id!HX3*^(urE z5B5zU=mYcXH=(zgmh{sf23;F}(arVir-r{hr?leNvDJ2qy2j9s$(719ypyVx+;DKr zEp}N8BDQ{oD|8HBS?RG3X6`9kxbJW5JhD6!JtX)Fv-%VG-LRDG*nFtDdODZ7+Z~B) z*72ef;o5w;4WjiqMgj}6kCNk9N zGGUhU)VH`ca|HAi&=j-{y>+*O#9?!?)`Fh=Ra7zQ!D@kg^& zB8gKSLq5M=px^ZVJ*GelyANw^PuOrGy~wF?N-u8F!X`OgR&?qd+okkG>%8=P&w*2% zYPze7?p}?SQR!FA3K2gbU@C`3JACxY+zoBf#&9McAIY7#HIbbOrOGSzwy0}jPD{D4(D{cyh3A9>V#OTgOTv zX3`hc6fQ|W_uA+p8tXo?@STWp9jtS_8{^fNkNRlfH0Ze42~;dmiHW^3P7{%PWg@!s zT?acdyWlEh)bvhzJ7gc{W9m;WG>7`FB{XvZcAx|hXn9D9T*8q;TnvHJoY*+JJbA;!!@brv3D@sO9rCf!%K-3vc43k}$#g;e z3QzW*L{c@-2Ra|hRDWZ?96?$yduFFk!%2l!^f z8Pd?_rDbki=fFk5C%C#lsATa23#QESOqW+mZ+hjdU66#3Sm4Q?^T!M1uV*oE!;#Z- zBdsyJIX@JR4h6bf{#<5G?dZ^?HaE9yDoC~pmYh4NBbG~yn)Ax&Fn;YBrUUI)0fdb9 zYgIH}dBKoQUB^iIT1MalkfsE#R9va*nG==IMu`aZC%+OoKg_mdhl;1{m@ZNd(Qe9N;Tf2e$ol;eOSc=Mn=tC3@f|5a zzc-&TU`DsfIK2TTp$>e&iZm5e|GifYdR>w>>ol3*Ci+yl7f)yOjXQtmQf`7vtF{S_ z$BZtjog=ynXJT^6^JSrpP|6g^@4Cd%zHKKUj-@svOt!{li_$VIyWBqX;@2;G>Vvk| zfBAi8N8;D2|2Bcf*s`)nFrwQ1GIAh(!yCXsdL~3L$eI4wJ*>zKgwf|Aim{siV9p*k zt|KZZShl9Thw?IiB2>lfx_4=FlgF%(EAqwS9!7+)qRV38bO=U5a%8q(3p4;1LP9^} z?U0-bU;&~K@~~fb)6w+p=5fG7N)tZjtfv81U`VK1*$T-SAz9GhRmLU}PMh5087*l@ zQr&=p4@C}(87SFi5mf-mJAx#FKY}v-KR@%rlXjz0sx2s`%2b!1l{oCMR&hqm^KKgj z&0ut8$;uN+*FLzO24mJ5mB_Ns#77ev%( zIB|Ys2U3fyM$vCW$cGW$wkX1Agx`Mw{eTmNTjk%`slTnia0LVF!9X!^R@fRrMI(P04 zdA2Unv1H!_0MH750RhF=Q~*E?Iyn$2$=4Q2{1Y^~6(7$)dQ&T9+@EX1TMMkxhY<6Fi5EgC!31GWVg*8R8R) zTl_$g`vWA^)L|#z-GA8UP8WW7e3$C=`kBNh<*xW&7*$y=PP9pu_Gxc_PF}qx{htRX zobW8%bjP_lJ(k)P4J*IPNk)Fg(?t%FK)o&dFHg88Ogs&_&vG|P`@6}bIZP~Z9=w^dor`XxL&bd*B zLBd@(CHGV8Magt~$-Arcwf8ZFJ^gYBz!PZy9Jg6+WGP0JIb*J^&2ec(LxD_Nn3hbNpx#0_j3Zo|_jl3@} z9b3zGNQ>u$tWqS2fN<#+1a^AEaJ-LIH)h*K^>G(ocXJv*fW?&`O8e=%@Bzb-nyx;W z_S)Rw_9h$ZpZ}B$z`*K439=bKtIS?Y7(W55%;n(Fx}#C&0Bbu(UnHII-Q6m7Yhzp# zdj!l+n9Z-`1U%pQIyd(iLWQi@+&0~+`BOrV`<>k52c}%H78gow6>XNtFk^GS=B29{ znmMza#SffHe5dZoplWKQuAY<)MYUHQ>mCHj}P>#<) zS%-;uX!%-F81s;kqQ2G3p8h5^5U|>?ll^DB#7K(A6$-7@irpr&pWpks*7gGg-%8Pm z3-s+K^cm^tpGzp~5DAY+f$sECVVY%C(jR*J{n>KJELqFMEUf#RF7QZOiv@1t5#%XSbO`dHE+B`ZV zQE!&(n+X(($N&x))XrG`2j%;RyKZaKmtOcIFC;?k{-nlYreEUUyrw`tVj)J^NQ8Ob zVnVX>m=37lH&CrFTDj&Rx^$TUF?K96%$s=;=6q1|?HfVNCh(*q^;yPq7>XeSd9_M; z>c70wUEg`MWqGzJ4lG^qxqBt32e!C;Cd`^~yaCC&8bsG<{(zUW*IV%W zT3GiG0hDP(e>+cbQ=hk5)W_umZ@|zxkfS`)=Rs{<+b9CRY!kw2w+}KqGj7J0nOJ`{ zuCB1=wb_-9NUV&%l={DlNAy{NZA^eeF$@6_m?E%n`$&)u!sW?WumD9tIZ-$w7?xW1 z)rfG2yM8_nL?9I4_D3La$Uq2|2>w$eW9^UUW+2Y&$dNt$a)woI== zhXMx$68B*p^d#*huINoBGQE>{3t0Dpvxb?Qb3?NaE6HXSj2+DIMq(_ZoE4JDwE=jD z7P35kFjZ<#{3la8+%Vh%-4OhnyyECzQjjD741!{8@}uv-ASA>r+&xmGEj)U(Ne21& z3A~bu%`1QG=6>(n`-$`#c{I}bNlg_~?fl)cR9JtTxni9_N|%bjL+0QlFYJ!5&+hcG z4`M3A#ITAYLQLy+pDFo@f_x2cRPDD=c;=>kUimH!l+Zt>@D|MEXvgZ6@*?p77>g7C zvqB$iyd~Q5X0tsDXLI^|N=0$~J9^sr;A{&oSRFyt1%+HtS7JZt_pS7K^83q1*+$PX z?7YEG>hrk9<-}h~T8qZS`22z@?f=+7R)rT}Go+}|Roy*dcig4tkOo~(Jvcjw(>1Bx z@lHcv&fPxamCUyc79tM_)?);jijzvHo~~(_S}+T2gTO{{Vj6 B7x(}G diff --git a/9router/public/providers/nvidia.png b/9router/public/providers/nvidia.png deleted file mode 100644 index 9215a386ff3c981fe7522bd24363d233dea3228b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2582 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 diff --git a/9router/public/providers/oai-cc.png b/9router/public/providers/oai-cc.png deleted file mode 100644 index 56a7a3e618c83a9887597e2c3ea45f75d5e7ea5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10137 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 diff --git a/9router/public/providers/oai-r.png b/9router/public/providers/oai-r.png deleted file mode 100644 index 0dbd61c734a2cc0d8812d03195debc25defc4237..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9041 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 CiF%L# diff --git a/9router/public/providers/opencode.png b/9router/public/providers/opencode.png deleted file mode 100644 index 2e709e1c49db5064be4dc445a3d6e3fb4407c1da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16378 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 n*Aliz+Nap5U(EkO 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 Fe*p!XzX>9{MA=3eeG z=TM?zwnT0@y0C`XhD^DB=kfg~zCS!)&*$U$d_5ko-(Ig*nzQ3EIkYMo03b)Sw{wv& zbrZ5OlKRs6WVeJ+;r3pU0Blj(1PtWl?E(NPAlliw#pKQA`kZt7O|6ri^r=OQ=3wFK zz5S9%QPDrX4_PEbPsNx8yD>H7ukn(4>gwp-t3GIt5KRX};lqmxdKI#oxW+!hb!Yv9 z7w?l2p#yz=HGx5~3eQ@`#sq>qLEOeftX1~Z(wf*NGRP5s0QRcmqXwB{r|qtAPrt3$ zqEq_BNy6Bty9ezeVlnfa7`=gKj$t>ulx}=0U^P=dys-7<#FEP{BFG-NGc<8TX-SrW z%g(rlEPMtkhyD2MQPIS0fi@$JU1qw{n%5CvS4?NPsWJbrbVHCMHU zB4nWDc^k@>hero{+jJOHHp=!~5B72~F5f}kwD|J3aq;c_CuUWtZQ#a}4J8^*<{IYHSozM_~^+00V&uZ946(m63b?Vj47(g5(}? zg}0qXK#qjhS_83Um-5*(=oAtZdiPx2BMUXpz(BhA2msc`G)w&`5MqM>pWu6&iU`R6 zGaH{Jn(q@Xj+TEo_SiGC`W-u=-o>V#7rMs#$X-6h8+x_>5q4>DN9f`flE>5&(qGeE z{hBi;OWcu+))-=!&zb5&yqdiUJjy?7$K@ZG+muAW&APr|8~-l#3j0~)n83VJi!~2T zjF!)r{v>v`_Eok=fkux&Qn`oEpVJk2QZs%Qa*$``#u}ry*O06NPSgu0RMt3rr^=Nn zD^TL0{sTV=vPL4mMD>sqfPFZUr(b)CWClvO=Z8b^{5mhGw@-AkUybv5(pMwY(i z0Hu~Ku;2<#MA@#eCT3)ky(M@RO{zW;EcMiA-^=R9^g zw5n#3`sjR%5*i5mNu+HR3J51&J~o#2ls)Lj zXxvP9-S@zvSD0Rn_oUnStkJy%#3Av33b;n$-)zbz?F5o3=#@B$qo;o0AGIzkjWfIx2 zPnxQ8J=4zW`j4Q5`_caIvD8M7^@q9MK6|A^c5`>>aAQHYOMF$K$(JVX@=!x{2(0a! z<qG zH0>tW;ZS-2=(!sx&^@;@WdJH?XLB0XcUoX?1$~5Qph;zGO0VY8i5*@`eMF$@?M;AL z{T&cN{wlL9OEsUJ88H|U8=r?A_Dk>*R#*;K&UQJ`1>8)2Wxx6E1XQa^M+;X*69iZv zYK`{Y8!4)-U^;x;_+EZxZblflah<}|GWL^Rj!_&MM*uDG)KS9%6v6{gI0M)a93uup z2*pE*F!sG7)eK#6_%flo~Ak9>%ov)+KTob<^&G+n$!8Y9gRp z1O{3TDDj;TUpy?J_65J7_0@J?29AJhgtqGk*DR4uY*#9rWcx}Np;>YBHIkV$EZr4j zY8ys@hYKr`lbRlCkKUu{nqg(nnr2hroMCCS0(cH9bbi~?n!qdM7j2Ma<&N(!OKDpDc_^o0FtGGERp$6l`JK%3N|M3Nq z8GLP$v&+cMlJM|5cy~&#zxH_Yc~gi-ONb zidf`yHOcWf;C26wXSlqw8`bs5{A~+Cgf1WDZ~E3(`xN?eCTb)}h(lt zNkKPv*LuC|V75GW`)X;F(SKCC|M)_^iM6>fmZOd>>&`E@!2ce@oU>9GUW5B;E(G&d SWi%x303iP6XjerDp#2vbqQYMQ diff --git a/9router/public/providers/qwen.png b/9router/public/providers/qwen.png deleted file mode 100644 index 0fc2d1b308466c5da00cea81da89b3d86dfedcea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14534 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 diff --git a/9router/public/providers/roo.png b/9router/public/providers/roo.png deleted file mode 100644 index 0503577502bc572ffc7e55874aeda3e9174dbc15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3084 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 JXCUmU{{yZdPlW&g diff --git a/9router/public/providers/together.png b/9router/public/providers/together.png deleted file mode 100644 index 7a53d4c43570a5fc098e657548c629a27f0675d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2024 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 G+ \ No newline at end of file diff --git a/9router/public/window.svg b/9router/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/9router/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/9router/scripts/translate-readme.js b/9router/scripts/translate-readme.js deleted file mode 100755 index af6a11a6..00000000 --- a/9router/scripts/translate-readme.js +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); - -// ============ CONFIGURATION ============ -const API_ENDPOINT = process.env.GLM_API_ENDPOINT || 'https://api.z.ai/api/anthropic/v1/messages'; -const API_MODEL = process.env.GLM_API_MODEL || 'glm-5'; -const API_KEY = process.env.GLM_API_KEY; -const MAX_TOKENS = parseInt(process.env.GLM_MAX_TOKENS || '32000'); -const TEMPERATURE = parseFloat(process.env.GLM_TEMPERATURE || '0.3'); -const BATCH_SIZE = parseInt(process.env.TRANSLATE_BATCH_SIZE || '2'); // Number of languages to translate in parallel - -const SUPPORTED_LANGUAGES = { - vi: 'Vietnamese', - 'zh-CN': 'Simplified Chinese' -}; - -// ============ VALIDATION ============ -if (!API_KEY) { - console.error('Error: GLM_API_KEY environment variable not set'); - process.exit(1); -} - -const targetLangs = process.argv.slice(2); -if (targetLangs.length === 0) { - console.error('Usage: node translate-readme.js [lang2] ...'); - console.error(`Supported languages: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}`); - process.exit(1); -} - -for (const lang of targetLangs) { - if (!SUPPORTED_LANGUAGES[lang]) { - console.error(`Unsupported language: ${lang}`); - process.exit(1); - } -} - -// ============ TRANSLATION FUNCTION ============ -async function translateToLanguage(readmeContent, targetLang) { - const langName = SUPPORTED_LANGUAGES[targetLang]; - console.log(`\n[${targetLang}] Translating to ${langName}...`); - console.log(`[${targetLang}] README size: ${readmeContent.length} characters`); - - const prompt = `Translate this entire Markdown document to ${langName}. - -CRITICAL RULES: -- Keep ALL markdown syntax EXACTLY as is (##, \`\`\`, -, *, |, tables, etc.) -- Do NOT modify code blocks, ASCII diagrams, or code fences -- Only translate human-readable text content -- Keep all URLs, links, and technical terms unchanged - -${readmeContent}`; - - const response = await fetch(API_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': API_KEY, - 'anthropic-version': '2023-06-01' - }, - body: JSON.stringify({ - model: API_MODEL, - messages: [{ role: 'user', content: prompt }], - temperature: TEMPERATURE, - max_tokens: MAX_TOKENS, - stream: true - }) - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`[${targetLang}] API Error: ${response.status} ${error}`); - } - - console.log(`[${targetLang}] Receiving translation stream...`); - - let translatedContent = ''; - let chunkCount = 0; - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split('\n'); - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') continue; - - try { - const parsed = JSON.parse(data); - if (parsed.type === 'content_block_delta' && parsed.delta?.text) { - translatedContent += parsed.delta.text; - chunkCount++; - if (chunkCount % 100 === 0) { - process.stdout.write(`\r[${targetLang}] Received ${translatedContent.length} chars...`); - } - } - } catch (e) { - // Skip invalid JSON - } - } - } - } - - process.stdout.write('\n'); - - console.log(`\n[${targetLang}] Stream complete, received ${translatedContent.length} characters`); - - if (!translatedContent) { - throw new Error(`[${targetLang}] No translation received`); - } - - console.log(`[${targetLang}] Fixing image paths...`); - - // Fix image paths - translatedContent = translatedContent - .replace(/!\[([^\]]*)\]\(\.\/images\//g, '![$1](../images/') - .replace(/!\[([^\]]*)\]\(\.\/public\//g, '![$1](../public/') - .replace(/ translateToLanguage(readmeContent, lang)); - - // Wait for all to complete - const batchResults = await Promise.allSettled(batchPromises); - - results.push(...batchResults); - - // Wait between batches to avoid rate limit - if (i + BATCH_SIZE < targetLangs.length) { - console.log('\nWaiting 3s before next batch...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - } - } - - console.log('\n' + '='.repeat(60)); - console.log('SUMMARY'); - console.log('='.repeat(60)); - - results.forEach((result) => { - if (result.status === 'fulfilled') { - console.log(`✅ ${result.value.lang}: ${result.value.path}`); - } else { - console.log(`❌ ${result.lang}: ${result.reason.message}`); - } - }); - - const failed = results.filter(r => r.status === 'rejected').length; - if (failed > 0) { - console.log(`\n⚠️ ${failed} translation(s) failed`); - process.exit(1); - } - - console.log('\n✅ All translations completed successfully!'); -} - -main().catch(err => { - console.error('Fatal error:', err); - process.exit(1); -}); diff --git a/9router/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js b/9router/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js deleted file mode 100644 index b5d03f5b..00000000 --- a/9router/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js +++ /dev/null @@ -1,206 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { Card, CardSkeleton } from "@/shared/components"; -import { CLI_TOOLS } from "@/shared/constants/cliTools"; -import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; -import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, DefaultToolCard, OpenCodeToolCard } from "./components"; - -const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; - - -const STATUS_ENDPOINTS = { - claude: "/api/cli-tools/claude-settings", - codex: "/api/cli-tools/codex-settings", - opencode: "/api/cli-tools/opencode-settings", - droid: "/api/cli-tools/droid-settings", - openclaw: "/api/cli-tools/openclaw-settings", -}; - -export default function CLIToolsPageClient({ machineId }) { - const [connections, setConnections] = useState([]); - const [loading, setLoading] = useState(true); - const [expandedTool, setExpandedTool] = useState(null); - const [modelMappings, setModelMappings] = useState({}); - const [cloudEnabled, setCloudEnabled] = useState(false); - const [tunnelEnabled, setTunnelEnabled] = useState(false); - const [tunnelUrl, setTunnelUrl] = useState(""); - const [apiKeys, setApiKeys] = useState([]); - const [toolStatuses, setToolStatuses] = useState({}); - - useEffect(() => { - fetchConnections(); - loadCloudSettings(); - fetchApiKeys(); - fetchAllStatuses(); - }, []); - - const fetchAllStatuses = async () => { - try { - const entries = await Promise.all( - Object.entries(STATUS_ENDPOINTS).map(async ([toolId, url]) => { - try { - const res = await fetch(url); - const data = await res.json(); - return [toolId, data]; - } catch { - return [toolId, null]; - } - }) - ); - setToolStatuses(Object.fromEntries(entries)); - } catch (error) { - console.log("Error fetching tool statuses:", error); - } - }; - - const loadCloudSettings = async () => { - try { - const [settingsRes, tunnelRes] = await Promise.all([ - fetch("/api/settings"), - fetch("/api/tunnel/status"), - ]); - if (settingsRes.ok) { - const data = await settingsRes.json(); - setCloudEnabled(data.cloudEnabled || false); - } - if (tunnelRes.ok) { - const data = await tunnelRes.json(); - setTunnelEnabled(data.enabled || false); - setTunnelUrl(data.tunnelUrl || ""); - } - } catch (error) { - console.log("Error loading settings:", error); - } - }; - - const fetchApiKeys = async () => { - try { - const res = await fetch("/api/keys"); - if (res.ok) { - const data = await res.json(); - setApiKeys(data.keys || []); - } - } catch (error) { - console.log("Error fetching API keys:", error); - } - }; - - const fetchConnections = async () => { - try { - const res = await fetch("/api/providers"); - const data = await res.json(); - if (res.ok) { - setConnections(data.connections || []); - } - } catch (error) { - console.log("Error fetching connections:", error); - } finally { - setLoading(false); - } - }; - - const getActiveProviders = () => connections.filter(c => c.isActive !== false); - - const getAllAvailableModels = () => { - const activeProviders = getActiveProviders(); - const models = []; - const seenModels = new Set(); - activeProviders.forEach(conn => { - const alias = PROVIDER_ID_TO_ALIAS[conn.provider] || conn.provider; - const providerModels = getModelsByProviderId(conn.provider); - providerModels.forEach(m => { - const modelValue = `${alias}/${m.id}`; - if (!seenModels.has(modelValue)) { - seenModels.add(modelValue); - models.push({ value: modelValue, label: `${alias}/${m.id}`, provider: conn.provider, alias, connectionName: conn.name, modelId: m.id }); - } - }); - }); - return models; - }; - - const handleModelMappingChange = useCallback((toolId, modelAlias, targetModel) => { - setModelMappings(prev => { - if (prev[toolId]?.[modelAlias] === targetModel) return prev; - return { ...prev, [toolId]: { ...prev[toolId], [modelAlias]: targetModel } }; - }); - }, []); - - const getBaseUrl = () => { - if (tunnelEnabled && tunnelUrl) return tunnelUrl; - if (cloudEnabled && CLOUD_URL) return CLOUD_URL; - if (typeof window !== "undefined") return window.location.origin; - return "http://localhost:20128"; - }; - - if (loading) { - return ( -