diff --git a/.env.example b/.env.example index afac229a..6ed8f81e 100644 --- a/.env.example +++ b/.env.example @@ -34,5 +34,8 @@ NEXT_PUBLIC_CLOUD_URL=https://9router.com # ALL_PROXY=socks5://127.0.0.1:7890 # NO_PROXY=localhost,127.0.0.1 +# Optional SearXNG endpoint for the built-in unauthenticated web-search provider. +# SEARXNG_URL=http://searxng:8080/search + # Currently unused by application runtime (kept as reference) # INSTANCE_NAME=9router diff --git a/.gitignore b/.gitignore index edd8c086..8ccac2c7 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,12 @@ gitbook/README.md open-sse.old/ .graphifyignore graphify-out/* + +# Local-only working dirs (notes, vendored repos, scripts, skills) +.claude/ +.docs/ +.repo/ +.script/ +.codegraph/ +.PR/ +.next-analyze/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b03e97..fb1f69ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,86 @@ +# v0.5.35 (2026-07-16) + +## Features +- **xAI**: Grok Imagine video generation (`/v1/videos`) + CLI +- **CLI tools**: Grok Build setup — writes `[model.9router]` to `~/.grok/config.toml` +- **GitHub Copilot**: route Claude models through Copilot's native `/v1/messages` +- **Kiro**: add GPT-5.6 model family (#2596) +- **RTK**: `X-9Router-Token-Saver` header to bypass token savers per request +- **Providers**: quota visibility settings +- **Translator**: drop temperature for all Claude models +- **i18n**: Thai (th) + Persian (fa) translations / README + +## Fixes +- **Providers**: bulk-add API keys no longer overwrite existing keys (gap-fill `Key N`) +- **Anthropic**: lowercase `anthropic-version` header to prevent duplication on `/v1/messages` +- **Alicode-intl**: use DashScope compatible-mode endpoint so standard keys work +- **Grok CLI**: align Grok Build with current subscription protocol (#2590) +- **Grok CLI**: surface `expiresAt` so proactive token refresh fires (#2546) +- **Kiro**: improve direct session cache reuse +- **Models**: populate capabilities for live-catalog LLM models +- **Models**: list compatible provider models in `/v1/models` +- **Thinking**: send explicit `thinking:{type:adaptive}` alongside `output_config.effort` +- **Translator**: strip `client_metadata` when converting openai-responses → openai + +## Improvements +- **Perf**: skip inactive background services on startup + +## Docs +- README: Persian YouTube tutorial + +# v0.5.30 (2026-07-10) + +## Features +- **Perplexity**: add Agent API provider (#2492) +- **Grok CLI**: add Grok CLI / Grok Build provider with OAuth device-code flow (#2502) +- **Featherless**: add OpenAI-compatible provider presets +- **SearXNG**: configure endpoint via SEARXNG_URL env (#2499) +- **Providers**: add max thinking level for gpt-5.6-sol (#2500) +- **Headroom**: add extras detection and install UI (#2403) +- **Headroom**: activate/uninstall extras + fix interpreter detection +- **PXPipe**: PXPIPE token saver — multimodal prompt compression (#2465) +- **Proxy-Pools**: auto-rotate strategy for no-auth providers (#2409) + +## Fixes +- **Cloudflare-AI**: support accountId in bulk key import (#2449) +- **DB**: backup on schema change, MCP child cleanup, codex models, usage providers OOM +- **Codex**: avoid bare-email OAuth dedup (#2477) +- **CLI**: allow staged app bundle builds (#2479) +- **Headroom**: compress Kiro conversation state (#2488) +- **Gemini-CLI**: raise output floor for thinking and add validated toolConfig (#2486) +- **GitHub**: label Copilot profiles by account identity (#2498) +- **OpenAI-to-Claude**: unwrap bare {function:{…}} tools without parent type (#2473) +- **Translator**: clamp thinking effort max->xhigh for OpenAI format (#2466) +- **RTK/find**: detect and group Windows backslash-style find output (#2448) +- **Codex**: handle fast tier and capacity SSE (#2452) +- **Volcengine-ark**: clamp Kimi max_tokens to 32768 endpoint cap +- **Antigravity**: align provider fingerprint with IDE Desktop 2.1.1 (#2389) +- **Pricing**: update Claude/Codex model rates and add new models + +## Improvements +- **i18n(zh-CN)**: complete Chinese translations for all UI strings (#2436) +- **API**: caching for tunnel and version status endpoints +- **Perf**: faster dev startup and lighter bundle + +# v0.5.20 (2026-07-07) + +## Features +- **Thinking**: per-model thinking level picker on provider page — appends `(level)` suffix to copied model names for forced reasoning effort across all formats (openai, claude, gemini, deepseek, kimi, qwen, zai, minimax, hunyuan, step) +- **RTK**: add JS-native git-log filter (#2423) +- **Caveman**: add targeted upstream-aligned style rules (#2424) +- **i18n**: add Farsi (fa) language support (#2385) + +## Fixes +- **Thinking**: strip `(level)` suffix from upstream `body.model` so providers no longer reject requests +- **Translator**: preserve developer instructions in openai-responses conversion (#2434) +- **count_tokens**: count structured Anthropic blocks (#2419) +- **Volcengine-ark**: clamp GLM-5 max_tokens to model output ceiling (#2428) +- **Kimi**: normalize reasoning_effort to backend enum (#2427) +- **Claude**: reconcile max_tokens vs thinking budget and lift per-model ceiling (#2381) +- **Kiro**: deliver system prompt natively, add Opus 4.5/4.7/4.8, tolerate dash version ids (#2366) +- **Headroom**: proxy dashboard through app (#2372) +- **MITM**: recover from stale lock file on server start + # v0.5.18 (2026-07-03) ## Features diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d7c21345 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +9Router (`9router-app`) — a local AI routing gateway + Next.js dashboard. It exposes one OpenAI-compatible endpoint (`/v1/*`) and routes traffic across 40+ upstream providers with format translation, model-combo fallback, multi-account fallback, OAuth/API-key credential management, token refresh, quota/usage tracking, and optional cloud sync. + +Two published artifacts live in this one repo: +- The **dashboard + gateway** (root `package.json`, `9router-app`) — the Next.js server that does the actual routing. +- The **CLI launcher** (`cli/`, published to npm as `9router`) — a separate package that installs/starts the server and manages the tray. It has its own `package.json`, version, and build. + +The code lives in `src/` (Next.js app + dashboard/compat APIs), `open-sse/` (the provider-agnostic routing/translation engine), `cli/` (the launcher package), and `tests/`. + +## Commands + +Dashboard/gateway (run from repo root): +```bash +cp .env.example .env +npm install +PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev # dev (webpack, port 20127 by default via next dev) +npm run build && PORT=20128 HOSTNAME=0.0.0.0 npm run start # production +``` +- Bun variants: `npm run dev:bun` / `build:bun` / `start:bun`. +- Default runtime port is **20128** (dashboard at `/dashboard`, API at `/v1`). +- Lint: `npx eslint .` (config `eslint.config.mjs`, extends `eslint-config-next`). + +CLI package (`cli/`): +```bash +npm run cli:pack # build + npm pack from root +cd cli && npm run dev # nodemon watch +``` + +Tests (vitest, in `tests/`, an **independent** ESM package — not wired into root `npm test`): +```bash +npm install # ROOT deps first — tests import from src/ which needs `open`, `undici`, etc. +cd tests && npm install # then tests' own deps (vitest) → tests/node_modules (allowed by tests/.gitignore) +npx vitest run # all tests; auto-discovers tests/vitest.config.js +npx vitest run unit/capabilities.test.js # single file (path relative to tests/) +``` +> The committed `tests/package.json` `test` script hardcodes Unix paths (`NODE_PATH=/tmp/node_modules …`) — a shared-install workaround from upstream. On Windows (or anywhere), ignore it and use the `npx vitest` form above; `vitest.config.js` resolves the `open-sse`/`@/` aliases from the repo root regardless of where vitest lives. +> +> **The suite is NOT expected to be all-green on a plain checkout.** ~938 pass, ~64 fail. Judge regressions with `tests/__baseline__/verify-no-regression.mjs`, not a raw run. Expected red: +> - 26 catalogued in `tests/__baseline__/known-fails.txt` (rtk, oauth-cursor-auto-import, translator-request-normalization, …). +> - `unit/embeddings.cloud.test.js` imports `cloud/src/handlers/embeddings.js` — the `cloud/` worker dir is **not in this repo**, so it always fails here. +> - `unit/xai-oauth-service.test.js` times out (5s) when the xAI endpoint-discovery fetch isn't reachable/mocked. +> - `real/*.real.test.js` make live provider calls — need credentials, skip otherwise. +- `*.real.test.js` under `tests/translator/real/` make live provider calls — skip unless credentials are set. +- Regression baselines: `tests/__baseline__/verify-*.mjs` compare against committed snapshots (providers, aliases, OAuth URLs). Run these after touching provider registry / alias logic. + +## Architecture + +Two authoritative docs already exist — read them before working in these areas rather than re-deriving: +- `docs/ARCHITECTURE.md` — full system: request lifecycle, combo/account fallback, OAuth + token refresh, cloud sync, data model. +- `open-sse/AGENTS.md` — the routing/translation engine's own conventions and "how to add a provider/executor/translator". **Read this before editing anything under `open-sse/`.** + +### Request flow (the thing to understand first) +`src/app/api/v1/*` route (Next rewrite maps `/v1/*` → `/api/v1/*` in `next.config.mjs`) +→ `src/sse/handlers/chat.js` (parse, combo expansion, account-selection loop) +→ `open-sse/handlers/chatCore.js` (detect source format, translate request, dispatch to executor, retry/refresh, stream setup) +→ `open-sse/executors/*` (per-provider upstream call; `default.js` handles any OpenAI-compatible provider) +→ `open-sse/translator/*` (client format ↔ provider format) +→ SSE back to client. + +`src/sse/` is the app-side entry glue; `open-sse/` is the provider-agnostic engine (also usable standalone). Cross that boundary consciously. + +### Translator engine (`open-sse/translator/`) +- Pivots through **OpenAI as the intermediate format**. A translator registered on an exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop. Prefer a direct route for fragile pairs (thinking blocks, tool ids, non-base64 images, `is_error`). +- Translators **self-register** via `register(from, to, reqFn, resFn)` as an import side effect — a new translator file MUST be imported in `open-sse/translator/index.js` or it never runs. +- Never hardcode role/block/model strings — use `open-sse/translator/schema/` and `open-sse/config/` constants. Config-driven and DRY is enforced by convention here. + +### Provider registry (`open-sse/providers/registry/*`) +- One file per provider. `providers/registry/index.js` is an **auto-generated** static import list — regenerate it with `scripts/migrate-registry.mjs` / `injectDisplayToRegistry.mjs`, don't hand-edit. +- Add a provider: copy `providers/REGISTRY_TEMPLATE.js`, add models to `config/providerModels.js`. Only add an executor for non-OpenAI-compatible upstreams. + +### Persistence — IMPORTANT (ARCHITECTURE.md is stale here) +State is **no longer `db.json`**. It's a SQLite layer under `src/lib/db/` with an adapter fallback chain (`driver.js`): `bun:sqlite` → `better-sqlite3` (optional native dep) → `node:sqlite` (Node ≥22.5) → `sql.js` (pure-JS fallback, always works). `better-sqlite3` is deliberately in `optionalDependencies` so install never fails without build tools. +- `src/lib/localDb.js` is a **backward-compat shim** re-exporting `src/lib/db/index.js`. New code should import from `@/lib/db/index.js`; per-entity logic lives in `src/lib/db/repos/*`. Schema/migrations in `src/lib/db/migrations/`. +- DB file location resolves via `src/lib/db/paths.js` (`DATA_DIR`, else `~/.9router/`). +- Usage/logs (`src/lib/usageDb.js`, `usage.json` + `log.txt`) still live under `~/.9router` and do **not** follow `DATA_DIR`. + +### RTK token saver (`open-sse/rtk/`) +Pre-translate hooks that compress `tool_result` content in-place to cut tokens. **Fail-open**: any error returns null and leaves the body untouched — never throw out of them. Skips `is_error`/`status:"error"` results to preserve traces. + +## Conventions & gotchas + +- Plain JavaScript (ESM), no TypeScript. `@/*` path alias → `src/*` (`jsconfig.json`). +- `custom-server.js` wraps the Next standalone server to derive client IP from the TCP socket and strip attacker-controlled `X-Forwarded-For` — trusting forwarding headers only from a loopback reverse proxy. Preserve this when touching request/IP/rate-limit code. +- Security-sensitive env: `JWT_SECRET` (session cookie), `INITIAL_PASSWORD` (default `123456` — must override), `API_KEY_SECRET`, `MACHINE_ID_SALT`. Full env contract in `.env.example` and ARCHITECTURE.md's env matrix. +- Binary/protobuf upstreams (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — they're handled inside their own executor, not the translator. +- Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`). diff --git a/README.md b/README.md index a9e169a9..a07dd18c 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,12 @@ [![GHCR](https://img.shields.io/badge/GHCR-decolua%2F9router-blue?logo=github)](https://github.com/decolua/9router/pkgs/container/9router) [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) - decolua%2F9router | Trendshift - - [🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [📖 Setup](#-setup-guide) • [🌐 Website](https://9router.com) +decolua%2F9router | Trendshift + +[🚀 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) • [🇯🇵 日本語](./i18n/README.ja-JP.md) • [🇷🇺 Русский](./i18n/README.ru.md) • [🇹🇭 ไทย](./i18n/README.th.md) • [🇮🇷 فارسی](./i18n/README.fa_IR.md) - [🇻🇳 Tiếng Việt](./i18n/README.vi.md) • [🇨🇳 中文](./i18n/README.zh-CN.md) • [🇯🇵 日本語](./i18n/README.ja-JP.md) • [🇷🇺 Русский](./i18n/README.ru.md) --- @@ -114,6 +115,7 @@ PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run ``` Default URLs: + - Dashboard: `http://localhost:20128/dashboard` - OpenAI-compatible API: `http://localhost:20128/v1` @@ -125,6 +127,20 @@ Default URLs: + + - + + - - - + + - - - + + + + +
+ + Tiết kiệm chi phí LLM với 9Router +
+ 🇻🇳 Tiếng Việt
+ Tiết kiệm chi phí LLM cho OpenClaw với 9Router
by Mì AI
+
+ + 9Router + Claude Code FREE Unlimited Setup +
+ 🇵🇰 اردو / हिन्दी
+ 9Router + Claude Code FREE Unlimited Setup
by Build AI With Hamid
+
9Router Setup Tutorial @@ -132,12 +148,15 @@ Default URLs: 🇺🇸 English
9Router + Claude Code FREE Setup
by
Build AI With Hamid
- - Tiết kiệm chi phí LLM với 9Router + +
+ + 9Router Setup Tutorial
- 🇻🇳 Tiếng Việt
- Tiết kiệm chi phí LLM cho OpenClaw với 9Router
by Mì AI
+ 🇺🇸 English
+ 9Router + Claude Code FREE Setup
by Build AI With Hamid
@@ -146,8 +165,6 @@ Default URLs: 🇺🇸 English
Claude Code FREE Forever — Unlimited Models
by
Build AI With Hamid
Claude CLI Free Setup @@ -155,7 +172,10 @@ Default URLs: 🇺🇸 English
Claude CLI Free Setup with 9Router 🚀
by
CodeVerse Soban
+ +
Cài đặt OpenClaw Free A-Z
@@ -169,8 +189,6 @@ Default URLs: 🇺🇸 English
FREE OpenClaw + Claude Opus 4.6
by Build AI With Hamid
Claude CLI Free Setup @@ -178,14 +196,26 @@ Default URLs: 🇮🇩 Indonesia
Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Quick Setup 9Router 🚀
by
Krisswuh
+ +
Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB
🇮🇩 Indonesia
Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB
by Krisswuh
+ + این شکلی از هر API ای استفاده کن برای هوش مصنوعی +
+ 🇮🇷 Persian-فارسی
+ این شکلی از هر API ای استفاده کن برای هوش مصنوعی
by Matin SenPai
+
@@ -408,22 +438,24 @@ Default URLs: ## 💡 Key Features -| Feature | What It Does | Why It Matters | -|---------|--------------|----------------| -| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compress tool outputs (`git diff`, `grep`, `ls`, `tree`...) before sending to LLM | Save **20-40% input tokens** per request | -| 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | Optional external `/v1/compress` proxy before provider routing | Save more context tokens without changing clients | -| 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inject caveman-speak prompt → LLM replies terse, technical substance preserved | Save **up to 65% output tokens** | -| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Inject "lazy senior dev" prompt → LLM writes minimal, YAGNI-first code (Lite/Full/Ultra) | **Fewer output tokens, less refactoring** | -| 🎯 **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 ↔ Cursor ↔ Kiro ↔ Vertex | 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 | What It Does | Why It Matters | +| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------- | +| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compress tool outputs (`git diff`, `grep`, `ls`, `tree`...) before sending to LLM | Save **20-40% input tokens** per request | +| 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | Optional external `/v1/compress` proxy before provider routing | Save more context tokens without changing clients | +| 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inject caveman-speak prompt → LLM replies terse, technical substance preserved | Save **up to 65% output tokens** | +| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Inject "lazy senior dev" prompt → LLM writes minimal, YAGNI-first code (Lite/Full/Ultra) | **Fewer output tokens, less refactoring** | +| 🎯 **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 ↔ Cursor ↔ Kiro ↔ Vertex | 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 | + +Set `X-9Router-Token-Saver: off` to bypass all token savers for one chat request.
📖 Feature Details @@ -474,7 +506,7 @@ If Headroom is down or returns an error, 9Router fails open and sends the origin ### 🐴 Ponytail (Lazy Senior Dev) -Ponytail injects a *"lazy senior dev"* system prompt into every request, biasing the LLM toward minimal, YAGNI-first code — deletion over addition, stdlib over new deps, one-liners over abstractions. Adapted from [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). +Ponytail injects a _"lazy senior dev"_ system prompt into every request, biasing the LLM toward minimal, YAGNI-first code — deletion over addition, stdlib over new deps, one-liners over abstractions. Adapted from [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). - **Lite** — Build what's asked, name the lazier alternative. - **Full** — YAGNI ladder enforced: stdlib → native → existing deps → one-liner → minimal code. @@ -510,6 +542,7 @@ Combo: "my-coding-stack" ### 🔄 Format Translation Seamless translation between formats: + - **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses** - Your CLI tool sends OpenAI format → 9Router translates → Provider receives native format - Works with any tool that supports custom OpenAI endpoints @@ -563,14 +596,14 @@ Seamless translation between formats: - Optimize your AI spending > **💡 IMPORTANT - Understanding Dashboard Costs:** -> -> The "cost" displayed in Usage Analytics is **for tracking and comparison purposes only**. +> +> 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 +> +> **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 +> +> Think of it as a "savings tracker" showing how much you're saving by using free models or > routing through 9Router! ### 🌐 Deploy Anywhere @@ -586,19 +619,19 @@ Seamless translation between formats: ## 💰 Pricing at a Glance -| Tier | Provider | Cost | Quota Reset | Best For | -|------|----------|------|-------------|----------| -| **🚀 TOKEN SAVER** | **RTK (built-in)** | **FREE** | Always on | **Save 20-40% tokens on EVERY request** | -| **💳 SUBSCRIPTION** | Claude Code (Pro/Max) | $20-200/mo | 5h + weekly | Already subscribed | -| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | -| | GitHub Copilot | $10-19/mo | Monthly | GitHub users | -| | Cursor IDE | $20/mo | Monthly | Cursor users | -| **💰 CHEAP** | GLM-5.1 / GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | -| | MiniMax M2.7 | $0.2/1M | 5-hour rolling | Cheapest option | -| | Kimi K2.5 | $9/mo flat | 10M tokens/mo | Predictable cost | -| **🆓 FREE** | Kiro AI | $0 | Unlimited | Claude 4.5 + GLM-5 + MiniMax free | -| | OpenCode Free | $0 | Unlimited | No auth, auto-fetch models | -| | Vertex AI | $300 credits | New GCP accounts | Gemini 3 Pro + DeepSeek + GLM-5 | +| Tier | Provider | Cost | Quota Reset | Best For | +| ------------------- | --------------------- | ------------ | ---------------- | --------------------------------------- | +| **🚀 TOKEN SAVER** | **RTK (built-in)** | **FREE** | Always on | **Save 20-40% tokens on EVERY request** | +| **💳 SUBSCRIPTION** | Claude Code (Pro/Max) | $20-200/mo | 5h + weekly | Already subscribed | +| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | +| | GitHub Copilot | $10-19/mo | Monthly | GitHub users | +| | Cursor IDE | $20/mo | Monthly | Cursor users | +| **💰 CHEAP** | GLM-5.1 / GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | +| | MiniMax M2.7 | $0.2/1M | 5-hour rolling | Cheapest option | +| | Kimi K2.5 | $9/mo flat | 10M tokens/mo | Predictable cost | +| **🆓 FREE** | Kiro AI | $0 | Unlimited | Claude 4.5 + GLM-5 + MiniMax free | +| | OpenCode Free | $0 | Unlimited | No auth, auto-fetch models | +| | Vertex AI | $300 credits | New GCP accounts | Gemini 3 Pro + DeepSeek + GLM-5 | **💡 Pro Tip:** RTK + Kiro AI + OpenCode Free combo = **$0 cost + 20-40% token savings**! @@ -619,6 +652,7 @@ Seamless translation between formats: 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 @@ -632,6 +666,7 @@ Reality Check: ``` **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 @@ -646,6 +681,7 @@ Reality Check: **Problem:** Quota expires unused, rate limits during heavy coding **Solution:** + ``` Combo: "maximize-claude" 1. cc/claude-opus-4-7 (use subscription fully) @@ -661,6 +697,7 @@ vs. $20 + hitting limits = frustration **Problem:** Can't afford subscriptions, need reliable AI coding **Solution:** + ``` Combo: "free-forever" 1. kr/claude-sonnet-4.5 (Claude 4.5 free unlimited) @@ -676,6 +713,7 @@ Quality: Production-ready models + RTK saves 20-40% tokens **Problem:** Deadlines, can't afford downtime **Solution:** + ``` Combo: "always-on" 1. cc/claude-opus-4-7 (best quality) @@ -693,6 +731,7 @@ Monthly cost: $20-200 (subscriptions) + $10-20 (backup) **Problem:** Need AI assistant in messaging apps (WhatsApp, Telegram, Slack...), completely free **Solution:** + ``` Combo: "openclaw-free" 1. kr/claude-sonnet-4.5 (Claude 4.5 free) @@ -713,6 +752,7 @@ Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... 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** @@ -728,6 +768,7 @@ The cost display is a "savings tracker" to help you understand your usage patter **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** @@ -742,6 +783,7 @@ The cost display is a "savings tracker" to help you understand your usage patter **Yes!** The current FREE providers (Kiro, OpenCode Free, Vertex) are genuinely free with **no hidden charges**. These are free services offered by those respective companies: + - **Kiro AI**: Free unlimited Claude 4.5 + GLM-5 + MiniMax via AWS Builder ID / Google / GitHub OAuth - **OpenCode Free**: No-auth passthrough proxy, models auto-fetched from `opencode.ai/zen/v1/models` - **Vertex AI**: $300 free credits for new Google Cloud accounts (90 days) @@ -749,6 +791,7 @@ These are free services offered by those respective companies: 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. **Discontinued free tiers (no longer recommended):** + - ❌ **iFlow**: Was free unlimited, now changed to paid (2026) - ❌ **Qwen Code**: Free OAuth tier discontinued by Alibaba on 2026-04-15 - ❌ **Gemini CLI**: Still works, but using it with non-CLI tools (Claude, Codex, Cursor...) may result in account bans — only use if you stick to Gemini CLI itself @@ -761,17 +804,21 @@ These are free services offered by those respective companies: **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:** @@ -790,10 +837,12 @@ These are free services offered by those respective companies: **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 @@ -1117,6 +1166,7 @@ pm2 startup ### Docker Published images (multi-platform `linux/amd64` + `linux/arm64`): + - Docker Hub: [`decolua/9router`](https://hub.docker.com/r/decolua/9router) - GHCR: [`ghcr.io/decolua/9router`](https://github.com/decolua/9router/pkgs/container/9router) @@ -1144,6 +1194,7 @@ docker run -d --name 9router -p 20128:20128 \ ``` **Container defaults:** + - `PORT=20128` - `HOSTNAME=0.0.0.0` @@ -1160,26 +1211,28 @@ docker pull decolua/9router:latest # update to latest ### Environment Variables -| Variable | Default | Description | -|----------|---------|-------------| -| `JWT_SECRET` | Auto-generated (`~/.9router/jwt-secret`) | JWT signing secret for dashboard auth cookie (override to share across instances) | -| `INITIAL_PASSWORD` | `123456` | First login password when no saved hash exists | -| `DATA_DIR` | `~/.9router` | Main app data location (SQLite at `$DATA_DIR/db/data.sqlite`) | -| `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 | +| Variable | Default | Description | +| ---------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | +| `JWT_SECRET` | Auto-generated (`~/.9router/jwt-secret`) | JWT signing secret for dashboard auth cookie (override to share across instances) | +| `INITIAL_PASSWORD` | `123456` | First login password when no saved hash exists | +| `DATA_DIR` | `~/.9router` | Main app data location (SQLite at `$DATA_DIR/db/data.sqlite`) | +| `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 | +| `SEARXNG_URL` | `http://localhost:8888/search` | Endpoint for the built-in unauthenticated SearXNG web-search provider | 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. @@ -1202,6 +1255,7 @@ Notes: View all available models **Claude Code (`cc/`)** - Pro/Max: + - `cc/claude-opus-4-7` - `cc/claude-opus-4-6` - `cc/claude-sonnet-4-6` @@ -1209,6 +1263,7 @@ Notes: - `cc/claude-haiku-4-5-20251001` **Codex (`cx/`)** - Plus/Pro: + - `cx/gpt-5.5` - `cx/gpt-5.4` - `cx/gpt-5.3-codex` @@ -1216,6 +1271,7 @@ Notes: - `cx/gpt-5.1-codex-max` **GitHub Copilot (`gh/`)**: + - `gh/gpt-5.4` - `gh/claude-opus-4.7` - `gh/claude-sonnet-4.6` @@ -1223,25 +1279,30 @@ Notes: - `gh/grok-code-fast-1` **Cursor (`cu/`)** - Subscription: + - `cu/claude-4.6-opus-max` - `cu/claude-4.5-sonnet-thinking` - `cu/gpt-5.3-codex` - `cu/kimi-k2.5` **GLM (`glm/`)** - $0.6/1M: + - `glm/glm-5.1` - `glm/glm-5` - `glm/glm-4.7` **MiniMax (`minimax/`)** - $0.2/1M: + - `minimax/MiniMax-M2.7` - `minimax/MiniMax-M2.5` **Kimi (`kimi/`)** - $9/mo flat: + - `kimi/kimi-k2.5` - `kimi/kimi-k2.5-thinking` **Kiro (`kr/`)** - FREE unlimited: + - `kr/claude-sonnet-4.5` - `kr/claude-haiku-4.5` - `kr/glm-5` @@ -1250,9 +1311,11 @@ Notes: - `kr/deepseek-3.2` **OpenCode Free (`oc/`)** - FREE no-auth: + - Auto-fetched from `opencode.ai/zen/v1/models` **Vertex AI (`vertex/`)** - $300 free credits: + - `vertex/gemini-3.1-pro-preview` - `vertex/gemini-3-flash-preview` - `vertex/gemini-2.5-flash` @@ -1266,31 +1329,38 @@ Notes: ## 🐛 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-7 → glm/glm-5.1 → kr/claude-sonnet-4.5` **OAuth token expired** + - Auto-refreshed by 9Router - If issues persist: Dashboard → Provider → Reconnect **High costs** + - Enable RTK in Dashboard → Endpoint settings (default ON, saves 20-40% tokens) - Check usage stats in Dashboard - Switch primary model to GLM/MiniMax - Use free tier (Kiro, OpenCode Free, Vertex) 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` --- @@ -1353,8 +1423,6 @@ Thanks to all contributors who helped make 9Router better! [![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. @@ -1367,8 +1435,8 @@ Built on the shoulders of giants: - **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — original Go implementation that inspired this JavaScript port. - **[RTK](https://github.com/rtk-ai/rtk)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — Rust token-saver. 9Router ports its compression pipeline to JS → **−20-40% input tokens** on every request. -- **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral *"why use many token when few token do trick"*. 9Router adapts its prompt → **−65% output tokens**. -- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) by **[@DietrichGebert](https://github.com/DietrichGebert)** — *"lazy senior dev"* skill. 9Router injects its YAGNI-first ladder → **fewer tokens, less code, shorter diffs**. +- **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral _"why use many token when few token do trick"_. 9Router adapts its prompt → **−65% output tokens**. +- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) by **[@DietrichGebert](https://github.com/DietrichGebert)** — _"lazy senior dev"_ skill. 9Router injects its YAGNI-first ladder → **fewer tokens, less code, shorter diffs**. Huge thanks to these authors — without their work, 9Router's token-saving features wouldn't exist. ⭐ them on GitHub! diff --git a/cli/cli.js b/cli/cli.js index 09057b2e..8da59653 100755 --- a/cli/cli.js +++ b/cli/cli.js @@ -4,8 +4,28 @@ const { spawn, exec, execSync } = require("child_process"); const path = require("path"); const fs = require("fs"); const https = require("https"); +const net = require("net"); const os = require("os"); +// Poll until the server accepts TCP connections on port, or timeout — avoids blind fixed waits. +function waitServerReady(port, { timeoutMs = 15000, intervalMs = 150 } = {}) { + const deadline = Date.now() + timeoutMs; + return new Promise((resolve) => { + const tryConnect = () => { + const socket = net.connect({ host: "127.0.0.1", port }, () => { + socket.destroy(); + resolve(true); + }); + socket.on("error", () => { + socket.destroy(); + if (Date.now() >= deadline) return resolve(false); + setTimeout(tryConnect, intervalMs); + }); + }; + tryConnect(); + }); +} + // Native spinner - no external dependency function createSpinner(text) { const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; @@ -47,6 +67,19 @@ const { ensureSqliteRuntime, buildEnvWithRuntime } = require("./hooks/sqliteRunt const { ensureTrayRuntime } = require("./hooks/trayRuntime"); const args = process.argv.slice(2); +// Subcommands (`9router xai video …`) run against an already-running gateway +// and bypass the launcher flow (no runtime self-heal, no server spawn). +if (args[0] === "xai" && args[1] === "video") { + const { run } = require("./src/cli/commands/xaiVideo"); + run(args.slice(2)) + .then((code) => process.exit(code)) + .catch((err) => { + console.error(`❌ ${err?.message || err}`); + process.exit(1); + }); + return; +} + // Self-heal SQLite runtime deps (sql.js + better-sqlite3) into ~/.9router/runtime // so the server can resolve them via NODE_PATH. Best-effort — sql.js is required, // better-sqlite3 is optional. Logs to stderr only on failure. @@ -119,6 +152,11 @@ Options: --skip-update Skip auto-update check -h, --help Show this help message -v, --version Show version + +Commands: + xai video --prompt "..." --output video.mp4 + Generate a Grok Imagine video via the running gateway + (see: ${APP_NAME} xai video --help) `); process.exit(0); } else if (args[i] === "--version" || args[i] === "-v") { @@ -212,17 +250,18 @@ function killCloudflaredByAppPort(appPort) { function killAllAppProcesses(appPort) { return new Promise((resolve) => { try { - // Kill MIT first (privileged process, needs special handling) - killProxyByPidFile(); - // Kill cloudflared/tailscale by PID file (precise, only this app's tunnel) - killTunnelByPidFile(); + // Background: MITM + tunnel/cloudflared run on separate ports/processes — + // killing them doesn't free the app port, so don't block the critical path. + // Server-side MITM manager has stale-lock recovery and starts deferred (~3s). + setImmediate(() => { + try { killProxyByPidFile(); } catch {} + try { killTunnelByPidFile(); } catch {} + try { killCloudflaredByAppPort(appPort); } catch {} + }); const platform = process.platform; let pids = []; - // Catch stale PID files: kill cloudflared bound to this app's port - pids.push(...killCloudflaredByAppPort(appPort)); - if (platform === "win32") { // Windows: use WMI to get full CommandLine (tasklist /V doesn't include it) try { @@ -499,14 +538,11 @@ if (!fs.existsSync(serverPath)) { process.exit(1); } -// Check for updates FIRST, then start server -checkForUpdate().then((latestVersion) => { - killAllAppProcesses(port).then(() => { - return killProcessOnPort(port); - }).then(() => { - startServer(latestVersion); - }); -}); +// Start server immediately; run update check in parallel (not on the critical path). +const updatePromise = checkForUpdate(); +killAllAppProcesses(port) + .then(() => killProcessOnPort(port)) + .then(() => startServer(updatePromise)); // Show interface selection menu async function showInterfaceMenu(latestVersion) { @@ -556,7 +592,9 @@ async function showInterfaceMenu(latestVersion) { const MAX_RESTARTS = 2; const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s -function startServer(latestVersion) { +function startServer(updatePromise) { + // Accept either a Promise (parallel update check) or a resolved value. + const latestVersionPromise = Promise.resolve(updatePromise); const displayHost = getDisplayHost(); const url = `http://${displayHost}:${port}/dashboard`; // Surface real network exposure when bound to all interfaces (default 0.0.0.0). @@ -677,17 +715,19 @@ function startServer(latestVersion) { console.log(`\n🚀 ${pkg.name} v${pkg.version}`); console.log(`Server: http://${displayHost}:${port}`); - setTimeout(() => { + waitServerReady(port).then(() => { initTrayIcon(); console.log("\n💡 Router is now running in system tray. Close this terminal if you want."); console.log(" Right-click tray icon to open dashboard or quit.\n"); - }, 2000); + }); return; } // Wait for server to be ready, then show interface menu loop + tray - setTimeout(async () => { + waitServerReady(port).then(async () => { + // Resolve parallel update check (already running); don't block server start on it. + const latestVersion = await latestVersionPromise; // Start tray icon alongside TUI initTrayIcon(); @@ -772,7 +812,7 @@ function startServer(latestVersion) { cleanup(); process.exit(1); } - }, 3000); + }); function attachServerEvents() { server.on("error", (err) => { diff --git a/cli/package.json b/cli/package.json index f55e7751..d7fd2054 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "9router", - "version": "0.5.18", + "version": "0.5.35", "description": "9Router CLI - Start and manage 9Router server", "bin": { "9router": "./cli.js" diff --git a/cli/scripts/build-cli.js b/cli/scripts/build-cli.js index 625d94ba..a1b28c5f 100644 --- a/cli/scripts/build-cli.js +++ b/cli/scripts/build-cli.js @@ -7,7 +7,7 @@ const { execSync } = require("child_process"); const cliDir = path.resolve(__dirname, ".."); const appDir = path.resolve(cliDir, ".."); const rootDir = path.resolve(appDir, ".."); -const cliAppDir = path.join(cliDir, "app"); +const cliAppDir = process.env.NINEROUTER_CLI_APP_DIR || path.join(cliDir, "app"); const buildHomeDir = path.join(cliDir, ".build-home"); const buildDistDirName = ".next-cli-build"; const buildDistDir = path.join(appDir, buildDistDirName); diff --git a/cli/scripts/buildMitm.js b/cli/scripts/buildMitm.js index 45c1664c..e47f593a 100644 --- a/cli/scripts/buildMitm.js +++ b/cli/scripts/buildMitm.js @@ -12,7 +12,8 @@ const BUILD_CONFIG = { const cliDir = path.resolve(__dirname, ".."); const appDir = path.resolve(cliDir, ".."); -const cliMitmDir = path.join(cliDir, "app", "src", "mitm"); +const cliAppDir = process.env.NINEROUTER_CLI_APP_DIR || path.join(cliDir, "app"); +const cliMitmDir = path.join(cliAppDir, "src", "mitm"); // Bundle everything — no externals. This keeps MITM runtime self-contained so // it can be copied to DATA_DIR/runtime/ and spawned from there (escapes // node_modules file locks that block `npm i -g 9router@latest` on Windows). diff --git a/cli/src/cli/commands/xaiVideo.js b/cli/src/cli/commands/xaiVideo.js new file mode 100644 index 00000000..27d829f7 --- /dev/null +++ b/cli/src/cli/commands/xaiVideo.js @@ -0,0 +1,300 @@ +/** + * `9router xai video` — generate a Grok Imagine video through the local + * 9router gateway and save the result as an MP4 file. + * + * Flow: POST /v1/videos/generations → poll GET /v1/videos/{request_id} + * until done/failed/timeout → download video.url → atomic rename. + * + * No OAuth tokens or Authorization headers are ever printed. + */ + +const http = require("http"); +const https = require("https"); +const fs = require("fs"); +const path = require("path"); + +const DEFAULT_PORT = 20128; +const DEFAULT_HOST = "127.0.0.1"; +const DEFAULT_MODEL = "xai/grok-imagine-video"; +const DEFAULT_TIMEOUT_SEC = 600; +const DEFAULT_POLL_INTERVAL_MS = 5000; + +const TERMINAL_STATUSES = new Set(["done", "failed", "completed", "error", "expired", "cancelled"]); +const FAILED_STATUSES = new Set(["failed", "error", "expired", "cancelled"]); + +const HELP = ` +Usage: 9router xai video --prompt "..." [options] + +Generate a Grok Imagine video via your local 9router gateway +(requires a connected xAI account — Grok Build OAuth or API key). + +Options: + --prompt Video description (required) + --output Output MP4 path (default: video.mp4) + --model Model (default: ${DEFAULT_MODEL}) + --duration Video duration + --aspect-ratio e.g. 16:9, 9:16, 1:1 + --resolution 480p | 720p | 1080p + --image Image input for image-to-video + --timeout Max wait for the job (default: ${DEFAULT_TIMEOUT_SEC}) + --port Gateway port (default: ${DEFAULT_PORT}) + --host Gateway host (default: ${DEFAULT_HOST}) + --api-key 9router API key (or env NINE_ROUTER_API_KEY) + -h, --help Show this help +`; + +function sanitizeText(text) { + return String(text ?? "").replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [redacted]"); +} + +function parseArgs(argv) { + const opts = { + model: DEFAULT_MODEL, + output: "video.mp4", + timeoutSec: DEFAULT_TIMEOUT_SEC, + port: DEFAULT_PORT, + host: DEFAULT_HOST, + apiKey: process.env.NINE_ROUTER_API_KEY || null, + pollIntervalMs: DEFAULT_POLL_INTERVAL_MS, + }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + const next = () => argv[++i]; + if (a === "--prompt") opts.prompt = next(); + else if (a === "--output" || a === "-o") opts.output = next(); + else if (a === "--model") opts.model = next(); + else if (a === "--duration") opts.duration = parseInt(next(), 10); + else if (a === "--aspect-ratio") opts.aspectRatio = next(); + else if (a === "--resolution") opts.resolution = next(); + else if (a === "--image") opts.image = next(); + else if (a === "--timeout") opts.timeoutSec = parseInt(next(), 10) || DEFAULT_TIMEOUT_SEC; + else if (a === "--port" || a === "-p") opts.port = parseInt(next(), 10) || DEFAULT_PORT; + else if (a === "--host" || a === "-H") opts.host = next() || DEFAULT_HOST; + else if (a === "--api-key") opts.apiKey = next(); + else if (a === "--poll-interval-ms") opts.pollIntervalMs = parseInt(next(), 10) || DEFAULT_POLL_INTERVAL_MS; + else if (a === "-h" || a === "--help") opts.help = true; + else { + throw new Error(`Unknown option: ${a}`); + } + } + return opts; +} + +/** Local file path → base64 data URL; URLs pass through untouched. */ +function imageInputToUrl(input) { + if (/^(https?:|data:)/i.test(input)) return input; + const buf = fs.readFileSync(input); + const ext = path.extname(input).toLowerCase(); + const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg"; + return `data:${mime};base64,${buf.toString("base64")}`; +} + +/** Minimal JSON request against the local gateway. Returns { status, headers, body }. */ +function gatewayRequest({ host, port, apiKey, method, reqPath, body, signal }) { + return new Promise((resolve, reject) => { + const payload = body ? JSON.stringify(body) : null; + const headers = { Accept: "application/json" }; + if (payload) { + headers["Content-Type"] = "application/json"; + headers["Content-Length"] = Buffer.byteLength(payload); + } + if (apiKey) headers.Authorization = `Bearer ${apiKey}`; + + const req = http.request({ hostname: host, port, path: reqPath, method, headers, signal }, (res) => { + let data = ""; + res.on("data", (c) => (data += c)); + res.on("end", () => { + let parsed = null; + try { parsed = data ? JSON.parse(data) : null; } catch { /* keep raw */ } + resolve({ status: res.statusCode, headers: res.headers, body: parsed, raw: data }); + }); + }); + req.on("error", reject); + if (payload) req.write(payload); + req.end(); + }); +} + +const sleep = (ms, signal) => + new Promise((resolve, reject) => { + const t = setTimeout(resolve, ms); + signal?.addEventListener?.("abort", () => { clearTimeout(t); reject(new Error("aborted")); }, { once: true }); + }); + +/** + * Poll GET /v1/videos/{id} until a terminal status or deadline. + * @returns {Promise} final poll body (status done) — throws on failed/timeout. + */ +async function pollUntilDone({ host, port, apiKey, requestId, connectionId, timeoutSec, pollIntervalMs, signal, onProgress }) { + const deadline = Date.now() + timeoutSec * 1000; + while (true) { + if (signal?.aborted) throw new Error("aborted"); + if (Date.now() > deadline) { + throw new Error(`Timed out after ${timeoutSec}s waiting for video job ${requestId}`); + } + + const res = await gatewayRequestWithConnection({ host, port, apiKey, requestId, connectionId, signal }); + if (res.status === 200 && res.body) { + const status = String(res.body.status || "").toLowerCase(); + onProgress?.(status || "pending", res.body.progress); + if (FAILED_STATUSES.has(status)) { + const msg = res.body.error?.message || res.body.error || "video generation failed"; + throw new Error(`Job ${requestId} failed: ${sanitizeText(typeof msg === "string" ? msg : JSON.stringify(msg))}`); + } + if (TERMINAL_STATUSES.has(status)) return res.body; + } else if (res.status >= 400 && res.status !== 429 && res.status !== 503) { + throw new Error(`Polling failed (HTTP ${res.status}): ${sanitizeText(res.raw?.slice(0, 300))}`); + } + await sleep(pollIntervalMs, signal); + } +} + +function gatewayRequestWithConnection({ host, port, apiKey, requestId, connectionId, signal }) { + return new Promise((resolve, reject) => { + const headers = { Accept: "application/json" }; + if (apiKey) headers.Authorization = `Bearer ${apiKey}`; + if (connectionId) headers["x-connection-id"] = connectionId; + const req = http.request( + { hostname: host, port, path: `/v1/videos/${encodeURIComponent(requestId)}`, method: "GET", headers, signal }, + (res) => { + let data = ""; + res.on("data", (c) => (data += c)); + res.on("end", () => { + let parsed = null; + try { parsed = data ? JSON.parse(data) : null; } catch { /* keep raw */ } + resolve({ status: res.statusCode, body: parsed, raw: data }); + }); + } + ); + req.on("error", reject); + req.end(); + }); +} + +/** + * Download a URL to `outputPath` via a `.part` temp file with atomic rename. + * The temp file is removed on any failure. + */ +async function downloadToFile(url, outputPath, { signal } = {}) { + const partPath = `${outputPath}.part`; + await new Promise((resolve, reject) => { + const cleanupAnd = (fn) => (err) => { + try { fs.unlinkSync(partPath); } catch { /* not created yet */ } + fn(err); + }; + const get = (target, redirectsLeft) => { + const mod = target.startsWith("https:") ? https : http; + const req = mod.get(target, { signal }, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirectsLeft > 0) { + res.resume(); + return get(new URL(res.headers.location, target).toString(), redirectsLeft - 1); + } + if (res.statusCode !== 200) { + res.resume(); + return cleanupAnd(reject)(new Error(`Download failed: HTTP ${res.statusCode}`)); + } + const out = fs.createWriteStream(partPath); + res.pipe(out); + out.on("finish", () => out.close(resolve)); + out.on("error", cleanupAnd(reject)); + res.on("error", cleanupAnd(reject)); + }); + req.on("error", cleanupAnd(reject)); + }; + get(url, 5); + }); + fs.renameSync(partPath, outputPath); +} + +async function run(argv) { + let opts; + try { + opts = parseArgs(argv); + } catch (err) { + console.error(`❌ ${err.message}`); + console.log(HELP); + return 1; + } + if (opts.help) { + console.log(HELP); + return 0; + } + if (!opts.prompt) { + console.error("❌ --prompt is required"); + console.log(HELP); + return 1; + } + + const controller = new AbortController(); + const partPath = `${opts.output}.part`; + const onSigint = () => { + controller.abort(); + try { fs.unlinkSync(partPath); } catch { /* absent */ } + console.error("\n✋ Cancelled"); + process.exit(130); + }; + process.on("SIGINT", onSigint); + + try { + const body = { model: opts.model, prompt: opts.prompt }; + if (opts.duration) body.duration = opts.duration; + if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio; + if (opts.resolution) body.resolution = opts.resolution; + if (opts.image) body.image = { url: imageInputToUrl(opts.image) }; + + console.log(`🎬 Requesting video (${opts.model})…`); + const create = await gatewayRequest({ + host: opts.host, port: opts.port, apiKey: opts.apiKey, + method: "POST", reqPath: "/v1/videos/generations", body, signal: controller.signal, + }); + + if (create.status !== 200 || !create.body?.request_id) { + const detail = create.body?.error?.message || create.body?.error || create.raw || `HTTP ${create.status}`; + console.error(`❌ Create failed: ${sanitizeText(typeof detail === "string" ? detail : JSON.stringify(detail)).slice(0, 500)}`); + if (create.status === 400 && /No credentials/i.test(String(detail))) { + console.error(" Connect an xAI account first: dashboard → Providers → xAI (Grok)."); + } + return 1; + } + + const requestId = create.body.request_id; + const connectionId = create.headers["x-9router-connection-id"] || null; + console.log(`📋 Job accepted: ${requestId}`); + + let lastLine = ""; + const result = await pollUntilDone({ + host: opts.host, port: opts.port, apiKey: opts.apiKey, + requestId, connectionId, + timeoutSec: opts.timeoutSec, pollIntervalMs: opts.pollIntervalMs, + signal: controller.signal, + onProgress: (status, progress) => { + const line = `⏳ ${status}${Number.isFinite(progress) ? ` ${progress}%` : ""}`; + if (line !== lastLine) { + lastLine = line; + if (process.stdout.isTTY) process.stdout.write(`\r\x1b[K${line}`); + else console.log(line); + } + }, + }); + if (process.stdout.isTTY) process.stdout.write("\n"); + + const videoUrl = result.video?.url || result.video?.file_output?.public_url; + if (!videoUrl) { + console.error("❌ Job finished but no video URL was returned"); + return 1; + } + + console.log("⬇️ Downloading…"); + await downloadToFile(videoUrl, opts.output, { signal: controller.signal }); + console.log(`✅ Saved ${opts.output}`); + return 0; + } catch (err) { + if (process.stdout.isTTY) process.stdout.write("\n"); + console.error(`❌ ${sanitizeText(err?.message || String(err))}`); + return 1; + } finally { + process.removeListener("SIGINT", onSigint); + } +} + +module.exports = { run, parseArgs, pollUntilDone, downloadToFile, imageInputToUrl, sanitizeText }; diff --git a/i18n/README.fa_IR.md b/i18n/README.fa_IR.md new file mode 100644 index 00000000..e486fa61 --- /dev/null +++ b/i18n/README.fa_IR.md @@ -0,0 +1,1442 @@ +
+ داشبورد 9Router + + # 9Router - مسیریاب رایگان هوش مصنوعی و ذخیره‌ساز توکن + + **هرگز کدنویسی را متوقف نکنید. با RTK بین ۲۰ تا ۴۰٪ در توکن‌ها صرفه‌جویی کنید + بازگشت خودکار به مدل‌های رایگان و ارزان هوش مصنوعی.** + + **همه ابزارهای کدنویسی مبتنی بر هوش مصنوعی (Claude Code، Cursor، Antigravity، Copilot، Codex، Gemini، OpenCode، Cline، 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) + [![Docker Pulls](https://img.shields.io/docker/pulls/decolua/9router.svg?logo=docker&label=Docker%20pulls)](https://hub.docker.com/r/decolua/9router) + [![GHCR](https://img.shields.io/badge/GHCR-decolua%2F9router-blue?logo=github)](https://github.com/decolua/9router/pkgs/container/9router) + [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) + +decolua%2F9router | Trendshift + +[🚀 شروع سریع](#-شروع-سریع) • [💡 ویژگی‌ها](#-ویژگی‌های-کلیدی) • [📖 راه‌اندازی](#-راهنمای-راه‌اندازی) • [🌐 وب‌سایت](https://9router.com) + +[🇻🇳 Tiếng Việt](./i18n/README.vi.md) • [🇨🇳 中文](./i18n/README.zh-CN.md) • [🇯🇵 日本語](./i18n/README.ja-JP.md) • [🇷🇺 Русский](./i18n/README.ru.md) • [🇮🇷 فارسی](./i18n/README.fa_IR.md) + +
+ +--- + +## 🤔 چرا 9Router؟ + +**هدررفت پول، توکن و برخورد با محدودیت‌ها را متوقف کنید:** + +- ❌ سهمیه اشتراک هر ماه بدون استفاده منقضی می‌شود +- ❌ محدودیت نرخ درخواست، شما را در میانه کدنویسی متوقف می‌کند +- ❌ خروجی ابزارها (git diff، grep، ls...) به سرعت توکن می‌سوزانند +- ❌ APIهای گران قیمت (۲۰ تا ۵۰ دلار در ماه برای هر ارائه‌دهنده) +- ❌ جابجایی دستی بین ارائه‌دهندگان + +**9Router این مشکلات را حل می‌کند:** + +- ✅ **ذخیره‌ساز توکن RTK** - فشرده‌سازی خودکار محتوای tool_result، صرفه‌جویی ۲۰ تا ۴۰٪ توکن در هر درخواست +- ✅ **حداکثر استفاده از اشتراک‌ها** - پیگیری سهمیه، استفاده از هر ذره قبل از بازنشانی +- ✅ **بازگشت خودکار** - اشتراک → ارزان → رایگان، بدون توقف +- ✅ **چند حساب کاربری** - چرخش گردشی بین حساب‌ها برای هر ارائه‌دهنده +- ✅ **جهانی** - با Claude Code، Codex، Cursor، Cline و هر ابزار خط فرمان کار می‌کند + +--- + +## 🔄 نحوه عملکرد + +``` +┌─────────────┐ +│ ابزار خط │ (Claude Code, Codex, OpenClaw, Cursor, Cline...) +│ فرمان شما │ +└──────┬──────┘ + │ http://localhost:20128/v1 + ↓ +┌─────────────────────────────────────────────┐ +│ 9Router (مسیریاب هوشمند) │ +│ • ذخیره‌ساز توکن RTK (کاهش توکن‌های tool_result) │ +│ • ترجمه قالب (OpenAI ↔ Claude) │ +│ • پیگیری سهمیه │ +│ • بازسازی خودکار توکن │ +└──────┬──────────────────────────────────────┘ + │ + ├─→ [لایه ۱: اشتراک] Claude Code, Codex, GitHub Copilot + │ ↓ اتمام سهمیه + ├─→ [لایه ۲: ارزان] GLM (۰.۶ دلار/میلیون), MiniMax (۰.۲ دلار/میلیون) + │ ↓ محدودیت بودجه + └─→ [لایه ۳: رایگان] Kiro, OpenCode Free, Vertex (۳۰۰ دلار اعتبار) + +نتیجه: هرگز کدنویسی را متوقف نکنید، حداقل هزینه + صرفه‌جویی ۲۰-۴۰٪ توکن با RTK +``` + +--- + +## ⚡ شروع سریع + +**۱. نصب سراسری:** + +```bash +npm install -g 9router +9router +``` + +🎉 داشبورد در آدرس `http://localhost:20128` باز می‌شود + +**۲. اتصال یک ارائه‌دهنده رایگان (بدون نیاز به ثبت‌نام):** + +داشبورد → ارائه‌دهندگان → اتصال **Kiro AI** (کلود رایگان نامحدود) یا **OpenCode Free** (بدون احراز هویت) → انجام شد! + +**۳. استفاده در ابزار خط فرمان خود:** + +``` +تنظیمات Claude Code/Codex/OpenClaw/Cursor/Cline: + آدرس端点: http://localhost:20128/v1 + کلید API: [کپی از داشبورد] + مدل: kr/claude-sonnet-4.5 +``` + +**کار تمام!** با مدل‌های رایگان هوش مصنوعی کدنویسی را شروع کنید. + +**روش جایگزین: اجرا از سورس (این مخزن):** + +بسته این مخزن خصوصی است (`9router-app`)، بنابراین اجرا از سورس/داکر مسیر معمول توسعه محلی است. + +```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 +``` + +آدرس‌های پیش‌فرض: + +- داشبورد: `http://localhost:20128/dashboard` +- API سازگار با OpenAI: `http://localhost:20128/v1` + +--- + +## راهنماهای تصویری + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + صرفه‌جویی در هزینه LLM با 9Router +
+ 🇻🇳 ویتنامی
+ صرفه‌جویی در هزینه LLM برای OpenClaw با 9Router
توسط Mì AI
+
+ + راه‌اندازی نامحدود رایگان 9Router + Claude Code +
+ 🇵🇰 اردو / हिन्दी
+ راه‌اندازی نامحدود رایگان 9Router + Claude Code
توسط Build AI With Hamid
+
+ + آموزش راه‌اندازی 9Router +
+ 🇺🇸 انگلیسی
+ راه‌اندازی رایگان 9Router + Claude Code
توسط Build AI With Hamid
+
+ + آموزش راه‌اندازی 9Router +
+ 🇺🇸 انگلیسی
+ راه‌اندازی رایگان 9Router + Claude Code
توسط Build AI With Hamid
+
+ + Claude Code رایگان برای همیشه +
+ 🇺🇸 انگلیسی
+ Claude Code رایگان برای همیشه — مدل‌های نامحدود
توسط Build AI With Hamid
+
+ + راه‌اندازی رایگان Claude CLI +
+ 🇺🇸 انگلیسی
+ راه‌اندازی رایگان Claude CLI با 9Router 🚀
توسط CodeVerse Soban
+
+ + نصب کامل OpenClaw رایگان +
+ 🇻🇳 ویتنامی
+ نصب کامل OpenClaw رایگان از صفر تا صد + 9Router
توسط Mai Gia
+
+ + OpenClaw رایگان با Claude Opus +
+ 🇺🇸 انگلیسی
+ OpenClaw رایگان + Claude Opus 4.6
توسط Build AI With Hamid
+
+ + راه‌اندازی رایگان Claude CLI +
+ 🇮🇩 اندونزیایی
+ کدنویسی ۲۴ ساعته بدون محدودیت نرخ! صرفه‌جویی ۶۵٪ توکن هوش مصنوعی | آموزش راه‌اندازی سریع 9Router 🚀
توسط Krisswuh
+
+ + روش استقرار 9Router در Hugging Face رایگان و همیشه روشن! | جایگزین VPS با ۱۶ گیگابایت رم +
+ 🇮🇩 اندونزیایی
+ روش استقرار 9Router در Hugging Face رایگان و همیشه روشن! | جایگزین VPS با ۱۶ گیگابایت رم
توسط Krisswuh
+
+ +
+ +> 🎬 **درباره 9Router ویدیو ساخته‌اید؟** یک [درخواست Pull](https://github.com/decolua/9router/pulls) برای افزودن ویدیوی خود به این بخش ارسال کنید — ما آن را ادغام خواهیم کرد! + +--- + +## 🛠️ ابزارهای خط فرمان پشتیبانی شده + +9Router به‌طور یکپارچه با تمام ابزارهای اصلی کدنویسی هوش مصنوعی کار می‌کند: + +
+ + + + + + + + + + + + + + + + + +
+ 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 +
+ Kimchi
+ Kimchi +
+
+ +### 🆓 ارائه‌دهندگان رایگان + +
+ + + + + + +
+ Kiro
+ Kiro AI
+ Claude 4.5 + GLM-5 + MiniMax
نامحدود رایگان
+
+ OpenCode Free
+ OpenCode Free
+ بدون احراز هویت • دریافت خودکار مدل‌ها
نامحدود رایگان
+
+ Vertex AI
+ Vertex AI
+ Gemini 3 Pro + GLM-5 + DeepSeek
۳۰۰ دلار اعتبار رایگان
+
+
+ +> **توجه:** لایه‌های رایگان iFlow، Qwen و Gemini CLI در سال ۲۰۲۶ متوقف شدند. به جای آنها از Kiro / OpenCode Free / Vertex استفاده کنید. + +### 🔑 ارائه‌دهندگان کلید API (۴۰+) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ 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 +
+

...و بیش از ۲۰ ارائه‌دهنده دیگر از جمله Nebius، Chutes، Hyperbolic و نقاط پایانی سفارشی سازگار با OpenAI/Anthropic

+
+ +--- + +## 💡 ویژگی‌های کلیدی + +| ویژگی | عملکرد | اهمیت آن | +| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------- | +| 🚀 **ذخیره‌ساز توکن RTK** ([RTK](https://github.com/rtk-ai/rtk) ⭐۴۰هزار) | فشرده‌سازی خروجی ابزارها (`git diff`، `grep`، `ls`، `tree`...) قبل از ارسال به LLM | صرفه‌جویی **۲۰ تا ۴۰٪ توکن ورودی** در هر درخواست | +| 🧠 **ذخیره‌ساز توکن Headroom** ([Headroom](https://github.com/chopratejas/headroom)) | پروکسی خارجی اختیاری `/v1/compress` قبل از مسیریابی به ارائه‌دهنده | صرفه‌جویی توکن‌های زمینه بیشتر بدون تغییر کلاینت | +| 🪨 **حالت غارنشین** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐۵۲هزار) | تزریق پرامپت حالت غارنشین → پاسخ‌های مختصر LLM با حفظ محتوای فنی | صرفه‌جویی **تا ۶۵٪ توکن خروجی** | +| 🐴 **دم‌اسب** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | تزریق پرامپت "توسعه‌دهنده ارشد تنبل" → کدنویسی حداقلی و YAGNI-first (سبک/کامل/فوق‌سبک) | **توکن خروجی کمتر، بازنویسی کمتر** | +| 🎯 **بازگشت هوشمند ۳ لایه** | مسیریابی خودکار: اشتراک → ارزان → رایگان | هرگز کدنویسی متوقف نمی‌شود، بدون توقف | +| 📊 **پیگیری سهمیه به‌روز** | تعداد توکن زنده + شمارش معکوس بازنشانی | حداکثر استفاده از اشتراک | +| 🔄 **ترجمه قالب** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | کار با هر ابزار خط فرمان | +| 👥 **پشتیبانی از چند حساب** | چند حساب برای هر ارائه‌دهنده | توزیع بار + افزونگی | +| 🔄 **بازسازی خودکار توکن** | توکن‌های OAuth به‌طور خودکار بازسازی می‌شوند | بدون نیاز به ورود مجدد دستی | +| 🎨 **ترکیب‌های سفارشی** | ایجاد ترکیب‌های نامحدود مدل | تنظیم بازگشت بر اساس نیاز شما | +| 📝 **ثبت درخواست** | حالت اشکال‌زدایی با لاگ‌های کامل درخواست/پاسخ | عیب‌یابی آسان مسائل | +| 💾 **همگام‌سازی ابری** | همگام‌سازی تنظیمات بین دستگاه‌ها | همان تنظیمات در همه جا | +| 📊 **تحلیل استفاده** | پیگیری توکن‌ها، هزینه، روندها در طول زمان | بهینه‌سازی هزینه‌ها | +| 🌐 **استقرار در هر جا** | لوکال‌هست، VPS، داکر، Cloudflare Workers | گزینه‌های استقرار انعطاف‌پذیر | + +
+📖 جزئیات ویژگی‌ها + +### 🚀 ذخیره‌ساز توکن RTK + +خروجی ابزارها (`git diff`، `grep`، `find`، `ls`، `tree`، دامپ لاگ‌ها...) اغلب ۳۰ تا ۵۰٪ از بودجه پرامپت شما را مصرف می‌کنند. RTK آنها را شناسایی کرده و فشرده‌سازی هوشمند و بدون افت کیفیت **قبل از رسیدن درخواست به LLM** اعمال می‌کند: + +- **فیلترها:** `git-diff`، `git-status`، `grep`، `find`، `ls`، `tree`، `dedup-log`، `smart-truncate`، `read-numbered`، `search-list` +- **تشخیص خودکار:** نیازی به تنظیمات نیست — RTK یک کیلوبایت اول هر `tool_result` را بررسی کرده و فیلتر مناسب را انتخاب می‌کند. +- **ایمن در طراحی:** اگر فیلتری با شکست مواجه شود، خطا دهد یا خروجی را بزرگ‌تر کند، RTK بی‌صدا متن اصلی را نگه می‌دارد. خطاها هرگز درخواست شما را خراب نمی‌کنند. +- **جهانی:** در همه فرمت‌ها (OpenAI، Claude، Gemini، Cursor، Kiro، OpenAI Responses) کار می‌کند زیرا **قبل از** هرگونه ترجمه قالب اجرا می‌شود. +- **روشن پیش‌فرض:** در هر زمان در داشبورد → تنظیمات نقطه پایانی قابل تغییر است. + +``` +بدون RTK: ۴۷ هزار توکن ارسال شده به LLM +با RTK: ۲۸ هزار توکن ارسال شده به LLM (۴۰٪ صرفه‌جویی · همان زمینه · همان پاسخ) +``` + +### 🧠 ذخیره‌ساز توکن Headroom + +Headroom اختیاری است و به‌طور جداگانه اجرا می‌شود. 9Router نقطه پایانی محلی `/v1/compress` Headroom را فراخوانی کرده، سپس مسیریابی معمولی، بازگشت، احراز هویت و پیگیری مصرف را ادامه می‌دهد: + +``` +کلاینت → 9Router → Headroom /v1/compress → 9Router → ارائه‌دهنده +``` + +راه‌اندازی محلی: + +```bash +pip install "headroom-ai[proxy]" +headroom proxy --port 8787 +``` + +در داشبورد → نقطه پایانی → ذخیره‌ساز توکن → Headroom فعال کنید. آدرس پیش‌فرض: `http://localhost:8787`. + +مثال‌های داکر: + +```bash +# سرویس Headroom در همان شبکه داکر +http://headroom:8787 + +# Headroom در حال اجرا روی ماشین میزبان +http://host.docker.internal:8787 +``` + +اگر Headroom از کار بیفتد یا خطا برگرداند، 9Router به‌حالت بازگشت باز می‌شود و درخواست اصلی را ارسال می‌کند. + +### 🐴 دم‌اسب (توسعه‌دهنده ارشد تنبل) + +دم‌اسب یک پرامپت سیستمی _"توسعه‌دهنده ارشد تنبل"_ را به هر درخواست تزریق می‌کند و LLM را به سمت کدنویسی حداقلی و YAGNI-first سوق می‌دهد — حذف به جای افزودن، کتابخانه استاندارد به جای وابستگی‌های جدید، یک خطی به جای انتزاعات. اقتباس شده از [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). + +- **سبک** — آنچه خواسته شده را بساز، جایگزین تنبل‌تر را نام ببر. +- **کامل** — نردبان YAGNI اعمال می‌شود: کتابخانه استاندارد → بومی → وابستگی‌های موجود → یک خطی → حداقل کد. +- **فوق‌سبک** — افراط‌گرای YAGNI: اول حذف، یک خطی را ارسال کن، بقیه نیازمندی را در همان پاسخ به چالش بکش. + +``` +بدون دم‌اسب: کد پرحجم، انتزاعات اضافی، داربست‌های "فقط در صورت نیاز" +با دم‌اسب: کوتاه‌ترین دیف کاری، بدون انتزاعات درخواست نشده، توکن کمتر +``` + +هرگز موارد زیر را قربانی نمی‌کند: اعتبارسنجی ورودی، مدیریت خطا که از از دست رفتن داده جلوگیری می‌کند، امنیت، دسترس‌پذیری، یا هر چیزی که به‌صراحت درخواست شده باشد. در داشبورد → نقطه پایانی → دم‌اسب فعال کنید. با حالت غارنشین (مختصر بودن خروجی) و RTK (فشرده‌سازی ورودی) ترکیب می‌شود. + +### 🎯 بازگشت هوشمند ۳ لایه + +ترکیب‌هایی با بازگشت خودکار ایجاد کنید: + +``` +ترکیب: "my-coding-stack" + 1. cc/claude-opus-4-6 (اشتراک شما) + 2. glm/glm-4.7 (پشتیبان ارزان، ۰.۶ دلار/میلیون) + 3. if/kimi-k2-thinking (بازگشت رایگان) + +→ وقتی سهمیه تمام شود یا خطا رخ دهد، به‌طور خودکار تغییر می‌کند +``` + +### 📊 پیگیری سهمیه به‌روز + +- مصرف توکن به ازای هر ارائه‌دهنده +- شمارش معکوس بازنشانی (۵ ساعته، روزانه، هفتگی) +- تخمین هزینه برای لایه‌های پولی +- گزارش‌های هزینه ماهانه + +### 🔄 ترجمه قالب + +ترجمه یکپارچه بین قالب‌ها: + +- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses** +- ابزار خط فرمان شما قالب OpenAI ارسال می‌کند → 9Router ترجمه می‌کند → ارائه‌دهنده قالب بومی دریافت می‌کند +- با هر ابزاری که از نقاط پایانی سفارشی OpenAI پشتیبانی می‌کند کار می‌کند + +### 👥 پشتیبانی از چند حساب + +- افزودن چند حساب برای هر ارائه‌دهنده +- مسیریابی خودکار گردشی یا اولویت‌محور +- بازگشت به حساب بعدی وقتی یکی به سهمیه رسید + +### 🔄 بازسازی خودکار توکن + +- توکن‌های OAuth به‌طور خودکار قبل از انقضا بازسازی می‌شوند +- بدون نیاز به احراز هویت مجدد دستی +- تجربه یکپارچه در همه ارائه‌دهندگان + +### 🎨 ترکیب‌های سفارشی + +- ایجاد ترکیب‌های نامحدود مدل +- ترکیب لایه‌های اشتراک، ارزان و رایگان +- نام‌گذاری ترکیب‌ها برای دسترسی آسان +- اشتراک‌گذاری ترکیب‌ها بین دستگاه‌ها با همگام‌سازی ابری + +### 📝 ثبت درخواست + +- فعال‌سازی حالت اشکال‌زدایی برای لاگ‌های کامل درخواست/پاسخ +- پیگیری فراخوانی‌های API، هدرها و محموله‌ها +- عیب‌یابی مسائل یکپارچه‌سازی +- خروجی لاگ‌ها برای تحلیل + +### 💾 همگام‌سازی ابری + +- همگام‌سازی ارائه‌دهندگان، ترکیب‌ها و تنظیمات بین دستگاه‌ها +- همگام‌سازی خودکار در پس‌زمینه +- ذخیره‌سازی رمزگذاری شده امن +- دسترسی به تنظیمات خود از هر جا + +#### نکات اجرای ابری + +- در تولید از متغیرهای سمت سرور ابری استفاده کنید: + - `BASE_URL` (آدرس داخلی بازگشت برای برنامه‌ریز همگام‌سازی) + - `CLOUD_URL` (آدرس پایه نقطه پایانی همگام‌سازی ابری) +- `NEXT_PUBLIC_BASE_URL` و `NEXT_PUBLIC_CLOUD_URL` همچنان برای سازگاری/رابط کاربری پشتیبانی می‌شوند، اما زمان اجرای سرور اکنون `BASE_URL`/`CLOUD_URL` را اولویت می‌دهد. +- درخواست‌های همگام‌سازی ابری اکنون از زمان‌بندی + رفتار شکست سریع برای جلوگیری از هنگ کردن رابط کاربری در صورت عدم دسترسی شبکه ابری/DNS استفاده می‌کنند. + +### 📊 تحلیل استفاده + +- پیگیری مصرف توکن به ازای هر ارائه‌دهنده و مدل +- تخمین هزینه و روندهای هزینه +- گزارش‌های ماهانه و بینش‌ها +- بهینه‌سازی هزینه هوش مصنوعی + +> **💡 مهم - درک هزینه‌های داشبورد:** +> +> "هزینه" نمایش داده شده در تحلیل استفاده **فقط برای پیگیری و مقایسه** است. +> خود 9Router **هرگز از شما هزینه‌ای دریافت نمی‌کند**. شما فقط مستقیماً به ارائه‌دهندگان هزینه می‌پردازید (در صورت استفاده از خدمات پولی). +> +> **مثال:** اگر داشبورد شما "۲۹۰ دلار هزینه کل" را هنگام استفاده از مدل‌های iFlow نشان می‌دهد، این مبلغ چیزی است که در صورت استفاده مستقیم از APIهای پولی پرداخت می‌کردید. هزینه واقعی شما = **۰ دلار** (iFlow رایگان نامحدود است). +> +> به آن به عنوان "ردیاب پس‌انداز" فکر کنید که نشان می‌دهد با استفاده از مدل‌های رایگان یا مسیریابی از طریق 9Router چقدر صرفه‌جویی می‌کنید! + +### 🌐 استقرار در هر جا + +- 💻 **لوکال‌هست** - پیش‌فرض، آفلاین کار می‌کند +- ☁️ **VPS/ابر** - اشتراک‌گذاری بین دستگاه‌ها +- 🐳 **داکر** - استقرار با یک دستور +- 🚀 **Cloudflare Workers** - شبکه لبه جهانی + +
+ +--- + +## 💰 قیمت‌گذاری در یک نگاه + +| لایه | ارائه‌دهنده | هزینه | بازنشانی سهمیه | بهترین استفاده | +| ------------------- | --------------------- | ------------ | ---------------- | --------------------------------------- | +| **🚀 ذخیره‌ساز توکن** | **RTK (ساخته شده)** | **رایگان** | همیشه روشن | **صرفه‌جویی ۲۰-۴۰٪ توکن در هر درخواست** | +| **💳 اشتراک** | Claude Code (Pro/Max) | ۲۰-۲۰۰ دلار/ماه | ۵ ساعته + هفتگی | قبلاً اشتراک دارید | +| | Codex (Plus/Pro) | ۲۰-۲۰۰ دلار/ماه | ۵ ساعته + هفتگی | کاربران OpenAI | +| | GitHub Copilot | ۱۰-۱۹ دلار/ماه | ماهانه | کاربران GitHub | +| | Cursor IDE | ۲۰ دلار/ماه | ماهانه | کاربران Cursor | +| **💰 ارزان** | GLM-5.1 / GLM-4.7 | ۰.۶ دلار/میلیون | روزانه ساعت ۱۰ صبح | پشتیبان بودجه | +| | MiniMax M2.7 | ۰.۲ دلار/میلیون | ۵ ساعته گردشی | ارزان‌ترین گزینه | +| | Kimi K2.5 | ۹ دلار/ماه مسطح | ۱۰ میلیون توکن/ماه | هزینه قابل پیش‌بینی | +| **🆓 رایگان** | Kiro AI | ۰ دلار | نامحدود | Claude 4.5 + GLM-5 + MiniMax رایگان | +| | OpenCode Free | ۰ دلار | نامحدود | بدون احراز هویت، دریافت خودکار مدل‌ها | +| | Vertex AI | ۳۰۰ دلار اعتبار | حساب‌های جدید GCP | Gemini 3 Pro + DeepSeek + GLM-5 | + +**💡 نکته حرفه‌ای:** ترکیب RTK + Kiro AI + OpenCode Free = **۰ دلار هزینه + ۲۰-۴۰٪ صرفه‌جویی توکن**! + +--- + +### 📊 درک هزینه‌ها و صورتحساب 9Router + +**واقعیت صورتحساب 9Router:** + +✅ **نرم‌افزار 9Router = رایگان برای همیشه** (منبع باز، هرگز هزینه‌ای دریافت نمی‌کند) +✅ **"هزینه‌های" داشبورد = فقط نمایش/پیگیری** (صورتحساب واقعی نیستند) +✅ **شما مستقیماً به ارائه‌دهندگان هزینه می‌پردازید** (اشتراک‌ها یا هزینه‌های API) +✅ **ارائه‌دهندگان رایگان واقعاً رایگان هستند** (iFlow، Kiro، Qwen = ۰ دلار نامحدود) +❌ **9Router هرگز صورتحساب ارسال نمی‌کند** یا کارت شما را شارژ نمی‌کند + +**نحوه عملکرد نمایش هزینه:** + +داشبورد **هزینه‌های تخمینی** را نشان می‌دهد گویی مستقیماً از APIهای پولی استفاده می‌کنید. این **صورتحساب نیست** - این یک ابزار مقایسه برای نشان دادن پس‌انداز شماست. + +**سناریوی مثال:** + +``` +نمایش داشبورد: +• تعداد درخواست‌ها: ۱,۶۶۲ +• کل توکن‌ها: ۴۷ میلیون +• هزینه نمایشی: ۲۹۰ دلار + +بررسی واقعیت: +• ارائه‌دهنده: iFlow (رایگان نامحدود) +• پرداخت واقعی: ۰.۰۰ دلار +• منظور از ۲۹۰ دلار: مبلغی که با استفاده از مدل‌های رایگان پس‌انداز کرده‌اید! +``` + +**قوانین پرداخت:** + +- **ارائه‌دهندگان اشتراک** (Claude Code، Codex): مستقیماً از طریق وب‌سایت‌هایشان به آنها پرداخت کنید +- **ارائه‌دهندگان ارزان** (GLM، MiniMax): مستقیماً به آنها پرداخت کنید، 9Router فقط مسیریابی می‌کند +- **ارائه‌دهندگان رایگان** (iFlow، Kiro، Qwen): واقعاً برای همیشه رایگان، بدون هزینه پنهان +- **9Router**: هرگز هیچ هزینه‌ای دریافت نمی‌کند، همیشه + +--- + +## 🎯 موارد استفاده + +### مورد ۱: "من اشتراک Claude Pro دارم" + +**مشکل:** سهمیه بدون استفاده منقضی می‌شود، محدودیت نرخ در حین کدنویسی سنگین + +**راه‌حل:** + +``` +ترکیب: "maximize-claude" + 1. cc/claude-opus-4-7 (استفاده کامل از اشتراک) + 2. glm/glm-5.1 (پشتیبان ارزان وقتی سهمیه تمام شد) + 3. kr/claude-sonnet-4.5 (بازگشت اضطراری رایگان) + +هزینه ماهانه: ۲۰ دلار (اشتراک) + حدود ۵ دلار (پشتیبان) = ۲۵ دلار کل +در مقابل ۲۰ دلار + برخورد با محدودیت = ناامیدی +``` + +### مورد ۲: "من هزینه صفر می‌خواهم" + +**مشکل:** توانایی پرداخت اشتراک را ندارم، به هوش مصنوعی کدنویسی قابل اعتماد نیاز دارم + +**راه‌حل:** + +``` +ترکیب: "free-forever" + 1. kr/claude-sonnet-4.5 (Claude 4.5 رایگان نامحدود) + 2. kr/glm-5 (GLM-5 رایگان از طریق Kiro) + 3. oc/ (OpenCode Free، بدون احراز هویت) + +هزینه ماهانه: ۰ دلار +کیفیت: مدل‌های آماده تولید + RTK صرفه‌جویی ۲۰-۴۰٪ توکن +``` + +### مورد ۳: "به کدنویسی ۲۴/۷ بدون وقفه نیاز دارم" + +**مشکل:** ضرب‌الاجل‌ها، توانایی پرداخت هزینه توقف را ندارم + +**راه‌حل:** + +``` +ترکیب: "always-on" + 1. cc/claude-opus-4-7 (بهترین کیفیت) + 2. cx/gpt-5.5 (اشتراک دوم) + 3. glm/glm-5.1 (ارزان، بازنشانی روزانه) + 4. minimax/MiniMax-M2.7 (ارزان‌ترین، بازنشانی ۵ ساعته) + 5. kr/claude-sonnet-4.5 (رایگان نامحدود) + +نتیجه: ۵ لایه بازگشت = بدون توقف +هزینه ماهانه: ۲۰-۲۰۰ دلار (اشتراک‌ها) + ۱۰-۲۰ دلار (پشتیبان) +``` + +### مورد ۴: "من هوش مصنوعی رایگان در OpenClaw می‌خواهم" + +**مشکل:** به دستیار هوش مصنوعی در برنامه‌های پیام‌رسان (واتساپ، تلگرام، اسلک...) نیاز دارم، کاملاً رایگان + +**راه‌حل:** + +``` +ترکیب: "openclaw-free" + 1. kr/claude-sonnet-4.5 (Claude 4.5 رایگان) + 2. kr/glm-5 (GLM-5 رایگان) + 3. kr/MiniMax-M2.5 (MiniMax رایگان) + +هزینه ماهانه: ۰ دلار +دسترسی از طریق: واتساپ، تلگرام، اسلک، دیسکورد، iMessage، سیگنال... +``` + +--- + +## ❓ سوالات متداول + +
+📊 چرا داشبورد من هزینه‌های بالا نشان می‌دهد؟ + +داشبورد مصرف توکن شما را پیگیری کرده و **هزینه‌های تخمینی** را نشان می‌دهد گویی مستقیماً از APIهای پولی استفاده می‌کنید. این **صورتحساب واقعی نیست** - این یک مرجع برای نشان دادن میزان پس‌انداز شما با استفاده از مدل‌های رایگان یا اشتراک‌های موجود از طریق 9Router است. + +**مثال:** + +- **داشبورد نشان می‌دهد:** "۲۹۰ دلار هزینه کل" +- **واقعیت:** شما از iFlow (رایگان نامحدود) استفاده می‌کنید +- **هزینه واقعی شما:** **۰.۰۰ دلار** +- **منظور از ۲۹۰ دلار:** مبلغی که با استفاده از مدل‌های رایگان به جای APIهای پولی **پس‌انداز** کرده‌اید! + +نمایش هزینه یک "ردیاب پس‌انداز" است تا به شما در درک الگوهای مصرف و فرصت‌های بهینه‌سازی کمک کند. + +
+ +
+💳 آیا توسط 9Router شارژ می‌شوم؟ + +**خیر.** 9Router نرم‌افزاری رایگان و منبع باز است که روی رایانه خودتان اجرا می‌شود. هرگز از شما هزینه‌ای دریافت نمی‌کند. + +**شما فقط پرداخت می‌کنید:** + +- ✅ **ارائه‌دهندگان اشتراک** (Claude Code ۲۰ دلار/ماه، Codex ۲۰-۲۰۰ دلار/ماه) → مستقیماً در وب‌سایت‌هایشان به آنها پرداخت کنید +- ✅ **ارائه‌دهندگان ارزان** (GLM، MiniMax) → مستقیماً به آنها پرداخت کنید، 9Router فقط درخواست‌های شما را مسیریابی می‌کند +- ❌ **خود 9Router** → **هرگز هیچ هزینه‌ای دریافت نمی‌کند، همیشه** + +9Router یک پروکسی/مسیریاب محلی است. کارت اعتباری شما را ندارد، نمی‌تواند صورتحساب ارسال کند و سیستم صورتحساب ندارد. این نرم‌افزار کاملاً رایگان است. + +
+ +
+🆓 آیا ارائه‌دهندگان رایگان واقعاً نامحدود هستند؟ + +**بله!** ارائه‌دهندگان رایگان فعلی (Kiro، OpenCode Free، Vertex) واقعاً رایگان هستند و **هزینه پنهانی ندارند**. + +اینها خدمات رایگانی هستند که توسط آن شرکت‌ها ارائه می‌شوند: + +- **Kiro AI**: Claude 4.5 + GLM-5 + MiniMax نامحدود رایگان از طریق AWS Builder ID / Google / GitHub OAuth +- **OpenCode Free**: پروکسی عبوری بدون احراز هویت، مدل‌ها به‌طور خودکار از `opencode.ai/zen/v1/models` دریافت می‌شوند +- **Vertex AI**: ۳۰۰ دلار اعتبار رایگان برای حساب‌های جدید Google Cloud (۹۰ روز) + +9Router فقط درخواست‌های شما را به آنها مسیریابی می‌کند - هیچ "دام" یا صورتحساب آینده‌ای وجود ندارد. آنها واقعاً خدمات رایگان هستند و 9Router استفاده از آنها را با پشتیبانی از بازگشت آسان می‌کند. + +**لایه‌های رایگان متوقف شده (دیگر توصیه نمی‌شوند):** + +- ❌ **iFlow**: قبلاً رایگان نامحدود بود، اکنون به پولی تغییر کرده است (۲۰۲۶) +- ❌ **Qwen Code**: لایه رایگان OAuth توسط علی‌بابا در ۲۰۲۶-۰۴-۱۵ متوقف شد +- ❌ **Gemini CLI**: همچنان کار می‌کند، اما استفاده از آن با ابزارهای غیر CLI (Claude، Codex، Cursor...) ممکن است منجر به مسدود شدن حساب شود — فقط در صورت استفاده از خود Gemini CLI از آن استفاده کنید + +
+ +
+💰 چگونه هزینه‌های واقعی هوش مصنوعی خود را به حداقل برسانم؟ + +**استراتژی اولویت با رایگان:** + +۱. **با ترکیب ۱۰۰٪ رایگان شروع کنید:** + + ``` + 1. gc/gemini-3-flash (۱۸۰ هزار توکن/ماه رایگان از گوگل) + 2. if/kimi-k2-thinking (نامحدود رایگان از iFlow) + 3. qw/qwen3-coder-plus (نامحدود رایگان از Qwen) + ``` + + **هزینه: ۰ دلار/ماه** + +۲. **در صورت نیاز، پشتیبان ارزان اضافه کنید:** + + ``` + 4. glm/glm-4.7 (۰.۶ دلار/میلیون توکن) + ``` + + **هزینه اضافی: فقط برای چیزی که واقعاً استفاده می‌کنید پرداخت کنید** + +۳. **از ارائه‌دهندگان اشتراک در آخر استفاده کنید:** + - فقط در صورتی که از قبل آنها را دارید + - 9Router با پیگیری سهمیه به حداکثر رساندن ارزش آنها کمک می‌کند + +**نتیجه:** اکثر کاربران می‌توانند با استفاده فقط از لایه‌های رایگان با ۰ دلار/ماه کار کنند! + +
+ +
+📈 اگر مصرف من ناگهان افزایش یابد چه؟ + +بازگشت هوشمند 9Router از هزینه‌های غافلگیرکننده جلوگیری می‌کند: + +**سناریو:** شما در یک ماراتن کدنویسی هستید و سهمیه‌های خود را تمام می‌کنید + +**بدون 9Router:** + +- ❌ برخورد با محدودیت نرخ → کار متوقف می‌شود → ناامیدی +- ❌ یا: به‌طور تصادفی صورت‌حساب‌های عظیم API جمع می‌کنید + +**با 9Router:** + +- ✅ اشتراک به حد مجاز می‌رسد → بازگشت خودکار به لایه ارزان +- ✅ لایه ارزان گران می‌شود → بازگشت خودکار به لایه رایگان +- ✅ هرگز کدنویسی را متوقف نکنید → هزینه‌های قابل پیش‌بینی + +**شما کنترل دارید:** محدودیت‌های هزینه را برای هر ارائه‌دهنده در داشبورد تنظیم کنید و 9Router به آنها احترام می‌گذارد. + +
+ +--- + +## 📖 راهنمای راه‌اندازی + +
+🔐 ارائه‌دهندگان اشتراک (حداکثر کردن ارزش) + +### Claude Code (Pro/Max) + +```bash +داشبورد → ارائه‌دهندگان → اتصال Claude Code +→ ورود OAuth → بازسازی خودکار توکن +→ پیگیری سهمیه ۵ ساعته + هفتگی + +مدل‌ها: + cc/claude-opus-4-7 + cc/claude-opus-4-6 + cc/claude-sonnet-4-6 + cc/claude-haiku-4-5-20251001 +``` + +**نکته حرفه‌ای:** از Opus برای کارهای پیچیده و Sonnet برای سرعت استفاده کنید. 9Router سهمیه را به ازای هر مدل پیگیری می‌کند! + +### OpenAI Codex (Plus/Pro) + +```bash +داشبورد → ارائه‌دهندگان → اتصال Codex +→ ورود OAuth (پورت ۱۴۵۵) +→ بازنشانی ۵ ساعته + هفتگی + +مدل‌ها: + cx/gpt-5.5 + cx/gpt-5.4 + cx/gpt-5.3-codex + cx/gpt-5.2-codex +``` + +### GitHub Copilot + +```bash +داشبورد → ارائه‌دهندگان → اتصال GitHub +→ OAuth از طریق GitHub +→ بازنشانی ماهانه (اول ماه) + +مدل‌ها: + gh/gpt-5.4 + gh/claude-opus-4.7 + gh/claude-sonnet-4.6 + gh/gemini-3.1-pro-preview + gh/grok-code-fast-1 +``` + +### Cursor IDE + +```bash +داشبورد → ارائه‌دهندگان → اتصال Cursor +→ ورود OAuth +→ اشتراک ماهانه + +مدل‌ها: + cu/claude-4.6-opus-max + cu/claude-4.5-sonnet-thinking + cu/gpt-5.3-codex +``` + +
+ +
+💰 ارائه‌دهندگان ارزان (پشتیبان) + +### GLM-5.1 / GLM-4.7 (بازنشانی روزانه، ۰.۶ دلار/میلیون) + +۱. ثبت‌نام: [Zhipu AI](https://open.bigmodel.cn/) +۲. دریافت کلید API از Coding Plan +۳. داشبورد → افزودن کلید API: + - ارائه‌دهنده: `glm` + - کلید API: `your-key` + +**استفاده:** `glm/glm-5.1`، `glm/glm-5`، `glm/glm-4.7` + +**نکته حرفه‌ای:** Coding Plan ۳ برابر سهمیه با ۱/۷ هزینه ارائه می‌دهد! بازنشانی روزانه ساعت ۱۰:۰۰ صبح. + +### MiniMax M2.7 (بازنشانی ۵ ساعته، ۰.۲۰ دلار/میلیون) + +۱. ثبت‌نام: [MiniMax](https://www.minimax.io/) +۲. دریافت کلید API +۳. داشبورد → افزودن کلید API + +**استفاده:** `minimax/MiniMax-M2.7`، `minimax/MiniMax-M2.5` + +**نکته حرفه‌ای:** ارزان‌ترین گزینه برای زمینه طولانی (۱ میلیون توکن)! + +### Kimi K2.5 (۹ دلار/ماه مسطح) + +۱. اشتراک: [Moonshot AI](https://platform.moonshot.ai/) +۲. دریافت کلید API +۳. داشبورد → افزودن کلید API + +**استفاده:** `kimi/kimi-k2.5`، `kimi/kimi-k2.5-thinking` + +**نکته حرفه‌ای:** ۹ دلار/ماه ثابت برای ۱۰ میلیون توکن = هزینه مؤثر ۰.۹۰ دلار/میلیون! + +
+ +
+🆓 ارائه‌دهندگان رایگان (توصیه شده) + +### Kiro AI (Claude 4.5 + GLM-5 + MiniMax رایگان) + +```bash +داشبورد → اتصال Kiro +→ AWS Builder ID، AWS IAM Identity Center، Google، یا GitHub +→ استفاده نامحدود + +مدل‌ها: + kr/claude-sonnet-4.5 + kr/claude-haiku-4.5 + kr/glm-5 + kr/MiniMax-M2.5 + kr/qwen3-coder-next + kr/deepseek-3.2 +``` + +**نکته حرفه‌ای:** بهترین گزینه رایگان برای Claude. بدون کلید API، بدون پرداخت، کاملاً نامحدود. + +### OpenCode Free (بدون احراز هویت، دریافت خودکار مدل‌ها) + +```bash +داشبورد → اتصال OpenCode Free +→ بدون نیاز به ورود (پروکسی عبوری) +→ مدل‌ها به‌طور خودکار از opencode.ai/zen/v1/models دریافت می‌شوند +``` + +**نکته حرفه‌ای:** سریع‌ترین راه‌اندازی. فقط متصل شوید و کدنویسی را شروع کنید. + +### Vertex AI (۳۰۰ دلار اعتبار رایگان برای حساب‌های جدید GCP) + +```bash +داشبورد → اتصال Vertex AI +→ آپلود JSON حساب سرویس Google Cloud +→ فعال‌سازی API Vertex AI در پروژه GCP خود + +مدل‌ها: + vertex/gemini-3.1-pro-preview + vertex/gemini-3-flash-preview + vertex/gemini-2.5-flash + +Vertex Partner (Anthropic / DeepSeek / GLM / Qwen از طریق Vertex): + vertex-partner/glm-5-maas + vertex-partner/deepseek-v3.2-maas + vertex-partner/qwen3-next-80b-a3b-thinking-maas +``` + +**نکته حرفه‌ای:** حساب‌های جدید Google Cloud ۳۰۰ دلار اعتبار رایگان به مدت ۹۰ روز دریافت می‌کنند. برای کدنویسی روزانه کافی است. + +
+ +
+🎨 ایجاد ترکیب‌ها + +### مثال ۱: حداکثر اشتراک → پشتیبان ارزان + +``` +داشبورد → ترکیب‌ها → ایجاد جدید + +نام: premium-coding +مدل‌ها: + 1. cc/claude-opus-4-7 (اشتراک اصلی) + 2. glm/glm-5.1 (پشتیبان ارزان، ۰.۶ دلار/میلیون) + 3. minimax/MiniMax-M2.7 (ارزان‌ترین بازگشت، ۰.۲۰ دلار/میلیون) + +استفاده در CLI: premium-coding + +مثال هزینه ماهانه (۱۰۰ میلیون توکن): + ۸۰ میلیون از طریق Claude (اشتراک): ۰ دلار اضافی + ۱۵ میلیون از طریق GLM: ۹ دلار + ۵ میلیون از طریق MiniMax: ۱ دلار + کل: ۱۰ دلار + اشتراک شما +``` + +### مثال ۲: فقط رایگان (هزینه صفر) + +``` +نام: free-combo +مدل‌ها: + 1. kr/claude-sonnet-4.5 (Claude 4.5 رایگان نامحدود) + 2. kr/glm-5 (GLM-5 رایگان از طریق Kiro) + 3. vertex/gemini-3.1-pro-preview (۳۰۰ دلار اعتبار رایگان) + +هزینه: ۰ دلار برای همیشه (+ صرفه‌جویی ۲۰-۴۰٪ توکن با RTK)! +``` + +
+ +
+🔧 یکپارچه‌سازی با CLI + +### Cursor IDE + +``` +تنظیمات → مدل‌ها → پیشرفته: + آدرس پایه API OpenAI: http://localhost:20128/v1 + کلید API OpenAI: [از داشبورد 9router] + مدل: cc/claude-opus-4-7 +``` + +یا از ترکیب استفاده کنید: `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 + +**گزینه ۱ — داشبورد (توصیه می‌شود):** + +``` +داشبورد → ابزارهای CLI → OpenClaw → انتخاب مدل → اعمال +``` + +**گزینه ۲ — دستی:** ویرایش `~/.openclaw/openclaw.json`: + +```json +{ + "agents": { + "defaults": { + "model": { + "primary": "9router/kr/claude-sonnet-4.5" + } + } + }, + "models": { + "providers": { + "9router": { + "baseUrl": "http://127.0.0.1:20128/v1", + "apiKey": "sk_9router", + "api": "openai-completions", + "models": [ + { + "id": "kr/claude-sonnet-4.5", + "name": "Claude Sonnet 4.5 (Kiro Free)" + } + ] + } + } + } +} +``` + +> **توجه:** OpenClaw فقط با 9Router محلی کار می‌کند. برای جلوگیری از مشکلات وضوح IPv6 از `127.0.0.1` به جای `localhost` استفاده کنید. + +### Cline / Continue / RooCode + +``` +ارائه‌دهنده: سازگار با OpenAI +آدرس پایه: http://localhost:20128/v1 +کلید API: [از داشبورد] +مدل: cc/claude-opus-4-7 +``` + +
+ +
+🚀 استقرار + +### استقرار در VPS + +```bash +# کلون و نصب +git clone https://github.com/decolua/9router.git +cd 9router +npm install +npm run build + +# پیکربندی +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" + +# شروع +npm run start + +# یا استفاده از PM2 +npm install -g pm2 +pm2 start npm --name 9router -- start +pm2 save +pm2 startup +``` + +### داکر + +تصاویر منتشر شده (چند پلتفرم `linux/amd64` + `linux/arm64`): + +- Docker Hub: [`decolua/9router`](https://hub.docker.com/r/decolua/9router) +- GHCR: [`ghcr.io/decolua/9router`](https://github.com/decolua/9router/pkgs/container/9router) + +**شروع سریع (استفاده از تصویر منتشر شده):** + +```bash +docker run -d \ + --name 9router \ + -p 20128:20128 \ + -v "$HOME/.9router:/app/data" \ + -e DATA_DIR=/app/data \ + decolua/9router:latest +``` + +→ باز کردن http://localhost:20128 + +**ساخت از سورس (توسعه):** + +```bash +git clone https://github.com/decolua/9router.git +cd 9router/app +docker build -t 9router . +docker run -d --name 9router -p 20128:20128 \ + -v "$HOME/.9router:/app/data" -e DATA_DIR=/app/data 9router +``` + +**پیش‌فرض‌های کانتینر:** + +- `PORT=20128` +- `HOSTNAME=0.0.0.0` + +**دستورات مفید:** + +```bash +docker logs -f 9router +docker restart 9router +docker stop 9router && docker rm 9router +docker pull decolua/9router:latest # به‌روزرسانی به آخرین نسخه +``` + +**ماندگاری داده:** `$HOME/.9router/db/data.sqlite` در میزبان ↔ `/app/data/db/data.sqlite` در کانتینر. + +### متغیرهای محیطی + +| متغیر | پیش‌فرض | توضیحات | +| ---------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | +| `JWT_SECRET` | تولید خودکار (`~/.9router/jwt-secret`) | راز امضای JWT برای کوکی احراز هویت داشبورد (برای اشتراک بین نمونه‌ها بازنویسی کنید) | +| `INITIAL_PASSWORD` | `123456` | رمز عبور اولین ورود در صورت عدم وجود هش ذخیره شده | +| `DATA_DIR` | `~/.9router` | مکان اصلی داده‌های برنامه (SQLite در `$DATA_DIR/db/data.sqlite`) | +| `PORT` | پیش‌فرض فریم‌ورک | پورت سرویس (`۲۰۱۲۸` در مثال‌ها) | +| `HOSTNAME` | پیش‌فرض فریم‌ورک | هاست بایند (داکر پیش‌فرض `۰.۰.۰.۰` است) | +| `NODE_ENV` | پیش‌فرض زمان اجرا | برای استقرار `production` را تنظیم کنید | +| `BASE_URL` | `http://localhost:20128` | آدرس پایه داخلی سمت سرور که توسط کارهای همگام‌سازی ابری استفاده می‌شود | +| `CLOUD_URL` | `https://9router.com` | آدرس پایه نقطه پایانی همگام‌سازی ابری سمت سرور | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | آدرس پایه عمومی/سازگار با گذشته (برای زمان اجرای سرور `BASE_URL` را ترجیح دهید) | +| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | آدرس ابری عمومی/سازگار با گذشته (برای زمان اجرای سرور `CLOUD_URL` را ترجیح دهید) | +| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | راز HMAC برای کلیدهای API تولید شده | +| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | نمک برای هش کردن شناسه ماشین پایدار | +| `ENABLE_REQUEST_LOGS` | `false` | لاگ‌های درخواست/پاسخ را در `logs/` فعال می‌کند | +| `AUTH_COOKIE_SECURE` | `false` | کوکی احراز هویت `Secure` را اعمال می‌کند (در پشت پروکسی معکوس HTTPS `true` تنظیم کنید) | +| `REQUIRE_API_KEY` | `false` | اعمال کلید API Bearer در مسیرهای `/v1/*` (برای استقرارهای در معرض اینترنت توصیه می‌شود) | +| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | خالی | پروکسی خروجی اختیاری برای فراخوانی‌های ارائه‌دهنده بالا دست | +| `SEARXNG_URL` | `http://localhost:8888/search` | نقطه پایانی برای ارائه‌دهنده جستجوی وب SearXNG ساخته شده بدون احراز هویت | + +نکات: + +- متغیرهای پروکسی با حروف کوچک نیز پشتیبانی می‌شوند: `http_proxy`، `https_proxy`، `all_proxy`، `no_proxy`. +- `.env` در تصویر داکر تعبیه نشده است (`.dockerignore`)؛ پیکربندی زمان اجرا را با `--env-file` یا `-e` تزریق کنید. +- در ویندوز، می‌توان از `APPDATA` برای وضوح مسیر ذخیره‌سازی محلی استفاده کرد. +- `INSTANCE_NAME` در مستندات قدیمی/الگوهای env ظاهر می‌شود، اما در حال حاضر در زمان اجرا استفاده نمی‌شود. + +### فایل‌های زمان اجرا و ذخیره‌سازی + +- وضعیت اصلی برنامه: `${DATA_DIR}/db/data.sqlite` (SQLite — ارائه‌دهندگان، ترکیب‌ها، نام‌های مستعار، کلیدها، تنظیمات، تاریخچه استفاده) +- پشتیبان‌گیری خودکار: `${DATA_DIR}/db/backups/` +- لاگ‌های اختیاری درخواست/مترجم: `/logs/...` وقتی `ENABLE_REQUEST_LOGS=true` +- هر دو `${DATA_DIR}` و `~/.9router` در یک کانتینر داکر به یک مکان اشاره می‌کنند — symlink `/root/.9router -> /app/data` در زمان ساخت ایجاد می‌شود. + +
+ +--- + +## 📊 مدل‌های موجود + +
+مشاهده همه مدل‌های موجود + +**Claude Code (`cc/`)** - Pro/Max: + +- `cc/claude-opus-4-7` +- `cc/claude-opus-4-6` +- `cc/claude-sonnet-4-6` +- `cc/claude-sonnet-4-5-20250929` +- `cc/claude-haiku-4-5-20251001` + +**Codex (`cx/`)** - Plus/Pro: + +- `cx/gpt-5.5` +- `cx/gpt-5.4` +- `cx/gpt-5.3-codex` +- `cx/gpt-5.2-codex` +- `cx/gpt-5.1-codex-max` + +**GitHub Copilot (`gh/`)**: + +- `gh/gpt-5.4` +- `gh/claude-opus-4.7` +- `gh/claude-sonnet-4.6` +- `gh/gemini-3.1-pro-preview` +- `gh/grok-code-fast-1` + +**Cursor (`cu/`)** - اشتراک: + +- `cu/claude-4.6-opus-max` +- `cu/claude-4.5-sonnet-thinking` +- `cu/gpt-5.3-codex` +- `cu/kimi-k2.5` + +**GLM (`glm/`)** - ۰.۶ دلار/میلیون: + +- `glm/glm-5.1` +- `glm/glm-5` +- `glm/glm-4.7` + +**MiniMax (`minimax/`)** - ۰.۲ دلار/میلیون: + +- `minimax/MiniMax-M2.7` +- `minimax/MiniMax-M2.5` + +**Kimi (`kimi/`)** - ۹ دلار/ماه مسطح: + +- `kimi/kimi-k2.5` +- `kimi/kimi-k2.5-thinking` + +**Kiro (`kr/`)** - رایگان نامحدود: + +- `kr/claude-sonnet-4.5` +- `kr/claude-haiku-4.5` +- `kr/glm-5` +- `kr/MiniMax-M2.5` +- `kr/qwen3-coder-next` +- `kr/deepseek-3.2` + +**OpenCode Free (`oc/`)** - رایگان بدون احراز هویت: + +- دریافت خودکار از `opencode.ai/zen/v1/models` + +**Vertex AI (`vertex/`)** - ۳۰۰ دلار اعتبار رایگان: + +- `vertex/gemini-3.1-pro-preview` +- `vertex/gemini-3-flash-preview` +- `vertex/gemini-2.5-flash` +- `vertex-partner/glm-5-maas` +- `vertex-partner/deepseek-v3.2-maas` + +
+ +--- + +## 🐛 عیب‌یابی + +**"مدل زبان پیامی ارائه نکرد"** + +- سهمیه ارائه‌دهنده تمام شده → پیگیری سهمیه در داشبورد را بررسی کنید +- راه‌حل: از بازگشت ترکیبی استفاده کنید یا به لایه ارزان‌تر تغییر دهید + +**محدودیت نرخ درخواست** + +- سهمیه اشتراک تمام شده → بازگشت به GLM/MiniMax +- ترکیب اضافه کنید: `cc/claude-opus-4-7 → glm/glm-5.1 → kr/claude-sonnet-4.5` + +**توکن OAuth منقضی شده است** + +- توسط 9Router به‌طور خودکار بازسازی می‌شود +- اگر مشکل ادامه داشت: داشبورد → ارائه‌دهنده → اتصال مجدد + +**هزینه‌های بالا** + +- RTK را در داشبورد → تنظیمات نقطه پایانی فعال کنید (پیش‌فرض روشن است، ۲۰-۴۰٪ توکن صرفه‌جویی می‌کند) +- آمار مصرف را در داشبورد بررسی کنید +- مدل اصلی را به GLM/MiniMax تغییر دهید +- برای کارهای غیر حیاتی از لایه رایگان (Kiro، OpenCode Free، Vertex) استفاده کنید + +**داشبورد در پورت اشتباه باز می‌شود** + +- `PORT=20128` و `NEXT_PUBLIC_BASE_URL=http://localhost:20128` را تنظیم کنید + +**اولین ورود کار نمی‌کند** + +- `INITIAL_PASSWORD` را در `.env` بررسی کنید +- در صورت تنظیم نشدن، رمز عبور پیش‌فرض `123456` است + +**لاگ‌های درخواست در `logs/` وجود ندارد** + +- `ENABLE_REQUEST_LOGS=true` را تنظیم کنید + +--- + +## 🛠️ پشته فنی + +- **زمان اجرا**: Node.js 20+ +- **فریم‌ورک**: Next.js 16 +- **UI**: React 19 + Tailwind CSS 4 +- **پایگاه داده**: SQLite (better-sqlite3 / node:sqlite / بازگشت sql.js) +- **پخش جریانی**: رویدادهای ارسال شده از سرور (SSE) +- **احراز هویت**: OAuth 2.0 (PKCE) + JWT + کلیدهای API + +--- + +## 📝 مرجع API + +### تکمیل‌های چت + +```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 +} +``` + +### لیست مدل‌ها + +```bash +GET http://localhost:20128/v1/models +Authorization: Bearer your-api-key + +→ همه مدل‌ها + ترکیب‌ها را در قالب OpenAI برمی‌گرداند +``` + +## 📧 پشتیبانی + +- **وب‌سایت**: [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/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors) + +--- + +## 📊 نمودار ستاره + +[![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) + +## 🔀 فورک‌ها + +**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — یک فورک کامل TypeScript از 9Router. بیش از ۳۶ ارائه‌دهنده، بازگشت خودکار ۴ لایه، APIهای چندوجهی (تصاویر، جاسازی‌ها، صدا، TTS)، قطع‌کننده مدار، حافظه پنهان معنایی، ارزیابی‌های LLM و داشبوردی زیبا اضافه می‌کند. بیش از ۳۶۸ تست واحد. از طریق npm و داکر در دسترس است. + +--- + +## 🙏 قدردانی + +ساخته شده بر روی شانه‌های غول‌ها: + +- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — پیاده‌سازی اصلی Go که الهام‌بخش این پورت جاوااسکریپت بود. +- **[RTK](https://github.com/rtk-ai/rtk)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — ذخیره‌ساز توکن Rust. 9Router خط لوله فشرده‌سازی آن را به JS منتقل می‌کند → **۲۰-۴۰٪- توکن ورودی** در هر درخواست. +- **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) توسط **[@JuliusBrussee](https://github.com/JuliusBrussee)** — پرامپت ویروسی _"چرا از توکن زیاد استفاده کنی وقتی توکن کم کار را انجام می‌دهد"_. 9Router پرامپت آن را تطبیق می‌دهد → **۶۵٪- توکن خروجی**. +- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) توسط **[@DietrichGebert](https://github.com/DietrichGebert)** — مهارت _"توسعه‌دهنده ارشد تنبل"_. 9Router نردبان YAGNI-first آن را تزریق می‌کند → **توکن کمتر، کد کمتر، دیف‌های کوتاه‌تر**. + +تشکر فراوان از این نویسندگان — بدون کار آنها، ویژگی‌های ذخیره‌سازی توکن 9Router وجود نداشت. ⭐ آنها را در GitHub بدهید! + +--- + +## 📄 مجوز + +مجوز MIT - برای جزئیات به [LICENSE](LICENSE) مراجعه کنید. + +--- + +
+ ساخته شده با ❤️ برای توسعه‌دهندگانی که ۲۴/۷ کدنویسی می‌کنند +
diff --git a/i18n/README.th.md b/i18n/README.th.md new file mode 100644 index 00000000..cfd8dc4b --- /dev/null +++ b/i18n/README.th.md @@ -0,0 +1,723 @@ +นี่คือเอกสารแปลภาษาไทยของไฟล์ Markdown ต้นฉบับ โดยรักษาโครงสร้างและซินแท็กซ์ทางเทคนิคทั้งหมดไว้เหมือนเดิม + +
+ แดชบอร์ด 9Router + + # 9Router - Free AI Router + + **ไม่ต้องหยุดเขียนโค้ด ประหยัดโทเค็น 20-40% ด้วย RTK + สลับอัตโนมัติไปยังโมเดล AI ฟรีและราคาถูก** + + **ผู้ให้บริการ AI ฟรีสำหรับ 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) + + [🚀 เริ่มต้นใช้งาน](#-quick-start) • [💡 ฟีเจอร์](#-key-features) • [📖 การตั้งค่า](#-setup-guide) • [🌐 เว็บไซต์](https://9router.com) +
+ +--- + +## 🤔 ทำไมต้อง 9Router? + +**หยุดเสียเงินและเจอขีดจำกัด:** + +- ❌ โควตาสมาชิกหมดอายุโดยไม่ได้ใช้ทุกเดือน +- ❌ Rate Limit หยุดคุณระหว่างเขียนโค้ด +- ❌ ค่า API แพง ($20-50/เดือน ต่อผู้ให้บริการแต่ละราย) +- ❌ ต้องสลับผู้ให้บริการด้วยตนเอง + +**9Router แก้ปัญหาเหล่านี้:** + +- ✅ **ประหยัดโทเค็น RTK** - บีบอัดผลลัพธ์จากเครื่องมือ (`git diff`, `grep`, `ls`...) ก่อนส่งให้ LLM +- ✅ **เพิ่มประสิทธิภาพสมาชิก** - ติดตามโควตา ใช้ทุกบิตก่อนรีเซ็ต +- ✅ **สลับอัตโนมัติ** - สมาชิก → ถูก → ฟรี, ไม่มีเวลาหยุดทำงาน +- ✅ **รองรับหลายบัญชี** - Round-robin ระหว่างบัญชีของผู้ให้บริการแต่ละราย +- ✅ **ใช้งานได้ทุกที่** - ใช้ได้กับ Claude Code, Codex, Cursor, Cline, เครื่องมือ CLI ใดก็ได้ + +--- + +## 🔄 วิธีการทำงาน + +``` +┌─────────────┐ +│ Your CLI │ (Claude Code, Codex, OpenClaw, Cursor, Cline...) +│ Tool │ +└──────┬──────┘ + │ http://localhost:20128/v1 + ↓ +┌─────────────────────────────────────────────┐ +│ 9Router (Smart Router) │ +│ • RTK Token Saver (ตัดโทเค็น tool_result) │ +│ • แปลงรูปแบบ (OpenAI ↔ Claude) │ +│ • ติดตามโควตา │ +│ • รีเฟรชโทเค็นอัตโนมัติ │ +└──────┬──────────────────────────────────────┘ + │ + ├─→ [Tier 1: สมาชิก] Claude Code, Codex, GitHub Copilot + │ ↓ โควตาหมด + ├─→ [Tier 2: ถูก] GLM ($0.6/1M), MiniMax ($0.2/1M) + │ ↓ งบหมด + └─→ [Tier 3: ฟรี] Kiro, OpenCode Free, Vertex ($300 เครดิตฟรี) + +ผลลัพธ์: ไม่ต้องหยุดเขียนโค้ด ค่าใช้จ่ายน้อยที่สุด + ประหยัดโทเค็น 20-40% ด้วย RTK +``` + +--- + +## ⚡ เริ่มต้นใช้งาน + +**1. ติดตั้งแบบ Global:** + +```bash +npm install -g 9router +9router +``` + +🎉 เปิดแดชบอร์ดที่ `http://localhost:20128` + +**2. เชื่อมต่อผู้ให้บริการฟรี (ไม่ต้องสมัคร):** + +แดชบอร์ด → Providers → เชื่อมต่อ **Kiro AI** (Claude ฟรีไม่จำกัด) หรือ **OpenCode Free** (ไม่ต้องยืนยันตัวตน) → เสร็จ! + +**3. ใช้ในเครื่องมือ CLI ของคุณ:** + +``` +ตั้งค่า Claude Code/Codex/OpenClaw/Cursor/Cline: + Endpoint: http://localhost:20128/v1 + API Key: [คัดลอกจากแดชบอร์ด] + Model: kr/claude-sonnet-4.5 +``` + +**เสร็จแล้ว!** เริ่มเขียนโค้ดด้วยโมเดล AI ฟรี + +**วิธีอื่น: รันจากซอร์สโค้ด (เก็บรักษาไว้ใน repo นี้):** + +Repo นี้เป็น private package (`9router-app`) ดังนั้นการรันจากซอร์ส/Docker คือเส้นทางพัฒนาท้องถิ่นที่คาดไว้ + +```bash +cp .env.example .env +npm install +PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev +``` + +โหมด Production: + +```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-compatible API: `http://localhost:20128/v1` + +--- + +## 🛠️ เครื่องมือ 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 +
+
+ +### 🆓 ผู้ให้บริการฟรี + +
+ + + + + + +
+ Kiro
+ Kiro AI
+ Claude 4.5 + GLM-5 + MiniMax • ไม่จำกัด ฟรี +
+ OpenCode
+ OpenCode Free
+ ไม่ต้องยืนยันตัวตน • ดึงโมเดลอัตโนมัติ • ไม่จำกัด ฟรี +
+ Vertex AI
+ Vertex AI
+ Gemini 3 Pro + GLM-5 + DeepSeek • เครดิตฟรี $300 +
+
+ +> **หมายเหตุ:** iFlow, Qwen และ Gemini CLI หยุดให้บริการในปี 2026 แล้ว ใช้ Kiro / OpenCode Free / Vertex แทน + +### 🔑 ผู้ให้บริการ 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 compatible endpoints แบบกำหนดเอง

+
+ +--- + +## 💡 ฟีเจอร์หลัก + +| ฟีเจอร์ | ทำอะไร | ทำไมถึงสำคัญ | +|---------|--------------|----------------| +| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | บีบอัดผลลัพธ์จากเครื่องมือ (`git diff`, `grep`, `ls`, `tree`...) ก่อนส่งให้ LLM | ประหยัด **โทเค็น input 20-40%** ต่อคำขอ | +| 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | พร็อกซี `/v1/compress` ภายนอกก่อนเลือกผู้ให้บริการ | ประหยัดโทเค็นบริบทมากขึ้นโดยไม่ต้องเปลี่ยน client | +| 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | ฉีด caveman-speak prompt → LLM ตอบสั้นกระชับ เนื้อหาทางเทคนิคยังครบถ้วน | ประหยัด **โทเค็น output สูงสุด 65%** | +| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | ฉีด prompt "lazy senior dev" → LLM เขียนโค้ดน้อยที่สุด YAGNI-first (Lite/Full/Ultra) | **โทเค็น output น้อยลง, ไม่ต้อง refactor มาก** | +| 🎯 **Smart 3-Tier Fallback** | เลือกเส้นทางอัตโนมัติ: สมาชิก → ถูก → ฟรี | ไม่ต้องหยุดเขียนโค้ด, ไม่มีเวลาหยุดทำงาน | +| 📊 **ติดตามโควตาแบบ Real-Time** | นับโทเค็นแบบ live + นับถอยหลังรีเซ็ต | เพิ่มประสิทธิภาพมูลค่าสมาชิก | +| 🔄 **แปลงรูปแบบ** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | ใช้ได้กับเครื่องมือ CLI ทุกประเภท | +| 👥 **รองรับหลายบัญชี** | หลายบัญชีต่อผู้ให้บริการ | Load balancing + สำรองข้อมูล | +| 🔄 **รีเฟรชโทเค็นอัตโนมัติ** | OAuth token รีเฟรชอัตโนมัติ | ไม่ต้องล็อกอินซ้ำด้วยตนเอง | +| 🎨 **Combo กำหนดเอง** | สร้างการผสมผสานโมเดลไม่จำกัด | ปรับแต่ง fallback ตามความต้องการ | +| 📝 **บันทึก Request** | โหมด debug พร้อม log request/response ครบถ้วน | แก้ไขปัญหาได้ง่าย | +| 💾 **ซิงค์คลาวด์** | ซิงค์การตั้งค่าระหว่างอุปกรณ์ | การตั้งค่าเดียวกันทุกที่ | +| 📊 **วิเคราะห์การใช้งาน** | ติดตามโทเค็น, ค่าใช้จ่าย, แนวโน้มตามเวลา | ปรับแต่งค่าใช้จ่าย | +| 🌐 **Deploy ได้ทุกที่** | Localhost, VPS, Docker, Cloudflare Workers | ตัวเลือก deploy ที่ยืดหยุ่น | + +
+📖 รายละเอียดฟีเจอร์ + +### 🚀 RTK Token Saver + +ผลลัพธ์จากเครื่องมือ (`git diff`, `grep`, `find`, `ls`, `tree`, log dumps...) มักกินงบประมาณ prompt 30-50% RTK ตรวจสอบและบีบอัดอย่างชาญฉลาดแบบ lossless **ก่อน**คำขอถึง LLM: + +- **ตัวกรอง:** `git-diff`, `git-status`, `grep`, `find`, `ls`, `tree`, `dedup-log`, `smart-truncate`, `read-numbered`, `search-list` +- **ตรวจจับอัตโนมัติ:** ไม่ต้องตั้งค่า — RTK .peek 1KB แรกของแต่ละ `tool_result` และเลือกตัวกรองที่ถูกต้อง +- **ปลอดภัยโดยการออกแบบ:** ถ้าตัวกรองล้มเหลว, ขว้าง error, หรือทำให้ผลลัพธ์ใหญ่ขึ้น RTK จะเก็บข้อความต้นฉบับไว้โดยเงียบๆ ไม่มี error ทำให้คำขอของคุณล้มเหลว +- **ใช้ได้ทุกที่:** ใช้ได้กับทุกรูปแบบ (OpenAI, Claude, Gemini, Cursor, Kiro, OpenAI Responses) เพราะทำงาน **ก่อน**การแปลงรูปแบบใดๆ +- **เปิดใช้งานเป็นค่าเริ่มต้น:** ปิด/เปิดได้ตลอดเวลาใน แดชบอร์ด → ตั้งค่า Endpoint + +``` +ไม่ใช้ RTK: ส่ง 47K โทเค็นให้ LLM +ใช้ RTK: ส่ง 28K โทเค็นให้ LLM (ประหยัด 40% · บริบทเดียวกัน · คำตอบเดียวกัน) +``` + +### 🧠 Headroom Token Saver + +Headroom เป็นตัวเลือกและทำงานแยกกัน 9Router เรียก endpoint `/v1/compress` ของ Headroom จากนั้นยังคงเลือกเส้นทาง, fallback, auth และติดตามการใช้งานตามปกติ: + +``` +Client → 9Router → Headroom /v1/compress → 9Router → provider +``` + +ตั้งค่าท้องถิ่น: + +```bash +pip install "headroom-ai[proxy]" +headroom proxy --port 8787 +``` + +เปิดใช้งานใน แดชบอร์ด → Endpoint → Token Saver → Headroom URL ค่าเริ่มต้น: `http://localhost:8787` + +ตัวอย่าง Docker: + +```bash +# Headroom service ใน Docker network เดียวกัน +http://host.docker.internal:8787 +``` + +ถ้า Headroom ดับหรือคืน error, 9Router จะ fail open และส่งคำขอต้นฉบับ + +### 🐴 Ponytail (Lazy Senior Dev) + +Ponytail ฉีด prompt *"lazy senior dev"* เข้าไปในทุกคำขอ ทำให้ LLM เขียนโค้ดน้อยที่สุดแบบ YAGNI-first — ลบมากกว่าเพิ่ม, stdlib มากกว่า dep ใหม่, one-liner มากกว่า abstraction + +- **Lite** — สร้างตามที่ขอ, บอกชื่อทางเลือกที่ lazy กว่า +- **Full** — บังคับ YAGNI ladder: stdlib → native → existing deps → one-liner → minimal code +- **Ultra** — YAGNI extremist: ลบก่อน, ส่ง one-liner, ตั้งคำถามกับ requirement ที่เหลือในคำตอบเดียวกัน + +``` +ไม่ใช้ Ponytail: โค้ดเยอะ, abstraction เยอะ, "เผื่อไว้" scaffolding +ใช้ Ponytail: diff สั้นที่สุดที่ทำงานได้, ไม่เพิ่ม abstraction ที่ไม่ได้ขอ, โทเค็นน้อยลง +``` + +ไม่มีวันแลก: input validation, error handling ที่ป้องกัน data loss, security, accessibility หรือสิ่งที่ขอมาอย่างชัดเจน เปิดใช้งานใน แดชบอร์ด → Endpoint → Ponytail ใช้คู่กับ Caveman (ความกระชับ output) และ RTK (การบีบอัด input) ได้ + +### 🎯 Smart 3-Tier Fallback + +สร้าง combo พร้อม fallback อัตโนมัติ: + +``` +Combo: "my-coding-stack" + 1. cc/claude-opus-4-6 (สมาชิกของคุณ) + 2. glm/glm-4.7 (สำรองราคาถูก, $0.6/1M) + 3. if/kimi-k2-thinking (fallback ฟรี) + +→ สลับอัตโนมัติเมื่อโควตาหมดหรือเกิด error +``` + +### 📊 ติดตามโควตาแบบ Real-Time + +- การใช้โทเค็นต่อผู้ให้บริการ +- นับถอยหลังรีเซ็ต (5 ชั่วโมง, รายวัน, รายสัปดาห์) +- ประมาณการค่าใช้จ่ายสำหรับชั้นแบบเสียค่าใช้จ่าย +- รายงานค่าใช้จ่ายรายเดือน + +### 🔄 แปลงรูปแบบ + +แปลงรูปแบบได้อย่างราบรื่น: +- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses** +- เครื่องมือ CLI ของคุณส่งรูปแบบ OpenAI → 9Router แปลง → ผู้ให้บริการได้รับรูปแบบต้นฉบับ +- ใช้ได้กับเครื่องมือใดก็ได้ที่รองรับ custom OpenAI endpoints + +### 👥 รองรับหลายบัญชี + +- เพิ่มหลายบัญชีสำหรับผู้ให้บริการแต่ละราย +- เลือกเส้นทาง round-robin หรือตามลำดับความสำคัญอัตโนมัติ +- Fallback ไปยังบัญชีถัดไปเมื่อบัญชีหนึ่งชนโควตา + +### 🔄 รีเฟรชโทเค็นอัตโนมัติ + +- OAuth token รีเฟรชอัตโนมัติก่อนหมดอายุ +- ไม่ต้องยืนยันตัวตนใหม่ด้วยตนเอง +- ประสบการณ์ที่ราบรื่นบนผู้ให้บริการทุกราย + +### 🎨 Combo กำหนดเอง + +- สร้างการผสมผสานโมเดลไม่จำกัด +- ผสมชั้นสมาชิก, ราคาถูกและฟรี +- ตั้งชื่อ combo เพื่อเข้าถึงง่าย +- แชร์ combo ระหว่างอุปกรณ์ด้วยการซิงค์คลาวด์ + +### 📝 บันทึก Request + +- เปิดโหมด debug เพื่อดู log request/response ครบถ้วน +- ติดตาม API calls, headers และ payloads +- แก้ไขปัญหาการเชื่อมต่อ +- Export log เพื่อวิเคราะห์ + +### 💾 ซิงค์คลาวด์ + +- ซิงค์ผู้ให้บริการ, combo และการตั้งค่าระหว่างอุปกรณ์ +- ซิงค์เบื้องหลังอัตโนมัติ +- จัดเก็บข้อมูลแบบเข้ารหัสปลอดภัย +- เข้าถึงการตั้งค่าของคุณจากทุกที่ + +### 📊 วิเคราะห์การใช้งาน + +- ติดตามการใช้โทเค็นตามผู้ให้บริการและโมเดล +- ประมาณการค่าใช้จ่ายและแนวโน้มค่าใช้จ่าย +- รายงานและข้อมูลเชิงลึกรายเดือน +- ปรับแต่งค่าใช้จ่าย AI ของคุณ + +### 🌐 Deploy ได้ทุกที่ + +- 💻 **Localhost** - ค่าเริ่มต้น, ทำงานออฟไลน์ +- ☁️ **VPS/Cloud** - แชร์ระหว่างอุปกรณ์ +- 🐳 **Docker** - Deploy ด้วยคำสั่งเดียว +- 🚀 **Cloudflare Workers** - เครือข่าย edge ทั่วโลก + +
+ +--- + +## 💰 สรุปราคา + +| ประเภท | ผู้ให้บริการ | ค่าใช้จ่าย | รีเซ็ตโควตา | ดีที่สุดสำหรับ | +|------|----------|------|-------------|----------| +| **💳 สมาชิก** | Claude Code (Pro) | $20/เดือน | 5 ชม. + รายสัปดาห์ | มีสมาชิกอยู่แล้ว | +| | Codex (Plus/Pro) | $20-200/เดือน | 5 ชม. + รายสัปดาห์ | ผู้ใช้ OpenAI | +| | GitHub Copilot | $10-19/เดือน | รายเดือน | ผู้ใช้ GitHub | +| **💰 ราคาถูก** | GLM-4.7 | $0.6/1M | ทุกวัน 10:00 AM | สำรองงบ | +| | MiniMax M2.1 | $0.2/1M | 5 ชั่วโมง | ถูกที่สุด | +| | Kimi K2 | $9/เดือน คงที่ | 10M โทเค็น/เดือน | ค่าใช้จ่ายที่คาดเดาได้ | +| **🆓 ฟรี** | Kiro | $0 | ไม่จำกัด | Claude ฟรี | +| | OpenCode Free | $0 | ไม่จำกัด | ไม่ต้องยืนยันตัวตน | +| | Vertex AI | $0 | $300 เครดิตฟรี | Gemini 3 Pro | + +**💡 เคล็ดลับ:** เริ่มจาก combo Kiro (Claude ฟรีไม่จำกัด) + OpenCode Free (ไม่ต้องยืนยันตัวตน) = ค่าใช้จ่าย $0! + +--- + +## 🎯 กรณีการใช้งาน + +### กรณีที่ 1: "ฉันมีสมาชิก Claude Pro" + +**ปัญหา:** โควตาหมดอายุโดยไม่ได้ใช้, Rate Limit ตอนเขียนโค้ดหนัก + +**วิธีแก้:** +``` +Combo: "maximize-claude" + 1. cc/claude-opus-4-6 (ใช้สมาชิกเต็มที่) + 2. glm/glm-4.7 (สำรองราคาถูกเมื่อโควตาหมด) + 3. kr/claude-sonnet-4.5 (fallback ฉุกเฉินฟรี) + +ค่าใช้จ่ายรายเดือน: $20 (สมาชิก) + ~$5 (สำรอง) = $25 รวม +เทียบกับ $20 + ชนโควตา = ผิดหวัง +``` + +### กรณีที่ 2: "ฉันต้องการค่าใช้จ่ายเป็นศูนย์" + +**ปัญหา:** ไม่มีงบจ่ายสมาชิก, ต้องการ AI เขียนโค้ดที่เชื่อถือได้ + +**วิธีแก้:** +``` +Combo: "free-forever" + 1. kr/claude-sonnet-4.5 (Claude ฟรีไม่จำกัด) + 2. oc/* (OpenCode Free ไม่ต้องยืนยันตัวตน) + 3. vertex/gemini-3.1-pro-preview (Vertex $300 เครดิตฟรี) + +ค่าใช้จ่ายรายเดือน: $0 +คุณภาพ: โมเดลพร้อมใช้งาน production +``` + +### กรณีที่ 3: "ฉันต้องเขียนโค้ด 24/7 ไม่มีสะดุด" + +**ปัญหา:** Deadline, ไม่สามารถหยุดทำงานได้ + +**วิธีแก้:** +``` +Combo: "always-on" + 1. cc/claude-opus-4-6 (คุณภาพดีที่สุด) + 2. cx/gpt-5.5 (สมาชิกที่สอง) + 3. glm/glm-5.1 (ราคาถูก, รีเซ็ตทุกวัน) + 4. minimax/MiniMax-M2.7 (ถูกที่สุด, รีเซ็ต 5 ชม.) + 5. kr/claude-sonnet-4.5 (ฟรีไม่จำกัด) + +ผลลัพธ์: 5 ชั้น fallback = ไม่มีเวลาหยุดทำงาน +ค่าใช้จ่ายเดือน: $20-200 (สมาชิก) + $10-20 (สำรอง) +``` + +### กรณีที่ 4: "ฉันต้องการ AI ฟรีใน OpenClaw" + +**ปัญหา:** ต้องการ AI assistant ในแอปพลิเคชันแชท (WhatsApp, Telegram, Slack...), ฟรีทั้งหมด + +**วิธีแก้:** +``` +Combo: "openclaw-free" + 1. kr/claude-sonnet-4.5 (Claude ฟรีไม่จำกัด) + 2. kr/glm-5 (GLM ฟรีไม่จำกัด) + 3. kr/MiniMax-M2.5 (MiniMax ฟรีไม่จำกัด) + +ค่าใช้จ่ายรายเดือน: $0 +เข้าถึงผ่าน: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... +``` + +--- + +## ❓ คำถามที่พบบ่อย + +
+💳 9Router เก็บเงินฉันหรือไม่? + +**ไม่.** 9Router เป็นซอฟต์แวร์ฟรีแบบ open source ที่ทำงานบนเครื่องของคุณเอง มันไม่มีวันเรียกเก็บเงินจากคุณ + +**คุณจ่ายเงินเฉพาะ:** +- ✅ **ผู้ให้บริการสมาชิก** (Claude Code $20/เดือน, Codex $20-200/เดือน) → จ่ายตรงให้พวกเขาบนเว็บไซต์ของพวกเขา +- ✅ **ผู้ให้บริการราคาถูก** (GLM, MiniMax) → จ่ายตรงให้พวกเขา, 9Router แค่เลือกเส้นทางคำขอของคุณ +- ❌ **ตัว 9Router เอง** → **ไม่มีวันเรียกเก็บเงินใดๆ ทั้งสิ้น** + +9Router เป็น proxy/router ท้องถิ่น มันไม่มีบัตรเครดิตของคุณ, ไม่สามารถส่งใบแจ้งหนี้ได้ และไม่มีระบบชำระเงิน เป็นซอฟต์แวร์ฟรีทั้งหมด + +
+ +
+🆓 ผู้ให้บริการฟรีไม่จำกัดจริงหรือ? + +**จริง!** ผู้ให้บริการที่ระบุว่าฟรี (Kiro, OpenCode Free, Vertex) ไม่จำกัดจริงๆ **ไม่มีค่าใช้จ่ายแอบแฝง** + +นี่คือบริการฟรีที่บริษัทต่างๆ ให้บริการ: +- **Kiro**: Claude ฟรีไม่จำกัดผ่าน AWS Builder ID +- **OpenCode Free**: ไม่ต้องยืนยันตัวตน, ดึงโมเดลอัตโนมัติ +- **Vertex AI**: $300 เครดิตฟรีสำหรับ Gemini 3 Pro + +9Router แค่เลือกเส้นทางคำขอของคุณไปหาพวกเขา — ไม่มี "กับดัก" หรือการเรียกเก็บเงินในอนาคต เป็นบริการที่ฟรีจริงๆ และ 9Router ทำให้ใช้งานง่ายด้วยการรองรับ fallback + +
+ +
+💰 ทำอย่างไรเพื่อลดค่าใช้จ่าย AI จริงของฉัน? + +**กลยุทธ์ Free First:** + +1. **เริ่มจาก combo ฟรี 100%:** + ``` + 1. kr/claude-sonnet-4.5 (Claude ฟรีไม่จำกัด) + 2. oc/* (OpenCode Free ไม่ต้องยืนยันตัวตน) + 3. vertex/gemini-3.1-pro-preview ($300 เครดิตฟรี) + ``` + **ค่าใช้จ่าย: $0/เดือน** + +2. **เพิ่มสำรองราคาถูก** เมื่อจำเป็นเท่านั้น: + ``` + 4. glm/glm-5.1 ($0.6/1M โทเค็น) + ``` + **ค่าใช้จ่ายเพิ่มเติม:** จ่ายเฉพาะที่ใช้ + +3. **ใช้ผู้ให้บริการสมาชิก** ก็ต่อเมื่อมีอยู่แล้ว: + - 9Router ช่วยเพิ่มประสิทธิภาพมูลค่าของพวกเขาผ่านการติดตามโควตา + +**ผลลัพธ์:** ผู้ใช้ส่วนใหญ่สามารถทำงานที่ $0/เดือน โดยใช้เฉพาะชั้นฟรี! + +
+ +--- + +## 🐛 การแก้ไขปัญหา + +**"Language model did not provide messages"** +- โควตาผู้ให้บริการหมด → ตรวจสอบตัวติดตามโควตาในแดชบอร์ด +- วิธีแก้: ใช้ combo fallback หรือสลับไปชั้นที่ถูกกว่า + +**Rate Limiting** +- สมาชิกหมดโควตา → Fallback ไป GLM/MiniMax +- เพิ่ม combo: `cc/claude-opus-4-6 → glm/glm-5.1 → kr/claude-sonnet-4.5` + +**OAuth Token หมดอายุ** +- รีเฟรชอัตโนมัติโดย 9Router +- ถ้าปัญหายังคงอยู่: แดชบอร์ด → ผู้ให้บริการ → เชื่อมต่อใหม่ + +**ค่าใช้จ่ายสูง** +- เปิดใช้ RTK ใน แดชบอร์ด → ตั้งค่า Endpoint (เปิดเป็นค่าเริ่มต้น, ประหยัด 20-40% โทเค็น) +- ตรวจสอบสถิติการใช้งานในแดชบอร์ด +- สลับโมเดลหลักไป GLM/MiniMax +- ใช้ชั้นฟรี (Kiro, OpenCode Free, Vertex) สำหรับงานที่ไม่สำคัญ + +**แดชบอร์ดเปิดผิดพอร์ต** +- ตั้ง `PORT=20128` และ `NEXT_PUBLIC_BASE_URL=http://localhost:20128` + +**ล็อกอินครั้งแรกไม่ทำงาน** +- ตรวจสอบ `INITIAL_PASSWORD` ใน `.env` +- ถ้ายังไม่ตั้งค่า รหัสผ่านสำรองคือ `123456` + +**ไม่มี request log ใต้ `logs/`** +- ตั้ง `ENABLE_REQUEST_LOGS=true` + +--- + +## 🛠️ Tech Stack + +- **Runtime**: Node.js 20+ +- **Framework**: Next.js 16 +- **UI**: React 19 + Tailwind CSS 4 +- **Database**: SQLite (better-sqlite3 / node:sqlite / sql.js fallback) +- **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": "เขียนฟังก์ชันเพื่อ..."} + ], + "stream": true +} +``` + +### List Models + +```bash +GET http://localhost:20128/v1/models +Authorization: Bearer your-api-key + +→ คืนค่าโมเดลทั้งหมด + combo ในรูปแบบ OpenAI +``` + +--- + +## 📧 สนับสนุน + +- **เว็บไซต์**: [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) + +--- + +## 👥 ผู้มีส่วนร่วม + +ขอขอบคุณผู้มีส่วนร่วมทุกคนที่ช่วยทำให้ 9Router ดียิ่งขึ้น! + +[![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1)](https://github.com/decolua/9router/graphs/contributors) + +--- + +## 📄 ลิขสิทธิ์ + +MIT License - ดู [LICENSE](../LICENSE) สำหรับรายละเอียด + +--- + +
+ สร้างด้วย ❤️ สำหรับนักพัฒนาที่เขียนโค้ด 24/7 +
diff --git a/next.config.mjs b/next.config.mjs index 91f55af9..d017c157 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -30,6 +30,8 @@ const nextConfig = { proxyClientMaxBodySize, // Cache fetch responses across HMR refreshes for faster dev reloads. serverComponentsHmrCache: true, + // Tree-shake heavy barrel imports to cut compile + bundle size + optimizePackageImports: ["@xyflow/react", "@dnd-kit/core", "@dnd-kit/sortable", "material-symbols", "marked"], }, webpack: (config, { isServer }) => { // Ignore fs/path modules in browser bundle diff --git a/open-sse/config/appConstants.js b/open-sse/config/appConstants.js index 6ac9d324..cc46697c 100644 --- a/open-sse/config/appConstants.js +++ b/open-sse/config/appConstants.js @@ -1,5 +1,6 @@ import { platform, arch } from "os"; import { PROVIDERS, PROVIDER_OAUTH } from "./providers.js"; +import { ANTIGRAVITY_IDE_USER_AGENT } from "../providers/shared.js"; // === Gemini CLI === derive từ registry gemini-cli.transport export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion; @@ -59,7 +60,7 @@ export function getPlatformEnum() { } export function getPlatformUserAgent() { - return `antigravity/1.104.0 ${platform()}/${arch()}`; + return ANTIGRAVITY_IDE_USER_AGENT; } export const CLIENT_METADATA = { @@ -129,7 +130,7 @@ export const AG_DEFAULT_TOOLS = new Set([ // Antigravity chat/stream headers export const ANTIGRAVITY_HEADERS = { - "User-Agent": `antigravity/1.107.0 ${platform()}/${arch()}` + "User-Agent": ANTIGRAVITY_IDE_USER_AGENT }; // Cloud Code Assist API diff --git a/open-sse/config/grokCli.js b/open-sse/config/grokCli.js new file mode 100644 index 00000000..f2e024e4 --- /dev/null +++ b/open-sse/config/grokCli.js @@ -0,0 +1,10 @@ +export const GROK_CLI_VERSION = "0.2.99"; +export const GROK_CLI_MODEL = "grok-build"; +export const GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1"; +export const GROK_CLI_CLIENT_IDENTIFIER = "grok-shell"; +export const GROK_CLI_USER_AGENT = `grok-shell/${GROK_CLI_VERSION} (linux; x86_64)`; + +export function supportsGrokCliReasoningEffort(model) { + // ponytail: unknown models omit effort until live metadata reaches dispatch. + return /^grok-4\.5(?:$|-)/.test(String(model || "")); +} diff --git a/open-sse/config/kiroConstants.js b/open-sse/config/kiroConstants.js index 3ff6acbb..93b8f456 100644 --- a/open-sse/config/kiroConstants.js +++ b/open-sse/config/kiroConstants.js @@ -131,6 +131,50 @@ export function resolveKiroThinkingBudget(body, headers, model) { return null; } +export function extractKiroEffortLevel(body) { + const effort = + body?.output_config?.effort ?? + body?.reasoning_effort ?? + (typeof body?.reasoning === "object" ? body.reasoning?.effort : null); + if (typeof effort !== "string") return null; + const normalized = effort.toLowerCase(); + if (normalized === "none" || normalized === "off" || normalized === "disabled") return null; + if (normalized === "xhigh" || normalized === "max") return "high"; + if (["low", "medium", "high"].includes(normalized)) return normalized; + return null; +} + +export function buildKiroAdditionalModelRequestFields(body) { + const effort = extractKiroEffortLevel(body); + if (!effort) return undefined; + // Mirrors Kiro CLI/KAS buildEffortRequestFields("output_config"). + return { + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort }, + }; +} + +export function supportsKiroAdditionalModelRequestFields(model) { + if (typeof model !== "string") return false; + const normalized = model.toLowerCase().replace(/-/g, "."); + if (!normalized.includes("claude")) return false; + const match = normalized.match(/(?:^|[/.])claude(?:[/.][a-z]+)*[/.](\d+)(?:[/.](\d+))?(?:[/.]|$)/); + if (!match) return false; + const [, majorText, minorText] = match; + const major = Number(majorText); + const minor = minorText === undefined ? null : Number(minorText); + const dateSuffixMinor = minor !== null && minor >= 1000; + // Kiro rejected additionalModelRequestFields on legacy 4.5 models in live smoke. + // Default future Claude/Kiro models to supported so new model releases do not + // need a code allowlist update. + return !(major < 4 || (major === 4 && (minor === null || minor <= 5 || dateSuffixMinor))); +} + +export function buildKiroAdditionalModelRequestFieldsForModel(body, model) { + if (!supportsKiroAdditionalModelRequestFields(model)) return undefined; + return buildKiroAdditionalModelRequestFields(body); +} + /** * Detect whether an inbound request is asking for reasoning / thinking output. * Thin wrapper over resolveKiroThinkingBudget (single source of truth). diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index c4cfa413..108806e8 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -2,7 +2,7 @@ import { PROVIDERS } from "./providers.js"; import REGISTRY from "../providers/registry/index.js"; // PROVIDER_MODELS now built from providers/registry (transport + models co-located) import { PROVIDER_MODELS } from "../providers/index.js"; -import { modelQuotaFamily, modelStrip, modelTargetFormat } from "../providers/models/schema.js"; +import { modelQuotaFamily, modelStrip, modelTargetFormat, normalizeModelId } from "../providers/models/schema.js"; import { CODEX_REVIEW_SUFFIX } from "../providers/models/helpers.js"; export { PROVIDER_MODELS }; @@ -18,46 +18,69 @@ export function getDefaultModel(aliasOrId) { return models?.[0]?.id || null; } +// Providers whose registry uses dots in version numbers (e.g. "claude-sonnet-4.5"). +// For these, we tolerate clients sending dashes ("claude-sonnet-4-5") by normalizing +// digit-hyphen-digit to digit-dot-digit before lookup. Other providers are left untouched. +const DOT_VERSION_PROVIDERS = new Set(["kr", "kiro"]); + +// Find a registry entry by id. For Kiro models, tolerates dash/dot version separators +// ("claude-sonnet-4-5" ~= "claude-sonnet-4.5"). Other providers use exact match only. +function findModel(models, modelId, aliasOrId) { + if (!models) return undefined; + const found = models.find(m => m.id === modelId); + if (found) return found; + if (!DOT_VERSION_PROVIDERS.has(aliasOrId)) return undefined; + const normalized = normalizeModelId(modelId); + if (normalized === modelId) return undefined; + return models.find(m => m.id === normalized); +} + 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); + return !!findModel(models, modelId, aliasOrId); } export function findModelName(aliasOrId, modelId) { const models = PROVIDER_MODELS[aliasOrId]; if (!models) return modelId; - const found = models.find(m => m.id === modelId); + const found = findModel(models, modelId, aliasOrId); return found?.name || modelId; } export function getModelTargetFormat(aliasOrId, modelId) { const models = PROVIDER_MODELS[aliasOrId]; if (!models) return null; - return modelTargetFormat(models.find(m => m.id === modelId)); + return modelTargetFormat(findModel(models, modelId, aliasOrId)); } export function getModelType(aliasOrId, modelId) { const models = PROVIDER_MODELS[aliasOrId]; if (!models) return null; - const found = models.find(m => m.id === modelId); + const found = findModel(models, modelId, aliasOrId); return found?.kind || found?.type || null; } export function getModelUpstreamId(aliasOrId, modelId) { + // Split off thinking suffix "(level)" so lookup hits the base id; re-append it to + // the result so downstream applyThinking still sees the suffix (body.model is stripped separately). + const sufMatch = typeof modelId === "string" ? modelId.match(/\([^()]+\)\s*$/) : null; + const suffix = sufMatch ? sufMatch[0] : ""; + const baseId = suffix ? modelId.slice(0, sufMatch.index).trim() : modelId; const models = PROVIDER_MODELS[aliasOrId]; - const found = models?.find(m => m.id === modelId); - if (found?.upstreamModelId) return found.upstreamModelId; - if (aliasOrId === "cx" && typeof modelId === "string" && modelId.endsWith(CODEX_REVIEW_SUFFIX)) { - return modelId.slice(0, -CODEX_REVIEW_SUFFIX.length); + const found = findModel(models, baseId, aliasOrId); + if (found?.upstreamModelId) return found.upstreamModelId + suffix; + if (found?.id) return found.id + suffix; + if (aliasOrId === "cx" && typeof baseId === "string" && baseId.endsWith(CODEX_REVIEW_SUFFIX)) { + return baseId.slice(0, -CODEX_REVIEW_SUFFIX.length) + suffix; } - return modelId; + return baseId + suffix; } export function getModelQuotaFamily(aliasOrId, modelId) { const models = PROVIDER_MODELS[aliasOrId]; - return modelQuotaFamily(models?.find(m => m.id === modelId)); + return modelQuotaFamily(findModel(models, modelId, aliasOrId)); } // OAuth short aliases — derived from registry `alias` (single source). everything else: alias = id. @@ -79,5 +102,5 @@ export function getModelsByProviderId(providerId) { // Get strip list for a model entry (explicit opt-in only) // Returns array of content types to strip, e.g. ["image", "audio"] export function getModelStrip(alias, modelId) { - return modelStrip(PROVIDER_MODELS[alias]?.find(m => m.id === modelId)); + return modelStrip(findModel(PROVIDER_MODELS[alias], modelId, alias)); } diff --git a/open-sse/config/runtimeConfig.js b/open-sse/config/runtimeConfig.js index de199233..a27d3ea9 100644 --- a/open-sse/config/runtimeConfig.js +++ b/open-sse/config/runtimeConfig.js @@ -39,6 +39,15 @@ function envMs(name, def) { return Number.isFinite(n) && n > 0 ? n : def; } +function envUrl(name, def) { + const raw = process.env[name]?.trim(); + return raw || def; +} + +// SearXNG endpoint used by the unauthenticated web-search provider. +// Configure this for a separate Docker service or remote SearXNG instance. +export const SEARXNG_URL = envUrl("SEARXNG_URL", "http://localhost:8888/search"); + // Inter-chunk stall timeout (once tokens are flowing). Generous headroom so // slow reasoning models aren't aborted mid-stream. Env: STREAM_STALL_TIMEOUT_MS. export const STREAM_STALL_TIMEOUT_MS = envMs("STREAM_STALL_TIMEOUT_MS", 360 * 1000); @@ -56,6 +65,8 @@ export const GEMINI_NATIVE_TTS_FETCH_TIMEOUT_MS = envMs("GEMINI_NATIVE_TTS_FETCH export const DEFAULT_MAX_TOKENS = 64000; export const DEFAULT_MIN_TOKENS = 32000; +export const TOKEN_SAVER_HEADER = "x-9router-token-saver"; + // Retry config for 429 responses (legacy - kept for backward compatibility) export const RETRY_CONFIG = { maxAttempts: 2, diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index 5249a3ec..f669e3a7 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -1,7 +1,7 @@ import crypto from "crypto"; import { BaseExecutor } from "./base.js"; import { PROVIDERS } from "../config/providers.js"; -import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAULT_TOOLS, AG_TOOL_SUFFIX } from "../config/appConstants.js"; +import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, AG_DEFAULT_TOOLS, AG_TOOL_SUFFIX } from "../config/appConstants.js"; import { HTTP_STATUS } from "../config/runtimeConfig.js"; import { resolveSessionId } from "../utils/sessionManager.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; @@ -18,7 +18,8 @@ function sanitizeFunctionName(name) { const MAX_RETRY_AFTER_MS = 10000; const ANTIGRAVITY_TRANSIENT_RETRY_MAX_MS = 15000; -const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384; +const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 64000; +const ANTIGRAVITY_IDE_REQUEST_ID_RE = /^agent\/[^/]+\/\d+\/[^/]+\/\d+$/; const ANTIGRAVITY_TRANSIENT_ERROR_PATTERNS = [ /high\s+traffic/i, @@ -87,6 +88,27 @@ function parseImageConfig(model) { return config; } +function uuidFromSeed(seed) { + const bytes = crypto.createHash("sha256").update(String(seed || "antigravity")).digest().subarray(0, 16); + bytes[6] = (bytes[6] & 0x0f) | 0x50; + 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)}`; +} + +function buildIdeRequestId({ body, request, credentials, model, requestType }) { + if (ANTIGRAVITY_IDE_REQUEST_ID_RE.test(body?.requestId || "")) { + return body.requestId; + } + + const sessionId = request?.sessionId || body?.request?.sessionId || credentials?._clientSessionId || credentials?.connectionId || credentials?.email || "anonymous"; + const conversationId = uuidFromSeed(`antigravity:conversation:${sessionId}`); + const trajectoryId = uuidFromSeed(`antigravity:trajectory:${sessionId}:${model}:${requestType}`); + const contentCount = Array.isArray(request?.contents) ? request.contents.length : 1; + const step = Math.max(1, contentCount * 2 - 1); + return `agent/${conversationId}/${Date.now()}/${trajectoryId}/${step}`; +} + export class AntigravityExecutor extends BaseExecutor { constructor() { super("antigravity", PROVIDERS.antigravity); @@ -104,14 +126,10 @@ export class AntigravityExecutor extends BaseExecutor { // sessionId comes from transformRequest output; base.execute runs transformRequest before // buildHeaders, so we read it from instance state cached there (fallback: explicit arg). buildHeaders(credentials, stream = true, sessionId = null) { - const sid = sessionId || this._lastSessionId; 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, - ...(sid && { "X-Machine-Session-Id": sid }), - "Accept": stream ? "text/event-stream" : "application/json" }; } @@ -142,25 +160,26 @@ export class AntigravityExecutor extends BaseExecutor { }); this._lastSessionId = sessionId; + const request = { + contents, + generationConfig: { + temperature: 1.0, + topP: 0.95, + topK: 40, + maxOutputTokens: 8192, + imageConfig, + }, + sessionId, + // No tools, no systemInstruction, no safetySettings for image gen + }; return { project: projectId, model: cleanModel, userAgent: "antigravity", requestType: "image_gen", - requestId: `agent-${crypto.randomUUID()}`, - request: { - contents, - generationConfig: { - temperature: 1.0, - topP: 0.95, - topK: 40, - maxOutputTokens: 8192, - imageConfig, - }, - sessionId, - // No tools, no systemInstruction, no safetySettings for image gen - }, + requestId: buildIdeRequestId({ body, request, credentials, model: cleanModel, requestType: "image_gen" }), + request, }; } @@ -248,7 +267,7 @@ export class AntigravityExecutor extends BaseExecutor { model: model, userAgent: "antigravity", requestType: "agent", - requestId: `agent-${crypto.randomUUID()}`, + requestId: buildIdeRequestId({ body, request: transformedRequest, credentials, model, requestType: "agent" }), request: transformedRequest }; } diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js index e19b0787..3b04445f 100644 --- a/open-sse/executors/codex.js +++ b/open-sse/executors/codex.js @@ -8,13 +8,21 @@ import { import { normalizeResponsesInput } from "../translator/formats/responsesApi.js"; import { fetchImageAsBase64 } from "../translator/concerns/image.js"; import { getModelUpstreamId } from "../config/providerModels.js"; -import { DEFAULT_RETRY_CONFIG, resolveRetryEntry } from "../config/runtimeConfig.js"; +import { DEFAULT_RETRY_CONFIG, HTTP_STATUS, resolveRetryEntry } from "../config/runtimeConfig.js"; import { dbg } from "../utils/debugLog.js"; import { resolveSessionId } from "../utils/sessionManager.js"; -// SSE error patterns inside 200-OK body that should trigger retry as if 503 -const CODEX_SSE_OVERLOADED_PATTERNS = ["server_is_overloaded", "service_unavailable_error"]; -const CODEX_SSE_PEEK_BYTES = 4096; +// SSE error patterns inside 200-OK bodies. Some retry same account first; capacity rotates accounts. +const CODEX_SSE_RETRY_PATTERNS = ["server_is_overloaded", "service_unavailable_error"]; +const CODEX_SSE_ACCOUNT_FALLBACK_PATTERNS = ["selected model is at capacity", "model_at_capacity"]; +const CODEX_SSE_USER_OUTPUT_PATTERNS = [ + "event: response.output_text.delta", + "event: response.function_call_arguments.delta", + '"type":"response.output_text.delta"', + '"type":"response.function_call_arguments.delta"', +]; +const CODEX_SSE_PEEK_BYTES = 256 * 1024; +const CODEX_MODEL_CAPACITY_MESSAGE = "Selected model is at capacity. Please try a different model."; // Server-generated item id prefixes that Codex /responses cannot resolve when store=false const SERVER_ID_PATTERN = /^(rs|fc|resp|msg)_/; @@ -116,6 +124,62 @@ function resolveCacheSessionId(body, credentials) { }); } +function normalizeReasoningEffort(value) { + return value === "max" ? "xhigh" : value; +} + +function findNestedMessage(value, depth = 0) { + if (!value || depth > 6 || typeof value === "string") return null; + if (Array.isArray(value)) { + for (const item of value) { + const found = findNestedMessage(item, depth + 1); + if (found) return found; + } + return null; + } + if (typeof value !== "object") return null; + if (typeof value.message === "string" && value.message.trim()) return value.message; + if (typeof value.error?.message === "string" && value.error.message.trim()) return value.error.message; + if (typeof value.response?.error?.message === "string" && value.response.error.message.trim()) return value.response.error.message; + for (const child of Object.values(value)) { + const found = findNestedMessage(child, depth + 1); + if (found) return found; + } + return null; +} + +function extractSseErrorMessage(text, fallback) { + const exact = text?.match(/Selected model is at capacity\. Please try a different model\./i)?.[0]; + if (exact) return exact; + + for (const line of String(text || "").split(/\r?\n/)) { + if (!line.startsWith("data:")) continue; + const data = line.slice(5).trim(); + if (!data || data === "[DONE]") continue; + try { + const message = findNestedMessage(JSON.parse(data)); + if (message) return message; + } catch { + // Ignore non-JSON SSE data lines. + } + } + + return fallback || CODEX_MODEL_CAPACITY_MESSAGE; +} + +function codexSseErrorResponse(status, message) { + return new Response(JSON.stringify({ + error: { + message, + type: status >= 500 ? "server_error" : "invalid_request_error", + code: status === HTTP_STATUS.SERVICE_UNAVAILABLE ? "service_unavailable" : "upstream_error", + } + }), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + /** * Codex Executor - handles OpenAI Codex API (Responses API format) * Automatically injects default instructions if missing @@ -135,10 +199,17 @@ export class CodexExecutor extends BaseExecutor { headers["session_id"] = this._currentSessionId || credentials?.connectionId || "default"; // Identify client type to Codex backend (matches official codex CLI) if (!headers["originator"]) headers["originator"] = "codex_cli_rs"; - // Workspace binding header — improves account scope + cache affinity - const workspaceId = credentials?.providerSpecificData?.workspaceId; - if (typeof workspaceId === "string" && workspaceId && !headers["chatgpt-account-id"]) { - headers["chatgpt-account-id"] = workspaceId; + // Account/workspace binding header — required when multiple Codex accounts + // are configured. OAuth import stores ChatGPT account ID as chatgptAccountId; + // older/custom rows may use workspaceId/accountId. Prefer explicit workspaceId + // but fall back to chatgptAccountId so requests don't cross-bind to the wrong + // OpenAI account and surface as token_invalid after adding another account. + const accountId = + credentials?.providerSpecificData?.workspaceId || + credentials?.providerSpecificData?.chatgptAccountId || + credentials?.providerSpecificData?.accountId; + if (typeof accountId === "string" && accountId && !headers["ChatGPT-Account-ID"]) { + headers["ChatGPT-Account-ID"] = accountId; } return headers; } @@ -198,7 +269,7 @@ export class CodexExecutor extends BaseExecutor { let attempt = 0; while (true) { const result = await super.execute(args); - const peek = await this._peekSseOverloaded(result.response); + const peek = await this._peekSseTransientError(result.response); if (!peek.matched) { // Replace body with re-assembled stream (prefix bytes already read + rest) if (peek.replacementBody) { @@ -210,48 +281,57 @@ export class CodexExecutor extends BaseExecutor { } return result; } + if (peek.accountFallback) { + args.log?.warn?.("RETRY", `CODEX | SSE account fallback "${peek.message}"`); + result.response = codexSseErrorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, peek.message || CODEX_MODEL_CAPACITY_MESSAGE); + return result; + } if (attempt >= attempts) { args.log?.warn?.("RETRY", `CODEX | SSE overloaded "${peek.matched}" — retries exhausted (${attempt}/${attempts})`); - // Out of retries → return with replacement body so client gets the error - if (peek.replacementBody) { - result.response = new Response(peek.replacementBody, { - status: result.response.status, - statusText: result.response.statusText, - headers: result.response.headers, - }); - } + result.response = codexSseErrorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, peek.message || peek.matched); return result; } attempt++; args.log?.debug?.("RETRY", `CODEX | SSE "${peek.matched}" retry ${attempt}/${attempts} after ${delayMs / 1000}s`); dbg("CODEX", `SSE overloaded "${peek.matched}" → retry ${attempt}/${attempts} in ${delayMs}ms`); - try { await result.response.body?.cancel?.(); } catch { /* noop */ } await new Promise(r => setTimeout(r, delayMs)); } } - // Peek first N bytes of SSE body to detect upstream "overloaded" errors. - // Returns { matched: string|null, replacementBody: ReadableStream|null }. - // Caller MUST use replacementBody (original body has been read). - async _peekSseOverloaded(response) { - if (!response || !response.ok || !response.body) return { matched: null, replacementBody: null }; + // Peek first N bytes of SSE body to detect upstream transient errors. + // Returns { matched: string|null, message: string|null, accountFallback: boolean, replacementBody: ReadableStream|null }. + // Caller must use replacementBody when no error matched (original body has been read). + async _peekSseTransientError(response) { + if (!response || !response.ok || !response.body) return { matched: null, message: null, accountFallback: false, replacementBody: null }; const reader = response.body.getReader(); const decoder = new TextDecoder(); const chunks = []; let text = ""; let matched = null; + let accountFallback = false; try { while (text.length < CODEX_SSE_PEEK_BYTES) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); text += decoder.decode(value, { stream: true }); - const hit = CODEX_SSE_OVERLOADED_PATTERNS.find(p => text.includes(p)); - if (hit) { matched = hit; break; } + const lowerText = text.toLowerCase(); + const accountHit = CODEX_SSE_ACCOUNT_FALLBACK_PATTERNS.find(p => lowerText.includes(p)); + if (accountHit) { matched = accountHit; accountFallback = true; break; } + const retryHit = CODEX_SSE_RETRY_PATTERNS.find(p => lowerText.includes(p)); + if (retryHit) { matched = retryHit; break; } + if (CODEX_SSE_USER_OUTPUT_PATTERNS.some(p => lowerText.includes(p))) break; } } catch (e) { dbg("CODEX", `peek read error: ${e.message}`); } + + if (matched) { + try { await reader.cancel(); } catch { /* noop */ } + try { reader.releaseLock(); } catch { /* noop */ } + return { matched, message: extractSseErrorMessage(text, matched), accountFallback, replacementBody: null }; + } + reader.releaseLock(); // Re-assemble stream: prefix chunks + remaining upstream body @@ -273,7 +353,7 @@ export class CodexExecutor extends BaseExecutor { try { upstreamReader?.cancel(reason); } catch { /* noop */ } }, }); - return { matched, replacementBody }; + return { matched: null, message: null, accountFallback: false, replacementBody }; } // Parse Codex usage_limit_reached to extract precise resetsAtMs; fallback to default otherwise @@ -347,7 +427,7 @@ export class CodexExecutor extends BaseExecutor { // 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']; + const effortLevels = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']; let modelEffort = null; for (const level of effortLevels) { if (body.model.endsWith(`-${level}`)) { @@ -360,10 +440,11 @@ export class CodexExecutor extends BaseExecutor { // Priority: explicit reasoning.effort > reasoning_effort param > model suffix > default (medium) if (!body.reasoning) { - const effort = body.reasoning_effort || modelEffort || 'low'; + const effort = normalizeReasoningEffort(body.reasoning_effort || modelEffort || 'low'); body.reasoning = { effort, summary: "auto" }; - } else if (!body.reasoning.summary) { - body.reasoning.summary = "auto"; + } else { + body.reasoning.effort = normalizeReasoningEffort(body.reasoning.effort); + if (!body.reasoning.summary) body.reasoning.summary = "auto"; } delete body.reasoning_effort; @@ -391,6 +472,9 @@ export class CodexExecutor extends BaseExecutor { delete body.safety_identifier; // Droid CLI sends this but Codex doesn't support it delete body.previous_response_id; // store=false → backend can't resolve previous resp; avoid 404 + if (body.service_tier === "fast") body.service_tier = "priority"; + if (body.service_tier && body.service_tier !== "priority") delete body.service_tier; + // Final allowlist filter — strip any unknown field that could trigger upstream "routing_unsupported" for (const k of Object.keys(body)) { if (!RESPONSES_API_ALLOWLIST.has(k)) delete body[k]; diff --git a/open-sse/executors/github.js b/open-sse/executors/github.js index 2f4d68ba..208ff8f2 100644 --- a/open-sse/executors/github.js +++ b/open-sse/executors/github.js @@ -4,11 +4,13 @@ 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 { initState, translateRequest, translateResponse } from "../translator/index.js"; +import { FORMATS } from "../translator/formats.js"; import { parseSSELine, formatSSE } from "../utils/streamHelpers.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js"; import { SSE_DONE } from "../utils/sseConstants.js"; +import { ANTHROPIC_API_VERSION } from "../providers/shared.js"; import crypto from "crypto"; export class GithubExecutor extends BaseExecutor { @@ -17,6 +19,16 @@ export class GithubExecutor extends BaseExecutor { this.knownCodexModels = new Set(); } + // Claude models get routed to Copilot's Anthropic-native /v1/messages shim (see + // executeWithMessagesEndpoint below) — the only Copilot endpoint that surfaces + // prompt-cache token counts. gpt/gemini/grok models stay on /chat/completions + // (or /responses). Name-pattern check, not a registry field: Copilot's live model + // catalog (services/copilotModels.js) regularly exposes claude-* variants ahead + // of the static registry (registry/github.js). + isClaudeModel(model) { + return /claude/i.test(model || ""); + } + buildUrl(model, stream, urlIndex = 0) { return this.config.baseUrl; } @@ -35,47 +47,20 @@ export class GithubExecutor extends BaseExecutor { "x-request-id": crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`, "x-vscode-user-agent-library-version": "electron-fetch", "X-Initiator": "user", + // Harmless no-op on /chat/completions and /responses; required by /v1/messages. + "anthropic-version": ANTHROPIC_API_VERSION, "Accept": stream ? "text/event-stream" : "application/json" }; } - // Sanitize messages for GitHub Copilot /chat/completions endpoint. + // Sanitize messages for GitHub Copilot /chat/completions endpoint (gpt/gemini/grok models — + // claude models never reach this, see execute() below). // 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; @@ -138,6 +123,15 @@ export class GithubExecutor extends BaseExecutor { async execute(options) { const { model, log } = options; + // Claude models: route to Copilot's Anthropic-native /v1/messages shim — the only + // Copilot endpoint that surfaces prompt-cache token counts for Claude. Detected by + // model NAME (not a registry field): Copilot's live model catalog regularly exposes + // claude-* variants the static registry hasn't caught up with yet (see registry/github.js). + if (this.isClaudeModel(model)) { + log?.debug("GITHUB", `Using /v1/messages route for ${model}`); + return this.executeWithMessagesEndpoint(options); + } + // Only use /responses for models that are explicitly known to need it (e.g. gpt codex models) // and that the /responses endpoint actually serves (excludes Gemini/Claude, see #1062). if (this.knownCodexModels.has(model) && this.supportsResponsesEndpoint(model)) { @@ -145,8 +139,8 @@ export class GithubExecutor extends BaseExecutor { 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 + // Sanitize messages before sending to /chat/completions (gpt/gemini/grok — the + // endpoint rejects non-text/image_url content parts). const sanitizedOptions = { ...options, body: this.sanitizeMessagesForChatCompletions(options.body) @@ -251,6 +245,101 @@ export class GithubExecutor extends BaseExecutor { }; } + // Claude models arrive here OpenAI-shape (chatCore.js targets "openai" for github — + // see the note in execute() above), so we translate to Anthropic-native ourselves. + // This is what makes prepareClaudeRequest() (translator/formats/claude.js) inject + // cache_control — /chat/completions never gets there, so it never sees cache tokens. + async executeWithMessagesEndpoint({ model, body, stream, credentials, signal, log, proxyOptions = null }) { + const url = this.config.messagesUrl; + const headers = this.buildHeaders(credentials, stream); + + // Force stream:true upstream regardless of client preference, same as + // executeWithResponsesEndpoint below — chatCore.js's non-streaming handler already + // knows how to buffer an SSE response into a single JSON reply when the client + // asked for stream:false. + const transformedBody = translateRequest(FORMATS.OPENAI, FORMATS.CLAUDE, model, body, true, credentials, "github"); + // _toolNameMap is internal bookkeeping (see openai-to-claude.js) — chatCore.js + // normally strips it before dispatch and threads it into the response state to + // restore original tool names; we must do the same here, or Anthropic's strict + // schema rejects the extra field with a 400. + const toolNameMap = transformedBody._toolNameMap; + delete transformedBody._toolNameMap; + + log?.debug("GITHUB", "Sending translated request to /v1/messages"); + + 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(FORMATS.CLAUDE); + state.model = model; + if (toolNameMap) state.toolNameMap = toolNameMap; + + const decoder = new TextDecoder(); + let buffer = ""; + + const emitAll = (controller, chunks) => { + for (const c of chunks) { + controller.enqueue(new TextEncoder().encode(formatSSE(c, "openai"))); + } + }; + + 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(SSE_DONE)); + continue; + } + + emitAll(controller, translateResponse(FORMATS.CLAUDE, FORMATS.OPENAI, parsed, state)); + } + }, + flush(controller) { + if (buffer.trim()) { + const parsed = parseSSELine(buffer.trim()); + if (parsed && !parsed.done) { + emitAll(controller, translateResponse(FORMATS.CLAUDE, FORMATS.OPENAI, parsed, state)); + } + } + } + }); + + 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, proxyOptions = null) { try { const response = await proxyAwareFetch("https://api.github.com/copilot_internal/v2/token", { diff --git a/open-sse/executors/grok-cli.js b/open-sse/executors/grok-cli.js new file mode 100644 index 00000000..59baa2e2 --- /dev/null +++ b/open-sse/executors/grok-cli.js @@ -0,0 +1,552 @@ +import crypto from "node:crypto"; +import { BaseExecutor } from "./base.js"; +import { PROVIDERS } from "../config/providers.js"; +import { + refreshProviderCredentials, + shouldRefreshCredentials, +} from "../services/oauthCredentialManager.js"; +import { normalizeResponsesInput } from "../translator/formats/responsesApi.js"; +import { getModelUpstreamId } from "../config/providerModels.js"; +import { + GROK_CLI_CLIENT_IDENTIFIER, + GROK_CLI_VERSION, + supportsGrokCliReasoningEffort, +} from "../config/grokCli.js"; +import { MEMORY_CONFIG } from "../config/runtimeConfig.js"; +import { resolveSessionId } from "../utils/sessionManager.js"; +import { getConsistentMachineId } from "../shared/machineId.js"; + +// Server-generated item id prefixes that /responses cannot resolve when store=false +const SERVER_ID_PATTERN = /^(rs|fc|resp|msg)_/; + +// Hosted tool types executed server-side by Grok CLI backend +const HOSTED_TOOL_TYPES = new Set([ + "web_search", + "x_search", + "web_search_preview", + "file_search", + "image_generation", + "code_interpreter", + "mcp", + "local_shell", +]); + +// Fields accepted by cli-chat-proxy Responses API (mirrors Codex allowlist + Grok extras) +const RESPONSES_API_ALLOWLIST = new Set([ + "model", + "input", + "instructions", + "tools", + "tool_choice", + "stream", + "store", + "reasoning", + "include", + "temperature", + "top_p", + "max_output_tokens", + "parallel_tool_calls", + "text", + "metadata", + "prompt_cache_key", +]); + +const EFFORT_LEVELS = ["low", "medium", "high", "xhigh"]; +const GROK_CLI_TURN_STORE_MAX = 5000; +const GROK_CLI_NATIVE_ITEM_ID = /^(?:rs|msg|fc)_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const GROK_CLI_FREEFORM_TOOL_PARAMETERS = { + type: "object", + properties: { input: { type: "string" } }, + required: ["input"], +}; + +// Per-session last turn index so multi-turn headers never go backwards within this process +const sessionTurnStore = new Map(); +let requestTurnStore = new WeakMap(); + +/** + * Count user turns in a Responses `input` array. + * Official CLI sets x-grok-turn-idx to the 1-based conversation turn (≈ user messages). + * HAR: first chat turn → "1". + */ +export function countGrokCliUserTurns(input) { + if (!Array.isArray(input)) return 1; + let n = 0; + for (const item of input) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const type = typeof item.type === "string" ? item.type : ""; + // Responses message items (type omitted or "message") with role user + if (item.role === "user" && (!type || type === "message")) n += 1; + } + return Math.max(1, n); +} + +/** + * Resolve monotonic turn index for a session. + * Prefers user-message count from the payload (full history clients), but never + * decreases vs the last index observed for the same sessionId in this process. + */ +export function resolveGrokCliTurnIdx(sessionId, input, requestKey = null) { + const fromInput = countGrokCliUserTurns(input); + if (!sessionId) return fromInput; + + if (requestKey && requestTurnStore.has(requestKey)) { + return requestTurnStore.get(requestKey); + } + + const now = Date.now(); + const existing = sessionTurnStore.get(sessionId); + const prev = existing && now - existing.lastUsed <= MEMORY_CONFIG.sessionTtlMs + ? existing.turn + : 0; + if (existing) sessionTurnStore.delete(sessionId); + + // A new delta-style request advances the turn; retries reuse requestKey. + const turn = prev > 0 ? Math.max(fromInput, prev + (requestKey ? 1 : 0)) : fromInput; + while (sessionTurnStore.size >= GROK_CLI_TURN_STORE_MAX) { + sessionTurnStore.delete(sessionTurnStore.keys().next().value); + } + sessionTurnStore.set(sessionId, { turn, lastUsed: now }); + if (requestKey) requestTurnStore.set(requestKey, turn); + return turn; +} + +/** Test helper — clear in-memory turn counters */ +export function _resetGrokCliTurnStore() { + sessionTurnStore.clear(); + requestTurnStore = new WeakMap(); +} + +export function _getGrokCliTurnStoreSize() { + return sessionTurnStore.size; +} + +export function normalizeGrokCliEffort(value) { + const effort = typeof value === "string" ? value.trim().toLowerCase() : ""; + if (effort === "max") return "xhigh"; + if (EFFORT_LEVELS.includes(effort)) return effort; + return "high"; +} + +export { supportsGrokCliReasoningEffort } from "../config/grokCli.js"; + +export function resolveGrokCliSessionId(credentials, body) { + // ponytail: clients without stable thread metadata share one connection session; + // split further when their wire format exposes a durable conversation id. + const explicitSessionBody = { + prompt_cache_key: body?.prompt_cache_key, + session_id: body?.session_id, + conversation_id: body?.conversation_id, + metadata: body?.metadata, + }; + return resolveSessionId({ + headers: credentials?.rawHeaders, + body: explicitSessionBody, + connectionId: credentials?.connectionId || credentials?.id, + workspaceId: credentials?.providerSpecificData?.workspaceId, + scope: "grok-cli", + }); +} + +function stringifyGrokCliToolOutput(output) { + if (typeof output === "string") return output; + if (output === undefined) return ""; + return JSON.stringify(output); +} + +function isNativeGrokCliItemId(id) { + return typeof id === "string" && GROK_CLI_NATIVE_ITEM_ID.test(id); +} + +function normalizeGrokCliInputItem(item) { + if (!item || typeof item !== "object" || Array.isArray(item)) return item; + const { internal_chat_message_metadata_passthrough: _metadata, ...clean } = item; + + if (item.type === "reasoning") { + if (!isNativeGrokCliItemId(item.id) || typeof item.encrypted_content !== "string") return null; + return clean; + } + + if (item.type === "custom_tool_call") { + const callId = item.call_id || item.id; + const name = typeof item.name === "string" ? item.name.trim() : ""; + if (!callId || !name) return null; + return { + type: "function_call", + call_id: callId, + name, + arguments: JSON.stringify({ input: stringifyGrokCliToolOutput(item.input ?? item.arguments) }), + }; + } + + if (item.type === "custom_tool_call_output" || item.type === "function_call_output") { + const callId = item.call_id || item.id; + if (!callId) return null; + return { + type: "function_call_output", + call_id: callId, + output: stringifyGrokCliToolOutput(item.output), + }; + } + + if (item.type === "function_call") { + const callId = item.call_id || item.id; + const name = typeof item.name === "string" ? item.name.trim() : ""; + if (!callId || !name) return null; + return { + type: "function_call", + ...(isNativeGrokCliItemId(item.id) ? { id: item.id } : {}), + call_id: callId, + name, + arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {}), + ...(typeof item.status === "string" ? { status: item.status } : {}), + }; + } + + return clean; +} + +export function normalizeGrokCliInput(body) { + if (!Array.isArray(body?.input)) return body; + const normalized = body.input.map(normalizeGrokCliInputItem).filter(Boolean); + const callIds = new Set( + normalized + .filter((item) => item?.type === "function_call" && item.call_id) + .map((item) => item.call_id) + ); + body.input = normalized.filter( + (item) => item?.type !== "function_call_output" || callIds.has(item.call_id) + ); + return body; +} + +function stripStoredItemReferences(body) { + if (!Array.isArray(body.input)) return; + body.input = body.input.filter((item) => { + if (typeof item === "string" && SERVER_ID_PATTERN.test(item)) return false; + if (item && typeof item === "object" && !Array.isArray(item)) { + if (item.type === "item_reference") return false; + if ( + typeof item.id === "string" && + SERVER_ID_PATTERN.test(item.id) && + !isNativeGrokCliItemId(item.id) + ) delete item.id; + } + return true; + }); +} + +/** + * Flatten Chat Completions tool shape → Responses flat format. + * Keep hosted tools (web_search / x_search) passthrough. + */ +function normalizeGrokCliTools(body) { + if (!Array.isArray(body.tools) || body.tools.length === 0) { + delete body.tools; + delete body.tool_choice; + return; + } + const validNames = new Set(); + const hostedTypes = new Set(); + body.tools = body.tools.filter((tool) => { + if (!tool || typeof tool !== "object" || Array.isArray(tool)) return false; + const type = typeof tool.type === "string" ? tool.type : ""; + + if (type !== "function") { + // Hosted tools: { type: "web_search" } / { type: "x_search" } + if (HOSTED_TOOL_TYPES.has(type)) { + hostedTypes.add(type); + return true; + } + // Nested function shape without type + if (!type && tool.function) { + // fall through to function flatten below + } else if (!type || typeof tool.name === "string") { + // treat as bare function if name present + } else { + return false; + } + } + + const isFunction = + type === "function" || type === "" || tool.function || typeof tool.name === "string"; + if (!isFunction || HOSTED_TOOL_TYPES.has(type)) { + return HOSTED_TOOL_TYPES.has(type); + } + + const fn = + tool.function && typeof tool.function === "object" && !Array.isArray(tool.function) + ? tool.function + : null; + const rawName = + typeof tool.name === "string" ? tool.name : typeof fn?.name === "string" ? fn.name : ""; + const name = rawName.trim(); + if (!name) return false; + + const description = + typeof tool.description === "string" + ? tool.description + : typeof fn?.description === "string" + ? fn.description + : ""; + const parameters = type === "custom" + ? GROK_CLI_FREEFORM_TOOL_PARAMETERS + : tool.parameters && typeof tool.parameters === "object" && !Array.isArray(tool.parameters) + ? tool.parameters + : fn?.parameters && typeof fn.parameters === "object" && !Array.isArray(fn.parameters) + ? fn.parameters + : { type: "object", properties: {} }; + + for (const k of Object.keys(tool)) delete tool[k]; + tool.type = "function"; + tool.name = name.slice(0, 128); + if (description) tool.description = description; + tool.parameters = parameters; + validNames.add(tool.name); + return true; + }); + + if (body.tools.length === 0) { + delete body.tools; + delete body.tool_choice; + return; + } + + if (body.tool_choice && typeof body.tool_choice === "object" && !Array.isArray(body.tool_choice)) { + const choiceType = typeof body.tool_choice.type === "string" ? body.tool_choice.type : ""; + if (choiceType === "function" || choiceType === "custom") { + const rawName = body.tool_choice.name ?? body.tool_choice.function?.name; + const name = typeof rawName === "string" ? rawName.trim().slice(0, 128) : ""; + if (!name || !validNames.has(name)) delete body.tool_choice; + else body.tool_choice = { type: "function", name }; + } else if (!hostedTypes.has(choiceType)) { + delete body.tool_choice; + } + } +} + +function resolveEffortFromModel(modelId) { + if (!modelId || typeof modelId !== "string") return null; + for (const level of EFFORT_LEVELS) { + if (modelId.endsWith(`-${level}`)) return level; + } + return null; +} + +/** + * Grok CLI Executor — OpenAI Responses API on cli-chat-proxy.grok.com + * Auth: OAuth device-code access token (xai-grok-cli). + */ +export class GrokCliExecutor extends BaseExecutor { + constructor() { + super("grok-cli", PROVIDERS["grok-cli"]); + this._currentSessionId = null; + this._currentReqId = null; + this._currentTurnIdx = 1; + this._agentId = null; + } + + buildUrl() { + return this.config.baseUrl; + } + + async refreshCredentials(credentials, log, proxyOptions = null) { + if (!credentials?.refreshToken) return null; + return refreshProviderCredentials("grok-cli", credentials, log, proxyOptions); + } + + needsRefresh(credentials) { + return shouldRefreshCredentials("grok-cli", credentials); + } + + buildHeaders(credentials, stream = true) { + const headers = super.buildHeaders(credentials, stream); + + // Static fingerprint from registry + const staticHeaders = this.config.headers || {}; + for (const [k, v] of Object.entries(staticHeaders)) { + if (v != null && headers[k] === undefined) headers[k] = v; + } + + headers["x-grok-client-identifier"] = + this.config.clientIdentifier || headers["x-grok-client-identifier"] || GROK_CLI_CLIENT_IDENTIFIER; + headers["x-grok-client-version"] = + this.config.clientVersion || headers["x-grok-client-version"] || GROK_CLI_VERSION; + + const sessionId = this._currentSessionId || credentials?.connectionId || crypto.randomUUID(); + const reqId = this._currentReqId || crypto.randomUUID(); + headers["x-grok-session-id"] = sessionId; + // CLI uses the same id for conv + session on chat turns + headers["x-grok-conv-id"] = sessionId; + headers["x-grok-req-id"] = reqId; + headers["x-grok-turn-idx"] = String(this._currentTurnIdx || 1); + + if (this._agentId) headers["x-grok-agent-id"] = this._agentId; + + // Surface model override (CLI always sets this) + if (this._currentModel) headers["x-grok-model-override"] = this._currentModel; + + // Identity: mapTokens stores email top-level AND in providerSpecificData; + // fall back either way so OAuth connections always fingerprint like the CLI. + const psd = credentials?.providerSpecificData || {}; + const email = psd.email || credentials?.email; + const userId = psd.userId || credentials?.userId || credentials?.providerUserId; + if (email) headers["x-email"] = email; + if (userId) headers["x-userid"] = userId; + + return headers; + } + + parseError(response, bodyText) { + // 402 personal-team-blocked:spending-limit → surface as payment/quota for fallback + if (response.status === 402 && bodyText) { + try { + const json = JSON.parse(bodyText); + const code = json?.code || ""; + const msg = json?.error || json?.message || bodyText; + return { + status: 402, + message: typeof msg === "string" ? msg : bodyText, + code: typeof code === "string" ? code : undefined, + }; + } catch { + /* fall through */ + } + } + return super.parseError(response, bodyText); + } + + transformRequest(model, body, stream, credentials) { + // Session / request ids for headers — stable per client conversation when possible + const requestKey = body; + this._currentSessionId = resolveGrokCliSessionId(credentials, body); + this._currentReqId = crypto.randomUUID(); + this._agentId = + credentials?.providerSpecificData?.deviceId || + credentials?.providerSpecificData?.agentId || + null; + + // Normalize Responses input + const normalized = normalizeResponsesInput(body.input); + if (normalized) body.input = normalized; + + // Chat Completions clients arrive with messages[] — translator should have + // converted already, but guard empty input. + if (!body.input || (Array.isArray(body.input) && body.input.length === 0)) { + if (Array.isArray(body.messages) && body.messages.length > 0) { + // Soft fallback: map messages → input messages (string content only) + body.input = body.messages.map((m) => ({ + type: "message", + role: m.role || "user", + content: typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""), + })); + delete body.messages; + } else { + body.input = [{ type: "message", role: "user", content: "..." }]; + } + } + + // Keep role:"system" as-is — official grok-pager HAR sends system, not developer + // (Codex converts system→developer; Grok CLI does not). + normalizeGrokCliInput(body); + stripStoredItemReferences(body); + normalizeGrokCliTools(body); + + // Turn index after input is finalized (user-message count, monotonic per session) + this._currentTurnIdx = resolveGrokCliTurnIdx(this._currentSessionId, body.input, requestKey); + + body.stream = true; + body.store = false; + + // Resolve upstream model id (strip effort suffix virtual models) + let modelEffort = resolveEffortFromModel(body.model || model); + let resolvedModel = body.model || model; + if (modelEffort) { + resolvedModel = resolvedModel.replace(new RegExp(`-${modelEffort}$`), ""); + } + resolvedModel = getModelUpstreamId("gcli", resolvedModel) || resolvedModel; + // Also try provider id key + if (resolvedModel === (body.model || model)) { + resolvedModel = getModelUpstreamId("grok-cli", resolvedModel) || resolvedModel; + } + body.model = resolvedModel; + this._currentModel = resolvedModel; + + // Reasoning effort priority: explicit > reasoning_effort > model suffix > default high. + // grok-build and Composer reject reasoningEffort but still accept summary/encrypted continuity. + const supportsReasoningEffort = supportsGrokCliReasoningEffort(resolvedModel); + if (!body.reasoning || typeof body.reasoning !== "object") { + body.reasoning = { summary: "concise" }; + if (supportsReasoningEffort) { + body.reasoning.effort = normalizeGrokCliEffort(body.reasoning_effort || modelEffort); + } + } else { + if (supportsReasoningEffort) { + body.reasoning.effort = normalizeGrokCliEffort( + body.reasoning.effort || body.reasoning_effort || modelEffort, + ); + } else { + delete body.reasoning.effort; + } + if (!body.reasoning.summary) body.reasoning.summary = "concise"; + } + delete body.reasoning_effort; + + // Encrypted reasoning for multi-turn continuity (CLI always requests this) + if (body.reasoning && body.reasoning.effort !== "none") { + const include = Array.isArray(body.include) ? body.include : []; + if (!include.includes("reasoning.encrypted_content")) { + include.push("reasoning.encrypted_content"); + } + body.include = include; + } + + // Drop Chat Completions leftovers that Responses rejects + delete body.messages; + delete body.max_tokens; + delete body.max_completion_tokens; + delete body.n; + delete body.seed; + delete body.logprobs; + delete body.top_logprobs; + delete body.frequency_penalty; + delete body.presence_penalty; + delete body.logit_bias; + delete body.user; + delete body.stream_options; + delete body.prompt_cache_retention; + delete body.safety_identifier; + delete body.previous_response_id; // store=false → cannot resolve + + for (const k of Object.keys(body)) { + if (!RESPONSES_API_ALLOWLIST.has(k)) delete body[k]; + } + + return body; + } + + async execute(args) { + // Lazy-resolve stable agent id once per process if connection has none + if (!this._agentId && !args.credentials?.providerSpecificData?.deviceId) { + try { + const mid = await getConsistentMachineId("grok-cli-agent"); + // Format as UUID-ish for header aesthetics + this._agentId = [ + mid.slice(0, 8), + mid.slice(8, 12), + "5" + mid.slice(13, 16), + "a" + mid.slice(17, 20), + mid.slice(0, 12).padEnd(12, "0"), + ].join("-"); + } catch { + this._agentId = crypto.randomUUID(); + } + } else if (args.credentials?.providerSpecificData?.deviceId) { + this._agentId = args.credentials.providerSpecificData.deviceId; + } + + return super.execute(args); + } +} + +export default GrokCliExecutor; diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index 52ae29bb..b6091b9d 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -13,6 +13,7 @@ import { QwenExecutor } from "./qwen.js"; import { OpenCodeExecutor } from "./opencode.js"; import { OpenCodeGoExecutor } from "./opencode-go.js"; import { GrokWebExecutor } from "./grok-web.js"; +import { GrokCliExecutor } from "./grok-cli.js"; import { PerplexityWebExecutor } from "./perplexity-web.js"; import { OllamaLocalExecutor } from "./ollama-local.js"; import { CommandCodeExecutor } from "./commandcode.js"; @@ -39,6 +40,9 @@ const executors = { opencode: new OpenCodeExecutor(), "opencode-go": new OpenCodeGoExecutor(), "grok-web": new GrokWebExecutor(), + "grok-cli": new GrokCliExecutor(), + gcli: new GrokCliExecutor(), // Alias + gb: new GrokCliExecutor(), // Alias (Grok Build) "perplexity-web": new PerplexityWebExecutor(), "ollama-local": new OllamaLocalExecutor(), commandcode: new CommandCodeExecutor(), @@ -77,6 +81,7 @@ export { QwenExecutor } from "./qwen.js"; export { OpenCodeExecutor } from "./opencode.js"; export { OpenCodeGoExecutor } from "./opencode-go.js"; export { GrokWebExecutor } from "./grok-web.js"; +export { GrokCliExecutor } from "./grok-cli.js"; export { PerplexityWebExecutor } from "./perplexity-web.js"; export { OllamaLocalExecutor } from "./ollama-local.js"; export { CommandCodeExecutor } from "./commandcode.js"; diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index b5cf8a84..47190acf 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -1,18 +1,19 @@ import { detectFormat, getTargetFormat, resolveTransport } from "../services/provider.js"; import { translateRequest } from "../translator/index.js"; +import { stripThinkingSuffix } from "../translator/concerns/thinkingUnified.js"; import { FORMATS } from "../translator/formats.js"; import { normalizeClaudePassthrough } from "../translator/formats/claude.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, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js"; import { PROVIDERS } from "../config/providers.js"; import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js"; -import { HTTP_STATUS } from "../config/runtimeConfig.js"; +import { HTTP_STATUS, TOKEN_SAVER_HEADER } 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 { supportsGrokCliReasoningEffort } from "../config/grokCli.js"; import { buildRequestDetail, extractRequestConfig } from "./chatCore/requestDetail.js"; import { handleForcedSSEToJson } from "./chatCore/sseToJsonHandler.js"; import { handleNonStreamingResponse } from "./chatCore/nonStreamingHandler.js"; @@ -23,9 +24,12 @@ import { injectCaveman } from "../rtk/caveman.js"; import { injectPonytail } from "../rtk/ponytail.js"; import { compressMessages, formatRtkLog } from "../rtk/index.js"; import { compressWithHeadroom, formatHeadroomLog, formatHeadroomSizeLog, isHeadroomPhantomSavings } from "../rtk/headroom.js"; +import { compressWithPxpipe } from "../rtk/pxpipe.js"; import { getCapabilitiesForModel } from "../providers/capabilities.js"; import { stripUnsupportedModalities } from "../translator/concerns/modality.js"; import { prefetchRemoteImages } from "../translator/concerns/prefetch.js"; +import { extractThinking } from "../translator/concerns/thinkingUnified.js"; +import { resolveSessionId } from "../utils/sessionManager.js"; /** * Core chat handler - shared between SSE and Worker @@ -34,9 +38,18 @@ import { prefetchRemoteImages } from "../translator/concerns/prefetch.js"; * @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, rtkEnabled, headroomEnabled, headroomUrl, headroomCompressUserMessages, cavemanEnabled, cavemanLevel, ponytailEnabled, ponytailLevel, sourceFormatOverride, providerThinking }) { +export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, rtkEnabled, headroomEnabled, headroomUrl, headroomCompressUserMessages, cavemanEnabled, cavemanLevel, ponytailEnabled, ponytailLevel, pxpipeEnabled, pxpipeMinChars, pxpipeTimeoutMs, pxpipeTransform, onPxpipeEvent, sourceFormatOverride, providerThinking }) { const { provider, model } = modelInfo; const requestStartTime = Date.now(); + // Stable per-session color so all lines of one CLI conversation share a tag + const sessionSeed = (() => { + try { + return resolveSessionId({ headers: clientRawRequest?.headers, body, connectionId, scope: provider }); + } catch { + return connectionId || ""; + } + })(); + const reqTag = log?.tagForSession ? log.tagForSession(sessionSeed) : (log?.nextTag ? log.nextTag() : ""); const sourceFormat = sourceFormatOverride || detectFormat(body); @@ -123,9 +136,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred let toolNameMap; if (passthrough) { log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`); - translatedBody = { ...body, model: upstreamModel }; + translatedBody = { ...body, model: stripThinkingSuffix(upstreamModel) }; // Normalize newer Cowork/CC beta shapes (adaptive thinking, mid-conversation system) the API rejects - if (clientTool === "claude") normalizeClaudePassthrough(translatedBody, upstreamModel); + if (clientTool === "claude") normalizeClaudePassthrough(translatedBody, translatedBody.model); } else { translatedBody = translateRequest(sourceFormat, targetFormat, upstreamModel, body, stream, credentials, provider, reqLogger, stripList, connectionId, clientTool); if (!translatedBody) { @@ -134,7 +147,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred } toolNameMap = translatedBody._toolNameMap; delete translatedBody._toolNameMap; - translatedBody.model = upstreamModel; + translatedBody.model = stripThinkingSuffix(upstreamModel); } // Dedupe duplicate built-in tools when equivalent MCP tools are present (Claude clients only). @@ -150,41 +163,83 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred // Covers both passthrough (source shape) and translated (target shape) flows const finalFormat = passthrough ? sourceFormat : targetFormat; + // Request line: one correlated summary (fmt + thinking + counts + account) + if (log?.line) { + const clientModel = clientRawRequest?.body?.model || `${provider}/${model}`; + const msgN = translatedBody.messages?.length || translatedBody.input?.length || translatedBody.contents?.length || body.messages?.length || body.input?.length || 0; + const toolN = translatedBody.tools?.length || body.tools?.length || 0; + const fmtStr = passthrough ? `FMT: ${sourceFormat} (passthrough)` : `FMT: ${sourceFormat}→${targetFormat}`; + const showThinking = provider !== "grok-cli" || supportsGrokCliReasoningEffort(model); + const think = showThinking ? log.fmtThink?.(extractThinking(translatedBody)) : null; + const acc = credentials?.connectionName || credentials?.connectionId?.slice(0, 8) || "-"; + const parts = [ + `POST ${clientModel} → ${provider}/${model}`, + fmtStr, + stream ? "STREAM" : "JSON", + `${msgN} MSG`, + ]; + if (toolN) parts.push(`${toolN} TOOL`); + if (think) parts.push(`THINK:${think}`); + parts.push(`ACC:${acc}`); + log.line(reqTag, "▶", parts.join(" · ")); + } + // TTS models don't support tool messages/function calling if (getModelType(alias, model) === "tts" && translatedBody.messages) { translatedBody.messages = translatedBody.messages.filter(msg => msg.role !== "tool"); delete translatedBody.tools; } + // Per-request opt-out: client can bypass all token savers via header + const tokenSaverEnabled = clientRawRequest?.headers?.[TOKEN_SAVER_HEADER]?.toLowerCase() !== "off"; + // RTK: compress tool_result content - const rtkStats = compressMessages(translatedBody, rtkEnabled); + const rtkStats = compressMessages(translatedBody, tokenSaverEnabled && rtkEnabled); const rtkLine = formatRtkLog(rtkStats); if (rtkLine) console.log(rtkLine); // Headroom: optional external proxy compression; fail open if proxy is absent. const headroomDiagnostics = {}; - const headroomStats = await compressWithHeadroom(translatedBody, { enabled: headroomEnabled, url: headroomUrl, model: upstreamModel, format: finalFormat, compressUserMessages: headroomCompressUserMessages, diagnostics: headroomDiagnostics }); + const headroomStats = await compressWithHeadroom(translatedBody, { enabled: tokenSaverEnabled && headroomEnabled, url: headroomUrl, model: upstreamModel, format: finalFormat, compressUserMessages: headroomCompressUserMessages, diagnostics: headroomDiagnostics }); const headroomLine = formatHeadroomLog(headroomStats); const headroomSizeLine = formatHeadroomSizeLog(headroomDiagnostics); if (headroomLine) { log?.info?.("HEADROOM", `${headroomLine}${headroomSizeLine ? ` | ${headroomSizeLine}` : ""}`); if (isHeadroomPhantomSavings(headroomStats, headroomDiagnostics)) { - log?.warn?.("HEADROOM", `reported token delta, but outbound JSON shrank <5%; provider may bill near-original payload | ${headroomSizeLine}`); + log?.warn?.("HEADROOM", `reported token delta, but outbound JSON shrank <5%; provider may bill near-original payload | ${formatHeadroomSizeLog(headroomDiagnostics)}`); } - } else if (headroomEnabled) log?.warn?.("HEADROOM", `skipped: ${headroomDiagnostics.reason || "compression unavailable"}${headroomDiagnostics.endpoint ? ` (${headroomDiagnostics.endpoint})` : ""}`); + } else if (tokenSaverEnabled && headroomEnabled) log?.warn?.("HEADROOM", `skipped: ${headroomDiagnostics.reason || "compression unavailable"}${headroomDiagnostics.endpoint ? ` (${headroomDiagnostics.endpoint})` : ""}`); + + // Token-saver flags accumulator for the single "⚙" log line below. + const xf = []; // Caveman: inject terse-style system prompt - if (cavemanEnabled && cavemanLevel) { + if (tokenSaverEnabled && cavemanEnabled && cavemanLevel) { injectCaveman(translatedBody, finalFormat, cavemanLevel); - log?.debug?.("CAVEMAN", `${cavemanLevel} | ${finalFormat}`); + xf.push(`CAVEMAN:${cavemanLevel}`); } // Ponytail: inject lazy-senior-dev system prompt - if (ponytailEnabled && ponytailLevel) { + if (tokenSaverEnabled && ponytailEnabled && ponytailLevel) { injectPonytail(translatedBody, finalFormat, ponytailLevel); - log?.debug?.("PONYTAIL", `${ponytailLevel} | ${finalFormat}`); + xf.push(`PONYTAIL:${ponytailLevel}`); } + // PXPIPE: image bulky context (Claude-format bodies only), last saver before dispatch + let pxpipeSummary = null; + if (pxpipeEnabled) { + const pxpipeResult = await compressWithPxpipe(translatedBody, { + enabled: true, format: finalFormat, model: upstreamModel, + minChars: pxpipeMinChars, timeoutMs: pxpipeTimeoutMs, transform: pxpipeTransform, + }); + pxpipeSummary = pxpipeResult.summary; + if (pxpipeResult.body) translatedBody = pxpipeResult.body; + if (pxpipeSummary?.applied) xf.push(`PXPIPE:${pxpipeSummary.imageCount}img`); + try { onPxpipeEvent?.({ provider, model, ...pxpipeSummary }); } catch { /* stats must not break requests */ } + } + + if (xf.length && log?.line) log.line(reqTag, "⚙", xf.join(" · ")); + const executor = getExecutor(provider); trackPendingRequest(model, provider, connectionId, true); appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => { }); @@ -198,7 +253,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred if (onDisconnect) onDisconnect(reason); }, onError: () => trackPendingRequest(model, provider, connectionId, false), - log, provider, model + log, provider, model, reqTag }); const proxyOptions = { @@ -253,6 +308,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred request: extractRequestConfig(body, stream), providerRequest: translatedBody || null, response: { error: error.message || String(error), status: error.name === "AbortError" ? 499 : 502, thinking: null }, + pxpipe: pxpipeSummary, status: "error" })).catch(() => { }); @@ -261,7 +317,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred return createErrorResult(499, "Request aborted"); } const errMsg = formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY); - console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`); + if (log?.errorLine) { + log.errorLine(reqTag, "✗", `ERROR 502 · ${provider}/${model} · ${Date.now() - requestStartTime}ms\n ${errMsg}${error.stack ? `\n ${error.stack}` : ""}`); + } return createErrorResult(HTTP_STATUS.BAD_GATEWAY, errMsg); } @@ -270,7 +328,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred try { const newCredentials = await refreshWithRetry(() => executor.refreshCredentials(credentials, log), 3, log); if (newCredentials?.accessToken || newCredentials?.copilotToken) { - log?.info?.("TOKEN", `${provider.toUpperCase()} | refreshed`); + if (log?.line) log.line(reqTag, "🔑", `TOKEN REFRESHED · ${provider}/${model}`); Object.assign(credentials, newCredentials); if (onCredentialsRefreshed) { try { await onCredentialsRefreshed(newCredentials); } catch (e) { log?.warn?.("TOKEN", `onCredentialsRefreshed failed: ${e.message}`); } @@ -299,16 +357,20 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred request: extractRequestConfig(body, stream), providerRequest: finalBody || translatedBody || null, response: { error: message, status: statusCode, thinking: null }, + pxpipe: pxpipeSummary, status: "error" })).catch(() => { }); const errMsg = formatProviderError(new Error(message), provider, model, statusCode); - console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`); + if (log?.errorLine) { + const urlStr = providerUrl ? `\n URL: ${providerUrl}` : ""; + log.errorLine(reqTag, "✗", `ERROR ${statusCode} · ${provider}/${model} · ${Date.now() - requestStartTime}ms${urlStr}\n ${errMsg}`); + } reqLogger.logError(new Error(message), finalBody || translatedBody); return createErrorResult(statusCode, errMsg, resetsAtMs); } - const sharedCtx = { provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess }; + const sharedCtx = { provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, pxpipe: pxpipeSummary, reqTag, log }; const appendLog = (extra) => appendRequestLog({ model, provider, connectionId, ...extra }).catch(() => { }); const trackDone = () => trackPendingRequest(model, provider, connectionId, false); diff --git a/open-sse/handlers/chatCore/nonStreamingHandler.js b/open-sse/handlers/chatCore/nonStreamingHandler.js index 3996f94f..f93058fd 100644 --- a/open-sse/handlers/chatCore/nonStreamingHandler.js +++ b/open-sse/handlers/chatCore/nonStreamingHandler.js @@ -6,7 +6,7 @@ import { addBufferToUsage, filterUsageForFormat } from "../../utils/usageTrackin 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 { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, saveUsageStats, formatDoneLine } from "./requestDetail.js"; import { appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js"; import { decloakToolNames } from "../../utils/claudeCloaking.js"; @@ -198,7 +198,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source /** * 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, toolNameMap, trackDone, appendLog }) { +export async function handleNonStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, trackDone, appendLog, pxpipe, reqTag, log }) { trackDone(); const contentType = providerResponse.headers.get("content-type") || ""; let responseBody; @@ -235,7 +235,8 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m const usage = extractUsageFromResponse(responseBody); appendLog({ tokens: usage, status: "200 OK" }); - saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint }); + saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, silent: true }); + if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency: { total: Date.now() - requestStartTime } })); const translatedResponse = needsTranslation(targetFormat, sourceFormat) ? translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) @@ -296,6 +297,7 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m thinking: translatedResponse?.choices?.[0]?.message?.reasoning_content || translatedResponse?.reasoning_content || null, finish_reason: translatedResponse?.choices?.[0]?.finish_reason || "unknown" }, + pxpipe, status: "success" }, { endpoint: clientRawRequest?.endpoint || null })).catch(err => { console.error("[RequestDetail] Failed to save:", err.message); diff --git a/open-sse/handlers/chatCore/requestDetail.js b/open-sse/handlers/chatCore/requestDetail.js index 451111ad..b3d24115 100644 --- a/open-sse/handlers/chatCore/requestDetail.js +++ b/open-sse/handlers/chatCore/requestDetail.js @@ -69,12 +69,31 @@ export function buildRequestDetail(base, overrides = {}) { providerRequest: base.providerRequest || null, providerResponse: base.providerResponse || null, response: base.response || {}, + pxpipe: base.pxpipe || undefined, status: base.status || "success", ...overrides }; } -export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, endpoint, label = "USAGE" }) { +// Build the "done" summary: duration, ttft, in/out tokens with cache breakdown +export function formatDoneLine({ usage, latency }) { + const u = usage || {}; + const inTok = u.prompt_tokens ?? u.input_tokens ?? 0; + const outTok = u.completion_tokens ?? u.output_tokens ?? 0; + const cacheRead = u.cache_read_input_tokens ?? u.cached_tokens ?? u.prompt_tokens_details?.cached_tokens ?? 0; + const cacheCreate = u.cache_creation_input_tokens ?? 0; + let inStr = `IN ${inTok}`; + if (cacheRead || cacheCreate) { + const parts = []; + if (cacheRead) parts.push(`↻${cacheRead}`); + if (cacheCreate) parts.push(`+${cacheCreate}`); + inStr += ` (CACHE ${parts.join(" ")})`; + } + const ttftStr = latency?.ttft ? ` · TTFT ${latency.ttft}ms` : ""; + return `DONE ${latency?.total ?? 0}ms${ttftStr} · ${inStr} · OUT ${outTok}`; +} + +export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, endpoint, label = "USAGE", silent = false }) { if (!tokens || typeof tokens !== "object") return; const inTokens = tokens.input_tokens ?? tokens.prompt_tokens ?? 0; @@ -82,9 +101,11 @@ export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, 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}`); + if (!silent) { + 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}`); + } // Canonicalize to one storage convention (prompt_tokens cache-inclusive) so // cached/cache-creation tokens survive to cost calc + stats. See canonicalizeUsage. diff --git a/open-sse/handlers/chatCore/sseToJsonHandler.js b/open-sse/handlers/chatCore/sseToJsonHandler.js index 1e0edeba..f25986c5 100644 --- a/open-sse/handlers/chatCore/sseToJsonHandler.js +++ b/open-sse/handlers/chatCore/sseToJsonHandler.js @@ -3,7 +3,7 @@ import { createErrorResult } from "../../utils/error.js"; import { HTTP_STATUS } from "../../config/runtimeConfig.js"; import { FORMATS } from "../../translator/formats.js"; import { PROVIDERS } from "../../config/providers.js"; -import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js"; +import { buildRequestDetail, extractRequestConfig, saveUsageStats, formatDoneLine } from "./requestDetail.js"; // Responses-API providers (e.g. codex) may emit SSE without content-type + use Responses output shape const isResponsesProvider = (p) => PROVIDERS[p]?.format === FORMATS.OPENAI_RESPONSES; @@ -102,7 +102,7 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) { * 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 }) { +export async function handleForcedSSEToJson({ providerResponse, sourceFormat, provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, trackDone, appendLog, reqTag, log }) { const contentType = providerResponse.headers.get("content-type") || ""; const isSSE = contentType.includes("text/event-stream") || (contentType === "" && isResponsesProvider(provider)); if (!isSSE) return null; // not handled here @@ -124,7 +124,8 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr const usage = jsonResponse.usage || {}; appendLog({ tokens: usage, status: "200 OK" }); - saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint }); + saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, silent: true }); + if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency: { total: Date.now() - requestStartTime } })); const { msgItem, textContent } = pickAssistantMessageForChatCompletion(jsonResponse.output); const totalLatency = Date.now() - requestStartTime; @@ -200,7 +201,8 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr const usage = parsed.usage || {}; appendLog({ tokens: usage, status: "200 OK" }); - saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint }); + saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, silent: true }); + if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency: { total: Date.now() - requestStartTime } })); const totalLatency = Date.now() - requestStartTime; saveRequestDetail(buildRequestDetail({ diff --git a/open-sse/handlers/chatCore/streamingHandler.js b/open-sse/handlers/chatCore/streamingHandler.js index fb14bc0b..1fb8a322 100644 --- a/open-sse/handlers/chatCore/streamingHandler.js +++ b/open-sse/handlers/chatCore/streamingHandler.js @@ -5,7 +5,7 @@ import { pipeWithDisconnect } from "../../utils/streamHandler.js"; import { PROVIDERS } from "../../config/providers.js"; import { STREAM_STALL_TIMEOUT_MS } from "../../config/runtimeConfig.js"; import { buildAbortedResponsesTerminalBytes } from "../../utils/responsesStreamHelpers.js"; -import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js"; +import { buildRequestDetail, extractRequestConfig, saveUsageStats, formatDoneLine } from "./requestDetail.js"; import { saveRequestDetail } from "@/lib/usageDb.js"; import { SSE_HEADERS_CORS as SSE_HEADERS } from "../../utils/sseConstants.js"; @@ -43,7 +43,7 @@ function buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, /** * Handle streaming response — pipe provider SSE through transform stream to client. */ -export async function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, streamController, onStreamComplete, streamDetailId }) { +export async function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, streamController, onStreamComplete, streamDetailId, pxpipe, reqTag, log }) { if (onRequestSuccess) { Promise.resolve() .then(onRequestSuccess) @@ -67,7 +67,8 @@ export async function handleStreamingResponse({ providerResponse, provider, mode const shortMsg = sanitizedTitle || (bodyText.length < 200 ? bodyText.replace(/<[^>]*>/g, '').trim().slice(0, 160) : `Upstream returned non-SSE response (${upstreamContentType})`); const status = providerResponse.status || 502; - console.warn(`[STREAM] ${provider} | ${model} | blocked pipe: ${shortMsg} [${status}]`); + if (log?.errorLine) log.errorLine(reqTag, "✗", `BLOCKED ${status} · ${provider}/${model} · non-SSE (${upstreamContentType})\n ${shortMsg}`); + else console.warn(`[STREAM] ${provider} | ${model} | blocked pipe: ${shortMsg} [${status}]`); streamController?.handleError?.(new Error(`upstream non-SSE: ${status}`)); return { success: false, @@ -94,6 +95,7 @@ export async function handleStreamingResponse({ providerResponse, provider, mode providerRequest: finalBody || translatedBody || null, providerResponse: "[Streaming - raw response not captured]", response: { content: "[Streaming in progress...]", thinking: null, type: "streaming" }, + pxpipe, status: "success" }, { id: streamDetailId })).catch(err => { console.error("[RequestDetail] Failed to save streaming request:", err.message); @@ -108,7 +110,7 @@ export async function handleStreamingResponse({ providerResponse, provider, mode /** * Build onStreamComplete callback for streaming usage tracking. */ -export function buildOnStreamComplete({ provider, model, connectionId, apiKey, requestStartTime, body, stream, finalBody, translatedBody, clientRawRequest }) { +export function buildOnStreamComplete({ provider, model, connectionId, apiKey, requestStartTime, body, stream, finalBody, translatedBody, clientRawRequest, pxpipe, reqTag, log }) { const streamDetailId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; const onStreamComplete = (contentObj, usage, ttftAt) => { @@ -127,12 +129,15 @@ export function buildOnStreamComplete({ provider, model, connectionId, apiKey, r providerRequest: finalBody || translatedBody || null, providerResponse: safeContent, response: { content: safeContent, thinking: safeThinking, type: "streaming" }, + pxpipe, 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" }); + // Persist stream usage to DB (no console line; the "📊 done" line below is authoritative) + saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, label: "STREAM USAGE", silent: true }); + if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency })); }; return { onStreamComplete, streamDetailId }; diff --git a/open-sse/handlers/search/chatSearch.js b/open-sse/handlers/search/chatSearch.js index a8a7841e..c5bfb3ad 100644 --- a/open-sse/handlers/search/chatSearch.js +++ b/open-sse/handlers/search/chatSearch.js @@ -273,6 +273,53 @@ const CHAT_SEARCH_CONFIG = { const tokens = data?.usage?.total_tokens || 0; return { text, citations, tokens }; } + }, + + "perplexity-agent": { + endpoint: () => searchEndpoint("perplexity-agent"), + buildBody: (query, model) => ({ + model, + input: query, + tools: [{ type: "web_search" }] + }), + buildHeaders: (token) => ({ + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }), + extractAnswer: (data) => { + const output = Array.isArray(data?.output) ? data.output : []; + let text = ""; + const citations = []; + for (const item of output) { + const parts = Array.isArray(item?.content) ? item.content : []; + for (const p of parts) { + if (typeof p?.text === "string") text += p.text; + const anns = Array.isArray(p?.annotations) ? p.annotations : []; + for (const a of anns) { + const c = normalizeCitation(a?.url ? a : a?.url_citation); + if (c) citations.push(c); + } + } + const results = Array.isArray(item?.results) ? item.results : []; + for (const r of results) { + const url = r?.url || r?.link; + if (!url) continue; + citations.push({ + url, + title: r?.title || "", + snippet: r?.snippet || "" + }); + } + } + if (!citations.length && Array.isArray(data?.citations)) { + for (const c of data.citations) { + const n = normalizeCitation(c); + if (n) citations.push(n); + } + } + const tokens = data?.usage?.total_tokens || 0; + return { text, citations, tokens }; + } } }; diff --git a/open-sse/handlers/videoCore.js b/open-sse/handlers/videoCore.js new file mode 100644 index 00000000..98d60157 --- /dev/null +++ b/open-sse/handlers/videoCore.js @@ -0,0 +1,166 @@ +import { createErrorResult } from "../utils/error.js"; +import { HTTP_STATUS } from "../config/runtimeConfig.js"; +import { refreshTokenByProvider } from "../services/tokenRefresh.js"; +import { PROVIDER_MEDIA } from "../providers/index.js"; + +// Upstream fetch deadline for video job submission/polling (the job itself is +// async upstream — this only bounds the HTTP round-trip, not video rendering). +const VIDEO_FETCH_TIMEOUT_MS = Number(process.env.VIDEO_FETCH_TIMEOUT_MS || 120000); + +// POST /videos/* creates a billable upstream job. A network error after the +// request left the socket may still have created the job, so creation is NEVER +// auto-retried (the only re-send is the auth retry after a 401/403 refresh, +// which upstream rejects before job creation). +export const VIDEO_ACTIONS = new Set(["generations", "edits", "extensions"]); + +export function getVideoConfig(provider) { + return PROVIDER_MEDIA[provider]?.videoConfig || null; +} + +/** Strip bearer tokens / obvious secrets from text destined for clients or logs. */ +export function sanitizeSecrets(text, credentials = null) { + if (!text) return text; + let out = String(text).replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [redacted]"); + for (const key of ["accessToken", "refreshToken", "apiKey"]) { + const secret = credentials?.[key]; + if (typeof secret === "string" && secret.length >= 8) { + out = out.split(secret).join("[redacted]"); + } + } + return out; +} + +function buildUpstreamUrl(config, action, requestId) { + const base = config.baseUrl.replace(/\/$/, ""); + return requestId ? `${base}/${encodeURIComponent(requestId)}` : `${base}/${action}`; +} + +function buildHeaders({ token, contentType, idempotencyKey }) { + const headers = { Accept: "application/json" }; + if (token) headers.Authorization = `Bearer ${token}`; + if (contentType) headers["Content-Type"] = contentType; + if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey; + return headers; +} + +function combineSignals(signal, timeoutMs) { + const timeoutSignal = typeof AbortSignal?.timeout === "function" ? AbortSignal.timeout(timeoutMs) : null; + if (signal && timeoutSignal && typeof AbortSignal.any === "function") { + return AbortSignal.any([signal, timeoutSignal]); + } + return signal || timeoutSignal || undefined; +} + +/** + * Transparent proxy for async video jobs (xAI Grok Imagine shape). + * + * - Forwards the raw body byte-for-byte (JSON or multipart) — no reshaping. + * - Passes upstream JSON (request_id, status, video.url, error) back verbatim. + * - 401/403 with a refresh token: refresh ONCE, retry ONCE. No other retry. + * - Upstream error text is sanitized before it reaches the client. + * + * @param {object} options + * @param {string} options.provider - Provider id (must have registry videoConfig) + * @param {"generations"|"edits"|"extensions"|null} options.action - Creation action (POST) + * @param {string|null} [options.requestId] - Poll target (GET /videos/{id}) + * @param {Buffer|string|null} [options.rawBody] - Exact body to forward + * @param {string|null} [options.contentType] - Original Content-Type header + * @param {string|null} [options.idempotencyKey] - Forwarded Idempotency-Key + * @param {object} options.credentials - { accessToken?, apiKey?, refreshToken?, authType? } + * @param {AbortSignal} [options.signal] - Client cancellation signal + * @param {number} [options.timeoutMs] + * @param {object} [options.log] + * @param {function} [options.onCredentialsRefreshed] + * @returns {Promise<{ success: boolean, response: Response, status?: number, error?: string }>} + */ +export async function handleVideoProxyCore({ + provider, + action = null, + requestId = null, + rawBody = null, + contentType = null, + idempotencyKey = null, + credentials, + signal, + timeoutMs = VIDEO_FETCH_TIMEOUT_MS, + log, + onCredentialsRefreshed, +}) { + const config = getVideoConfig(provider); + if (!config) { + return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Provider '${provider}' does not support video generation`); + } + if (!requestId && !VIDEO_ACTIONS.has(action)) { + return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Unknown video action: ${action}`); + } + + const method = requestId ? "GET" : "POST"; + const url = buildUpstreamUrl(config, action, requestId); + const fetchSignal = combineSignals(signal, timeoutMs); + + const doFetch = (token) => + fetch(url, { + method, + headers: buildHeaders({ token, contentType: method === "POST" ? contentType : null, idempotencyKey: method === "POST" ? idempotencyKey : null }), + body: method === "POST" ? rawBody : undefined, + signal: fetchSignal, + }); + + let upstream; + try { + upstream = await doFetch(credentials?.accessToken || credentials?.apiKey); + } catch (error) { + if (error?.name === "AbortError" || error?.name === "TimeoutError") { + return createErrorResult(HTTP_STATUS.REQUEST_TIMEOUT, `[${provider}] video ${method} aborted: ${error.message}`); + } + // Never re-send a creation POST on network error — the job may already exist upstream. + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, sanitizeSecrets(`[${provider}] video upstream fetch failed: ${error.message}`, credentials)); + } + + // 401/403 → refresh once → retry once (OAuth accounts only; API keys can't refresh) + if ( + (upstream.status === HTTP_STATUS.UNAUTHORIZED || upstream.status === HTTP_STATUS.FORBIDDEN) && + credentials?.refreshToken + ) { + let refreshed = null; + try { + refreshed = await refreshTokenByProvider(provider, credentials, log); + } catch (error) { + log?.warn?.("TOKEN", `${provider} | video refresh error: ${sanitizeSecrets(error.message, credentials)}`); + } + if (refreshed?.accessToken) { + log?.info?.("TOKEN", `${provider.toUpperCase()} | refreshed for video ${method}`); + Object.assign(credentials, refreshed); + if (onCredentialsRefreshed) await onCredentialsRefreshed(refreshed); + try { + await upstream.body?.cancel?.(); + } catch { /* noop */ } + try { + upstream = await doFetch(credentials.accessToken || credentials.apiKey); + } catch (error) { + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, sanitizeSecrets(`[${provider}] video retry after refresh failed: ${error.message}`, credentials)); + } + } else { + log?.warn?.("TOKEN", `${provider.toUpperCase()} | video refresh failed — account needs re-auth`); + } + } + + const bodyText = await upstream.text().catch(() => ""); + + if (!upstream.ok) { + const message = sanitizeSecrets(bodyText || `HTTP ${upstream.status}`, credentials); + return createErrorResult(upstream.status, `[${provider}] ${message.slice(0, 2000)}`); + } + + // Success: pass the upstream JSON through untouched (request_id / status / video.url). + return { + success: true, + response: new Response(bodyText, { + status: upstream.status, + headers: { + "Content-Type": upstream.headers.get("content-type") || "application/json", + "Access-Control-Allow-Origin": "*", + }, + }), + }; +} diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index c73a4bc8..6e54a683 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -98,6 +98,8 @@ export const MODEL_CAPABILITIES = { "coder-model": { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 }, }; +const KIRO_GPT_5_6_CAPABILITIES = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 }; + /** * Provider-specific capability overrides. Keyed by provider alias/id. */ @@ -111,6 +113,20 @@ export const PROVIDER_CAPABILITIES = { "deepseek-ai/deepseek-v4-pro": { reasoning: true, thinkingFormat: "openai", contextWindow: 1000000, maxOutput: 65536 }, "deepseek-ai/deepseek-v4-flash": { reasoning: true, thinkingFormat: "openai", contextWindow: 1000000, maxOutput: 65536 }, }, + "kiro": { + "gpt-5.6-sol": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-terra": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-luna": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-sol-thinking": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-terra-thinking": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-luna-thinking": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-sol-agentic": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-terra-agentic": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-luna-agentic": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-sol-thinking-agentic": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-terra-thinking-agentic": KIRO_GPT_5_6_CAPABILITIES, + "gpt-5.6-luna-thinking-agentic": KIRO_GPT_5_6_CAPABILITIES, + }, // CodeBuddy.cn — authoritative per-model metadata from the gateway's model // config (contextWindow=maxInputTokens, maxOutput=maxOutputTokens, vision= // supportsImages). Every model reasons via OpenAI-style reasoning_effort @@ -186,6 +202,8 @@ export const PATTERN_CAPABILITIES = [ // ── Grok (vision + Live Search) ────────────────────────────────── { pattern: "*grok*image*", caps: { imageOutput: true } }, { pattern: "*grok-code*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 256000 } }, + // Grok 4.5 (Grok CLI / Grok Build): 500k context per cli-chat-proxy /v1/models + { pattern: "*grok-4.5*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 500000, maxOutput: 64000 } }, { pattern: "*grok-4*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } }, { pattern: "*grok-3*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 131072 } }, { pattern: "*grok*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } }, @@ -269,13 +287,17 @@ export const PATTERN_CAPABILITIES = [ export function getCapabilitiesForModel(provider, model) { if (!model) return { ...DEFAULT_CAPABILITIES }; + // Canonical exact lookup strips vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7". + const baseModel = model.includes("/") ? model.split("/").pop() : model; + // 1. Provider-specific override - if (provider && PROVIDER_CAPABILITIES[provider]?.[model]) { - return { ...DEFAULT_CAPABILITIES, ...PROVIDER_CAPABILITIES[provider][model] }; + if (provider) { + const providerCaps = PROVIDER_CAPABILITIES[provider]; + if (providerCaps?.[model]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[model] }; + if (providerCaps?.[baseModel]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[baseModel] }; } - // 2. Canonical exact (strip vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7") - const baseModel = model.includes("/") ? model.split("/").pop() : model; + // 2. Canonical exact if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] }; if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] }; diff --git a/open-sse/providers/models/schema.js b/open-sse/providers/models/schema.js index 8be351ad..c14a3d58 100644 --- a/open-sse/providers/models/schema.js +++ b/open-sse/providers/models/schema.js @@ -1,5 +1,14 @@ import { deriveModelName } from "./namePatterns.js"; +// Normalize version separators in a model id: hyphen between two digits becomes a dot. +// Registry ids use dots for versions ("claude-sonnet-4.5") but clients (CLIs, aliases) +// often send them with dashes ("claude-sonnet-4-5"). Only digit-digit hyphens are +// touched, so word/suffix hyphens stay intact ("-thinking", "-agentic", "qwen3-coder-next"). +export function normalizeModelId(modelId) { + if (typeof modelId !== "string") return modelId; + return modelId.replace(/(\d)-(\d)/g, "$1.$2"); +} + // Model defaults centralized (was scattered as `m.kind || "llm"`, `quotaFamily || "normal"`, etc.) export const MODEL_DEFAULTS = { kind: "llm", diff --git a/open-sse/providers/pricing.js b/open-sse/providers/pricing.js index 2fb0cfc9..943365a1 100644 --- a/open-sse/providers/pricing.js +++ b/open-sse/providers/pricing.js @@ -28,6 +28,7 @@ export const MODEL_PRICING = { "claude-sonnet-4.6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 }, "claude-opus-4-5-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, "claude-opus-4-6-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, + "claude-fable-5": { input: 10.00, output: 50.00, cached: 1.00, reasoning: 50.00, cache_creation: 12.50 }, // === OpenAI / GPT === "gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 }, @@ -36,22 +37,22 @@ export const MODEL_PRICING = { "gpt-4o": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 }, "gpt-4o-mini": { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 }, "gpt-4.1": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 }, - "gpt-5": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, - "gpt-5-mini": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 }, - "gpt-5-codex": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, - "gpt-5.1": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 }, - "gpt-5.1-codex": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 }, + "gpt-5": { input: 1.25, output: 10.00, cached: 0.625, reasoning: 10.00, cache_creation: 1.25 }, + "gpt-5-mini": { input: 0.25, output: 2.00, cached: 0.125, reasoning: 2.00, cache_creation: 0.25 }, + "gpt-5-codex": { input: 1.25, output: 10.00, cached: 0.625, reasoning: 10.00, cache_creation: 1.25 }, + "gpt-5.1": { input: 1.25, output: 10.00, cached: 0.625, reasoning: 10.00, cache_creation: 1.25 }, + "gpt-5.1-codex": { input: 1.25, output: 10.00, cached: 0.625, reasoning: 10.00, cache_creation: 1.25 }, "gpt-5.1-codex-mini": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 }, "gpt-5.1-codex-mini-high": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 }, "gpt-5.1-codex-max": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 }, - "gpt-5.2": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 }, - "gpt-5.2-codex": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 }, - "gpt-5.3-codex": { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 }, - "gpt-5.3-codex-xhigh": { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 }, - "gpt-5.3-codex-high": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 }, - "gpt-5.3-codex-low": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 }, - "gpt-5.3-codex-none": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, + "gpt-5.2": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 }, + "gpt-5.2-codex": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 }, + "gpt-5.3-codex": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 }, "gpt-5.3-codex-spark": { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 }, + "gpt-5.6": { input: 2.50, output: 15.00, cached: 0.25, reasoning: 15.00, cache_creation: 2.50 }, + "gpt-5.6-luna": { input: 1.00, output: 6.00, cached: 0.10, reasoning: 6.00, cache_creation: 1.00 }, + "gpt-5.6-terra": { input: 2.50, output: 15.00, cached: 0.25, reasoning: 15.00, cache_creation: 2.50 }, + "gpt-5.6-sol": { input: 5.00, output: 30.00, cached: 0.50, reasoning: 30.00, cache_creation: 5.00 }, "o1": { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 }, "o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, @@ -122,7 +123,7 @@ export const MODEL_PRICING = { * Keyed by provider alias (cc, cx, gc, gh, ...) or provider id (openai, anthropic, ...). */ export const PROVIDER_PRICING = { - // GitHub Copilot (gh) — gpt-5.3-codex has different rate than canonical + // GitHub Copilot (gh) — explicit override, matches canonical gpt-5.3-codex rate gh: { "gpt-5.3-codex": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 }, }, @@ -140,11 +141,11 @@ export const PATTERN_PRICING = [ { pattern: "*-codex-max", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } }, { pattern: "*-codex-mini-*", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } }, { pattern: "*-codex-mini", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } }, - { pattern: "*-codex-low", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } }, - { pattern: "*-codex-none", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "*-codex-low", pricing: { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 } }, + { pattern: "*-codex-none", pricing: { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 } }, { pattern: "*-codex-spark", pricing: { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 } }, - { pattern: "codex-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, - { pattern: "*-codex", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "codex-*", pricing: { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 } }, + { pattern: "*-codex", pricing: { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 } }, // --- Claude --- { pattern: "claude-opus-*", pricing: { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 } }, @@ -161,11 +162,12 @@ export const PATTERN_PRICING = [ { pattern: "gemini-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } }, // --- GPT (specific first, generic last) --- - { pattern: "gpt-5.3-*", pricing: { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 } }, - { pattern: "gpt-5.2-*", pricing: { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 } }, - { pattern: "gpt-5.1-*", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } }, - { pattern: "gpt-5-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, - { pattern: "gpt-5*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "gpt-5.6-*", pricing: { input: 2.50, output: 15.00, cached: 0.25, reasoning: 15.00, cache_creation: 2.50 } }, + { pattern: "gpt-5.3-*", pricing: { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 } }, + { pattern: "gpt-5.2-*", pricing: { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 } }, + { pattern: "gpt-5.1-*", pricing: { input: 1.25, output: 10.00, cached: 0.625, reasoning: 10.00, cache_creation: 1.25 } }, + { pattern: "gpt-5-*", pricing: { input: 1.25, output: 10.00, cached: 0.625, reasoning: 10.00, cache_creation: 1.25 } }, + { pattern: "gpt-5*", pricing: { input: 1.25, output: 10.00, cached: 0.625, reasoning: 10.00, cache_creation: 1.25 } }, { pattern: "gpt-4o-*", pricing: { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 } }, { pattern: "gpt-4o", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } }, { pattern: "gpt-4*", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } }, diff --git a/open-sse/providers/registry/alicode-intl.js b/open-sse/providers/registry/alicode-intl.js index b2eca7d8..b93d1d89 100644 --- a/open-sse/providers/registry/alicode-intl.js +++ b/open-sse/providers/registry/alicode-intl.js @@ -14,7 +14,7 @@ export default { }, category: "apikey", transport: { - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", + baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions", headers: {}, quirks: { preserveCacheControl: true }, }, diff --git a/open-sse/providers/registry/anthropic.js b/open-sse/providers/registry/anthropic.js index 1f6a3494..f83937fe 100644 --- a/open-sse/providers/registry/anthropic.js +++ b/open-sse/providers/registry/anthropic.js @@ -1,5 +1,3 @@ -import { CLAUDE_API_HEADERS } from "../shared.js"; - export default { id: "anthropic", priority: 30, @@ -19,7 +17,7 @@ export default { baseUrl: "https://api.anthropic.com/v1/messages", format: "claude", headers: { - "Anthropic-Version": "2023-06-01", + "anthropic-version": "2023-06-01", "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", }, }, diff --git a/open-sse/providers/registry/antigravity.js b/open-sse/providers/registry/antigravity.js index 29003527..dd7fbc02 100644 --- a/open-sse/providers/registry/antigravity.js +++ b/open-sse/providers/registry/antigravity.js @@ -1,5 +1,4 @@ -import { platform, arch } from "os"; -import { ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js"; +import { ANTIGRAVITY_IDE_BASE_URL, ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js"; export default { id: "antigravity", @@ -20,13 +19,10 @@ export default { category: "oauth", serviceKinds: ["llm", "image"], transport: { - baseUrls: [ - "https://daily-cloudcode-pa.googleapis.com", - "https://daily-cloudcode-pa.sandbox.googleapis.com", - ], + baseUrls: [ANTIGRAVITY_IDE_BASE_URL], format: "antigravity", headers: { - "User-Agent": "antigravity/1.107.0 darwin/arm64", + "User-Agent": ANTIGRAVITY_IDE_USER_AGENT, }, retry: { "429": { diff --git a/open-sse/providers/registry/claude.js b/open-sse/providers/registry/claude.js index 9d483d8f..a2912701 100644 --- a/open-sse/providers/registry/claude.js +++ b/open-sse/providers/registry/claude.js @@ -60,12 +60,10 @@ export default { }, }, models: [ + { id: "claude-fable-5", name: "Claude Fable 5" }, + { id: "claude-sonnet-5", name: "Claude Sonnet 5" }, { id: "claude-opus-4-8", name: "Claude Opus 4.8" }, { id: "claude-opus-4-7", name: "Claude Opus 4.7" }, - { 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" }, ], oauth: { diff --git a/open-sse/providers/registry/codex.js b/open-sse/providers/registry/codex.js index 0d2ddc05..6fc7501d 100644 --- a/open-sse/providers/registry/codex.js +++ b/open-sse/providers/registry/codex.js @@ -45,22 +45,18 @@ export default { }, }, models: [ + { id: "gpt-5.6-sol", name: "GPT 5.6 Sol" }, + { id: "gpt-5.6-sol-review", name: "GPT 5.6 Sol Review", upstreamModelId: "gpt-5.6-sol", quotaFamily: "review" }, + { id: "gpt-5.6-terra", name: "GPT 5.6 Terra" }, + { id: "gpt-5.6-terra-review", name: "GPT 5.6 Terra Review", upstreamModelId: "gpt-5.6-terra", quotaFamily: "review" }, + { id: "gpt-5.6-luna", name: "GPT 5.6 Luna" }, + { id: "gpt-5.6-luna-review", name: "GPT 5.6 Luna Review", upstreamModelId: "gpt-5.6-luna", quotaFamily: "review" }, { id: "gpt-5.5", name: "GPT 5.5" }, { id: "gpt-5.5-review", name: "GPT 5.5 Review", upstreamModelId: "gpt-5.5", quotaFamily: "review" }, { id: "gpt-5.4", name: "GPT 5.4" }, { id: "gpt-5.4-review", name: "GPT 5.4 Review", upstreamModelId: "gpt-5.4", quotaFamily: "review" }, { id: "gpt-5.4-mini", name: "GPT 5.4 Mini" }, { id: "gpt-5.4-mini-review", name: "GPT 5.4 Mini Review", upstreamModelId: "gpt-5.4-mini", quotaFamily: "review" }, - { id: "gpt-5.3-codex", name: "GPT 5.3 Codex" }, - { id: "gpt-5.3-codex-review", name: "GPT 5.3 Codex Review", upstreamModelId: "gpt-5.3-codex", quotaFamily: "review" }, - { id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex (xHigh)" }, - { id: "gpt-5.3-codex-xhigh-review", name: "GPT 5.3 Codex (xHigh) Review", upstreamModelId: "gpt-5.3-codex-xhigh", quotaFamily: "review" }, - { id: "gpt-5.3-codex-high", name: "GPT 5.3 Codex (High)" }, - { id: "gpt-5.3-codex-high-review", name: "GPT 5.3 Codex (High) Review", upstreamModelId: "gpt-5.3-codex-high", quotaFamily: "review" }, - { id: "gpt-5.3-codex-low", name: "GPT 5.3 Codex (Low)" }, - { id: "gpt-5.3-codex-low-review", name: "GPT 5.3 Codex (Low) Review", upstreamModelId: "gpt-5.3-codex-low", quotaFamily: "review" }, - { id: "gpt-5.3-codex-none", name: "GPT 5.3 Codex (None)" }, - { id: "gpt-5.3-codex-none-review", name: "GPT 5.3 Codex (None) Review", upstreamModelId: "gpt-5.3-codex-none", quotaFamily: "review" }, { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, { id: "gpt-5.3-codex-spark-review", name: "GPT 5.3 Codex Spark Review", upstreamModelId: "gpt-5.3-codex-spark", quotaFamily: "review" }, { id: "gpt-5.5-image", name: "GPT 5.5 Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, diff --git a/open-sse/providers/registry/featherless.js b/open-sse/providers/registry/featherless.js new file mode 100644 index 00000000..85240df7 --- /dev/null +++ b/open-sse/providers/registry/featherless.js @@ -0,0 +1,34 @@ +export default { + id: "featherless", + priority: 65, + alias: "featherless", + aliases: [ + "fl", + ], + uiAlias: "fl", + display: { + name: "Featherless", + icon: "flutter_dash", + color: "#111827", + textIcon: "FL", + website: "https://featherless.ai", + notice: { + apiKeyUrl: "https://featherless.ai/account/api-keys", + }, + }, + category: "apikey", + authType: "apikey", + transport: { + baseUrl: "https://api.featherless.ai/v1/chat/completions", + validateUrl: "https://api.featherless.ai/v1/models", + }, + models: [ + { id: "deepseek-ai/DeepSeek-V4-Pro", name: "DeepSeek V4 Pro" }, + { id: "deepseek-ai/DeepSeek-V4-Flash", name: "DeepSeek V4 Flash" }, + { id: "zai-org/GLM-5.2", name: "GLM 5.2" }, + { id: "zai-org/GLM-5.1", name: "GLM 5.1" }, + { id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code" }, + { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6" }, + { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5" }, + ], +}; diff --git a/open-sse/providers/registry/github.js b/open-sse/providers/registry/github.js index 95169eb3..1104eeb3 100644 --- a/open-sse/providers/registry/github.js +++ b/open-sse/providers/registry/github.js @@ -18,6 +18,7 @@ export default { transport: { baseUrl: "https://api.githubcopilot.com/chat/completions", responsesUrl: "https://api.githubcopilot.com/responses", + messagesUrl: "https://api.githubcopilot.com/v1/messages", headers: { "copilot-integration-id": "vscode-chat", "editor-version": "vscode/1.110.0", @@ -46,6 +47,14 @@ export default { { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, { id: "gpt-5.4", name: "GPT-5.4" }, { id: "gpt-5.4-mini", name: "GPT-5.4 Mini" }, + // Note: routing to Copilot's Anthropic-native /v1/messages shim (see + // executors/github.js) is decided by model-NAME pattern at request time, not by + // a static targetFormat field here — Copilot's live model catalog (see + // services/copilotModels.js) regularly exposes claude-* models this static list + // hasn't caught up with yet (e.g. claude-opus-4.8), and a static per-entry + // targetFormat would silently miss those while also double-translating requests + // for models that ARE listed here (chatCore.js would pre-translate to Claude + // shape, then the executor would translate again). Keep these as plain entries. { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, { id: "claude-opus-4.5", name: "Claude Opus 4.5" }, { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, diff --git a/open-sse/providers/registry/grok-cli.js b/open-sse/providers/registry/grok-cli.js new file mode 100644 index 00000000..403c1abe --- /dev/null +++ b/open-sse/providers/registry/grok-cli.js @@ -0,0 +1,96 @@ +/** + * Grok CLI / Grok Build (cli-chat-proxy.grok.com) + * + * Source of truth: wire capture of official @xai-official/grok 0.2.99 + * talking to https://cli-chat-proxy.grok.com (OpenAI Responses API). + * + * Distinct from: + * - `xai` → api.x.ai (API key / xAI API OAuth PKCE) + * - `grok-web` → grok.com web SSO cookie + */ +import { + GROK_CLI_BASE_URL, + GROK_CLI_CLIENT_IDENTIFIER, + GROK_CLI_MODEL, + GROK_CLI_USER_AGENT, + GROK_CLI_VERSION, +} from "../../config/grokCli.js"; + +export default { + id: "grok-cli", + priority: 275, + alias: "gcli", + aliases: ["grok-build", "gb"], + uiAlias: "gcli", + display: { + name: "Grok CLI (Grok Build)", + icon: "auto_awesome", + color: "#1DA1F2", + textIcon: "GC", + website: "https://x.ai", + notice: { + text: "Sign in with your xAI / Grok account via device code. Uses Grok Build subscription credits (cli-chat-proxy.grok.com).", + signupUrl: "https://grok.com/supergrok", + }, + }, + category: "oauth", + authModes: ["oauth"], + hasOAuth: true, + thinkingConfig: { + options: ["low", "medium", "high", "xhigh"], + defaultMode: "high", + }, + transport: { + baseUrl: `${GROK_CLI_BASE_URL}/responses`, + format: "openai-responses", + forceStream: true, + modelsUrl: `${GROK_CLI_BASE_URL}/models`, + userUrl: `${GROK_CLI_BASE_URL}/user`, + billingUrl: `${GROK_CLI_BASE_URL}/billing`, + clientVersion: GROK_CLI_VERSION, + clientIdentifier: GROK_CLI_CLIENT_IDENTIFIER, + tokenAuth: "xai-grok-cli", + headers: { + "User-Agent": GROK_CLI_USER_AGENT, + "x-grok-client-identifier": GROK_CLI_CLIENT_IDENTIFIER, + "x-grok-client-version": GROK_CLI_VERSION, + }, + // Quota tracker: official CLI polls billing?format=credits + user?include=subscription + usage: { + url: `${GROK_CLI_BASE_URL}/billing?format=credits`, + userUrl: `${GROK_CLI_BASE_URL}/user?include=subscription`, + }, + retry: { + 429: { attempts: 2, delayMs: 2000 }, + 502: { attempts: 2, delayMs: 1500 }, + 503: { attempts: 2, delayMs: 1500 }, + }, + }, + models: [ + { + id: GROK_CLI_MODEL, + name: "Grok Build", + contextLength: 500000, + maxOutputTokens: 64000, + }, + { id: "grok-4.5", name: "Grok 4.5" }, + { id: "grok-4.5-high", name: "Grok 4.5 (High)", upstreamModelId: "grok-4.5" }, + { id: "grok-4.5-medium", name: "Grok 4.5 (Medium)", upstreamModelId: "grok-4.5" }, + { id: "grok-4.5-low", name: "Grok 4.5 (Low)", upstreamModelId: "grok-4.5" }, + ], + features: { + usage: true, + }, + oauth: { + // Same public client_id as Grok CLI / existing xai OAuth + clientId: "b1a00492-073a-47ea-816f-4c329264a828", + deviceCodeUrl: "https://auth.x.ai/oauth2/device/code", + tokenUrl: "https://auth.x.ai/oauth2/token", + refreshUrl: "https://auth.x.ai/oauth2/token", + // HAR scope includes conversations read/write beyond the api-only xai scope + scope: + "openid profile email offline_access grok-cli:access api:access conversations:read conversations:write", + referrer: "grok-build", + refreshLeadMs: 5 * 60 * 1000, + }, +}; diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index 6f0f6826..e4cf5439 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -1,4 +1,4 @@ -// Auto-generated: static imports of all registry entries +// Auto-generated: static imports for all registry entries import p0 from "./alicode-intl.js"; import p1 from "./alicode.js"; import p2 from "./anthropic.js"; @@ -30,72 +30,75 @@ import p27 from "./edge-tts.js"; import p28 from "./elevenlabs.js"; import p29 from "./exa.js"; import p30 from "./fal-ai.js"; -import p31 from "./firecrawl.js"; -import p32 from "./fireworks.js"; -import p33 from "./gemini-cli.js"; -import p34 from "./gemini.js"; -import p35 from "./github.js"; -import p36 from "./gitlab.js"; -import p37 from "./glm-cn.js"; -import p38 from "./glm.js"; -import p39 from "./google-pse.js"; -import p40 from "./google-tts.js"; -import p41 from "./grok-web.js"; -import p42 from "./groq.js"; -import p43 from "./huggingface.js"; -import p44 from "./hyperbolic.js"; -import p45 from "./iflow.js"; -import p46 from "./inworld.js"; -import p47 from "./jina-ai.js"; -import p48 from "./jina-reader.js"; -import p49 from "./kilocode.js"; -import p50 from "./kimchi.js"; -import p51 from "./kimi-coding.js"; -import p52 from "./kimi.js"; -import p53 from "./kiro.js"; -import p54 from "./linkup.js"; -import p55 from "./local-device.js"; -import p56 from "./mimo-free.js"; -import p57 from "./minimax-cn.js"; -import p58 from "./minimax.js"; -import p59 from "./mistral.js"; -import p60 from "./mmf.js"; -import p61 from "./nanobanana.js"; -import p62 from "./nebius.js"; -import p63 from "./nvidia.js"; -import p64 from "./ollama-local.js"; -import p65 from "./ollama.js"; -import p66 from "./openai.js"; -import p67 from "./opencode-go.js"; -import p68 from "./opencode.js"; -import p69 from "./openrouter.js"; -import p70 from "./perplexity-web.js"; -import p71 from "./perplexity.js"; -import p72 from "./playht.js"; -import p73 from "./qoder.js"; -import p74 from "./qwen.js"; -import p75 from "./recraft.js"; -import p76 from "./runwayml.js"; -import p77 from "./sdwebui.js"; -import p78 from "./searchapi.js"; -import p79 from "./searxng.js"; -import p80 from "./serper.js"; -import p81 from "./siliconflow.js"; -import p82 from "./stability-ai.js"; -import p83 from "./tavily.js"; -import p84 from "./together.js"; -import p85 from "./topaz.js"; -import p86 from "./tortoise.js"; -import p87 from "./venice.js"; -import p88 from "./vercel-ai-gateway.js"; -import p89 from "./vertex-partner.js"; -import p90 from "./vertex.js"; -import p91 from "./volcengine-ark.js"; -import p92 from "./voyage-ai.js"; -import p93 from "./xai.js"; -import p94 from "./xiaomi-mimo.js"; -import p95 from "./xiaomi-tokenplan.js"; -import p96 from "./youcom.js"; +import p31 from "./featherless.js"; +import p32 from "./firecrawl.js"; +import p33 from "./fireworks.js"; +import p34 from "./gemini-cli.js"; +import p35 from "./gemini.js"; +import p36 from "./github.js"; +import p37 from "./gitlab.js"; +import p38 from "./glm-cn.js"; +import p39 from "./glm.js"; +import p40 from "./google-pse.js"; +import p41 from "./google-tts.js"; +import p42 from "./grok-cli.js"; +import p43 from "./grok-web.js"; +import p44 from "./groq.js"; +import p45 from "./huggingface.js"; +import p46 from "./hyperbolic.js"; +import p47 from "./iflow.js"; +import p48 from "./inworld.js"; +import p49 from "./jina-ai.js"; +import p50 from "./jina-reader.js"; +import p51 from "./kilocode.js"; +import p52 from "./kimchi.js"; +import p53 from "./kimi-coding.js"; +import p54 from "./kimi.js"; +import p55 from "./kiro.js"; +import p56 from "./linkup.js"; +import p57 from "./local-device.js"; +import p58 from "./mimo-free.js"; +import p59 from "./minimax-cn.js"; +import p60 from "./minimax.js"; +import p61 from "./mistral.js"; +import p62 from "./mmf.js"; +import p63 from "./nanobanana.js"; +import p64 from "./nebius.js"; +import p65 from "./nvidia.js"; +import p66 from "./ollama-local.js"; +import p67 from "./ollama.js"; +import p68 from "./openai.js"; +import p69 from "./opencode-go.js"; +import p70 from "./opencode.js"; +import p71 from "./openrouter.js"; +import p72 from "./perplexity-web.js"; +import p73 from "./perplexity.js"; +import p74 from "./perplexity-agent.js"; +import p75 from "./playht.js"; +import p76 from "./qoder.js"; +import p77 from "./qwen.js"; +import p78 from "./recraft.js"; +import p79 from "./runwayml.js"; +import p80 from "./sdwebui.js"; +import p81 from "./searchapi.js"; +import p82 from "./searxng.js"; +import p83 from "./serper.js"; +import p84 from "./siliconflow.js"; +import p85 from "./stability-ai.js"; +import p86 from "./tavily.js"; +import p87 from "./together.js"; +import p88 from "./topaz.js"; +import p89 from "./tortoise.js"; +import p90 from "./venice.js"; +import p91 from "./vercel-ai-gateway.js"; +import p92 from "./vertex-partner.js"; +import p93 from "./vertex.js"; +import p94 from "./volcengine-ark.js"; +import p95 from "./voyage-ai.js"; +import p96 from "./xai.js"; +import p97 from "./xiaomi-mimo.js"; +import p98 from "./xiaomi-tokenplan.js"; +import p99 from "./youcom.js"; export default [ p0, @@ -194,5 +197,8 @@ export default [ p93, p94, p95, - p96 + p96, + p97, + p98, + p99 ]; diff --git a/open-sse/providers/registry/kiro.js b/open-sse/providers/registry/kiro.js index 12015643..1a47adaa 100644 --- a/open-sse/providers/registry/kiro.js +++ b/open-sse/providers/registry/kiro.js @@ -42,22 +42,53 @@ export default { }, }, models: [ + // Opus (added per kiro.dev/changelog/models and kiro.dev/docs/models) + { id: "claude-opus-4.8", name: "Claude Opus 4.8" }, + { id: "claude-opus-4.8-thinking", name: "Claude Opus 4.8 (Thinking)" }, + { id: "claude-opus-4.8-agentic", name: "Claude Opus 4.8 (Agentic)" }, + { id: "claude-opus-4.8-thinking-agentic", name: "Claude Opus 4.8 (Thinking + Agentic)" }, + { id: "claude-opus-4.7", name: "Claude Opus 4.7" }, + { id: "claude-opus-4.7-thinking", name: "Claude Opus 4.7 (Thinking)" }, + { id: "claude-opus-4.7-agentic", name: "Claude Opus 4.7 (Agentic)" }, + { id: "claude-opus-4.7-thinking-agentic", name: "Claude Opus 4.7 (Thinking + Agentic)" }, + { id: "claude-opus-4.5", name: "Claude Opus 4.5" }, + { id: "claude-opus-4.5-thinking", name: "Claude Opus 4.5 (Thinking)" }, + { id: "claude-opus-4.5-agentic", name: "Claude Opus 4.5 (Agentic)" }, + { id: "claude-opus-4.5-thinking-agentic", name: "Claude Opus 4.5 (Thinking + Agentic)" }, + // Sonnet { id: "claude-sonnet-5", name: "Claude Sonnet 5" }, { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, + // Haiku { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, + // Non-Anthropic { id: "deepseek-3.2", name: "DeepSeek 3.2", strip: ["image","audio"] }, { id: "qwen3-coder-next", name: "Qwen3 Coder Next", strip: ["image","audio"] }, { id: "glm-5", name: "GLM 5" }, { id: "MiniMax-M2.5", name: "MiniMax M2.5" }, + { id: "gpt-5.6-sol", name: "GPT 5.6 Sol", contextLength: 272000, rateMultiplier: 2.4, upstreamModelId: "gpt-5.6-sol", description: "Experimental preview of OpenAI GPT 5.6 Sol with 272k context window" }, + { id: "gpt-5.6-terra", name: "GPT 5.6 Terra", contextLength: 272000, rateMultiplier: 1.2, upstreamModelId: "gpt-5.6-terra", description: "Experimental preview of OpenAI GPT 5.6 Terra with 272k context window" }, + { id: "gpt-5.6-luna", name: "GPT 5.6 Luna", contextLength: 272000, rateMultiplier: 0.6, upstreamModelId: "gpt-5.6-luna", description: "Experimental preview of OpenAI GPT 5.6 Luna with 272k context window" }, + // Thinking variants { id: "claude-sonnet-5-thinking", name: "Claude Sonnet 5 (Thinking)" }, { id: "claude-sonnet-4.5-thinking", name: "Claude Sonnet 4.5 (Thinking)" }, { id: "claude-haiku-4.5-thinking", name: "Claude Haiku 4.5 (Thinking)" }, + { id: "gpt-5.6-sol-thinking", name: "GPT 5.6 Sol (Thinking)", contextLength: 272000, rateMultiplier: 2.4, upstreamModelId: "gpt-5.6-sol", description: "Experimental preview of OpenAI GPT 5.6 Sol with 272k context window" }, + { id: "gpt-5.6-terra-thinking", name: "GPT 5.6 Terra (Thinking)", contextLength: 272000, rateMultiplier: 1.2, upstreamModelId: "gpt-5.6-terra", description: "Experimental preview of OpenAI GPT 5.6 Terra with 272k context window" }, + { id: "gpt-5.6-luna-thinking", name: "GPT 5.6 Luna (Thinking)", contextLength: 272000, rateMultiplier: 0.6, upstreamModelId: "gpt-5.6-luna", description: "Experimental preview of OpenAI GPT 5.6 Luna with 272k context window" }, + // Agentic variants { id: "claude-sonnet-5-agentic", name: "Claude Sonnet 5 (Agentic)" }, { id: "claude-sonnet-4.5-agentic", name: "Claude Sonnet 4.5 (Agentic)" }, { id: "claude-haiku-4.5-agentic", name: "Claude Haiku 4.5 (Agentic)" }, + { id: "gpt-5.6-sol-agentic", name: "GPT 5.6 Sol (Agentic)", contextLength: 272000, rateMultiplier: 2.4, upstreamModelId: "gpt-5.6-sol", description: "Experimental preview of OpenAI GPT 5.6 Sol with 272k context window" }, + { id: "gpt-5.6-terra-agentic", name: "GPT 5.6 Terra (Agentic)", contextLength: 272000, rateMultiplier: 1.2, upstreamModelId: "gpt-5.6-terra", description: "Experimental preview of OpenAI GPT 5.6 Terra with 272k context window" }, + { id: "gpt-5.6-luna-agentic", name: "GPT 5.6 Luna (Agentic)", contextLength: 272000, rateMultiplier: 0.6, upstreamModelId: "gpt-5.6-luna", description: "Experimental preview of OpenAI GPT 5.6 Luna with 272k context window" }, + // Thinking + Agentic variants { id: "claude-sonnet-5-thinking-agentic", name: "Claude Sonnet 5 (Thinking + Agentic)" }, { id: "claude-sonnet-4.5-thinking-agentic", name: "Claude Sonnet 4.5 (Thinking + Agentic)" }, { id: "claude-haiku-4.5-thinking-agentic", name: "Claude Haiku 4.5 (Thinking + Agentic)" }, + { id: "gpt-5.6-sol-thinking-agentic", name: "GPT 5.6 Sol (Thinking + Agentic)", contextLength: 272000, rateMultiplier: 2.4, upstreamModelId: "gpt-5.6-sol", description: "Experimental preview of OpenAI GPT 5.6 Sol with 272k context window" }, + { id: "gpt-5.6-terra-thinking-agentic", name: "GPT 5.6 Terra (Thinking + Agentic)", contextLength: 272000, rateMultiplier: 1.2, upstreamModelId: "gpt-5.6-terra", description: "Experimental preview of OpenAI GPT 5.6 Terra with 272k context window" }, + { id: "gpt-5.6-luna-thinking-agentic", name: "GPT 5.6 Luna (Thinking + Agentic)", contextLength: 272000, rateMultiplier: 0.6, upstreamModelId: "gpt-5.6-luna", description: "Experimental preview of OpenAI GPT 5.6 Luna with 272k context window" }, ], oauth: { ssoOidcEndpoint: "https://oidc.us-east-1.amazonaws.com", diff --git a/open-sse/providers/registry/perplexity-agent.js b/open-sse/providers/registry/perplexity-agent.js new file mode 100644 index 00000000..5c8785eb --- /dev/null +++ b/open-sse/providers/registry/perplexity-agent.js @@ -0,0 +1,49 @@ +export default { + id: "perplexity-agent", + priority: 181, + alias: "perplexity-agent", + aliases: [ + "pplx-agent", + "pplx-responses", + ], + uiAlias: "pa", + display: { + name: "Perplexity Agent", + icon: "travel_explore", + color: "#20808D", + textIcon: "PA", + website: "https://www.perplexity.ai", + notice: { + text: "Perplexity Agent API exposes GPT, Claude, Gemini, Grok, GLM, Kimi, and Sonar models through one OpenAI-compatible Responses API.", + apiKeyUrl: "https://www.perplexity.ai/settings/api", + }, + }, + category: "apikey", + authType: "apikey", + transport: { + baseUrl: "https://api.perplexity.ai/v1/responses", + validateUrl: "https://api.perplexity.ai/v1/models", + format: "openai-responses", + }, + models: [ + { id: "perplexity/sonar", name: "Perplexity Sonar" }, + { id: "openai/gpt-5.5", name: "GPT-5.5" }, + { id: "openai/gpt-5.4", name: "GPT-5.4" }, + { id: "openai/gpt-5.4-mini", name: "GPT-5.4 Mini" }, + { id: "anthropic/claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + { id: "anthropic/claude-opus-4-8", name: "Claude Opus 4.8" }, + { id: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro" }, + { id: "xai/grok-4.20-reasoning", name: "Grok 4.20 Reasoning" }, + { id: "perplexity/glm-5.2", name: "GLM 5.2" }, + { id: "perplexity/kimi-k2.7-code", name: "Kimi K2.7 Code" }, + { id: "nvidia/nemotron-3-super-120b-a12b", name: "Nemotron 3 Super 120B" }, + ], + serviceKinds: ["llm", "webSearch"], + searchViaChat: { + defaultModel: "perplexity/sonar", + endpoint: "https://api.perplexity.ai/v1/responses", + pricingUrl: "https://docs.perplexity.ai/docs/agent-api/models", + }, + modelsFetcher: { url: "https://api.perplexity.ai/v1/models", type: "openai" }, + passthroughModels: true, +}; diff --git a/open-sse/providers/registry/searxng.js b/open-sse/providers/registry/searxng.js index 308eabbc..bfbe4fdb 100644 --- a/open-sse/providers/registry/searxng.js +++ b/open-sse/providers/registry/searxng.js @@ -1,3 +1,5 @@ +import { SEARXNG_URL } from "../../config/runtimeConfig.js"; + export default { id: "searxng", alias: "searxng", @@ -15,7 +17,7 @@ export default { ], noAuth: true, searchConfig: { - baseUrl: "http://localhost:8888/search", + baseUrl: SEARXNG_URL, method: "GET", authType: "none", authHeader: "none", diff --git a/open-sse/providers/registry/xai.js b/open-sse/providers/registry/xai.js index 1ae9ff41..f04ad5f0 100644 --- a/open-sse/providers/registry/xai.js +++ b/open-sse/providers/registry/xai.js @@ -54,13 +54,22 @@ export default { params: ["n", "aspect_ratio", "resolution", "response_format", "size"], kind: "image", }, + { + id: "grok-imagine-video", + name: "Grok Imagine Video", + params: ["duration", "aspect_ratio", "resolution"], + kind: "video", + }, ], - serviceKinds: ["llm","imageToText","webSearch","image"], + serviceKinds: ["llm", "imageToText", "webSearch", "image", "video"], imageConfig: { baseUrl: "https://api.x.ai/v1/images/generations", editsUrl: "https://api.x.ai/v1/images/edits", bodyFields: ["model", "prompt", "n", "response_format", "aspect_ratio", "resolution", "image", "images"], }, + // Async video jobs (POST returns { request_id }, GET polls until done/failed). + // Docs: https://docs.x.ai/developers/rest-api-reference/inference/videos + videoConfig: { baseUrl: "https://api.x.ai/v1/videos" }, searchViaChat: { defaultModel: "grok-4.20-reasoning", endpoint: "https://api.x.ai/v1/responses", diff --git a/open-sse/providers/shared.js b/open-sse/providers/shared.js index 32388584..6f6a2c20 100644 --- a/open-sse/providers/shared.js +++ b/open-sse/providers/shared.js @@ -54,6 +54,13 @@ export const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1/messages"; export const OPENAI_COMPAT_BASE = "https://api.openai.com/v1"; export const ANTHROPIC_COMPAT_BASE = "https://api.anthropic.com/v1"; +// Official Antigravity IDE Desktop 2.1.1 fingerprint captured from macOS arm64. +// Keep this static even when 9router runs on Linux: the provider profile is +// intentionally matching the IDE client, not the server host. +export const ANTIGRAVITY_IDE_VERSION = "2.1.1"; +export const ANTIGRAVITY_IDE_BASE_URL = "https://cloudcode-pa.googleapis.com"; +export const ANTIGRAVITY_IDE_USER_AGENT = `antigravity/ide/${ANTIGRAVITY_IDE_VERSION} darwin/arm64`; + // Antigravity OAuth client credentials (public CLI client — duplicated in usage.js + src/lib/oauth) export const ANTIGRAVITY_OAUTH_CLIENT = { clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com", diff --git a/open-sse/providers/thinkingLevels.js b/open-sse/providers/thinkingLevels.js new file mode 100644 index 00000000..ba998ee7 --- /dev/null +++ b/open-sse/providers/thinkingLevels.js @@ -0,0 +1,48 @@ +// Resolve valid thinking levels per model — drives UI level picker (suffix "model(level)"). +// Reuses capabilities.js (thinkingFormat/canDisable) so this file only maps format→levels (DRY). +import { getCapabilitiesForModel } from "./capabilities.js"; +import { matchPattern } from "./pricing.js"; + +// Shared level sets (deduped) — verified against provider docs + wire in thinkingUnified.applyFormat. +const L = { + base: ["none", "low", "medium", "high"], // qwen, step, hunyuan, gemini-budget + onOff: ["none", "thinking"], // zai (binary), minimax (adaptive) + openai: ["none", "minimal", "low", "medium", "high", "xhigh"], // GPT-5.x / o-series (no "max") + levelMax: ["none", "low", "medium", "high", "max"], // claude-adaptive, kimi + budgetX: ["none", "low", "medium", "high", "xhigh", "max"], // claude-budget + gemini: ["minimal", "low", "medium", "high"], // gemini-3 thinkingLevel (no disable) + hiMax: ["none", "high", "max"], // deepseek (low/med→high, xhigh→max) +}; + +// thinkingFormat → valid selectable levels (source of truth for UI options). +const FORMAT_LEVELS = { + openai: L.openai, + "claude-adaptive": L.levelMax, + "claude-budget": L.budgetX, + "gemini-level": L.gemini, + "gemini-budget": L.base, + zai: L.onOff, + qwen: L.base, + kimi: L.levelMax, + deepseek: L.hiMax, + minimax: L.onOff, + hunyuan: L.base, + step: L.base, +}; + +// Model-name pattern overrides (glob, first match wins) — more precise than format default. +const PATTERN_THINKING = [ + // gpt-5.6-sol accepts max (maps to xhigh on wire); live probe rejected ultra. + { pattern: "*gpt-5.6-sol*", levels: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }, + { pattern: "*codex*", levels: ["low", "medium", "high", "xhigh"] }, // codex cannot disable thinking +]; + +// Returns valid thinking levels for a model, or null when the model has no reasoning. +export function getThinkingLevels(provider, model) { + const caps = getCapabilitiesForModel(provider, model); + if (!caps.reasoning) return null; + const hit = PATTERN_THINKING.find((p) => matchPattern(p.pattern, model)); + let levels = hit?.levels || FORMAT_LEVELS[caps.thinkingFormat] || L.base; + if (caps.thinkingCanDisable === false) levels = levels.filter((l) => l !== "none"); + return levels; +} diff --git a/open-sse/rtk/autodetect.js b/open-sse/rtk/autodetect.js index 99ab6a77..81992034 100644 --- a/open-sse/rtk/autodetect.js +++ b/open-sse/rtk/autodetect.js @@ -1,9 +1,10 @@ // Port of auto_detect_filter (rtk/src/cmds/system/pipe_cmd.rs:132-188) + JS extras -// Order: git-diff → git-status → build-output → grep → find → tree → ls → search-list -// → read-numbered → dedup-log → smart-truncate → null +// Detection order: git-log → git-diff → git-status → build-output → grep → find → tree → ls → search-list +// → read-numbered → dedup-log → smart-truncate → null import { DETECT_WINDOW, READ_NUMBERED_MIN_HIT_RATIO, SMART_TRUNCATE_MIN_LINES } from "./constants.js"; import { gitDiff } from "./filters/gitDiff.js"; import { gitStatus } from "./filters/gitStatus.js"; +import { gitLog } from "./filters/gitLog.js"; import { buildOutput } from "./filters/buildOutput.js"; import { grep } from "./filters/grep.js"; import { find } from "./filters/find.js"; @@ -17,6 +18,7 @@ import { searchList, SEARCH_LIST_HEADER_RE } from "./filters/searchList.js"; const RE_GIT_DIFF = /^diff --git /m; const RE_GIT_DIFF_HUNK = /^@@ /m; const RE_GIT_STATUS = /^On branch |^nothing to commit|^Changes (not |to be )|^Untracked files:/m; +const RE_GIT_LOG = /^[*|/\\ ]*commit [0-9a-f]{7,40}$/m; const RE_PORCELAIN = /^[ MADRCU?!][ MADRCU?!] \S/m; const RE_BUILD_OUTPUT = /^(npm (warn|error|ERR!)|yarn (warn|error)|\s*Compiling\s+\S+|\s*Downloading\s+\S+|added \d+ package|\[ERROR\]|BUILD (SUCCESS|FAILED)|\s*Finished\s+|Successfully (installed|built)|ERROR:)/im; const RE_TREE_GLYPH = /[├└]──|│ /; @@ -27,6 +29,7 @@ export function autoDetectFilter(text) { // Rust: floor_char_boundary to avoid UTF-8 split — JS .slice() by char is safe const head = text.length > DETECT_WINDOW ? text.slice(0, DETECT_WINDOW) : text; + if (RE_GIT_LOG.test(head)) return gitLog; if (RE_GIT_DIFF.test(head) || RE_GIT_DIFF_HUNK.test(head)) return gitDiff; if (RE_GIT_STATUS.test(head)) return gitStatus; @@ -81,6 +84,11 @@ function isGrepLine(line) { function isPathLike(line) { const t = line.trim(); if (t.length === 0) return false; + // A drive-letter prefix (e.g. "C:\Users\me" or "C:/Users/me") marks a + // Windows absolute path, so treat the whole line as path-like. Trailing + // colons (e.g. "C:\path\file.js:10") are tolerated, matching grep-style + // suffixes on Windows dumps. + if (/^[A-Za-z]:[\\/]/.test(t)) return true; if (t.includes(":")) return false; return t.startsWith(".") || t.startsWith("/") || t.includes("/"); } diff --git a/open-sse/rtk/cavemanPrompts.js b/open-sse/rtk/cavemanPrompts.js index 0b6f6f57..7b533d82 100644 --- a/open-sse/rtk/cavemanPrompts.js +++ b/open-sse/rtk/cavemanPrompts.js @@ -18,6 +18,14 @@ const SHARED_AUTO_CLARITY = "Auto-Clarity: drop caveman for security warnings, i const SHARED_PERSISTENCE = "ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure."; +const SHARED_NO_INVENTED_ABBREV = "No invented abbreviations. Standard well-known tech acronyms (DB, API, HTTP, URL, JSON, ID, OS, CPU) OK. Names of code symbols, function names, API names, error strings: keep verbatim."; + +const SHARED_PRESERVE_LANGUAGE = "Preserve the user's dominant language. User wrote Vietnamese, reply Vietnamese. User wrote English, reply English. Wenyan/classical-Chinese levels override this language-preservation rule. Code identifiers, error strings, file paths, commands: keep in their original form regardless of language."; + +const SHARED_NO_SELF_REFERENCE = 'No self-reference. Do not name or announce the style (no "caveman mode", no "me caveman think", no "compressed mode active"). Just respond.'; + +const SHARED_NO_DECORATION = 'No decorative emoji. No narrating tool calls ("I will now search", "I used X to find Y"). No status phrases ("Sure!", "Of course!", "I\'d be happy to"). No causal arrow shorthand ("A -> B -> fails"). State the thing, the action, the reason. Then next step.'; + export const CAVEMAN_PROMPTS = { [CAVEMAN_LEVELS.LITE]: [ "Respond tersely. Keep grammar and full sentences but drop filler, hedging and pleasantries (just/really/basically/sure/of course/I'd be happy to).", @@ -26,6 +34,10 @@ export const CAVEMAN_PROMPTS = { SHARED_BOUNDARIES, SHARED_AUTO_CLARITY, SHARED_PERSISTENCE, + SHARED_NO_INVENTED_ABBREV, + SHARED_PRESERVE_LANGUAGE, + SHARED_NO_SELF_REFERENCE, + SHARED_NO_DECORATION, ].join(" "), [CAVEMAN_LEVELS.FULL]: [ @@ -36,16 +48,24 @@ export const CAVEMAN_PROMPTS = { SHARED_BOUNDARIES, SHARED_AUTO_CLARITY, SHARED_PERSISTENCE, + SHARED_NO_INVENTED_ABBREV, + SHARED_PRESERVE_LANGUAGE, + SHARED_NO_SELF_REFERENCE, + SHARED_NO_DECORATION, ].join(" "), [CAVEMAN_LEVELS.ULTRA]: [ "Respond ultra-terse. Maximum compression. Telegraphic.", - "Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, use arrows for causality (X → Y). One word when one word enough.", - "Pattern: [thing] → [result]. [fix].", + "Strip conjunctions. One word when one word enough.", + "Pattern: [thing] [action] [reason]. [next step].", SHARED_EXAMPLES, SHARED_BOUNDARIES, SHARED_AUTO_CLARITY, SHARED_PERSISTENCE, + SHARED_NO_INVENTED_ABBREV, + SHARED_PRESERVE_LANGUAGE, + SHARED_NO_SELF_REFERENCE, + SHARED_NO_DECORATION, ].join(" "), [CAVEMAN_LEVELS.WENYAN_LITE]: [ @@ -55,6 +75,10 @@ export const CAVEMAN_PROMPTS = { SHARED_BOUNDARIES, SHARED_AUTO_CLARITY, SHARED_PERSISTENCE, + SHARED_NO_INVENTED_ABBREV, + SHARED_PRESERVE_LANGUAGE, + SHARED_NO_SELF_REFERENCE, + SHARED_NO_DECORATION, ].join(" "), [CAVEMAN_LEVELS.WENYAN]: [ @@ -65,6 +89,10 @@ export const CAVEMAN_PROMPTS = { SHARED_BOUNDARIES, SHARED_AUTO_CLARITY, SHARED_PERSISTENCE, + SHARED_NO_INVENTED_ABBREV, + SHARED_PRESERVE_LANGUAGE, + SHARED_NO_SELF_REFERENCE, + SHARED_NO_DECORATION, ].join(" "), [CAVEMAN_LEVELS.WENYAN_ULTRA]: [ @@ -74,5 +102,9 @@ export const CAVEMAN_PROMPTS = { SHARED_BOUNDARIES, SHARED_AUTO_CLARITY, SHARED_PERSISTENCE, + SHARED_NO_INVENTED_ABBREV, + SHARED_PRESERVE_LANGUAGE, + SHARED_NO_SELF_REFERENCE, + SHARED_NO_DECORATION, ].join(" "), }; diff --git a/open-sse/rtk/constants.js b/open-sse/rtk/constants.js index 752c2fee..bc80c23a 100644 --- a/open-sse/rtk/constants.js +++ b/open-sse/rtk/constants.js @@ -4,6 +4,7 @@ export const MIN_COMPRESS_SIZE = 500; // bytes; skip tiny blobs export const DETECT_WINDOW = 1024; // autodetect peeks first N chars export const GIT_DIFF_HUNK_MAX_LINES = 100; // per-hunk line cap export const GIT_DIFF_CONTEXT_KEEP = 3; // context lines around changes +export const GIT_LOG_MAX_LINES = 200; // gitLog line cap export const DEDUP_LINE_MAX = 2000; // dedupLog truncation cap // Rust pipe_cmd.rs parity caps diff --git a/open-sse/rtk/filters/find.js b/open-sse/rtk/filters/find.js index 5770a99b..4b64169b 100644 --- a/open-sse/rtk/filters/find.js +++ b/open-sse/rtk/filters/find.js @@ -9,16 +9,17 @@ export function find(input) { const byDir = new Map(); for (const path of lines) { - const lastSlash = path.lastIndexOf("/"); + // Accept both Unix ("/a/b") and Windows ("C:\a\b") separators + const lastSep = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")); let dir; let basename; - if (lastSlash === -1) { + if (lastSep === -1) { dir = "."; basename = path; } else { // Rust: PathBuf::from(path).parent().display() + file_name().display() - dir = path.slice(0, lastSlash) || "/"; - basename = path.slice(lastSlash + 1); + dir = path.slice(0, lastSep) || "/"; + basename = path.slice(lastSep + 1); } if (!byDir.has(dir)) byDir.set(dir, []); byDir.get(dir).push(basename); @@ -31,7 +32,8 @@ export function find(input) { const showDirs = dirs.slice(0, FIND_TOTAL_DIR_MAX); for (const dir of showDirs) { const files = byDir.get(dir); - out += `${dir}/ (${files.length})\n`; + const dirLabel = dir.replace(/\\/g, "/"); + out += `${dirLabel}/ (${files.length})\n`; const showFiles = files.slice(0, FIND_PER_DIR_MAX); for (const f of showFiles) out += ` ${f}\n`; if (files.length > FIND_PER_DIR_MAX) { diff --git a/open-sse/rtk/filters/gitLog.js b/open-sse/rtk/filters/gitLog.js new file mode 100644 index 00000000..9769c6de --- /dev/null +++ b/open-sse/rtk/filters/gitLog.js @@ -0,0 +1,99 @@ +// JS-native git-log filter +// Compresses `git log` output: keeps commit headers, subjects, Author/Date; +// drops body padding, decoration, embedded diff lines. +import { GIT_LOG_MAX_LINES } from "../constants.js"; + +export function gitLog(text, maxLines = GIT_LOG_MAX_LINES) { + if (!text) return ""; + + const input = String(text); + const lines = input.split("\n"); + const out = []; + let skipped = 0; + let inCommit = false; + let subjectSeen = false; + + function pushLine(l) { + if (out.length < maxLines) { + out.push(l); + return true; + } + skipped++; + return false; + } + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const line = raw.trimEnd(); + const trimmed = line.trim(); + + // commit header — starts new commit entry + // Also matched with leading graph decoration (`* commit abc1234...` — --graph without --oneline) + if (/^commit [0-9a-f]{7,40}$/i.test(trimmed) || /^[*|/\\ ]+commit [0-9a-f]{7,40}/i.test(trimmed)) { + inCommit = true; + subjectSeen = false; + pushLine(line); + continue; + } + + if (inCommit) { + // Author / Date — keep as-is (already column 0 in raw, or graph-prefix stripped by commit-header match) + if (/^[*|/\\ ]*(Author|Date):/i.test(trimmed)) { + pushLine(trimmed); + continue; + } + // blank — skip + if (trimmed === "") continue; + // indented subject (4 spaces, optionally preceded by graph decoration) — first one is subject + if (!subjectSeen && /^[*|/\\ ]* \S/.test(line)) { + pushLine(" Subject: " + trimmed); + subjectSeen = true; + continue; + } + // stat summary: "N file(s) changed, N insertions(+), N deletions(-)" + if (/^\d+ file\w* changed/.test(trimmed)) { + pushLine(" " + trimmed); + continue; + } + // embedded diff header — one-line marker + if (/^diff --git /.test(trimmed)) { + pushLine(" ... diff body omitted"); + continue; + } + // everything else in commit body — drop + continue; + } + + // Not in a commit block (--oneline / --graph modes): + + // Graph decoration + sha + subject: "*|/\\ " + const graphMatch = trimmed.match(/^[*|/\\ ]+([0-9a-f]{7,40}\s+.+)/i); + if (graphMatch) { + pushLine(graphMatch[1]); + continue; + } + + // Plain oneline: " " + if (/^[0-9a-f]{7,40}\s+/.test(trimmed)) { + pushLine(trimmed); + continue; + } + + // Pure graph decoration (no sha) — drop + if (/^[*|/\\ ]+$/.test(trimmed) && /[*|/\\]/.test(trimmed)) { + continue; + } + + // catch-all pass-through + pushLine(trimmed); + } + + if (skipped > 0) out.push(`... (${skipped} more lines)`); + + const result = out.join("\n"); + if (!result && input) return input; + if (result.length > input.length) return input; + return result; +} + +gitLog.filterName = "git-log"; diff --git a/open-sse/rtk/headroom.js b/open-sse/rtk/headroom.js index 8f3b1f33..880195ff 100644 --- a/open-sse/rtk/headroom.js +++ b/open-sse/rtk/headroom.js @@ -18,6 +18,8 @@ function jsonBytes(value) { function messagePayload(body) { if (Array.isArray(body?.messages)) return body.messages; if (Array.isArray(body?.input)) return body.input; + const kiro = collectKiroHeadroomMessages(body); + if (kiro) return kiro.messages; return null; } @@ -81,6 +83,121 @@ function hasUnsafeResponsesInputForCompression(body) { }); } +function collectKiroHeadroomMessages(body) { + const state = body?.conversationState; + if (!state || typeof state !== "object") return null; + + const messages = []; + const targets = []; + + const addTextTarget = (role, text, target, extra = {}) => { + if (typeof text !== "string") return; + messages.push({ role, content: text, ...extra }); + targets.push(target); + }; + + const toToolCalls = (toolUses) => { + if (!Array.isArray(toolUses) || toolUses.length === 0) return undefined; + const calls = toolUses.map((toolUse) => ({ + id: toolUse?.toolUseId, + type: "function", + function: { + name: toolUse?.name || "", + arguments: JSON.stringify(toolUse?.input || {}), + }, + })).filter((call) => call.id || call.function.name); + return calls.length > 0 ? calls : undefined; + }; + + const visit = (item) => { + const user = item?.userInputMessage; + if (user) { + addTextTarget("system", user.systemInstruction, { object: user, key: "systemInstruction" }); + addTextTarget("user", user.content, { object: user, key: "content" }); + + const toolResults = user.userInputMessageContext?.toolResults; + if (Array.isArray(toolResults)) { + for (const toolResult of toolResults) { + const content = toolResult?.content; + if (!Array.isArray(content)) continue; + for (const part of content) { + addTextTarget( + "tool", + part?.text, + { object: part, key: "text" }, + toolResult?.toolUseId ? { tool_call_id: toolResult.toolUseId } : {} + ); + } + } + } + return; + } + + const assistant = item?.assistantResponseMessage; + if (assistant) { + const toolCalls = toToolCalls(assistant.toolUses); + addTextTarget( + "assistant", + assistant.content, + { object: assistant, key: "content" }, + toolCalls ? { tool_calls: toolCalls } : {} + ); + } + }; + + if (Array.isArray(state.history)) { + for (const item of state.history) visit(item); + } + if (state.currentMessage) visit(state.currentMessage); + + return messages.length > 0 ? { messages, targets } : null; +} + +function textFromHeadroomMessage(message) { + const content = message?.content; + if (typeof content === "string") return content; + if (!Array.isArray(content)) return null; + + const parts = []; + for (const part of content) { + if (typeof part === "string") { + parts.push(part); + } else if (typeof part?.text === "string") { + parts.push(part.text); + } + } + return parts.length > 0 ? parts.join("\n") : null; +} + +function applyKiroHeadroomMessages(projection, compressedMessages, diagnostics) { + if (!Array.isArray(compressedMessages) || compressedMessages.length !== projection.messages.length) { + setDiagnostic(diagnostics, "proxy response did not match Kiro message count"); + return false; + } + + const updates = []; + for (let i = 0; i < projection.messages.length; i++) { + const expected = projection.messages[i]; + const actual = compressedMessages[i]; + if (!actual || actual.role !== expected.role) { + setDiagnostic(diagnostics, "proxy response did not preserve Kiro message order"); + return false; + } + + const text = textFromHeadroomMessage(actual); + if (text === null) { + setDiagnostic(diagnostics, "proxy response missing Kiro text content"); + return false; + } + updates.push({ target: projection.targets[i], text }); + } + + for (const update of updates) { + update.target.object[update.target.key] = update.text; + } + return true; +} + // POST messages to Headroom /v1/compress; returns compressed messages + stats or null. async function callCompress(url, messages, model, timeoutMs, compressUserMessages, diagnostics) { const endpoint = buildCompressEndpoint(url); @@ -171,6 +288,22 @@ export async function compressWithHeadroom(body, { enabled, url, model, format, return data; } + // Kiro shape: conversationState.history/currentMessage are projected to + // OpenAI messages for the proxy, then copied back into the original Kiro + // fields. Keep the provider payload shape intact for Kiro's executor. + if (format === "kiro") { + const projection = collectKiroHeadroomMessages(body); + if (!projection) { + setDiagnostic(diagnostics, "Kiro request did not project to messages[]"); + return null; + } + const data = await callCompress(url, projection.messages, model, timeoutMs, compressUserMessages, diagnostics || {}); + if (!data) return null; + if (!applyKiroHeadroomMessages(projection, data.messages, diagnostics)) return null; + if (diagnostics) diagnostics.after = captureSizeSnapshot(body); + return data; + } + // OpenAI shape: messages/input go straight to the proxy. const key = Array.isArray(body.messages) ? "messages" : Array.isArray(body.input) ? "input" diff --git a/open-sse/rtk/pxpipe.js b/open-sse/rtk/pxpipe.js new file mode 100644 index 00000000..04ac8f45 --- /dev/null +++ b/open-sse/rtk/pxpipe.js @@ -0,0 +1,104 @@ +// PXPIPE: render bulky Claude-format context as dense PNGs via pxpipe-proxy's +// library API (transformAnthropicMessages). Fail-open like every token saver: +// any error/timeout returns { body: null, summary } and leaves the request untouched. +import { FORMATS } from "../translator/formats.js"; + +const DEFAULT_TIMEOUT_MS = 15000; +const DEFAULT_MIN_CHARS = 25000; +// pxpipe's own profitability gate assumes ~4 chars/token; reuse it for the +// estimated before/after numbers surfaced in stats (marked "estimated" in UI). +const EST_CHARS_PER_TOKEN = 4; + +function bodyChars(body) { + try { + return JSON.stringify(body)?.length || 0; + } catch { + return 0; + } +} + +function estTokens(chars) { + return Math.round(chars / EST_CHARS_PER_TOKEN); +} + +function skipped(reason, extra = {}) { + return { body: null, summary: { applied: false, reason, ...extra } }; +} + +// Transform a Claude-format request body through pxpipe. Returns +// { body: | null, summary } — body is null when nothing changed. +// opts.transform is injected by the host (src side) so open-sse stays free of +// filesystem/install concerns and remains usable standalone. +export async function compressWithPxpipe(body, { enabled, format, model, minChars, timeoutMs, transform } = {}) { + if (!enabled) return skipped("disabled"); + if (typeof transform !== "function") return skipped("not_installed"); + if (!body) return skipped("missing_body"); + if (format !== FORMATS.CLAUDE) return skipped("unsupported_format", { detail: format }); + + const startedAt = Date.now(); + const originalChars = bodyChars(body); + const threshold = Number(minChars) > 0 ? Number(minChars) : DEFAULT_MIN_CHARS; + if (originalChars < threshold) { + return skipped("below_threshold", { originalChars, threshold }); + } + + try { + const encoded = new TextEncoder().encode(JSON.stringify(body)); + const budget = Number(timeoutMs) > 0 ? Number(timeoutMs) : DEFAULT_TIMEOUT_MS; + // transformAnthropicMessages is local CPU work and can't be aborted; race a + // timer and discard the result if it loses (input body is never mutated). + const result = await Promise.race([ + transform({ + body: encoded, + model, + options: { minCompressChars: threshold }, + }), + new Promise((resolve) => setTimeout(() => resolve(null), budget)), + ]); + if (!result) return skipped("timeout", { originalChars, durationMs: Date.now() - startedAt }); + if (!result.applied) { + return skipped(result.reason || "passthrough", { + detail: result.detail, + originalChars, + durationMs: Date.now() - startedAt, + }); + } + + const newBody = JSON.parse(new TextDecoder().decode(result.body)); + const compressedBodyChars = bodyChars(newBody); + const info = result.info || {}; + const imagedChars = info.compressedChars || 0; + // The transformed body is BIGGER in bytes (base64 PNGs) but cheaper in tokens: + // images bill by pixels (Anthropic: pixels/750), not by encoded length. So the + // after-estimate is remaining-text tokens + image tokens — never chars/4 of the + // new body. Provider-billed usage recorded per request stays the ground truth. + const imageTokensEst = info.imageTokens + || (info.imagePixels ? Math.round(info.imagePixels / 750) : (info.imageCount || 0) * 4761); + const summary = { + applied: true, + reason: "applied", + originalChars, + compressedBodyChars, + imagedChars, + imageCount: info.imageCount || 0, + imageBytes: info.imageBytes || 0, + tokensBeforeEst: info.baselineTokens || estTokens(originalChars), + tokensAfterEst: estTokens(Math.max(0, originalChars - imagedChars)) + imageTokensEst, + durationMs: Date.now() - startedAt, + cacheOwnsControl: result.cache?.ownsCacheControl === true, + }; + summary.tokensSavedEst = Math.max(0, summary.tokensBeforeEst - summary.tokensAfterEst); + summary.savedPct = summary.tokensBeforeEst > 0 + ? +((summary.tokensSavedEst / summary.tokensBeforeEst) * 100).toFixed(2) + : 0; + return { body: newBody, summary }; + } catch (e) { + return skipped("transform_error", { detail: e?.message || String(e), originalChars, durationMs: Date.now() - startedAt }); + } +} + +export function formatPxpipeLog(summary) { + if (!summary) return null; + if (!summary.applied) return null; + return `imaged ${summary.imagedChars}ch → ${summary.imageCount} image(s) | est ${summary.tokensBeforeEst}→${summary.tokensAfterEst} tokens (-${summary.savedPct}%) | ${summary.durationMs}ms`; +} diff --git a/open-sse/rtk/registry.js b/open-sse/rtk/registry.js index d9d9bf56..5378aabd 100644 --- a/open-sse/rtk/registry.js +++ b/open-sse/rtk/registry.js @@ -1,6 +1,7 @@ import { FILTERS } from "./constants.js"; import { gitDiff } from "./filters/gitDiff.js"; import { gitStatus } from "./filters/gitStatus.js"; +import { gitLog } from "./filters/gitLog.js"; import { grep } from "./filters/grep.js"; import { find } from "./filters/find.js"; import { dedupLog } from "./filters/dedupLog.js"; @@ -13,6 +14,7 @@ import { searchList } from "./filters/searchList.js"; const REGISTRY = { [FILTERS.GIT_DIFF]: gitDiff, [FILTERS.GIT_STATUS]: gitStatus, + [FILTERS.GIT_LOG]: gitLog, [FILTERS.GREP]: grep, [FILTERS.FIND]: find, [FILTERS.DEDUP_LOG]: dedupLog, diff --git a/open-sse/services/grokCliModels.js b/open-sse/services/grokCliModels.js new file mode 100644 index 00000000..58f5c216 --- /dev/null +++ b/open-sse/services/grokCliModels.js @@ -0,0 +1,127 @@ +import { + GROK_CLI_BASE_URL, + GROK_CLI_CLIENT_IDENTIFIER, + GROK_CLI_MODEL, + GROK_CLI_USER_AGENT, + GROK_CLI_VERSION, +} from "../config/grokCli.js"; +import { refreshProviderCredentials } from "./oauthCredentialManager.js"; +import { proxyAwareFetch } from "../utils/proxyFetch.js"; + +const MODELS_URL = `${GROK_CLI_BASE_URL}/models`; + +function modelEntries(data) { + const value = Array.isArray(data) ? data : data?.data ?? data?.models ?? data?.results ?? []; + if (Array.isArray(value)) return value.map((item) => [null, item]); + if (value && typeof value === "object") return Object.entries(value); + return []; +} + +export function parseGrokCliModels(data) { + const seen = new Set(); + const models = []; + + for (const [key, raw] of modelEntries(data)) { + const item = typeof raw === "string" ? { id: raw } : raw; + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const id = String( + item.id ?? item.model_id ?? item.modelId ?? item.model ?? item.slug ?? key ?? item.name ?? "", + ).trim(); + if (!id || seen.has(id)) continue; + seen.add(id); + + const model = { + ...item, + id, + name: item.display_name ?? item.displayName ?? item.name ?? id, + }; + const contextLength = Number( + item.context_length ?? item.contextLength ?? item.context_window ?? item.contextWindow, + ); + const maxOutputTokens = Number(item.max_output_tokens ?? item.maxOutputTokens); + if (Number.isFinite(contextLength) && contextLength > 0) model.contextLength = contextLength; + if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0) { + model.maxOutputTokens = maxOutputTokens; + } + if (id === GROK_CLI_MODEL) { + model.contextLength ||= 500000; + model.maxOutputTokens ||= 64000; + } + models.push(model); + } + + return models; +} + +function buildHeaders(accessToken, providerSpecificData = {}) { + const headers = { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "User-Agent": GROK_CLI_USER_AGENT, + "x-xai-token-auth": "xai-grok-cli", + "x-grok-client-version": GROK_CLI_VERSION, + "x-grok-client-identifier": GROK_CLI_CLIENT_IDENTIFIER, + "x-grok-client-mode": "headless", + }; + const email = providerSpecificData?.email; + const userId = providerSpecificData?.userId || providerSpecificData?.principalId; + if (email) headers["x-email"] = email; + if (userId) headers["x-userid"] = userId; + return headers; +} + +export async function resolveGrokCliModels(credentials, options = {}) { + const { + fetchFn = proxyAwareFetch, + log = console, + proxyOptions = null, + onCredentialsRefreshed, + } = options; + let accessToken = credentials?.accessToken; + if (!accessToken) return { models: [], warning: "Grok CLI access token is missing." }; + + const request = (token) => fetchFn( + MODELS_URL, + { + method: "GET", + headers: buildHeaders(token, credentials?.providerSpecificData), + }, + proxyOptions, + ); + + try { + let response = await request(accessToken); + if ((response.status === 401 || response.status === 403) && credentials?.refreshToken) { + const refreshed = await refreshProviderCredentials( + "grok-cli", + credentials, + log, + proxyOptions, + ); + if (refreshed?.accessToken) { + accessToken = refreshed.accessToken; + try { + await onCredentialsRefreshed?.(refreshed); + } catch (error) { + log?.warn?.("Grok CLI credential persistence failed", error); + } + response = await request(accessToken); + } + } + + if (!response.ok) { + const detail = await response.text().catch(() => ""); + return { + models: [], + warning: `Grok CLI model discovery failed (${response.status})${detail ? `: ${detail.slice(0, 160)}` : ""}`, + }; + } + + const models = parseGrokCliModels(await response.json()); + return models.length + ? { models } + : { models: [], warning: "Grok CLI returned no selectable models." }; + } catch (error) { + return { models: [], warning: `Grok CLI model discovery failed: ${error.message}` }; + } +} diff --git a/open-sse/services/model.js b/open-sse/services/model.js index 6558d707..5b88809c 100644 --- a/open-sse/services/model.js +++ b/open-sse/services/model.js @@ -17,6 +17,10 @@ for (const entry of REGISTRY) { for (const a of entry.aliases || []) ALIAS_TO_PROVIDER_ID[a] = entry.id; } +const BUILTIN_MODEL_ALIASES = { + "grok-build": "gcli/grok-build", +}; + /** * Resolve provider alias to provider ID */ @@ -104,7 +108,9 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) { : aliasesOrGetter; // Resolve alias - const resolved = resolveModelAliasFromMap(parsed.model, aliases); + const resolved = + resolveModelAliasFromMap(parsed.model, aliases) || + resolveModelAliasFromMap(parsed.model, BUILTIN_MODEL_ALIASES); if (resolved) { return resolved; } diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index f759493e..c8d264c6 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -129,6 +129,9 @@ const REFRESH_HANDLERS = { github: (c, log) => refreshGitHubToken(c.refreshToken, log), kiro: (c, log) => refreshKiroToken(c.refreshToken, c.providerSpecificData, log), xai: (c, log) => refreshXaiToken(c.refreshToken, log), + // Grok CLI shares xAI OAuth client + token endpoint (device-code tokens refresh the same way) + "grok-cli": (c, log) => refreshXaiToken(c.refreshToken, log), + gcli: (c, log) => refreshXaiToken(c.refreshToken, log), "codebuddy-cn": (c, log) => refreshCodebuddyToken(c.refreshToken, log), vertex: vertexRefreshHandler, "vertex-partner": vertexRefreshHandler @@ -187,6 +190,7 @@ export function formatProviderCredentials(provider, credentials, log) { case "openai": case "openrouter": case "xai": + case "grok-cli": return { apiKey: credentials.apiKey, accessToken: credentials.accessToken diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index c789d4b7..5331adb3 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -12,6 +12,7 @@ import { getKiroUsage } from "./usage/kiro.js"; import { getMiniMaxUsage } from "./usage/minimax.js"; import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.js"; import { getXaiUsage } from "./usage/xai.js"; +import { getGrokCliUsage } from "./usage/grok-cli.js"; import { getQwenUsage, getIflowUsage, @@ -45,6 +46,7 @@ const USAGE_HANDLERS = { "vercel-ai-gateway": (c) => getVercelAiGatewayUsage(c.apiKey, c.proxyOptions), "codebuddy-cn": (c) => getCodeBuddyCnUsage(c.accessToken, c.apiKey, c.providerSpecificData, c.proxyOptions), xai: (c) => getXaiUsage(c.accessToken, c.proxyOptions), + "grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions), }; export async function getUsageForProvider(connection, proxyOptions = null) { diff --git a/open-sse/services/usage/google.js b/open-sse/services/usage/google.js index 71c53e89..f0e4d23e 100644 --- a/open-sse/services/usage/google.js +++ b/open-sse/services/usage/google.js @@ -2,15 +2,15 @@ * Google usage handlers (Gemini CLI + Antigravity) */ -import { CLIENT_METADATA, getPlatformUserAgent } from "../../config/appConstants.js"; -import { ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js"; +import { CLIENT_METADATA } from "../../config/appConstants.js"; +import { ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_IDE_VERSION, ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js"; import { U, parseResetTime, normalizeCloudCodeProjectId, fetchWithTimeout } from "./shared.js"; // Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here const ANTIGRAVITY_CONFIG = { ...U("antigravity"), ...ANTIGRAVITY_OAUTH_CLIENT, - userAgent: getPlatformUserAgent(), + userAgent: ANTIGRAVITY_IDE_USER_AGENT, }; /** @@ -129,8 +129,7 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro "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 + "X-Client-Version": ANTIGRAVITY_IDE_VERSION, }, body: JSON.stringify({ ...(projectId ? { project: projectId } : {}) @@ -229,7 +228,6 @@ async function getAntigravitySubscriptionInfo(accessToken, proxyOptions = null) "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 }), }, 10000, proxyOptions); diff --git a/open-sse/services/usage/grok-cli.js b/open-sse/services/usage/grok-cli.js new file mode 100644 index 00000000..865baeb0 --- /dev/null +++ b/open-sse/services/usage/grok-cli.js @@ -0,0 +1,328 @@ +/** + * Grok CLI / Grok Build usage handler + * + * Source of truth: official grok-shell/grok-pager traffic to cli-chat-proxy.grok.com + * GET /v1/billing?format=credits + * GET /v1/user?include=subscription + * + * Observed billing shape (protobuf-json style `{ val: number }`): + * { + * config: { + * currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", start, end }, + * onDemandCap: { val }, + * onDemandUsed: { val }, + * prepaidBalance: { val }, + * isUnifiedBillingUser: true, + * billingPeriodStart, billingPeriodEnd + * } + * } + * + * Exhausted free/promo accounts return cap=0/used=0/prepaid=0 and chat 402s with + * personal-team-blocked:spending-limit. Paid/sub accounts surface non-zero cap + * or prepaidBalance; richer credit fields are parsed opportunistically if present. + */ + +import { proxyAwareFetch } from "../../utils/proxyFetch.js"; +import { U, parseResetTime, toFiniteNumber } from "./shared.js"; +import { + GROK_CLI_CLIENT_IDENTIFIER, + GROK_CLI_USER_AGENT, + GROK_CLI_VERSION, +} from "../../config/grokCli.js"; + +const USAGE = U("grok-cli"); +const BILLING_URL = USAGE.url || "https://cli-chat-proxy.grok.com/v1/billing?format=credits"; +const USER_URL = USAGE.userUrl || "https://cli-chat-proxy.grok.com/v1/user?include=subscription"; + +/** Unwrap protobuf-json `{ val: n }` or plain numbers/strings. */ +function unwrapVal(value, fallback = 0) { + if (value == null) return fallback; + if (typeof value === "object" && !Array.isArray(value) && "val" in value) { + return toFiniteNumber(value.val, fallback); + } + return toFiniteNumber(value, fallback); +} + +function buildGrokCliHeaders(accessToken, providerSpecificData = {}) { + const psd = providerSpecificData || {}; + const headers = { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "User-Agent": GROK_CLI_USER_AGENT, + "x-xai-token-auth": "xai-grok-cli", + "x-grok-client-identifier": GROK_CLI_CLIENT_IDENTIFIER, + "x-grok-client-version": GROK_CLI_VERSION, + "x-grok-client-mode": "headless", + }; + const email = psd.email; + const userId = psd.userId || psd.principalId; + if (email) headers["x-email"] = email; + if (userId) headers["x-userid"] = userId; + return headers; +} + +function subscriptionTier(user, config) { + const rawTier = + user?.subscriptionTier ?? + user?.subscription_tier ?? + user?.subscription?.tier ?? + config?.subscriptionTier ?? + config?.subscription_tier; + return typeof rawTier === "string" ? rawTier.trim() : ""; +} + +function resolvePlan(user, config) { + const tier = subscriptionTier(user, config); + if (tier) { + return tier + .replace(/[_-]+/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); + } + if (user?.hasGrokCodeAccess === true) return "Grok Code"; + if (config?.isUnifiedBillingUser === true) return "Grok Build"; + return "Grok Build"; +} + +function makeQuota({ used, total, resetAt, unlimited = false }) { + const safeTotal = Math.max(0, toFiniteNumber(total, 0)); + const safeUsed = Math.max(0, toFiniteNumber(used, 0)); + // Do NOT set absolute `remaining` — QuotaTable's getRemainingPercentage treats + // `remaining` as a 0–100 percentage (same trap as Qoder credits). + if (unlimited || safeTotal === 0) { + return { + used: safeUsed, + total: 0, + remainingPercentage: unlimited ? 100 : 0, + resetAt: resetAt || null, + unlimited: true, + }; + } + const remaining = Math.max(0, safeTotal - safeUsed); + const remainingPercentage = (remaining / safeTotal) * 100; + return { + used: safeUsed, + total: safeTotal, + remainingPercentage, + resetAt: resetAt || null, + unlimited: false, + }; +} + +/** + * Map billing JSON → normalized quotas object for the dashboard. + * Returns { quotas, periodEnd, exhaustedHint } or empty quotas when nothing usable. + */ +export function parseGrokCliBilling(billing, user = null) { + const root = billing && typeof billing === "object" ? billing : {}; + const config = + root.config && typeof root.config === "object" && !Array.isArray(root.config) + ? root.config + : root; + + const periodEnd = + parseResetTime(config.billingPeriodEnd) || + parseResetTime(config.billing_period_end) || + parseResetTime(config.currentPeriod?.end) || + parseResetTime(config.resetAt || config.resetsAt || config.periodEnd) || + parseResetTime(root.billingPeriodEnd) || + parseResetTime(root.billing_period_end) || + parseResetTime(root.resetAt || root.resetsAt || root.periodEnd) || + null; + + const quotas = {}; + const tier = subscriptionTier(user, config); + const subscriptionAccess = Boolean(tier) && !/^(free|none|null)$/i.test(tier); + + // Current Grok Build responses expose included monthly usage at top level. + const monthlyLimit = unwrapVal( + config.monthlyLimit ?? config.monthly_limit ?? root.monthlyLimit ?? root.monthly_limit, + NaN, + ); + const includedUsed = unwrapVal( + config.includedUsed ?? config.included_used ?? root.includedUsed ?? root.included_used, + NaN, + ); + const totalUsed = unwrapVal( + config.totalUsed ?? config.total_used ?? root.totalUsed ?? root.total_used, + NaN, + ); + if (Number.isFinite(monthlyLimit) && monthlyLimit > 0) { + quotas["Monthly included"] = makeQuota({ + used: Number.isFinite(includedUsed) + ? includedUsed + : Number.isFinite(totalUsed) + ? totalUsed + : 0, + total: monthlyLimit, + resetAt: periodEnd, + }); + } + + // Primary: on-demand spending window (subscription / promo credits) + const onDemandCap = unwrapVal(config.onDemandCap ?? root.onDemandCap, NaN); + const onDemandUsed = unwrapVal(config.onDemandUsed ?? root.onDemandUsed, NaN); + if (Number.isFinite(onDemandCap) && onDemandCap > 0) { + const used = Number.isFinite(onDemandUsed) ? Math.max(0, onDemandUsed) : 0; + quotas["On-demand"] = makeQuota({ + used, + total: onDemandCap, + resetAt: periodEnd, + }); + } else if ( + !subscriptionAccess && + Number.isFinite(onDemandCap) && + onDemandCap === 0 && + Number.isFinite(onDemandUsed) + ) { + // Cap 0 is the exhausted free/promo state (chat returns 402 spending-limit). + // UI treats total===0 as unlimited, so use a synthetic 1/1 depleted row. + quotas["On-demand"] = { + used: 1, + total: 1, + remainingPercentage: 0, + resetAt: periodEnd, + unlimited: false, + }; + } + + // Prepaid top-up balance (remaining credits; no fixed allotment known) + const prepaid = unwrapVal(config.prepaidBalance ?? root.prepaidBalance, NaN); + if (Number.isFinite(prepaid) && prepaid > 0) { + // Show full bar against the current balance (0 spent of this remaining pot). + quotas["Prepaid"] = { + used: 0, + total: prepaid, + remainingPercentage: 100, + resetAt: null, + unlimited: false, + }; + } + + // Opportunistic richer credit envelopes (future / other account types) + const creditBags = [ + root.credits, + root.creditBalance, + root.usage, + config.credits, + config.includedCredits, + config.subscriptionCredits, + ].filter((bag) => bag && typeof bag === "object" && !Array.isArray(bag)); + + for (const bag of creditBags) { + const total = unwrapVal( + bag.total ?? bag.limit ?? bag.cap ?? bag.allocation ?? bag.amount, + NaN, + ); + const used = unwrapVal(bag.used ?? bag.spent ?? bag.consumed, NaN); + const remaining = unwrapVal(bag.remaining ?? bag.balance ?? bag.left, NaN); + if (Number.isFinite(total) && total > 0) { + const resolvedUsed = Number.isFinite(used) + ? used + : Number.isFinite(remaining) + ? Math.max(0, total - remaining) + : 0; + if (!quotas.Credits) { + quotas.Credits = makeQuota({ + used: resolvedUsed, + total, + resetAt: parseResetTime(bag.resetAt || bag.resetsAt || bag.end) || periodEnd, + }); + } + } else if (Number.isFinite(remaining) && remaining >= 0 && !quotas.Credits) { + quotas.Credits = { + used: 0, + total: remaining > 0 ? remaining : 1, + remainingPercentage: remaining > 0 ? 100 : 0, + resetAt: periodEnd, + unlimited: false, + }; + } + } + + // Exhausted when every finite quota bar is at 0% remaining + const exhausted = + Object.keys(quotas).length > 0 && + Object.values(quotas).every( + (q) => q.unlimited !== true && (q.remainingPercentage ?? 100) <= 0, + ); + + return { + plan: resolvePlan(user, config), + quotas, + periodEnd, + exhausted, + subscriptionAccess, + rawConfig: config, + }; +} + +/** + * @param {string} accessToken + * @param {object|null} providerSpecificData + * @param {object|null} proxyOptions + */ +export async function getGrokCliUsage(accessToken, providerSpecificData = null, proxyOptions = null) { + if (!accessToken) { + return { message: "Grok CLI access token not available." }; + } + + const headers = buildGrokCliHeaders(accessToken, providerSpecificData); + + try { + // Fetch billing + user profile in parallel (same pattern as official CLI startup) + const [billingRes, userRes] = await Promise.all([ + proxyAwareFetch( + BILLING_URL, + { method: "GET", headers }, + proxyOptions, + ), + proxyAwareFetch( + USER_URL, + { method: "GET", headers }, + proxyOptions, + ).catch(() => null), + ]); + + if (billingRes.status === 401 || billingRes.status === 403) { + return { message: "Grok CLI authentication expired. Please re-authorize." }; + } + + if (!billingRes.ok) { + const errText = await billingRes.text().catch(() => ""); + const trimmed = errText ? `: ${errText.slice(0, 200)}` : ""; + return { message: `Grok CLI billing API error (${billingRes.status})${trimmed}` }; + } + + const billing = await billingRes.json().catch(() => null); + if (!billing || typeof billing !== "object") { + return { message: "Grok CLI billing response was not JSON." }; + } + + let user = null; + if (userRes?.ok) { + user = await userRes.json().catch(() => null); + } + + const parsed = parseGrokCliBilling(billing, user); + + if (!parsed.quotas || Object.keys(parsed.quotas).length === 0) { + return { + plan: parsed.plan, + message: parsed.subscriptionAccess + ? "Subscription access is active; Grok does not expose a numeric included quota." + : "Grok Build connected, but no credit allotment was returned. Free promo may be exhausted.", + quotas: {}, + }; + } + + // Dashboard hides QuotaTable whenever `message` is set, so only attach a + // message when there are no quota rows to render. Depleted accounts keep + // the 0% On-demand bar without a blocking message. + return { + plan: parsed.plan, + quotas: parsed.quotas, + }; + } catch (error) { + return { message: `Grok CLI usage error: ${error.message}` }; + } +} diff --git a/open-sse/translator/concerns/paramSupport.js b/open-sse/translator/concerns/paramSupport.js index dc030194..e222b23f 100644 --- a/open-sse/translator/concerns/paramSupport.js +++ b/open-sse/translator/concerns/paramSupport.js @@ -1,17 +1,26 @@ +import { getCapabilitiesForModel } from "../../providers/capabilities.js"; + // Strip request params a given provider/model rejects upstream (e.g. HTTP 400). // Config-driven: add a rule instead of scattering `delete body.x` across executors. // Each rule: optional provider, regex match on model, list of params to drop. // A param is removed only when it is present (!== undefined). const STRIP_RULES = [ - // claude-opus-4 series: temperature is deprecated (Anthropic 400). #1748 - { match: /claude-opus-4/i, drop: ["temperature"] }, + // All Claude models: temperature deprecated/rejected upstream (Anthropic 400). #1748 + { match: /claude/i, drop: ["temperature"] }, // GitHub Copilot gpt-5.4: temperature unsupported. { provider: "github", match: /gpt-5\.4/i, drop: ["temperature"] }, // GitHub Copilot Claude (except opus/sonnet 4.6): thinking + reasoning_effort rejected. #713 { provider: "github", match: (m) => /claude/i.test(m) && !/claude.*(opus|sonnet).*4\.6/i.test(m), drop: ["thinking", "reasoning_effort"] }, // Cloudflare Workers AI: content must be plain string, rejects OpenAI content-part array (#1926) { provider: "cloudflare-ai", flattenContent: true }, + { provider: "volcengine-ark", match: /glm-5/i, clampToModelMaxOutput: true }, + // VolcEngine Ark caps the Kimi family at max_tokens <= 32768, but the model's + // advertised ceiling is far higher (Kimi-K2.7-Code resolves to maxOutput 262144), + // so clampToModelMaxOutput alone leaves it uncapped and the request 400s with + // "integer above maximum value, expected <= 32768". Pin an explicit endpoint cap; + // min() with the model ceiling still applies if a variant's own limit is lower. + { provider: "volcengine-ark", match: /kimi/i, maxOutputCap: 32768, clampToModelMaxOutput: true }, ]; // Test a rule's match (regex or predicate) against the model id. @@ -20,6 +29,12 @@ function matches(rule, model) { return typeof rule.match === "function" ? rule.match(model) : rule.match.test(model); } +function clampNumber(body, key, ceiling) { + if (typeof body[key] === "number" && Number.isFinite(body[key]) && body[key] > ceiling) { + body[key] = ceiling; + } +} + // Remove unsupported params from body in place; returns body. export function stripUnsupportedParams(provider, model, body) { if (!model || !body || typeof body !== "object") return body; @@ -39,6 +54,22 @@ export function stripUnsupportedParams(provider, model, body) { } } } + if (rule.clampToModelMaxOutput || Number.isFinite(rule.maxOutputCap)) { + const modelCeiling = getCapabilitiesForModel(provider, model).maxOutput; + const candidates = []; + if (rule.clampToModelMaxOutput && Number.isFinite(modelCeiling) && modelCeiling > 0) { + candidates.push(modelCeiling); + } + if (Number.isFinite(rule.maxOutputCap) && rule.maxOutputCap > 0) { + candidates.push(rule.maxOutputCap); + } + if (candidates.length > 0) { + const ceiling = Math.min(...candidates); + clampNumber(body, "max_tokens", ceiling); + clampNumber(body, "max_completion_tokens", ceiling); + clampNumber(body, "max_output_tokens", ceiling); + } + } } return body; } diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index 1cf44384..ee0b86aa 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -20,6 +20,13 @@ const FORMAT_TO_NATIVE = { kiro: "kiro", }; +// Strip a trailing thinking suffix "model(value)" → "model" (no-op when absent). +export function stripThinkingSuffix(model) { + if (typeof model !== "string") return model; + const m = model.match(/^(.*)\([^()]+\)\s*$/); + return m ? m[1].trim() : model; +} + // Parse model-name suffix "model(value)" → { cleanModel, override }. // value: level name (high) | number (8192) | auto | none. null override when absent. export function parseSuffix(model) { @@ -132,18 +139,66 @@ function toGeminiThinkingLevel(cfg) { return effortToThinkingLevel(raw); } +function toKimiReasoningEffort(cfg) { + const level = toLevel(cfg); + if (level === "auto") return "high"; + if (level === "minimal") return "low"; + if (level === "xhigh") return "max"; + if (["low", "medium", "high", "max"].includes(level)) return level; + return null; +} + +const GEMINI_LEVEL_OUTPUT_FLOOR = { + minimal: 4096, + low: 8192, + medium: 16384, + high: 65535, +}; + +function geminiBudgetOutputFloor(budget) { + if (budget === -1) return 32768; + if (!Number.isFinite(budget)) return 32768; + if (budget <= 1024) return 8192; + if (budget <= 8192) return 16384; + if (budget <= 24576) return 32768; + return 65535; +} + +function geminiLevelOutputFloor(level) { + return GEMINI_LEVEL_OUTPUT_FLOOR[level] || GEMINI_LEVEL_OUTPUT_FLOOR.high; +} + // Gemini nests thinkingConfig under generationConfig. gemini-cli / antigravity wrap // the whole request in a { request: { generationConfig } } envelope — target the // envelope's generationConfig when present, else the top-level one. +function getGeminiGenerationConfig(body) { + if (body.request && typeof body.request === "object") { + if (!body.request.generationConfig || typeof body.request.generationConfig !== "object") { + body.request.generationConfig = {}; + } + return body.request.generationConfig; + } + if (!body.generationConfig || typeof body.generationConfig !== "object") { + body.generationConfig = {}; + } + return body.generationConfig; +} + function setGeminiThinking(body, tc) { - const gc = body.request?.generationConfig - ? body.request.generationConfig - : (body.generationConfig && typeof body.generationConfig === "object" - ? body.generationConfig - : (body.generationConfig = {})); + const gc = getGeminiGenerationConfig(body); gc.thinkingConfig = tc; } +function ensureGeminiOutputFloor(body, floor, caps) { + const cap = Number.isFinite(caps?.maxOutput) ? caps.maxOutput : floor; + const target = Math.min(floor, cap); + const gc = getGeminiGenerationConfig(body); + const current = Number(gc.maxOutputTokens); + if (!Number.isFinite(current) || current < target) { + gc.maxOutputTokens = target; + } +} + // Strip every known thinking field from a body (used before re-applying / when unsupported). function stripAll(body) { delete body.thinking; @@ -168,11 +223,18 @@ function applyFormat(fmt, body, cfg, caps) { case "openai": { if (none && canDisable) { body.reasoning_effort = "none"; break; } const level = toLevel(eff); - if (level) body.reasoning_effort = level; + // OpenAI reasoning_effort enum caps at "xhigh" (no "max"); clamp Claude Code's "max". + if (level) body.reasoning_effort = level === "max" ? "xhigh" : level; break; } case "claude-adaptive": { if (none && canDisable) { body.thinking = { type: "disabled" }; break; } + // output_config.effort alone does NOT turn thinking on: Anthropic requires + // an explicit thinking:{type:"adaptive"} on Opus 4.6/4.7/4.8 and Sonnet 4.6 + // ("thinking is off unless you explicitly set it"), and Anthropic-compatible + // shims (e.g. GitHub Copilot /v1/messages) default thinking off even for + // Sonnet 5. Send both fields — the documented adaptive-thinking shape. + body.thinking = { type: "adaptive" }; const level = toLevel(eff); body.output_config = { effort: level === "xhigh" ? "high" : level }; break; @@ -186,12 +248,14 @@ function applyFormat(fmt, body, cfg, caps) { case "gemini-level": { const level = none ? "minimal" : toGeminiThinkingLevel(eff); setGeminiThinking(body, { thinkingLevel: level, includeThoughts: level !== "minimal" }); + ensureGeminiOutputFloor(body, geminiLevelOutputFloor(level), caps); break; } case "gemini-budget": { if (none && canDisable) { setGeminiThinking(body, { thinkingBudget: 0, includeThoughts: false }); break; } const budget = toBudget(eff, caps.thinkingRange); setGeminiThinking(body, { thinkingBudget: budget ?? -1, includeThoughts: true }); + ensureGeminiOutputFloor(body, geminiBudgetOutputFloor(budget ?? -1), caps); break; } case "zai": { @@ -217,8 +281,8 @@ function applyFormat(fmt, body, cfg, caps) { } case "kimi": { if (none && canDisable) { body.thinking = { type: "disabled" }; break; } - const level = toLevel(eff); - if (level) body.reasoning_effort = level === "max" ? "high" : level; + const effort = toKimiReasoningEffort(eff); + if (effort) body.reasoning_effort = effort; break; } case "minimax": { diff --git a/open-sse/translator/formats/claude.js b/open-sse/translator/formats/claude.js index ec6e6c47..adbd3ce8 100644 --- a/open-sse/translator/formats/claude.js +++ b/open-sse/translator/formats/claude.js @@ -192,10 +192,27 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne delete body.output_config; } - // Clamp max_tokens to the model output ceiling (never above DEFAULT_MAX_TOKENS) + // Clamp max_tokens to the model's real output ceiling. Models whose caps + // declare a higher maxOutput (e.g. Opus 4.8 / Sonnet 4.6 = 128000) are allowed + // up to it, so max-effort thinking gets full budget; others fall back to the + // conservative 64000 default. if (body.max_tokens) { - const ceiling = Math.min(getCapabilitiesForModel(provider, body.model).maxOutput, DEFAULT_MAX_TOKENS); + const ceiling = getCapabilitiesForModel(provider, body.model).maxOutput || DEFAULT_MAX_TOKENS; if (body.max_tokens > ceiling) body.max_tokens = ceiling; + + // Reconcile against thinking budget. applyThinking (thinkingUnified.js) runs + // AFTER adjustMaxTokens capped max_tokens, and the claude-budget format maps + // max effort → budget_tokens 128000 — larger than the clamped max_tokens. + // Anthropic requires max_tokens strictly greater than budget_tokens (else 400). + // Prefer raising max_tokens to preserve the requested thinking depth; if the + // budget alone meets/exceeds the ceiling, cap output and shrink the budget so + // some tokens remain for the answer. + if (body.thinking?.type === "enabled" && body.thinking.budget_tokens && body.thinking.budget_tokens >= body.max_tokens) { + body.max_tokens = Math.min(body.thinking.budget_tokens + 1024, ceiling); + if (body.thinking.budget_tokens >= body.max_tokens) { + body.thinking.budget_tokens = Math.max(1024, body.max_tokens - 1024); + } + } } // 1. System: remove all cache_control, add only to last block with ttl 1h diff --git a/open-sse/translator/formats/maxTokens.js b/open-sse/translator/formats/maxTokens.js index 0e5b36f2..4d2cd209 100644 --- a/open-sse/translator/formats/maxTokens.js +++ b/open-sse/translator/formats/maxTokens.js @@ -3,9 +3,13 @@ import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../config/runtimeConf /** * Adjust max_tokens based on request context * @param {object} body - Request body + * @param {number} [ceiling=DEFAULT_MAX_TOKENS] - Upper bound for max_tokens. + * Callers with model context (e.g. openai-to-claude) pass the model's real + * maxOutput so high-output models (Opus 4.8 = 128000) aren't pre-clamped to + * the conservative 64000 default before the model-aware step sees them. * @returns {number} Adjusted max_tokens */ -export function adjustMaxTokens(body) { +export function adjustMaxTokens(body, ceiling = DEFAULT_MAX_TOKENS) { let maxTokens = body.max_tokens || DEFAULT_MAX_TOKENS; // Auto-increase for tool calling to prevent truncated arguments (min never above max) @@ -16,14 +20,14 @@ export function adjustMaxTokens(body) { } // Ensure max_tokens > thinking.budget_tokens (Claude API requirement) - // Claude API requires strictly greater, so add buffer instead of using DEFAULT_MAX_TOKENS - // which could equal budget_tokens when budget_tokens >= 64000 + // Claude API requires strictly greater, so add buffer instead of using the + // ceiling which could equal budget_tokens when budget_tokens >= ceiling if (body.thinking?.budget_tokens && maxTokens <= body.thinking.budget_tokens) { maxTokens = body.thinking.budget_tokens + 1024; } - // Never exceed the global ceiling - if (maxTokens > DEFAULT_MAX_TOKENS) maxTokens = DEFAULT_MAX_TOKENS; + // Never exceed the ceiling + if (maxTokens > ceiling) maxTokens = ceiling; return maxTokens; } diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index e9c84971..37e5bde6 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -103,8 +103,16 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream } } - // Normalize thinking to the target provider-native format (config-driven, capability-aware) - applyThinking(targetFormat, model, result, provider, thinkingIntent); + // Normalize thinking to the target provider-native format (config-driven, capability-aware). + // Kiro's GenerateAssistantResponse request does not accept the generic top-level + // `thinking` field; its translators map thinking intent to KAS-compatible + // systemPrompt/additionalModelRequestFields instead. + const kiroThinkingMappedByTranslator = + targetFormat === FORMATS.KIRO && + (sourceFormat === FORMATS.OPENAI || sourceFormat === FORMATS.CLAUDE); + if (!kiroThinkingMappedByTranslator) { + applyThinking(targetFormat, model, result, provider, thinkingIntent); + } // Always normalize to clean OpenAI format when target is OpenAI // This handles hybrid requests (e.g., OpenAI messages + Claude tools) diff --git a/open-sse/translator/request/claude-to-kiro.js b/open-sse/translator/request/claude-to-kiro.js index 5d891c11..bcb8b97f 100644 --- a/open-sse/translator/request/claude-to-kiro.js +++ b/open-sse/translator/request/claude-to-kiro.js @@ -24,13 +24,15 @@ */ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; -import { v4 as uuidv4 } from "uuid"; +import { applyKiroSessionReplay } from "../../utils/kiroSessionReplay.js"; +import { resolveContinuationId, resolveSessionIdentity } from "../../utils/sessionManager.js"; import { resolveKiroModel, resolveKiroThinkingBudget, buildThinkingSystemPrefix, KIRO_AGENTIC_SYSTEM_PROMPT, resolveDefaultProfileArn, + buildKiroAdditionalModelRequestFieldsForModel, } from "../../config/kiroConstants.js"; import { DEFAULT_IMAGE_MIME } from "../schema/index.js"; import { ROLE, CLAUDE_BLOCK } from "../schema/index.js"; @@ -363,6 +365,18 @@ function reconcileOrphanedToolResults(history, currentMessage) { } } +function extractClaudeSystemText(system) { + if (!system) return ""; + if (typeof system === "string") return system; + if (Array.isArray(system)) { + return system.map((s) => { + if (typeof s === "string") return s; + return s?.text || ""; + }).filter(Boolean).join("\n"); + } + return ""; +} + /** * Build a Kiro payload directly from a Claude Messages API request body. */ @@ -402,50 +416,75 @@ export function claudeToKiroRequest(model, body, stream, credentials) { ? (credentials?.providerSpecificData?.profileArn || "") : (credentials?.providerSpecificData?.profileArn || resolveDefaultProfileArn(authMethod)); - let finalContent = currentMessage?.userInputMessage?.content || ""; - - // System prompt → prepend to the user content. - if (body.system) { - let systemText = ""; - if (typeof body.system === "string") { - systemText = body.system; - } else if (Array.isArray(body.system)) { - systemText = body.system.map((s) => s.text || "").join("\n"); - } - if (systemText) finalContent = `${systemText}\n\n${finalContent}`; - } - - // Prefix order: thinking_mode tag, timestamp marker, then agentic prompt. + // Kiro CLI/KAS sends system prompt as top-level `systemPrompt`. Keep a + // content fallback too because the CodeWhisperer surface does not always + // enforce top-level systemPrompt for direct calls. const timestamp = new Date().toISOString(); - const prefixParts = []; - if (thinkingBudget !== null) prefixParts.push(buildThinkingSystemPrefix(thinkingBudget)); - prefixParts.push(`[Context: Current time is ${timestamp}]`); - if (agentic) prefixParts.push(KIRO_AGENTIC_SYSTEM_PROMPT); - finalContent = `${prefixParts.join("\n\n")}\n\n${finalContent}`; + const systemPromptParts = []; + if (thinkingBudget !== null) systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget)); + if (agentic) systemPromptParts.push(KIRO_AGENTIC_SYSTEM_PROMPT); + const systemInstruction = extractClaudeSystemText(body.system); + if (systemInstruction) systemPromptParts.push(systemInstruction); + const systemPrompt = systemPromptParts.filter(Boolean).join("\n\n"); + const currentTimeContext = `[Context: Current time is ${timestamp}]`; + const contentPrefix = [systemPrompt, currentTimeContext].filter(Boolean).join("\n\n"); + + const sessionIdentity = resolveSessionIdentity({ + headers: credentials?.rawHeaders, + body, + connectionId: credentials?.connectionId, + scope: "kiro", + }); + const conversationId = sessionIdentity.sessionId; + const continuationId = resolveContinuationId({ + sessionId: conversationId, + connectionId: credentials?.connectionId, + scope: "kiro", + ephemeral: sessionIdentity.ephemeral, + }); + const replay = applyKiroSessionReplay({ + conversationId, + connectionId: credentials?.connectionId, + modelId: upstreamModel, + systemPrompt, + contentPrefix, + currentContentPrefix: currentTimeContext, + history, + currentMessage, + }); + const replayCurrent = replay.currentMessage?.userInputMessage || {}; + const userInputMessage = { + content: replayCurrent.content || "", + modelId: upstreamModel, + origin: "AI_EDITOR", + ...(replayCurrent.userInputMessageContext && { + userInputMessageContext: replayCurrent.userInputMessageContext, + }), + ...(replayCurrent.images && { + images: replayCurrent.images, + }), + }; const payload = { conversationState: { chatTriggerType: "MANUAL", - conversationId: uuidv4(), + conversationId, + agentContinuationId: continuationId, + agentTaskType: "vibe", currentMessage: { - userInputMessage: { - content: finalContent, - modelId: upstreamModel, - origin: "AI_EDITOR", - ...(currentMessage?.userInputMessage?.userInputMessageContext && { - userInputMessageContext: - currentMessage.userInputMessage.userInputMessageContext, - }), - ...(currentMessage?.userInputMessage?.images && { - images: currentMessage.userInputMessage.images, - }), - }, + userInputMessage, }, - history, + history: replay.history, }, + agentMode: "vibe", }; if (profileArn) payload.profileArn = profileArn; + if (systemPrompt) payload.systemPrompt = systemPrompt; + const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel); + if (additionalModelRequestFields) { + payload.additionalModelRequestFields = additionalModelRequestFields; + } if (maxTokens || temperature !== undefined || topP !== undefined) { payload.inferenceConfig = {}; diff --git a/open-sse/translator/request/claude-to-openai.js b/open-sse/translator/request/claude-to-openai.js index c6e92ed6..3956f828 100644 --- a/open-sse/translator/request/claude-to-openai.js +++ b/open-sse/translator/request/claude-to-openai.js @@ -129,14 +129,15 @@ function fixMissingToolResponsesOpenAI(messages) { } } -// Wrap mid-conversation system text so it ends as a user turn (avoids Anthropic prefill 400) +// Wrap mid-conversation system text so it ends as a user turn (avoids Anthropic prefill 400). +// Uses tags that Claude models treat as authoritative directives. function systemReminderText(content) { const parts = Array.isArray(content) ? content.filter(c => c?.type === CLAUDE_BLOCK.TEXT).map(c => c.text || "") : [typeof content === "string" ? content : ""]; const text = parts.filter(Boolean).join("\n"); if (!text.trim()) return ""; - return `\n${text}\n`; + return `\n${text}\n`; } // Convert single Claude message - returns single message or array of messages diff --git a/open-sse/translator/request/openai-responses.js b/open-sse/translator/request/openai-responses.js index 98c516cd..1fc05825 100644 --- a/open-sse/translator/request/openai-responses.js +++ b/open-sse/translator/request/openai-responses.js @@ -31,11 +31,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) let currentAssistantMsg = null; let pendingToolResults = []; let pendingReasoning = ""; + let pendingReasoningEncrypted = ""; const inputItems = normalizeResponsesInput(body.input); if (!inputItems) return body; - // Extract reasoning text from summary[].text or encrypted_content fallback + // Extract reasoning text from summary[].text (encrypted_content is continuity-only) const extractReasoningText = (item) => { if (Array.isArray(item.summary)) { const txt = item.summary.map(s => s?.text || "").filter(Boolean).join("\n"); @@ -48,6 +49,13 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) return ""; }; + const attachPendingReasoning = (msg) => { + if (pendingReasoning) msg.reasoning_content = pendingReasoning; + if (pendingReasoningEncrypted) msg.encrypted_content = pendingReasoningEncrypted; + pendingReasoning = ""; + pendingReasoningEncrypted = ""; + }; + 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 @@ -80,11 +88,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) }) : item.content; const msg = { role: item.role, content }; - // Attach buffered reasoning to assistant turn (required by xiaomi-mimo thinking mode) - if (item.role === ROLE.ASSISTANT && pendingReasoning) { - msg.reasoning_content = pendingReasoning; + // Attach buffered reasoning to assistant turn (required by xiaomi-mimo + store=false continuity) + if (item.role === ROLE.ASSISTANT) attachPendingReasoning(msg); + else { + pendingReasoning = ""; + pendingReasoningEncrypted = ""; } - pendingReasoning = ""; result.messages.push(msg); } else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) { @@ -95,10 +104,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) content: null, tool_calls: [] }; - if (pendingReasoning) { - currentAssistantMsg.reasoning_content = pendingReasoning; - pendingReasoning = ""; - } + attachPendingReasoning(currentAssistantMsg); } // Skip items with empty/missing name — Codex/OpenAI reject nameless tool calls (#444) if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue; @@ -132,9 +138,15 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) }); } else if (itemType === RESPONSES_ITEM.REASONING) { - // Buffer reasoning text; attached to next assistant message/function_call + // Buffer reasoning text; attached to next assistant message/function_call. + // Also stash encrypted_content so a later openai→responses hop can restore + // the store=false continuity blob (Grok CLI / Codex multi-turn). const txt = extractReasoningText(item); if (txt) pendingReasoning = pendingReasoning ? `${pendingReasoning}\n${txt}` : txt; + if (typeof item.encrypted_content === "string" && item.encrypted_content) { + // Prefer attaching to the next assistant message we create + pendingReasoningEncrypted = item.encrypted_content; + } continue; } } @@ -189,6 +201,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) delete result.prompt_cache_key; delete result.store; delete result.reasoning; + delete result.client_metadata; return result; } @@ -202,6 +215,43 @@ function normalizeToolParameters(params) { return params; } +/** + * Build a Responses `reasoning` input item from Chat Completions assistant fields. + * Preserves encrypted blobs needed by store=false multi-turn (Grok CLI / Codex). + * Returns null when the message has nothing useful to re-send. + */ +function buildReasoningInputItem(msg) { + if (!msg || typeof msg !== "object") return null; + + const encrypted = + (typeof msg.encrypted_content === "string" && msg.encrypted_content) || + (typeof msg.reasoning_encrypted_content === "string" && msg.reasoning_encrypted_content) || + (typeof msg.reasoning?.encrypted_content === "string" && msg.reasoning.encrypted_content) || + ""; + + let summaryText = ""; + if (typeof msg.reasoning_content === "string" && msg.reasoning_content.trim()) { + summaryText = msg.reasoning_content; + } else if (typeof msg.reasoning === "string" && msg.reasoning.trim()) { + summaryText = msg.reasoning; + } else if (Array.isArray(msg.reasoning_details)) { + summaryText = msg.reasoning_details + .map((d) => (typeof d?.text === "string" ? d.text : typeof d?.content === "string" ? d.content : "")) + .filter(Boolean) + .join("\n"); + } + + if (!encrypted && !summaryText) return null; + + const item = { type: RESPONSES_ITEM.REASONING }; + if (summaryText) { + item.summary = [{ type: RESPONSES_ITEM.SUMMARY_TEXT, text: summaryText }]; + } + // encrypted_content is the continuity token for store=false backends + if (encrypted) item.encrypted_content = encrypted; + return item; +} + /** * Convert OpenAI Chat Completions to OpenAI Responses API format */ @@ -221,17 +271,26 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) const messages = body.messages || []; for (const msg of messages) { - if (msg.role === ROLE.SYSTEM) { - // Use first system message as instructions + if (msg.role === ROLE.SYSTEM || msg.role === ROLE.DEVELOPER) { + // Use the first instruction-bearing message as instructions. + // OpenAI recommends role="developer" for GPT-5/Codex as the system-level prompt. if (!hasSystemMessage) { result.instructions = typeof msg.content === "string" ? msg.content : ""; hasSystemMessage = true; } - continue; // Skip system messages in input + continue; // Skip instruction messages in input } // Convert user/assistant messages to input items if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) { + // Multi-turn continuity for store=false Responses backends (Codex / Grok CLI): + // re-emit a reasoning item before the assistant message when the chat-format + // history carried reasoning text and/or encrypted_content from a prior turn. + if (msg.role === ROLE.ASSISTANT) { + const reasoningItem = buildReasoningInputItem(msg); + if (reasoningItem) result.input.push(reasoningItem); + } + const contentType = msg.role === ROLE.USER ? RESPONSES_ITEM.INPUT_TEXT : RESPONSES_ITEM.OUTPUT_TEXT; const content = typeof msg.content === "string" ? [{ type: contentType, text: msg.content }] diff --git a/open-sse/translator/request/openai-to-claude.js b/open-sse/translator/request/openai-to-claude.js index bc73149b..580debfe 100644 --- a/open-sse/translator/request/openai-to-claude.js +++ b/open-sse/translator/request/openai-to-claude.js @@ -6,6 +6,7 @@ import { safeParseJSON } from "../concerns/json.js"; import { parseDataUri } from "../concerns/image.js"; import { extractTextContent } from "../formats/gemini.js"; import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js"; +import { getCapabilitiesForModel } from "../../providers/capabilities.js"; // Empty prefix matches real Claude Code behavior (no tool name prefix). // Previously "proxy_" was used but this is a detectable fingerprint difference. @@ -15,9 +16,13 @@ const CLAUDE_OAUTH_TOOL_PREFIX = ""; export function openaiToClaudeRequest(model, body, stream) { // Tool name mapping for Claude OAuth (capitalizedName → originalName) const toolNameMap = new Map(); + // Cap max_tokens at the model's real output ceiling (e.g. Opus 4.8 = 128000), + // not the conservative 64000 default — otherwise a high-output model is + // pre-clamped here before prepareClaudeRequest's model-aware step runs. + const modelCeiling = getCapabilitiesForModel(null, model).maxOutput || undefined; const result = { model: model, - max_tokens: adjustMaxTokens(body), + max_tokens: adjustMaxTokens(body, modelCeiling), stream: stream }; @@ -148,7 +153,15 @@ Respond ONLY with the JSON object, no other text.`); continue; } - const toolData = toolType === OPENAI_BLOCK.FUNCTION && tool.function ? tool.function : tool; + // Function-shaped tools arrive in two flavors from real clients: + // (a) openai-spec: { type: "function", function: { name, ... } } + // (b) legacy/loose: { function: { name, ... } } (no parent `type`) + // Both must yield toolData.name = "echo". Treat the bare-function shape + // as a function tool too — Anthropic-compatible gateways (notably + // MiniMax M3 at api.minimaxi.com) reject payloads where this branch + // falls through with `toolData.name === undefined`, returning their + // upstream code (2013) "invalid tool type". See #2435. + const toolData = tool.function ?? tool; const originalName = toolData.name; // Claude OAuth requires prefixed tool names to avoid conflicts diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index afb3effc..9e029e2c 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -1,7 +1,6 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; import { DEFAULT_THINKING_AG_SIGNATURE, DEFAULT_THINKING_GEMINI_CLI_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(); @@ -282,31 +281,17 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra // 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: GEMINI_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; } + if (geminiCLI.tools?.length > 0) { + envelope.request.toolConfig = { + functionCallingConfig: { mode: "VALIDATED" } + }; + } + return envelope; } @@ -414,12 +399,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu } } - // 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]` } - ]; - + const systemParts = []; // Merge user system prompt from claudeRequest if (claudeRequest.system) { if (Array.isArray(claudeRequest.system)) { @@ -431,10 +411,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu } } - // Merge existing systemInstruction parts (from contents conversion) - if (envelope.request.systemInstruction?.parts) { - envelope.request.systemInstruction.parts.unshift(...systemParts); - } else { + if (systemParts.length > 0) { envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts }; } @@ -463,4 +440,3 @@ export function openaiToAntigravityRequest(model, body, stream, credentials = nu 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/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index ee886666..a2681b5a 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -5,13 +5,15 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; import { v4 as uuidv4 } from "uuid"; -import { resolveSessionId } from "../../utils/sessionManager.js"; +import { applyKiroSessionReplay } from "../../utils/kiroSessionReplay.js"; +import { resolveContinuationId, resolveSessionIdentity } from "../../utils/sessionManager.js"; import { resolveKiroModel, resolveKiroThinkingBudget, buildThinkingSystemPrefix, KIRO_AGENTIC_SYSTEM_PROMPT, - resolveDefaultProfileArn + resolveDefaultProfileArn, + buildKiroAdditionalModelRequestFieldsForModel } from "../../config/kiroConstants.js"; import { parseDataUri } from "../concerns/image.js"; import { DEFAULT_IMAGE_MIME } from "../schema/index.js"; @@ -270,6 +272,7 @@ function convertMessages(messages, tools, model) { let role = msg.role; // Normalize: system/tool -> user + const wasSystem = role === ROLE.SYSTEM; if (role === ROLE.SYSTEM || role === ROLE.TOOL) { role = ROLE.USER; } @@ -338,7 +341,10 @@ function convertMessages(messages, tools, model) { content: [{ text: toolContent }] }); } else if (content) { - pendingUserContent.push(content); + // tags: Claude models treat these as authoritative directives. + pendingUserContent.push( + wasSystem ? `\n${content}\n` : content + ); } } else if (role === ROLE.ASSISTANT) { // Extract text content and tool uses @@ -542,47 +548,74 @@ export function openaiToKiroRequest(model, body, stream, credentials) { ? (credentials?.providerSpecificData?.profileArn || "") : (credentials?.providerSpecificData?.profileArn || resolveDefaultProfileArn(authMethod)); - let finalContent = currentMessage?.userInputMessage?.content || ""; - const timestamp = new Date().toISOString(); - // Build the system-prompt prefix that goes ABOVE the user message body. - // Order: thinking_mode tag first (so Kiro sees it before any user text), - // then context/timestamp marker, then optional agentic chunked-write prompt. - const prefixParts = []; + // Kiro CLI/KAS sends these as top-level systemPrompt. Keep a content fallback + // too because the CodeWhisperer surface does not always enforce top-level + // systemPrompt for direct calls. + const systemPromptParts = []; if (thinkingBudget !== null) { - prefixParts.push(buildThinkingSystemPrefix(thinkingBudget)); + systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget)); } - prefixParts.push(`[Context: Current time is ${timestamp}]`); if (agentic) { - prefixParts.push(KIRO_AGENTIC_SYSTEM_PROMPT); + systemPromptParts.push(KIRO_AGENTIC_SYSTEM_PROMPT); } - finalContent = `${prefixParts.join("\n\n")}\n\n${finalContent}`; + const systemPrompt = systemPromptParts.filter(Boolean).join("\n\n"); + const currentTimeContext = `[Context: Current time is ${timestamp}]`; + const contentPrefix = [systemPrompt, currentTimeContext].filter(Boolean).join("\n\n"); + + const sessionIdentity = resolveSessionIdentity({ headers: credentials?.rawHeaders, body, connectionId: credentials?.connectionId, scope: "kiro" }); + const conversationId = sessionIdentity.sessionId; + const continuationId = resolveContinuationId({ + sessionId: conversationId, + connectionId: credentials?.connectionId, + scope: "kiro", + ephemeral: sessionIdentity.ephemeral, + }); + const replay = applyKiroSessionReplay({ + conversationId, + connectionId: credentials?.connectionId, + modelId: upstreamModel, + systemPrompt, + contentPrefix, + currentContentPrefix: currentTimeContext, + history, + currentMessage, + }); + const replayCurrent = replay.currentMessage?.userInputMessage || {}; const payload = { conversationState: { chatTriggerType: "MANUAL", - conversationId: resolveSessionId({ headers: credentials?.rawHeaders, body, connectionId: credentials?.connectionId, scope: "kiro" }), + conversationId, + agentContinuationId: continuationId, + agentTaskType: "vibe", currentMessage: { userInputMessage: { - content: finalContent, + content: replayCurrent.content || "", modelId: upstreamModel, origin: "AI_EDITOR", - ...(currentMessage?.userInputMessage?.images?.length > 0 && { - images: currentMessage.userInputMessage.images + ...(replayCurrent.images?.length > 0 && { + images: replayCurrent.images }), - ...(currentMessage?.userInputMessage?.userInputMessageContext && { - userInputMessageContext: currentMessage.userInputMessage.userInputMessageContext + ...(replayCurrent.userInputMessageContext && { + userInputMessageContext: replayCurrent.userInputMessageContext }) } }, - history: history - } + history: replay.history + }, + agentMode: "vibe", }; if (profileArn) { payload.profileArn = profileArn; } + if (systemPrompt) payload.systemPrompt = systemPrompt; + const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel); + if (additionalModelRequestFields) { + payload.additionalModelRequestFields = additionalModelRequestFields; + } if (maxTokens || temperature !== undefined || topP !== undefined) { payload.inferenceConfig = {}; diff --git a/open-sse/utils/kiroSessionReplay.js b/open-sse/utils/kiroSessionReplay.js new file mode 100644 index 00000000..11cae9cd --- /dev/null +++ b/open-sse/utils/kiroSessionReplay.js @@ -0,0 +1,125 @@ +import { MEMORY_CONFIG } from "../config/runtimeConfig.js"; + +const sessionStartStore = new Map(); +const MAX_SESSION_STARTS = 5000; + +function clone(value) { + return value == null ? value : JSON.parse(JSON.stringify(value)); +} + +function sessionKey(connectionId, conversationId) { + return `${connectionId || ""}:${conversationId || ""}`; +} + +function ensureUserMessageModelId(message, modelId) { + if (message?.userInputMessage && !message.userInputMessage.modelId && modelId) { + message.userInputMessage.modelId = modelId; + } + return message; +} + +function ensureHistoryModelIds(history, modelId) { + for (const item of history || []) { + ensureUserMessageModelId(item, modelId); + } + return history; +} + +function prefixUserMessage(message, contentPrefix, modelId) { + const out = clone(message) || { userInputMessage: { content: "" } }; + if (!out.userInputMessage) out.userInputMessage = { content: "" }; + ensureUserMessageModelId(out, modelId); + if (contentPrefix) { + const content = out.userInputMessage.content || ""; + out.userInputMessage.content = content + ? `${contentPrefix}\n\n${content}` + : contentPrefix; + } + return out; +} + +function findFirstUserIndex(history) { + return history.findIndex((item) => item?.userInputMessage); +} + +function rememberSessionStart(key, entry) { + if (sessionStartStore.size >= MAX_SESSION_STARTS) { + sessionStartStore.delete(sessionStartStore.keys().next().value); + } + sessionStartStore.set(key, { ...entry, lastUsed: Date.now() }); +} + +/** + * Preserve Kiro cacheability by freezing the first user message (`msg0`) for a + * session, replaying that exact message as the first history user on later + * turns, and injecting volatile current-time context only into the current turn. + */ +export function applyKiroSessionReplay({ + conversationId, + connectionId, + modelId, + systemPrompt = "", + contentPrefix = "", + currentContentPrefix = "", + history = [], + currentMessage, +} = {}) { + const key = sessionKey(connectionId, conversationId); + const existing = conversationId ? sessionStartStore.get(key) : null; + const baseHistory = clone(history) || []; + const baseCurrent = clone(currentMessage) || { userInputMessage: { content: "" } }; + + if (existing && existing.modelId === modelId && existing.systemPrompt === systemPrompt) { + existing.lastUsed = Date.now(); + const firstUserIndex = findFirstUserIndex(baseHistory); + const sessionStart = ensureUserMessageModelId(clone(existing.sessionStart), modelId); + if (firstUserIndex >= 0) { + baseHistory[firstUserIndex] = sessionStart; + } else { + baseHistory.unshift(sessionStart); + } + return { + history: ensureHistoryModelIds(baseHistory, modelId), + currentMessage: prefixUserMessage(baseCurrent, currentContentPrefix, modelId), + replayed: true, + }; + } + + const firstUserIndex = findFirstUserIndex(baseHistory); + let sessionStart; + let nextCurrent = ensureUserMessageModelId(baseCurrent, modelId); + if (firstUserIndex >= 0) { + sessionStart = prefixUserMessage(baseHistory[firstUserIndex], contentPrefix, modelId); + baseHistory[firstUserIndex] = clone(sessionStart); + nextCurrent = prefixUserMessage(baseCurrent, currentContentPrefix, modelId); + } else { + sessionStart = prefixUserMessage(baseCurrent, contentPrefix, modelId); + nextCurrent = clone(sessionStart); + } + + if (conversationId) { + rememberSessionStart(key, { + sessionStart: clone(sessionStart), + modelId, + systemPrompt, + }); + } + + return { + history: ensureHistoryModelIds(baseHistory, modelId), + currentMessage: nextCurrent, + replayed: false, + }; +} + +export function clearKiroSessionReplayStore() { + sessionStartStore.clear(); +} + +const cleanup = setInterval(() => { + const now = Date.now(); + for (const [key, entry] of sessionStartStore) { + if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) sessionStartStore.delete(key); + } +}, MEMORY_CONFIG.sessionCleanupIntervalMs); +if (cleanup.unref) cleanup.unref(); diff --git a/open-sse/utils/sessionManager.js b/open-sse/utils/sessionManager.js index 05f90896..b6f16f1a 100644 --- a/open-sse/utils/sessionManager.js +++ b/open-sse/utils/sessionManager.js @@ -13,6 +13,7 @@ import { MEMORY_CONFIG } from "../config/runtimeConfig.js"; // Runtime storage: Key = connectionId, Value = { sessionId, lastUsed } const runtimeSessionStore = new Map(); +const continuationStore = new Map(); // Periodically evict entries that haven't been used within TTL const cleanupInterval = setInterval(() => { @@ -80,6 +81,7 @@ export function generateBinaryStyleId() { export function clearSessionStore() { runtimeSessionStore.clear(); assistantSessionStore.clear(); + continuationStore.clear(); } // Conversation-stable session store: Key = hash(scope+assistant text), Value = { sessionId, lastUsed } @@ -87,9 +89,10 @@ const assistantSessionStore = new Map(); const ASSISTANT_MIN_LEN = 50; const ASSISTANT_CAP_LEN = 50; const MAX_ASSISTANT_SESSIONS = 5000; +const MAX_CONTINUATION_SESSIONS = 5000; // Client headers/body fields that carry an upstream session id (priority order) -const SESSION_HEADER_KEYS = ["x-session-id", "session-id", "session_id", "x-amp-thread-id", "x-client-request-id"]; +const SESSION_HEADER_KEYS = ["x-session-id", "session-id", "session_id", "x-amp-thread-id"]; const CLAUDE_CODE_SESSION_RE = /_session_([a-f0-9-]+)$/; function sha16(text) { @@ -131,7 +134,7 @@ function extractAntigravitySession(body) { return m ? normalizeSessionId(m[1]) : null; } -function extractClientSessionId(headers, body) { +function extractClientSessionId(headers, body, scope = "") { const claude = extractClaudeCodeSession(body?.metadata?.user_id); if (claude) return `claude:${claude}`; const antigravity = extractAntigravitySession(body); @@ -140,18 +143,25 @@ function extractClientSessionId(headers, body) { const v = headerValue(headers, key); if (v) return v; } + const requestId = scope === "kiro" ? null : headerValue(headers, "x-client-request-id"); + if (requestId) return requestId; const fromBody = normalizeSessionId(body?.prompt_cache_key) || normalizeSessionId(body?.session_id) || normalizeSessionId(body?.conversation_id) || - normalizeSessionId(body?.metadata?.user_id); + (scope === "kiro" ? null : normalizeSessionId(body?.metadata?.user_id)); return fromBody || null; } +function requestMessages(body) { + if (Array.isArray(body?.messages)) return body.messages; + if (Array.isArray(body?.input)) return body.input; + return []; +} + // Accumulate assistant text from OpenAI/Responses-style input/messages (cap-limited) function accumulateAssistantText(body) { - const items = Array.isArray(body?.input) ? body.input - : Array.isArray(body?.messages) ? body.messages : null; + const items = requestMessages(body); if (!items) return ""; let text = ""; for (const item of items) { @@ -193,16 +203,39 @@ function assistantTextSessionId(scope, body) { * @param {string} [opts.connectionId] - Connection identifier (fallback scope) * @param {string} [opts.workspaceId] - Provider workspace id (account-wide fallback) * @param {string} [opts.scope] - Provider scope to isolate cache keys across providers - * @returns {string} A stable session id + * @returns {{sessionId: string, ephemeral: boolean}} A session id plus whether it is one-shot */ -export function resolveSessionId({ headers, body, connectionId, workspaceId, scope = "" } = {}) { - const client = extractClientSessionId(headers, body); - if (client) return client; - const fromAssistant = assistantTextSessionId(`${scope}:${connectionId || ""}`, body); - if (fromAssistant) return fromAssistant; +export function resolveSessionIdentity({ headers, body, connectionId, workspaceId, scope = "" } = {}) { + const client = extractClientSessionId(headers, body, scope); + if (client) return { sessionId: client, ephemeral: false }; + const fromAssistant = scope === "kiro" ? null : assistantTextSessionId(`${scope}:${connectionId || ""}`, body); + if (fromAssistant) return { sessionId: fromAssistant, ephemeral: false }; const ws = normalizeSessionId(workspaceId); - if (ws) return ws; - return deriveSessionId(connectionId); + if (ws) return { sessionId: ws, ephemeral: false }; + if (scope === "kiro") return { sessionId: generateBinaryStyleId(), ephemeral: true }; + return { sessionId: deriveSessionId(connectionId), ephemeral: false }; +} + +export function resolveSessionId(opts = {}) { + return resolveSessionIdentity(opts).sessionId; +} + +export function resolveContinuationId({ sessionId, connectionId, scope = "", ephemeral = false } = {}) { + if (ephemeral) return crypto.randomUUID(); + const key = `${scope}:${connectionId || ""}:${sessionId || ""}`; + const existing = continuationStore.get(key); + if (existing) { + existing.lastUsed = Date.now(); + continuationStore.delete(key); + continuationStore.set(key, existing); + return existing.continuationId; + } + const continuationId = crypto.randomUUID(); + if (continuationStore.size >= MAX_CONTINUATION_SESSIONS) { + continuationStore.delete(continuationStore.keys().next().value); + } + continuationStore.set(key, { continuationId, lastUsed: Date.now() }); + return continuationId; } // Capture session id from request body + credentials (envelope still intact here) @@ -227,5 +260,8 @@ const assistantCleanup = setInterval(() => { for (const [key, entry] of assistantSessionStore) { if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) assistantSessionStore.delete(key); } + for (const [key, entry] of continuationStore) { + if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) continuationStore.delete(key); + } }, MEMORY_CONFIG.sessionCleanupIntervalMs); if (assistantCleanup.unref) assistantCleanup.unref(); diff --git a/open-sse/utils/streamHandler.js b/open-sse/utils/streamHandler.js index b8a06e2f..7f04427d 100644 --- a/open-sse/utils/streamHandler.js +++ b/open-sse/utils/streamHandler.js @@ -15,16 +15,19 @@ function getTimeString() { * @param {string} options.provider - Provider name * @param {string} options.model - Model name */ -export function createStreamController({ onDisconnect, onError, log, provider, model } = {}) { +export function createStreamController({ onDisconnect, onError, log, provider, model, reqTag = "" } = {}) { const abortController = new AbortController(); const startTime = Date.now(); let disconnected = false; let abortTimeout = null; - const logStream = (status) => { + // Only abnormal terminations are logged; normal completion is covered by "📊 done". + // isError uses errorLine (always shown, ignores LOG_LEVEL) so failures survive quiet levels. + const logStream = (symbol, status, isError = false) => { const duration = Date.now() - startTime; - const p = provider?.toUpperCase() || "UNKNOWN"; - console.log(`[${getTimeString()}] 🌊 [STREAM] ${p} | ${model || "unknown"} | ${duration}ms | ${status}`); + const emit = isError ? log?.errorLine : log?.line; + if (emit) emit(reqTag, symbol, `${status} · ${provider}/${model} · ${duration}ms`); + else console.log(`[${getTimeString()}] ${symbol} ${provider}/${model} · ${status} · ${duration}ms`); }; return { @@ -38,7 +41,7 @@ export function createStreamController({ onDisconnect, onError, log, provider, m if (disconnected) return; disconnected = true; - logStream(`disconnect: ${reason}`); + logStream("⚡", `DISCONNECT: ${reason}`); dbg("CTRL", `${provider}/${model} | disconnect=${reason} | dur=${Date.now() - startTime}ms`); // Delay abort to allow cleanup @@ -49,13 +52,11 @@ export function createStreamController({ onDisconnect, onError, log, provider, m onDisconnect?.({ reason, duration: Date.now() - startTime }); }, - // Call when stream completes normally + // Call when stream completes normally (no line here — "📊 done" is authoritative) handleComplete: () => { if (disconnected) return; disconnected = true; - logStream("complete"); - if (abortTimeout) { clearTimeout(abortTimeout); abortTimeout = null; @@ -73,11 +74,11 @@ export function createStreamController({ onDisconnect, onError, log, provider, m } if (error.name === "AbortError") { - logStream("aborted"); + logStream("⚡", "ABORTED"); return; } - logStream(`error: ${error.message}`); + logStream("✗", `ERROR: ${error.message}${error.stack ? `\n ${error.stack}` : ""}`, true); onError?.(error); }, diff --git a/open-sse/utils/usageTracking.js b/open-sse/utils/usageTracking.js index 663f2eaf..24518ef3 100644 --- a/open-sse/utils/usageTracking.js +++ b/open-sse/utils/usageTracking.js @@ -4,6 +4,9 @@ import { FORMATS } from "../translator/formats.js"; +// Legacy per-chunk usage console line; off by default (superseded by "📊 done") +const DEBUG_USAGE = process.env.LOG_USAGE_VERBOSE === "1"; + // ANSI color codes export const COLORS = { reset: "\x1b[0m", @@ -401,6 +404,10 @@ export function estimateUsage(body, contentLength, targetFormat = FORMATS.OPENAI export function logUsage(provider, usage, model = null, connectionId = null, apiKey = null) { if (!usage || typeof usage !== "object") return; + // Console output moved to the unified "📊 done" line (streamingHandler). Kept as + // a no-op hook so callers stay unchanged; usage persistence happens via saveUsageStats. + if (!DEBUG_USAGE) return; + const p = provider?.toUpperCase() || "UNKNOWN"; // Support both formats: diff --git a/package.json b/package.json index a08b475d..ba2170c5 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "9router-app", - "version": "0.5.18", + "version": "0.5.35", "description": "9Router web dashboard", "private": true, "scripts": { - "dev": "next dev --webpack --port 20127", + "dev": "next dev --port 20127", + "dev:webpack": "next dev --webpack --port 20127", "build": "next build --webpack", - "start": "next start", + "start": "next start --port 20127", "dev:bun": "bun --bun next dev --webpack --port 20127", "build:bun": "bun --bun next build --webpack", "start:bun": "bun ./.next/standalone/server.js", @@ -24,7 +25,6 @@ "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", "marked": "^18.0.1", diff --git a/public/i18n/literals/fa.json b/public/i18n/literals/fa.json new file mode 100644 index 00000000..b298fc57 --- /dev/null +++ b/public/i18n/literals/fa.json @@ -0,0 +1,1391 @@ +{ + "($/1M tokens). Example: An input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "($/۱ میلیون توکن). مثال: نرخ ورودی ۲.۵۰ به معنای ۲.۵۰ دلار به ازای هر ۱٬۰۰۰٬۰۰۰ توکن ورودی است.", + "($/1M tokens). Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "($/۱ میلیون توکن). مثال: نرخ ورودی ۲.۵۰ به معنای ۲.۵۰ دلار به ازای هر ۱٬۰۰۰٬۰۰۰ توکن ورودی است.", + "(Caveman)": "(Caveman)", + "(Headroom)": "(Headroom)", + "(Ponytail)": "(Ponytail)", + "(RTK)": "(RTK)", + "(via inference test)": "(از طریق آزمون استنتاج)", + "+ Browse": "+ مرور", + "+ Combo": "+ ترکیب", + "+ Custom": "+ سفارشی", + "+ Save current as...": "+ ذخیره فعلی به عنوان...", + "-compatible models manually or import them from the /models endpoint.": "مدل‌های سازگار را به صورت دستی وارد کنید یا از نقطه پایانی /models وارد کنید.", + ". Click \"Apply\" to auto-configure.": ". برای پیکربندی خودکار روی «اعمال» کلیک کنید.", + "1. CLI & SDKs": "۱. CLI و SDK", + "1. Client Request (Input)": "۱. درخواست مشتری (ورودی)", + "1. Generates SSL cert & adds to system keychain": "۱. گواهی SSL تولید می‌کند و به زنجیره کلید سیستم اضافه می‌کند", + "2. 9Router Hub": "۲. مرکز 9Router", + "2. Provider Request (Translated)": "۲. درخواست ارائه‌دهنده (ترجمه شده)", + "2. Redirects": "۲. تغییر مسیرها", + "24h": "۲۴ ساعت", + "3. AI Providers": "۳. ارائه‌دهندگان هوش مصنوعی", + "3. Maps Antigravity models to any provider via 9Router": "۳. مدل‌های Antigravity را از طریق 9Router به هر ارائه‌دهنده‌ای نگاشت می‌کند", + "3. Provider Response (Raw)": "۳. پاسخ ارائه‌دهنده (خام)", + "30D": "۳۰ روز", + "4. Client Response (Final)": "۴. پاسخ مشتری (نهایی)", + "60D": "۶۰ روز", + "7D": "۷ روز", + "9Router (Entry)": "9Router (ورودی)", + "9Router Base URL": "آدرس پایه 9Router", + ": Account | Workers Scripts | Edit": ": حساب | اسکریپت‌های Workers | ویرایش", + ": Include | Account |": ": شامل | حساب |", + "AI endpoint proxy with web dashboard - A JavaScript port of CLIProxyAPI. Works seamlessly with Claude Code, OpenAI Codex, Cline, RooCode, and other CLI tools.": "پروکسی نقطه پایانی هوش مصنوعی با داشبورد وب - یک پورت جاوااسکریپت از CLIProxyAPI. به‌طور یکپارچه با Claude Code، OpenAI Codex، Cline، RooCode و سایر ابزارهای CLI کار می‌کند.", + "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 Token": "توکن API", + "API Tokens": "توکن‌های API", + "API Type": "نوع API", + "API Version": "نسخه API", + "API endpoint configuration": "پیکربندی نقطه پایانی API", + "AWS Builder ID": "AWS Builder ID", + "AWS IAM Identity Center": "مرکز هویت AWS IAM", + "AWS Region": "منطقه AWS", + "AWS region for the key (default: us-east-1)": "منطقه AWS برای کلید (پیش‌فرض: us-east-1)", + "AWS region for your Identity Center (default: us-east-1)": "منطقه AWS برای مرکز هویت شما (پیش‌فرض: us-east-1)", + "About": "درباره", + "Access Anywhere": "دسترسی از هر جا", + "Access Token": "توکن دسترسی", + "Access token will be auto-filled...": "توکن دسترسی به‌طور خودکار پر می‌شود...", + "Access your terminal, desktop & files from anywhere": "از هر جایی به ترمینال، دسکتاپ و فایل‌های خود دسترسی داشته باشید", + "Account": "حساب", + "Account ID": "شناسه حساب", + "Account Resources": "منابع حساب", + "Accounts per page": "تعداد حساب در هر صفحه", + "Action": "عملیات", + "Activate": "فعال‌سازی", + "Active": "فعال", + "Active All": "فعال‌سازی همه", + "Active:": "فعال:", + "Add": "افزودن", + "Add API Key": "افزودن کلید API", + "Add Anthropic Compatible": "افزودن سازگار با Anthropic", + "Add Connection": "افزودن اتصال", + "Add Custom Embedding": "افزودن تعبیه سفارشی", + "Add Custom MCP": "افزودن MCP سفارشی", + "Add Custom Model": "افزودن مدل سفارشی", + "Add Model": "افزودن مدل", + "Add Model Config": "افزودن پیکربندی مدل", + "Add Model for GitHub Copilot": "افزودن مدل برای GitHub Copilot", + "Add Model for OpenCode": "افزودن مدل برای OpenCode", + "Add Model to Combo": "افزودن مدل به ترکیب", + "Add New Provider": "افزودن ارائه‌دهنده جدید", + "Add OpenAI Compatible": "افزودن سازگار با OpenAI", + "Add Provider": "افزودن ارائه‌دهنده", + "Add Proxy Pool": "افزودن استخر پروکسی", + "Add Shorthands": "افزودن میان‌نویس‌ها", + "Add a connection to enable importing models.": "برای فعال‌سازی وارد کردن مدل‌ها، یک اتصال اضافه کنید.", + "Add connection using browser cookie": "افزودن اتصال با استفاده از کوکی مرورگر", + "Add model": "افزودن مدل", + "Add server": "افزودن سرور", + "Add the following configuration to your models array:": "پیکربندی زیر را به آرایه مدل‌های خود اضافه کنید:", + "Add your first connection to get started": "اولین اتصال خود را برای شروع اضافه کنید", + "Administrator required": "نیاز به مدیر سیستم", + "Administrator required — restart 9Router as Administrator to use MITM": "نیاز به مدیر سیستم — برای استفاده از MITM، 9Router را به عنوان مدیر راه‌اندازی مجدد کنید", + "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 پاسخ بازگشت را از مرورگر خود کپی کرده و در اینجا بچسبانید.", + "Alibaba Qwen Code CLI — supports OpenAI, Anthropic & Gemini providers via 9Router": "علی‌بابا Qwen Code CLI — از ارائه‌دهندگان OpenAI، Anthropic و Gemini از طریق 9Router پشتیبانی می‌کند", + "All": "همه", + "All AI Providers": "همه ارائه‌دهندگان هوش مصنوعی", + "All Providers": "همه ارائه‌دهندگان", + "All models are responding normally.": "همه مدل‌ها به‌طور عادی پاسخ می‌دهند.", + "All providers": "همه ارائه‌دهندگان", + "All rates are in": "همه نرخ‌ها بر حسب", + "All selected currently unbound": "همه موارد انتخاب شده در حال حاضر بدون اتصال هستند", + "Allow dashboard access via tunnel": "اجازه دسترسی به داشبورد از طریق تونل", + "Allow either password or OIDC.": "اجازه ورود با رمز عبور یا OIDC را بدهید.", + "An error occurred": "خطایی رخ داد", + "An error occurred. Please try again.": "خطایی رخ داد. لطفاً دوباره تلاش کنید.", + "Anthropic Claude Code CLI": "Anthropic Claude Code CLI", + "Anthropic Compatible (Prod)": "سازگار با Anthropic (تولید)", + "Anthropic Compatible Details": "جزئیات سازگاری با Anthropic", + "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", + "Any model available in 9Router can be used — not just Qwen models. Select from Qwen, Claude, Gemini, GPT, and more.": "هر مدلی که در 9Router موجود است قابل استفاده است — نه فقط مدل‌های Qwen. از بین Qwen، Claude، Gemini، GPT و بیشتر انتخاب کنید.", + "App Name": "نام برنامه", + "Apply": "اعمال", + "Apply Proxy": "اعمال پروکسی", + "Applying...": "در حال اعمال...", + "Are you sure you want to close the proxy server?": "آیا مطمئن هستید که می‌خواهید سرور پروکسی را ببندید؟", + "Are you sure you want to disable the tunnel?": "آیا مطمئن هستید که می‌خواهید تونل را غیرفعال کنید؟", + "Attempting to reconnect...": "در حال تلاش برای اتصال مجدد...", + "Audio File": "فایل صوتی", + "Auth Mode": "حالت احراز هویت", + "Authenticate": "احراز هویت", + "Authentication Method": "روش احراز هویت", + "Authentication Successful": "احراز هویت موفق", + "Authentication Successful!": "احراز هویت موفق!", + "Authless": "بدون احراز هویت", + "Authorization Successful!": "مجوز با موفقیت انجام شد!", + "Authorize": "مجوز", + "Auto (by priority)": "خودکار (بر اساس اولویت)", + "Auto Refresh (3s)": "تازه‌سازی خودکار (۳ ثانیه)", + "Auto-detect": "تشخیص خودکار", + "Auto-detecting token...": "در حال تشخیص خودکار توکن...", + "Auto-detecting tokens...": "در حال تشخیص خودکار توکن‌ها...", + "Auto-ping": "پینگ خودکار", + "Auto-refresh": "تازه‌سازی خودکار", + "Auto:": "خودکار:", + "Automatically switch between providers when limits are hit.": "هنگام رسیدن به محدودیت‌ها به‌طور خودکار بین ارائه‌دهندگان جابجا شوید.", + "Available": "موجود", + "Available Models": "مدل‌های موجود", + "Azure Endpoint": "نقطه پایانی Azure", + "Azure OpenAI Configuration": "پیکربندی Azure OpenAI", + "BXAuth=xxx; ...": "BXAuth=xxx; ...", + "Back": "بازگشت", + "Back to CLI Tools": "بازگشت به ابزارهای CLI", + "Back to Providers": "بازگشت به ارائه‌دهندگان", + "Base URL": "آدرس پایه", + "Batch Import": "وارد کردن دسته‌ای", + "Batch Import Proxies": "وارد کردن دسته‌ای پروکسی‌ها", + "Batch Size": "اندازه دسته", + "Beautiful web dashboard for managing providers and monitoring usage.": "داشبورد وب زیبا برای مدیریت ارائه‌دهندگان و نظارت بر مصرف.", + "Best quality, but costs the most": "بهترین کیفیت، اما هزینه‌برترین", + "Bias the model toward minimal code: YAGNI, reuse stdlib, deletion over addition": "مدل را به سمت کد حداقلی سوق دهید: YAGNI، استفاده مجدد از کتابخانه استاندارد، حذف به جای افزودن", + "Binary File": "فایل باینری", + "Blog": "وبلاگ", + "Both": "هر دو", + "Browse & edit files": "مرور و ویرایش فایل‌ها", + "Browse MCP Marketplace": "مرور بازار MCP", + "Browse source, README, and examples.": "مرور کد منبع، README و مثال‌ها.", + "Browser Control (Browser MCP)": "کنترل مرورگر (Browser MCP)", + "Bulk Add": "افزودن عمده", + "CLI Support": "پشتیبانی CLI", + "CLI Tools": "ابزارهای CLI", + "CLI on the host →": "CLI روی میزبان →", + "CLIProxyAPI Auth JSON": "احراز هویت CLIProxyAPI JSON", + "Cache Creation": "ایجاد حافظه پنهان", + "Cache Creation:": "ایجاد حافظه پنهان:", + "Cached": "ذخیره شده در حافظه پنهان", + "Cached Tokens": "توکن‌های ذخیره شده در حافظه پنهان", + "Cached Tokens:": "توکن‌های ذخیره شده در حافظه پنهان:", + "Cached input tokens (typically 50% of input rate)": "توکن‌های ورودی ذخیره شده در حافظه پنهان (معمولاً ۵۰٪ نرخ ورودی)", + "Cached:": "ذخیره شده در حافظه پنهان:", + "Calls per account before switching": "تعداد تماس به ازای هر حساب قبل از تغییر", + "Calls per combo model before switching": "تعداد تماس به ازای هر مدل ترکیبی قبل از تغییر", + "Cancel": "لغو", + "Capacity auto-switch": "تغییر خودکار ظرفیت", + "Cert": "گواهی", + "Change Log": "تاریخچه تغییرات", + "Changelog": "تاریخچه تغییرات", + "Chat": "گفتگو", + "Chat / code-gen via OpenAI or Anthropic format with streaming.": "گفتگو / تولید کد از طریق فرمت OpenAI یا Anthropic با پخش جریانی.", + "Chat Completions": "تکمیل گفتگو", + "Check": "بررسی", + "Checking Claude CLI...": "در حال بررسی Claude CLI...", + "Checking Claude Cowork...": "در حال بررسی Claude Cowork...", + "Checking Cline...": "در حال بررسی Cline...", + "Checking Codex CLI...": "در حال بررسی Codex CLI...", + "Checking Copilot config...": "در حال بررسی پیکربندی Copilot...", + "Checking DeepSeek TUI...": "در حال بررسی DeepSeek TUI...", + "Checking Factory Droid CLI...": "در حال بررسی Factory Droid CLI...", + "Checking Hermes Agent...": "در حال بررسی Hermes Agent...", + "Checking Kilo Code...": "در حال بررسی Kilo Code...", + "Checking Open Claw CLI...": "در حال بررسی Open Claw CLI...", + "Checking OpenCode CLI...": "در حال بررسی OpenCode CLI...", + "Checking jcode CLI...": "در حال بررسی jcode CLI...", + "Checking...": "در حال بررسی...", + "Choose API Provider → Ollama": "ارائه‌دهنده API را انتخاب کنید → Ollama", + "Choose how to authenticate with GitLab Duo:": "نحوه احراز هویت با GitLab Duo را انتخاب کنید:", + "Choose your authentication method:": "روش احراز هویت خود را انتخاب کنید:", + "Claude": "Claude", + "Claude CLI - Manual Configuration": "Claude CLI - پیکربندی دستی", + "Claude CLI not detected locally": "Claude CLI در سیستم محلی شناسایی نشد", + "Claude CLI not installed": "Claude CLI نصب نشده است", + "Claude Cowork - Manual Configuration": "Claude Cowork - پیکربندی دستی", + "Claude Desktop (Cowork mode) not detected": "Claude Desktop (حالت Cowork) شناسایی نشد", + "Claude Desktop Cowork (third-party inference)": "Claude Desktop Cowork (استنتاج شخص ثالث)", + "Clear": "پاک کردن", + "Clear (will use main model)": "پاک کردن (از مدل اصلی استفاده خواهد شد)", + "Clear Filters": "پاک کردن فیلترها", + "Clear search": "پاک کردن جستجو", + "Click": "کلیک", + "Click \"View All Model\" → \"Add Custom Model\"": "روی «مشاهده همه مدل‌ها» → «افزودن مدل سفارشی» کلیک کنید", + "Click a model to set/clear active": "برای تنظیم/لغو فعال بودن، روی یک مدل کلیک کنید", + "Click to add, click again to remove. Changes are saved automatically.": "برای افزودن کلیک کنید، برای حذف دوباره کلیک کنید. تغییرات به‌طور خودکار ذخیره می‌شوند.", + "Click to edit": "برای ویرایش کلیک کنید", + "Click to retry": "برای تلاش مجدد کلیک کنید", + "Client ID": "شناسه مشتری", + "Client Request": "درخواست مشتری", + "Client Response": "پاسخ مشتری", + "Client Secret": "راز مشتری", + "Cline - Manual Configuration": "Cline - پیکربندی دستی", + "Cline AI Coding Assistant": "دستیار کدنویسی هوش مصنوعی Cline", + "Cline not detected locally": "Cline در سیستم محلی شناسایی نشد", + "Close": "بستن", + "Close Proxy": "بستن پروکسی", + "Close provider filter": "بستن فیلتر ارائه‌دهنده", + "Close reset credit expiry modal": "بستن پنجره انقضای اعتبار بازنشانی", + "Close test results": "بستن نتایج آزمایش", + "Closing in": "در حال بسته شدن در", + "Cloud Sync": "همگام‌سازی ابری", + "Cloudflare Relay": "Cloudflare Relay", + "Cloudflare Tunnel": "تونل Cloudflare", + "Cloudflare Workers AI": "Cloudflare Workers AI", + "Codex CLI - Manual Configuration": "Codex CLI - پیکربندی دستی", + "Codex CLI not detected locally": "Codex CLI در سیستم محلی شناسایی نشد", + "Codex CLI not installed": "Codex CLI نصب نشده است", + "Codex Reset Credit Expiry": "انقضای اعتبار بازنشانی Codex", + "Codex uses": "Codex استفاده می‌کند", + "Combo Name": "نام ترکیب", + "Combo Round Robin": "چرخشی ترکیب", + "Combo Sticky Limit": "محدودیت چسبندگی ترکیب", + "Combos": "ترکیبات", + "Coming soon...": "به زودی...", + "Comma-separated hostnames/domains to bypass the proxy.": "نام میزبان/دامنه‌ها با جداکننده ویرگول برای دور زدن پروکسی.", + "Comma-separated hosts/domains to bypass proxy": "نام میزبان/دامنه‌ها با جداکننده ویرگول برای دور زدن پروکسی", + "Company": "شرکت", + "Complete the authorization in the popup window.": "مجوز را در پنجره بازشو تکمیل کنید.", + "Completion/response tokens": "توکن‌های تکمیل/پاسخ", + "Compress LLM output": "فشرده‌سازی خروجی LLM", + "Compress context": "فشرده‌سازی زمینه", + "Compress prompts via /v1/compress before routing to the model": "فشرده‌سازی پرامپت‌ها از طریق /v1/compress قبل از مسیردهی به مدل", + "Compress tool output": "فشرده‌سازی خروجی ابزار", + "Compress tool output to reduce token usage.": "خروجی ابزار را برای کاهش مصرف توکن فشرده کنید.", + "Config path: Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml": "مسیر پیکربندی: Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml", + "Config path: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json": "مسیر پیکربندی: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json", + "Configuration": "پیکربندی", + "Configure 9router as an OpenAI-compatible provider to route all jcode requests through 9router's optimization layer.": "9router را به عنوان یک ارائه‌دهنده سازگار با OpenAI پیکربندی کنید تا تمام درخواست‌های jcode را از طریق لایه بهینه‌سازی 9router مسیردهی کند.", + "Configure CLI tools": "پیکربندی ابزارهای CLI", + "Configure a new AI provider to use with your applications.": "یک ارائه‌دهنده هوش مصنوعی جدید برای استفاده با برنامه‌های خود پیکربندی کنید.", + "Configure pricing rates for cost tracking and calculations": "نرخ‌های قیمت‌گذاری را برای پیگیری و محاسبه هزینه پیکربندی کنید", + "Configure providers and API keys via web interface": "پیکربندی ارائه‌دهندگان و کلیدهای API از طریق رابط وب", + "Configured": "پیکربندی شده", + "Confirm": "تأیید", + "Confirm New Password": "تأیید رمز عبور جدید", + "Confirm Password": "تأیید رمز عبور", + "Confirm new password": "تأیید رمز عبور جدید", + "Connect": "اتصال", + "Connect AI tools remotely": "اتصال ابزارهای هوش مصنوعی از راه دور", + "Connect Cursor IDE": "اتصال Cursor IDE", + "Connect GitLab Duo": "اتصال GitLab Duo", + "Connect Kiro": "اتصال Kiro", + "Connect to providers with OAuth to track your API quota limits and usage.": "با استفاده از OAuth به ارائه‌دهندگان متصل شوید تا محدودیت‌ها و مصرف سهمیه API خود را پیگیری کنید.", + "Connect via OAuth or API keys. Securely manage credentials.": "از طریق OAuth یا کلیدهای API متصل شوید. اعتبارنامه‌ها را به‌طور امن مدیریت کنید.", + "Connect with OAuth2": "اتصال با OAuth2", + "Connect your account using OAuth2 authentication.": "حساب خود را با استفاده از احراز هویت OAuth2 متصل کنید.", + "Connected": "متصل", + "Connected Successfully!": "اتصال با موفقیت انجام شد!", + "Connected providers only": "فقط ارائه‌دهندگان متصل", + "Connecting...": "در حال اتصال...", + "Connection": "اتصال", + "Connection Details": "جزئیات اتصال", + "Connection Failed": "اتصال ناموفق", + "Connections": "اتصالات", + "Console Log": "لاگ کنسول", + "Contact": "تماس", + "Content": "محتوای", + "Continue": "ادامه", + "Continue AI Assistant": "دستیار هوش مصنوعی Continue", + "Continue to summary": "ادامه به خلاصه", + "Continue with GitHub": "ادامه با GitHub", + "Continue with Google": "ادامه با Google", + "Cookie": "کوکی", + "Cookie Auth": "احراز هویت کوکی", + "Cookie String": "رشته کوکی", + "Cooldown": "آرامش", + "Copied!": "کپی شد!", + "Copy": "کپی", + "Copy & Shutdown": "کپی و خاموش کردن", + "Copy This URL": "کپی این URL", + "Copy a link and paste to your AI to use 9Router — no install needed": "یک لینک کپی کرده و به هوش مصنوعی خود بچسبانید تا از 9Router استفاده کنید — نیازی به نصب نیست", + "Copy combo name": "کپی نام ترکیب", + "Copy install command": "کپی دستور نصب", + "Copy model": "کپی مدل", + "Copy the JSON below to your ~/.qwen/settings.json file.": "JSON زیر را در فایل ~/.qwen/settings.json خود کپی کنید.", + "Copy the entire cookie string (must include BXAuth)": "کل رشته کوکی را کپی کنید (باید شامل BXAuth باشد)", + "Cost": "هزینه", + "Cost Calculation:": "محاسبه هزینه:", + "Costs": "هزینه‌ها", + "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)": "هزینه‌ها بر اساس مصرف توکن و نرخ‌های قیمت‌گذاری محاسبه می‌شوند. هزینه هر درخواست با فرمول زیر تعیین می‌شود: (توکن‌های ورودی × نرخ ورودی) + (توکن‌های خروجی × نرخ خروجی) + (توکن‌های ذخیره شده × نرخ ذخیره شده)", + "Could not read Cursor database automatically.": "امکان خواندن خودکار پایگاه داده Cursor وجود ندارد.", + "Create": "ایجاد", + "Create API Key": "ایجاد کلید API", + "Create Combo": "ایجاد ترکیب", + "Create Cowork Combo": "ایجاد ترکیب Cowork", + "Create Key": "ایجاد کلید", + "Create Provider": "ایجاد ارائه‌دهنده", + "Create Token": "ایجاد توکن", + "Create a": "ایجاد یک", + "Create a proxy pool entry, then assign it to connections.": "یک ورودی استخر پروکسی ایجاد کنید، سپس آن را به اتصالات اختصاص دهید.", + "Create model combos with fallback support": "ایجاد ترکیبات مدل با پشتیبانی از پشتیبان جایگزین", + "Create your first API key to get started": "اولین کلید API خود را برای شروع ایجاد کنید", + "Created": "ایجاد شد", + "Creating...": "در حال ایجاد...", + "Current": "فعلی", + "Current Password": "رمز عبور فعلی", + "Current Pricing Overview": "بررسی قیمت‌گذاری فعلی", + "Current password": "رمز عبور فعلی", + "Current: Keeps": "فعلی: نگهداری می‌کند", + "Currently using accounts in priority order (Fill First).": "در حال حاضر از حساب‌ها به ترتیب اولویت استفاده می‌کند (ابتدا پر کردن).", + "Cursor AI Code Editor": "ویرایشگر کد هوش مصنوعی Cursor", + "Cursor IDE not detected. Please paste your tokens manually.": "Cursor IDE شناسایی نشد. لطفاً توکن‌های خود را به صورت دستی بچسبانید.", + "Cursor routes requests through its own server, so local endpoint is not supported. Please enable Tunnel or Cloud Endpoint in Settings.": "Cursor درخواست‌ها را از طریق سرور خود مسیردهی می‌کند، بنابراین نقطه پایانی محلی پشتیبانی نمی‌شود. لطفاً تونل یا نقطه پایانی ابری را در تنظیمات فعال کنید.", + "Custom": "سفارشی", + "Custom Pricing:": "قیمت‌گذاری سفارشی:", + "Custom Providers (OpenAI/Anthropic Compatible)": "ارائه‌دهندگان سفارشی (سازگار با OpenAI/Anthropic)", + "Custom Token": "توکن سفارشی", + "Custom accounts per page": "تعداد حساب سفارشی در هر صفحه", + "Custom providers": "ارائه‌دهندگان سفارشی", + "Custom...": "سفارشی...", + "Cycle through accounts to distribute load": "چرخش بین حساب‌ها برای توزیع بار", + "Cycle through providers in combos instead of always starting with first": "چرخش بین ارائه‌دهندگان در ترکیبات به جای همیشه شروع با اولین", + "DNS off": "DNS خاموش", + "Dashboard": "داشبورد", + "Dashboard Password": "رمز عبور داشبورد", + "Dashboard:": "داشبورد:", + "Data Location:": "مکان داده:", + "Data flows seamlessly from your application through our intelligent routing layer to the best provider for the job.": "داده‌ها به‌طور یکپارچه از برنامه شما از طریق لایه مسیریابی هوشمند ما به بهترین ارائه‌دهنده برای کار جریان می‌یابد.", + "Data flows seamlessly through our intelligent routing system": "داده‌ها به‌طور یکپارچه از طریق سیستم مسیریابی هوشمند ما جریان می‌یابند", + "Database Location": "مکان پایگاه داده", + "Database backup downloaded": "پشتیبان پایگاه داده دانلود شد", + "Database imported successfully": "پایگاه داده با موفقیت وارد شد", + "DateTime": "تاریخ و زمان", + "Deactivate": "غیرفعال‌سازی", + "Debug": "اشکال‌زدایی", + "Debug translation flow between formats": "اشکال‌زدایی جریان ترجمه بین فرمت‌ها", + "DeepSeek TUI - Manual Configuration": "DeepSeek TUI - پیکربندی دستی", + "DeepSeek TUI not detected locally": "DeepSeek TUI در سیستم محلی شناسایی نشد", + "DeepSeek TUI uses ~/.deepseek/config.toml for configuration. 9Router will update the provider to 'openai' mode with your base_url, api_key, and model.": "DeepSeek TUI از ~/.deepseek/config.toml برای پیکربندی استفاده می‌کند. 9Router ارائه‌دهنده را به حالت 'openai' با base_url، api_key و model شما به‌روز می‌کند.", + "DeepSeek Terminal Coding Agent (Rust TUI)": "عامل کدنویسی ترمینال DeepSeek (Rust TUI)", + "Default Model": "مدل پیش‌فرض", + "Default password is": "رمز عبور پیش‌فرض است", + "Default password is 123456": "رمز عبور پیش‌فرض ۱۲۳۴۵۶ است", + "Delete": "حذف", + "Delete API Key": "حذف کلید API", + "Delete connection": "حذف اتصال", + "Delete saved endpoint": "حذف نقطه پایانی ذخیره شده", + "Delete selected preset": "حذف تنظیم از پیش انتخاب شده", + "Delete this combo?": "این ترکیب حذف شود؟", + "Delete this connection?": "این اتصال حذف شود؟", + "Deno Deploy API Token": "توکن Deno Deploy API", + "Deno Deploy v2 runs on a high-performance global edge network": "Deno Deploy v2 بر روی شبکه لبه جهانی با کارایی بالا اجرا می‌شود", + "Deno Relay": "Deno Relay", + "Deploy": "استقرار", + "Deploy Cloudflare Relay": "استقرار Cloudflare Relay", + "Deploy Deno Relay": "استقرار Deno Relay", + "Deploy Relay": "استقرار Relay", + "Deploy Vercel Relay": "استقرار Vercel Relay", + "Deploy multiple relays for maximum IP diversity": "استقرار چندین Relay برای حداکثر تنوع IP", + "Deploy multiple relays on different accounts for more IP diversity": "استقرار چندین Relay در حساب‌های مختلف برای تنوع بیشتر IP", + "Deploying... (may take ~1 min)": "در حال استقرار... (ممکن است حدود ۱ دقیقه طول بکشد)", + "Deployment Name": "نام استقرار", + "Deploys a Cloudflare Worker as a proxy relay. All AI provider requests will be forwarded through Cloudflare's global edge network.": "یک Cloudflare Worker را به عنوان Relay پروکسی استقرار می‌دهد. تمام درخواست‌های ارائه‌دهنده هوش مصنوعی از طریق شبکه لبه جهانی Cloudflare ارسال می‌شوند.", + "Deploys a relay worker to Deno Deploy's global edge network. All AI provider requests are forwarded through Deno's edge, masking your real IP.": "یک Relay Worker را به شبکه لبه جهانی Deno Deploy استقرار می‌دهد. تمام درخواست‌های ارائه‌دهنده هوش مصنوعی از طریق لبه Deno ارسال می‌شوند و IP واقعی شما را پنهان می‌کنند.", + "Deploys an edge relay function to Vercel that proxies requests through Vercel's network.": "یک تابع Relay لبه را در Vercel استقرار می‌دهد که درخواست‌ها را از طریق شبکه Vercel پروکسی می‌کند.", + "Deploys an edge relay function to Vercel. All AI provider requests will be forwarded through Vercel's edge network, masking your real IP from providers.": "یک تابع Relay لبه را در Vercel استقرار می‌دهد. تمام درخواست‌های ارائه‌دهنده هوش مصنوعی از طریق شبکه لبه Vercel ارسال می‌شوند و IP واقعی شما را از ارائه‌دهندگان پنهان می‌کنند.", + "Desktop": "دسکتاپ", + "Detail": "جزئیات", + "Details": "جزئیات", + "Dimensions": "ابعاد", + "Disable": "غیرفعال‌سازی", + "Disable All": "غیرفعال‌سازی همه", + "Disable Tailscale": "غیرفعال‌سازی Tailscale", + "Disable Tunnel": "غیرفعال‌سازی تونل", + "Disable connections with depleted quota on the current page": "غیرفعال‌سازی اتصالات با سهمیه تمام شده در صفحه فعلی", + "Disable provider": "غیرفعال‌سازی ارائه‌دهنده", + "Disable this model": "غیرفعال‌سازی این مدل", + "Disabled": "غیرفعال", + "Disabling...": "در حال غیرفعال‌سازی...", + "Disconnected from server": "قطع شده از سرور", + "Dismiss notification": "رد اعلان", + "Display Name": "نام نمایشی", + "Display language": "زبان نمایش", + "Docs": "مستندات", + "Documentation": "مستندات", + "Domain:": "دامنه:", + "Donate": "کمک مالی", + "Done": "انجام شد", + "Download": "دانلود", + "Download Backup": "دانلود پشتیبان", + "Drag to reorder": "برای مرتب‌سازی دوباره بکشید", + "Easy Setup": "راه‌اندازی آسان", + "Edit": "ویرایش", + "Edit Combo": "ویرایش ترکیب", + "Edit Connection": "ویرایش اتصال", + "Edit Pricing": "ویرایش قیمت‌گذاری", + "Edit Proxy Pool": "ویرایش استخر پروکسی", + "Edit connection": "ویرایش اتصال", + "Edit hosts file manually to add the following entries:": "فایل hosts را به صورت دستی ویرایش کنید تا ورودی‌های زیر را اضافه کنید:", + "Email": "ایمیل", + "Embedding": "تعبیه", + "Embeddings": "تعبیه‌ها", + "Enable": "فعال‌سازی", + "Enable DNS per tool below to activate interception": "DNS را برای هر ابزار در زیر فعال کنید تا رهگیری فعال شود", + "Enable DNS to edit model mappings": "برای ویرایش نگاشت‌های مدل، DNS را فعال کنید", + "Enable Observability": "فعال‌سازی مشاهده‌پذیری", + "Enable OpenAI API": "فعال‌سازی OpenAI API", + "Enable Tunnel": "فعال‌سازی تونل", + "Enable connections that still have quota on the current page": "فعال‌سازی اتصالاتی که هنوز در صفحه فعلی سهمیه دارند", + "Enable provider": "فعال‌سازی ارائه‌دهنده", + "Enable proxy for OAuth + provider outbound requests.": "فعال‌سازی پروکسی برای درخواست‌های خروجی OAuth + ارائه‌دهنده.", + "Encrypted": "رمزگذاری شده", + "End Date": "تاریخ پایان", + "End-to-end TLS via Cloudflare": "TLS انتها به انتها از طریق Cloudflare", + "Endpoint": "نقطه پایانی", + "Endpoint & Key": "نقطه پایانی و کلید", + "Endpoint is exposed without an API key.": "نقطه پایانی بدون کلید API در معرض دسترسی است.", + "Enter current password": "رمز عبور فعلی را وارد کنید", + "Enter model id": "شناسه مدل را وارد کنید", + "Enter model id (provider-specific)": "شناسه مدل را وارد کنید (مخصوص ارائه‌دهنده)", + "Enter new API key": "کلید API جدید را وارد کنید", + "Enter new password": "رمز عبور جدید را وارد کنید", + "Enter or pick API key": "کلید API را وارد یا انتخاب کنید", + "Enter password": "رمز عبور را وارد کنید", + "Enter sudo password": "رمز عبور sudo را وارد کنید", + "Enter the model ID exactly as your compatible endpoint expects it. This model will be saved as the connection default.": "شناسه مدل را دقیقاً همانطور که نقطه پایانی سازگار شما انتظار دارد وارد کنید. این مدل به عنوان پیش‌فرض اتصال ذخیره می‌شود.", + "Enter your API key": "کلید API خود را وارد کنید", + "Enter your current password to": "رمز عبور فعلی خود را وارد کنید تا", + "Enter your password to access the dashboard": "برای دسترسی به داشبورد رمز عبور خود را وارد کنید", + "Error": "خطا", + "Est. Cost": "هزینه تقریبی", + "Estimated, not actual billing": "تخمینی، نه صورتحساب واقعی", + "Everything you need to manage your AI infrastructure efficiently.": "هر آنچه برای مدیریت کارآمد زیرساخت هوش مصنوعی خود نیاز دارید.", + "Everything you need to manage your AI infrastructure in one place, built for scale.": "هر آنچه برای مدیریت زیرساخت هوش مصنوعی خود در یک مکان نیاز دارید، ساخته شده برای مقیاس.", + "Example": "مثال", + "Experimental": "آزمایشی", + "Expires At": "منقضی می‌شود در", + "Expiring first": "ابتدا در حال انقضا", + "Expiring-first currently reorders accounts inside the current page. Cross-page ordering still follows backend pagination.": "«ابتدا در حال انقضا» در حال حاضر حساب‌ها را در صفحه فعلی دوباره مرتب می‌کند. ترتیب بین صفحه‌ها همچنان از صفحه‌بندی backend پیروی می‌کند.", + "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 و سایر ابزارهای هوش مصنوعی از هر جایی استفاده کنید.", + "Factory Droid - Manual Configuration": "Factory Droid - پیکربندی دستی", + "Factory Droid AI Assistant": "دستیار هوش مصنوعی Factory Droid", + "Factory Droid CLI not detected locally": "Factory Droid CLI در سیستم محلی شناسایی نشد", + "Factory Droid CLI not installed": "Factory Droid CLI نصب نشده است", + "Fail request if proxy is unreachable instead of falling back to direct.": "در صورت عدم دسترسی به پروکسی، درخواست را با شکست مواجه کنید به جای بازگشت به مستقیم.", + "Failed to apply settings": "اعمال تنظیمات ناموفق بود", + "Failed to create combo": "ایجاد ترکیب ناموفق بود", + "Failed to load changelog:": "بارگذاری تاریخچه تغییرات ناموفق بود:", + "Failed to load usage statistics.": "بارگذاری آمار مصرف ناموفق بود.", + "Failed to reset settings": "بازنشانی تنظیمات ناموفق بود", + "Failed to set alias": "تنظیم نام مستعار ناموفق بود", + "Failed to update combo": "به‌روزرسانی ترکیب ناموفق بود", + "Failed to update password": "به‌روزرسانی رمز عبور ناموفق بود", + "Failed to update proxy settings": "به‌روزرسانی تنظیمات پروکسی ناموفق بود", + "Fallback": "پشتیبان جایگزین", + "Fallback — tries models in order (next on failure)": "پشتیبان جایگزین — مدل‌ها را به ترتیب امتحان می‌کند (در صورت شکست به بعدی می‌رود)", + "Fallback — try in order": "پشتیبان جایگزین — به ترتیب امتحان کنید", + "Features": "ویژگی‌ها", + "Fetch Qoder Models": "دریافت مدل‌های Qoder", + "Fetching...": "در حال دریافت...", + "Files": "فایل‌ها", + "Filter accounts by status": "فیلتر حساب‌ها بر اساس وضعیت", + "Filter naming": "فیلتر نام‌گذاری", + "Filter naming requests": "فیلتر درخواست‌های نام‌گذاری", + "Filter quota providers": "فیلتر ارائه‌دهندگان سهمیه", + "Find MCPs →": "یافتن MCPها →", + "Find your Account ID in the right sidebar of": "شناسه حساب خود را در نوار کناری سمت راست پیدا کنید", + "Find your Account ID in the right sidebar of dash.cloudflare.com": "شناسه حساب خود را در نوار کناری سمت راست dash.cloudflare.com پیدا کنید", + "First Page": "صفحه اول", + "Flush Interval (ms)": "فاصله تخلیه (میلی‌ثانیه)", + "For enterprise users with custom AWS IAM Identity Center.": "برای کاربران سازمانی با مرکز هویت AWS IAM سفارشی.", + "Forgot password? Open": "رمز عبور را فراموش کرده‌اید؟ باز کنید", + "Format": "فرمت", + "Found on the right side of the Cloudflare dashboard overview page.": "در سمت راست صفحه نمای کلی داشبورد Cloudflare یافت می‌شود.", + "Free": "رایگان", + "Free & Free Tier Providers": "ارائه‌دهندگان رایگان و لایه رایگان", + "Free Providers": "ارائه‌دهندگان رایگان", + "Free Tier": "لایه رایگان", + "Free Tier Providers": "ارائه‌دهندگان لایه رایگان", + "Free tier: 100,000 requests per day": "لایه رایگان: ۱۰۰٬۰۰۰ درخواست در روز", + "Free tier: 100GB bandwidth/month, 500K edge invocations": "لایه رایگان: ۱۰۰ گیگابایت پهنای باند در ماه، ۵۰۰٬۰۰۰ فراخوانی لبه", + "Free tier: 1M requests & 100GiB outbound traffic per month": "لایه رایگان: ۱ میلیون درخواست و ۱۰۰ گیگابایت ترافیک خروجی در ماه", + "Fresh API key obtained": "کلید API جدید دریافت شد", + "Full shell access": "دسترسی کامل به شل", + "Fusion": "همجوشی", + "Fusion — panel + judge": "همجوشی — پنل + داور", + "Fusion — queries all models in parallel, then a judge synthesizes one answer": "همجوشی — همه مدل‌ها را به طور موازی پرس و جو می‌کند، سپس یک داور یک پاسخ را ترکیب می‌کند", + "Get 9Remote": "دریافت 9Remote", + "Get API Key": "دریافت کلید API", + "Get API Key →": "دریافت کلید API →", + "Get Started": "شروع کنید", + "Get Started in 30 Seconds": "شروع در ۳۰ ثانیه", + "Get started": "شروع کنید", + "Get started in seconds. Just install, open, and route.": "در چند ثانیه شروع کنید. فقط نصب کنید، باز کنید و مسیردهی کنید.", + "Get token →": "دریافت توکن →", + "GitHub": "GitHub", + "GitHub Account": "حساب GitHub", + "GitHub Copilot - Manual Configuration": "GitHub Copilot - پیکربندی دستی", + "GitHub Copilot IDE with MITM": "GitHub Copilot IDE با MITM", + "GitLab Access Tokens": "توکن‌های دسترسی GitLab", + "GitLab Applications": "برنامه‌های GitLab", + "GitLab Base URL": "آدرس پایه GitLab", + "Go to": "رفتن به", + "Go to Roo Settings panel": "رفتن به پنل تنظیمات Roo", + "Google Account": "حساب Google", + "Google Antigravity IDE with MITM": "Google Antigravity IDE با MITM", + "Granted At": "اعطا شده در", + "Group models under one name, then pick a strategy per combo:": "مدل‌ها را تحت یک نام گروه‌بندی کنید، سپس برای هر ترکیب یک استراتژی انتخاب کنید:", + "Headroom proxy is reachable. You can enable the token saver.": "پروکسی Headroom قابل دسترسی است. می‌توانید ذخیره‌ساز توکن را فعال کنید.", + "Help Center": "مرکز راهنما", + "Hermes Agent - Manual Configuration": "Hermes Agent - پیکربندی دستی", + "Hermes Agent not detected locally": "Hermes Agent در سیستم محلی شناسایی نشد", + "Hide": "پنهان کردن", + "Hide key": "پنهان کردن کلید", + "High performance global routing and IP masking via Cloudflare Workers": "مسیریابی جهانی با کارایی بالا و پنهان‌سازی IP از طریق Cloudflare Workers", + "High-performance Rust-based coding agent harness": "چارچوب عامل کدنویسی مبتنی بر Rust با کارایی بالا", + "History": "تاریخچه", + "How 9Router Works": "نحوه عملکرد 9Router", + "How Pricing Works": "نحوه عملکرد قیمت‌گذاری", + "How it Works": "نحوه عملکرد", + "How it works:": "نحوه عملکرد:", + "How to Install": "نحوه نصب", + "How to generate API token:": "نحوه تولید توکن API:", + "How to generate your API Token:": "نحوه تولید توکن API خود:", + "How to get cookie:": "نحوه دریافت کوکی:", + "ID:": "شناسه:", + "IDC Start URL": "آدرس شروع IDC", + "If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead.": "اگر ارائه‌دهنده نقطه پایانی /models را ندارد، یک شناسه مدل را برای اعتبارسنجی از طریق chat/completions وارد کنید.", + "Image Generation": "تولید تصویر", + "Image to Text": "تصویر به متن", + "Import": "وارد کردن", + "Import Backup": "وارد کردن پشتیبان", + "Import CLIProxyAPI JSON": "وارد کردن CLIProxyAPI JSON", + "Import Token": "وارد کردن توکن", + "Importing...": "در حال وارد کردن...", + "In": "ورودی", + "In / Out": "ورودی/خروجی", + "Inactive": "غیرفعال", + "Inactive pools are ignored by runtime resolution.": "استخرهای غیرفعال توسط وضوح زمان اجرا نادیده گرفته می‌شوند.", + "Inc. All rights reserved.": "شرکت. تمام حقوق محفوظ است.", + "Initializing...": "در حال مقداردهی اولیه...", + "Input": "ورودی", + "Input Cost": "هزینه ورودی", + "Input Tokens": "توکن‌های ورودی", + "Input Tokens:": "توکن‌های ورودی:", + "Input:": "ورودی:", + "Install 9Router": "نصب 9Router", + "Install 9Router, configure your providers via web dashboard, and start routing AI requests.": "9Router را نصب کنید، ارائه‌دهندگان خود را از طریق داشبورد وب پیکربندی کنید و مسیردهی درخواست‌های هوش مصنوعی را شروع کنید.", + "Install Chrome extension": "نصب افزونه Chrome", + "Install Cline VS Code extension or CLI from": "افزونه یا CLI Cline VS Code را از نصب کنید", + "Install Kilo Code from": "Kilo Code را از نصب کنید", + "Install Qwen Code": "نصب Qwen Code", + "Install Tailscale": "نصب Tailscale", + "Install command:": "دستور نصب:", + "Install jcode to enable automatic configuration:": "jcode را نصب کنید تا پیکربندی خودکار فعال شود:", + "Install the Amp CLI using the package manager supported by your environment.": "Amp CLI را با استفاده از مدیر بسته پشتیبانی شده توسط محیط خود نصب کنید.", + "Install then click Start:": "نصب کنید سپس روی شروع کلیک کنید:", + "Install via npm:": "نصب از طریق npm:", + "Installation Guide": "راهنمای نصب", + "Installing Tailscale...": "در حال نصب Tailscale...", + "Interactive diagram visible on desktop": "نمودار تعاملی در دسکتاپ قابل مشاهده است", + "Intercept CLI tool traffic and route through 9Router": "ترافیک ابزار CLI را رهگیری کرده و از طریق 9Router مسیردهی کنید", + "Intercepts Antigravity traffic via DNS redirect, letting you reroute models through 9Router.": "ترافیک Antigravity را از طریق تغییر مسیر DNS رهگیری می‌کند و به شما امکان می‌دهد مدل‌ها را از طریق 9Router مسیردهی مجدد کنید.", + "Intercepts Claude Code's topic-naming requests and returns a fake response locally, saving API tokens.": "درخواست‌های نام‌گذاری موضوع Claude Code را رهگیری کرده و یک پاسخ ساختگی به صورت محلی برمی‌گرداند و توکن‌های API را ذخیره می‌کند.", + "Invalid": "نامعتبر", + "Invalid password": "رمز عبور نامعتبر", + "Issuer URL": "آدرس صادرکننده", + "JSON Response": "پاسخ JSON", + "Join developers who are streamlining their AI integrations with 9Router. Open source and free to start.": "به توسعه‌دهندگانی بپیوندید که با 9Router یکپارچه‌سازی‌های هوش مصنوعی خود را ساده‌سازی می‌کنند. منبع باز و رایگان برای شروع.", + "Judge": "داور", + "Just now": "همین الان", + "KB per field": "کیلوبایت در هر فیلد", + "Keep the legacy password login.": "ورود با رمز عبور قدیمی را حفظ کنید.", + "Key Name": "نام کلید", + "KiRo dashboard": "داشبورد KiRo", + "Kill & Start": "پایان و شروع", + "Kill this process to start MITM Server?": "برای راه‌اندازی سرور MITM این فرآیند را پایان دهید؟", + "Kilo Code - Manual Configuration": "Kilo Code - پیکربندی دستی", + "Kilo Code AI Assistant": "دستیار هوش مصنوعی Kilo Code", + "Kilo Code not detected locally": "Kilo Code در سیستم محلی شناسایی نشد", + "Kimi": "Kimi", + "Kiro AI": "Kiro AI", + "Kiro IDE not detected. Please paste your refresh token manually.": "Kiro IDE شناسایی نشد. لطفاً توکن بازسازی خود را به صورت دستی بچسبانید.", + "Kiro IDE with MITM": "Kiro IDE با MITM", + "Language": "زبان", + "Languages": "زبان‌ها", + "Last Page": "آخرین صفحه", + "Last Used": "آخرین استفاده", + "Last tested:": "آخرین آزمایش:", + "Last updated:": "آخرین به‌روزرسانی:", + "Latency": "تاخیر", + "Latency:": "تاخیر:", + "Lazy senior dev": "توسعه‌دهنده ارشد تنبل", + "Lean": "ساده", + "Leave blank to keep existing secret": "برای حفظ راز موجود خالی بگذارید", + "Leave blank to use": "برای استفاده خالی بگذارید", + "Leave empty for public PKCE app": "برای برنامه PKCE عمومی خالی بگذارید", + "Leave empty to inherit existing env proxy (if any).": "برای ارث‌بری از پروکسی موجود محیط، خالی بگذارید (در صورت وجود).", + "Legacy manual proxy fields are still accepted by API for backward compatibility.": "فیلدهای پروکسی دستی قدیمی هنوز برای سازگاری با گذشته توسط API پذیرفته می‌شوند.", + "Legacy:": "قدیمی:", + "Legal": "قانونی", + "Live server console output": "خروجی کنسول سرور زنده", + "Load": "بارگذاری", + "Loading logs...": "در حال بارگذاری لاگ‌ها...", + "Loading models from provider...": "در حال بارگذاری مدل‌ها از ارائه‌دهنده...", + "Loading pricing data...": "در حال بارگذاری داده‌های قیمت‌گذاری...", + "Loading registry...": "در حال بارگذاری رجیستری...", + "Loading reset credits...": "در حال بارگذاری اعتبارات بازنشانی...", + "Loading...": "در حال بارگذاری...", + "Local": "محلی", + "Local Mode": "حالت محلی", + "Local Mode - All data stored on your machine": "حالت محلی - تمام داده‌ها روی دستگاه شما ذخیره می‌شوند", + "Local Plugins": "افزونه‌های محلی", + "Locked. Retry in": "قفل شد. دوباره تلاش کنید در", + "Login": "ورود", + "Login Button Label": "برچسب دکمه ورود", + "Login URL": "آدرس ورود", + "Login to your account": "وارد حساب خود شوید", + "Login with your GitHub account (manual callback).": "با حساب GitHub خود وارد شوید (بازگشت دستی).", + "Login with your Google account (manual callback).": "با حساب Google خود وارد شوید (بازگشت دستی).", + "Logout": "خروج", + "Logs": "لاگ‌ها", + "Logs are loaded from the request history database.": "لاگ‌ها از پایگاه داده تاریخچه درخواست بارگذاری می‌شوند.", + "Logs are saved to log.txt in the application data directory.": "لاگ‌ها در log.txt در دایرکتوری داده برنامه ذخیره می‌شوند.", + "MIT License": "مجوز MIT", + "MITM": "MITM", + "MITM Proxy": "پروکسی MITM", + "MITM Server": "سرور MITM", + "MITM Tools": "ابزارهای MITM", + "Machine ID": "شناسه ماشین", + "Machine ID will be auto-filled...": "شناسه ماشین به‌طور خودکار پر می‌شود...", + "Make sure Cursor IDE has been opened at least once, then click": "مطمئن شوید Cursor IDE حداقل یک بار باز شده است، سپس کلیک کنید", + "Manage": "مدیریت", + "Manage reusable per-connection proxies and bind them to provider connections.": "پروکسی‌های قابل استفاده مجدد به ازای هر اتصال را مدیریت کرده و آنها را به اتصالات ارائه‌دهنده متصل کنید.", + "Manage your AI provider connections": "مدیریت اتصالات ارائه‌دهندگان هوش مصنوعی خود", + "Manage your Embedding providers": "مدیریت ارائه‌دهندگان تعبیه خود", + "Manage your Image to Text providers": "مدیریت ارائه‌دهندگان تصویر به متن خود", + "Manage your Music providers": "مدیریت ارائه‌دهندگان موسیقی خود", + "Manage your Speech To Text providers": "مدیریت ارائه‌دهندگان گفتار به متن خود", + "Manage your Text To Speech providers": "مدیریت ارائه‌دهندگان متن به گفتار خود", + "Manage your Text to Image providers": "مدیریت ارائه‌دهندگان متن به تصویر خود", + "Manage your Video providers": "مدیریت ارائه‌دهندگان ویدیوی خود", + "Manage your Web Fetch providers": "مدیریت ارائه‌دهندگان دریافت وب خود", + "Manage your Web Search providers": "مدیریت ارائه‌دهندگان جستجوی وب خود", + "Manage your preferences": "مدیریت تنظیمات شخصی", + "Manage your proxy pool configurations": "مدیریت پیکربندی‌های استخر پروکسی خود", + "Manual / current endpoint": "دستی / نقطه پایانی فعلی", + "Manual Callback Required": "بازگشت دستی مورد نیاز است", + "Manual Config": "پیکربندی دستی", + "Manual configuration is still available if 9router is deployed on a remote server.": "اگر 9router روی یک سرور راه دور مستقر شده باشد، پیکربندی دستی همچنان در دسترس است.", + "Map Amp shorthand names such as g25p or cs45 to 9Router aliases in your local config.": "نام‌های میان‌نویس Amp مانند g25p یا cs45 را به نام‌های مستعار 9Router در پیکربندی محلی خود نگاشت کنید.", + "Mask (URL)": "ماسک (URL)", + "Max JSON Size (KB)": "حداکثر اندازه JSON (کیلوبایت)", + "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)": "حداکثر زمان انتظار قبل از تخلیه بافر (از از دست رفتن داده در ترافیک کم جلوگیری می‌کند)", + "Media Providers": "ارائه‌دهندگان رسانه", + "Menu": "منو", + "Message AI": "ارسال پیام به هوش مصنوعی", + "Messages": "پیام‌ها", + "Messages API": "API پیام‌ها", + "MiniMax": "MiniMax", + "Model": "مدل", + "Model Fallback": "پشتیبان مدل", + "Model ID": "شناسه مدل", + "Model ID (from OpenRouter)": "شناسه مدل (از OpenRouter)", + "Model ID (optional)": "شناسه مدل (اختیاری)", + "Model Status": "وضعیت مدل", + "Model combos": "ترکیبات مدل", + "Model combos with fallback": "ترکیبات مدل با پشتیبان جایگزین", + "Model is reachable": "مدل قابل دسترسی است", + "Model list is filtered from connected providers.": "لیست مدل‌ها از ارائه‌دهندگان متصل فیلتر شده است.", + "Model mappings will be available soon.": "نگاشت‌های مدل به زودی در دسترس خواهند بود.", + "Model not reachable": "مدل قابل دسترسی نیست", + "Model:": "مدل:", + "Models": "مدل‌ها", + "Monitor your API usage, token consumption, and request logs": "نظارت بر مصرف API، مصرف توکن و لاگ درخواست‌ها", + "More on GitHub": "بیشتر در GitHub", + "Move down": "پایین آوردن", + "Move up": "بالا بردن", + "Music": "موسیقی", + "My Profile": "پروفایل من", + "N/A": "ناموجود", + "NPM": "NPM", + "Name": "نام", + "Name is required": "نام الزامی است", + "Native CLI tool support for Cursor, Claude, Copilot, and more.": "پشتیبانی بومی از ابزارهای CLI برای Cursor، Claude، Copilot و بیشتر.", + "Navigate to home": "رفتن به صفحه اصلی", + "Network": "شبکه", + "Network Error": "خطای شبکه", + "Network error": "خطای شبکه", + "Never": "هرگز", + "New Password": "رمز عبور جدید", + "New password": "رمز عبور جدید", + "Next": "بعدی", + "Next accounts page": "صفحه بعدی حساب‌ها", + "No API keys - Create one in Keys page": "بدون کلید API - یکی در صفحه کلیدها ایجاد کنید", + "No API keys yet": "هنوز کلید API وجود ندارد", + "No MCPs added": "هیچ MCP اضافه نشده است", + "No Providers Connected": "هیچ ارائه‌دهنده‌ای متصل نیست", + "No Proxy": "بدون پروکسی", + "No active connections found for this group.": "هیچ اتصال فعالی برای این گروه یافت نشد.", + "No active providers": "هیچ ارائه‌دهنده فعالی وجود ندارد", + "No active proxy pools available. Create one in Proxy Pools page first.": "هیچ استخر پروکسی فعالی در دسترس نیست. ابتدا یکی را در صفحه استخرهای پروکسی ایجاد کنید.", + "No authentication required": "نیازی به احراز هویت نیست", + "No combos yet": "هنوز ترکیبی وجود ندارد", + "No combos yet.": "هنوز ترکیبی وجود ندارد.", + "No compatible providers added yet": "هنوز هیچ ارائه‌دهنده سازگاری اضافه نشده است", + "No connections": "بدون اتصال", + "No connections yet": "هنوز اتصالی وجود ندارد", + "No console logs yet.": "هنوز لاگ کنسولی وجود ندارد.", + "No conversations yet.": "هنوز گفتگویی وجود ندارد.", + "No custom providers": "هیچ ارائه‌دهنده سفارشی وجود ندارد", + "No custom providers — use buttons above to add OpenAI/Anthropic compatible endpoints": "هیچ ارائه‌دهنده سفارشی وجود ندارد — از دکمه‌های بالا برای افزودن نقاط پایانی سازگار با OpenAI/Anthropic استفاده کنید", + "No data for this period": "داده‌ای برای این دوره وجود ندارد", + "No key configured": "هیچ کلیدی پیکربندی نشده است", + "No language selected": "هیچ زبانی انتخاب نشده است", + "No languages found.": "هیچ زبانی یافت نشد.", + "No logs recorded yet.": "هنوز هیچ لاگی ثبت نشده است.", + "No model selected.": "هیچ مدلی انتخاب نشده است.", + "No models": "هیچ مدلی", + "No models added yet": "هنوز هیچ مدلی اضافه نشده است", + "No models configured": "هیچ مدلی پیکربندی نشده است", + "No models found": "هیچ مدلی یافت نشد", + "No models match your filter.": "هیچ مدلی با فیلتر شما مطابقت ندارد.", + "No models selected": "هیچ مدلی انتخاب نشده است", + "No port forwarding needed": "نیازی به انتقال پورت نیست", + "No pricing data available": "هیچ داده قیمت‌گذاری در دسترس نیست", + "No providers connected": "هیچ ارائه‌دهنده‌ای متصل نیست", + "No providers match your search": "هیچ ارائه‌دهنده‌ای با جستجوی شما مطابقت ندارد", + "No providers support": "هیچ ارائه‌دهنده‌ای پشتیبانی نمی‌کند", + "No providers yet.": "هنوز هیچ ارائه‌دهنده‌ای وجود ندارد.", + "No providers.": "هیچ ارائه‌دهنده‌ای وجود ندارد.", + "No proxy pool entries yet": "هنوز هیچ ورودی استخر پروکسی وجود ندارد", + "No proxy:": "بدون پروکسی:", + "No quota data available": "هیچ داده سهمیه‌ای در دسترس نیست", + "No request details found": "هیچ جزئیات درخواستی یافت نشد", + "No requests yet.": "هنوز هیچ درخواستی وجود ندارد.", + "No reset credit details returned for this account.": "هیچ جزئیات اعتبار بازنشانی برای این حساب بازگردانده نشد.", + "No results": "نتیجه‌ای یافت نشد", + "No servers match filter": "هیچ سروری با فیلتر مطابقت ندارد", + "No tools advertised by server.": "هیچ ابزاری توسط سرور اعلام نشده است.", + "No usage yet.": "هنوز مصرفی وجود ندارد.", + "None": "هیچکدام", + "None (unbind all)": "هیچکدام (لغو پیوند همه)", + "Not configured": "پیکربندی نشده", + "Not installed": "نصب نشده", + "Notice": "توجه", + "Nous Research self-improving AI agent": "عامل هوش مصنوعی خودبهبود Nous Research", + "Number of items to accumulate before writing to database (higher = better performance)": "تعداد موارد قبل از نوشتن در پایگاه داده (بیشتر = عملکرد بهتر)", + "OAuth": "OAuth", + "OAuth & API Keys": "OAuth و کلیدهای API", + "OAuth Account": "حساب OAuth", + "OAuth App": "برنامه OAuth", + "OAuth Providers": "ارائه‌دهندگان OAuth", + "OAuth required": "نیاز به OAuth", + "OIDC Dashboard Login": "ورود به داشبورد با OIDC", + "OIDC active": "OIDC فعال است", + "OIDC login is currently active. Password login is disabled until you switch back.": "ورود با OIDC در حال حاضر فعال است. ورود با رمز عبور تا زمانی که تغییر دهید غیرفعال است.", + "OIDC login is enabled, but the issuer/client fields are not configured yet. Password login is still available for recovery.": "ورود با OIDC فعال است، اما فیلدهای صادرکننده/مشتری هنوز پیکربندی نشده‌اند. ورود با رمز عبور همچنان برای بازیابی در دسترس است.", + "OIDC only": "فقط OIDC", + "Observability": "مشاهده‌پذیری", + "Office Proxy": "پروکسی اداری", + "Ollama Host URL": "آدرس میزبان Ollama", + "One Endpoint for": "یک نقطه پایانی برای", + "One key per line. Format:": "یک کلید در هر خط. فرمت:", + "One-to-one (rotate)": "یک به یک (چرخش)", + "Only from connected providers": "فقط از ارائه‌دهندگان متصل", + "Only letters, numbers, - and _ allowed": "فقط حروف، اعداد، - و _ مجاز است", + "Only letters, numbers, -, _ and .": "فقط حروف، اعداد، -، _ و .", + "Only letters, numbers, -, _ and . allowed": "فقط حروف، اعداد، -، _ و . مجاز است", + "Only one connection is allowed per compatible node. Add another node if you need more connections.": "به ازای هر گره سازگار فقط یک اتصال مجاز است. در صورت نیاز به اتصالات بیشتر، گره دیگری اضافه کنید.", + "Open": "باز کردن", + "Open Claude Desktop → Help → Troubleshooting → Enable Developer mode → Configure third-party inference, then return here.": "Claude Desktop → Help → Troubleshooting → Enable Developer mode → Configure third-party inference را باز کنید، سپس به اینجا بازگردید.", + "Open Claw - Manual Configuration": "Open Claw - پیکربندی دستی", + "Open Claw AI Assistant": "دستیار هوش مصنوعی Open Claw", + "Open Claw CLI not detected locally": "Open Claw CLI در سیستم محلی شناسایی نشد", + "Open Claw CLI not installed": "Open Claw CLI نصب نشده است", + "Open Continue configuration file": "باز کردن فایل پیکربندی Continue", + "Open Dashboard": "باز کردن داشبورد", + "Open DevTools (F12) → Application/Storage → Cookies": "DevTools (F12) → Application/Storage → Cookies را باز کنید", + "Open Settings": "باز کردن تنظیمات", + "Open platform.iflow.cn in your browser": "platform.iflow.cn را در مرورگر خود باز کنید", + "OpenAI / ElevenLabs / Edge / Google / Deepgram voices.": "صداهای OpenAI / ElevenLabs / Edge / Google / Deepgram.", + "OpenAI Codex CLI": "OpenAI Codex CLI", + "OpenAI Compatible (Prod)": "سازگار با OpenAI (تولید)", + "OpenAI Compatible Details": "جزئیات سازگاری با OpenAI", + "OpenAI Intermediate": "قالب میانی OpenAI", + "OpenAI Response": "پاسخ OpenAI", + "OpenCode - Manual Configuration": "OpenCode - پیکربندی دستی", + "OpenCode AI Terminal Assistant": "دستیار ترمینال هوش مصنوعی OpenCode", + "OpenCode CLI not detected locally": "OpenCode CLI در سیستم محلی شناسایی نشد", + "OpenCode CLI not installed": "OpenCode CLI نصب نشده است", + "OpenRouter": "OpenRouter", + "OpenRouter supports any model. Add models and create aliases for quick access.": "OpenRouter از هر مدلی پشتیبانی می‌کند. مدل‌ها را اضافه کرده و برای دسترسی سریع نام مستعار ایجاد کنید.", + "Optional SSO via Authentik/Keycloak/Google": "SSO اختیاری از طریق Authentik/Keycloak/Google", + "Or paste callback URL manually": "یا آدرس پاسخ بازگشت را به صورت دستی بچسبانید", + "Organization": "سازمان", + "Organization Domain": "دامنه سازمان", + "Organization ID": "شناسه سازمان", + "Organization Token": "توکن سازمان", + "Organization Tokens": "توکن‌های سازمان", + "Other": "سایر", + "Our engine analyzes the prompt and routes through your subscription, cheap, and free provider tiers with automatic fallback.": "موتور ما پرامپت را تحلیل کرده و از طریق لایه‌های اشتراک، ارزان و رایگان ارائه‌دهنده با بازگشت خودکار مسیردهی می‌کند.", + "Our engine analyzes the prompt, checks provider health, and routes for lowest latency or cost.": "موتور ما پرامپت را تحلیل کرده، سلامت ارائه‌دهنده را بررسی کرده و برای کمترین تاخیر یا هزینه مسیردهی می‌کند.", + "Out": "خروجی", + "Outbound Proxy": "پروکسی خروجی", + "Output": "خروجی", + "Output Cost": "هزینه خروجی", + "Output Format": "قالب خروجی", + "Output Tokens": "توکن‌های خروجی", + "Output Tokens:": "توکن‌های خروجی:", + "Output:": "خروجی:", + "Overview": "بررسی کلی", + "Paid": "پولی", + "Partial preview": "پیش‌نمایش جزئی", + "Password": "رمز عبور", + "Password + OIDC active": "رمز عبور + OIDC فعال است", + "Password and OIDC login are both active.": "ورود با رمز عبور و OIDC هر دو فعال هستند.", + "Password and OIDC login are both enabled.": "ورود با رمز عبور و OIDC هر دو فعال شده‌اند.", + "Password only": "فقط رمز عبور", + "Password updated successfully": "رمز عبور با موفقیت به‌روزرسانی شد", + "Passwords do not match": "رمزهای عبور مطابقت ندارند", + "Paste Proxy List (One per line)": "چسباندن لیست پروکسی (یک در هر خط)", + "Paste a long-lived Kiro/CodeWhisperer API key. It is validated against AWS and stored directly as a bearer credential (no refresh).": "یک کلید API طولانی‌مدت Kiro/CodeWhisperer را بچسبانید. در برابر AWS تأیید شده و مستقیماً به عنوان اعتبارنامه Bearer ذخیره می‌شود (بدون بازسازی).", + "Paste external_idp auth JSON from CLIProxyAPI/Kiro Microsoft login.": "JSON احراز هویت external_idp را از ورود Microsoft CLIProxyAPI/Kiro بچسبانید.", + "Paste it below": "آن را در زیر بچسبانید", + "Paste refresh token from Kiro IDE.": "توکن بازسازی را از Kiro IDE بچسبانید.", + "Paste the Kiro CLIProxyAPI auth JSON containing auth_method=external_idp. Only Microsoft login token endpoints are accepted.": "JSON احراز هویت Kiro CLIProxyAPI حاوی auth_method=external_idp را بچسبانید. فقط نقاط پایانی توکن ورود Microsoft پذیرفته می‌شوند.", + "Paste the URL from your browser address bar": "URL را از نوار آدرس مرورگر خود بچسبانید", + "Paste the command into your terminal and press Enter.": "دستور را در ترمینال خود بچسبانید و Enter را فشار دهید.", + "Paste this to your AI:": "این را به هوش مصنوعی خود بچسبانید:", + "Paste your Kiro API key...": "کلید API Kiro خود را بچسبانید...", + "Pause API Key": "مکث کلید API", + "Pause key": "مکث کلید", + "Paused": "مکث شده", + "Permissions": "مجوزها", + "Personal Access Token": "توکن دسترسی شخصی", + "Pick the model that fuses panel answers": "مدلی را انتخاب کنید که پاسخ‌های پنل را ترکیب می‌کند", + "Please add an active Qoder connection first": "لطفاً ابتدا یک اتصال Qoder فعال اضافه کنید", + "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": "لطفاً یک آدرس پروکسی برای آزمایش وارد کنید", + "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.": "لطفاً در حالی که مجوز را تکمیل می‌کنیم، منتظر بمانید.", + "Point your CLI tools to http://localhost:20128": "ابزارهای CLI خود را به http://localhost:20128 هدایت کنید", + "Pool:": "استخر:", + "Popup blocked? Enter URL manually": "پنجره بازشو مسدود شد؟ URL را به صورت دستی وارد کنید", + "Port 443 Already In Use": "پورت ۴۴۳ در حال استفاده است", + "Port 443 is currently used by another process:": "پورت ۴۴۳ در حال حاضر توسط فرآیند دیگری استفاده می‌شود:", + "Powerful Features": "ویژگی‌های قدرتمند", + "Prefix": "پیشوند", + "Preset": "تنظیم از پیش", + "Prev": "قبلی", + "Preview": "پیش‌نمایش", + "Previous accounts page": "صفحه قبلی حساب‌ها", + "Pricing": "قیمت‌گذاری", + "Pricing Configuration": "پیکربندی قیمت‌گذاری", + "Pricing Format:": "فرمت قیمت‌گذاری:", + "Pricing Rates Format": "قالب نرخ‌های قیمت‌گذاری", + "Pricing Settings": "تنظیمات قیمت‌گذاری", + "Priority": "اولویت", + "Privacy Policy": "سیاست حفظ حریم خصوصی", + "Probing server for tools...": "در حال بررسی سرور برای ابزارها...", + "Processing...": "در حال پردازش...", + "Product": "محصول", + "Production Key": "کلید تولید", + "Project Name": "نام پروژه", + "Prompt": "پرامپت", + "Provider": "ارائه‌دهنده", + "Provider Details": "جزئیات ارائه‌دهنده", + "Provider Limits": "محدودیت‌های ارائه‌دهنده", + "Provider Response": "پاسخ ارائه‌دهنده", + "Provider not found": "ارائه‌دهنده یافت نشد", + "Provider test failed": "آزمایش ارائه‌دهنده ناموفق بود", + "Provider:": "ارائه‌دهنده:", + "Providers": "ارائه‌دهندگان", + "Proxy": "پروکسی", + "Proxy Action": "عملیات پروکسی", + "Proxy Pool": "استخر پروکسی", + "Proxy Pools": "استخرهای پروکسی", + "Proxy URL": "آدرس پروکسی", + "Proxy disabled": "پروکسی غیرفعال شد", + "Proxy enabled": "پروکسی فعال شد", + "Proxy pool created": "استخر پروکسی ایجاد شد", + "Proxy pool deleted": "استخر پروکسی حذف شد", + "Proxy pool updated": "استخر پروکسی به‌روزرسانی شد", + "Proxy settings applied": "تنظیمات پروکسی اعمال شد", + "Proxy test OK": "آزمایش پروکسی موفق بود", + "Proxy test failed": "آزمایش پروکسی ناموفق بود", + "Proxy test passed": "آزمایش پروکسی گذرانده شد", + "Purpose:": "هدف:", + "Python >= 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "برای حالت مدیریت محلی به Python >= 3.10 نیاز است. ابتدا Python را نصب کنید یا از آدرس پروکسی خارجی استفاده کنید.", + "Python ≥ 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "برای حالت مدیریت محلی به Python ≥ 3.10 نیاز است. ابتدا Python را نصب کنید یا از آدرس پروکسی خارجی استفاده کنید.", + "Quota Tracker": "پیگیری سهمیه", + "Qwen": "Qwen", + "Qwen Code supports multiple provider types (openai, anthropic, gemini) via modelProviders in settings.json. 9Router works as an OpenAI-compatible endpoint.": "Qwen Code از انواع مختلف ارائه‌دهندگان (openai، anthropic، gemini) از طریق modelProviders در settings.json پشتیبانی می‌کند. 9Router به عنوان یک نقطه پایانی سازگار با OpenAI کار می‌کند.", + "Qwen OAuth free tier was discontinued on 2026-04-15. Use 9Router with alicode/openrouter/anthropic/gemini providers instead.": "لایه رایگان OAuth Qwen در ۲۰۲۶-۰۴-۱۵ متوقف شد. به جای آن از 9Router با ارائه‌دهندگان alicode/openrouter/anthropic/gemini استفاده کنید.", + "Rate Limited": "محدودیت نرخ", + "Read Documentation": "مطالعه مستندات", + "Reading from AWS SSO cache": "خواندن از حافظه پنهان AWS SSO", + "Reading from Cursor IDE database": "خواندن از پایگاه داده Cursor IDE", + "Ready": "آماده", + "Ready to Simplify Your AI Infrastructure?": "آماده ساده‌سازی زیرساخت هوش مصنوعی خود هستید؟", + "Ready to route! ✓": "آماده برای مسیردهی! ✓", + "Ready! Requests route automatically through your configured providers.": "آماده! درخواست‌ها به‌طور خودکار از طریق ارائه‌دهندگان پیکربندی شده شما مسیردهی می‌شوند.", + "Reasoning": "استدلال", + "Reasoning:": "استدلال:", + "Recent Requests": "درخواست‌های اخیر", + "Recent chats": "گفتگوهای اخیر", + "Recheck": "بررسی مجدد", + "Recommended for most users. Free AWS account required.": "توصیه شده برای اکثر کاربران. نیاز به حساب رایگان AWS دارد.", + "Record request details for inspection in the logs view": "ثبت جزئیات درخواست برای بازرسی در نمای لاگ‌ها", + "Redirect URI": "URI تغییر مسیر", + "Ref Image (URL)": "تصویر مرجع (URL)", + "Refresh": "تازه‌سازی", + "Refresh All": "تازه‌سازی همه", + "Refresh Token": "توکن بازسازی", + "Refresh all": "تازه‌سازی همه", + "Refresh quota": "تازه‌سازی سهمیه", + "Region": "منطقه", + "Reload Page": "بارگذاری مجدد صفحه", + "Reload VS Code after applying for changes to take effect.": "پس از اعمال، VS Code را دوباره بارگذاری کنید تا تغییرات اعمال شوند.", + "Remaining": "باقیمانده", + "Remote": "دور", + "Remove": "حذف", + "Remove attachment": "حذف پیوست", + "Remove custom model": "حذف مدل سفارشی", + "Remove model": "حذف مدل", + "Replaces built-in WebSearch/WebFetch. Auto-strips duplicates from tool list.": "جایگزین WebSearch/WebFetch داخلی می‌شود. به‌طور خودکار موارد تکراری را از لیست ابزارها حذف می‌کند.", + "Replay request flow — matches log files": "پخش مجدد جریان درخواست — مطابق با فایل‌های لاگ", + "Request": "درخواست", + "Request Details": "جزئیات درخواست", + "Request Logs": "لاگ‌های درخواست", + "Requests": "درخواست‌ها", + "Requests without a valid key will be rejected": "درخواست‌های بدون کلید معتبر رد می‌شوند", + "Require API key": "نیاز به کلید API", + "Require OIDC for dashboard access.": "برای دسترسی به داشبورد به OIDC نیاز است.", + "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 مورد نیاز است", + "Required. A friendly label for this node.": "الزامی. یک برچسب دوستانه برای این گره.", + "Required. Used as the provider prefix for model IDs.": "الزامی. به عنوان پیشوند ارائه‌دهنده برای شناسه‌های مدل استفاده می‌شود.", + "Requires \"Workers Scripts: Edit\" permission.": "نیاز به مجوز \"Workers Scripts: Edit\" دارد.", + "Requires Cloudflare Account ID and a Workers API Token (Edit Workers permission)": "نیاز به شناسه حساب Cloudflare و یک توکن Workers API (مجوز ویرایش Workers) دارد", + "Requires Cursor Pro account to use this feature.": "برای استفاده از این ویژگی به حساب Cursor Pro نیاز است.", + "Requires jcode installed. Install via: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash": "به نصب jcode نیاز دارد. نصب از طریق: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash", + "Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.": "نیاز به پورت خروجی ۷۸۴۴ (TCP/UDP) دارد. اتصال ممکن است ۱۰-۳۰ ثانیه طول بکشد.", + "Reset": "بازنشانی", + "Reset Codex limit?": "بازنشانی محدودیت Codex؟", + "Reset Password to Default": "بازنشانی رمز عبور به پیش‌فرض", + "Reset judge to Auto": "بازنشانی داور به خودکار", + "Reset time": "زمان بازنشانی", + "Reset to Defaults": "بازنشانی به پیش‌فرض", + "Reset to default": "بازنشانی به پیش‌فرض", + "Resources": "منابع", + "Response": "پاسخ", + "Response Format": "قالب پاسخ", + "Responses": "پاسخ‌ها", + "Responses API": "API پاسخ‌ها", + "Restart": "راه‌اندازی مجدد", + "Restore model": "بازیابی مدل", + "Resume key": "ادامه کلید", + "Retry": "تلاش مجدد", + "Risk Notice": "اطلاعیه ریسک", + "Roo AI Assistant": "دستیار هوش مصنوعی Roo", + "Rotate providers across requests instead of strict fallback order.": "ارائه‌دهندگان را در بین درخواست‌ها به جای ترتیب بازگشت دقیق، بچرخانید.", + "Round Robin": "چرخشی", + "Round Robin — rotate": "چرخشی — چرخش", + "Round Robin — rotates models across requests to spread load": "چرخشی — مدل‌ها را در بین درخواست‌ها برای توزیع بار می‌چرخاند", + "Route AI requests through subscription, cheap, and free tiers with auto-fallback. One endpoint for Claude, GPT, Gemini, and more.": "درخواست‌های هوش مصنوعی را از طریق لایه‌های اشتراک، ارزان و رایگان با بازگشت خودکار مسیردهی کنید. یک نقطه پایانی برای Claude، GPT، Gemini و بیشتر.", + "Route Requests": "مسیردهی درخواست‌ها", + "Routing Strategy": "استراتژی مسیردهی", + "Rows:": "ردیف‌ها:", + "Run": "اجرا", + "Run npx command to start the server instantly": "دستور npx را برای راه‌اندازی فوری سرور اجرا کنید", + "Run this command in your terminal, then click": "این دستور را در ترمینال خود اجرا کنید، سپس کلیک کنید", + "Running": "در حال اجرا", + "Running on your machine": "در حال اجرا روی دستگاه شما", + "Runtime": "زمان اجرا", + "SSE URL": "آدرس SSE", + "START HERE": "از اینجا شروع کنید", + "Save": "ذخیره", + "Save Changes": "ذخیره تغییرات", + "Save Config": "ذخیره پیکربندی", + "Save Mappings": "ذخیره نگاشت‌ها", + "Save auth mode": "ذخیره حالت احراز هویت", + "Save current Base URL and API key as a browser-local preset": "ذخیره آدرس پایه و کلید API فعلی به عنوان یک تنظیم از پیش محلی مرورگر", + "Save this key now!": "این کلید را همین حالا ذخیره کنید!", + "Saved": "ذخیره شد", + "Saving": "در حال ذخیره", + "Saving...": "در حال ذخیره...", + "Scan QR to connect instantly": "برای اتصال فوری، QR را اسکن کنید", + "Scopes": "حوزه‌ها", + "Screen sharing": "اشتراک‌گذاری صفحه", + "Scroll down to": "به پایین اسکرول کنید تا", + "Search by name or description...": "جستجو بر اساس نام یا توضیحات...", + "Search language...": "جستجوی زبان...", + "Search model id": "جستجوی شناسه مدل", + "Search providers...": "جستجوی ارائه‌دهندگان...", + "Search...": "جستجو...", + "Security": "امنیت", + "Security required: ": "نیاز به امنیت: ", + "Security risk: no password set. You will be asked to set one when logging in remotely.": "خطر امنیتی: رمز عبور تنظیم نشده است. هنگام ورود از راه دور از شما خواسته می‌شود یک رمز عبور تنظیم کنید.", + "Select": "انتخاب", + "Select All": "انتخاب همه", + "Select Cowork Model": "انتخاب مدل Cowork", + "Select Endpoint": "انتخاب نقطه پایانی", + "Select Judge Model": "انتخاب مدل داور", + "Select Language": "انتخاب زبان", + "Select Model": "انتخاب مدل", + "Select Model for Cline": "انتخاب مدل برای Cline", + "Select Model for Codex": "انتخاب مدل برای Codex", + "Select Model for DeepSeek TUI": "انتخاب مدل برای DeepSeek TUI", + "Select Model for Factory Droid": "انتخاب مدل برای Factory Droid", + "Select Model for GitHub Copilot": "انتخاب مدل برای GitHub Copilot", + "Select Model for Hermes Agent": "انتخاب مدل برای Hermes Agent", + "Select Model for Kilo Code": "انتخاب مدل برای Kilo Code", + "Select Model for Open Claw": "انتخاب مدل برای Open Claw", + "Select Model for OpenCode": "انتخاب مدل برای OpenCode", + "Select Model for jcode": "انتخاب مدل برای jcode", + "Select Provider": "انتخاب ارائه‌دهنده", + "Select Subagent Model for Codex": "انتخاب مدل زیرعامل برای Codex", + "Select Subagent Model for OpenCode": "انتخاب مدل زیرعامل برای OpenCode", + "Select a provider": "یک ارائه‌دهنده انتخاب کنید", + "Select all": "انتخاب همه", + "Select language": "انتخاب زبان", + "Select models to add": "مدل‌ها را برای افزودن انتخاب کنید", + "Select one or more connections, then click Proxy Action.": "یک یا چند اتصال را انتخاب کنید، سپس روی عملیات پروکسی کلیک کنید.", + "Select to pre-fill, then edit model ID in the input": "برای پیش‌پر کردن انتخاب کنید، سپس شناسه مدل را در ورودی ویرایش کنید", + "Select your": "خود را انتخاب کنید", + "Selected connections have mixed proxy bindings": "اتصالات انتخاب شده دارای پیوندهای پروکسی مختلط هستند", + "Selected only": "فقط انتخاب شده", + "Selected provider": "ارائه‌دهنده انتخاب شده", + "Selecting None will unbind selected connections from proxy pool.": "انتخاب «هیچکدام» پیوند اتصالات انتخاب شده را از استخر پروکسی لغو می‌کند.", + "Send": "ارسال", + "Send to Provider": "ارسال به ارائه‌دهنده", + "Sent to provider as:": "ارسال به ارائه‌دهنده به عنوان:", + "Server": "سرور", + "Server Disconnected": "سرور قطع شد", + "Server off": "سرور خاموش", + "Server running on": "سرور در حال اجرا روی", + "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "سرویس در ترمینال در حال اجراست. می‌توانید این صفحه وب را ببندید. خاموش کردن، سرویس را متوقف می‌کند.", + "Set Password": "تنظیم رمز عبور", + "Set a new password before accessing the dashboard remotely.": "قبل از دسترسی از راه دور به داشبورد، یک رمز عبور جدید تنظیم کنید.", + "Set password": "تنظیم رمز عبور", + "Setting password for the first time. Leave current password empty or use default:": "تنظیم رمز عبور برای اولین بار. رمز عبور فعلی را خالی بگذارید یا از پیش‌فرض استفاده کنید:", + "Setting up": "در حال راه‌اندازی", + "Settings": "تنظیمات", + "Settings applied successfully!": "تنظیمات با موفقیت اعمال شد!", + "Settings reset successfully!": "تنظیمات با موفقیت بازنشانی شد!", + "Setup": "راه‌اندازی", + "Setup + index of all capabilities. Start here — covers base URL, auth, model discovery, and links to every capability skill.": "راه‌اندازی + فهرست همه قابلیت‌ها. از اینجا شروع کنید — شامل آدرس پایه، احراز هویت، کشف مدل و پیوند به هر مهارت قابلیت است.", + "Share Endpoint": "اشتراک‌گذاری نقطه پایانی", + "Share URL with team members": "اشتراک‌گذاری URL با اعضای تیم", + "Show": "نمایش", + "Show all": "نمایش همه", + "Show key": "نمایش کلید", + "Show only selected models": "فقط مدل‌های انتخاب شده را نشان دهید", + "Showing": "در حال نمایش", + "Shutdown": "خاموش کردن", + "Sign in with OIDC": "ورود با OIDC", + "Simple chat interface to interact with any AI model from connected providers. Select a model and start chatting!": "رابط گفتگوی ساده برای تعامل با هر مدل هوش مصنوعی از ارائه‌دهندگان متصل. یک مدل انتخاب کنید و شروع به گفتگو کنید!", + "Single": "تک", + "Single API endpoint for all major AI providers. Simplify your integration.": "یک نقطه پایانی API برای همه ارائه‌دهندگان اصلی هوش مصنوعی. یکپارچه‌سازی خود را ساده کنید.", + "Some models are not responding": "برخی از مدل‌ها پاسخ نمی‌دهند", + "Sort Codex quotas by remaining": "مرتب‌سازی سهمیه‌های Codex بر اساس باقیمانده", + "Sort accounts by earliest quota reset time": "مرتب‌سازی حساب‌ها بر اساس زودترین زمان بازنشانی سهمیه", + "Source Body": "بدنه منبع", + "Sourcegraph Amp coding assistant CLI": "دستیار کدنویسی Sourcegraph Amp CLI", + "Special reasoning/thinking tokens (fallback to output rate)": "توکن‌های استدلال/تفکر ویژه (بازگشت به نرخ خروجی)", + "Speech To Text": "گفتار به متن", + "Speech-to-Text": "گفتار به متن", + "Standard prompt tokens": "توکن‌های پرامپت استاندارد", + "Start DNS": "راه‌اندازی DNS", + "Start Date": "تاریخ شروع", + "Start Free": "شروع رایگان", + "Start Headroom": "راه‌اندازی Headroom", + "Start Headroom separately at the configured URL, then recheck.": "Headroom را به صورت جداگانه در آدرس پیکربندی شده راه‌اندازی کنید، سپس دوباره بررسی کنید.", + "Start MITM": "راه‌اندازی MITM", + "Start Server": "راه‌اندازی سرور", + "Start Tunnel": "راه‌اندازی تونل", + "Start a conversation": "شروع یک گفتگو", + "Starting 9Router...": "در حال راه‌اندازی 9Router...", + "Status": "وضعیت", + "Status:": "وضعیت:", + "Step 1: Open this URL in your browser": "مرحله ۱: این URL را در مرورگر خود باز کنید", + "Step 2: Paste the callback URL here": "مرحله ۲: URL پاسخ بازگشت را در اینجا بچسبانید", + "Sticky Limit": "محدودیت چسبندگی", + "Sticky:": "چسبنده:", + "Stop": "توقف", + "Stop DNS": "توقف DNS", + "Stop Headroom": "توقف Headroom", + "Stop MITM": "توقف MITM", + "Stop Server": "توقف سرور", + "Stopped": "متوقف شد", + "Strict Proxy": "پروکسی سختگیرانه", + "Subagent Model": "مدل زیرعامل", + "Sudo Password Required": "رمز عبور sudo الزامی است", + "Sudo password is required": "رمز عبور sudo الزامی است", + "Suggested free models (≥200k context):": "مدل‌های رایگان پیشنهادی (≥۲۰۰k زمینه):", + "Suggested shorthand examples: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929.": "مثال‌های میان‌نویس پیشنهادی: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929.", + "Support up to 20 active apps & 50 custom domains": "پشتیبانی از حداکثر ۲۰ برنامه فعال و ۵۰ دامنه سفارشی", + "Supported formats: protocol://user:pass@host:port, host:port:user:pass": "فرمت‌های پشتیبانی شده: protocol://user:pass@host:port, host:port:user:pass", + "Sync settings across devices with optional cloud storage.": "همگام‌سازی تنظیمات بین دستگاه‌ها با ذخیره‌سازی اختیاری ابری.", + "System": "سیستم", + "TTFT:": "TTFT:", + "Tailscale": "Tailscale", + "Tailscale Funnel": "قیف Tailscale", + "Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working.": "قیف Tailscale متوقف خواهد شد. دسترسی از راه دور از طریق URL Tailscale از کار خواهد افتاد.", + "Tailscale installed": "Tailscale نصب شد", + "Tailscale is not installed. Install it to enable Funnel.": "Tailscale نصب نشده است. برای فعال‌سازی Funnel آن را نصب کنید.", + "Target Request": "درخواست هدف", + "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com.": "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com.", + "Temperature": "دما", + "Terminal": "ترمینال", + "Terms of Service": "شرایط خدمات", + "Terse-style system prompt → ~65% fewer output tokens (up to 87%)": "پرامپت سیستم مختصر → ~۶۵٪ توکن خروجی کمتر (تا ۸۷٪)", + "Test": "آزمایش", + "Test Again": "آزمایش مجدد", + "Test All": "آزمایش همه", + "Test Example": "مثال آزمایش", + "Test Results": "نتایج آزمایش", + "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 connection": "آزمایش اتصال", + "Test model": "آزمایش مدل", + "Test proxy": "آزمایش پروکسی", + "Test proxy URL": "آزمایش آدرس پروکسی", + "Testing...": "در حال آزمایش...", + "Text To Speech": "متن به گفتار", + "Text To Speech combo": "ترکیب متن به گفتار", + "Text to Image": "متن به تصویر", + "Text to Image combo": "ترکیب متن به تصویر", + "Text-to-Speech": "متن به گفتار", + "Text-to-image via DALL-E, Imagen, FLUX, MiniMax, SDWebUI…": "تولید متن به تصویر از طریق DALL-E، Imagen، FLUX، MiniMax، SDWebUI…", + "The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working.": "تونل Cloudflare قطع خواهد شد. دسترسی از راه دور از طریق URL تونل از کار خواهد افتاد.", + "The proxy server has been stopped.": "سرور پروکسی متوقف شده است.", + "The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly.": "درخواست فوراً توسط OpenAI، Anthropic، Gemini یا دیگران برآورده می‌شود.", + "The tunnel will be disconnected. Remote access will stop working.": "تونل قطع خواهد شد. دسترسی از راه دور از کار خواهد افتاد.", + "The unified endpoint for AI generation. Connect, route, and manage your AI providers with ease.": "نقطه پایانی یکپارچه برای تولید هوش مصنوعی. به راحتی ارائه‌دهندگان هوش مصنوعی خود را متصل، مسیردهی و مدیریت کنید.", + "The unified interface for modern AI infrastructure": "رابط یکپارچه برای زیرساخت مدرن هوش مصنوعی", + "The unified interface for modern AI infrastructure. Secure, observable, and scalable.": "رابط یکپارچه برای زیرساخت مدرن هوش مصنوعی. امن، قابل مشاهده و مقیاس‌پذیر.", + "Theme": "پوسته", + "Thinking": "تفکر", + "Thinking Process": "فرآیند تفکر", + "This is the only time you will see this key. Store it securely.": "این تنها باری است که این کلید را می‌بینید. آن را به‌طور امن ذخیره کنید.", + "This provider is ready to use.": "این ارائه‌دهنده آماده استفاده است.", + "This provider is ready to use. Optionally route requests through a proxy pool to bypass IP-based limits.": "این ارائه‌دهنده آماده استفاده است. در صورت تمایل، درخواست‌ها را از طریق یک استخر پروکسی برای دور زدن محدودیت‌های مبتنی بر IP مسیردهی کنید.", + "This value is write-only after saving.": "این مقدار پس از ذخیره فقط نوشتنی است.", + "Timestamp": "زمان‌سنج", + "Timestamp:": "زمان‌سنج:", + "To get a fresh API key, paste your browser cookie from": "برای دریافت یک کلید API جدید، کوکی مرورگر خود را از", + "Today": "امروز", + "Toggle DNS to redirect": "تغییر وضعیت DNS برای تغییر مسیر", + "Toggle auto-ping": "تغییر وضعیت پینگ خودکار", + "Token Saver": "ذخیره‌ساز توکن", + "Token Types:": "انواع توکن:", + "Token auto-detected from Kiro IDE successfully!": "توکن با موفقیت از Kiro IDE به‌طور خودکار تشخیص داده شد!", + "Token is used once for deployment and not stored.": "توکن فقط یک بار برای استقرار استفاده می‌شود و ذخیره نمی‌شود.", + "Token is used once for deployment, not stored. Found in Organization Settings.": "توکن فقط یک بار برای استقرار استفاده می‌شود، ذخیره نمی‌شود. در تنظیمات سازمان یافت می‌شود.", + "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)": "توکن‌های استفاده شده برای ایجاد ورودی‌های حافظه پنهان (بازگشت به نرخ ورودی)", + "Tomorrow": "فردا", + "Tool not found or disabled.": "ابزار یافت نشد یا غیرفعال است.", + "Tools": "ابزارها", + "Tools:": "ابزارها:", + "Total Cost": "هزینه کل", + "Total Input Tokens": "کل توکن‌های ورودی", + "Total Models": "تعداد کل مدل‌ها", + "Total Requests": "کل درخواست‌ها", + "Total Tokens": "کل توکن‌ها", + "Total:": "مجموع:", + "Track and manage your API quota limits": "پیگیری و مدیریت محدودیت‌های سهمیه API خود", + "Track token usage, costs, and performance across all providers.": "پیگیری مصرف توکن، هزینه‌ها و عملکرد در همه ارائه‌دهندگان.", + "Transcribe audio via OpenAI Whisper, Groq, Gemini, Deepgram, AssemblyAI…": "رونویسی صدا از طریق OpenAI Whisper، Groq، Gemini، Deepgram، AssemblyAI…", + "Transferring data...": "در حال انتقال داده...", + "Translator": "مترجم", + "Translator Debug": "اشکال‌زدایی مترجم", + "Tried in order (top-down) or rotated when round-robin is on.": "به ترتیب امتحان شده (بالا به پایین) یا در صورت روشن بودن چرخشی، چرخش می‌یابد.", + "Trust Cert": "اعتماد به گواهی", + "Trusted": "معتمد", + "Try Again": "دوباره تلاش کنید", + "Tunnel": "تونل", + "Tunnel connected!": "تونل متصل شد!", + "Tunnel disabled": "تونل غیرفعال شد", + "Turn off Empty": "خاموش کردن حساب‌های خالی", + "Turn on Available": "روشن کردن حساب‌های موجود", + "Turn request detail recording on/off globally": "روشن/خاموش کردن ضبط جزئیات درخواست به صورت سراسری", + "Twitter": "توییتر", + "URL → markdown / text / HTML via Firecrawl, Jina, Tavily, Exa.": "URL → مارک‌داون / متن / HTML از طریق Firecrawl، Jina، Tavily، Exa.", + "Unavailable": "ناموجود", + "Under": "زیر", + "Unified Endpoint": "نقطه پایانی یکپارچه", + "Unknown": "ناشناخته", + "Unselect all": "لغو انتخاب همه", + "Update": "به‌روزرسانی", + "Update 9Router": "به‌روزرسانی 9Router", + "Update Password": "به‌روزرسانی رمز عبور", + "Update now": "همین حالا به‌روزرسانی کنید", + "Upstream Auth Error": "خطای احراز هویت بالادست", + "Upstream Unavailable": "بالادست در دسترس نیست", + "Usage": "مصرف", + "Usage & Analytics": "مصرف و تحلیل", + "Usage / Limit": "مصرف / محدودیت", + "Usage Logs": "لاگ‌های مصرف", + "Usage Tracking": "پیگیری مصرف", + "Usage by API Key": "مصرف بر اساس کلید API", + "Usage by Account": "مصرف بر اساس حساب", + "Usage by Endpoint": "مصرف بر اساس نقطه پایانی", + "Usage by Model": "مصرف بر اساس مدل", + "Usage:": "مصرف:", + "Use 9Router model aliases to keep Amp shorthand mappings stable across provider updates.": "برای حفظ پایداری نگاشت‌های میان‌نویس Amp در به‌روزرسانی‌های ارائه‌دهنده، از نام‌های مستعار مدل 9Router استفاده کنید.", + "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "استفاده از Antigravity IDE و GitHub Copilot → با هر ارائه‌دهنده/مدلی از 9Router", + "Use Authentik or any OIDC provider to sign in to the dashboard.": "برای ورود به داشبورد از Authentik یا هر ارائه‌دهنده OIDC استفاده کنید.", + "Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys.": "برای ورود به داشبورد از Authentik یا هر ارائه‌دهنده OIDC استفاده کنید. می‌توانید برای داشبورد فقط رمز عبور، فقط OIDC یا هر دو را فعال کنید؛ دسترسی به API مدل همچنان از کلیدهای API استفاده می‌کند.", + "Use a GitLab OAuth application": "از یک برنامه OAuth GitLab استفاده کنید", + "Use a GitLab PAT with api scope": "از یک GitLab PAT با محدوده api استفاده کنید", + "Use a direct xAI API key from console.x.ai. This is separate from Grok Build OAuth.": "از یک کلید API مستقیم xAI از console.x.ai استفاده کنید. این از Grok Build OAuth جدا است.", + "Use a local proxy for Start/Stop, or an external Docker sidecar like http://headroom:8787.": "برای شروع/توقف از یک پروکسی محلی استفاده کنید، یا از یک sidecar خارجی داکر مانند http://headroom:8787.", + "Use a long-lived Kiro/CodeWhisperer API key (headless auth).": "از یک کلید API طولانی‌مدت Kiro/CodeWhisperer (احراز هویت بدون رابط) استفاده کنید.", + "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 خود از هر شبکه‌ای استفاده کنید", + "Valid": "معتبر", + "Vectors for RAG / semantic search via OpenAI, Gemini, Mistral…": "بردارها برای RAG / جستجوی معنایی از طریق OpenAI، Gemini، Mistral…", + "Vercel API Token": "توکن Vercel API", + "Vercel Relay": "Vercel Relay", + "Vercel serves millions of apps — providers can't block Vercel IPs without affecting legitimate traffic": "Vercel به میلیون‌ها برنامه خدمت می‌کند — ارائه‌دهندگان نمی‌توانند IPهای Vercel را بدون تأثیر بر ترافیک قانونی مسدود کنند", + "Verification URL": "آدرس تأیید", + "Video": "ویدیو", + "View Codex reset credit expiry": "مشاهده انقضای اعتبار بازنشانی Codex", + "View Full Details": "مشاهده جزئیات کامل", + "View on GitHub": "مشاهده در GitHub", + "Visit the URL below and enter the code:": "از URL زیر بازدید کرده و کد را وارد کنید:", + "Visit the login URL below and authorize:": "از URL ورود زیر بازدید کرده و مجوز دهید:", + "Voice": "صدا", + "Voice ID": "شناسه صدا", + "Voyage AI": "Voyage AI", + "Waiting for Authorization": "در انتظار مجوز", + "Waiting for authorization...": "در انتظار مجوز...", + "Warning": "هشدار", + "Web Fetch": "دریافت وب", + "Web Fetch & Search": "جستجو و دریافت وب", + "Web Search": "جستجوی وب", + "Web Search & Fetch (Exa)": "جستجو و دریافت وب (Exa)", + "Welcome": "خوش آمدید", + "What is Cloudflare Relay?": "Cloudflare Relay چیست؟", + "What is Deno Relay?": "Deno Relay چیست؟", + "What is Vercel Relay?": "Vercel Relay چیست؟", + "When": "زمان", + "When ON, dashboard requires password. When OFF, access without login.": "در حالت روشن، داشبورد به رمز عبور نیاز دارد. در حالت خاموش، دسترسی بدون نیاز به ورود.", + "Windows:": "ویندوز:", + "Windows: Run 9Router terminal as Administrator": "ویندوز: ترمینال 9Router را به عنوان مدیر اجرا کنید", + "Windows: Run terminal (9Router) as Administrator to enable MITM": "ویندوز: ترمینال (9Router) را به عنوان مدیر اجرا کنید تا MITM فعال شود", + "Worker Name": "نام Worker", + "Works on any device": "روی هر دستگاهی کار می‌کند", + "Writes to": "نوشته می‌شود به", + "You can override default pricing for specific models. Reset to defaults anytime to restore standard rates.": "می‌توانید قیمت‌گذاری پیش‌فرض را برای مدل‌های خاص بازنویسی کنید. هر زمان که بخواهید با بازنشانی به پیش‌فرض، نرخ‌های استاندارد را بازیابی کنید.", + "Your": "شما", + "Your Account Name": "نام حساب شما", + "Your Code": "کد شما", + "Your Kiro account via": "حساب Kiro شما از طریق", + "Your OAuth application client ID": "شناسه مشتری برنامه OAuth شما", + "Your organization's AWS IAM Identity Center URL": "URL مرکز هویت AWS IAM سازمان شما", + "Your requests start from your favorite tools or our unified SDK. Just change the base URL.": "درخواست‌های شما از ابزارهای مورد علاقه شما یا SDK یکپارچه ما شروع می‌شود. فقط آدرس پایه را تغییر دهید.", + "Your requests start from your favorite tools — Cursor, Claude, Copilot, or any OpenAI-compatible SDK.": "درخواست‌های شما از ابزارهای مورد علاقه شما شروع می‌شوند — Cursor، Claude، Copilot یا هر SDK سازگار با OpenAI.", + "account has been connected.": "حساب متصل شده است.", + "active": "فعال", + "add OpenAI/Anthropic compatible endpoints": "افزودن نقاط پایانی سازگار با OpenAI/Anthropic", + "added)": "افزوده شد)", + "again after install.": "دوباره پس از نصب.", + "and click": "و کلیک کنید", + "apiKey": "apiKey", + "below.": "در زیر.", + "bound": "پیوند شده", + "chars)": "کاراکتر)", + "cloudflare relay": "cloudflare relay", + "connection": "اتصال", + "connections": "اتصالات", + "daily-cloudcode-pa.googleapis.com": "daily-cloudcode-pa.googleapis.com", + "dark": "تاریک", + "disabled": "غیرفعال", + "dollars per million tokens": "دلار به ازای هر میلیون توکن", + "e.g. CwhRBWXzGAHq8TQ4Fs17": "مثلاً CwhRBWXzGAHq8TQ4Fs17", + "e.g. claude-opus-4-5": "مثلاً claude-opus-4-5", + "e.g. my-model-id": "مثلاً my-model-id", + "e.g. tts-1-hd": "مثلاً tts-1-hd", + "e.g. voyage-3, embed-english-v3.0, text-embedding-3-small": "مثلاً voyage-3, embed-english-v3.0, text-embedding-3-small", + "e.g., Production API, Dev Environment": "مثلاً، Production API، Dev Environment", + "every request bills all panel models + the judge": "هر درخواست همه مدل‌های پنل + داور را صورتحساب می‌کند", + "export": "خروجی", + "failed": "ناموفق", + "git/grep/ls/tree/logs → 60-90% fewer input tokens": "git/grep/ls/tree/logs → ۶۰-۹۰٪ توکن ورودی کمتر", + "h ago": "ساعت قبل", + "has been connected.": "متصل شده است.", + "iFlow AI": "iFlow AI", + "iFlow Cookie Authentication": "احراز هویت کوکی iFlow", + "import": "وارد کردن", + "inactive": "غیرفعال", + "jcode - Manual Configuration": "jcode - پیکربندی دستی", + "jcode CLI not detected locally": "jcode CLI در سیستم محلی شناسایی نشد", + "jcode is a Rust-based coding agent with semantic memory, multi-agent swarms, and extreme performance (27.8 MB RAM, 14ms boot).": "jcode یک عامل کدنویسی مبتنی بر Rust با حافظه معنایی، خوشه‌های چندعاملی و عملکرد فوق‌العاده (۲۷.۸ مگابایت رم، ۱۴ میلی‌ثانیه بوت) است.", + "kiro://kiro.kiroAgent/authenticate-success?code=...": "kiro://kiro.kiroAgent/authenticate-success?code=...", + "light": "روشن", + "m ago": "دقیقه قبل", + "macOS / Linux / Windows:": "macOS / Linux / Windows:", + "macOS / Linux:": "macOS / Linux:", + "macOS/Linux:": "macOS/Linux:", + "more": "بیشتر", + "more providers": "ارائه‌دهندگان بیشتر", + "ms / Total": "میلی‌ثانیه / کل", + "name|apiKey": "name|apiKey", + "no_proxy:": "بدون پروکسی:", + "not detected locally": "در سیستم محلی شناسایی نشد", + "npm install -g 9router": "npm install -g 9router", + "npx 9router": "npx 9router", + "open http://localhost:9099": "باز کردن http://localhost:9099", + "openid profile email": "openid profile email", + "optional context to improve accuracy": "زمینه اختیاری برای بهبود دقت", + "or VS Code extension marketplace.": "یا بازار افزونه VS Code.", + "or just": "یا فقط", + "passed": "گذرانده شد", + "platform.iflow.cn": "platform.iflow.cn", + "queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "همه مدل‌ها را به طور موازی پرس و جو می‌کند، سپس یک داور یک پاسخ را ترکیب می‌کند. بهترین کیفیت، اما هزینه‌برترین: هر درخواست همه مدل‌های پنل + داور را صورتحساب می‌کند (تماس‌های N+1)", + "records, batches every": "رکوردها، هر دسته", + "requests, max": "درخواست‌ها، حداکثر", + "rotates models across requests to spread load": "مدل‌ها را در بین درخواست‌ها برای توزیع بار می‌چرخاند", + "s)": "ثانیه)", + "s...": "ثانیه...", + "seconds...": "ثانیه...", + "sends image/PDF/audio requests to a model that supports them first": "درخواست‌های تصویر/PDF/صدا را ابتدا به مدلی که از آنها پشتیبانی می‌کند ارسال می‌کند", + "sk-...": "sk-...", + "sk_9router (default)": "sk_9router (پیش‌فرض)", + "system": "سیستم", + "tested": "آزمایش شد", + "the database.": "پایگاه داده.", + "to apply changes": "برای اعمال تغییرات", + "to verify.": "برای تأیید.", + "traffic through 9Router via MITM.": "ترافیک از طریق 9Router از طریق MITM.", + "tries models in order (next on failure)": "مدل‌ها را به ترتیب امتحان می‌کند (در صورت شکست به بعدی می‌رود)", + "unknown": "ناشناخته", + "v1.0 is now live": "v1.0 اکنون زنده است", + "vercel relay": "vercel relay", + "yet.": "هنوز.", + "your-org.deno.net": "your-org.deno.net", + "© 2025 9Router. All rights reserved.": "© ۲۰۲۵ 9Router. تمام حقوق محفوظ است.", + "— queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "— همه مدل‌ها را به طور موازی پرس و جو می‌کند، سپس یک داور یک پاسخ را ترکیب می‌کند. بهترین کیفیت، اما هزینه‌برترین: هر درخواست همه مدل‌های پنل + داور را صورتحساب می‌کند (تماس‌های N+1)", + "— rotates models across requests to spread load": "— مدل‌ها را در بین درخواست‌ها برای توزیع بار می‌چرخاند", + "— sends image/PDF/audio requests to a model that supports them first": "— درخواست‌های تصویر/PDF/صدا را ابتدا به مدلی که از آنها پشتیبانی می‌کند ارسال می‌کند", + "— tries models in order (next on failure)": "— مدل‌ها را به ترتیب امتحان می‌کند (در صورت شکست به بعدی می‌رود)", + "→ OpenAI": "→ OpenAI", + "→ Target": "→ هدف", + "→ localhost": "→ localhost", + "⚠️ Enable DNS to edit model mappings": "⚠️ برای ویرایش نگاشت‌های مدل، DNS را فعال کنید", + "⚠️ Local plugins run as subprocess via": "⚠️ افزونه‌های محلی به عنوان زیرفرآیند از طریق اجرا می‌شوند", + "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM ترافیک HTTPS ابزارهای IDE (Antigravity، GitHub Copilot، Kiro) را از طریق CA محلی رهگیری می‌کند تا درخواست‌ها را به ارائه‌دهندگان شما مسیردهی کند. ممکن است شرایط خدمات را نقض کند → مسدود شدن حساب. با مسئولیت خود استفاده کنید.", + "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ اطلاعیه ریسک: این ارائه‌دهنده از اشتراک/جلسه OAuth استفاده می‌کند که به طور رسمی برای استفاده پروکسی/روتر مجوز ندارد. حساب ممکن است محدود یا مسدود شود. با مسئولیت خود استفاده کنید.", + "✓ Confirm Add": "✓ تأیید افزودن", + "📝 Configure providers in dashboard or use environment variables": "📝 ارائه‌دهندگان را در داشبورد پیکربندی کنید یا از متغیرهای محیطی استفاده کنید", + "🔐 OAuth required. Add now and authenticate after Apply; tool list will be discovered after first connect.": "🔐 نیاز به OAuth. اکنون اضافه کنید و پس از اعمال، احراز هویت کنید؛ لیست ابزارها پس از اولین اتصال کشف می‌شود." +} diff --git a/public/i18n/literals/th.json b/public/i18n/literals/th.json index 6169829f..7d528201 100644 --- a/public/i18n/literals/th.json +++ b/public/i18n/literals/th.json @@ -1,195 +1,1391 @@ { - "Cancel": "ยกเลิก", - "Delete": "ลบ", - "Edit": "แก้ไข", - "Save": "บันทึก", - "Close": "ปิด", + "($/1M tokens). Example: An input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "(฿/1M tokens) ตัวอย่าง: อัตราขาเข้า 2.50 หมายถึง $2.50 ต่อ 1,000,000 input tokens", + "($/1M tokens). Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "(฿/1M tokens) ตัวอย่าง: อัตราขาเข้า 2.50 หมายถึง $2.50 ต่อ 1,000,000 input tokens", + "(Caveman)": "(Caveman)", + "(Headroom)": "(Headroom)", + "(Ponytail)": "(Ponytail)", + "(RTK)": "(RTK)", + "(via inference test)": "(ผ่านการทดสอบ推理)", + "+ Browse": "+ เรียกดู", + "+ Combo": "+ Combo", + "+ Custom": "+ กำหนดเอง", + "+ Save current as...": "+ บันทึกปัจจุบันเป็น...", + "-compatible models manually or import them from the /models endpoint.": "- เพิ่มโมเดลแบบ compatible ด้วยตนเอง หรือนำเข้าจาก /models endpoint", + ". Click \"Apply\" to auto-configure.": ". คลิก \"Apply\" เพื่อกำหนดค่าอัตโนมัติ", + "1. CLI & SDKs": "1. CLI & SDKs", + "1. Client Request (Input)": "1. คำขอจากลูกค้า (ขาเข้า)", + "1. Generates SSL cert & adds to system keychain": "1. สร้าง SSL cert แล้วเพิ่มเข้า system keychain", + "2. 9Router Hub": "2. 9Router Hub", + "2. Provider Request (Translated)": "2. คำขอจากผู้ให้บริการ (แปลแล้ว)", + "2. Redirects": "2. Redirects", + "24h": "24 ชม.", + "3. AI Providers": "3. ผู้ให้บริการ AI", + "3. Maps Antigravity models to any provider via 9Router": "3. Map โมเดล Antigravity ไปยังผู้ให้บริการใดก็ได้ผ่าน 9Router", + "3. Provider Response (Raw)": "3. การตอบกลับจากผู้ให้บริการ (ดิบ)", + "30D": "30 วัน", + "4. Client Response (Final)": "4. การตอบกลับลูกค้า (สุดท้าย)", + "60D": "60 วัน", + "7D": "7 วัน", + "9Router (Entry)": "9Router (ทางเข้า)", + "9Router Base URL": "9Router Base URL", + ": Account | Workers Scripts | Edit": ": บัญชี | Workers Scripts | Edit", + ": Include | Account |": ": Include | บัญชี |", + "AI endpoint proxy with web dashboard - A JavaScript port of CLIProxyAPI. Works seamlessly with Claude Code, OpenAI Codex, Cline, RooCode, and other CLI tools.": "AI endpoint proxy พร้อม web dashboard — JavaScript port ของ CLIProxyAPI ใช้งานร่วมกับ Claude Code, OpenAI Codex, Cline, RooCode และเครื่องมือ CLI อื่นๆ ได้อย่างราบรื่น", + "API Endpoint": "API Endpoint", + "API Key": "API Key", + "API Key (for Check)": "API Key (สำหรับตรวจสอบ)", + "API Key Compatible Providers": "ผู้ให้บริการที่ compatible กับ API Key", + "API Key Created": "สร้าง API Key แล้ว", + "API Key Name": "ชื่อ API Key", + "API Key Providers": "ผู้ให้บริการ API Key", + "API Keys": "API Keys", + "API Reference": "เอกสาร API", + "API Token": "API Token", + "API Tokens": "API Tokens", + "API Type": "ประเภท API", + "API Version": "เวอร์ชัน API", + "API endpoint configuration": "การกำหนดค่า API endpoint", + "AWS Builder ID": "AWS Builder ID", + "AWS IAM Identity Center": "AWS IAM Identity Center", + "AWS Region": "AWS Region", + "AWS region for the key (default: us-east-1)": "AWS region สำหรับ key (ค่าเริ่มต้น: us-east-1)", + "AWS region for your Identity Center (default: us-east-1)": "AWS region สำหรับ Identity Center ของคุณ (ค่าเริ่มต้น: us-east-1)", + "About": "เกี่ยวกับ", + "Access Anywhere": "เข้าถึงได้ทุกที่", + "Access Token": "Access Token", + "Access token will be auto-filled...": "Access token จะถูกเติมอัตโนมัติ...", + "Access your terminal, desktop & files from anywhere": "เข้าถึง terminal, desktop และไฟล์ของคุณจากทุกที่", + "Account": "บัญชี", + "Account ID": "Account ID", + "Account Resources": "ทรัพยากรบัญชี", + "Accounts per page": "จำนวนบัญชีต่อหน้า", + "Action": "การดำเนินการ", + "Activate": "เปิดใช้งาน", + "Active": "ใช้งานอยู่", + "Active All": "เปิดใช้งานทั้งหมด", + "Active:": "ใช้งานอยู่:", "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": "ตกลง", + "Add API Key": "เพิ่ม API Key", + "Add Anthropic Compatible": "เพิ่ม Anthropic Compatible", + "Add Connection": "เพิ่มการเชื่อมต่อ", + "Add Custom Embedding": "เพิ่ม Custom Embedding", + "Add Custom MCP": "เพิ่ม Custom MCP", + "Add Custom Model": "เพิ่ม Custom Model", + "Add Model": "เพิ่มโมเดล", + "Add Model Config": "เพิ่มการกำหนดค่าโมเดล", + "Add Model for GitHub Copilot": "เพิ่มโมเดลสำหรับ GitHub Copilot", + "Add Model for OpenCode": "เพิ่มโมเดลสำหรับ OpenCode", + "Add Model to Combo": "เพิ่มโมเดลเข้า Combo", + "Add New Provider": "เพิ่มผู้ให้บริการใหม่", + "Add OpenAI Compatible": "เพิ่ม OpenAI Compatible", + "Add Provider": "เพิ่มผู้ให้บริการ", + "Add Proxy Pool": "เพิ่ม Proxy Pool", + "Add Shorthands": "เพิ่ม Shorthands", + "Add a connection to enable importing models.": "เพิ่มการเชื่อมต่อเพื่อเปิดใช้งานการนำเข้าโมเดล", + "Add connection using browser cookie": "เพิ่มการเชื่อมต่อโดยใช้ browser cookie", + "Add model": "เพิ่มโมเดล", + "Add server": "เพิ่มเซิร์ฟเวอร์", + "Add the following configuration to your models array:": "เพิ่มการกำหนดค่าต่อไปนี้ใน models array ของคุณ:", + "Add your first connection to get started": "เพิ่มการเชื่อมต่อแรกของคุณเพื่อเริ่มต้น", + "Administrator required": "ต้องใช้สิทธิ์ผู้ดูแลระบบ", + "Administrator required — restart 9Router as Administrator to use MITM": "ต้องใช้สิทธิ์ผู้ดูแลระบบ — เริ่มต้น 9Router ใหม่ในฐานะผู้ดูแลระบบเพื่อใช้ MITM", + "After authorization, copy the full URL from your browser address bar.": "หลังการอนุมัติ คัดลอก URL เต็มจาก address bar ของเบราว์เซอร์", + "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.": "หลังเข้าสู่ระบบ คุณจะต้องคัดลอก callback URL จากเบราว์เซอร์แล้ววางกลับมาที่นี่", + "Alibaba Qwen Code CLI — supports OpenAI, Anthropic & Gemini providers via 9Router": "Alibaba Qwen Code CLI — รองรับผู้ให้บริการ OpenAI, Anthropic & Gemini ผ่าน 9Router", + "All": "ทั้งหมด", + "All AI Providers": "ผู้ให้บริการ AI ทั้งหมด", + "All Providers": "ผู้ให้บริการทั้งหมด", + "All models are responding normally.": "โมเดลทั้งหมดตอบสนองปกติ", + "All providers": "ผู้ให้บริการทั้งหมด", + "All rates are in": "อัตราทั้งหมดเป็น", + "All selected currently unbound": "ที่เลือกทั้งหมดยังไม่ได้เชื่อมต่อ", + "Allow dashboard access via tunnel": "อนุญาตให้เข้าถึง dashboard ผ่าน tunnel", + "Allow either password or OIDC.": "อนุญาตทั้งรหัสผ่านหรือ OIDC", + "An error occurred": "เกิดข้อผิดพลาด", + "An error occurred. Please try again.": "เกิดข้อผิดพลาด กรุณาลองใหม่", + "Anthropic Claude Code CLI": "Anthropic Claude Code CLI", + "Anthropic Compatible (Prod)": "Anthropic Compatible (Production)", + "Anthropic Compatible Details": "รายละเอียด Anthropic Compatible", + "Antigravity/Copilot IDE request → DNS redirect to localhost:443 → MITM proxy intercepts → 9Router → response to Antigravity/Copilot": "คำขอ Antigravity/Copilot IDE → DNS redirect ไปยัง localhost:443 → MITM proxy ดักจับ → 9Router → ส่งกลับไปยัง Antigravity/Copilot", + "Any model available in 9Router can be used — not just Qwen models. Select from Qwen, Claude, Gemini, GPT, and more.": "โมเดลใดก็ได้ที่มีใน 9Router สามารถใช้ได้ — ไม่ใช่แค่ Qwen เลือกจาก Qwen, Claude, Gemini, GPT และอื่นๆ", + "App Name": "ชื่อแอป", "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": "ออฟไลน์", + "Apply Proxy": "ใช้ Proxy", + "Applying...": "กำลังใช้...", + "Are you sure you want to close the proxy server?": "คุณแน่ใจหรือว่าต้องการปิด proxy server?", + "Are you sure you want to disable the tunnel?": "คุณแน่ใจหรือว่าต้องการปิด tunnel?", + "Attempting to reconnect...": "กำลังพยายามเชื่อมต่อใหม่...", + "Audio File": "ไฟล์เสียง", + "Auth Mode": "โหมดยืนยันตัวตน", + "Authenticate": "ยืนยันตัวตน", + "Authentication Method": "วิธียืนยันตัวตน", + "Authentication Successful": "ยืนยันตัวตนสำเร็จ", + "Authentication Successful!": "ยืนยันตัวตนสำเร็จ!", + "Authless": "ไม่ต้องยืนยันตัวตน", + "Authorization Successful!": "อนุมัติสำเร็จ!", + "Authorize": "อนุมัติ", + "Auto (by priority)": "อัตโนมัติ (ตามลำดับความสำคัญ)", + "Auto Refresh (3s)": "รีเฟรชอัตโนมัติ (3 วินาที)", + "Auto-detect": "ตรวจจับอัตโนมัติ", + "Auto-detecting token...": "กำลังตรวจจับ token...", + "Auto-detecting tokens...": "กำลังตรวจจับ tokens...", + "Auto-ping": "Auto-ping", + "Auto-refresh": "รีเฟรชอัตโนมัติ", + "Auto:": "อัตโนมัติ:", + "Automatically switch between providers when limits are hit.": "สลับระหว่างผู้ให้บริการโดยอัตโนมัติเมื่อถึงขีดจำกัด", "Available": "พร้อมใช้งาน", - "Unavailable": "ไม่พร้อมใช้งาน", - "Required": "จำเป็น", - "Optional": "ไม่บังคับ", - "Default": "ค่าเริ่มต้น", - "Custom": "กำหนดเอง", - "Advanced": "ขั้นสูง", - "Basic": "พื้นฐาน", - "Help": "ช่วยเหลือ", - "Support": "สนับสนุน", - "Documentation": "เอกสาร", - "Version": "เวอร์ชัน", - "Language": "ภาษา", - "Theme": "ธีม", - "Light": "สว่าง", - "Dark": "มืด", - "Auto": "อัตโนมัติ", - "Endpoint": "จุดสิ้นสุด", - "Combos": "ชุดรวม", - "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", - "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": "หยุดแล้ว", + "Available Models": "โมเดลที่พร้อมใช้งาน", + "Azure Endpoint": "Azure Endpoint", + "Azure OpenAI Configuration": "การกำหนดค่า Azure OpenAI", + "BXAuth=xxx; ...": "BXAuth=xxx; ...", + "Back": "ย้อนกลับ", + "Back to CLI Tools": "กลับไป CLI Tools", + "Back to Providers": "กลับไปยังผู้ให้บริการ", + "Base URL": "Base URL", + "Batch Import": "นำเข้าแบบ Batch", + "Batch Import Proxies": "นำเข้า Proxies แบบ Batch", + "Batch Size": "ขนาด Batch", + "Beautiful web dashboard for managing providers and monitoring usage.": "web dashboard สวยงามสำหรับจัดการผู้ให้บริการและตรวจสอบการใช้งาน", + "Best quality, but costs the most": "คุณภาพดีที่สุด แต่มีค่าใช้จ่ายมากที่สุด", + "Bias the model toward minimal code: YAGNI, reuse stdlib, deletion over addition": "บังคับโมเดลให้เขียน code น้อยที่สุด: YAGNI, ใช้ stdlib ซ้ำ, ลบมากกว่าเพิ่ม", + "Binary File": "ไฟล์ไบนารี", + "Blog": "บล็อก", + "Both": "ทั้งคู่", + "Browse & edit files": "เรียกดูและแก้ไขไฟล์", + "Browse MCP Marketplace": "เรียกดู MCP Marketplace", + "Browse source, README, and examples.": "เรียกดู source, README และตัวอย่าง", + "Browser Control (Browser MCP)": "การควบคุมเบราว์เซอร์ (Browser MCP)", + "Bulk Add": "เพิ่มจำนวนมาก", + "CLI Support": "รองรับ CLI", + "CLI Tools": "เครื่องมือ CLI", + "CLI on the host →": "CLI บนโฮสต์ →", + "CLIProxyAPI Auth JSON": "CLIProxyAPI Auth JSON", + "Cache Creation": "สร้าง Cache", + "Cache Creation:": "สร้าง Cache:", + "Cached": "แคช", + "Cached Tokens": "Cached Tokens", + "Cached Tokens:": "Cached Tokens:", + "Cached input tokens (typically 50% of input rate)": "Input tokens ที่แคช (ปกติคิดอัตรา 50% ของ input rate)", + "Cached:": "แคช:", + "Calls per account before switching": "จำนวนเรียกก่อนสลับบัญชี", + "Calls per combo model before switching": "จำนวนเรียกก่อนสลับโมเดล combo", + "Cancel": "ยกเลิก", + "Capacity auto-switch": "สลับอัตโนมัติเมื่อเต็ม", "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", - "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", + "Change Log": "บันทึกการเปลี่ยนแปลง", + "Changelog": "Changelog", + "Chat": "แชท", + "Chat / code-gen via OpenAI or Anthropic format with streaming.": "แชท / สร้างโค้ดผ่าน OpenAI หรือ Anthropic format พร้อม streaming", + "Chat Completions": "Chat Completions", + "Check": "ตรวจสอบ", + "Checking Claude CLI...": "กำลังตรวจสอบ Claude CLI...", + "Checking Claude Cowork...": "กำลังตรวจสอบ Claude Cowork...", + "Checking Cline...": "กำลังตรวจสอบ Cline...", + "Checking Codex CLI...": "กำลังตรวจสอบ Codex CLI...", + "Checking Copilot config...": "กำลังตรวจสอบ Copilot config...", + "Checking DeepSeek TUI...": "กำลังตรวจสอบ DeepSeek TUI...", + "Checking Factory Droid CLI...": "กำลังตรวจสอบ Factory Droid CLI...", + "Checking Hermes Agent...": "กำลังตรวจสอบ Hermes Agent...", + "Checking Kilo Code...": "กำลังตรวจสอบ Kilo Code...", + "Checking Open Claw CLI...": "กำลังตรวจสอบ Open Claw CLI...", + "Checking OpenCode CLI...": "กำลังตรวจสอบ OpenCode CLI...", + "Checking jcode CLI...": "กำลังตรวจสอบ jcode CLI...", + "Checking...": "กำลังตรวจสอบ...", + "Choose API Provider → Ollama": "เลือก API Provider → Ollama", + "Choose how to authenticate with GitLab Duo:": "เลือกวิธียืนยันตัวตนกับ GitLab Duo:", + "Choose your authentication method:": "เลือกวิธียืนยันตัวตน:", + "Claude": "Claude", + "Claude CLI - Manual Configuration": "Claude CLI - กำหนดค่าด้วยตนเอง", + "Claude CLI not detected locally": "ไม่พบ Claude CLI บนเครื่อง", + "Claude CLI not installed": "ไม่ได้ติดตั้ง Claude CLI", + "Claude Cowork - Manual Configuration": "Claude Cowork - กำหนดค่าด้วยตนเอง", + "Claude Desktop (Cowork mode) not detected": "ไม่พบ Claude Desktop (Cowork mode)", + "Claude Desktop Cowork (third-party inference)": "Claude Desktop Cowork (inference จากบุคคลที่สาม)", + "Clear": "ล้าง", + "Clear (will use main model)": "ล้าง (จะใช้โมเดลหลัก)", + "Clear Filters": "ล้างตัวกรอง", + "Clear search": "ล้างการค้นหา", + "Click": "คลิก", + "Click \"View All Model\" → \"Add Custom Model\"": "คลิก \"View All Model\" → \"Add Custom Model\"", + "Click a model to set/clear active": "คลิกโมเดลเพื่อตั้งค่า/ยกเลิกสถานะใช้งาน", "Click to add, click again to remove. Changes are saved automatically.": "คลิกเพื่อเพิ่ม คลิกอีกครั้งเพื่อลบ การเปลี่ยนแปลงจะถูกบันทึกโดยอัตโนมัติ", - "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ ประกาศความเสี่ยง: ผู้ให้บริการนี้ใช้เซสชันสมัครสมาชิก/OAuth ที่ไม่ได้รับอนุญาตอย่างเป็นทางการสำหรับการใช้งานพร็อกซี/เราเตอร์ บัญชีอาจถูกจำกัดหรือถูกแบน ใช้งานด้วยความเสี่ยงของคุณเอง", - "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM ดักจับการรับส่งข้อมูล HTTPS ของเครื่องมือ IDE (Antigravity, GitHub Copilot, Kiro) ผ่าน CA ท้องถิ่นเพื่อเปลี่ยนเส้นทางคำขอไปยังผู้ให้บริการของคุณ อาจละเมิด ToS → เสี่ยงถูกแบนบัญชี ใช้งานด้วยความเสี่ยงของคุณเอง", - "Endpoint is exposed without an API key.": "เอนด์พอยต์เปิดให้เข้าถึงโดยไม่มีคีย์ API" -} + "Click to edit": "คลิกเพื่อแก้ไข", + "Click to retry": "คลิกเพื่อลองใหม่", + "Client ID": "Client ID", + "Client Request": "คำขอจากลูกค้า", + "Client Response": "การตอบกลับลูกค้า", + "Client Secret": "Client Secret", + "Cline - Manual Configuration": "Cline - กำหนดค่าด้วยตนเอง", + "Cline AI Coding Assistant": "Cline AI Coding Assistant", + "Cline not detected locally": "ไม่พบ Cline บนเครื่อง", + "Close": "ปิด", + "Close Proxy": "ปิด Proxy", + "Close provider filter": "ปิดตัวกรองผู้ให้บริการ", + "Close reset credit expiry modal": "ปิดหน้าต่าง reset credit expiry", + "Close test results": "ปิดผลการทดสอบ", + "Closing in": "ปิดใน", + "Cloud Sync": "Cloud Sync", + "Cloudflare Relay": "Cloudflare Relay", + "Cloudflare Tunnel": "Cloudflare Tunnel", + "Cloudflare Workers AI": "Cloudflare Workers AI", + "Codex CLI - Manual Configuration": "Codex CLI - กำหนดค่าด้วยตนเอง", + "Codex CLI not detected locally": "ไม่พบ Codex CLI บนเครื่อง", + "Codex CLI not installed": "ไม่ได้ติดตั้ง Codex CLI", + "Codex Reset Credit Expiry": "Reset Credit Expiry ของ Codex", + "Codex uses": "Codex ใช้", + "Combo Name": "ชื่อ Combo", + "Combo Round Robin": "Combo Round Robin", + "Combo Sticky Limit": "Combo Sticky Limit", + "Combos": "Combos", + "Coming soon...": "เร็วๆ นี้...", + "Comma-separated hostnames/domains to bypass the proxy.": "โฮสต์/โดเมน คั่นด้วยเครื่องหมายจุลภาค เพื่อข้าม proxy", + "Comma-separated hosts/domains to bypass proxy": "โฮสต์/โดเมน คั่นด้วยเครื่องหมายจุลภาค เพื่อข้าม proxy", + "Company": "บริษัท", + "Complete the authorization in the popup window.": "ดำเนินการอนุมัติให้เสร็จสิ้นในหน้าต่างป๊อปอัป", + "Completion/response tokens": "Completion/Response tokens", + "Compress LLM output": "บีบอัด output ของ LLM", + "Compress context": "บีบอัดบริบท", + "Compress prompts via /v1/compress before routing to the model": "บีบอัด prompt ผ่าน /v1/compress ก่อนส่งไปยังโมเดล", + "Compress tool output": "บีบอัด tool output", + "Compress tool output to reduce token usage.": "บีบอัด tool output เพื่อลดการใช้งาน token", + "Config path: Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml": "เส้นทาง Config: Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml", + "Config path: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json": "เส้นทาง Config: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json", + "Configuration": "การกำหนดค่า", + "Configure 9router as an OpenAI-compatible provider to route all jcode requests through 9router's optimization layer.": "กำหนดค่า 9router เป็น OpenAI-compatible provider เพื่อส่งต่อคำขอ jcode ทั้งหมดผ่าน optimization layer ของ 9router", + "Configure CLI tools": "กำหนดค่าเครื่องมือ CLI", + "Configure a new AI provider to use with your applications.": "กำหนดค่า AI provider ใหม่เพื่อใช้กับแอปพลิเคชันของคุณ", + "Configure pricing rates for cost tracking and calculations": "กำหนดค่าอัตราการคิดราคาสำหรับการติดตามและคำนวณค่าใช้จ่าย", + "Configure providers and API keys via web interface": "กำหนดค่า providers และ API keys ผ่าน web interface", + "Configured": "กำหนดค่าแล้ว", + "Confirm": "ยืนยัน", + "Confirm New Password": "ยืนยันรหัสผ่านใหม่", + "Confirm Password": "ยืนยันรหัสผ่าน", + "Confirm new password": "ยืนยันรหัสผ่านใหม่", + "Connect": "เชื่อมต่อ", + "Connect AI tools remotely": "เชื่อมต่อเครื่องมือ AI จากที่ไกล", + "Connect Cursor IDE": "เชื่อมต่อ Cursor IDE", + "Connect GitLab Duo": "เชื่อมต่อ GitLab Duo", + "Connect Kiro": "เชื่อมต่อ Kiro", + "Connect to providers with OAuth to track your API quota limits and usage.": "เชื่อมต่อกับ providers ด้วย OAuth เพื่อติดตาม API quota limits และการใช้งานของคุณ", + "Connect via OAuth or API keys. Securely manage credentials.": "เชื่อมต่อผ่าน OAuth หรือ API keys จัดการข้อมูลรับรองอย่างปลอดภัย", + "Connect with OAuth2": "เชื่อมต่อด้วย OAuth2", + "Connect your account using OAuth2 authentication.": "เชื่อมต่อบัญชีของคุณโดยใช้ OAuth2 authentication", + "Connected": "เชื่อมต่อแล้ว", + "Connected Successfully!": "เชื่อมต่อสำเร็จ!", + "Connected providers only": "เฉพาะ providers ที่เชื่อมต่อแล้ว", + "Connecting...": "กำลังเชื่อมต่อ...", + "Connection": "การเชื่อมต่อ", + "Connection Details": "รายละเอียดการเชื่อมต่อ", + "Connection Failed": "เชื่อมต่อล้มเหลว", + "Connections": "การเชื่อมต่อ", + "Console Log": "Console Log", + "Contact": "ติดต่อ", + "Content": "เนื้อหา", + "Continue": "ดำเนินการต่อ", + "Continue AI Assistant": "Continue AI Assistant", + "Continue to summary": "ดำเนินการต่อไปยังสรุป", + "Continue with GitHub": "ดำเนินการต่อด้วย GitHub", + "Continue with Google": "ดำเนินการต่อด้วย Google", + "Cookie": "Cookie", + "Cookie Auth": "Cookie Auth", + "Cookie String": "Cookie String", + "Cooldown": "พักเครื่อง", + "Copied!": "คัดลอกแล้ว!", + "Copy": "คัดลอก", + "Copy & Shutdown": "คัดลอกและปิดระบบ", + "Copy This URL": "คัดลอก URL นี้", + "Copy a link and paste to your AI to use 9Router — no install needed": "คัดลอกลิงก์แล้ววางให้ AI ของคุณเพื่อใช้ 9Router — ไม่ต้องติดตั้ง", + "Copy combo name": "คัดลอกชื่อ combo", + "Copy install command": "คัดลอกคำสั่งติดตั้ง", + "Copy model": "คัดลอกโมเดล", + "Copy the JSON below to your ~/.qwen/settings.json file.": "คัดลอก JSON ด้านล่างไปยังไฟล์ ~/.qwen/settings.json ของคุณ", + "Copy the entire cookie string (must include BXAuth)": "คัดลอก cookie string ทั้งหมด (ต้องมี BXAuth)", + "Cost": "ค่าใช้จ่าย", + "Cost Calculation:": "การคำนวณค่าใช้จ่าย:", + "Costs": "ค่าใช้จ่าย", + "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)": "ค่าใช้จ่ายคำนวณจาก token usage และ pricing rates ค่าใช้จ่ายของแต่ละคำขอคำนวณจาก: (input_tokens × input_rate) + (output_tokens × output_rate) + (cached_tokens × cached_rate)", + "Could not read Cursor database automatically.": "ไม่สามารถอ่าน Cursor database โดยอัตโนมัติได้", + "Create": "สร้าง", + "Create API Key": "สร้าง API Key", + "Create Combo": "สร้าง Combo", + "Create Cowork Combo": "สร้าง Cowork Combo", + "Create Key": "สร้าง Key", + "Create Provider": "สร้าง Provider", + "Create Token": "สร้าง Token", + "Create a": "สร้าง", + "Create a proxy pool entry, then assign it to connections.": "สร้าง proxy pool entry แล้วมอบหมายให้กับการเชื่อมต่อ", + "Create model combos with fallback support": "สร้าง model combos ที่รองรับ fallback", + "Create your first API key to get started": "สร้าง API key แรกของคุณเพื่อเริ่มต้น", + "Created": "สร้างแล้ว", + "Creating...": "กำลังสร้าง...", + "Current": "ปัจจุบัน", + "Current Password": "รหัสผ่านปัจจุบัน", + "Current Pricing Overview": "ภาพรวม Pricing ปัจจุบัน", + "Current password": "รหัสผ่านปัจจุบัน", + "Current: Keeps": "ปัจจุบัน: คงไว้", + "Currently using accounts in priority order (Fill First).": "ใช้บัญชีตามลำดับความสำคัญ (เติมก่อน)", + "Cursor AI Code Editor": "Cursor AI Code Editor", + "Cursor IDE not detected. Please paste your tokens manually.": "ไม่พบ Cursor IDE กรุณาวาง tokens ด้วยตนเอง", + "Cursor routes requests through its own server, so local endpoint is not supported. Please enable Tunnel or Cloud Endpoint in Settings.": "Cursor ส่งต่อคำขอผ่านเซิร์ฟเวอร์ของตัวเอง จึงไม่รองรับ local endpoint กรุณาเปิดใช้งาน Tunnel หรือ Cloud Endpoint ในการตั้งค่า", + "Custom": "กำหนดเอง", + "Custom Pricing:": "กำหนดราคาเอง:", + "Custom Providers (OpenAI/Anthropic Compatible)": "Custom Providers (OpenAI/Anthropic Compatible)", + "Custom Token": "Custom Token", + "Custom accounts per page": "กำหนดจำนวนบัญชีต่อหน้าเอง", + "Custom providers": "Custom providers", + "Custom...": "กำหนดเอง...", + "Cycle through accounts to distribute load": "วนลูปบัญชีเพื่อกระจายโหลด", + "Cycle through providers in combos instead of always starting with first": "วนลูป providers ใน combos แทนที่จะเริ่มจากตัวแรกเสมอ", + "DNS off": "DNS ปิด", + "Dashboard": "แดชบอร์ด", + "Dashboard Password": "รหัสผ่านแดชบอร์ด", + "Dashboard:": "แดชบอร์ด:", + "Data Location:": "ตำแหน่งข้อมูล:", + "Data flows seamlessly from your application through our intelligent routing layer to the best provider for the job.": "ข้อมูลไหลอย่างราบรื่นจากแอปพลิเคชันของคุณผ่าน intelligent routing layer ไปยังผู้ให้บริการที่เหมาะสมที่สุด", + "Data flows seamlessly through our intelligent routing system": "ข้อมูลไหลอย่างราบรื่นผ่านระบบ intelligent routing ของเรา", + "Database Location": "ตำแหน่งฐานข้อมูล", + "Database backup downloaded": "ดาวน์โหลดฐานข้อมูลสำรองแล้ว", + "Database imported successfully": "นำเข้าฐานข้อมูลสำเร็จ", + "DateTime": "วันที่และเวลา", + "Deactivate": "ปิดใช้งาน", + "Debug": "ดีบัก", + "Debug translation flow between formats": "ดีบักการไหลของการแปลระหว่าง formats", + "DeepSeek TUI - Manual Configuration": "DeepSeek TUI - กำหนดค่าด้วยตนเอง", + "DeepSeek TUI not detected locally": "ไม่พบ DeepSeek TUI บนเครื่อง", + "DeepSeek TUI uses ~/.deepseek/config.toml for configuration. 9Router will update the provider to 'openai' mode with your base_url, api_key, and model.": "DeepSeek TUI ใช้ ~/.deepseek/config.toml สำหรับการกำหนดค่า 9Router จะอัปเดต provider เป็น 'openai' mode พร้อม base_url, api_key และ model ของคุณ", + "DeepSeek Terminal Coding Agent (Rust TUI)": "DeepSeek Terminal Coding Agent (Rust TUI)", + "Default Model": "โมเดลเริ่มต้น", + "Default password is": "รหัสผ่านเริ่มต้นคือ", + "Default password is 123456": "รหัสผ่านเริ่มต้นคือ 123456", + "Delete": "ลบ", + "Delete API Key": "ลบ API Key", + "Delete connection": "ลบการเชื่อมต่อ", + "Delete saved endpoint": "ลบ endpoint ที่บันทึกไว้", + "Delete selected preset": "ลบ preset ที่เลือก", + "Delete this combo?": "ลบ combo นี้?", + "Delete this connection?": "ลบการเชื่อมต่อนี้?", + "Deno Deploy API Token": "Deno Deploy API Token", + "Deno Deploy v2 runs on a high-performance global edge network": "Deno Deploy v2 ทำงานบน高性能 global edge network", + "Deno Relay": "Deno Relay", + "Deploy": "deploy", + "Deploy Cloudflare Relay": "Deploy Cloudflare Relay", + "Deploy Deno Relay": "Deploy Deno Relay", + "Deploy Relay": "Deploy Relay", + "Deploy Vercel Relay": "Deploy Vercel Relay", + "Deploy multiple relays for maximum IP diversity": "deploy relays หลายตัวเพื่อความหลากหลายของ IP มากที่สุด", + "Deploy multiple relays on different accounts for more IP diversity": "deploy relays หลายตัวบนบัญชีต่างกันเพื่อความหลากหลายของ IP มากขึ้น", + "Deploying... (may take ~1 min)": "กำลัง deploy... (อาจใช้เวลาประมาณ 1 นาที)", + "Deployment Name": "ชื่อ deployment", + "Deploys a Cloudflare Worker as a proxy relay. All AI provider requests will be forwarded through Cloudflare's global edge network.": "deploy Cloudflare Worker เป็น proxy relay คำขอจาก AI providers ทั้งหมดจะถูกส่งต่อผ่าน global edge network ของ Cloudflare", + "Deploys a relay worker to Deno Deploy's global edge network. All AI provider requests are forwarded through Deno's edge, masking your real IP.": "deploy relay worker ไปยัง global edge network ของ Deno Deploy คำขอจาก AI providers ทั้งหมดจะถูกส่งต่อผ่าน Deno edge ปิดบัง IP จริงของคุณ", + "Deploys an edge relay function to Vercel that proxies requests through Vercel's network.": "deploy edge relay function ไปยัง Vercel ที่ส่งต่อคำขอผ่านเครือข่าย Vercel", + "Deploys an edge relay function to Vercel. All AI provider requests will be forwarded through Vercel's edge network, masking your real IP from providers.": "deploy edge relay function ไปยัง Vercel คำขอจาก AI providers ทั้งหมดจะถูกส่งต่อผ่าน edge network ของ Vercel ปิดบัง IP จริงของคุณจาก providers", + "Desktop": "เดสก์ท็อป", + "Detail": "รายละเอียด", + "Details": "รายละเอียด", + "Dimensions": "มิติ", + "Disable": "ปิดใช้งาน", + "Disable All": "ปิดทั้งหมด", + "Disable Tailscale": "ปิด Tailscale", + "Disable Tunnel": "ปิด Tunnel", + "Disable connections with depleted quota on the current page": "ปิดการเชื่อมต่อที่ quota หมดบนหน้าปัจจุบัน", + "Disable provider": "ปิด provider", + "Disable this model": "ปิดโมเดลนี้", + "Disabled": "ปิดใช้งานแล้ว", + "Disabling...": "กำลังปิด...", + "Disconnected from server": "ตัดการเชื่อมต่อจากเซิร์ฟเวอร์", + "Dismiss notification": "ปิดการแจ้งเตือน", + "Display Name": "ชื่อที่แสดง", + "Display language": "ภาษาที่แสดง", + "Docs": "เอกสาร", + "Documentation": "เอกสาร", + "Domain:": "โดเมน:", + "Donate": "บริจาค", + "Done": "เสร็จสิ้น", + "Download": "ดาวน์โหลด", + "Download Backup": "ดาวน์โหลดข้อมูลสำรอง", + "Drag to reorder": "ลากเพื่อจัดเรียงใหม่", + "Easy Setup": "ตั้งค่าง่าย", + "Edit": "แก้ไข", + "Edit Combo": "แก้ไข Combo", + "Edit Connection": "แก้ไขการเชื่อมต่อ", + "Edit Pricing": "แก้ไข Pricing", + "Edit Proxy Pool": "แก้ไข Proxy Pool", + "Edit connection": "แก้ไขการเชื่อมต่อ", + "Edit hosts file manually to add the following entries:": "แก้ไขไฟล์ hosts ด้วยตนเองเพื่อเพิ่มรายการต่อไปนี้:", + "Email": "อีเมล", + "Embedding": "Embedding", + "Embeddings": "Embeddings", + "Enable": "เปิดใช้งาน", + "Enable DNS per tool below to activate interception": "เปิดใช้งาน DNS สำหรับแต่ละเครื่องมือด้านล่างเพื่อเปิดใช้งานการดักจับ", + "Enable DNS to edit model mappings": "เปิดใช้งาน DNS เพื่อแก้ไข model mappings", + "Enable Observability": "เปิดใช้งาน Observability", + "Enable OpenAI API": "เปิดใช้งาน OpenAI API", + "Enable Tunnel": "เปิดใช้งาน Tunnel", + "Enable connections that still have quota on the current page": "เปิดการเชื่อมต่อที่ยังมี quota บนหน้าปัจจุบัน", + "Enable provider": "เปิด provider", + "Enable proxy for OAuth + provider outbound requests.": "เปิด proxy สำหรับ OAuth + provider outbound requests", + "Encrypted": "เข้ารหัสแล้ว", + "End Date": "วันที่สิ้นสุด", + "End-to-end TLS via Cloudflare": "TLS แบบ End-to-end ผ่าน Cloudflare", + "Endpoint": "Endpoint", + "Endpoint & Key": "Endpoint & Key", + "Endpoint is exposed without an API key.": "Endpoint เปิดให้เข้าถึงโดยไม่มี API key", + "Enter current password": "กรุณาป้อนรหัสผ่านปัจจุบัน", + "Enter model id": "กรุณาป้อน model id", + "Enter model id (provider-specific)": "กรุณาป้อน model id (เจาะจงผู้ให้บริการ)", + "Enter new API key": "ป้อน API key ใหม่", + "Enter new password": "ป้อนรหัสผ่านใหม่", + "Enter or pick API key": "ป้อนหรือเลือก API key", + "Enter password": "ป้อนรหัสผ่าน", + "Enter sudo password": "ป้อนรหัสผ่าน sudo", + "Enter the model ID exactly as your compatible endpoint expects it. This model will be saved as the connection default.": "ป้อน model ID ตรงตามที่ compatible endpoint ของคุณต้องการ โมเดลนี้จะถูกบันทึกเป็นค่าเริ่มต้นของการเชื่อมต่อ", + "Enter your API key": "ป้อน API key ของคุณ", + "Enter your current password to": "ป้อนรหัสผ่านปัจจุบันเพื่อ", + "Enter your password to access the dashboard": "ป้อนรหัสผ่านเพื่อเข้าถึงแดชบอร์ด", + "Error": "ข้อผิดพลาด", + "Est. Cost": "ค่าใช้จ่ายโดยประมาณ", + "Estimated, not actual billing": "ค่าใช้จ่ายโดยประมาณ ไม่ใช่บิลจริง", + "Everything you need to manage your AI infrastructure efficiently.": "ทุกสิ่งที่คุณต้องการในการจัดการ AI infrastructure ของคุณอย่างมีประสิทธิภาพ", + "Everything you need to manage your AI infrastructure in one place, built for scale.": "ทุกสิ่งที่คุณต้องการในการจัดการ AI infrastructure ในที่เดียว สร้างมาเพื่อรองรับขนาดใหญ่", + "Example": "ตัวอย่าง", + "Experimental": "ทดลองใช้", + "Expires At": "หมดอายุ", + "Expiring first": "ใกล้หมดอายุก่อน", + "Expiring-first currently reorders accounts inside the current page. Cross-page ordering still follows backend pagination.": "การจัดเรียงแบบใกล้หมดอายุก่อนจะจัดเรียงบัญชีภายในหน้าปัจจุบัน การจัดเรียงข้ามหน้ายังคงใช้ pagination จาก backend", + "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.": "เปิด local 9Router ของคุณสู่อินเทอร์เน็ต ไม่ต้อง port forwarding ไม่ต้อง static IP แชร์ endpoint URL กับทีมหรือใช้ใน Cursor, Cline และเครื่องมือ AI อื่นๆ จากทุกที่", + "Factory Droid - Manual Configuration": "Factory Droid - กำหนดค่าด้วยตนเอง", + "Factory Droid AI Assistant": "Factory Droid AI Assistant", + "Factory Droid CLI not detected locally": "ไม่พบ Factory Droid CLI บนเครื่อง", + "Factory Droid CLI not installed": "ไม่ได้ติดตั้ง Factory Droid CLI", + "Fail request if proxy is unreachable instead of falling back to direct.": "ล้มเหลวคำขอเมื่อ proxy ไม่สามารถเข้าถึงได้แทนที่จะ fallback ไปยัง direct", + "Failed to apply settings": "ไม่สามารถใช้การตั้งค่าได้", + "Failed to create combo": "ไม่สามารถสร้าง combo ได้", + "Failed to load changelog:": "ไม่สามารถโหลด changelog ได้:", + "Failed to load usage statistics.": "ไม่สามารถโหลด usage statistics ได้", + "Failed to reset settings": "ไม่สามารถรีเซ็ตการตั้งค่าได้", + "Failed to set alias": "ไม่สามารถตั้ง alias ได้", + "Failed to update combo": "ไม่สามารถอัปเดต combo ได้", + "Failed to update password": "ไม่สามารถอัปเดตรหัสผ่านได้", + "Failed to update proxy settings": "ไม่สามารถอัปเดต proxy settings ได้", + "Fallback": "Fallback", + "Fallback — tries models in order (next on failure)": "Fallback — ลองโมเดลตามลำดับ (ถัดไปเมื่อล้มเหลว)", + "Fallback — try in order": "Fallback — ลองตามลำดับ", + "Features": "คุณสมบัติ", + "Fetch Qoder Models": "ดึง Qoder Models", + "Fetching...": "กำลังดึงข้อมูล...", + "Files": "ไฟล์", + "Filter accounts by status": "กรองบัญชีตามสถานะ", + "Filter naming": "กรอง naming", + "Filter naming requests": "กรอง naming requests", + "Filter quota providers": "กรอง quota providers", + "Find MCPs →": "ค้นหา MCPs →", + "Find your Account ID in the right sidebar of": "ค้นหา Account ID ของคุณในแถบด้านขวาของ", + "Find your Account ID in the right sidebar of dash.cloudflare.com": "ค้นหา Account ID ของคุณในแถบด้านขวาของ dash.cloudflare.com", + "First Page": "หน้าแรก", + "Flush Interval (ms)": "Flush Interval (ms)", + "For enterprise users with custom AWS IAM Identity Center.": "สำหรับผู้ใช้ enterprise ที่มี AWS IAM Identity Center กำหนดเอง", + "Forgot password? Open": "ลืมรหัสผ่าน? เปิด", + "Format": "รูปแบบ", + "Found on the right side of the Cloudflare dashboard overview page.": "พบที่ด้านขวาของหน้าภาพรวม Cloudflare dashboard", + "Free": "ฟรี", + "Free & Free Tier Providers": "Free & Free Tier Providers", + "Free Providers": "Free Providers", + "Free Tier": "Free Tier", + "Free Tier Providers": "Free Tier Providers", + "Free tier: 100,000 requests per day": "Free tier: 100,000 requests ต่อวัน", + "Free tier: 100GB bandwidth/month, 500K edge invocations": "Free tier: 100GB bandwidth/เดือน, 500K edge invocations", + "Free tier: 1M requests & 100GiB outbound traffic per month": "Free tier: 1M requests & 100GiB outbound traffic ต่อเดือน", + "Fresh API key obtained": "ได้รับ API key ใหม่แล้ว", + "Full shell access": "Full shell access", + "Fusion": "Fusion", + "Fusion — panel + judge": "Fusion — panel + judge", + "Fusion — queries all models in parallel, then a judge synthesizes one answer": "Fusion — query โมเดลทั้งหมดแบบ parallel แล้ว judge สร้างคำตอบเดียว", + "Get 9Remote": "รับ 9Remote", + "Get API Key": "รับ API Key", + "Get API Key →": "รับ API Key →", + "Get Started": "เริ่มต้นใช้งาน", + "Get Started in 30 Seconds": "เริ่มต้นใน 30 วินาที", + "Get started": "เริ่มต้น", + "Get started in seconds. Just install, open, and route.": "เริ่มต้นในไม่กี่วินาที ติดตั้ง เปิด และ route", + "Get token →": "รับ token →", + "GitHub": "GitHub", + "GitHub Account": "GitHub Account", + "GitHub Copilot - Manual Configuration": "GitHub Copilot - กำหนดค่าด้วยตนเอง", + "GitHub Copilot IDE with MITM": "GitHub Copilot IDE พร้อม MITM", + "GitLab Access Tokens": "GitLab Access Tokens", + "GitLab Applications": "GitLab Applications", + "GitLab Base URL": "GitLab Base URL", + "Go to": "ไปที่", + "Go to Roo Settings panel": "ไปที่ Roo Settings panel", + "Google Account": "Google Account", + "Google Antigravity IDE with MITM": "Google Antigravity IDE พร้อม MITM", + "Granted At": "ให้สิทธิ์เมื่อ", + "Group models under one name, then pick a strategy per combo:": "รวมโมเดลภายใต้ชื่อเดียว แล้วเลือกกลยุทธ์สำหรับแต่ละ combo:", + "Headroom proxy is reachable. You can enable the token saver.": "Headroom proxy สามารถเข้าถึงได้ คุณสามารถเปิดใช้งาน token saver", + "Help Center": "ศูนย์ช่วยเหลือ", + "Hermes Agent - Manual Configuration": "Hermes Agent - กำหนดค่าด้วยตนเอง", + "Hermes Agent not detected locally": "ไม่พบ Hermes Agent บนเครื่อง", + "Hide": "ซ่อน", + "Hide key": "ซ่อน key", + "High performance global routing and IP masking via Cloudflare Workers": "High performance global routing และ IP masking ผ่าน Cloudflare Workers", + "High-performance Rust-based coding agent harness": "High-performance coding agent harness ที่สร้างด้วย Rust", + "History": "ประวัติ", + "How 9Router Works": "วิธีการทำงานของ 9Router", + "How Pricing Works": "วิธีการทำงานของ Pricing", + "How it Works": "วิธีการทำงาน", + "How it works:": "วิธีการทำงาน:", + "How to Install": "วิธีการติดตั้ง", + "How to generate API token:": "วิธีสร้าง API token:", + "How to generate your API Token:": "วิธีสร้าง API Token ของคุณ:", + "How to get cookie:": "วิธีรับ cookie:", + "ID:": "ID:", + "IDC Start URL": "IDC Start URL", + "If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead.": "หาก provider ไม่มี /models endpoint ให้ป้อน model ID เพื่อตรวจสอบผ่าน chat/completions แทน", + "Image Generation": "สร้างรูปภาพ", + "Image to Text": "รูปภาพเป็นข้อความ", + "Import": "นำเข้า", + "Import Backup": "นำเข้าข้อมูลสำรอง", + "Import CLIProxyAPI JSON": "นำเข้า CLIProxyAPI JSON", + "Import Token": "นำเข้า Token", + "Importing...": "กำลังนำเข้า...", + "In": "ขาเข้า", + "In / Out": "ขาเข้า / ขาออก", + "Inactive": "ไม่ใช้งาน", + "Inactive pools are ignored by runtime resolution.": "Inactive pools จะถูกเมินโดย runtime resolution", + "Inc. All rights reserved.": "Inc. สงวนลิขสิทธิ์", + "Initializing...": "กำลังเริ่มต้น...", + "Input": "ขาเข้า", + "Input Cost": "ค่าใช้จ่ายขาเข้า", + "Input Tokens": "Input Tokens", + "Input Tokens:": "Input Tokens:", + "Input:": "ขาเข้า:", + "Install 9Router": "ติดตั้ง 9Router", + "Install 9Router, configure your providers via web dashboard, and start routing AI requests.": "ติดตั้ง 9Router กำหนดค่า providers ผ่าน web dashboard แล้วเริ่ม routing AI requests", + "Install Chrome extension": "ติดตั้ง Chrome extension", + "Install Cline VS Code extension or CLI from": "ติดตั้ง Cline VS Code extension หรือ CLI จาก", + "Install Kilo Code from": "ติดตั้ง Kilo Code จาก", + "Install Qwen Code": "ติดตั้ง Qwen Code", + "Install Tailscale": "ติดตั้ง Tailscale", + "Install command:": "คำสั่งติดตั้ง:", + "Install jcode to enable automatic configuration:": "ติดตั้ง jcode เพื่อเปิดใช้งานการกำหนดค่าอัตโนมัติ:", + "Install the Amp CLI using the package manager supported by your environment.": "ติดตั้ง Amp CLI โดยใช้ package manager ที่รองรับในสภาพแวดล้อมของคุณ", + "Install then click Start:": "ติดตั้งแล้วคลิกเริ่ม:", + "Install via npm:": "ติดตั้งผ่าน npm:", + "Installation Guide": "คู่มือการติดตั้ง", + "Installing Tailscale...": "กำลังติดตั้ง Tailscale...", + "Interactive diagram visible on desktop": "แผนภาพแบบ interactive ที่มองเห็นบนเดสก์ท็อป", + "Intercept CLI tool traffic and route through 9Router": "ดักจับ CLI tool traffic แล้วส่งต่อผ่าน 9Router", + "Intercepts Antigravity traffic via DNS redirect, letting you reroute models through 9Router.": "ดักจับ Antigravity traffic ผ่าน DNS redirect ช่วยให้คุณ reroute โมเดลผ่าน 9Router", + "Intercepts Claude Code's topic-naming requests and returns a fake response locally, saving API tokens.": "ดักจับ topic-naming requests ของ Claude Code แล้วส่ง fake response กลับภายในเครื่อง ประหยัด API tokens", + "Invalid": "ไม่ถูกต้อง", + "Invalid password": "รหัสผ่านไม่ถูกต้อง", + "Issuer URL": "Issuer URL", + "JSON Response": "JSON Response", + "Join developers who are streamlining their AI integrations with 9Router. Open source and free to start.": "เข้าร่วมกับนักพัฒนาที่กำลังปรับปรุง AI integrations ของพวกเขาด้วย 9Router Open source และเริ่มต้นใช้งานฟรี", + "Judge": "Judge", + "Just now": "เมื่อสักครู่", + "KB per field": "KB ต่อฟิลด์", + "Keep the legacy password login.": "คงการเข้าสู่ระบบด้วยรหัสผ่านแบบเดิมไว้", + "Key Name": "ชื่อ Key", + "KiRo dashboard": "KiRo dashboard", + "Kill & Start": "หยุดและเริ่มใหม่", + "Kill this process to start MITM Server?": "หยุด process นี้เพื่อเริ่ม MITM Server?", + "Kilo Code - Manual Configuration": "Kilo Code - กำหนดค่าด้วยตนเอง", + "Kilo Code AI Assistant": "Kilo Code AI Assistant", + "Kilo Code not detected locally": "ไม่พบ Kilo Code บนเครื่อง", + "Kimi": "Kimi", + "Kiro AI": "Kiro AI", + "Kiro IDE not detected. Please paste your refresh token manually.": "ไม่พบ Kiro IDE กรุณาวาง refresh token ด้วยตนเอง", + "Kiro IDE with MITM": "Kiro IDE พร้อม MITM", + "Language": "ภาษา", + "Languages": "ภาษา", + "Last Page": "หน้าสุดท้าย", + "Last Used": "ใช้ล่าสุด", + "Last tested:": "ทดสอบล่าสุด:", + "Last updated:": "อัปเดตล่าสุด:", + "Latency": "ความเร็ว", + "Latency:": "ความเร็ว:", + "Lazy senior dev": "senior dev ขี้เกียจ", + "Lean": "Lean", + "Leave blank to keep existing secret": "เว้นว่างไว้เพื่อคง secret ที่มีอยู่", + "Leave blank to use": "เว้นว่างไว้เพื่อใช้", + "Leave empty for public PKCE app": "เว้นว่างสำหรับ PKCE app สาธารณะ", + "Leave empty to inherit existing env proxy (if any).": "เว้นว่างไว้เพื่อสืบทอด env proxy ที่มีอยู่ (ถ้ามี)", + "Legacy manual proxy fields are still accepted by API for backward compatibility.": "Legacy manual proxy fields ยังคงได้รับการยอมรับจาก API เพื่อ backward compatibility", + "Legacy:": "เดิม:", + "Legal": "กฎหมาย", + "Live server console output": "Live server console output", + "Load": "โหลด", + "Loading logs...": "กำลังโหลด logs...", + "Loading models from provider...": "กำลังโหลดโมเดลจาก provider...", + "Loading pricing data...": "กำลังโหลด pricing data...", + "Loading registry...": "กำลังโหลด registry...", + "Loading reset credits...": "กำลังโหลด reset credits...", + "Loading...": "กำลังโหลด...", + "Local": "ท้องถิ่น", + "Local Mode": "Local Mode", + "Local Mode - All data stored on your machine": "Local Mode - ข้อมูลทั้งหมดจัดเก็บบนเครื่องของคุณ", + "Local Plugins": "Local Plugins", + "Locked. Retry in": "ล็อก. ลองใหม่ใน", + "Login": "เข้าสู่ระบบ", + "Login Button Label": "ป้ายปุ่มเข้าสู่ระบบ", + "Login URL": "Login URL", + "Login to your account": "เข้าสู่ระบบบัญชีของคุณ", + "Login with your GitHub account (manual callback).": "เข้าสู่ระบบด้วย GitHub account (manual callback)", + "Login with your Google account (manual callback).": "เข้าสู่ระบบด้วย Google account (manual callback)", + "Logout": "ออกจากระบบ", + "Logs": "Logs", + "Logs are loaded from the request history database.": "Logs จะถูกโหลดจาก request history database", + "Logs are saved to log.txt in the application data directory.": "Logs จะถูกบันทึกลง log.txt ใน application data directory", + "MIT License": "MIT License", + "MITM": "MITM", + "MITM Proxy": "MITM Proxy", + "MITM Server": "MITM Server", + "MITM Tools": "MITM Tools", + "Machine ID": "Machine ID", + "Machine ID will be auto-filled...": "Machine ID จะถูกเติมอัตโนมัติ...", + "Make sure Cursor IDE has been opened at least once, then click": "ตรวจสอบว่า Cursor IDE เปิดอย่างน้อยหนึ่งครั้งแล้ว แล้วคลิก", + "Manage": "จัดการ", + "Manage reusable per-connection proxies and bind them to provider connections.": "จัดการ reusable per-connection proxies แล้ว bind เข้ากับ provider connections", + "Manage your AI provider connections": "จัดการการเชื่อมต่อ AI providers ของคุณ", + "Manage your Embedding providers": "จัดการ Embedding providers ของคุณ", + "Manage your Image to Text providers": "จัดการ Image to Text providers ของคุณ", + "Manage your Music providers": "จัดการ Music providers ของคุณ", + "Manage your Speech To Text providers": "จัดการ Speech To Text providers ของคุณ", + "Manage your Text To Speech providers": "จัดการ Text To Speech providers ของคุณ", + "Manage your Text to Image providers": "จัดการ Text to Image providers ของคุณ", + "Manage your Video providers": "จัดการ Video providers ของคุณ", + "Manage your Web Fetch providers": "จัดการ Web Fetch providers ของคุณ", + "Manage your Web Search providers": "จัดการ Web Search providers ของคุณ", + "Manage your preferences": "จัดการการตั้งค่าของคุณ", + "Manage your proxy pool configurations": "จัดการการกำหนดค่า proxy pool ของคุณ", + "Manual / current endpoint": "Manual / current endpoint", + "Manual Callback Required": "ต้องใช้ Manual Callback", + "Manual Config": "กำหนดค่าด้วยตนเอง", + "Manual configuration is still available if 9router is deployed on a remote server.": "การกำหนดค่าด้วยตนเองยังใช้ได้หาก 9router ถูก deploy บน remote server", + "Map Amp shorthand names such as g25p or cs45 to 9Router aliases in your local config.": "Map Amp shorthand names เช่น g25p หรือ cs45 ไปยัง 9Router aliases ในการกำหนดค่าท้องถิ่นของคุณ", + "Mask (URL)": "Mask (URL)", + "Max JSON Size (KB)": "Max JSON Size (KB)", + "Max Records": "Max Records", + "Maximum request detail records to keep (older records are auto-deleted)": "จำนวน request detail records สูงสุดที่จะเก็บ (records เก่าจะถูกลบอัตโนมัติ)", + "Maximum size for each JSON field (request/response) before truncation": "ขนาดสูงสุดสำหรับ JSON field แต่ละตัว (request/response) ก่อนถูกตัด", + "Maximum time to wait before flushing buffer (prevents data loss during low traffic)": "เวลาสูงสุดที่จะรอก่อน flush buffer (ป้องกันข้อมูลสูญหายในช่วง traffic ต่ำ)", + "Media Providers": "Media Providers", + "Menu": "เมนู", + "Message AI": "ส่งข้อความหา AI", + "Messages": "ข้อความ", + "Messages API": "Messages API", + "MiniMax": "MiniMax", + "Model": "โมเดล", + "Model Fallback": "Model Fallback", + "Model ID": "Model ID", + "Model ID (from OpenRouter)": "Model ID (จาก OpenRouter)", + "Model ID (optional)": "Model ID (ไม่บังคับ)", + "Model Status": "สถานะโมเดล", + "Model combos": "Model combos", + "Model combos with fallback": "Model combos พร้อม fallback", + "Model is reachable": "โมเดลเข้าถึงได้", + "Model list is filtered from connected providers.": "รายชื่อโมเดลถูกกรองจาก connected providers", + "Model mappings will be available soon.": "Model mappings จะพร้อมใช้งานเร็วๆ นี้", + "Model not reachable": "โมเดลเข้าถึงไม่ได้", + "Model:": "โมเดล:", + "Models": "โมเดล", + "Monitor your API usage, token consumption, and request logs": "ตรวจสอบ API usage, token consumption และ request logs ของคุณ", + "More on GitHub": "ดูเพิ่มเติมบน GitHub", + "Move down": "เลื่อนลง", + "Move up": "เลื่อนขึ้น", + "Music": "เพลง", + "My Profile": "โปรไฟล์ของฉัน", + "N/A": "ไม่มี", + "NPM": "NPM", + "Name": "ชื่อ", + "Name is required": "ต้องระบุชื่อ", + "Native CLI tool support for Cursor, Claude, Copilot, and more.": "รองรับ CLI tools อย่างเป็นทางการสำหรับ Cursor, Claude, Copilot และอื่นๆ", + "Navigate to home": "ไปที่หน้าแรก", + "Network": "เครือข่าย", + "Network Error": "เครือข่ายขัดข้อง", + "Network error": "เครือข่ายขัดข้อง", + "Never": "ไม่เคย", + "New Password": "รหัสผ่านใหม่", + "New password": "รหัสผ่านใหม่", + "Next": "ถัดไป", + "Next accounts page": "หน้าบัญชีถัดไป", + "No API keys - Create one in Keys page": "ยังไม่มี API keys - สร้างในหน้า Keys", + "No API keys yet": "ยังไม่มี API keys", + "No MCPs added": "ยังไม่ได้เพิ่ม MCPs", + "No Providers Connected": "ยังไม่มี Providers เชื่อมต่อ", + "No Proxy": "ไม่มี Proxy", + "No active connections found for this group.": "ไม่พบการเชื่อมต่อที่ใช้งานอยู่สำหรับกลุ่มนี้", + "No active providers": "ไม่มี providers ที่ใช้งานอยู่", + "No active proxy pools available. Create one in Proxy Pools page first.": "ไม่มี proxy pools ที่ใช้งานอยู่ สร้างในหน้า Proxy Pools ก่อน", + "No authentication required": "ไม่ต้องยืนยันตัวตน", + "No combos yet": "ยังไม่มี combos", + "No combos yet.": "ยังไม่มี combos", + "No compatible providers added yet": "ยังไม่ได้เพิ่ม compatible providers", + "No connections": "ไม่มีการเชื่อมต่อ", + "No connections yet": "ยังไม่มีการเชื่อมต่อ", + "No console logs yet.": "ยังไม่มี console logs", + "No conversations yet.": "ยังไม่มีการสนทนา", + "No custom providers": "ไม่มี custom providers", + "No custom providers — use buttons above to add OpenAI/Anthropic compatible endpoints": "ไม่มี custom providers — ใช้ปุ่มด้านบนเพื่อเพิ่ม OpenAI/Anthropic compatible endpoints", + "No data for this period": "ไม่มีข้อมูลสำหรับช่วงเวลานี้", + "No key configured": "ยังไม่ได้กำหนดค่า key", + "No language selected": "ยังไม่ได้เลือกภาษา", + "No languages found.": "ไม่พบภาษา", + "No logs recorded yet.": "ยังไม่มี logs บันทึกไว้", + "No model selected.": "ยังไม่ได้เลือกโมเดล", + "No models": "ยังไม่มีโมเดล", + "No models added yet": "ยังไม่ได้เพิ่มโมเดล", + "No models configured": "ยังไม่ได้กำหนดค่าโมเดล", + "No models found": "ไม่พบโมเดล", + "No models match your filter.": "ไม่มีโมเดลที่ตรงกับตัวกรองของคุณ", + "No models selected": "ยังไม่ได้เลือกโมเดล", + "No port forwarding needed": "ไม่ต้อง port forwarding", + "No pricing data available": "ไม่มี pricing data ที่พร้อมใช้งาน", + "No providers connected": "ยังไม่มี providers เชื่อมต่อ", + "No providers match your search": "ไม่มี providers ที่ตรงกับการค้นหาของคุณ", + "No providers support": "ไม่มี providers รองรับ", + "No providers yet.": "ยังไม่มี providers", + "No providers.": "ไม่มี providers", + "No proxy pool entries yet": "ยังไม่มี proxy pool entries", + "No proxy:": "ไม่มี proxy:", + "No quota data available": "ไม่มี quota data ที่พร้อมใช้งาน", + "No request details found": "ไม่พบ request details", + "No requests yet.": "ยังไม่มี requests", + "No reset credit details returned for this account.": "ไม่มี reset credit details สำหรับบัญชีนี้", + "No results": "ไม่มีผลลัพธ์", + "No servers match filter": "ไม่มีเซิร์ฟเวอร์ที่ตรงกับตัวกรอง", + "No tools advertised by server.": "เซิร์ฟเวอร์ไม่ได้โฆษณาเครื่องมือใดๆ", + "No usage yet.": "ยังไม่มีการใช้งาน", + "None": "ไม่มี", + "None (unbind all)": "ไม่มี (unbind ทั้งหมด)", + "Not configured": "ยังไม่ได้กำหนดค่า", + "Not installed": "ไม่ได้ติดตั้ง", + "Notice": "ประกาศ", + "Nous Research self-improving AI agent": "Nous Research self-improving AI agent", + "Number of items to accumulate before writing to database (higher = better performance)": "จำนวนรายการที่สะสมก่อนเขียนลง database (ยิ่งมาก = ประสิทธิภาพยิ่งดี)", + "OAuth": "OAuth", + "OAuth & API Keys": "OAuth & API Keys", + "OAuth Account": "OAuth Account", + "OAuth App": "OAuth App", + "OAuth Providers": "OAuth Providers", + "OAuth required": "ต้องใช้ OAuth", + "OIDC Dashboard Login": "OIDC Dashboard Login", + "OIDC active": "OIDC ใช้งานอยู่", + "OIDC login is currently active. Password login is disabled until you switch back.": "OIDC login ใช้งานอยู่ในขณะนี้ การเข้าสู่ระบบด้วยรหัสผ่านจะปิดอยู่จนกว่าจะสลับกลับ", + "OIDC login is enabled, but the issuer/client fields are not configured yet. Password login is still available for recovery.": "OIDC login เปิดใช้งานแล้ว แต่ issuer/client fields ยังไม่ได้กำหนดค่า การเข้าสู่ระบบด้วยรหัสผ่านยังใช้ได้สำหรับการกู้คืน", + "OIDC only": "OIDC เท่านั้น", + "Observability": "Observability", + "Office Proxy": "Office Proxy", + "Ollama Host URL": "Ollama Host URL", + "One Endpoint for": "One Endpoint สำหรับ", + "One key per line. Format:": "หนึ่ง key ต่อบรรทัด รูปแบบ:", + "One-to-one (rotate)": "One-to-one (rotate)", + "Only from connected providers": "เฉพาะจาก connected providers", + "Only letters, numbers, - and _ allowed": "อนุญาตเฉพาะตัวอักษร, ตัวเลข, - และ _", + "Only letters, numbers, -, _ and .": "อนุญาตเฉพาะตัวอักษร, ตัวเลข, -, _ และ .", + "Only letters, numbers, -, _ and . allowed": "อนุญาตเฉพาะตัวอักษร, ตัวเลข, -, _ และ .", + "Only one connection is allowed per compatible node. Add another node if you need more connections.": "อนุญาตหนึ่งการเชื่อมต่อต่อ compatible node หากต้องการการเชื่อมต่อเพิ่มเติม ให้เพิ่ม node อีกตัว", + "Open": "เปิด", + "Open Claude Desktop → Help → Troubleshooting → Enable Developer mode → Configure third-party inference, then return here.": "เปิด Claude Desktop → Help → Troubleshooting → Enable Developer mode → Configure third-party inference แล้วกลับมาที่นี่", + "Open Claw - Manual Configuration": "Open Claw - กำหนดค่าด้วยตนเอง", + "Open Claw AI Assistant": "Open Claw AI Assistant", + "Open Claw CLI not detected locally": "ไม่พบ Open Claw CLI บนเครื่อง", + "Open Claw CLI not installed": "ไม่ได้ติดตั้ง Open Claw CLI", + "Open Continue configuration file": "เปิด Continue configuration file", + "Open Dashboard": "เปิด Dashboard", + "Open DevTools (F12) → Application/Storage → Cookies": "เปิด DevTools (F12) → Application/Storage → Cookies", + "Open Settings": "เปิดการตั้งค่า", + "Open platform.iflow.cn in your browser": "เปิด platform.iflow.cn ในเบราว์เซอร์ของคุณ", + "OpenAI / ElevenLabs / Edge / Google / Deepgram voices.": "OpenAI / ElevenLabs / Edge / Google / Deepgram voices", + "OpenAI Codex CLI": "OpenAI Codex CLI", + "OpenAI Compatible (Prod)": "OpenAI Compatible (Production)", + "OpenAI Compatible Details": "รายละเอียด OpenAI Compatible", + "OpenAI Intermediate": "OpenAI Intermediate", + "OpenAI Response": "OpenAI Response", + "OpenCode - Manual Configuration": "OpenCode - กำหนดค่าด้วยตนเอง", + "OpenCode AI Terminal Assistant": "OpenCode AI Terminal Assistant", + "OpenCode CLI not detected locally": "ไม่พบ OpenCode CLI บนเครื่อง", + "OpenCode CLI not installed": "ไม่ได้ติดตั้ง OpenCode CLI", + "OpenRouter": "OpenRouter", + "OpenRouter supports any model. Add models and create aliases for quick access.": "OpenRouter รองรับโมเดลใดก็ได้ เพิ่มโมเดลและสร้าง aliases เพื่อเข้าถึงอย่างรวดเร็ว", + "Optional SSO via Authentik/Keycloak/Google": "SSO ทางเลือกผ่าน Authentik/Keycloak/Google", + "Or paste callback URL manually": "หรือวาง callback URL ด้วยตนเอง", + "Organization": "องค์กร", + "Organization Domain": "Organization Domain", + "Organization ID": "Organization ID", + "Organization Token": "Organization Token", + "Organization Tokens": "Organization Tokens", + "Other": "อื่นๆ", + "Our engine analyzes the prompt and routes through your subscription, cheap, and free provider tiers with automatic fallback.": "engine ของเราจะวิเคราะห์ prompt แล้วส่งต่อผ่าน subscription, cheap และ free provider tiers ของคุณ พร้อม automatic fallback", + "Our engine analyzes the prompt, checks provider health, and routes for lowest latency or cost.": "engine ของเราจะวิเคราะห์ prompt ตรวจสอบ provider health แล้ว route ไปยัง latency ต่ำสุดหรือค่าใช้จ่ายน้อยที่สุด", + "Out": "ขาออก", + "Outbound Proxy": "Outbound Proxy", + "Output": "ขาออก", + "Output Cost": "ค่าใช้จ่ายขาออก", + "Output Format": "Output Format", + "Output Tokens": "Output Tokens", + "Output Tokens:": "Output Tokens:", + "Output:": "ขาออก:", + "Overview": "ภาพรวม", + "Paid": "เสียเงิน", + "Partial preview": "ตัวอย่างบางส่วน", + "Password": "รหัสผ่าน", + "Password + OIDC active": "รหัสผ่าน + OIDC ใช้งานอยู่", + "Password and OIDC login are both active.": "รหัสผ่านและ OIDC login ใช้งานอยู่ทั้งคู่", + "Password and OIDC login are both enabled.": "รหัสผ่านและ OIDC login เปิดใช้งานทั้งคู่", + "Password only": "เฉพาะรหัสผ่าน", + "Password updated successfully": "อัปเดตรหัสผ่านสำเร็จ", + "Passwords do not match": "รหัสผ่านไม่ตรงกัน", + "Paste Proxy List (One per line)": "วาง Proxy List (หนึ่งต่อบรรทัด)", + "Paste a long-lived Kiro/CodeWhisperer API key. It is validated against AWS and stored directly as a bearer credential (no refresh).": "วาง Kiro/CodeWhisperer API key ที่มีอายุการใช้งานยาวนาน จะถูกตรวจสอบกับ AWS แล้วจัดเก็บโดยตรงเป็น bearer credential (ไม่ต้องรีเฟรช)", + "Paste external_idp auth JSON from CLIProxyAPI/Kiro Microsoft login.": "วาง external_idp auth JSON จาก CLIProxyAPI/Kiro Microsoft login", + "Paste it below": "วางด้านล่าง", + "Paste refresh token from Kiro IDE.": "วาง refresh token จาก Kiro IDE", + "Paste the Kiro CLIProxyAPI auth JSON containing auth_method=external_idp. Only Microsoft login token endpoints are accepted.": "วาง Kiro CLIProxyAPI auth JSON ที่มี auth_method=external_idp จะยอมรับเฉพาะ Microsoft login token endpoints", + "Paste the URL from your browser address bar": "วาง URL จาก address bar ของเบราว์เซอร์", + "Paste the command into your terminal and press Enter.": "วางคำสั่งลงใน terminal แล้วกด Enter", + "Paste this to your AI:": "วางสิ่งนี้ให้ AI ของคุณ:", + "Paste your Kiro API key...": "วาง Kiro API key ของคุณ...", + "Pause API Key": "พัก API Key", + "Pause key": "พัก key", + "Paused": "หยุดชั่วคราว", + "Permissions": "สิทธิ์", + "Personal Access Token": "Personal Access Token", + "Pick the model that fuses panel answers": "เลือกโมเดลที่ fuse panel answers", + "Please add an active Qoder connection first": "กรุณาเพิ่ม Qoder connection ที่ใช้งานอยู่ก่อน", + "Please add and connect providers first to configure CLI tools.": "กรุณาเพิ่มและเชื่อมต่อ providers ก่อนเพื่อกำหนดค่า CLI tools", + "Please copy the URL from the address bar and paste it in the application.": "กรุณาคัดลอก URL จาก address bar แล้ววางในแอปพลิเคชัน", + "Please enter a Proxy URL to test": "กรุณาป้อน Proxy URL เพื่อทดสอบ", + "Please install Claude CLI to use this feature.": "กรุณาติดตั้ง Claude CLI เพื่อใช้คุณสมบัตินี้", + "Please install Codex CLI to use auto-apply feature.": "กรุณาติดตั้ง Codex CLI เพื่อใช้คุณสมบัติ auto-apply", + "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 เพื่อใช้คุณสมบัติ auto-apply", + "Please wait while we complete the authorization.": "กรุณารอในขณะที่เราดำเนินการอนุมัติให้เสร็จสิ้น", + "Point your CLI tools to http://localhost:20128": "ชี้ CLI tools ของคุณไปที่ http://localhost:20128", + "Pool:": "Pool:", + "Popup blocked? Enter URL manually": "ป๊อปอัปถูกบล็อก? ป้อน URL ด้วยตนเอง", + "Port 443 Already In Use": "Port 443 ถูกใช้งานอยู่แล้ว", + "Port 443 is currently used by another process:": "Port 443 ถูกใช้งานโดย process อื่นอยู่ในขณะนี้:", + "Powerful Features": "คุณสมบัติที่ทรงพลัง", + "Prefix": "Prefix", + "Preset": "Preset", + "Prev": "ก่อนหน้า", + "Preview": "ตัวอย่าง", + "Previous accounts page": "หน้าบัญชีก่อนหน้า", + "Pricing": "Pricing", + "Pricing Configuration": "การกำหนดค่า Pricing", + "Pricing Format:": "Pricing Format:", + "Pricing Rates Format": "Pricing Rates Format", + "Pricing Settings": "Pricing Settings", + "Priority": "ลำดับความสำคัญ", + "Privacy Policy": "นโยบายความเป็นส่วนตัว", + "Probing server for tools...": "กำลังตรวจสอบเซิร์ฟเวอร์สำหรับเครื่องมือ...", + "Processing...": "กำลังประมวลผล...", + "Product": "ผลิตภัณฑ์", + "Production Key": "Production Key", + "Project Name": "ชื่อโครงการ", + "Prompt": "Prompt", + "Provider": "Provider", + "Provider Details": "รายละเอียด Provider", + "Provider Limits": "Provider Limits", + "Provider Response": "Provider Response", + "Provider not found": "ไม่พบ Provider", + "Provider test failed": "ทดสอบ Provider ล้มเหลว", + "Provider:": "Provider:", + "Providers": "Providers", + "Proxy": "Proxy", + "Proxy Action": "Proxy Action", + "Proxy Pool": "Proxy Pool", + "Proxy Pools": "Proxy Pools", + "Proxy URL": "Proxy URL", + "Proxy disabled": "ปิด Proxy แล้ว", + "Proxy enabled": "เปิด Proxy แล้ว", + "Proxy pool created": "สร้าง Proxy Pool แล้ว", + "Proxy pool deleted": "ลบ Proxy Pool แล้ว", + "Proxy pool updated": "อัปเดต Proxy Pool แล้ว", + "Proxy settings applied": "ใช้ Proxy settings แล้ว", + "Proxy test OK": "ทดสอบ Proxy สำเร็จ", + "Proxy test failed": "ทดสอบ Proxy ล้มเหลว", + "Proxy test passed": "ทดสอบ Proxy ผ่าน", + "Purpose:": "วัตถุประสงค์:", + "Python >= 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "ต้องใช้ Python >= 3.10 สำหรับ local managed mode กรุณาติดตั้ง Python ก่อน หรือใช้ external proxy URL", + "Python ≥ 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "ต้องใช้ Python ≥ 3.10 สำหรับ local managed mode กรุณาติดตั้ง Python ก่อน หรือใช้ external proxy URL", + "Quota Tracker": "Quota Tracker", + "Qwen": "Qwen", + "Qwen Code supports multiple provider types (openai, anthropic, gemini) via modelProviders in settings.json. 9Router works as an OpenAI-compatible endpoint.": "Qwen Code รองรับ provider types หลายประเภท (openai, anthropic, gemini) ผ่าน modelProviders ใน settings.json 9Router ทำงานเป็น OpenAI-compatible endpoint", + "Qwen OAuth free tier was discontinued on 2026-04-15. Use 9Router with alicode/openrouter/anthropic/gemini providers instead.": "Qwen OAuth free tier ถูกยกเลิกเมื่อ 15 เมษายน 2569 ให้ใช้ 9Router กับ alicode/openrouter/anthropic/gemini providers แทน", + "Rate Limited": "ถูกจำกัดอัตรา", + "Read Documentation": "อ่านเอกสาร", + "Reading from AWS SSO cache": "อ่านจาก AWS SSO cache", + "Reading from Cursor IDE database": "อ่านจาก Cursor IDE database", + "Ready": "พร้อม", + "Ready to Simplify Your AI Infrastructure?": "พร้อมที่จะทำให้ AI infrastructure ของคุณง่ายขึ้น?", + "Ready to route! ✓": "พร้อม route แล้ว! ✓", + "Ready! Requests route automatically through your configured providers.": "พร้อมแล้ว! Requests จะ route อัตโนมัติผ่าน providers ที่คุณกำหนดค่าไว้", + "Reasoning": "Reasoning", + "Reasoning:": "Reasoning:", + "Recent Requests": "Requests ล่าสุด", + "Recent chats": "แชทล่าสุด", + "Recheck": "ตรวจสอบอีกครั้ง", + "Recommended for most users. Free AWS account required.": "แนะนำสำหรับผู้ใช้ส่วนใหญ่ ต้องใช้ AWS account ฟรี", + "Record request details for inspection in the logs view": "บันทึก request details สำหรับตรวจสอบใน logs view", + "Redirect URI": "Redirect URI", + "Ref Image (URL)": "Ref Image (URL)", + "Refresh": "รีเฟรช", + "Refresh All": "รีเฟรชทั้งหมด", + "Refresh Token": "Refresh Token", + "Refresh all": "รีเฟรชทั้งหมด", + "Refresh quota": "รีเฟรช quota", + "Region": "Region", + "Reload Page": "โหลดหน้าใหม่", + "Reload VS Code after applying for changes to take effect.": "โหลด VS Code ใหม่หลังจากใช้เพื่อให้การเปลี่ยนแปลงมีผล", + "Remaining": "เหลือ", + "Remote": "ระยะไกล", + "Remove": "นำออก", + "Remove attachment": "ลบไฟล์แนบ", + "Remove custom model": "ลบ custom model", + "Remove model": "ลบโมเดล", + "Replaces built-in WebSearch/WebFetch. Auto-strips duplicates from tool list.": "แทนที่ built-in WebSearch/WebFetch จะลบรายการซ้ำจาก tool list โดยอัตโนมัติ", + "Replay request flow — matches log files": "เล่นซ้ำ request flow — ตรงกับ log files", + "Request": "Request", + "Request Details": "Request Details", + "Request Logs": "Request Logs", + "Requests": "Requests", + "Requests without a valid key will be rejected": "Requests ที่ไม่มี key ที่ถูกต้องจะถูกปฏิเสธ", + "Require API key": "ต้องใช้ API key", + "Require OIDC for dashboard access.": "ต้องใช้ OIDC เพื่อเข้าถึง dashboard", + "Require login": "ต้องเข้าสู่ระบบ", + "Required for SSL certificate and DNS configuration": "ต้องใช้สำหรับ SSL certificate และ DNS configuration", + "Required for SSL certificate and server startup": "ต้องใช้สำหรับ SSL certificate และ server startup", + "Required to modify /etc/hosts and flush DNS cache": "ต้องใช้สำหรับแก้ไข /etc/hosts และ flush DNS cache", + "Required. A friendly label for this node.": "จำเป็น ป้ายที่อ่านง่ายสำหรับ node นี้", + "Required. Used as the provider prefix for model IDs.": "จำเป็น ใช้เป็น provider prefix สำหรับ model IDs", + "Requires \"Workers Scripts: Edit\" permission.": "ต้องใช้สิทธิ์ \"Workers Scripts: Edit\"", + "Requires Cloudflare Account ID and a Workers API Token (Edit Workers permission)": "ต้องใช้ Cloudflare Account ID และ Workers API Token (สิทธิ์ Edit Workers)", + "Requires Cursor Pro account to use this feature.": "ต้องใช้ Cursor Pro account เพื่อใช้คุณสมบัตินี้", + "Requires jcode installed. Install via: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash": "ต้องติดตั้ง jcode ติดตั้งผ่าน: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash", + "Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.": "ต้องใช้ outbound port 7844 (TCP/UDP) การเชื่อมต่ออาจใช้เวลา 10-30 วินาที", + "Reset": "รีเซ็ต", + "Reset Codex limit?": "รีเซ็ต Codex limit?", + "Reset Password to Default": "รีเซ็ตรหัสผ่านเป็นค่าเริ่มต้น", + "Reset judge to Auto": "รีเซ็ต judge เป็น Auto", + "Reset time": "เวลาที่รีเซ็ต", + "Reset to Defaults": "รีเซ็ตเป็นค่าเริ่มต้น", + "Reset to default": "รีเซ็ตเป็นค่าเริ่มต้น", + "Resources": "ทรัพยากร", + "Response": "Response", + "Response Format": "Response Format", + "Responses": "Responses", + "Responses API": "Responses API", + "Restart": "เริ่มต้นใหม่", + "Restore model": "กู้คืนโมเดล", + "Resume key": "Resume key", + "Retry": "ลองใหม่", + "Risk Notice": "ประกาศความเสี่ยง", + "Roo AI Assistant": "Roo AI Assistant", + "Rotate providers across requests instead of strict fallback order.": "หมุนเวียน providers ผ่าน requests แทนที่จะใช้ fallback order อย่างเคร่งครัด", + "Round Robin": "Round Robin", + "Round Robin — rotate": "Round Robin — หมุนเวียน", + "Round Robin — rotates models across requests to spread load": "Round Robin — หมุนเวียนโมเดลผ่าน requests เพื่อกระจายโหลด", + "Route AI requests through subscription, cheap, and free tiers with auto-fallback. One endpoint for Claude, GPT, Gemini, and more.": "Route AI requests ผ่าน subscription, cheap และ free tiers พร้อม auto-fallback One endpoint สำหรับ Claude, GPT, Gemini และอื่นๆ", + "Route Requests": "Route Requests", + "Routing Strategy": "Routing Strategy", + "Rows:": "Rows:", + "Run": "รัน", + "Run npx command to start the server instantly": "รันคำสั่ง npx เพื่อเริ่มต้นเซิร์ฟเวอร์ทันที", + "Run this command in your terminal, then click": "รันคำสั่งนี้ใน terminal ของคุณ แล้วคลิก", + "Running": "กำลังทำงาน", + "Running on your machine": "ทำงานบนเครื่องของคุณ", + "Runtime": "Runtime", + "SSE URL": "SSE URL", + "START HERE": "เริ่มที่นี่", + "Save": "บันทึก", + "Save Changes": "บันทึกการเปลี่ยนแปลง", + "Save Config": "บันทึก Config", + "Save Mappings": "บันทึก Mappings", + "Save auth mode": "บันทึกโหมดยืนยันตัวตน", + "Save current Base URL and API key as a browser-local preset": "บันทึก Base URL และ API key ปัจจุบันเป็น preset ที่เก็บในเบราว์เซอร์", + "Save this key now!": "บันทึก key นี้ตอนนี้!", + "Saved": "บันทึกแล้ว", + "Saving": "กำลังบันทึก", + "Saving...": "กำลังบันทึก...", + "Scan QR to connect instantly": "สแกน QR เพื่อเชื่อมต่อทันที", + "Scopes": "Scopes", + "Screen sharing": "แชร์หน้าจอ", + "Scroll down to": "เลื่อนลงมาที่", + "Search by name or description...": "ค้นหาตามชื่อหรือคำอธิบาย...", + "Search language...": "ค้นหาภาษา...", + "Search model id": "ค้นหา model id", + "Search providers...": "ค้นหา providers...", + "Search...": "ค้นหา...", + "Security": "ความปลอดภัย", + "Security required: ": "ต้องใช้ความปลอดภัย:", + "Security risk: no password set. You will be asked to set one when logging in remotely.": "ความเสี่ยงด้านความปลอดภัย: ไม่ได้ตั้งรหัสผ่าน คุณจะถูกถามให้ตั้งรหัสผ่านเมื่อเข้าสู่ระบบจากที่ไกล", + "Select": "เลือก", + "Select All": "เลือกทั้งหมด", + "Select Cowork Model": "เลือก Cowork Model", + "Select Endpoint": "เลือก Endpoint", + "Select Judge Model": "เลือก Judge Model", + "Select Language": "เลือกภาษา", + "Select Model": "เลือกโมเดล", + "Select Model for Cline": "เลือกโมเดลสำหรับ Cline", + "Select Model for Codex": "เลือกโมเดลสำหรับ Codex", + "Select Model for DeepSeek TUI": "เลือกโมเดลสำหรับ DeepSeek TUI", + "Select Model for Factory Droid": "เลือกโมเดลสำหรับ Factory Droid", + "Select Model for GitHub Copilot": "เลือกโมเดลสำหรับ GitHub Copilot", + "Select Model for Hermes Agent": "เลือกโมเดลสำหรับ Hermes Agent", + "Select Model for Kilo Code": "เลือกโมเดลสำหรับ Kilo Code", + "Select Model for Open Claw": "เลือกโมเดลสำหรับ Open Claw", + "Select Model for OpenCode": "เลือกโมเดลสำหรับ OpenCode", + "Select Model for jcode": "เลือกโมเดลสำหรับ jcode", + "Select Provider": "เลือก Provider", + "Select Subagent Model for Codex": "เลือก Subagent Model สำหรับ Codex", + "Select Subagent Model for OpenCode": "เลือก Subagent Model สำหรับ OpenCode", + "Select a provider": "เลือก provider", + "Select all": "เลือกทั้งหมด", + "Select language": "เลือกภาษา", + "Select models to add": "เลือกโมเดลที่ต้องการเพิ่ม", + "Select one or more connections, then click Proxy Action.": "เลือกการเชื่อมต่อหนึ่งรายการขึ้นไป แล้วคลิก Proxy Action", + "Select to pre-fill, then edit model ID in the input": "เลือกเพื่อเติมล่วงหน้า แล้วแก้ไข model ID ในช่องป้อน", + "Select your": "เลือก", + "Selected connections have mixed proxy bindings": "การเชื่อมต่อที่เลือกมี proxy bindings ที่แตกต่างกัน", + "Selected only": "เฉพาะที่เลือก", + "Selected provider": "Provider ที่เลือก", + "Selecting None will unbind selected connections from proxy pool.": "เลือก None จะ unbind selected connections จาก proxy pool", + "Send": "ส่ง", + "Send to Provider": "ส่งไปยัง Provider", + "Sent to provider as:": "ส่งไปยัง Provider ในรูปแบบ:", + "Server": "เซิร์ฟเวอร์", + "Server Disconnected": "เซิร์ฟเวอร์ตัดการเชื่อมต่อ", + "Server off": "ปิดเซิร์ฟเวอร์", + "Server running on": "เซิร์ฟเวอร์ทำงานบน", + "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "บริการกำลังทำงานใน terminal คุณสามารถปิดหน้าเว็บนี้ได้ การปิดระบบจะหยุดบริการ", + "Set Password": "ตั้งรหัสผ่าน", + "Set a new password before accessing the dashboard remotely.": "ตั้งรหัสผ่านใหม่ก่อนเข้าถึง dashboard จากที่ไกล", + "Set password": "ตั้งรหัสผ่าน", + "Setting password for the first time. Leave current password empty or use default:": "ตั้งรหัสผ่านครั้งแรก ปล่อยรหัสผ่านปัจจุบันว่างหรือใช้ค่าเริ่มต้น:", + "Setting up": "กำลังตั้งค่า", + "Settings": "การตั้งค่า", + "Settings applied successfully!": "ใช้การตั้งค่าสำเร็จ!", + "Settings reset successfully!": "รีเซ็ตการตั้งค่าสำเร็จ!", + "Setup": "ตั้งค่า", + "Setup + index of all capabilities. Start here — covers base URL, auth, model discovery, and links to every capability skill.": "การตั้งค่า + index ของทุกคุณสมบัติ เริ่มที่นี่ — ครอบคลุม base URL, auth, model discovery และลิงก์ไปยังทุกคุณสมบัติ", + "Share Endpoint": "แชร์ Endpoint", + "Share URL with team members": "แชร์ URL กับสมาชิกทีม", + "Show": "แสดง", + "Show all": "แสดงทั้งหมด", + "Show key": "แสดง key", + "Show only selected models": "แสดงเฉพาะโมเดลที่เลือก", + "Showing": "กำลังแสดง", + "Shutdown": "ปิดระบบ", + "Sign in with OIDC": "เข้าสู่ระบบด้วย OIDC", + "Simple chat interface to interact with any AI model from connected providers. Select a model and start chatting!": "แชท interface ง่ายๆ สำหรับโต้ตอบกับ AI models จาก connected providers เลือกโมเดลแล้วเริ่มแชท!", + "Single": "เดี่ยว", + "Single API endpoint for all major AI providers. Simplify your integration.": "API endpoint เดียวสำหรับ AI providers ทั้งหมด ทำให้ integration ของคุณง่ายขึ้น", + "Some models are not responding": "โมเดลบางตัวไม่ตอบสนอง", + "Sort Codex quotas by remaining": "เรียง Codex quotas ตามจำนวนที่เหลือ", + "Sort accounts by earliest quota reset time": "เรียงบัญชีตามเวลา quota reset เร็วที่สุด", + "Source Body": "Source Body", + "Sourcegraph Amp coding assistant CLI": "Sourcegraph Amp coding assistant CLI", + "Special reasoning/thinking tokens (fallback to output rate)": "Special reasoning/thinking tokens (fallback ไปยัง output rate)", + "Speech To Text": "Speech To Text", + "Speech-to-Text": "Speech-to-Text", + "Standard prompt tokens": "Standard prompt tokens", + "Start DNS": "เริ่ม DNS", + "Start Date": "วันที่เริ่มต้น", + "Start Free": "เริ่มฟรี", + "Start Headroom": "เริ่ม Headroom", + "Start Headroom separately at the configured URL, then recheck.": "เริ่ม Headroom แยกต่างหากที่ URL ที่กำหนด แล้วตรวจสอบอีกครั้ง", + "Start MITM": "เริ่ม MITM", + "Start Server": "เริ่มเซิร์ฟเวอร์", + "Start Tunnel": "เริ่ม Tunnel", + "Start a conversation": "เริ่มการสนทนา", + "Starting 9Router...": "กำลังเริ่ม 9Router...", + "Status": "สถานะ", + "Status:": "สถานะ:", + "Step 1: Open this URL in your browser": "ขั้นตอนที่ 1: เปิด URL นี้ในเบราว์เซอร์ของคุณ", + "Step 2: Paste the callback URL here": "ขั้นตอนที่ 2: วาง callback URL ที่นี่", + "Sticky Limit": "Sticky Limit", + "Sticky:": "Sticky:", + "Stop": "หยุด", + "Stop DNS": "หยุด DNS", + "Stop Headroom": "หยุด Headroom", + "Stop MITM": "หยุด MITM", + "Stop Server": "หยุดเซิร์ฟเวอร์", + "Stopped": "หยุดแล้ว", + "Strict Proxy": "Strict Proxy", + "Subagent Model": "Subagent Model", + "Sudo Password Required": "ต้องใช้ Sudo Password", + "Sudo password is required": "ต้องใช้ Sudo password", + "Suggested free models (≥200k context):": "โมเดลฟรีที่แนะนำ (≥200k context):", + "Suggested shorthand examples: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929.": "ตัวอย่าง shorthand ที่แนะนำ: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929", + "Support up to 20 active apps & 50 custom domains": "รองรับสูงสุด 20 active apps & 50 custom domains", + "Supported formats: protocol://user:pass@host:port, host:port:user:pass": "Supported formats: protocol://user:pass@host:port, host:port:user:pass", + "Sync settings across devices with optional cloud storage.": "ซิงค์การตั้งค่าผ่านอุปกรณ์ต่างๆ ด้วย cloud storage ทางเลือก", + "System": "ระบบ", + "TTFT:": "TTFT:", + "Tailscale": "Tailscale", + "Tailscale Funnel": "Tailscale Funnel", + "Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working.": "Tailscale Funnel จะหยุด การเข้าถึงจากที่ไกลผ่าน Tailscale URL จะหยุดทำงาน", + "Tailscale installed": "ติดตั้ง Tailscale แล้ว", + "Tailscale is not installed. Install it to enable Funnel.": "ไม่ได้ติดตั้ง Tailscale ติดตั้งเพื่อเปิดใช้งาน Funnel", + "Target Request": "Target Request", + "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com.": "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com", + "Temperature": "Temperature", + "Terminal": "Terminal", + "Terms of Service": "ข้อกำหนดการให้บริการ", + "Terse-style system prompt → ~65% fewer output tokens (up to 87%)": "Terse-style system prompt → ลด output tokens ประมาณ 65% (สูงสุด 87%)", + "Test": "ทดสอบ", + "Test Again": "ทดสอบอีกครั้ง", + "Test All": "ทดสอบทั้งหมด", + "Test Example": "ตัวอย่างทดสอบ", + "Test Results": "ผลการทดสอบ", + "Test all API Key connections": "ทดสอบ API Key connections ทั้งหมด", + "Test all Compatible connections": "ทดสอบ Compatible connections ทั้งหมด", + "Test all Free connections": "ทดสอบ Free connections ทั้งหมด", + "Test all Free provider connections": "ทดสอบ Free provider connections ทั้งหมด", + "Test all OAuth connections": "ทดสอบ OAuth connections ทั้งหมด", + "Test connection": "ทดสอบการเชื่อมต่อ", + "Test model": "ทดสอบโมเดล", + "Test proxy": "ทดสอบ proxy", + "Test proxy URL": "ทดสอบ proxy URL", + "Testing...": "กำลังทดสอบ...", + "Text To Speech": "Text To Speech", + "Text To Speech combo": "Text To Speech combo", + "Text to Image": "Text to Image", + "Text to Image combo": "Text to Image combo", + "Text-to-Speech": "Text-to-Speech", + "Text-to-image via DALL-E, Imagen, FLUX, MiniMax, SDWebUI…": "Text-to-image ผ่าน DALL-E, Imagen, FLUX, MiniMax, SDWebUI...", + "The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working.": "Cloudflare tunnel จะถูกตัดการเชื่อมต่อ การเข้าถึงจากที่ไกลผ่าน tunnel URL จะหยุดทำงาน", + "The proxy server has been stopped.": "Proxy server ถูกหยุดแล้ว", + "The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly.": "Request ได้รับการตอบสนองจาก OpenAI, Anthropic, Gemini หรือผู้ให้บริการอื่นทันที", + "The tunnel will be disconnected. Remote access will stop working.": "Tunnel จะถูกตัดการเชื่อมต่อ การเข้าถึงจากที่ไกลจะหยุดทำงาน", + "The unified endpoint for AI generation. Connect, route, and manage your AI providers with ease.": "Endpoint เดียวสำหรับ AI generation เชื่อมต่อ route และจัดการ AI providers ของคุณอย่างง่ายดาย", + "The unified interface for modern AI infrastructure": "Interface เดียวสำหรับ AI infrastructure สมัยใหม่", + "The unified interface for modern AI infrastructure. Secure, observable, and scalable.": "Interface เดียวสำหรับ AI infrastructure สมัยใหม่ ปลอดภัย ตรวจสอบได้ และขยายขนาดได้", + "Theme": "ธีม", + "Thinking": "Thinking", + "Thinking Process": "Thinking Process", + "This is the only time you will see this key. Store it securely.": "นี่เป็นครั้งเดียวที่คุณจะเห็น key นี้ กรุณาจัดเก็บอย่างปลอดภัย", + "This provider is ready to use.": "Provider นี้พร้อมใช้งาน", + "This provider is ready to use. Optionally route requests through a proxy pool to bypass IP-based limits.": "Provider นี้พร้อมใช้งาน ทางเลือกสามารถ route requests ผ่าน proxy pool เพื่อหลีกเลี่ยง IP-based limits", + "This value is write-only after saving.": "ค่านี้จะเขียนได้เฉพาะหลังจากบันทึกแล้ว", + "Timestamp": "Timestamp", + "Timestamp:": "Timestamp:", + "To get a fresh API key, paste your browser cookie from": "เพื่อรับ API key ใหม่ วาง browser cookie ของคุณจาก", + "Today": "วันนี้", + "Toggle DNS to redirect": "เปิด/ปิด DNS เพื่อ redirect", + "Toggle auto-ping": "เปิด/ปิด auto-ping", + "Token Saver": "Token Saver", + "Token Types:": "Token Types:", + "Token auto-detected from Kiro IDE successfully!": "ตรวจจับ Token จาก Kiro IDE สำเร็จ!", + "Token is used once for deployment and not stored.": "Token ใช้ครั้งเดียวสำหรับ deployment ไม่ได้จัดเก็บ", + "Token is used once for deployment, not stored. Found in Organization Settings.": "Token ใช้ครั้งเดียวสำหรับ deployment ไม่ได้จัดเก็บ พบใน Organization Settings", + "Token will be auto-filled...": "Token จะถูกเติมอัตโนมัติ...", + "Tokens": "Tokens", + "Tokens auto-detected from Cursor IDE successfully!": "ตรวจจับ Tokens จาก Cursor IDE สำเร็จ!", + "Tokens used to create cache entries (fallback to input rate)": "Tokens ที่ใช้สร้าง cache entries (fallback ไปยัง input rate)", + "Tomorrow": "พรุ่งนี้", + "Tool not found or disabled.": "ไม่พบเครื่องมือหรือถูกปิดใช้งาน", + "Tools": "เครื่องมือ", + "Tools:": "เครื่องมือ:", + "Total Cost": "Total Cost", + "Total Input Tokens": "Input Tokens ทั้งหมด", + "Total Models": "โมเดลทั้งหมด", + "Total Requests": "Requests ทั้งหมด", + "Total Tokens": "Tokens ทั้งหมด", + "Total:": "ทั้งหมด:", + "Track and manage your API quota limits": "ติดตามและจัดการ API quota limits ของคุณ", + "Track token usage, costs, and performance across all providers.": "ติดตาม token usage, costs และ performance ของ providers ทั้งหมด", + "Transcribe audio via OpenAI Whisper, Groq, Gemini, Deepgram, AssemblyAI…": "Transcribe audio ผ่าน OpenAI Whisper, Groq, Gemini, Deepgram, AssemblyAI...", + "Transferring data...": "กำลังถ่ายโอนข้อมูล...", + "Translator": "Translator", + "Translator Debug": "Translator Debug", + "Tried in order (top-down) or rotated when round-robin is on.": "ลองตามลำดับ (บนลงล่าง) หรือหมุนเวียนเมื่อ round-robin เปิดอยู่", + "Trust Cert": "Trust Cert", + "Trusted": "เชื่อถือแล้ว", + "Try Again": "ลองอีกครั้ง", + "Tunnel": "Tunnel", + "Tunnel connected!": "Tunnel เชื่อมต่อแล้ว!", + "Tunnel disabled": "ปิด Tunnel แล้ว", + "Turn off Empty": "ปิด Empty", + "Turn on Available": "เปิด Available", + "Turn request detail recording on/off globally": "เปิด/ปิดการบันทึก request details ทั่วโลก", + "Twitter": "Twitter", + "URL → markdown / text / HTML via Firecrawl, Jina, Tavily, Exa.": "URL → markdown / text / HTML ผ่าน Firecrawl, Jina, Tavily, Exa", + "Unavailable": "ไม่พร้อมใช้งาน", + "Under": "ภายใต้", + "Unified Endpoint": "Unified Endpoint", + "Unknown": "ไม่ทราบ", + "Unselect all": "ยกเลิกเลือกทั้งหมด", + "Update": "อัปเดต", + "Update 9Router": "อัปเดต 9Router", + "Update Password": "อัปเดตรหัสผ่าน", + "Update now": "อัปเดตตอนนี้", + "Upstream Auth Error": "Upstream Auth Error", + "Upstream Unavailable": "Upstream ไม่พร้อมใช้งาน", + "Usage": "การใช้งาน", + "Usage & Analytics": "การใช้งานและ Analytics", + "Usage / Limit": "การใช้งาน / ขีดจำกัด", + "Usage Logs": "Usage Logs", + "Usage Tracking": "Usage Tracking", + "Usage by API Key": "การใช้งานตาม API Key", + "Usage by Account": "การใช้งานตามบัญชี", + "Usage by Endpoint": "การใช้งานตาม Endpoint", + "Usage by Model": "การใช้งานตามโมเดล", + "Usage:": "การใช้งาน:", + "Use 9Router model aliases to keep Amp shorthand mappings stable across provider updates.": "ใช้ 9Router model aliases เพื่อรักษา Amp shorthand mappings ให้คงที่ตลอดการอัปเดต providers", + "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "ใช้ Antigravity IDE & GitHub Copilot → กับ providers/models ใดก็ได้จาก 9Router", + "Use Authentik or any OIDC provider to sign in to the dashboard.": "ใช้ Authentik หรือ OIDC providers ใดก็ได้เพื่อเข้าสู่ระบบ dashboard", + "Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys.": "ใช้ Authentik หรือ OIDC providers ใดก็ได้เพื่อเข้าสู่ระบบ dashboard คุณสามารถเปิดใช้งานเฉพาะรหัสผ่าน เฉพาะ OIDC หรือทั้งคู่สำหรับ dashboard; model API access ยังคงใช้ API keys", + "Use a GitLab OAuth application": "ใช้ GitLab OAuth application", + "Use a GitLab PAT with api scope": "ใช้ GitLab PAT ที่มี api scope", + "Use a direct xAI API key from console.x.ai. This is separate from Grok Build OAuth.": "ใช้ xAI API key โดยตรงจาก console.x.ai นี้แยกจาก Grok Build OAuth", + "Use a local proxy for Start/Stop, or an external Docker sidecar like http://headroom:8787.": "ใช้ local proxy สำหรับ Start/Stop หรือ external Docker sidecar เช่น http://headroom:8787", + "Use a long-lived Kiro/CodeWhisperer API key (headless auth).": "ใช้ Kiro/CodeWhisperer API key ที่มีอายุการใช้งานยาวนาน (headless auth)", + "Use in Cursor/Cline": "ใช้ใน Cursor/Cline", + "Use the buttons above to add OpenAI or Anthropic compatible endpoints": "ใช้ปุ่มด้านบนเพื่อเพิ่ม OpenAI หรือ Anthropic compatible endpoints", + "Use your API from any network": "ใช้ API ของคุณจากเครือข่ายใดก็ได้", + "Valid": "ถูกต้อง", + "Vectors for RAG / semantic search via OpenAI, Gemini, Mistral…": "Vectors สำหรับ RAG / semantic search ผ่าน OpenAI, Gemini, Mistral...", + "Vercel API Token": "Vercel API Token", + "Vercel Relay": "Vercel Relay", + "Vercel serves millions of apps — providers can't block Vercel IPs without affecting legitimate traffic": "Vercel ให้บริการ millions of apps — providers ไม่สามารถบล็อก Vercel IPs โดยไม่กระทบ traffic ที่ถูกกฎหมาย", + "Verification URL": "Verification URL", + "Video": "วิดีโอ", + "View Codex reset credit expiry": "ดู Codex reset credit expiry", + "View Full Details": "ดูรายละเอียดทั้งหมด", + "View on GitHub": "ดูบน GitHub", + "Visit the URL below and enter the code:": "เยี่ยมชม URL ด้านล่างแล้วป้อนรหัส:", + "Visit the login URL below and authorize:": "เยี่ยมชม login URL ด้านล่างแล้วอนุมัติ:", + "Voice": "เสียง", + "Voice ID": "Voice ID", + "Voyage AI": "Voyage AI", + "Waiting for Authorization": "รอการอนุมัติ", + "Waiting for authorization...": "รอการอนุมัติ...", + "Warning": "คำเตือน", + "Web Fetch": "Web Fetch", + "Web Fetch & Search": "Web Fetch & Search", + "Web Search": "Web Search", + "Web Search & Fetch (Exa)": "Web Search & Fetch (Exa)", + "Welcome": "ยินดีต้อนรับ", + "What is Cloudflare Relay?": "Cloudflare Relay คืออะไร?", + "What is Deno Relay?": "Deno Relay คืออะไร?", + "What is Vercel Relay?": "Vercel Relay คืออะไร?", + "When": "เมื่อ", + "When ON, dashboard requires password. When OFF, access without login.": "เมื่อเปิด dashboard ต้องใช้รหัสผ่าน เมื่อปิด เข้าถึงได้โดยไม่ต้องเข้าสู่ระบบ", + "Windows:": "Windows:", + "Windows: Run 9Router terminal as Administrator": "Windows: รัน 9Router terminal ในฐานะผู้ดูแลระบบ", + "Windows: Run terminal (9Router) as Administrator to enable MITM": "Windows: รัน terminal (9Router) ในฐานะผู้ดูแลระบบเพื่อเปิดใช้งาน MITM", + "Worker Name": "Worker Name", + "Works on any device": "ใช้งานได้บนทุกอุปกรณ์", + "Writes to": "เขียนไปที่", + "You can override default pricing for specific models. Reset to defaults anytime to restore standard rates.": "คุณสามารถแทนที่ default pricing สำหรับโมเดลเฉพาะได้ รีเซ็ตเป็นค่าเริ่มต้นเมื่อใดก็ได้เพื่อกลับไปใช้อัตราปกติ", + "Your": "ของคุณ", + "Your Account Name": "ชื่อบัญชีของคุณ", + "Your Code": "โค้ดของคุณ", + "Your Kiro account via": "Kiro account ของคุณผ่าน", + "Your OAuth application client ID": "OAuth application client ID ของคุณ", + "Your organization's AWS IAM Identity Center URL": "AWS IAM Identity Center URL ขององค์กรคุณ", + "Your requests start from your favorite tools or our unified SDK. Just change the base URL.": "Request ของคุณเริ่มจากเครื่องมือที่คุณชื่นชอบหรือ unified SDK ของเรา เพียงเปลี่ยน base URL", + "Your requests start from your favorite tools — Cursor, Claude, Copilot, or any OpenAI-compatible SDK.": "Request ของคุณเริ่มจากเครื่องมือที่คุณชื่นชอบ — Cursor, Claude, Copilot หรือ OpenAI-compatible SDK ใดก็ได้", + "account has been connected.": "บัญชีเชื่อมต่อแล้ว", + "active": "ใช้งานอยู่", + "add OpenAI/Anthropic compatible endpoints": "เพิ่ม OpenAI/Anthropic compatible endpoints", + "added)": "เพิ่มแล้ว)", + "again after install.": "อีกครั้งหลังการติดตั้ง", + "and click": "แล้วคลิก", + "apiKey": "apiKey", + "below.": "ด้านล่าง", + "bound": "เชื่อมต่อแล้ว", + "chars)": "ตัวอักษร)", + "cloudflare relay": "cloudflare relay", + "connection": "การเชื่อมต่อ", + "connections": "การเชื่อมต่อ", + "daily-cloudcode-pa.googleapis.com": "daily-cloudcode-pa.googleapis.com", + "dark": "มืด", + "disabled": "ปิดใช้งานแล้ว", + "dollars per million tokens": "ดอลลาร์ต่อล้าน tokens", + "e.g. CwhRBWXzGAHq8TQ4Fs17": "เช่น CwhRBWXzGAHq8TQ4Fs17", + "e.g. claude-opus-4-5": "เช่น claude-opus-4-5", + "e.g. my-model-id": "เช่น my-model-id", + "e.g. tts-1-hd": "เช่น tts-1-hd", + "e.g. voyage-3, embed-english-v3.0, text-embedding-3-small": "เช่น voyage-3, embed-english-v3.0, text-embedding-3-small", + "e.g., Production API, Dev Environment": "เช่น Production API, Dev Environment", + "every request bills all panel models + the judge": "ทุก request จะคิดค่าใช้จ่าย panel models ทั้งหมด + judge", + "export": "ส่งออก", + "failed": "ล้มเหลว", + "git/grep/ls/tree/logs → 60-90% fewer input tokens": "git/grep/ls/tree/logs → ลด input tokens 60-90%", + "h ago": "ชั่วโมงที่แล้ว", + "has been connected.": "เชื่อมต่อแล้ว", + "iFlow AI": "iFlow AI", + "iFlow Cookie Authentication": "iFlow Cookie Authentication", + "import": "นำเข้า", + "inactive": "ไม่ใช้งาน", + "jcode - Manual Configuration": "jcode - กำหนดค่าด้วยตนเอง", + "jcode CLI not detected locally": "ไม่พบ jcode CLI บนเครื่อง", + "jcode is a Rust-based coding agent with semantic memory, multi-agent swarms, and extreme performance (27.8 MB RAM, 14ms boot).": "jcode เป็น coding agent ที่สร้างด้วย Rust พร้อม semantic memory, multi-agent swarms และ extreme performance (27.8 MB RAM, 14ms boot)", + "kiro://kiro.kiroAgent/authenticate-success?code=...": "kiro://kiro.kiroAgent/authenticate-success?code=...", + "light": "สว่าง", + "m ago": "นาทีที่แล้ว", + "macOS / Linux / Windows:": "macOS / Linux / Windows:", + "macOS / Linux:": "macOS / Linux:", + "macOS/Linux:": "macOS/Linux:", + "more": "เพิ่มเติม", + "more providers": "providers เพิ่มเติม", + "ms / Total": "ms / ทั้งหมด", + "name|apiKey": "name|apiKey", + "no_proxy:": "ไม่มี proxy:", + "not detected locally": "ไม่พบบนเครื่อง", + "npm install -g 9router": "npm install -g 9router", + "npx 9router": "npx 9router", + "open http://localhost:9099": "open http://localhost:9099", + "openid profile email": "openid profile email", + "optional context to improve accuracy": "optional context เพื่อเพิ่มความแม่นยำ", + "or VS Code extension marketplace.": "หรือ VS Code extension marketplace", + "or just": "หรือเพียง", + "passed": "ผ่าน", + "platform.iflow.cn": "platform.iflow.cn", + "queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "query โมเดลทั้งหมดแบบ parallel แล้ว judge สร้างคำตอบเดียว คุณภาพดีที่สุด แต่มีค่าใช้จ่ายมากที่สุด: ทุก request จะคิดค่าใช้จ่าย panel models ทั้งหมด + judge (N+1 calls)", + "records, batches every": "records, ทุก batch", + "requests, max": "requests, สูงสุด", + "rotates models across requests to spread load": "หมุนเวียนโมเดลผ่าน requests เพื่อกระจายโหลด", + "s)": "วินาที)", + "s...": "วินาที...", + "seconds...": "วินาที...", + "sends image/PDF/audio requests to a model that supports them first": "ส่ง image/PDF/audio requests ไปยังโมเดลที่รองรับก่อน", + "sk-...": "sk-...", + "sk_9router (default)": "sk_9router (ค่าเริ่มต้น)", + "system": "ตามระบบ", + "tested": "ทดสอบแล้ว", + "the database.": "ฐานข้อมูล", + "to apply changes": "เพื่อให้การเปลี่ยนแปลงมีผล", + "to verify.": "เพื่อตรวจสอบ", + "traffic through 9Router via MITM.": "traffic ผ่าน 9Router ผ่าน MITM", + "tries models in order (next on failure)": "ลองโมเดลตามลำดับ (ถัดไปเมื่อล้มเหลว)", + "unknown": "ไม่ทราบ", + "v1.0 is now live": "v1.0 พร้อมใช้งานแล้ว", + "vercel relay": "vercel relay", + "yet.": "ในตอนนี้", + "your-org.deno.net": "your-org.deno.net", + "© 2025 9Router. All rights reserved.": "© 2025 9Router สงวนลิขสิทธิ์", + "— queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "— query โมเดลทั้งหมดแบบ parallel แล้ว judge สร้างคำตอบเดียว คุณภาพดีที่สุด แต่มีค่าใช้จ่ายมากที่สุด: ทุก request จะคิดค่าใช้จ่าย panel models ทั้งหมด + judge (N+1 calls)", + "— rotates models across requests to spread load": "— หมุนเวียนโมเดลผ่าน requests เพื่อกระจายโหลด", + "— sends image/PDF/audio requests to a model that supports them first": "— ส่ง image/PDF/audio requests ไปยังโมเดลที่รองรับก่อน", + "— tries models in order (next on failure)": "— ลองโมเดลตามลำดับ (ถัดไปเมื่อล้มเหลว)", + "→ OpenAI": "→ OpenAI", + "→ Target": "→ Target", + "→ localhost": "→ localhost", + "⚠️ Enable DNS to edit model mappings": "⚠️ เปิดใช้งาน DNS เพื่อแก้ไข model mappings", + "⚠️ Local plugins run as subprocess via": "⚠️ Local plugins ทำงานในฐานะ subprocess ผ่าน", + "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM ดักจับ HTTPS traffic ของ IDE tools (Antigravity, GitHub Copilot, Kiro) ผ่าน local CA เพื่อ redirect requests ไปยัง providers ของคุณ อาจละเมิด ToS → บัญชีถูกแบน ใช้ด้วยความเสี่ยงของคุณเอง", + "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ ประกาศความเสี่ยง: Provider นี้ใช้ subscription/OAuth session ที่ไม่ได้รับอนุญาตอย่างเป็นทางการสำหรับการใช้งาน proxy/router บัญชีอาจถูกจำกัดหรือแบน ใช้ด้วยความเสี่ยงของคุณเอง", + "✓ Confirm Add": "✓ ยืนยันการเพิ่ม", + "📝 Configure providers in dashboard or use environment variables": "📝 กำหนดค่า providers ใน dashboard หรือใช้ environment variables", + "🔐 OAuth required. Add now and authenticate after Apply; tool list will be discovered after first connect.": "🔐 ต้องใช้ OAuth เพิ่มตอนนี้แล้ว authenticate หลัง Apply; รายการเครื่องมือจะถูกค้นพบหลังการเชื่อมต่อครั้งแรก" +} \ No newline at end of file diff --git a/public/i18n/literals/zh-CN.json b/public/i18n/literals/zh-CN.json index 41d29c5e..8459ae22 100644 --- a/public/i18n/literals/zh-CN.json +++ b/public/i18n/literals/zh-CN.json @@ -1,46 +1,36 @@ { - "-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 万 Token)。示例:输入费率 2.50 表示每 1,000,000 个输入 Token 需 2.50 美元。", "($/1M tokens). Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens.": "($/100 万 Token)。示例:输入费率 2.50 表示每 1,000,000 个输入 Token 需 2.50 美元。", + "(Caveman)": "(Caveman)", + "(Headroom)": "(Headroom)", + "(Ponytail)": "(Ponytail)", + "(RTK)": "(RTK)", + "(via inference test)": "(通过推理测试)", + "+ Browse": "+ 浏览", + "+ Combo": "+ 组合", + "+ Custom": "+ 自定义", + "+ Save current as...": "+ 另存为...", + "-compatible models manually or import them from the /models endpoint.": "- 手动兼容模型或从 /models 端点导入它们。", + ". Click \"Apply\" to auto-configure.": "。单击“应用”进行自动配置。", + "1. CLI & SDKs": "1. CLI 和 SDK", "1. Client Request (Input)": "1. 客户端请求(输入)", "1. Generates SSL cert & adds to system keychain": "1. 生成 SSL 证书并添加到系统钥匙串", + "2. 9Router Hub": "2. 9Router 枢纽", "2. Provider Request (Translated)": "2. 提供商请求(已​​翻译)", "2. Redirects": "2. 重定向", "24h": "24小时", + "3. AI Providers": "3. AI 提供商", "3. Maps Antigravity models to any provider via 9Router": "3. 通过 9Router 将Antigravity模型映射到任何提供商", "3. Provider Response (Raw)": "3. 提供商响应(原始)", + "30D": "30 天", "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 兼容(生产)", + "60D": "60 天", + "7D": "7 天", + "9Router (Entry)": "9Router(入口)", + "9Router Base URL": "9Router 基础 URL", + ": Account | Workers Scripts | Edit": ":账号 | Workers 脚本 | 编辑", + ": Include | Account |": ":包含 | 账号 |", + "AI endpoint proxy with web dashboard - A JavaScript port of CLIProxyAPI. Works seamlessly with Claude Code, OpenAI Codex, Cline, RooCode, and other CLI tools.": "AI 端点代理,附带 Web 仪表盘 — CLIProxyAPI 的 JavaScript 移植版。与 Claude Code、OpenAI Codex、Cline、RooCode 和其他 CLI 工具无缝协作。", "API Endpoint": "API端点", "API Key": "API密钥", "API Key (for Check)": "API 密钥(用于检查)", @@ -50,286 +40,824 @@ "API Key Providers": "API 密钥提供商", "API Keys": "API 密钥", "API Reference": "API参考", + "API Token": "API 令牌", + "API Tokens": "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": "可用", + "API Version": "API 版本", + "API endpoint configuration": "API 端点配置", "AWS Builder ID": "AWS 构建器 ID", "AWS IAM Identity Center": "AWS IAM 身份中心", "AWS Region": "AWS 区域", + "AWS region for the key (default: us-east-1)": "密钥的 AWS 区域(默认:us-east-1)", "AWS region for your Identity Center (default: us-east-1)": "您的身份中心的 AWS 区域(默认值:us-east-1)", + "About": "关于", + "Access Anywhere": "随处访问", + "Access Token": "访问令牌", + "Access token will be auto-filled...": "访问令牌将自动填充...", + "Access your terminal, desktop & files from anywhere": "从任何地方访问您的终端、桌面和文件", + "Account": "账号", + "Account ID": "账号 ID", + "Account Resources": "账号资源", + "Accounts per page": "每页账号数", + "Action": "操作", + "Activate": "激活", + "Active": "活跃", + "Active All": "全部激活", + "Active:": "活跃:", + "Add": "添加", + "Add API Key": "添加 API 密钥", + "Add Anthropic Compatible": "添加Anthropic兼容", + "Add Connection": "添加连接", + "Add Custom Embedding": "添加自定义嵌入", + "Add Custom MCP": "添加自定义 MCP", + "Add Custom Model": "添加自定义模型", + "Add Model": "添加模型", + "Add Model Config": "添加模型配置", + "Add Model for GitHub Copilot": "为 GitHub Copilot 添加模型", + "Add Model for OpenCode": "为 OpenCode 添加模型", + "Add Model to Combo": "将模型添加到组合", + "Add New Provider": "添加新提供商", + "Add OpenAI Compatible": "添加 OpenAI 兼容", + "Add Provider": "添加提供商", + "Add Proxy Pool": "添加代理池", + "Add Shorthands": "添加简写", + "Add a connection to enable importing models.": "添加连接以启用导入模型。", + "Add connection using browser cookie": "使用浏览器 cookie 添加连接", + "Add model": "添加模型", + "Add server": "添加服务器", + "Add the following configuration to your models array:": "将以下配置添加到您的 models 数组中:", + "Add your first connection to get started": "添加您的第一个连接以开始使用", + "Administrator required": "需要管理员权限", + "Administrator required — restart 9Router as Administrator to use MITM": "需要管理员权限 — 以管理员身份重新启动 9Router 以使用 MITM", + "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 并将其粘贴回此处。", + "Alibaba Qwen Code CLI — supports OpenAI, Anthropic & Gemini providers via 9Router": "阿里巴巴 Qwen Code CLI — 通过 9Router 支持 OpenAI、Anthropic 和 Gemini 提供商", + "All": "全部", + "All AI Providers": "所有 AI 提供商", + "All Providers": "所有提供商", + "All models are responding normally.": "所有模型均响应正常。", + "All providers": "所有提供商", + "All rates are in": "所有费率均在", + "All selected currently unbound": "所有选中项当前未绑定", + "Allow dashboard access via tunnel": "允许通过隧道访问仪表盘", + "Allow either password or OIDC.": "允许密码或 OIDC 登录。", + "An error occurred": "发生错误", + "An error occurred. Please try again.": "发生错误,请重试。", + "Anthropic Claude Code CLI": "Anthropic Claude Code CLI", + "Anthropic Compatible (Prod)": "Anthropic 兼容(生产)", + "Anthropic Compatible Details": "Anthropic 兼容详情", + "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", + "Any model available in 9Router can be used — not just Qwen models. Select from Qwen, Claude, Gemini, GPT, and more.": "9Router 中可用的任何模型都可以使用——不仅仅是 Qwen 模型。从 Qwen、Claude、Gemini、GPT 等中选择。", + "App Name": "应用名称", + "Apply": "应用", + "Apply Proxy": "应用代理", + "Applying...": "应用中...", + "Are you sure you want to close the proxy server?": "您确定要关闭代理服务器吗?", + "Are you sure you want to disable the tunnel?": "您确定要禁用隧道吗?", + "Attempting to reconnect...": "正在尝试重新连接...", + "Audio File": "音频文件", + "Auth Mode": "认证模式", + "Authenticate": "认证", + "Authentication Method": "认证方式", + "Authentication Successful": "认证成功", + "Authentication Successful!": "认证成功!", + "Authless": "无需认证", + "Authorization Successful!": "授权成功!", + "Authorize": "授权", + "Auto (by priority)": "自动(按优先级)", + "Auto Refresh (3s)": "自动刷新(3秒)", + "Auto-detect": "自动检测", + "Auto-detecting token...": "自动检测令牌...", + "Auto-detecting tokens...": "自动检测令牌...", + "Auto-ping": "自动 Ping", + "Auto-refresh": "自动刷新", + "Auto:": "自动:", + "Automatically switch between providers when limits are hit.": "当达到限额时自动切换提供商。", + "Available": "可用", + "Available Models": "可用模型", + "Azure Endpoint": "Azure 端点", + "Azure OpenAI Configuration": "Azure OpenAI 配置", + "BXAuth=xxx; ...": "BXAuth=xxx;...", "Back": "返回", + "Back to CLI Tools": "返回 CLI 工具", "Back to Providers": "返回提供商", "Base URL": "基础 URL", + "Batch Import": "批量导入", + "Batch Import Proxies": "批量导入代理", "Batch Size": "批量大小", + "Beautiful web dashboard for managing providers and monitoring usage.": "精美的 Web 仪表盘,用于管理提供商和监控使用情况。", + "Best quality, but costs the most": "质量最佳但成本最高", + "Bias the model toward minimal code: YAGNI, reuse stdlib, deletion over addition": "引导模型编写最少代码:YAGNI、重用标准库、删除多于添加", + "Binary File": "二进制文件", "Blog": "博客", + "Both": "两者", + "Browse & edit files": "浏览和编辑文件", + "Browse MCP Marketplace": "浏览 MCP 市场", + "Browse source, README, and examples.": "浏览源代码、README 和示例。", + "Browser Control (Browser MCP)": "浏览器控制(Browser MCP)", + "Bulk Add": "批量添加", + "CLI Support": "CLI 支持", + "CLI Tools": "命令行工具", + "CLI on the host →": "主机上的 CLI →", + "CLIProxyAPI Auth JSON": "CLIProxyAPI 认证 JSON", "Cache Creation": "缓存创建", "Cache Creation:": "缓存创建:", "Cached": "缓存", + "Cached Tokens": "缓存 Token", + "Cached Tokens:": "缓存 Token:", "Cached input tokens (typically 50% of input rate)": "缓存输入 Token(通常为输入费率的 50%)", "Cached:": "缓存:", "Calls per account before switching": "切换前每个账号的调用次数", + "Calls per combo model before switching": "切换前每个组合模型的调用次数", "Cancel": "取消", + "Capacity auto-switch": "容量自动切换", "Cert": "证书", + "Change Log": "更新日志", "Changelog": "变更日志", - "chars)": "字符)", + "Chat": "对话", + "Chat / code-gen via OpenAI or Anthropic format with streaming.": "通过 OpenAI 或 Anthropic 格式进行聊天/代码生成,支持流式传输。", "Chat Completions": "聊天完成", + "Check": "检查", "Checking Claude CLI...": "检查 Claude CLI...", + "Checking Claude Cowork...": "正在检查 Claude Cowork...", + "Checking Cline...": "正在检查 Cline...", "Checking Codex CLI...": "正在检查 Codex CLI...", "Checking Copilot config...": "正在检查Copilot配置...", + "Checking DeepSeek TUI...": "正在检查 DeepSeek TUI...", "Checking Factory Droid CLI...": "检查 Factory Droid CLI...", + "Checking Hermes Agent...": "正在检查 Hermes Agent...", + "Checking Kilo Code...": "正在检查 Kilo Code...", "Checking Open Claw CLI...": "正在检查 Open Claw CLI...", "Checking OpenCode CLI...": "检查 OpenCode CLI...", + "Checking jcode CLI...": "正在检查 jcode CLI...", + "Checking...": "检查中...", + "Choose API Provider → Ollama": "选择 API 提供商 → Ollama", + "Choose how to authenticate with GitLab Duo:": "选择如何通过 GitLab Duo 认证:", "Choose your authentication method:": "选择您的身份验证方法:", "Claude": "Claude", "Claude CLI - Manual Configuration": "Claude CLI - 手动配置", + "Claude CLI not detected locally": "未在本地检测到 Claude CLI", "Claude CLI not installed": "Claude CLI 未安装", + "Claude Cowork - Manual Configuration": "Claude Cowork - 手动配置", + "Claude Desktop (Cowork mode) not detected": "未检测到 Claude Desktop(Cowork 模式)", + "Claude Desktop Cowork (third-party inference)": "Claude Desktop Cowork(第三方推理)", "Clear": "清除", + "Clear (will use main model)": "清除(将使用主模型)", "Clear Filters": "清除过滤器", "Clear search": "清除搜索", + "Click": "点击", + "Click \"View All Model\" → \"Add Custom Model\"": "点击「查看所有模型」→「添加自定义模型」", + "Click a model to set/clear active": "点击模型以设置/取消活跃状态", + "Click to add, click again to remove. Changes are saved automatically.": "点击添加,再次点击删除。更改将自动保存。", "Click to edit": "点击编辑", + "Click to retry": "点击重试", + "Client ID": "客户端 ID", + "Client Request": "客户端请求", + "Client Response": "客户端响应", + "Client Secret": "客户端密钥", + "Cline - Manual Configuration": "Cline - 手动配置", + "Cline AI Coding Assistant": "Cline AI 编程助手", + "Cline not detected locally": "未在本地检测到 Cline", + "Close": "关闭", + "Close Proxy": "关闭代理", + "Close provider filter": "关闭提供商筛选", + "Close reset credit expiry modal": "关闭重置信用有效期弹窗", "Close test results": "关闭测试结果", + "Closing in": "即将关闭", + "Cloud Sync": "云端同步", + "Cloudflare Relay": "Cloudflare Relay", "Cloudflare Tunnel": "Cloudflare 隧道", + "Cloudflare Workers AI": "Cloudflare Workers AI", "Codex CLI - Manual Configuration": "Codex CLI - 手动配置", + "Codex CLI not detected locally": "未在本地检测到 Codex CLI", "Codex CLI not installed": "Codex CLI 未安装", + "Codex Reset Credit Expiry": "Codex 重置信用有效期", "Codex uses": "Codex 使用", "Combo Name": "组合名称", + "Combo Round Robin": "组合轮询", + "Combo Sticky Limit": "组合粘性限制", "Combos": "组合", "Coming soon...": "即将推出...", "Comma-separated hostnames/domains to bypass the proxy.": "以逗号分隔的主机名/域以绕过代理。", + "Comma-separated hosts/domains to bypass proxy": "以逗号分隔的主机名/域以绕过代理", "Company": "公司", "Complete the authorization in the popup window.": "在弹出的窗口中完成授权。", "Completion/response tokens": "补全/响应 Token", + "Compress LLM output": "压缩 LLM 输出", + "Compress context": "压缩上下文", + "Compress prompts via /v1/compress before routing to the model": "在路由到模型之前通过 /v1/compress 压缩提示", + "Compress tool output": "压缩工具输出", + "Compress tool output to reduce token usage.": "压缩工具输出以减少 Token 使用量。", + "Config path: Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml": "配置路径:Linux/macOS ~/.deepseek/config.toml • Windows %USERPROFILE%\\.deepseek\\config.toml", + "Config path: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json": "配置路径:Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json", + "Configuration": "配置", + "Configure 9router as an OpenAI-compatible provider to route all jcode requests through 9router's optimization layer.": "将 9router 配置为 OpenAI 兼容提供商,以通过 9router 的优化层路由所有 jcode 请求。", + "Configure CLI tools": "配置 CLI 工具", "Configure a new AI provider to use with your applications.": "配置新的 AI 提供程序以与您的应用程序一起使用。", "Configure pricing rates for cost tracking and calculations": "配置定价以进行成本跟踪和计算", + "Configure providers and API keys via web interface": "通过 Web 界面配置提供商和 API 密钥", + "Configured": "已配置", "Confirm": "确认", - "Confirm new password": "确认新密码", "Confirm New Password": "确认新密码", + "Confirm Password": "确认密码", + "Confirm new password": "确认新密码", "Connect": "连接", "Connect AI tools remotely": "远程连接AI工具", "Connect Cursor IDE": "连接CursorIDE", + "Connect GitLab Duo": "连接 GitLab Duo", "Connect Kiro": "连接Kiro", "Connect to providers with OAuth to track your API quota limits and usage.": "使用 OAuth 连接到提供商以跟踪您的 API 配额限制和使用情况。", + "Connect via OAuth or API keys. Securely manage credentials.": "通过 OAuth 或 API 密钥连接。安全管理凭据。", "Connect with OAuth2": "使用 OAuth2 连接", "Connect your account using OAuth2 authentication.": "使用 OAuth2 身份验证连接您的账号。", "Connected": "已连接", "Connected Successfully!": "连接成功!", + "Connected providers only": "仅已连接的提供商", + "Connecting...": "连接中...", + "Connection": "连接", + "Connection Details": "连接详情", "Connection Failed": "连接失败", "Connections": "连接", + "Console Log": "控制台日志", "Contact": "联系", "Content": "内容", "Continue": "继续", + "Continue AI Assistant": "Continue AI 助手", + "Continue to summary": "继续到摘要", "Continue with GitHub": "继续使用 GitHub", "Continue with Google": "使用 Google 继续", "Cookie": "Cookie", "Cookie Auth": "Cookie 验证", "Cookie String": "Cookie 字符串", "Cooldown": "冷却", + "Copied!": "已复制!", "Copy": "复制", - "Copy combo name": "复制组合名称", - "Copy model": "复制模型", - "Copy the entire cookie string (must include BXAuth)": "复制整个 cookie 字符串(必须包括 BXAuth)", + "Copy & Shutdown": "复制并关闭", "Copy This URL": "复制此 URL", + "Copy a link and paste to your AI to use 9Router — no install needed": "复制链接并粘贴到您的 AI 以使用 9Router — 无需安装", + "Copy combo name": "复制组合名称", + "Copy install command": "复制安装命令", + "Copy model": "复制模型", + "Copy the JSON below to your ~/.qwen/settings.json file.": "将以下 JSON 复制到您的 ~/.qwen/settings.json 文件中。", + "Copy the entire cookie string (must include BXAuth)": "复制整个 cookie 字符串(必须包括 BXAuth)", "Cost": "成本", "Cost Calculation:": "成本计算:", + "Costs": "成本", "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)": "成本根据 Token 用量和费率计算。每个请求的成本由以下公式决定:(input_tokens × input_rate) + (output_tokens × output_rate) + (cached_tokens × cached_rate)", "Could not read Cursor database automatically.": "无法自动读取 Cursor 数据库。", "Create": "创建", "Create API Key": "创建 API 密钥", "Create Combo": "创建组合", + "Create Cowork Combo": "创建 Cowork 组合", "Create Key": "创建密钥", - "Create model combos with fallback support": "创建具有后备支持的模型组合", "Create Provider": "创建提供商", + "Create Token": "创建令牌", + "Create a": "创建一个", + "Create a proxy pool entry, then assign it to connections.": "创建代理池条目,然后分配到连接。", + "Create model combos with fallback support": "创建具有后备支持的模型组合", "Create your first API key to get started": "创建您的第一个 API 密钥以开始使用", "Created": "已创建", + "Creating...": "创建中...", "Current": "当前", "Current Password": "当前密码", "Current Pricing Overview": "当前定价概述", + "Current password": "当前密码", "Current: Keeps": "当前: 保留", + "Currently using accounts in priority order (Fill First).": "当前按优先级顺序使用账号(优先填满)。", + "Cursor AI Code Editor": "Cursor AI 代码编辑器", "Cursor IDE not detected. Please paste your tokens manually.": "未检测到Cursor IDE。请手动粘贴您的令牌。", + "Cursor routes requests through its own server, so local endpoint is not supported. Please enable Tunnel or Cloud Endpoint in Settings.": "Cursor 通过自己的服务器路由请求,因此不支持本地端点。请在设置中启用隧道或云端点。", + "Custom": "自定义", "Custom Pricing:": "定制定价:", + "Custom Providers (OpenAI/Anthropic Compatible)": "自定义提供商(OpenAI/Anthropic 兼容)", + "Custom Token": "自定义令牌", + "Custom accounts per page": "自定义每页账号数", + "Custom providers": "自定义提供商", + "Custom...": "自定义...", "Cycle through accounts to distribute load": "循环切换账号以分配负载", + "Cycle through providers in combos instead of always starting with first": "在组合中循环使用提供商,而不是总是从第一个开始", + "DNS off": "DNS 关闭", + "Dashboard": "仪表盘", + "Dashboard Password": "仪表盘密码", + "Dashboard:": "仪表盘:", + "Data Location:": "数据位置:", + "Data flows seamlessly from your application through our intelligent routing layer to the best provider for the job.": "数据从您的应用程序通过我们的智能路由层无缝流向最适合任务的提供商。", + "Data flows seamlessly through our intelligent routing system": "数据通过我们的智能路由系统无缝流转", + "Database Location": "数据库位置", "Database backup downloaded": "数据库备份已下载", "Database imported successfully": "数据库导入成功", - "Database Location": "数据库位置", "DateTime": "日期时间", + "Deactivate": "停用", + "Debug": "调试", "Debug translation flow between formats": "调试格式之间的翻译流程", + "DeepSeek TUI - Manual Configuration": "DeepSeek TUI - 手动配置", + "DeepSeek TUI not detected locally": "未在本地检测到 DeepSeek TUI", + "DeepSeek TUI uses ~/.deepseek/config.toml for configuration. 9Router will update the provider to 'openai' mode with your base_url, api_key, and model.": "DeepSeek TUI 使用 ~/.deepseek/config.toml 进行配置。9Router 将提供商更新为 'openai' 模式,包含您的 base_url、api_key 和 model。", + "DeepSeek Terminal Coding Agent (Rust TUI)": "DeepSeek 终端编程代理(Rust TUI)", + "Default Model": "默认模型", + "Default password is": "默认密码为", + "Default password is 123456": "默认密码为 123456", "Delete": "删除", + "Delete API Key": "删除 API 密钥", + "Delete connection": "删除连接", + "Delete saved endpoint": "删除已保存的端点", + "Delete selected preset": "删除选中的预设", + "Delete this combo?": "删除此组合?", + "Delete this connection?": "删除此连接?", + "Deno Deploy API Token": "Deno Deploy API 令牌", + "Deno Deploy v2 runs on a high-performance global edge network": "Deno Deploy v2 运行在高性能全球边缘网络上", + "Deno Relay": "Deno Relay", + "Deploy": "部署", + "Deploy Cloudflare Relay": "部署 Cloudflare Relay", + "Deploy Deno Relay": "部署 Deno Relay", + "Deploy Relay": "部署中继", + "Deploy Vercel Relay": "部署 Vercel Relay", + "Deploy multiple relays for maximum IP diversity": "部署多个中继以获得最大 IP 多样性", + "Deploy multiple relays on different accounts for more IP diversity": "在不同账号上部署多个中继以获得更多 IP 多样性", + "Deploying... (may take ~1 min)": "部署中...(可能需要约 1 分钟)", + "Deployment Name": "部署名称", + "Deploys a Cloudflare Worker as a proxy relay. All AI provider requests will be forwarded through Cloudflare's global edge network.": "将 Cloudflare Worker 部署为代理中继。所有 AI 提供商请求将通过 Cloudflare 的全球边缘网络转发。", + "Deploys a relay worker to Deno Deploy's global edge network. All AI provider requests are forwarded through Deno's edge, masking your real IP.": "将中继 Worker 部署到 Deno Deploy 的全球边缘网络。所有 AI 提供商请求通过 Deno 的边缘转发,隐藏您的真实 IP。", + "Deploys an edge relay function to Vercel that proxies requests through Vercel's network.": "将边缘中继函数部署到 Vercel,通过 Vercel 的网络代理请求。", + "Deploys an edge relay function to Vercel. All AI provider requests will be forwarded through Vercel's edge network, masking your real IP from providers.": "将边缘中继函数部署到 Vercel。所有 AI 提供商请求将通过 Vercel 的边缘网络转发,隐藏您的真实 IP。", + "Desktop": "桌面", "Detail": "详情", + "Details": "详情", + "Dimensions": "维度", + "Disable": "禁用", + "Disable All": "全部禁用", + "Disable Tailscale": "禁用 Tailscale", "Disable Tunnel": "禁用隧道", + "Disable connections with depleted quota on the current page": "禁用当前页面上配额已耗尽的连接", + "Disable provider": "禁用提供商", + "Disable this model": "禁用此模型", "Disabled": "已禁用", + "Disabling...": "关闭中...", + "Disconnected from server": "与服务器断开连接", + "Dismiss notification": "关闭通知", "Display Name": "显示名称", - "DNS off": "DNS 关闭", + "Display language": "显示语言", + "Docs": "文档", "Documentation": "文档", - "dollars per million tokens": "美元 / 百万 Token", "Domain:": "域名:", + "Donate": "捐赠", "Done": "完成", + "Download": "下载", "Download Backup": "下载备份", - "e.g. claude-opus-4-5": "例如 claude-opus-4-5", - "e.g., Production API, Dev Environment": "例如,生产 API、开发环境", + "Drag to reorder": "拖拽以排序", + "Easy Setup": "简单设置", "Edit": "编辑", + "Edit Combo": "编辑组合", "Edit Connection": "编辑连接", "Edit Pricing": "编辑定价", + "Edit Proxy Pool": "编辑代理池", + "Edit connection": "编辑连接", + "Edit hosts file manually to add the following entries:": "手动编辑 hosts 文件以添加以下条目:", "Email": "邮箱", + "Embedding": "嵌入", + "Embeddings": "嵌入", + "Enable": "启用", "Enable DNS per tool below to activate interception": "启用下面每个工具的 DNS 以激活拦截", + "Enable DNS to edit model mappings": "启用 DNS 以编辑模型映射", "Enable Observability": "启用可观察性", - "Enable proxy for OAuth + provider outbound requests.": "为 OAuth + 提供商出站请求启用代理。", + "Enable OpenAI API": "启用 OpenAI API", "Enable Tunnel": "启用隧道", + "Enable connections that still have quota on the current page": "启用当前页面上仍有配额的连接", + "Enable provider": "启用提供商", + "Enable proxy for OAuth + provider outbound requests.": "为 OAuth + 提供商出站请求启用代理。", "Encrypted": "已加密", "End Date": "结束日期", "End-to-end TLS via Cloudflare": "通过 Cloudflare 的端到端 TLS", "Endpoint": "端点", "Endpoint & Key": "端点与密钥", + "Endpoint is exposed without an API key.": "端点未设置 API 密钥即对外暴露。", "Enter current password": "输入当前密码", + "Enter model id": "输入模型 ID", + "Enter model id (provider-specific)": "输入模型 ID(提供商特定)", "Enter new API key": "输入新的 API 密钥", "Enter new password": "输入新密码", + "Enter or pick API key": "输入或选择 API 密钥", + "Enter password": "输入密码", "Enter sudo password": "输入sudo密码", + "Enter the model ID exactly as your compatible endpoint expects it. This model will be saved as the connection default.": "输入您的兼容端点所需的模型 ID。此模型将保存为连接的默认模型。", "Enter your API key": "输入您的 API 密钥", + "Enter your current password to": "输入当前密码以", + "Enter your password to access the dashboard": "输入密码以访问仪表盘", + "Error": "错误", "Est. Cost": "预估成本", "Estimated, not actual billing": "预估费用,非实际账单", + "Everything you need to manage your AI infrastructure efficiently.": "高效管理 AI 基础设施所需的一切。", + "Everything you need to manage your AI infrastructure in one place, built for scale.": "为规模化而构建,在一个地方管理 AI 基础设施所需的一切。", + "Example": "示例", + "Experimental": "实验性", + "Expires At": "过期时间", + "Expiring first": "即将过期优先", + "Expiring-first currently reorders accounts inside the current page. Cross-page ordering still follows backend pagination.": "「即将过期优先」当前会在当前页面内重新排序账号。跨页面排序仍遵循后端分页。", "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 AI Assistant": "Factory Droid AI 助手", + "Factory Droid CLI not detected locally": "未在本地检测到 Factory Droid CLI", "Factory Droid CLI not installed": "Factory Droid CLI 未安装", + "Fail request if proxy is unreachable instead of falling back to direct.": "当代理不可达时直接失败,而不是回退到直连。", + "Failed to apply settings": "应用设置失败", + "Failed to create combo": "创建组合失败", + "Failed to load changelog:": "加载更新日志失败:", + "Failed to load usage statistics.": "无法加载使用情况统计信息。", + "Failed to reset settings": "重置设置失败", + "Failed to set alias": "设置别名失败", + "Failed to update combo": "更新组合失败", + "Failed to update password": "更新密码失败", + "Failed to update proxy settings": "更新代理设置失败", + "Fallback": "回退", + "Fallback — tries models in order (next on failure)": "回退 — 按顺序尝试模型(失败时切换到下一个)", + "Fallback — try in order": "回退 — 按顺序尝试", + "Features": "功能特性", "Fetch Qoder Models": "获取 Qoder 模型", "Fetching...": "获取中...", - "Failed to load usage statistics.": "无法加载使用情况统计信息。", - "Features": "功能特性", + "Files": "文件", + "Filter accounts by status": "按状态筛选账号", + "Filter naming": "过滤命名", + "Filter naming requests": "过滤命名请求", + "Filter quota providers": "筛选配额提供商", + "Find MCPs →": "查找 MCP →", + "Find your Account ID in the right sidebar of": "在右侧边栏中找到您的账号 ID", + "Find your Account ID in the right sidebar of dash.cloudflare.com": "在 dash.cloudflare.com 的右侧边栏中找到您的账号 ID", + "First Page": "首页", "Flush Interval (ms)": "刷新间隔(毫秒)", "For enterprise users with custom AWS IAM Identity Center.": "适用于具有自定义 AWS IAM Identity Center 的企业用户。", + "Forgot password? Open": "忘记密码?打开", + "Format": "格式化", + "Found on the right side of the Cloudflare dashboard overview page.": "位于 Cloudflare 仪表盘概览页面的右侧。", + "Free": "免费", + "Free & Free Tier Providers": "免费及免费额度提供商", "Free Providers": "免费提供商", + "Free Tier": "免费额度", + "Free Tier Providers": "免费额度提供商", + "Free tier: 100,000 requests per day": "免费套餐:每天 10 万次请求", + "Free tier: 100GB bandwidth/month, 500K edge invocations": "免费套餐:100GB 带宽/月,50 万次边缘调用", + "Free tier: 1M requests & 100GiB outbound traffic per month": "免费套餐:每月 100 万次请求和 100GiB 出站流量", "Fresh API key obtained": "获得新的 API 密钥", + "Full shell access": "完整 Shell 访问", + "Fusion": "融合", + "Fusion — panel + judge": "融合 — 面板 + 裁判", + "Fusion — queries all models in parallel, then a judge synthesizes one answer": "融合 — 并行查询所有模型,然后由裁判综合出一个答案", + "Get 9Remote": "获取 9Remote", + "Get API Key": "获取 API 密钥", + "Get API Key →": "获取 API 密钥 →", + "Get Started": "开始使用", + "Get Started in 30 Seconds": "30 秒快速上手", + "Get started": "开始使用", + "Get started in seconds. Just install, open, and route.": "几秒钟即可上手。安装、打开、路由。", + "Get token →": "获取令牌 →", + "GitHub": "GitHub", "GitHub Account": "GitHub 账号", "GitHub Copilot - Manual Configuration": "GitHub Copilot - 手动配置", + "GitHub Copilot IDE with MITM": "GitHub Copilot IDE 与 MITM", + "GitLab Access Tokens": "GitLab 访问令牌", + "GitLab Applications": "GitLab 应用", + "GitLab Base URL": "GitLab 基础 URL", + "Go to": "前往", + "Go to Roo Settings panel": "前往 Roo 设置面板", "Google Account": "Google 账号", - "has been connected.": "已连接。", + "Google Antigravity IDE with MITM": "Google Antigravity IDE 与 MITM", + "Granted At": "授予时间", + "Group models under one name, then pick a strategy per combo:": "将模型归组到一个名称下,然后为每个组合选择策略:", + "Headroom proxy is reachable. You can enable the token saver.": "Headroom 代理可达。您可以启用 Token 节省器。", "Help Center": "帮助中心", - "How it works:": "工作原理:", + "Hermes Agent - Manual Configuration": "Hermes Agent - 手动配置", + "Hermes Agent not detected locally": "未在本地检测到 Hermes Agent", + "Hide": "隐藏", + "Hide key": "隐藏密钥", + "High performance global routing and IP masking via Cloudflare Workers": "通过 Cloudflare Workers 实现高性能全局路由和 IP 隐藏", + "High-performance Rust-based coding agent harness": "基于 Rust 的高性能编程代理框架", + "History": "历史记录", + "How 9Router Works": "9Router 工作原理", "How Pricing Works": "定价如何运作", + "How it Works": "工作原理", + "How it works:": "工作原理:", + "How to Install": "如何安装", + "How to generate API token:": "如何生成 API 令牌:", + "How to generate your API Token:": "如何生成您的 API 令牌:", "How to get cookie:": "如何获取cookie:", + "ID:": "ID:", "IDC Start URL": "IDC 起始 URL", - "iFlow AI": "iFlow AI", - "iFlow Cookie Authentication": "iFlow Cookie 身份验证", + "If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead.": "如果提供商不支持 /models 端点,请输入模型 ID 通过 chat/completions 进行验证。", + "Image Generation": "图像生成", + "Image to Text": "图像转文本", + "Import": "导入", "Import Backup": "导入备份", + "Import CLIProxyAPI JSON": "导入 CLIProxyAPI JSON", "Import Token": "导入令牌", + "Importing...": "导入中...", "In": "输入", "In / Out": "输入/输出", "Inactive": "未激活", + "Inactive pools are ignored by runtime resolution.": "未激活的代理池将被运行时解析忽略。", "Inc. All rights reserved.": "公司。保留所有权利。", + "Initializing...": "初始化中...", "Input": "输入", + "Input Cost": "输入成本", "Input Tokens": "输入 Token", "Input Tokens:": "输入 Token:", "Input:": "输入:", + "Install 9Router": "安装 9Router", + "Install 9Router, configure your providers via web dashboard, and start routing AI requests.": "安装 9Router,通过 Web 仪表盘配置您的提供商,然后开始路由 AI 请求。", + "Install Chrome extension": "安装 Chrome 扩展", + "Install Cline VS Code extension or CLI from": "从以下位置安装 Cline VS Code 扩展或 CLI", + "Install Kilo Code from": "从以下位置安装 Kilo Code", + "Install Qwen Code": "安装 Qwen Code", + "Install Tailscale": "安装 Tailscale", + "Install command:": "安装命令:", + "Install jcode to enable automatic configuration:": "安装 jcode 以启用自动配置:", + "Install the Amp CLI using the package manager supported by your environment.": "使用您环境支持的包管理器安装 Amp CLI。", + "Install then click Start:": "安装后点击启动:", + "Install via npm:": "通过 npm 安装:", "Installation Guide": "安装指南", + "Installing Tailscale...": "正在安装 Tailscale...", "Interactive diagram visible on desktop": "桌面上可见的交互式图表", + "Intercept CLI tool traffic and route through 9Router": "拦截 CLI 工具流量并通过 9Router 路由", "Intercepts Antigravity traffic via DNS redirect, letting you reroute models through 9Router.": "通过 DNS 重定向拦截Antigravity流量,让您可以通过 9Router 重新路由模型。", + "Intercepts Claude Code's topic-naming requests and returns a fake response locally, saving API tokens.": "拦截 Claude Code 的主题命名请求并在本地返回伪响应,节省 API Token。", + "Invalid": "无效", + "Invalid password": "密码错误", + "Issuer URL": "发行者 URL", + "JSON Response": "JSON 响应", + "Join developers who are streamlining their AI integrations with 9Router. Open source and free to start.": "加入正在使用 9Router 简化 AI 集成的开发者行列。开源且免费。", + "Judge": "裁判", + "Just now": "刚刚", "KB per field": "每个字段的 KB", + "Keep the legacy password login.": "保留原有密码登录方式。", "Key Name": "密钥名称", + "KiRo dashboard": "KiRo 仪表盘", + "Kill & Start": "终止并启动", + "Kill this process to start MITM Server?": "终止此进程以启动 MITM 服务器?", + "Kilo Code - Manual Configuration": "Kilo Code - 手动配置", + "Kilo Code AI Assistant": "Kilo Code AI 助手", + "Kilo Code not detected locally": "未在本地检测到 Kilo Code", "Kimi": "Kimi", "Kiro AI": "Kiro AI", "Kiro IDE not detected. Please paste your refresh token manually.": "未检测到 Kiro IDE。请手动粘贴您的刷新令牌。", - "Last updated:": "最后更新:", + "Kiro IDE with MITM": "Kiro IDE 与 MITM", + "Language": "语言", + "Languages": "语言", + "Last Page": "末页", "Last Used": "最后使用", + "Last tested:": "上次测试:", + "Last updated:": "最后更新:", "Latency": "延迟", "Latency:": "延迟:", + "Lazy senior dev": "懒人高级开发者", "Lean": "Lean", + "Leave blank to keep existing secret": "留空以保留现有密钥", + "Leave blank to use": "留空以使用", + "Leave empty for public PKCE app": "公开 PKCE 应用请留空", "Leave empty to inherit existing env proxy (if any).": "留空以继承现有的 env 代理(如果有)。", + "Legacy manual proxy fields are still accepted by API for backward compatibility.": "API 仍接受旧版手动代理字段以实现向后兼容。", + "Legacy:": "旧版:", + "Legal": "法律", + "Live server console output": "服务器实时控制台输出", "Load": "加载", "Loading logs...": "正在加载日志...", "Loading models from provider...": "正在从提供商处加载模型...", "Loading pricing data...": "正在加载定价数据...", + "Loading registry...": "正在加载注册表...", + "Loading reset credits...": "正在加载重置信用...", + "Loading...": "加载中...", + "Local": "本地", "Local Mode": "本地模式", "Local Mode - All data stored on your machine": "本地模式 - 所有数据都存储在您的计算机上", + "Local Plugins": "本地插件", + "Locked. Retry in": "已锁定。请在", + "Login": "登录", + "Login Button Label": "登录按钮标签", + "Login URL": "登录 URL", "Login to your account": "登录您的账号", "Login with your GitHub account (manual callback).": "使用您的 GitHub 账号登录(手动回调)。", "Login with your Google account (manual callback).": "使用您的 Google 账号登录(手动回调)。", + "Logout": "退出登录", + "Logs": "日志", + "Logs are loaded from the request history database.": "日志从请求历史数据库中加载。", "Logs are saved to log.txt in the application data directory.": "日志保存在应用程序数据目录下的log.txt中。", + "MIT License": "MIT 许可证", + "MITM": "MITM", + "MITM Proxy": "MITM 代理", + "MITM Server": "中间人服务器", + "MITM Tools": "MITM 工具", "Machine ID": "机器ID", "Machine ID will be auto-filled...": "机器 ID 将自动填充...", - "macOS / Linux / Windows:": "macOS / Linux / Windows:", - "macOS / Linux:": "macOS / Linux:", + "Make sure Cursor IDE has been opened at least once, then click": "请确保 Cursor IDE 至少已打开过一次,然后点击", + "Manage": "管理", + "Manage reusable per-connection proxies and bind them to provider connections.": "管理可复用的连接代理并绑定到提供商连接。", + "Manage your AI provider connections": "管理您的 AI 提供商连接", + "Manage your Embedding providers": "管理您的嵌入提供商", + "Manage your Image to Text providers": "管理您的图像转文本提供商", + "Manage your Music providers": "管理您的音乐提供商", + "Manage your Speech To Text providers": "管理您的语音转文本提供商", + "Manage your Text To Speech providers": "管理您的文本转语音提供商", + "Manage your Text to Image providers": "管理您的文本转图像提供商", + "Manage your Video providers": "管理您的视频提供商", + "Manage your Web Fetch providers": "管理您的 Web 抓取提供商", + "Manage your Web Search providers": "管理您的 Web 搜索提供商", + "Manage your preferences": "管理您的偏好设置", + "Manage your proxy pool configurations": "管理您的代理池配置", + "Manual / current endpoint": "手动/当前端点", "Manual Callback Required": "需要手动回调", "Manual Config": "手动配置", + "Manual configuration is still available if 9router is deployed on a remote server.": "如果 9router 部署在远程服务器上,仍可使用手动配置。", + "Map Amp shorthand names such as g25p or cs45 to 9Router aliases in your local config.": "将 Amp 简写名称(如 g25p 或 cs45)映射到本地配置中的 9Router 别名。", + "Mask (URL)": "遮罩(URL)", "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)": "刷新缓冲区之前等待的最长时间(防止低流量期间数据丢失)", + "Media Providers": "媒体提供商", + "Menu": "菜单", + "Message AI": "向 AI 发送消息", "Messages": "消息", + "Messages API": "消息 API", "MiniMax": "MiniMax", - "MITM Server": "中间人服务器", "Model": "模型", + "Model Fallback": "模型回退", "Model ID": "模型 ID", "Model ID (from OpenRouter)": "模型 ID(来自 OpenRouter)", - "Model is reachable": "模型可达", - "Model mappings will be available soon.": "模型映射即将推出。", + "Model ID (optional)": "模型 ID(可选)", "Model Status": "模型状态", + "Model combos": "模型组合", + "Model combos with fallback": "模型组合及回退", + "Model is reachable": "模型可达", + "Model list is filtered from connected providers.": "模型列表已从已连接的提供商中筛选。", + "Model mappings will be available soon.": "模型映射即将推出。", + "Model not reachable": "模型不可达", "Model:": "模型:", "Models": "模型", - "more providers": "更多提供商", + "Monitor your API usage, token consumption, and request logs": "监控您的 API 使用量、Token 消耗和请求日志", + "More on GitHub": "在 GitHub 上查看更多", "Move down": "下移", "Move up": "向上移动", - "ms / Total": "毫秒/总计", + "Music": "音乐", + "My Profile": "我的资料", + "N/A": "无", + "NPM": "NPM", "Name": "名称", + "Name is required": "名称为必填项", + "Native CLI tool support for Cursor, Claude, Copilot, and more.": "原生支持 Cursor、Claude、Copilot 等 CLI 工具。", + "Navigate to home": "导航到首页", "Network": "网络", + "Network Error": "网络错误", + "Network error": "网络错误", + "Never": "从未", "New Password": "新密码", + "New password": "新密码", + "Next": "下一页", + "Next accounts page": "下一页账号", + "No API keys - Create one in Keys page": "暂无 API 密钥 - 请在密钥页面创建", + "No API keys yet": "还没有 API 密钥", + "No MCPs added": "未添加 MCP", + "No Providers Connected": "没有连接提供商", + "No Proxy": "无代理", "No active connections found for this group.": "未找到该组的活跃连接。", "No active providers": "没有活跃的提供商", - "No API keys yet": "还没有 API 密钥", + "No active proxy pools available. Create one in Proxy Pools page first.": "没有可用的活跃代理池。请先在代理池页面创建一个。", + "No authentication required": "无需身份验证", "No combos yet": "还没有组合", + "No combos yet.": "暂无组合。", "No compatible providers added yet": "尚未添加兼容的提供商", "No connections": "无连接", "No connections yet": "还没有连接", "No console logs yet.": "还没有控制台日志。", + "No conversations yet.": "暂无对话。", + "No custom providers": "没有自定义提供商", + "No custom providers — use buttons above to add OpenAI/Anthropic compatible endpoints": "没有自定义提供商 — 使用上方按钮添加 OpenAI/Anthropic 兼容端点", "No data for this period": "此期间没有数据", + "No key configured": "未配置密钥", + "No language selected": "未选择语言", + "No languages found.": "未找到语言。", "No logs recorded yet.": "尚未记录任何日志。", + "No model selected.": "未选择模型。", "No models": "暂无模型", "No models added yet": "尚未添加模型", "No models configured": "未配置模型", "No models found": "未找到模型", "No models match your filter.": "没有模型匹配您的筛选条件。", + "No models selected": "未选择模型", + "No port forwarding needed": "无需端口转发", "No pricing data available": "无可用定价数据", - "No Providers Connected": "没有连接提供商", - "No Proxy": "无代理", + "No providers connected": "没有连接提供商", + "No providers match your search": "没有匹配搜索结果的提供商", + "No providers support": "没有提供商支持", + "No providers yet.": "暂无提供商。", + "No providers.": "暂无提供商。", + "No proxy pool entries yet": "暂无代理池条目", + "No proxy:": "无代理:", "No quota data available": "无可用配额数据", "No request details found": "未找到请求详细信息", "No requests yet.": "暂无请求。", + "No reset credit details returned for this account.": "此账号没有返回重置信用详情。", + "No results": "无结果", + "No servers match filter": "没有匹配筛选条件的服务器", + "No tools advertised by server.": "服务器未公布任何工具。", + "No usage yet.": "暂无使用记录。", + "None": "无", + "None (unbind all)": "无(解除全部绑定)", "Not configured": "未配置", + "Not installed": "未安装", + "Notice": "提示", + "Nous Research self-improving AI agent": "Nous Research 自我改进的 AI 代理", "Number of items to accumulate before writing to database (higher = better performance)": "写入数据库之前要累积的项目数(越高=性能越好)", + "OAuth": "OAuth", + "OAuth & API Keys": "OAuth 和 API 密钥", + "OAuth Account": "OAuth 账号", + "OAuth App": "OAuth 应用", "OAuth Providers": "OAuth 提供商", + "OAuth required": "需要 OAuth", + "OIDC Dashboard Login": "OIDC 仪表盘登录", + "OIDC active": "OIDC 已激活", + "OIDC login is currently active. Password login is disabled until you switch back.": "OIDC 登录当前已激活。密码登录已禁用,直到您切换回来。", + "OIDC login is enabled, but the issuer/client fields are not configured yet. Password login is still available for recovery.": "OIDC 登录已启用,但发行者/客户端字段尚未配置。密码登录仍可用于恢复。", + "OIDC only": "仅 OIDC", "Observability": "可观察性", + "Office Proxy": "办公代理", + "Ollama Host URL": "Ollama 主机 URL", + "One Endpoint for": "统一端点,接入", + "One key per line. Format:": "每行一个密钥。格式:", + "One-to-one (rotate)": "一对一(轮换)", + "Only from connected providers": "仅来自已连接的提供商", "Only letters, numbers, - and _ allowed": "只允许使用字母、数字、- 和 _", + "Only letters, numbers, -, _ and .": "仅允许使用字母、数字、-、_ 和 .", + "Only letters, numbers, -, _ and . allowed": "仅允许使用字母、数字、-、_ 和 .", "Only one connection is allowed per compatible node. Add another node if you need more connections.": "每个兼容节点仅允许一个连接。如果需要更多连接,请添加另一个节点。", + "Open": "打开", + "Open Claude Desktop → Help → Troubleshooting → Enable Developer mode → Configure third-party inference, then return here.": "打开 Claude Desktop → 帮助 → 故障排除 → 启用开发者模式 → 配置第三方推理,然后返回此处。", "Open Claw - Manual Configuration": "Open Claw - 手动配置", + "Open Claw AI Assistant": "Open Claw AI 助手", + "Open Claw CLI not detected locally": "未在本地检测到 Open Claw CLI", "Open Claw CLI not installed": "未安装 Open Claw CLI", + "Open Continue configuration file": "打开 Continue 配置文件", + "Open Dashboard": "打开仪表盘", "Open DevTools (F12) → Application/Storage → Cookies": "打开 DevTools (F12) → 应用程序/存储 → Cookie", + "Open Settings": "打开设置", "Open platform.iflow.cn in your browser": "在浏览器中打开platform.iflow.cn", + "OpenAI / ElevenLabs / Edge / Google / Deepgram voices.": "OpenAI / ElevenLabs / Edge / Google / Deepgram 语音。", + "OpenAI Codex CLI": "OpenAI Codex CLI", "OpenAI Compatible (Prod)": "OpenAI 兼容(生产)", + "OpenAI Compatible Details": "OpenAI 兼容详情", + "OpenAI Intermediate": "OpenAI 中间格式", + "OpenAI Response": "OpenAI 响应", "OpenCode - Manual Configuration": "OpenCode - 手动配置", + "OpenCode AI Terminal Assistant": "OpenCode AI 终端助手", + "OpenCode CLI not detected locally": "未在本地检测到 OpenCode CLI", "OpenCode CLI not installed": "未安装 OpenCode CLI", "OpenRouter": "OpenRouter", "OpenRouter supports any model. Add models and create aliases for quick access.": "OpenRouter 支持任何模型。添加模型并创建别名以便快速访问。", + "Optional SSO via Authentik/Keycloak/Google": "可选通过 Authentik/Keycloak/Google 的 SSO", + "Or paste callback URL manually": "或手动粘贴回调 URL", + "Organization": "组织", + "Organization Domain": "组织域名", + "Organization ID": "组织 ID", + "Organization Token": "组织令牌", + "Organization Tokens": "组织令牌", "Other": "其他", + "Our engine analyzes the prompt and routes through your subscription, cheap, and free provider tiers with automatic fallback.": "我们的引擎分析提示词并通过您的订阅、低价和免费提供商层级路由,自动回退。", + "Our engine analyzes the prompt, checks provider health, and routes for lowest latency or cost.": "我们的引擎分析提示词,检查提供商健康状态,并路由到最低延迟或成本的提供商。", "Out": "输出", "Outbound Proxy": "出站代理", "Output": "输出", + "Output Cost": "输出成本", + "Output Format": "输出格式", "Output Tokens": "输出 Token", "Output Tokens:": "输出 Token:", "Output:": "输出:", + "Overview": "概览", + "Paid": "付费", + "Partial preview": "部分预览", + "Password": "密码", + "Password + OIDC active": "密码 + OIDC 已激活", + "Password and OIDC login are both active.": "密码和 OIDC 登录均已激活。", + "Password and OIDC login are both enabled.": "密码和 OIDC 登录均已启用。", + "Password only": "仅密码", "Password updated successfully": "密码更新成功", "Passwords do not match": "密码不匹配", + "Paste Proxy List (One per line)": "粘贴代理列表(每行一个)", + "Paste a long-lived Kiro/CodeWhisperer API key. It is validated against AWS and stored directly as a bearer credential (no refresh).": "粘贴一个长期有效的 Kiro/CodeWhisperer API 密钥。它将通过 AWS 验证并直接存储为 Bearer 凭据(无需刷新)。", + "Paste external_idp auth JSON from CLIProxyAPI/Kiro Microsoft login.": "从 CLIProxyAPI/Kiro Microsoft 登录粘贴 external_idp 认证 JSON。", "Paste it below": "粘贴在下面", "Paste refresh token from Kiro IDE.": "从 Kiro IDE 粘贴刷新令牌。", + "Paste the Kiro CLIProxyAPI auth JSON containing auth_method=external_idp. Only Microsoft login token endpoints are accepted.": "粘贴包含 auth_method=external_idp 的 Kiro CLIProxyAPI 认证 JSON。仅接受 Microsoft 登录令牌端点。", + "Paste the URL from your browser address bar": "从浏览器地址栏粘贴 URL", + "Paste the command into your terminal and press Enter.": "将命令粘贴到终端中并按 Enter。", + "Paste this to your AI:": "将其粘贴到您的 AI:", + "Paste your Kiro API key...": "粘贴您的 Kiro API 密钥...", + "Pause API Key": "暂停 API 密钥", + "Pause key": "暂停密钥", "Paused": "已暂停", - "Please add and connect providers first to configure CLI tools.": "请先添加并连接提供商以配置 CLI 工具。", + "Permissions": "权限", + "Personal Access Token": "个人访问令牌", + "Pick the model that fuses panel answers": "选择用于融合面板回答的模型", "Please add an active Qoder connection first": "请先添加一个活跃的 Qoder 连接", + "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 才能使用此功能。", @@ -338,8 +866,17 @@ "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.": "我们正在完成授权,请稍候。", + "Point your CLI tools to http://localhost:20128": "将您的 CLI 工具指向 http://localhost:20128", + "Pool:": "代理池:", "Popup blocked? Enter URL manually": "弹出窗口被拦截?请手动输入 URL", + "Port 443 Already In Use": "端口 443 已被占用", + "Port 443 is currently used by another process:": "端口 443 当前被另一个进程占用:", + "Powerful Features": "强大功能", "Prefix": "前缀", + "Preset": "预设", + "Prev": "上一页", + "Preview": "预览", + "Previous accounts page": "上一页账号", "Pricing": "定价", "Pricing Configuration": "定价配置", "Pricing Format:": "定价格式:", @@ -347,427 +884,508 @@ "Pricing Settings": "定价设置", "Priority": "优先级", "Privacy Policy": "隐私政策", + "Probing server for tools...": "正在探测服务器工具...", + "Processing...": "处理中...", "Product": "产品", "Production Key": "生产密钥", + "Project Name": "项目名称", + "Prompt": "提示", "Provider": "提供商", + "Provider Details": "提供商详情", "Provider Limits": "提供商限制", + "Provider Response": "提供商响应", "Provider not found": "未找到提供商", + "Provider test failed": "提供商测试失败", "Provider:": "提供商:", "Providers": "提供商", - "Proxy settings applied": "已应用代理设置", + "Proxy": "代理", + "Proxy Action": "代理操作", + "Proxy Pool": "代理池", + "Proxy Pools": "代理池", "Proxy URL": "代理 URL", + "Proxy disabled": "代理已禁用", + "Proxy enabled": "代理已启用", + "Proxy pool created": "代理池已创建", + "Proxy pool deleted": "代理池已删除", + "Proxy pool updated": "代理池已更新", + "Proxy settings applied": "已应用代理设置", + "Proxy test OK": "代理测试通过", + "Proxy test failed": "代理测试失败", + "Proxy test passed": "代理测试通过", "Purpose:": "用途:", + "Python >= 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "本地管理模式需要 Python >= 3.10。请先安装 Python,或使用外部代理 URL。", + "Python ≥ 3.10 required for local managed mode. Install Python first, or use an external proxy URL.": "本地管理模式需要 Python ≥ 3.10。请先安装 Python,或使用外部代理 URL。", + "Quota Tracker": "配额跟踪器", "Qwen": "Qwen", + "Qwen Code supports multiple provider types (openai, anthropic, gemini) via modelProviders in settings.json. 9Router works as an OpenAI-compatible endpoint.": "Qwen Code 通过 settings.json 中的 modelProviders 支持多种提供商类型(openai、anthropic、gemini)。9Router 作为 OpenAI 兼容端点工作。", + "Qwen OAuth free tier was discontinued on 2026-04-15. Use 9Router with alicode/openrouter/anthropic/gemini providers instead.": "Qwen OAuth 免费套餐已于 2026-04-15 停止。请改用带有 alicode/openrouter/anthropic/gemini 提供商的 9Router。", + "Rate Limited": "被限流", + "Read Documentation": "阅读文档", "Reading from AWS SSO cache": "从 AWS SSO 缓存中读取", "Reading from Cursor IDE database": "从 Cursor IDE 数据库读取", + "Ready": "就绪", + "Ready to Simplify Your AI Infrastructure?": "准备简化您的 AI 基础设施?", + "Ready to route! ✓": "准备就绪,可以路由!✓", + "Ready! Requests route automatically through your configured providers.": "就绪!请求将自动通过您配置的提供商路由。", "Reasoning": "推理", "Reasoning:": "推理:", "Recent Requests": "最近的请求", + "Recent chats": "最近聊天", + "Recheck": "重新检查", "Recommended for most users. Free AWS account required.": "推荐给大多数用户。需要免费 AWS 帐户。", - "records, batches every": "记录,每批", + "Record request details for inspection in the logs view": "记录请求详情以在日志视图中查看", + "Redirect URI": "重定向 URI", + "Ref Image (URL)": "参考图片(URL)", "Refresh": "刷新", "Refresh All": "全部刷新", - "Refresh quota": "刷新配额", "Refresh Token": "刷新令牌", + "Refresh all": "全部刷新", + "Refresh quota": "刷新配额", + "Region": "区域", + "Reload Page": "重新加载页面", "Reload VS Code after applying for changes to take effect.": "应用更改后请重新加载 VS Code 以使其生效。", + "Remaining": "剩余", + "Remote": "远程", "Remove": "移除", + "Remove attachment": "移除附件", "Remove custom model": "删除自定义模型", "Remove model": "删除模型", + "Replaces built-in WebSearch/WebFetch. Auto-strips duplicates from tool list.": "替换内置的 WebSearch/WebFetch。自动从工具列表中去除重复项。", + "Replay request flow — matches log files": "重放请求流程——匹配日志文件", + "Request": "请求", "Request Details": "请求详情", "Request Logs": "请求日志", "Requests": "请求", "Requests without a valid key will be rejected": "没有有效密钥的请求将被拒绝", - "requests, max": "请求数,最大", "Require API key": "需要 API 密钥", + "Require OIDC for dashboard access.": "需要 OIDC 才能访问仪表盘。", "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 缓存", + "Required. A friendly label for this node.": "必填。为此节点设置一个友好的显示名称。", + "Required. Used as the provider prefix for model IDs.": "必填。用作模型 ID 的提供商前缀。", + "Requires \"Workers Scripts: Edit\" permission.": "需要 \"Workers Scripts: Edit\" 权限。", + "Requires Cloudflare Account ID and a Workers API Token (Edit Workers permission)": "需要 Cloudflare 账号 ID 和 Workers API 令牌(编辑 Workers 权限)", + "Requires Cursor Pro account to use this feature.": "需要 Cursor Pro 账号才能使用此功能。", + "Requires jcode installed. Install via: curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash": "需要安装 jcode。安装方式:curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash", "Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.": "需要出站端口 7844 (TCP/UDP)。连接可能需要 10-30 秒。", "Reset": "重置", - "Reset to default": "重置为默认值", + "Reset Codex limit?": "重置 Codex 限制?", + "Reset Password to Default": "重置密码为默认值", + "Reset judge to Auto": "重置裁判为自动", + "Reset time": "重置时间", "Reset to Defaults": "重置为默认值", + "Reset to default": "重置为默认值", "Resources": "资源", + "Response": "响应", + "Response Format": "响应格式", + "Responses": "响应", "Responses API": "响应API", + "Restart": "重新启动", + "Restore model": "恢复模型", + "Resume key": "恢复密钥", "Retry": "重试", + "Risk Notice": "风险提示", + "Roo AI Assistant": "Roo AI 助手", + "Rotate providers across requests instead of strict fallback order.": "在请求间轮换提供商,而不是严格的回退顺序。", "Round Robin": "轮询", + "Round Robin — rotate": "轮询 — 轮换", + "Round Robin — rotates models across requests to spread load": "轮询 — 在请求间轮换模型以分散负载", + "Route AI requests through subscription, cheap, and free tiers with auto-fallback. One endpoint for Claude, GPT, Gemini, and more.": "通过订阅、低价和免费层级路由 AI 请求并自动回退。一个端点接入 Claude、GPT、Gemini 等。", + "Route Requests": "路由请求", "Routing Strategy": "路由策略", "Rows:": "行:", + "Run": "运行", + "Run npx command to start the server instantly": "运行 npx 命令立即启动服务器", "Run this command in your terminal, then click": "在终端中运行此命令,然后单击", "Running": "运行中", "Running on your machine": "在你的机器上运行", - "s)": ")", + "Runtime": "运行时", + "SSE URL": "SSE URL", + "START HERE": "从这里开始", + "Save": "保存", + "Save Changes": "保存更改", + "Save Config": "保存配置", "Save Mappings": "保存映射", + "Save auth mode": "保存认证模式", + "Save current Base URL and API key as a browser-local preset": "将当前基础 URL 和 API 密钥保存为浏览器本地预设", "Save this key now!": "立即保存此密钥!", + "Saved": "已保存", + "Saving": "保存中", + "Saving...": "保存中...", + "Scan QR to connect instantly": "扫描二维码即刻连接", + "Scopes": "权限范围", + "Screen sharing": "屏幕共享", + "Scroll down to": "向下滚动到", + "Search by name or description...": "按名称或描述搜索...", + "Search language...": "搜索语言...", "Search model id": "搜索模型 ID", + "Search providers...": "搜索提供商...", + "Search...": "搜索...", "Security": "安全", + "Security required: ": "安全要求:", + "Security risk: no password set. You will be asked to set one when logging in remotely.": "安全风险:未设置密码。远程登录时将要求您设置一个密码。", "Select": "选择", - "Select a provider": "选择提供商", - "Select all": "选择全部", + "Select All": "全选", + "Select Cowork Model": "选择 Cowork 模型", + "Select Endpoint": "选择端点", + "Select Judge Model": "选择裁判模型", + "Select Language": "选择语言", "Select Model": "选择模型", + "Select Model for Cline": "选择 Cline 模型", "Select Model for Codex": "选择 Codex 模型", + "Select Model for DeepSeek TUI": "选择 DeepSeek TUI 模型", "Select Model for Factory Droid": "选择 Factory Droid 模型", "Select Model for GitHub Copilot": "选择 GitHub Copilot 模型", + "Select Model for Hermes Agent": "选择 Hermes Agent 模型", + "Select Model for Kilo Code": "选择 Kilo Code 模型", "Select Model for Open Claw": "选择 Open Claw 模型", "Select Model for OpenCode": "选择 OpenCode 模型", + "Select Model for jcode": "选择 jcode 模型", + "Select Provider": "选择提供商", + "Select Subagent Model for Codex": "选择 Codex 子代理模型", + "Select Subagent Model for OpenCode": "选择 OpenCode 子代理模型", + "Select a provider": "选择提供商", + "Select all": "选择全部", + "Select language": "选择语言", + "Select models to add": "选择要添加的模型", + "Select one or more connections, then click Proxy Action.": "选择一个或多个连接,然后点击代理操作。", + "Select to pre-fill, then edit model ID in the input": "选择以预填充,然后在输入框中编辑模型 ID", + "Select your": "选择您的", + "Selected connections have mixed proxy bindings": "所选连接的代理绑定状态不一致", "Selected only": "仅选定", "Selected provider": "选定的提供商", + "Selecting None will unbind selected connections from proxy pool.": "选择「无」将解除所选连接与代理池的绑定。", + "Send": "发送", "Send to Provider": "发送给提供商", "Sent to provider as:": "发送给提供商:", "Server": "服务器", + "Server Disconnected": "服务器已断开", "Server off": "服务器关闭", + "Server running on": "服务器运行在", + "Service is running in terminal. You can close this web page. Shutdown will stop the service.": "服务正在终端中运行。您可以关闭此网页。关闭将停止服务。", + "Set Password": "设置密码", + "Set a new password before accessing the dashboard remotely.": "在远程访问仪表盘之前设置一个新密码。", + "Set password": "设置密码", + "Setting password for the first time. Leave current password empty or use default:": "首次设置密码。将当前密码留空或使用默认值:", "Setting up": "设置", + "Settings": "设置", + "Settings applied successfully!": "设置已成功应用!", + "Settings reset successfully!": "设置已成功重置!", + "Setup": "设置", + "Setup + index of all capabilities. Start here — covers base URL, auth, model discovery, and links to every capability skill.": "所有功能的设置和索引。从这里开始 — 涵盖基础 URL、认证、模型发现,并链接到每个功能技能。", "Share Endpoint": "共享端点", "Share URL with team members": "与团队成员共享 URL", + "Show": "显示", + "Show all": "显示全部", + "Show key": "显示密钥", "Show only selected models": "仅显示选中的模型", "Showing": "显示中", + "Shutdown": "关闭", + "Sign in with OIDC": "使用 OIDC 登录", + "Simple chat interface to interact with any AI model from connected providers. Select a model and start chatting!": "简单的聊天界面,与已连接提供商的任何 AI 模型交互。选择一个模型开始聊天!", + "Single": "单个", + "Single API endpoint for all major AI providers. Simplify your integration.": "一个 API 端点接入所有主要 AI 提供商。简化集成。", + "Some models are not responding": "部分模型未响应", + "Sort Codex quotas by remaining": "按剩余量排序 Codex 配额", + "Sort accounts by earliest quota reset time": "按最早配额重置时间排序账号", + "Source Body": "源请求体", + "Sourcegraph Amp coding assistant CLI": "Sourcegraph Amp 编程助手 CLI", "Special reasoning/thinking tokens (fallback to output rate)": "特殊推理/思考 Token(回退至输出费率)", + "Speech To Text": "语音转文本", + "Speech-to-Text": "语音转文本", "Standard prompt tokens": "标准提示 Token", - "Start Date": "开始日期", "Start DNS": "启动 DNS", + "Start Date": "开始日期", + "Start Free": "免费开始", + "Start Headroom": "启动 Headroom", + "Start Headroom separately at the configured URL, then recheck.": "在配置的 URL 上单独启动 Headroom,然后重新检查。", "Start MITM": "启动中间人", "Start Server": "启动服务器", "Start Tunnel": "开始隧道", + "Start a conversation": "开始一个对话", + "Starting 9Router...": "正在启动 9Router...", "Status": "状态", "Status:": "状态:", "Step 1: Open this URL in your browser": "第 1 步:在浏览器中打开此 URL", "Step 2: Paste the callback URL here": "第 2 步:将回调 URL 粘贴到此处", "Sticky Limit": "粘性限制", + "Sticky:": "粘滞:", + "Stop": "停止", "Stop DNS": "停止 DNS", + "Stop Headroom": "停止 Headroom", "Stop MITM": "停止中间人", "Stop Server": "停止服务器", "Stopped": "已停止", + "Strict Proxy": "严格代理", + "Subagent Model": "子代理模型", "Sudo Password Required": "需要 sudo 密码", + "Sudo password is required": "需要 sudo 密码", + "Suggested free models (≥200k context):": "推荐的免费模型(≥200k 上下文):", + "Suggested shorthand examples: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929.": "建议的简写示例:g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929。", + "Support up to 20 active apps & 50 custom domains": "支持最多 20 个活跃应用和 50 个自定义域名", + "Supported formats: protocol://user:pass@host:port, host:port:user:pass": "支持的格式:protocol://user:pass@host:port, host:port:user:pass", + "Sync settings across devices with optional cloud storage.": "通过可选的云存储在设备间同步设置。", + "System": "系统", + "TTFT:": "TTFT:", + "Tailscale": "Tailscale", + "Tailscale Funnel": "Tailscale Funnel", + "Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working.": "Tailscale Funnel 将停止。通过 Tailscale URL 的远程访问将停止工作。", + "Tailscale installed": "Tailscale 已安装", + "Tailscale is not installed. Install it to enable Funnel.": "Tailscale 未安装。请安装它以启用 Funnel。", + "Target Request": "目标请求", + "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com.": "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com。", + "Temperature": "温度", + "Terminal": "终端", "Terms of Service": "服务条款", + "Terse-style system prompt → ~65% fewer output tokens (up to 87%)": "简洁风格系统提示 → ~减少 65% 的输出 Token(最高 87%)", "Test": "测试", + "Test Again": "再次测试", + "Test All": "全部测试", + "Test Example": "测试示例", + "Test Results": "测试结果", "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 connection": "测试连接", "Test model": "测试模型", + "Test proxy": "测试代理", "Test proxy URL": "测试代理 URL", - "Test Results": "测试结果", + "Testing...": "测试中...", + "Text To Speech": "文本转语音", + "Text To Speech combo": "文本转语音组合", + "Text to Image": "文本转图像", + "Text to Image combo": "文本转图像组合", + "Text-to-Speech": "文本转语音", + "Text-to-image via DALL-E, Imagen, FLUX, MiniMax, SDWebUI…": "通过 DALL-E、Imagen、FLUX、MiniMax、SDWebUI 等进行文本到图像生成。", + "The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working.": "Cloudflare 隧道将被断开。通过隧道 URL 的远程访问将停止工作。", + "The proxy server has been stopped.": "代理服务器已停止。", + "The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly.": "请求由 OpenAI、Anthropic、Gemini 或其他提供商即时响应。", "The tunnel will be disconnected. Remote access will stop working.": "隧道将被断开。远程访问将停止工作。", + "The unified endpoint for AI generation. Connect, route, and manage your AI providers with ease.": "统一的 AI 生成端点。轻松连接、路由和管理您的 AI 提供商。", + "The unified interface for modern AI infrastructure": "现代 AI 基础设施的统一接口", "The unified interface for modern AI infrastructure. Secure, observable, and scalable.": "现代人工智能基础设施的统一接口。安全、可观察且可扩展。", + "Theme": "主题", + "Thinking": "思考", "Thinking Process": "思考过程", "This is the only time you will see this key. Store it securely.": "这是您唯一一次看到此密钥的机会。请妥善保管。", + "This provider is ready to use.": "此提供商已准备就绪。", + "This provider is ready to use. Optionally route requests through a proxy pool to bypass IP-based limits.": "此提供商已准备就绪。可选择通过代理池路由请求以绕过 IP 限制。", + "This value is write-only after saving.": "此值保存后仅可写入。", "Timestamp": "时间戳", "Timestamp:": "时间戳:", "To get a fresh API key, paste your browser cookie from": "要获取新的 API 密钥,请粘贴您的浏览器 cookie", - "to verify.": "来验证。", + "Today": "今天", "Toggle DNS to redirect": "切换 DNS 重定向", - "Token auto-detected from Kiro IDE successfully!": "已成功从 Kiro IDE 自动检测到令牌!", + "Toggle auto-ping": "切换自动 Ping", + "Token Saver": "Token 节省器", "Token Types:": "Token 类型:", + "Token auto-detected from Kiro IDE successfully!": "已成功从 Kiro IDE 自动检测到令牌!", + "Token is used once for deployment and not stored.": "令牌仅用于部署一次,不会被存储。", + "Token is used once for deployment, not stored. Found in Organization Settings.": "令牌仅用于部署一次,不会被存储。可在组织设置中找到。", "Token will be auto-filled...": "令牌将自动填充...", "Tokens": "Token", "Tokens auto-detected from Cursor IDE successfully!": "已成功从 Cursor IDE 自动检测到令牌!", "Tokens used to create cache entries (fallback to input rate)": "用于创建缓存条目的 Token(回退至输入费率)", + "Tomorrow": "明天", + "Tool not found or disabled.": "未找到或工具已禁用。", + "Tools": "工具", + "Tools:": "工具:", + "Total Cost": "总成本", "Total Input Tokens": "输入 Token 总计", "Total Models": "模型总数", "Total Requests": "请求总数", + "Total Tokens": "总 Token", "Total:": "总计:", - "traffic through 9Router via MITM.": "通过 MITM 通过 9Router 的流量。", + "Track and manage your API quota limits": "跟踪和管理您的 API 配额限制", + "Track token usage, costs, and performance across all providers.": "跟踪所有提供商的 Token 使用量、成本和性能。", + "Transcribe audio via OpenAI Whisper, Groq, Gemini, Deepgram, AssemblyAI…": "通过 OpenAI Whisper、Groq、Gemini、Deepgram、AssemblyAI 等转录音频。", + "Transferring data...": "数据传输中...", + "Translator": "翻译器", "Translator Debug": "翻译器调试", + "Tried in order (top-down) or rotated when round-robin is on.": "按顺序尝试(从上到下)或开启轮询时轮换。", + "Trust Cert": "信任证书", + "Trusted": "已信任", "Try Again": "再试一次", + "Tunnel": "隧道", "Tunnel connected!": "隧道连通!", "Tunnel disabled": "隧道已禁用", + "Turn off Empty": "关闭空账号", + "Turn on Available": "开启可用账号", "Turn request detail recording on/off globally": "全局打开/关闭请求详细信息记录", "Twitter": "Twitter", + "URL → markdown / text / HTML via Firecrawl, Jina, Tavily, Exa.": "URL → 通过 Firecrawl、Jina、Tavily、Exa 转换为 markdown / text / HTML。", "Unavailable": "不可用", + "Under": "在", + "Unified Endpoint": "统一端点", "Unknown": "未知", "Unselect all": "取消选择全部", "Update": "更新", - "Usage by Account": "按账号统计", + "Update 9Router": "更新 9Router", + "Update Password": "修改密码", + "Update now": "立即更新", + "Upstream Auth Error": "上游认证错误", + "Upstream Unavailable": "上游不可用", + "Usage": "使用情况", + "Usage & Analytics": "使用量和分析", + "Usage / Limit": "使用量/限制", + "Usage Logs": "使用日志", + "Usage Tracking": "使用量跟踪", "Usage by API Key": "按 API 密钥统计", + "Usage by Account": "按账号统计", "Usage by Endpoint": "按端点统计", "Usage by Model": "按模型统计", + "Usage:": "用法:", + "Use 9Router model aliases to keep Amp shorthand mappings stable across provider updates.": "使用 9Router 模型别名来保持 Amp 简写映射在提供商更新时稳定。", "Use Antigravity IDE & GitHub Copilot → with ANY provider/model from 9Router": "使用 Antigravity IDE 和 GitHub Copilot → 与 9Router 的任何提供商/模型", + "Use Authentik or any OIDC provider to sign in to the dashboard.": "使用 Authentik 或任何 OIDC 提供商登录仪表盘。", + "Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys.": "使用 Authentik 或任何 OIDC 提供商登录仪表盘。您可以启用仅密码、仅 OIDC 或两者同时启用;模型 API 访问仍使用 API 密钥。", + "Use a GitLab OAuth application": "使用 GitLab OAuth 应用", + "Use a GitLab PAT with api scope": "使用具有 api 范围的 GitLab PAT", + "Use a direct xAI API key from console.x.ai. This is separate from Grok Build OAuth.": "使用来自 console.x.ai 的直接 xAI API 密钥。这与 Grok Build OAuth 是分开的。", + "Use a local proxy for Start/Stop, or an external Docker sidecar like http://headroom:8787.": "使用本地代理进行启动/停止,或使用外部 Docker sidecar 如 http://headroom:8787。", + "Use a long-lived Kiro/CodeWhisperer API key (headless auth).": "使用长期有效的 Kiro/CodeWhisperer API 密钥(无头认证)。", "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", + "Valid": "有效", + "Vectors for RAG / semantic search via OpenAI, Gemini, Mistral…": "通过 OpenAI、Gemini、Mistral 等为 RAG/语义搜索提供向量。", + "Vercel API Token": "Vercel API Token", + "Vercel Relay": "Vercel Relay", + "Vercel serves millions of apps — providers can't block Vercel IPs without affecting legitimate traffic": "Vercel 服务数百万应用 — 提供商无法在不影响合法流量的情况下封禁 Vercel IP", "Verification URL": "验证 URL", + "Video": "视频", + "View Codex reset credit expiry": "查看 Codex 重置信用有效期", "View Full Details": "查看完整详情", + "View on GitHub": "在 GitHub 上查看", "Visit the URL below and enter the code:": "访问以下网址并输入代码:", + "Visit the login URL below and authorize:": "访问下面的登录 URL 并进行授权:", + "Voice": "语音", + "Voice ID": "语音 ID", + "Voyage AI": "Voyage AI", "Waiting for Authorization": "等待授权", "Waiting for authorization...": "等待授权...", "Warning": "警告", + "Web Fetch": "Web 抓取", + "Web Fetch & Search": "Web 搜索与抓取", + "Web Search": "Web 搜索", + "Web Search & Fetch (Exa)": "Web 搜索与抓取(Exa)", + "Welcome": "欢迎", + "What is Cloudflare Relay?": "什么是 Cloudflare Relay?", + "What is Deno Relay?": "什么是 Deno Relay?", + "What is Vercel Relay?": "什么是 Vercel Relay?", "When": "时间", "When ON, dashboard requires password. When OFF, access without login.": "当打开时,仪表板需要密码。当关闭时,无需登录即可访问。", + "Windows:": "Windows:", "Windows: Run 9Router terminal as Administrator": "Windows:以管理员身份运行 9Router 终端", "Windows: Run terminal (9Router) as Administrator to enable MITM": "Windows:以管理员身份运行终端 (9Router) 以启用 MITM", + "Worker Name": "Worker 名称", + "Works on any device": "适用于任何设备", "Writes to": "写入到", "You can override default pricing for specific models. Reset to defaults anytime to restore standard rates.": "您可以覆盖特定模型的默认定价。随时重置为默认值以恢复标准费率。", "Your": "你的", + "Your Account Name": "您的账号名称", "Your Code": "你的代码", "Your Kiro account via": "您的 Kiro 帐户通过", + "Your OAuth application client ID": "您的 OAuth 应用客户端 ID", "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.": "服务正在终端中运行。您可以关闭此网页。关闭将停止服务。", - "One Endpoint for": "统一端点,接入", - "All AI Providers": "所有 AI 提供商", - "Route AI requests through subscription, cheap, and free tiers with auto-fallback. One endpoint for Claude, GPT, Gemini, and more.": "通过订阅、低价和免费层级路由 AI 请求并自动回退。一个端点接入 Claude、GPT、Gemini 等。", - "Get Started": "开始使用", - "View on GitHub": "在 GitHub 上查看", - "How 9Router Works": "9Router 工作原理", - "Data flows seamlessly through our intelligent routing system": "数据通过我们的智能路由系统无缝流转", - "1. CLI & SDKs": "1. CLI 和 SDK", + "Your requests start from your favorite tools or our unified SDK. Just change the base URL.": "您的请求从您最熟悉的工具或我们的统一 SDK 发起。只需更改基础 URL。", "Your requests start from your favorite tools — Cursor, Claude, Copilot, or any OpenAI-compatible SDK.": "请求从您常用的工具发起——Cursor、Claude、Copilot 或任何 OpenAI 兼容的 SDK。", - "2. 9Router Hub": "2. 9Router 枢纽", - "Our engine analyzes the prompt and routes through your subscription, cheap, and free provider tiers with automatic fallback.": "我们的引擎分析提示词并通过您的订阅、低价和免费提供商层级路由,自动回退。", - "3. AI Providers": "3. AI 提供商", - "The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly.": "请求由 OpenAI、Anthropic、Gemini 或其他提供商即时响应。", - "Powerful Features": "强大功能", - "Everything you need to manage your AI infrastructure efficiently.": "高效管理 AI 基础设施所需的一切。", - "Unified Endpoint": "统一端点", - "Single API endpoint for all major AI providers. Simplify your integration.": "一个 API 端点接入所有主要 AI 提供商。简化集成。", - "Easy Setup": "简单设置", - "Get started in seconds. Just install, open, and route.": "几秒钟即可上手。安装、打开、路由。", - "Model Fallback": "模型回退", - "Automatically switch between providers when limits are hit.": "当达到限额时自动切换提供商。", - "Usage Tracking": "使用量跟踪", - "Track token usage, costs, and performance across all providers.": "跟踪所有提供商的 Token 使用量、成本和性能。", - "OAuth & API Keys": "OAuth 和 API 密钥", - "Connect via OAuth or API keys. Securely manage credentials.": "通过 OAuth 或 API 密钥连接。安全管理凭据。", - "Cloud Sync": "云端同步", - "Sync settings across devices with optional cloud storage.": "通过可选的云存储在设备间同步设置。", - "CLI Support": "CLI 支持", - "Native CLI tool support for Cursor, Claude, Copilot, and more.": "原生支持 Cursor、Claude、Copilot 等 CLI 工具。", - "Dashboard": "仪表盘", - "Beautiful web dashboard for managing providers and monitoring usage.": "精美的 Web 仪表盘,用于管理提供商和监控使用情况。", - "Get Started in 30 Seconds": "30 秒快速上手", - "Install 9Router": "安装 9Router", - "Open Dashboard": "打开仪表盘", - "Route Requests": "路由请求", - "npm install -g 9router": "npm install -g 9router", - "open http://localhost:9099": "open http://localhost:9099", - "Ready! Requests route automatically through your configured providers.": "就绪!请求将自动通过您配置的提供商路由。", - "How it Works": "工作原理", - "Docs": "文档", - "GitHub": "GitHub", - "Legal": "法律", - "Manage your AI provider connections": "管理您的 AI 提供商连接", - "Model combos with fallback": "模型组合及回退", - "Monitor your API usage, token consumption, and request logs": "监控您的 API 使用量、Token 消耗和请求日志", - "Track and manage your API quota limits": "跟踪和管理您的 API 配额限制", - "Intercept CLI tool traffic and route through 9Router": "拦截 CLI 工具流量并通过 9Router 路由", - "Configure CLI tools": "配置 CLI 工具", - "Manage your proxy pool configurations": "管理您的代理池配置", - "API endpoint configuration": "API 端点配置", - "Manage your preferences": "管理您的偏好设置", - "Live server console output": "服务器实时控制台输出", - "Usage & Analytics": "使用量和分析", - "MITM Proxy": "MITM 代理", - "Translator": "翻译器", - "Media Providers": "媒体提供商", - "Theme": "主题", - "Remote": "远程", - "Logout": "退出登录", - "Change Log": "更新日志", - "Proxy Pools": "代理池", - "MITM": "MITM", - "Loading...": "加载中...", - "Enter your password to access the dashboard": "输入密码以访问仪表盘", - "Password": "密码", - "Enter password": "输入密码", - "Login": "登录", - "Default password is 123456": "默认密码为 123456", - "Invalid password": "密码错误", - "An error occurred. Please try again.": "发生错误,请重试。", - "light": "浅色", - "dark": "深色", - "system": "跟随系统", - "Combo Round Robin": "组合轮询", - "Cycle through providers in combos instead of always starting with first": "在组合中循环使用提供商,而不是总是从第一个开始", - "Currently using accounts in priority order (Fill First).": "当前按优先级顺序使用账号(优先填满)。", - "Record request details for inspection in the logs view": "记录请求详情以在日志视图中查看", - "Update Password": "修改密码", - "Set Password": "设置密码", - "Overview": "概览", - "Details": "详情", - "Search...": "搜索...", - "Saving...": "保存中...", - "Save": "保存", - "Save Changes": "保存更改", - "Saving": "保存中", - "Importing...": "导入中...", - "Import": "导入", - "Deploying... (may take ~1 min)": "部署中...(可能需要约 1 分钟)", - "Deploy": "部署", - "Enable": "启用", - "Disable": "禁用", - "Token Saver": "Token 节省器", - "Experimental": "实验性", - "Compress tool output to reduce token usage.": "压缩工具输出以减少 Token 使用量。", - "sk_9router (default)": "sk_9router(默认)", - "Install Tailscale": "安装 Tailscale", - "Installing Tailscale...": "正在安装 Tailscale...", - "Tailscale installed": "Tailscale 已安装", - "Tailscale Funnel": "Tailscale Funnel", - "Allow dashboard access via tunnel": "允许通过隧道访问仪表盘", - "Disconnected from server": "与服务器断开连接", - "Attempting to reconnect...": "正在尝试重新连接...", - "Click to retry": "点击重试", - "Failed to load changelog:": "加载更新日志失败:", - "Copied!": "已复制!", - "Manage reusable per-connection proxies and bind them to provider connections.": "管理可复用的连接代理并绑定到提供商连接。", - "Vercel Relay": "Vercel Relay", - "Batch Import": "批量导入", - "Add Proxy Pool": "添加代理池", - "No proxy pool entries yet": "暂无代理池条目", - "Create a proxy pool entry, then assign it to connections.": "创建代理池条目,然后分配到连接。", - "Batch Import Proxies": "批量导入代理", - "Paste Proxy List (One per line)": "粘贴代理列表(每行一个)", - "Supported formats: protocol://user:pass@host:port, host:port:user:pass": "支持的格式:protocol://user:pass@host:port, host:port:user:pass", - "Deploy Vercel Relay": "部署 Vercel Relay", - "What is Vercel Relay?": "什么是 Vercel Relay?", - "Deploys an edge relay function to Vercel that proxies requests through Vercel's network.": "将边缘中继函数部署到 Vercel,通过 Vercel 的网络代理请求。", - "Vercel API Token": "Vercel API Token", - "Project Name": "项目名称", - "Edit Proxy Pool": "编辑代理池", - "Strict Proxy": "严格代理", - "Fail request if proxy is unreachable instead of falling back to direct.": "当代理不可达时直接失败,而不是回退到直连。", - "Inactive pools are ignored by runtime resolution.": "未激活的代理池将被运行时解析忽略。", + "account has been connected.": "账号已连接。", "active": "活跃", - "inactive": "未激活", - "unknown": "未知", + "add OpenAI/Anthropic compatible endpoints": "添加 OpenAI/Anthropic 兼容端点", + "added)": "已添加)", + "again after install.": "安装后再次运行。", + "and click": "并点击", + "apiKey": "apiKey", + "below.": "下方。", "bound": "已绑定", - "Last tested:": "上次测试:", - "No proxy:": "无代理:", - "Proxy pool updated": "代理池已更新", - "Proxy pool created": "代理池已创建", - "Proxy pool deleted": "代理池已删除", - "Proxy test passed": "代理测试通过", - "Proxy test failed": "代理测试失败", - "Replay request flow — matches log files": "重放请求流程——匹配日志文件", - "Client Request": "客户端请求", - "Source Body": "源请求体", - "OpenAI Intermediate": "OpenAI 中间格式", - "Target Request": "目标请求", - "Provider Response": "提供商响应", - "OpenAI Response": "OpenAI 响应", - "Client Response": "客户端响应", - "Format": "格式化", - "Send": "发送", - "→ OpenAI": "→ OpenAI", - "→ Target": "→ 目标", - "Terminal": "终端", - "Full shell access": "完整 Shell 访问", - "Desktop": "桌面", - "Screen sharing": "屏幕共享", - "Files": "文件", - "Browse & edit files": "浏览和编辑文件", - "Scan QR to connect instantly": "扫描二维码即刻连接", - "No port forwarding needed": "无需端口转发", - "Works on any device": "适用于任何设备", - "Access your terminal, desktop & files from anywhere": "从任何地方访问您的终端、桌面和文件", - "Get 9Remote": "获取 9Remote", - "Manual configuration is still available if 9router is deployed on a remote server.": "如果 9router 部署在远程服务器上,仍可使用手动配置。", - "How to Install": "如何安装", - "Hide": "隐藏", - "Filter naming": "过滤命名", - "Filter naming requests": "过滤命名请求", - "Intercepts Claude Code's topic-naming requests and returns a fake response locally, saving API tokens.": "拦截 Claude Code 的主题命名请求并在本地返回伪响应,节省 API Token。", - "Settings applied successfully!": "设置已成功应用!", - "Failed to apply settings": "应用设置失败", - "Settings reset successfully!": "设置已成功重置!", - "Failed to reset settings": "重置设置失败", - "No API keys - Create one in Keys page": "暂无 API 密钥 - 请在密钥页面创建", - "Subagent Model": "子代理模型", - "Select Subagent Model for Codex": "选择 Codex 子代理模型", - "Select Subagent Model for OpenCode": "选择 OpenCode 子代理模型", - "No models selected": "未选择模型", - "Click a model to set/clear active": "点击模型以设置/取消活跃状态", - "Select models to add": "选择要添加的模型", - "Add Model for OpenCode": "为 OpenCode 添加模型", - "Default Model": "默认模型", - "9Router Base URL": "9Router 基础 URL", - "Trust Cert": "信任证书", - "Trusted": "已信任", - "not detected locally": "未在本地检测到", - "Select to pre-fill, then edit model ID in the input": "选择以预填充,然后在输入框中编辑模型 ID", - "Free & Free Tier Providers": "免费及免费额度提供商", - "Testing...": "测试中...", - "Test All": "全部测试", - "Ready": "就绪", - "Valid": "有效", - "Invalid": "无效", - "Checking...": "检查中...", - "Check": "检查", - "Creating...": "创建中...", - "Network error": "网络错误", - "Provider test failed": "提供商测试失败", - "Enable provider": "启用提供商", - "Disable provider": "禁用提供商", - "Chat": "对话", - "Responses": "响应", - "passed": "通过", - "failed": "失败", - "tested": "已测试", - "Required. A friendly label for this node.": "必填。为此节点设置一个友好的显示名称。", - "Required. Used as the provider prefix for model IDs.": "必填。用作模型 ID 的提供商前缀。", - "Model ID (optional)": "模型 ID(可选)", - "If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead.": "如果提供商不支持 /models 端点,请输入模型 ID 通过 chat/completions 进行验证。", - "(via inference test)": "(通过推理测试)", - "Delete this combo?": "删除此组合?", - "Name is required": "名称为必填项", - "Failed to create combo": "创建组合失败", - "Failed to update combo": "更新组合失败", - "Only letters, numbers, -, _ and . allowed": "仅允许使用字母、数字、-、_ 和 .", - "Input Cost": "输入成本", - "Output Cost": "输出成本", - "Total Cost": "总成本", - "Total Tokens": "总 Token", - "Never": "从未", - "Just now": "刚刚", - "m ago": "分钟前", - "h ago": "小时前", - "None": "无", - "disabled": "已禁用", - "OAuth Account": "OAuth 账号", - "no_proxy:": "无代理:", - "Pool:": "代理池:", - "Legacy:": "旧版:", - "Error": "错误", - "more": "更多", - "Proxy": "代理", - "No authentication required": "无需身份验证", - "This provider is ready to use.": "此提供商已准备就绪。", - "Available Models": "可用模型", - "Model not reachable": "模型不可达", - "Failed to set alias": "设置别名失败", - "Delete this connection?": "删除此连接?", - "Proxy Pool": "代理池", - "Proxy Action": "代理操作", - "Selecting None will unbind selected connections from proxy pool.": "选择「无」将解除所选连接与代理池的绑定。", - "Applying...": "应用中...", - "Select one or more connections, then click Proxy Action.": "选择一个或多个连接,然后点击代理操作。", - "All selected currently unbound": "所有选中项当前未绑定", - "Selected connections have mixed proxy bindings": "所选连接的代理绑定状态不一致", - "Anthropic Compatible Details": "Anthropic 兼容详情", - "OpenAI Compatible Details": "OpenAI 兼容详情", - "Messages API": "消息 API", - "Sticky:": "粘滞:", + "chars)": "字符)", + "cloudflare relay": "cloudflare 中继", "connection": "个连接", "connections": "个连接", - "Suggested free models (≥200k context):": "推荐的免费模型(≥200k 上下文):", - "Get API Key →": "获取 API 密钥 →", - "OAuth": "OAuth", - "Click to add, click again to remove. Changes are saved automatically.": "点击添加,再次点击删除。更改将自动保存。", - "Close": "关闭", - "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ 风险提示:此提供商使用的订阅/OAuth 会话未获官方授权用于代理/路由器使用。账户可能被限制或封禁。使用风险自负。", + "daily-cloudcode-pa.googleapis.com": "daily-cloudcode-pa.googleapis.com", + "dark": "深色", + "disabled": "已禁用", + "dollars per million tokens": "美元 / 百万 Token", + "e.g. CwhRBWXzGAHq8TQ4Fs17": "例如 CwhRBWXzGAHq8TQ4Fs17", + "e.g. claude-opus-4-5": "例如 claude-opus-4-5", + "e.g. my-model-id": "例如 my-model-id", + "e.g. tts-1-hd": "例如 tts-1-hd", + "e.g. voyage-3, embed-english-v3.0, text-embedding-3-small": "例如 voyage-3, embed-english-v3.0, text-embedding-3-small", + "e.g., Production API, Dev Environment": "例如,生产 API、开发环境", + "every request bills all panel models + the judge": "每次请求会计费所有面板模型 + 裁判", + "export": "导出", + "failed": "失败", + "git/grep/ls/tree/logs → 60-90% fewer input tokens": "git/grep/ls/tree/logs → 减少 60-90% 的输入 Token", + "h ago": "小时前", + "has been connected.": "已连接。", + "iFlow AI": "iFlow AI", + "iFlow Cookie Authentication": "iFlow Cookie 身份验证", + "import": "导入", + "inactive": "未激活", + "jcode - Manual Configuration": "jcode - 手动配置", + "jcode CLI not detected locally": "未在本地检测到 jcode CLI", + "jcode is a Rust-based coding agent with semantic memory, multi-agent swarms, and extreme performance (27.8 MB RAM, 14ms boot).": "jcode 是基于 Rust 的编程代理,具有语义记忆、多代理集群和极致性能(27.8 MB 内存,14ms 启动)。", + "kiro://kiro.kiroAgent/authenticate-success?code=...": "kiro://kiro.kiroAgent/authenticate-success?code=...", + "light": "浅色", + "m ago": "分钟前", + "macOS / Linux / Windows:": "macOS / Linux / Windows:", + "macOS / Linux:": "macOS / Linux:", + "macOS/Linux:": "macOS/Linux:", + "more": "更多", + "more providers": "更多提供商", + "ms / Total": "毫秒/总计", + "name|apiKey": "name|apiKey", + "no_proxy:": "无代理:", + "not detected locally": "未在本地检测到", + "npm install -g 9router": "npm install -g 9router", + "npx 9router": "npx 9router", + "open http://localhost:9099": "open http://localhost:9099", + "openid profile email": "openid profile email", + "optional context to improve accuracy": "可选的上下文,用于提高准确性", + "or VS Code extension marketplace.": "或 VS Code 扩展市场。", + "or just": "或仅", + "passed": "通过", + "platform.iflow.cn": "platform.iflow.cn", + "queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "并行查询所有模型,然后由裁判综合出一个答案。质量最佳但成本最高:每次请求会计费所有面板模型 + 裁判(N+1 次调用)", + "records, batches every": "记录,每批", + "requests, max": "请求数,最大", + "rotates models across requests to spread load": "在请求间轮换模型以分散负载", + "s)": ")", + "s...": "秒...", + "seconds...": "秒...", + "sends image/PDF/audio requests to a model that supports them first": "将图片/PDF/音频请求优先发送到支持的模型", + "sk-...": "sk-...", + "sk_9router (default)": "sk_9router(默认)", + "system": "跟随系统", + "tested": "已测试", + "the database.": "数据库。", + "to apply changes": "以使更改生效", + "to verify.": "来验证。", + "traffic through 9Router via MITM.": "通过 MITM 通过 9Router 的流量。", + "tries models in order (next on failure)": "按顺序尝试模型(失败时切换到下一个)", + "unknown": "未知", + "v1.0 is now live": "v1.0 现已上线", + "vercel relay": "vercel 中继", + "yet.": "。", + "your-org.deno.net": "your-org.deno.net", + "© 2025 9Router. All rights reserved.": "© 2025 9Router。保留所有权利。", + "— queries all models in parallel, then a judge synthesizes one answer. Best quality, but costs the most: every request bills all panel models + the judge (N+1 calls)": "— 并行查询所有模型,然后由裁判综合出一个答案。质量最佳但成本最高:每次请求会计费所有面板模型 + 裁判(N+1 次调用)", + "— rotates models across requests to spread load": "— 在请求间轮换模型以分散负载", + "— sends image/PDF/audio requests to a model that supports them first": "— 将图片/PDF/音频请求优先发送到支持的模型", + "— tries models in order (next on failure)": "— 按顺序尝试模型(失败时切换到下一个)", + "→ OpenAI": "→ OpenAI", + "→ Target": "→ 目标", + "→ localhost": "→ localhost", + "⚠️ Enable DNS to edit model mappings": "⚠️ 启用 DNS 以编辑模型映射", + "⚠️ Local plugins run as subprocess via": "⚠️ 本地插件通过子进程运行", "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM 通过本地 CA 拦截 IDE 工具(Antigravity、GitHub Copilot、Kiro)的 HTTPS 流量,将请求重定向到您的提供商。可能违反 ToS → 账户封禁风险。使用风险自负。", - "Endpoint is exposed without an API key.": "端点未设置 API 密钥即对外暴露。" + "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ 风险提示:此提供商使用的订阅/OAuth 会话未获官方授权用于代理/路由器使用。账户可能被限制或封禁。使用风险自负。", + "✓ Confirm Add": "✓ 确认添加", + "📝 Configure providers in dashboard or use environment variables": "📝 在仪表盘中配置提供商或使用环境变量", + "🔐 OAuth required. Add now and authenticate after Apply; tool list will be discovered after first connect.": "🔐 需要 OAuth。立即添加并在应用后认证;工具列表将在首次连接后自动发现。" } diff --git a/public/providers/grok-cli.png b/public/providers/grok-cli.png new file mode 100644 index 00000000..ef9d7abc Binary files /dev/null and b/public/providers/grok-cli.png differ diff --git a/skills/9router-video/SKILL.md b/skills/9router-video/SKILL.md new file mode 100644 index 00000000..37ea13b5 --- /dev/null +++ b/skills/9router-video/SKILL.md @@ -0,0 +1,76 @@ +--- +name: 9router-video +description: Generate videos via 9Router /v1/videos/generations using xAI Grok Imagine (grok-imagine-video). Async job flow - submit, poll request_id until done, download MP4. Use when the user wants to create, generate, or render a video, text-to-video (txt2vid), or image-to-video. +--- + +# 9Router — Video Generation (xAI Grok Imagine) + +Requires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup. + +Requires a connected **xAI account** in the 9Router dashboard — either **Grok Build OAuth** (SuperGrok / X Premium+ subscription sign-in) or a direct **xAI API key** from console.x.ai. The two are separate auth types with separate billing; the dashboard shows which one each connection uses. + +## Endpoints (async job flow) + +Video generation is **asynchronous**: the POST returns a `request_id` immediately, then you poll until the job is `done` or `failed`. + +| Endpoint | Purpose | +|---|---| +| `POST /v1/videos/generations` | text-to-video / image-to-video | +| `POST /v1/videos/edits` | edit an existing video | +| `POST /v1/videos/extensions` | extend an existing video | +| `GET /v1/videos/{request_id}` | poll job status | + +Request fields (passed through to xAI unchanged — see https://docs.x.ai/developers/rest-api-reference/inference/videos): + +| Field | Required | Notes | +|---|---|---| +| `model` | no | `xai/grok-imagine-video` (prefix is stripped before upstream) | +| `prompt` | yes for T2V | video description | +| `duration` | no | seconds | +| `aspect_ratio` | no | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `3:2`, `2:3` | +| `resolution` | no | `480p`, `720p`, `1080p` | +| `image` | no | `{ "url": "https://… or data:image/…;base64,…" }` for image-to-video | +| `video` | edits/extensions | `{ "url": "…mp4" }` or `{ "file_id": "…" }` | + +## Examples + +Submit a job: + +```bash +curl -X POST "$NINEROUTER_URL/v1/videos/generations" \ + -H "Authorization: Bearer $NINEROUTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{"model":"xai/grok-imagine-video","prompt":"A cinematic tracking shot through a neon city at night","duration":8,"aspect_ratio":"16:9","resolution":"720p"}' +# → {"request_id":"abc123"} (response header x-9router-connection-id: ) +``` + +Poll until done (echo the connection header back so the same account polls the job): + +```bash +curl "$NINEROUTER_URL/v1/videos/abc123" \ + -H "Authorization: Bearer $NINEROUTER_KEY" \ + -H "x-connection-id: " +# → {"status":"pending","progress":42} +# → {"status":"done","video":{"url":"https://…mp4","duration":8},"model":"grok-imagine-video"} +# → {"status":"failed","error":{"code":"…","message":"…"}} +``` + +Download: fetch `video.url` from the `done` response. + +## CLI one-shot + +```bash +9router xai video \ + --prompt "A cinematic tracking shot through a neon city at night" \ + --output video.mp4 +# options: --model --duration --aspect-ratio --resolution --image --timeout --port --api-key +``` + +Submits, polls with progress, downloads to `video.mp4.part`, atomically renames on success. Ctrl+C cancels cleanly; non-zero exit on failure. + +## Notes & limits + +- Jobs are **account-bound** upstream: poll with the same connection that created the job (`x-connection-id` header, value from the create response's `x-9router-connection-id`). +- Creation POSTs are **never auto-retried** (a retry could create and bill two videos). Only a 401→token-refresh→single-retry is performed, which upstream rejects before job creation. +- Video models are tagged `kind: "video"` and are excluded from chat model lists and chat fallback combos. +- Grok Build **subscription OAuth** tokens are sent to the same `api.x.ai/v1/videos` endpoints as API keys; whether a given subscription tier includes video-generation quota is controlled by xAI and is not verified by 9Router — a `403`/`permission_denied` from upstream means the connected account has no video access. diff --git a/skills/README.md b/skills/README.md index f9f06b90..f3c0062e 100644 --- a/skills/README.md +++ b/skills/README.md @@ -11,6 +11,7 @@ Drop-in skills for any AI agent (Claude, Cursor, ChatGPT, custom SDK). Just **co | **Entry / Setup** (start here) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md | | Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md | | Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md | +| Video generation (xAI Grok Imagine) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-video/SKILL.md | | Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md | | Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md | | Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md | diff --git a/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js b/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js index fa8d7111..2e735647 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js +++ b/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js @@ -9,7 +9,7 @@ import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, HermesToolCard, DefaultToolCard, OpenCodeToolCard, CoworkToolCard, CopilotToolCard, ClineToolCard, KiloToolCard, DeepSeekTuiToolCard, - JcodeToolCard, + JcodeToolCard, GrokBuildToolCard, } from "../components"; const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; @@ -139,6 +139,8 @@ export default function ToolDetailClient({ toolId, machineId }) { return ; case "jcode": return ; + case "grok-build": + return ; default: return ; } diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/GrokBuildToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/GrokBuildToolCard.js new file mode 100644 index 00000000..cc72ea4a --- /dev/null +++ b/src/app/(dashboard)/dashboard/cli-tools/components/GrokBuildToolCard.js @@ -0,0 +1,387 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; +import Image from "next/image"; +import BaseUrlSelect from "./BaseUrlSelect"; +import ApiKeySelect from "./ApiKeySelect"; +import { matchKnownEndpoint } from "./cliEndpointMatch"; + +const ENDPOINT = "/api/cli-tools/grok-build-settings"; +const MODEL_SLOT = "9router"; + +export default function GrokBuildToolCard({ + tool, + isExpanded, + onToggle, + baseUrl, + hasActiveProviders, + apiKeys, + activeProviders, + cloudEnabled, + initialStatus, + tunnelEnabled, + tunnelPublicUrl, + tailscaleEnabled, + tailscaleUrl, +}) { + const [grokStatus, setGrokStatus] = 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 [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 (!grokStatus?.installed) return null; + const cfg = grokStatus.settings?.model; + if (!cfg?.base_url) return "not_configured"; + if (matchKnownEndpoint(cfg.base_url, { tunnelPublicUrl, tailscaleUrl })) return "configured"; + return "other"; + }; + + const configStatus = getConfigStatus(); + + useEffect(() => { + if (apiKeys?.length > 0 && !selectedApiKey) { + setSelectedApiKey(apiKeys[0].key); + } + }, [apiKeys, selectedApiKey]); + + useEffect(() => { + if (initialStatus) setGrokStatus(initialStatus); + }, [initialStatus]); + + useEffect(() => { + if (isExpanded && !grokStatus) { + checkStatus(); + 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 (grokStatus?.installed && !hasInitializedModel.current) { + hasInitializedModel.current = true; + const cfg = grokStatus.settings?.model; + if (cfg?.model) setSelectedModel(cfg.model); + } + }, [grokStatus]); + + const checkStatus = async () => { + setChecking(true); + try { + const res = await fetch(ENDPOINT); + const data = await res.json(); + setGrokStatus(data); + } catch (error) { + setGrokStatus({ installed: false, error: error.message }); + } finally { + setChecking(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 handleApply = 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(ENDPOINT, { + 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(ENDPOINT, { 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 handleModelSelect = (model) => { + setSelectedModel(model.value); + setModalOpen(false); + }; + + const getManualConfigs = () => { + const keyToUse = (selectedApiKey && selectedApiKey.trim()) + ? selectedApiKey + : (!cloudEnabled ? "sk_9router" : ""); + + const modelId = selectedModel || "provider/model-id"; + const tomlContent = `[models] +default = "${MODEL_SLOT}" + +[model.${MODEL_SLOT}] +model = "${modelId}" +base_url = "${getEffectiveBaseUrl()}" +name = "9Router" +description = "Routed via 9Router gateway" +api_backend = "chat_completions" +api_key = "${keyToUse}" +`; + + return [ + { filename: "~/.grok/config.toml", content: tomlContent }, + ]; + }; + + 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 Grok Build... +
+ )} + + {!checking && grokStatus && !grokStatus.installed && ( +
+
+
+ warning +
+

Grok Build not detected locally

+

Install:

+ curl -fsSL https://x.ai/cli/install.sh | bash +

Manual configuration is still available if 9router is deployed on a remote server.

+
+
+
+ +
+
+
+ )} + + {!checking && grokStatus?.installed && ( + <> +
+ {tool.notes && tool.notes.length > 0 && ( +
+ {tool.notes.map((note, idx) => ( +
+ + {note.type === "warning" ? "warning" : note.type === "error" ? "error" : "info"} + + {note.text} +
+ ))} +
+ )} + +
+ Select Endpoint + arrow_forward + +
+ + {grokStatus?.settings?.model?.base_url && ( +
+ Current + arrow_forward + + {grokStatus.settings.model.base_url} + {grokStatus.settings.model.model ? ` · ${grokStatus.settings.model.model}` : ""} + +
+ )} + +
+ API Key + arrow_forward + +
+ +
+ Default Model + arrow_forward +
+ setSelectedModel(e.target.value)} + placeholder="provider/model-id" + className="w-full min-w-0 pl-2 pr-7 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" + /> + {selectedModel && ( + + )} +
+ +
+
+ + {message && ( +
+ {message.type === "success" ? "check_circle" : "error"} + {message.text} +
+ )} + +
+ + + +
+ + )} +
+ )} + + setModalOpen(false)} + onSelect={handleModelSelect} + selectedModel={selectedModel} + activeProviders={activeProviders} + modelAliases={modelAliases} + title="Select Model for Grok Build" + /> + + setShowManualConfigModal(false)} + title="Grok Build - Manual Configuration" + configs={getManualConfigs()} + /> +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/index.js b/src/app/(dashboard)/dashboard/cli-tools/components/index.js index aeca8700..e1399677 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/index.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/index.js @@ -12,6 +12,7 @@ export { default as ClineToolCard } from "./ClineToolCard"; export { default as KiloToolCard } from "./KiloToolCard"; export { default as DeepSeekTuiToolCard } from "./DeepSeekTuiToolCard"; export { default as JcodeToolCard } from "./JcodeToolCard"; +export { default as GrokBuildToolCard } from "./GrokBuildToolCard"; export { default as MitmServerCard } from "./MitmServerCard"; export { default as MitmToolCard } from "./MitmToolCard"; export { default as MitmLinkCard } from "./MitmLinkCard"; diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 85ebec06..b92f331e 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import PropTypes from "prop-types"; -import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components"; +import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal, ApiExplorerModal } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { TUNNEL_BENEFITS, @@ -21,6 +21,7 @@ export default function APIPageClient({ machineId }) { const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [showAddModal, setShowAddModal] = useState(false); + const [showApiExplorer, setShowApiExplorer] = useState(false); const [showImportModal, setShowImportModal] = useState(false); const [importKeyValue, setImportKeyValue] = useState(""); const [importKeyName, setImportKeyName] = useState(""); @@ -710,10 +711,20 @@ export default function APIPageClient({ machineId }) {
{/* Endpoint Card */} -

- api - API Endpoint -

+
+

+ api + API Endpoint +

+ +
{/* Endpoint rows */}
@@ -1389,6 +1400,12 @@ export default function APIPageClient({ machineId }) { message={confirmState?.message} variant="danger" /> + + {/* API Explorer — list + test all public AI endpoints */} + setShowApiExplorer(false)} + />
); } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js index 77fd9c07..5d19bd10 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js @@ -4,10 +4,11 @@ import { useState } from "react"; import PropTypes from "prop-types"; import { Button, Badge, Input, Modal, Select } from "@/shared/components"; import { AI_PROVIDERS } from "@/shared/constants/providers"; +import { planBulkAdd } from "@/shared/utils/bulkAdd"; const BULK_PLACEHOLDER = `name1|sk-key1\nname2|sk-key2\nsk-key-only-auto-named`; -export default function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, authType, authHint, website, proxyPools, error, onSave, onBulkDone, onClose }) { +export default function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, authType, authHint, website, proxyPools, error, existingNames, onSave, onBulkDone, onClose }) { const NONE_PROXY_POOL_VALUE = "__none__"; const isOllamaLocal = provider === "ollama-local"; const isCookie = authType === "cookie"; @@ -41,6 +42,10 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); const [saving, setSaving] = useState(false); + const bulkPlaceholder = isCloudflareAi + ? `name1|sk-key1|acc123456\nname2|sk-key2|def789012\nsk-key-only-auto-named` + : BULK_PLACEHOLDER; + const [mode, setMode] = useState("single"); // "single" | "bulk" const [bulkText, setBulkText] = useState(""); const [bulkResult, setBulkResult] = useState(null); // { success, failed } @@ -127,22 +132,30 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa }; const handleBulkSubmit = async () => { - const lines = bulkText.split("\n").map(l => l.trim()).filter(Boolean); + const lines = bulkText.split("\n"); if (!lines.length) return; + // Plan collision-free names against existing connections so a generated + // "Key N" never matches a saved name (which the backend would upsert / + // overwrite instead of inserting). See bulkAdd.js for the full rationale. + const plan = planBulkAdd(lines, existingNames, { isCloudflareAi }); + if (!plan.length) return; setSaving(true); setBulkResult(null); let success = 0; let failed = 0; - for (let i = 0; i < lines.length; i++) { - const parts = lines[i].split("|"); - const apiKey = parts.length >= 2 ? parts.slice(1).join("|").trim() : parts[0].trim(); - const baseName = parts.length >= 2 ? parts[0].trim() : "Key"; - const name = `${baseName} ${i + 1}`; + for (const entry of plan) { try { const res = await fetch("/api/providers", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, apiKey, name, priority: 1, testStatus: "unknown" }), + body: JSON.stringify({ + provider, + apiKey: entry.apiKey, + name: entry.name, + priority: 1, + testStatus: "unknown", + ...(entry.providerSpecificData ? { providerSpecificData: entry.providerSpecificData } : {}), + }), }); if (res.ok) success++; else failed++; @@ -168,10 +181,15 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa {mode === "bulk" && (
-

One key per line. Format: name|apiKey or just apiKey (auto-named by index).

+

+ {isCloudflareAi + ? <>One key per line. Format: name|apiKey|accountId or just apiKey (auto-named by index). + : <>One key per line. Format: name|apiKey or just apiKey (auto-named by index). + } +