{"owner":"decolua","repo":"9router","hasSkills":true,"totalSkillsCount":13,"totalTokensCount":17046,"categories":["claude-rule","project-spec","root-instruction","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md","docs/ARCHITECTURE.md","open-sse/AGENTS.md","skills/9router-chat/SKILL.md","skills/9router-embeddings/SKILL.md","skills/9router-image/SKILL.md","skills/9router-stt/SKILL.md","skills/9router-tts/SKILL.md","skills/9router-video/SKILL.md","skills/9router-web-fetch/SKILL.md","skills/9router-web-search/SKILL.md","skills/9router/SKILL.md","skills/README.md"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## What this is\n\n9Router (`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.\n\nTwo published artifacts live in this one repo:\n- The **dashboard + gateway** (root `package.json`, `9router-app`) — the Next.js server that does the actual routing.\n- 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.\n\nThe code lives in `src/` (Next.js app + dashboard/compat APIs), `open-sse/` (the provider-agnostic routing/translation engine), `cli/` (the launcher package), and `tests/`.\n\n## Commands\n\nDashboard/gateway (run from repo root):\n```bash\ncp .env.example .env\nnpm install\nPORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev   # dev (webpack, port 20127 by default via next dev)\nnpm run build && PORT=20128 HOSTNAME=0.0.0.0 npm run start           # production\n```\n- Bun variants: `npm run dev:bun` / `build:bun` / `start:bun`.\n- Default runtime port is **20128** (dashboard at `/dashboard`, API at `/v1`).\n- Lint: `npx eslint .` (config `eslint.config.mjs`, extends `eslint-config-next`).\n\nCLI package (`cli/`):\n```bash\nnpm run cli:pack       # build + npm pack from root\ncd cli && npm run dev  # nodemon watch\n```\n\nTests (vitest, in `tests/`, an **independent** ESM package — not wired into root `npm test`):\n```bash\nnpm install                             # ROOT deps first — tests import from src/ which needs `open`, `undici`, etc.\ncd tests && npm install                 # then tests' own deps (vitest) → tests/node_modules (allowed by tests/.gitignore)\nnpx vitest run                          # all tests; auto-discovers tests/vitest.config.js\nnpx vitest run unit/capabilities.test.js   # single file (path relative to tests/)\n```\n> 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.\n>\n> **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:\n> - 26 catalogued in `tests/__baseline__/known-fails.txt` (rtk, oauth-cursor-auto-import, translator-request-normalization, …).\n> - `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.\n> - `unit/xai-oauth-service.test.js` times out (5s) when the xAI endpoint-discovery fetch isn't reachable/mocked.\n> - `real/*.real.test.js` make live provider calls — need credentials, skip otherwise.\n- `*.real.test.js` under `tests/translator/real/` make live provider calls — skip unless credentials are set.\n- Regression baselines: `tests/__baseline__/verify-*.mjs` compare against committed snapshots (providers, aliases, OAuth URLs). Run these after touching provider registry / alias logic.\n\n## Architecture\n\nTwo authoritative docs already exist — read them before working in these areas rather than re-deriving:\n- `docs/ARCHITECTURE.md` — full system: request lifecycle, combo/account fallback, OAuth + token refresh, cloud sync, data model.\n- `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/`.**\n\n### Request flow (the thing to understand first)\n`src/app/api/v1/*` route (Next rewrite maps `/v1/*` → `/api/v1/*` in `next.config.mjs`)\n→ `src/sse/handlers/chat.js` (parse, combo expansion, account-selection loop)\n→ `open-sse/handlers/chatCore.js` (detect source format, translate request, dispatch to executor, retry/refresh, stream setup)\n→ `open-sse/executors/*` (per-provider upstream call; `default.js` handles any OpenAI-compatible provider)\n→ `open-sse/translator/*` (client format ↔ provider format)\n→ SSE back to client.\n\n`src/sse/` is the app-side entry glue; `open-sse/` is the provider-agnostic engine (also usable standalone). Cross that boundary consciously.\n\n### Translator engine (`open-sse/translator/`)\n- 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`).\n- 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.\n- 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.\n\n### Provider registry (`open-sse/providers/registry/*`)\n- 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.\n- Add a provider: copy `providers/REGISTRY_TEMPLATE.js`, add models to `config/providerModels.js`. Only add an executor for non-OpenAI-compatible upstreams.\n\n### Persistence — IMPORTANT (ARCHITECTURE.md is stale here)\nState 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.\n- `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/`.\n- DB file location resolves via `src/lib/db/paths.js` (`DATA_DIR`, else `~/.9router/`).\n- Usage/logs (`src/lib/usageDb.js`, `usage.json` + `log.txt`) still live under `~/.9router` and do **not** follow `DATA_DIR`.\n\n### RTK token saver (`open-sse/rtk/`)\nPre-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.\n\n## Conventions & gotchas\n\n- Plain JavaScript (ESM), no TypeScript. `@/*` path alias → `src/*` (`jsconfig.json`).\n- `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.\n- 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.\n- 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.\n- **Security-first on PRs**: Security is the top priority when reviewing or creating PRs. Audit authentication, credential/token storage & leaks, header manipulation (`X-Forwarded-For`), and SSRF risks before functional logic. Always include explicit security warnings/notes when reporting PR reviews or changes to the user.\n- Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`).\n","docs/ARCHITECTURE.md":"# 9Router Architecture\n\n_Last updated: 2026-02-06_\n\n## Executive Summary\n\n9Router is a local AI routing gateway and dashboard built on Next.js.\nIt provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic across multiple upstream providers with translation, fallback, token refresh, and usage tracking.\n\nCore capabilities:\n\n- OpenAI-compatible API surface for CLI/tools\n- Request/response translation across provider formats\n- Model combo fallback (multi-model sequence)\n- Account-level fallback (multi-account per provider)\n- OAuth + API-key provider connection management\n- Local persistence for providers, keys, aliases, combos, settings, pricing\n- Usage/cost tracking and request logging\n- Optional cloud sync for multi-device/state sync\n\nPrimary runtime model:\n\n- Next.js app routes under `src/app/api/*` implement both dashboard APIs and compatibility APIs\n- A shared SSE/routing core in `src/sse/*` + `open-sse/*` handles provider execution, translation, streaming, fallback, and usage\n\n## Scope and Boundaries\n\n### In Scope\n\n- Local gateway runtime\n- Dashboard management APIs\n- Provider authentication and token refresh\n- Request translation and SSE streaming\n- Local state + usage persistence\n- Optional cloud sync orchestration\n\n### Out of Scope\n\n- Cloud service implementation behind `NEXT_PUBLIC_CLOUD_URL`\n- Provider SLA/control plane outside local process\n- External CLI binaries themselves (Claude CLI, Codex CLI, etc.)\n\n## High-Level System Context\n\n```mermaid\nflowchart LR\n    subgraph Clients[Developer Clients]\n        C1[Claude Code]\n        C2[Codex CLI]\n        C3[OpenClaw / Droid / Cline / Continue / Roo]\n        C4[Custom OpenAI-compatible clients]\n        BROWSER[Browser Dashboard]\n    end\n\n    subgraph Router[9Router Local Process]\n        API[V1 Compatibility API\\n/v1/*]\n        DASH[Dashboard + Management API\\n/api/*]\n        CORE[SSE + Translation Core\\nopen-sse + src/sse]\n        DB[(db.json)]\n        UDB[(usage.json + log.txt)]\n    end\n\n    subgraph Upstreams[Upstream Providers]\n        P1[OAuth Providers\\nClaude/Codex/Gemini/Qwen/iFlow/GitHub/Kiro/Cursor/Antigravity]\n        P2[API Key Providers\\nOpenAI/Anthropic/OpenRouter/GLM/Kimi/MiniMax]\n        P3[Compatible Nodes\\nOpenAI-compatible / Anthropic-compatible]\n    end\n\n    subgraph Cloud[Optional Cloud Sync]\n        CLOUD[Cloud Sync Endpoint\\nNEXT_PUBLIC_CLOUD_URL]\n    end\n\n    C1 --> API\n    C2 --> API\n    C3 --> API\n    C4 --> API\n    BROWSER --> DASH\n\n    API --> CORE\n    DASH --> DB\n    CORE --> DB\n    CORE --> UDB\n\n    CORE --> P1\n    CORE --> P2\n    CORE --> P3\n\n    DASH --> CLOUD\n```\n\n## Core Runtime Components\n\n## 1) API and Routing Layer (Next.js App Routes)\n\nMain directories:\n\n- `src/app/api/v1/*` and `src/app/api/v1beta/*` for compatibility APIs\n- `src/app/api/*` for management/configuration APIs\n- Next rewrites in `next.config.mjs` map `/v1/*` to `/api/v1/*`\n\nImportant compatibility routes:\n\n- `src/app/api/v1/chat/completions/route.js`\n- `src/app/api/v1/messages/route.js`\n- `src/app/api/v1/responses/route.js`\n- `src/app/api/v1/models/route.js`\n- `src/app/api/v1/messages/count_tokens/route.js`\n- `src/app/api/v1beta/models/route.js`\n- `src/app/api/v1beta/models/[...path]/route.js`\n\nManagement domains:\n\n- Auth/settings: `src/app/api/auth/*`, `src/app/api/settings/*`\n- Providers/connections: `src/app/api/providers*`\n- Provider nodes: `src/app/api/provider-nodes*`\n- OAuth: `src/app/api/oauth/*`\n- Keys/aliases/combos/pricing: `src/app/api/keys*`, `src/app/api/models/alias`, `src/app/api/combos*`, `src/app/api/pricing`\n- Usage: `src/app/api/usage/*`\n- Sync/cloud: `src/app/api/sync/*`, `src/app/api/cloud/*`\n- CLI tooling helpers: `src/app/api/cli-tools/*`\n\n## 2) SSE + Translation Core\n\nMain flow modules:\n\n- Entry: `src/sse/handlers/chat.js`\n- Core orchestration: `open-sse/handlers/chatCore.js`\n- Provider execution adapters: `open-sse/executors/*`\n- Format detection/provider config: `open-sse/services/provider.js`\n- Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js`\n- Account fallback logic: `open-sse/services/accountFallback.js`\n- Translation registry: `open-sse/translator/index.js`\n- Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js`\n- Usage extraction/normalization: `open-sse/utils/usageTracking.js`\n\n## 3) Persistence Layer\n\nPrimary state DB:\n\n- `src/lib/localDb.js`\n- file: `${DATA_DIR}/db.json` (or `~/.9router/db.json` when `DATA_DIR` is unset)\n- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing\n\nUsage DB:\n\n- `src/lib/usageDb.js`\n- files: `~/.9router/usage.json`, `~/.9router/log.txt`\n- note: currently independent from `DATA_DIR`\n\n## 4) Auth + Security Surfaces\n\n- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js`\n- API key generation/verification: `src/shared/utils/apiKey.js`\n- Provider secrets persisted in `providerConnections` entries\n- Optional proxy support for upstream calls via env proxy variables (`open-sse/utils/proxyFetch.js`)\n\n## 5) Cloud Sync\n\n- Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js`\n- Periodic task: `src/shared/services/cloudSyncScheduler.js`\n- Control route: `src/app/api/sync/cloud/route.js`\n\n## Request Lifecycle (`/v1/chat/completions`)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant Client as CLI/SDK Client\n    participant Route as /api/v1/chat/completions\n    participant Chat as src/sse/handlers/chat\n    participant Core as open-sse/handlers/chatCore\n    participant Model as Model Resolver\n    participant Auth as Credential Selector\n    participant Exec as Provider Executor\n    participant Prov as Upstream Provider\n    participant Stream as Stream Translator\n    participant Usage as usageDb\n\n    Client->>Route: POST /v1/chat/completions\n    Route->>Chat: handleChat(request)\n    Chat->>Model: parse/resolve model or combo\n\n    alt Combo model\n        Chat->>Chat: iterate combo models (handleComboChat)\n    end\n\n    Chat->>Auth: getProviderCredentials(provider)\n    Auth-->>Chat: active account + tokens/api key\n\n    Chat->>Core: handleChatCore(body, modelInfo, credentials)\n    Core->>Core: detect source format\n    Core->>Core: translate request to target format\n    Core->>Exec: execute(provider, transformedBody)\n    Exec->>Prov: upstream API call\n    Prov-->>Exec: SSE/JSON response\n    Exec-->>Core: response + metadata\n\n    alt 401/403\n        Core->>Exec: refreshCredentials()\n        Exec-->>Core: updated tokens\n        Core->>Exec: retry request\n    end\n\n    Core->>Stream: translate/normalize stream to client format\n    Stream-->>Client: SSE chunks / JSON response\n\n    Stream->>Usage: extract usage + persist history/log\n```\n\n## Combo + Account Fallback Flow\n\n```mermaid\nflowchart TD\n    A[Incoming model string] --> B{Is combo name?}\n    B -- Yes --> C[Load combo models sequence]\n    B -- No --> D[Single model path]\n\n    C --> E[Try model N]\n    E --> F[Resolve provider/model]\n    D --> F\n\n    F --> G[Select account credentials]\n    G --> H{Credentials available?}\n    H -- No --> I[Return provider unavailable]\n    H -- Yes --> J[Execute request]\n\n    J --> K{Success?}\n    K -- Yes --> L[Return response]\n    K -- No --> M{Fallback-eligible error?}\n\n    M -- No --> N[Return error]\n    M -- Yes --> O[Mark account unavailable cooldown]\n    O --> P{Another account for provider?}\n    P -- Yes --> G\n    P -- No --> Q{In combo with next model?}\n    Q -- Yes --> E\n    Q -- No --> R[Return all unavailable]\n```\n\nFallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics.\n\n## OAuth Onboarding and Token Refresh Lifecycle\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Dashboard UI\n    participant OAuth as /api/oauth/[provider]/[action]\n    participant ProvAuth as Provider Auth Server\n    participant DB as localDb\n    participant Test as /api/providers/[id]/test\n    participant Exec as Provider Executor\n\n    UI->>OAuth: GET authorize or device-code\n    OAuth->>ProvAuth: create auth/device flow\n    ProvAuth-->>OAuth: auth URL or device code payload\n    OAuth-->>UI: flow data\n\n    UI->>OAuth: POST exchange or poll\n    OAuth->>ProvAuth: token exchange/poll\n    ProvAuth-->>OAuth: access/refresh tokens\n    OAuth->>DB: createProviderConnection(oauth data)\n    OAuth-->>UI: success + connection id\n\n    UI->>Test: POST /api/providers/[id]/test\n    Test->>Exec: validate credentials / optional refresh\n    Exec-->>Test: valid or refreshed token info\n    Test->>DB: update status/tokens/errors\n    Test-->>UI: validation result\n```\n\nRefresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`.\n\n## Cloud Sync Lifecycle (Enable / Sync / Disable)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Endpoint Page UI\n    participant Sync as /api/sync/cloud\n    participant DB as localDb\n    participant Cloud as External Cloud Sync\n    participant Claude as ~/.claude/settings.json\n\n    UI->>Sync: POST action=enable\n    Sync->>DB: set cloudEnabled=true\n    Sync->>DB: ensure API key exists\n    Sync->>Cloud: POST /sync/{machineId} (providers/aliases/combos/keys)\n    Cloud-->>Sync: sync result\n    Sync->>Cloud: GET /{machineId}/v1/verify\n    Sync-->>UI: enabled + verification status\n\n    UI->>Sync: POST action=sync\n    Sync->>Cloud: POST /sync/{machineId}\n    Cloud-->>Sync: remote data\n    Sync->>DB: update newer local tokens/status\n    Sync-->>UI: synced\n\n    UI->>Sync: POST action=disable\n    Sync->>DB: set cloudEnabled=false\n    Sync->>Cloud: DELETE /sync/{machineId}\n    Sync->>Claude: switch ANTHROPIC_BASE_URL back to local (if needed)\n    Sync-->>UI: disabled\n```\n\nPeriodic sync is triggered by `CloudSyncScheduler` when cloud is enabled.\n\n## Data Model and Storage Map\n\n```mermaid\nerDiagram\n    SETTINGS ||--o{ PROVIDER_CONNECTION : controls\n    PROVIDER_NODE ||--o{ PROVIDER_CONNECTION : backs_compatible_provider\n    PROVIDER_CONNECTION ||--o{ USAGE_ENTRY : emits_usage\n\n    SETTINGS {\n      boolean cloudEnabled\n      number stickyRoundRobinLimit\n      boolean requireLogin\n      string password_hash\n    }\n\n    PROVIDER_CONNECTION {\n      string id\n      string provider\n      string authType\n      string name\n      number priority\n      boolean isActive\n      string apiKey\n      string accessToken\n      string refreshToken\n      string expiresAt\n      string testStatus\n      string lastError\n      string rateLimitedUntil\n      json providerSpecificData\n    }\n\n    PROVIDER_NODE {\n      string id\n      string type\n      string name\n      string prefix\n      string apiType\n      string baseUrl\n    }\n\n    MODEL_ALIAS {\n      string alias\n      string targetModel\n    }\n\n    COMBO {\n      string id\n      string name\n      string[] models\n    }\n\n    API_KEY {\n      string id\n      string name\n      string key\n      string machineId\n      boolean isActive\n    }\n\n    USAGE_ENTRY {\n      string provider\n      string model\n      number prompt_tokens\n      number completion_tokens\n      string connectionId\n      string timestamp\n    }\n```\n\nPhysical storage files:\n\n- main state: `${DATA_DIR}/db.json` (or `~/.9router/db.json`)\n- usage stats: `~/.9router/usage.json`\n- request log lines: `~/.9router/log.txt`\n- optional translator/request debug sessions: `<repo>/logs/...`\n\n## Deployment Topology\n\n```mermaid\nflowchart LR\n    subgraph LocalHost[Developer Host]\n        CLI[CLI Tools]\n        Browser[Dashboard Browser]\n    end\n\n    subgraph ContainerOrProcess[9Router Runtime]\n        Next[Next.js Server\\nPORT=20128]\n        Core[SSE Core + Executors]\n        MainDB[(db.json)]\n        UsageDB[(usage.json/log.txt)]\n    end\n\n    subgraph External[External Services]\n        Providers[AI Providers]\n        SyncCloud[Cloud Sync Service]\n    end\n\n    CLI --> Next\n    Browser --> Next\n    Next --> Core\n    Next --> MainDB\n    Core --> MainDB\n    Core --> UsageDB\n    Core --> Providers\n    Next --> SyncCloud\n```\n\n## Module Mapping (Decision-Critical)\n\n### Route and API Modules\n\n- `src/app/api/v1/*`, `src/app/api/v1beta/*`: compatibility APIs\n- `src/app/api/providers*`: provider CRUD, validation, testing\n- `src/app/api/provider-nodes*`: custom compatible node management\n- `src/app/api/oauth/*`: OAuth/device-code flows\n- `src/app/api/keys*`: local API key lifecycle\n- `src/app/api/models/alias`: alias management\n- `src/app/api/combos*`: fallback combo management\n- `src/app/api/pricing`: pricing overrides for cost calculation\n- `src/app/api/usage/*`: usage and logs APIs\n- `src/app/api/sync/*` + `src/app/api/cloud/*`: cloud sync and cloud-facing helpers\n- `src/app/api/cli-tools/*`: local CLI config writers/checkers\n\n### Routing and Execution Core\n\n- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop\n- `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup\n- `open-sse/executors/*`: provider-specific network and format behavior\n\n### Translation Registry and Format Converters\n\n- `open-sse/translator/index.js`: translator registry and orchestration\n- Request translators: `open-sse/translator/request/*`\n- Response translators: `open-sse/translator/response/*`\n- Format constants: `open-sse/translator/formats.js`\n\n### Persistence\n\n- `src/lib/localDb.js`: persistent config/state\n- `src/lib/usageDb.js`: usage history and rolling request logs\n\n## Provider Executor Coverage\n\nSpecialized executors:\n\n- `antigravity`\n- `gemini-cli`\n- `github`\n- `kiro`\n- `codex`\n- `cursor`\n\nDefault executor path:\n\n- all other providers (including compatible node providers) use `open-sse/executors/default.js`\n\n## Format Translation Coverage\n\nDetected source formats include:\n\n- `openai`\n- `openai-responses`\n- `claude`\n- `gemini`\n\nTarget formats include:\n\n- OpenAI chat/Responses\n- Claude\n- Gemini/Gemini-CLI/Antigravity envelope\n- Kiro\n- Cursor\n\nTranslations are selected dynamically based on source payload shape and provider target format.\n\n## Failure Modes and Resilience\n\n## 1) Account/Provider Availability\n\n- provider account cooldown on transient/rate/auth errors\n- account fallback before failing request\n- combo model fallback when current model/provider path is exhausted\n\n## 2) Token Expiry\n\n- pre-check and refresh with retry for refreshable providers\n- 401/403 retry after refresh attempt in core path\n\n## 3) Stream Safety\n\n- disconnect-aware stream controller\n- translation stream with end-of-stream flush and `[DONE]` handling\n- usage estimation fallback when provider usage metadata is missing\n\n## 4) Cloud Sync Degradation\n\n- sync errors are surfaced but local runtime continues\n- scheduler has retry-capable logic, but periodic execution currently calls single-attempt sync by default\n\n## 5) Data Integrity\n\n- DB shape migration/repair for missing keys\n- corrupt JSON reset safeguards for localDb and usageDb\n\n## Observability and Operational Signals\n\nRuntime visibility sources:\n\n- console logs from `src/sse/utils/logger.js`\n- per-request usage aggregates in `usage.json`\n- textual request status log in `log.txt`\n- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true`\n- dashboard usage endpoints (`/api/usage/*`) for UI consumption\n\n## Security-Sensitive Boundaries\n\n- JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing\n- Initial password fallback (`INITIAL_PASSWORD`, default `123456`) must be overridden in real deployments\n- API key HMAC secret (`API_KEY_SECRET`) secures generated local API key format\n- Provider secrets (API keys/tokens) are persisted in local DB and should be protected at filesystem level\n- Cloud sync endpoints rely on API key auth + machine id semantics\n\n## Environment and Runtime Matrix\n\nEnvironment variables actively used by code:\n\n- App/auth: `JWT_SECRET`, `INITIAL_PASSWORD`\n- Storage: `DATA_DIR`\n- Security hashing: `API_KEY_SECRET`, `MACHINE_ID_SALT`\n- Logging: `ENABLE_REQUEST_LOGS`\n- Sync/cloud URLing: `NEXT_PUBLIC_BASE_URL`, `NEXT_PUBLIC_CLOUD_URL`\n- Outbound proxy: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` and lowercase variants\n- Platform/runtime helpers (not app-specific config): `APPDATA`, `NODE_ENV`, `PORT`, `HOSTNAME`\n\n## Known Architectural Notes\n\n1. `usageDb` currently stores under `~/.9router` and does not follow `DATA_DIR`.\n2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`.\n3. Request logger writes full headers/body when enabled; treat log directory as sensitive.\n4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability.\n\n## Operational Verification Checklist\n\n- Build from source: `cd /root/dev/9router && npm run build`\n- Build Docker image: `cd /root/dev/9router && docker build -t 9router .`\n- Start service and verify:\n- `GET /api/settings`\n- `GET /api/v1/models`\n- CLI target base URL should be `http://<host>:20128/v1` when `PORT=20128`\n","open-sse/AGENTS.md":"# open-sse\n\nProvider-agnostic SSE engine: one OpenAI-style request → any provider (LLM chat, image, embedding, tts, stt, search), streamed back in the client's format.\n\n## Request lifecycle (chat)\n\n`handlers/chatCore.js` → `services/model.js` `parseModel` (resolve `provider/model`) → **RTK for `cursor`** (`rtk/` compresses the source-format `tool_result` / `role:tool` in-place — its translator rewrites those shapes, so this one provider must run **before** translate) → `translator/index.js` `translateRequest` (client format → provider format) → **post-translate savers** (`rtk/` compress for every other provider, `rtk/headroom.js` proxy compress, `rtk/caveman.js` / `rtk/ponytail.js` system inject — all fail-open) → `executors/index.js` `getExecutor(provider)` → `executor.execute()` (streams upstream) → `translateResponse` (provider chunks → client format) → SSE out.\n\n## Directory map\n\n- `config/` — ALL constants/config (no hardcode elsewhere). `providers.js`/`registry/` (provider defs), `providerModels.js` (alias→models matrix), `runtimeConfig.js` (timeouts, token limits), `*Constants.js`.\n- `translator/` — format conversion. `request/<from>-to-<to>.js`, `response/<from>-to-<to>.js`, `schema/` (enums: ROLE, CLAUDE_BLOCK…), `concerns/` (shared logic), `formats.js`+`formats/` (per-format). `index.js` is the registry/entry.\n- `executors/` — per-provider upstream call. `base.js` (BaseExecutor), one file per special provider, `index.js` map.\n- `providers/` — registry build + `capabilities.js` + `pricing.js`. Entry: `index.js` (PROVIDERS).\n- `handlers/` — per-modality cores (chat/image/embedding/tts/stt/search) + sub-provider folders. `chatCore/` has the streaming/non-streaming/sse-to-json handlers.\n- `rtk/` — request token-killer. `index.js` compresses `tool_result` content in-place (OpenAI/Claude/Kiro shapes); `filters/` per-tool compressors + `autodetect.js`; `headroom.js` external compress proxy; `caveman.js` system-prompt injector.\n- `transformer/` — `responsesTransformer.js` (Chat Completions SSE → Codex Responses API SSE), `streamToJsonConverter.js`.\n- `shared/` — cross-provider auth/identity: `clineAuth.js`, `machineId.js`, `qoder/`.\n- `services/` — `model.js`, `provider.js`, `accountFallback.js`, `combo.js`, `compact.js`, `tokenRefresh/`+`tokenRefresh.js`, `oauthCredentialManager.js`, `usage/`, `projectId.js`, `kiroModels.js`/`qoderModels.js`.\n- `utils/` — streamHandler, stream, sse, error, sessionManager, claudeCloaking, clientDetector, proxyFetch (patches global fetch), cursorProtobuf/cursorChecksum, ollamaTransform.\n\n## Conventions\n\n- Config-driven, DRY, camelCase. NEVER hardcode values, models, or block/role strings — use `config/` + `schema/` constants.\n- Translator pipeline pivots through OpenAI as the intermediate format. A translator registered on the exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop.\n- Translators self-register via `register(from, to, reqFn, resFn)` as an import side-effect — new files MUST be imported in `translator/index.js`.\n\n## How to add\n\n- **Provider**: copy `providers/REGISTRY_TEMPLATE.js` → `providers/registry/{id}.js`; add models to `config/providerModels.js`. Generic providers need no executor (DefaultExecutor handles OpenAI-compatible APIs).\n- **Executor** (only for non-standard upstream): subclass `BaseExecutor` (override `getBaseUrls`/`buildHeaders`/`buildUrl`/`execute`), register in `executors/index.js` map. `getExecutor` falls back to `DefaultExecutor` when absent.\n- **Translator**: add `request|response/<from>-to-<to>.js` calling `register(...)`, then import it in `translator/index.js`. Reuse `schema/` + `concerns/` — don't re-implement parsing.\n\n## Pitfalls\n\n- OpenAI bridge is lossy (thinking, non-base64 images, tool ids, is_error) — prefer a direct route for fragile pairs.\n- `registry/index.js` is an auto-generated static import list; regenerate it (don't hand-edit) after adding a `registry/{id}.js`. REGISTRY_TEMPLATE is excluded by design.\n- Special binary/protobuf formats (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — handle in their executor.\n- `rtk/` + `headroom.js` mutate the request body in-place and are **fail-open**: any error returns null and leaves the body untouched — never throw out of them. RTK skips `is_error`/`status:\"error\"` tool results to preserve traces.\n","skills/9router-chat/SKILL.md":"---\nname: 9router-chat\ndescription: Chat / code generation via 9Router using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through 9Router.\n---\n\n# 9Router — Chat\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Endpoints\n\n- `POST $NINEROUTER_URL/v1/chat/completions` — OpenAI format\n- `POST $NINEROUTER_URL/v1/messages` — Anthropic format\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models | jq '.data[].id'\n# Per-model metadata (contextWindow, params)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/gpt-4o\"\n```\n\nCombos (e.g. `vip`, `mycodex`) auto-fallback through multiple providers.\n\n## OpenAI format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/chat/completions \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/gpt-5\",\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}],\"stream\":false}'\n```\n\nJS (OpenAI SDK):\n\n```js\nimport OpenAI from \"openai\";\nconst client = new OpenAI({ baseURL: `${process.env.NINEROUTER_URL}/v1`, apiKey: process.env.NINEROUTER_KEY });\nconst res = await client.chat.completions.create({\n  model: \"openai/gpt-5\",\n  messages: [{ role: \"user\", content: \"Hi\" }],\n  stream: true,\n});\nfor await (const chunk of res) process.stdout.write(chunk.choices[0]?.delta?.content || \"\");\n```\n\n## Anthropic format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/messages \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"anthropic-version: 2023-06-01\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"cc/claude-opus-4-7\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}]}'\n```\n\n## Response shape\n\nOpenAI (`/v1/chat/completions`):\n```json\n{ \"id\": \"chatcmpl-...\", \"object\": \"chat.completion\", \"model\": \"openai/gpt-5\",\n  \"choices\": [{ \"index\": 0, \"message\": { \"role\": \"assistant\", \"content\": \"Hello!\" }, \"finish_reason\": \"stop\" }],\n  \"usage\": { \"prompt_tokens\": 8, \"completion_tokens\": 2, \"total_tokens\": 10 } }\n```\n\nStreaming (`stream:true`) emits SSE: `data: {choices:[{delta:{content:\"...\"}}]}\\n\\n` ... `data: [DONE]\\n\\n`.\n\nAnthropic (`/v1/messages`):\n```json\n{ \"id\": \"msg_...\", \"type\": \"message\", \"role\": \"assistant\", \"model\": \"cc/claude-opus-4-7\",\n  \"content\": [{ \"type\": \"text\", \"text\": \"Hello!\" }],\n  \"stop_reason\": \"end_turn\", \"usage\": { \"input_tokens\": 8, \"output_tokens\": 2 } }\n```\n","skills/9router-embeddings/SKILL.md":"---\nname: 9router-embeddings\ndescription: Generate vector embeddings via 9Router /v1/embeddings using OpenAI / Gemini / Mistral / Voyage / Nvidia / GitHub embedding models for RAG, semantic search, similarity. Use when the user wants embeddings, vectors, RAG, semantic search, or to embed text.\n---\n\n# 9Router — Embeddings\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/embedding | jq '.data[].id'\n# Per-model dimensions\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/text-embedding-3-small\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/embeddings`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/embedding` |\n| `input` | yes | string OR array of strings |\n| `encoding_format` | no | `float` (default) / `base64` |\n| `dimensions` | no | OpenAI v3 only |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/embeddings \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/text-embedding-3-small\",\"input\":[\"hello\",\"world\"]}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/embeddings`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/text-embedding-004\", input: \"RAG chunk text\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].embedding.length);  // dimension\n```\n\n## Response shape\n\n```json\n{ \"object\": \"list\", \"model\": \"openai/text-embedding-3-small\",\n  \"data\": [\n    { \"object\": \"embedding\", \"index\": 0, \"embedding\": [0.0123, -0.045, ...] },\n    { \"object\": \"embedding\", \"index\": 1, \"embedding\": [...] }\n  ],\n  \"usage\": { \"prompt_tokens\": 5, \"total_tokens\": 5 } }\n```\n\n## Provider quirks\n\n| Provider | Notes |\n|---|---|\n| `openai`, `openrouter`, `mistral`, `voyage-ai`, `fireworks`, `together`, `nebius`, `github`, `nvidia`, `jina-ai` | Native OpenAI shape — `dimensions` works only on OpenAI v3 (`text-embedding-3-*`) |\n| `gemini`, `google_ai_studio` | Server auto-converts to `embedContent`/`batchEmbedContents` — send OpenAI shape |\n| `openai-compatible-*`, `custom-embedding-*` | Custom `baseUrl` from credentials |\n\nBatch (`input` as array) is faster; some providers cap batch size.\n","skills/9router-image/SKILL.md":"---\nname: 9router-image\ndescription: Generate images via 9Router /v1/images/generations using OpenAI / Gemini Imagen / DALL-E / FLUX / MiniMax / SDWebUI / ComfyUI / Codex models. Use when the user wants to create, generate, draw, or render an image, picture, or text-to-image (txt2img).\n---\n\n# 9Router — Image Generation\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/image | jq '.data[].id'\n# Per-model params/options (size enum, quality enum, capabilities like edit)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/dall-e-3\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/images/generations`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/image` |\n| `prompt` | yes | image description |\n| `n` | no | count (provider-dependent) |\n| `size` | no | `1024x1024`, `1792x1024`, ... |\n| `quality` | no | `standard` / `hd` (OpenAI) |\n| `response_format` | no | `url` (default) or `b64_json` |\n\nAdd query `?response_format=binary` to receive raw image bytes (handy for saving file).\n\n## Examples\n\nSave to file (binary):\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/images/generations?response_format=binary\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"gemini/gemini-3-pro-image-preview\",\"prompt\":\"watercolor mountains at sunrise\",\"size\":\"1024x1024\"}' \\\n  --output out.png\n```\n\nJS (URL response):\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/images/generations`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/gemini-3-pro-image-preview\", prompt: \"neon city\", size: \"1024x1024\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].url || data[0].b64_json.slice(0, 40));\n```\n\n## Response shape\n\nJSON (default `response_format=url`):\n```json\n{ \"created\": 1735000000, \"data\": [{ \"url\": \"https://...\" }] }\n```\n\n`response_format=b64_json`:\n```json\n{ \"created\": 1735000000, \"data\": [{ \"b64_json\": \"iVBORw0KGgo...\" }] }\n```\n\nQuery `?response_format=binary` returns raw image bytes (Content-Type `image/png` or `image/jpeg`).\n\n## Provider quirks\n\nCommon fields above work everywhere. These add/override:\n\n| Provider | Extra/changed fields | Notes |\n|---|---|---|\n| `openai`, `minimax`, `openrouter`, `recraft` | `quality`, `style`, `response_format` | Standard OpenAI shape |\n| `gemini` (nano-banana) | — | Only `prompt`; ignores `size`/`n` |\n| `codex` (gpt-5.4-image) | `image`, `images[]`, `image_detail`, `output_format`, `background` | SSE stream; **ChatGPT Plus/Pro required** |\n| `huggingface` | — | Only `prompt`; returns single image |\n| `nanobanana` | `image`, `images[]` (edit mode) | `size` → aspect ratio; async polling |\n| `fal-ai` | `image` (img2img) | `n` → `num_images`; `size` → ratio; async |\n| `stability-ai` | `style` (preset), `output_format` | `size` → `aspect_ratio` |\n| `black-forest-labs` (FLUX) | `image` (ref) | `size` → exact `width`/`height`; async |\n| `runwayml` | `image` (ref) | `size` → ratio; async; video models exist |\n| `sdwebui`, `comfyui` | — | Localhost noAuth (`:7860` / `:8188`) |\n","skills/9router-stt/SKILL.md":"---\nname: 9router-stt\ndescription: Speech-to-text via 9Router /v1/audio/transcriptions using OpenAI Whisper / Groq / Gemini / Deepgram / AssemblyAI / NVIDIA / HuggingFace models. Use when the user wants to transcribe audio, convert speech to text, or get subtitles from audio files.\n---\n\n# 9Router — Speech-to-Text\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/stt | jq '.data[].id'\n# Per-model params (language, response_format, prompt, temperature support)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/whisper-1\"\n```\n\n`model` = STT model ID (e.g. `openai/whisper-1`, `groq/whisper-large-v3`, `deepgram/nova-3`, `gemini/gemini-2.5-flash`).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/transcriptions` (OpenAI Whisper compatible, `multipart/form-data`)\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/stt` |\n| `file` | yes | audio file (mp3, wav, m4a, webm, ogg, flac) |\n| `language` | no | ISO-639-1 (e.g. `en`, `vi`) |\n| `prompt` | no | hint text to guide transcription |\n| `response_format` | no | `json` (default) / `text` / `verbose_json` / `srt` / `vtt` |\n| `temperature` | no | 0–1 |\n\n## Examples\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/transcriptions\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -F \"model=openai/whisper-1\" \\\n  -F \"file=@audio.mp3\" \\\n  -F \"language=vi\"\n```\n\nJS (Node):\n\n```js\nimport { createReadStream } from \"node:fs\";\nconst form = new FormData();\nform.append(\"model\", \"groq/whisper-large-v3-turbo\");\nform.append(\"file\", new Blob([await (await import(\"node:fs/promises\")).readFile(\"audio.mp3\")]), \"audio.mp3\");\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/transcriptions`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}` },\n  body: form,\n});\nconst { text } = await r.json();\nconsole.log(text);\n```\n\n## Response shape\n\nDefault (`response_format=json`):\n```json\n{ \"text\": \"Xin chào, đây là bản ghi âm.\" }\n```\n\n`verbose_json` adds `language`, `duration`, `segments[]` with timestamps.\n`srt` / `vtt` return subtitle text.\n\n## Provider quirks\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe` | Native OpenAI shape |\n| `groq` | `whisper-large-v3`, `whisper-large-v3-turbo`, `distil-whisper-large-v3-en` | Fastest; OpenAI shape |\n| `gemini` | `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-2.5-flash-lite` | Server converts to `generateContent` with audio inline |\n| `deepgram` | `nova-3`, `nova-2`, `whisper-large` | Token auth; server adapts response |\n| `assemblyai` | `universal-3-pro`, `universal-2` | Async upload+poll handled server-side |\n| `nvidia` | `nvidia/parakeet-ctc-1.1b-asr` | NIM endpoint |\n| `huggingface` | `openai/whisper-large-v3`, `openai/whisper-small` | HF Inference API |\n| `elevenlabs` | `scribe_v1`, `scribe_v2` | Whisper-compatible shape; `xi-api-key` auth, not Bearer. Extra params: `timestamps_granularity` (`word`/`character`/`none`), `tag_audio_events`, and speaker labelling via **either** `diarize=true` **or** `num_speakers` (1–32) — sending both makes the request invalid, so `diarize` wins and `num_speakers` is dropped. Blank `language` is omitted upstream for auto-detect. `srt`/`vtt` and `verbose_json` segments are served from Scribe's own `additional_formats` render, so `segments[]` is omitted when the upstream render is unavailable rather than synthesized. Does not accept `temperature` or `prompt` — those are not forwarded. |\n","skills/9router-tts/SKILL.md":"---\nname: 9router-tts\ndescription: Text-to-speech via 9Router /v1/audio/speech using OpenAI / ElevenLabs / Deepgram / Edge TTS / Google TTS / Hyperbolic / Inworld voices. Use when the user wants to convert text to speech, generate audio, voiceover, narrate, or read text aloud.\n---\n\n# 9Router — Text-to-Speech\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\n# 1) List models\ncurl $NINEROUTER_URL/v1/models/tts | jq '.data[].id'\n# 2) Per-model metadata (params, voicesUrl if voice-by-id)\ncurl \"$NINEROUTER_URL/v1/models/info?id=el/eleven_multilingual_v2\"\n# 3) List voices (elevenlabs, edge-tts, deepgram, inworld, local-device). Optional ?lang=vi\ncurl \"$NINEROUTER_URL/v1/audio/voices?provider=edge-tts&lang=vi\" | jq '.data[].model'\n```\n\n`model` field in `/v1/audio/speech` = voice ID directly (e.g. `edge-tts/vi-VN-HoaiMyNeural`, `el/<voice_id>`, or `openai/tts-1` model+default voice).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/speech`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | voice ID from `/v1/models/tts` |\n| `input` | yes | text to speak |\n\nQuery `?response_format=mp3` (default, raw bytes) or `?response_format=json` (`{audio: base64, format}`).\n\n## Examples\n\nSave MP3:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/speech\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/tts-1\",\"input\":\"Hello world\"}' \\\n  --output speech.mp3\n```\n\nJS (save file):\n\n```js\nimport { writeFile } from \"node:fs/promises\";\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/speech`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"el/eleven_multilingual_v2\", input: \"Xin chào\" }),\n});\nawait writeFile(\"speech.mp3\", Buffer.from(await r.arrayBuffer()));\n```\n\n## Response shape\n\nDefault → raw audio bytes (Content-Type `audio/mp3`).\n\n`?response_format=json`:\n```json\n{ \"audio\": \"SUQzBAAAA...\", \"format\": \"mp3\" }\n```\n\n## Provider quirks (model format)\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `tts-1/alloy` (model/voice) or just voice | Default model `gpt-4o-mini-tts` |\n| `elevenlabs` | `<model_id>/<voice_id>` or `<voice_id>` | Default model `eleven_flash_v2_5`; list voices in Dashboard |\n| `openrouter` | `openai/gpt-4o-mini-tts/alloy` | Streamed via chat-completions audio modality |\n| `edge-tts` | voice id e.g. `vi-VN-HoaiMyNeural` | **noAuth**; default `vi-VN-HoaiMyNeural` |\n| `google-tts` | language code e.g. `en`, `vi` | **noAuth** |\n| `local-device` | OS voice name (`say -v ?` / SAPI) | **noAuth**; needs `ffmpeg` |\n| `deepgram` | `aura-asteria-en` etc | Token auth |\n| `nvidia`, `inworld`, `cartesia`, `playht` | `model/voice` | Provider-specific auth header |\n| `coqui`, `tortoise` | speaker / voice id | Localhost noAuth |\n| `hyperbolic` | model id | Body = `{text}` only |\n","skills/9router-video/SKILL.md":"---\nname: 9router-video\ndescription: 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.\n---\n\n# 9Router — Video Generation (xAI Grok Imagine)\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\nRequires 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.\n\n## Endpoints (async job flow)\n\nVideo generation is **asynchronous**: the POST returns a `request_id` immediately, then you poll until the job is `done` or `failed`.\n\n| Endpoint | Purpose |\n|---|---|\n| `POST /v1/videos/generations` | text-to-video / image-to-video |\n| `POST /v1/videos/edits` | edit an existing video |\n| `POST /v1/videos/extensions` | extend an existing video |\n| `GET /v1/videos/{request_id}` | poll job status |\n\nRequest fields (passed through to xAI unchanged — see https://docs.x.ai/developers/rest-api-reference/inference/videos):\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | no | `xai/grok-imagine-video` (prefix is stripped before upstream) |\n| `prompt` | yes for T2V | video description |\n| `duration` | no | seconds |\n| `aspect_ratio` | no | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `3:2`, `2:3` |\n| `resolution` | no | `480p`, `720p`, `1080p` |\n| `image` | no | `{ \"url\": \"https://… or data:image/…;base64,…\" }` for image-to-video |\n| `video` | edits/extensions | `{ \"url\": \"…mp4\" }` or `{ \"file_id\": \"…\" }` |\n\n## Examples\n\nSubmit a job:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/videos/generations\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -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\"}'\n# → {\"request_id\":\"abc123\"}   (response header x-9router-connection-id: <id>)\n```\n\nPoll until done (echo the connection header back so the same account polls the job):\n\n```bash\ncurl \"$NINEROUTER_URL/v1/videos/abc123\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"x-connection-id: <id from create response>\"\n# → {\"status\":\"pending\",\"progress\":42}\n# → {\"status\":\"done\",\"video\":{\"url\":\"https://…mp4\",\"duration\":8},\"model\":\"grok-imagine-video\"}\n# → {\"status\":\"failed\",\"error\":{\"code\":\"…\",\"message\":\"…\"}}\n```\n\nDownload: fetch `video.url` from the `done` response.\n\n## CLI one-shot\n\n```bash\n9router xai video \\\n  --prompt \"A cinematic tracking shot through a neon city at night\" \\\n  --output video.mp4\n# options: --model --duration --aspect-ratio --resolution --image --timeout --port --api-key\n```\n\nSubmits, polls with progress, downloads to `video.mp4.part`, atomically renames on success. Ctrl+C cancels cleanly; non-zero exit on failure.\n\n## Notes & limits\n\n- 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`).\n- 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.\n- Video models are tagged `kind: \"video\"` and are excluded from chat model lists and chat fallback combos.\n- 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.\n","skills/9router-web-fetch/SKILL.md":"---\nname: 9router-web-fetch\ndescription: Fetch URL → markdown / text / HTML via 9Router /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown.\n---\n\n# 9Router — Web Fetch\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webFetch\") | .id'\n# Per-provider params\ncurl \"$NINEROUTER_URL/v1/models/info?id=firecrawl/fetch\"\n```\n\nIDs end in `/fetch` (e.g. `firecrawl/fetch`, `jina/fetch`). `fetch-combo` chains providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/web/fetch`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `firecrawl` or `jina-reader`) |\n| `url` | yes | URL to extract |\n| `format` | no | `markdown` (default) / `text` / `html` |\n| `max_characters` | no | truncate output |\n\n## Examples\n\n### Jina Reader\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"jina-reader\",\"url\":\"https://9router.com\",\"format\":\"markdown\"}'\n```\n\n### Exa\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"exa\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Firecrawl\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"firecrawl\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Tavily\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Ollama Cloud\n\nUses the API key from the existing `ollama` connection.\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"ollama\",\"url\":\"https://example.com\",\"format\":\"markdown\"}'\n```\n\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/web/fetch`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"fetch-combo\", url: \"https://example.com\", format: \"markdown\", max_characters: 5000 }),\n});\nconst { data } = await r.json();\nconsole.log(data.title, data.content.length);\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"jina-reader\",\n  \"url\": \"...\",\n  \"title\": \"...\",\n  \"content\": { \"format\": \"markdown\", \"text\": \"...\", \"length\": 1234 },\n  \"links\": [\"https://example.com/related\"],\n  \"metadata\": { \"author\": null, \"published_at\": null, \"language\": null },\n  \"usage\": { \"fetch_cost_usd\": 0 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700 }\n}\n```\n\n`links` is included when the upstream provider returns discovered page links (currently Ollama Cloud).\n\n## Provider quirks\n\n| Provider | Auth | Best for |\n|---|---|---|\n| `firecrawl` | Bearer | JS-rendered pages, `format=markdown/html` |\n| `jina-reader` | Bearer (optional) | Free tier (~1M chars/mo); fastest plain markdown |\n| `tavily` | Bearer | Bulk extract; returns `raw_content` |\n| `exa` | `x-api-key` | Pre-indexed pages; fast text extraction |\n| `ollama` | Bearer | Markdown plus page title and discovered links; uses the Ollama Cloud key |\n","skills/9router-web-search/SKILL.md":"---\nname: 9router-web-search\ndescription: Web and X search via 9Router /v1/search using Tavily / Exa / Brave / Serper / SearXNG / Google PSE / Linkup / SearchAPI / You.com / Perplexity / Xquik. Use when the user wants to search the web, find articles, or search public X posts.\n---\n\n# 9Router — Web Search\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webSearch\") | .id'\n# Per-provider params (searchTypes, maxResults, required options like cx for google-pse)\ncurl \"$NINEROUTER_URL/v1/models/info?id=tavily/search\"\n```\n\nIDs end in `/search` (e.g. `tavily/search`). Combos (`owned_by:\"combo\"`) chain providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/search`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `tavily` or `brave`) |\n| `query` | yes | search query |\n| `max_results` | no | default 5 |\n| `search_type` | no | `web` (default) / `news` / `x` for Xquik |\n| `country`, `language`, `time_range`, `domain_filter` | no | provider-dependent |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"query\":\"9Router open source\",\"max_results\":5}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/search`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"search-combo\", query: \"latest LLM benchmarks\", max_results: 10 }),\n});\nconsole.log(await r.json());\n```\n\nX search with Xquik:\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"xquik\",\"query\":\"from:github release\",\"max_results\":10,\"provider_options\":{\"queryType\":\"Latest\"}}'\n```\n\nAdd the Xquik API key in 9Router's provider settings. Xquik charges 1 credit per returned post. Continue a search by passing `pagination.next_cursor` as `provider_options.cursor`.\n\nXquik responses include provider pagination and credit usage:\n\n```json\n{\n  \"pagination\": { \"has_more\": true, \"next_cursor\": \"cursor-2\" },\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": null, \"provider_credits_used\": 10 }\n}\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"tavily\",\n  \"query\": \"9Router open source\",\n  \"results\": [\n    {\n      \"title\": \"...\", \"url\": \"https://...\", \"display_url\": \"github.com/...\",\n      \"snippet\": \"...\", \"position\": 1, \"score\": 0.92,\n      \"published_at\": null, \"favicon_url\": null, \"content\": null,\n      \"metadata\": { \"author\": null, \"language\": null, \"source_type\": null, \"image_url\": null },\n      \"citation\": { \"provider\": \"tavily\", \"retrieved_at\": \"2026-...\", \"rank\": 1 }\n    }\n  ],\n  \"answer\": null,\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": 0.008 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700, \"total_results_available\": 12 },\n  \"errors\": []\n}\n```\n\n## Provider quirks\n\nAll accept `query` + `max_results`. Optional fields vary:\n\n| Provider | Supports | Required extras |\n|---|---|---|\n| `tavily` | country, domain_filter, news topic | — |\n| `exa` | domain_filter (incl/excl), news category | — |\n| `brave-search` | country, language | — |\n| `serper` | country, language, news endpoint | — |\n| `perplexity` | country, language, domain_filter | — |\n| `linkup` | domain_filter, time_range | `depth: fast/standard/deep` (option) |\n| `google-pse` | country, language, time_range, offset | **`cx` required** (providerOptions) |\n| `searchapi` | country, language, pagination | — |\n| `youcom` | country, language, time_range, domain_filter, full_page | — |\n| `searxng` | language, time_range | Self-hosted, **noAuth** |\n| `xquik` | X/Twitter search operators, language, cursor pagination | `queryType: Latest/Top`, `cursor` (options) |\n\nProvider IS the model — `\"provider\":\"tavily\" ≡ \"model\":\"tavily\"`.\n","skills/9router/SKILL.md":"---\nname: 9router\ndescription: Entry point for 9Router — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions 9Router, NINEROUTER_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed.\n---\n\n# 9Router\n\nLocal/remote AI gateway exposing OpenAI-compatible REST. One key, many providers, auto-fallback.\n\n## Setup\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"      # or VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                      # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nAll requests: `${NINEROUTER_URL}/v1/...` with header `Authorization: Bearer ${NINEROUTER_KEY}` (omit if auth disabled).\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`\n\n## Discover models\n\n```bash\ncurl $NINEROUTER_URL/v1/models                  # chat/LLM (default)\ncurl $NINEROUTER_URL/v1/models/image            # image-gen\ncurl $NINEROUTER_URL/v1/models/tts              # text-to-speech\ncurl $NINEROUTER_URL/v1/models/embedding        # embeddings\ncurl $NINEROUTER_URL/v1/models/web              # web search + fetch (entries have `kind` field)\ncurl $NINEROUTER_URL/v1/models/stt              # speech-to-text\ncurl $NINEROUTER_URL/v1/models/image-to-text    # vision\n```\n\nUse `data[].id` as `model` field in requests. Combos appear with `owned_by:\"combo\"`.\n\nResponse shape:\n```json\n{ \"object\": \"list\", \"data\": [\n  { \"id\": \"openai/gpt-5\", \"object\": \"model\", \"owned_by\": \"openai\", \"created\": 1735000000 },\n  { \"id\": \"tavily/search\", \"object\": \"model\", \"kind\": \"webSearch\", \"owned_by\": \"tavily\", \"created\": 1735000000 }\n]}\n```\n\n## Capability skills\n\nWhen the user needs a specific capability, fetch that skill's `SKILL.md` from its raw URL:\n\n| Capability | Raw URL |\n|---|---|\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## Errors\n\n- 401 → set/refresh `NINEROUTER_KEY` (Dashboard → Keys)\n- 400 `Invalid model format` → check `model` exists in `/v1/models/<kind>`\n- 503 `All accounts unavailable` → wait `retry-after` or add another provider account\n","skills/README.md":"# 9Router — Agent Skills\n\nDrop-in skills for any AI agent (Claude, Cursor, ChatGPT, custom SDK). Just **copy a link** below and paste it to your AI — it will fetch the skill and use 9Router for you.\n\n> Tip: start with the **9router** entry skill — it covers setup and links to all capability skills.\n\n## Skills\n\n| Capability | Copy link below and paste to your AI |\n|---|---|\n| **Entry / Setup** (start here) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md |\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Video generation (xAI Grok Imagine) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-video/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## How to use\n\nPaste to your AI (Claude, Cursor, ChatGPT, …):\n\n```\nRead this skill and use it: https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md\n```\n\nThen ask normally — *\"generate an image of a cat\"*, *\"transcribe this URL\"*, etc.\n\n## Configure your shell once\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"   # local default, or your VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                   # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`.\n\n## Links\n\n- Source: https://github.com/decolua/9router\n- Dashboard: https://9router.com\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## What this is\n\n9Router (`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.\n\nTwo published artifacts live in this one repo:\n- The **dashboard + gateway** (root `package.json`, `9router-app`) — the Next.js server that does the actual routing.\n- 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.\n\nThe code lives in `src/` (Next.js app + dashboard/compat APIs), `open-sse/` (the provider-agnostic routing/translation engine), `cli/` (the launcher package), and `tests/`.\n\n## Commands\n\nDashboard/gateway (run from repo root):\n```bash\ncp .env.example .env\nnpm install\nPORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev   # dev (webpack, port 20127 by default via next dev)\nnpm run build && PORT=20128 HOSTNAME=0.0.0.0 npm run start           # production\n```\n- Bun variants: `npm run dev:bun` / `build:bun` / `start:bun`.\n- Default runtime port is **20128** (dashboard at `/dashboard`, API at `/v1`).\n- Lint: `npx eslint .` (config `eslint.config.mjs`, extends `eslint-config-next`).\n\nCLI package (`cli/`):\n```bash\nnpm run cli:pack       # build + npm pack from root\ncd cli && npm run dev  # nodemon watch\n```\n\nTests (vitest, in `tests/`, an **independent** ESM package — not wired into root `npm test`):\n```bash\nnpm install                             # ROOT deps first — tests import from src/ which needs `open`, `undici`, etc.\ncd tests && npm install                 # then tests' own deps (vitest) → tests/node_modules (allowed by tests/.gitignore)\nnpx vitest run                          # all tests; auto-discovers tests/vitest.config.js\nnpx vitest run unit/capabilities.test.js   # single file (path relative to tests/)\n```\n> 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.\n>\n> **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:\n> - 26 catalogued in `tests/__baseline__/known-fails.txt` (rtk, oauth-cursor-auto-import, translator-request-normalization, …).\n> - `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.\n> - `unit/xai-oauth-service.test.js` times out (5s) when the xAI endpoint-discovery fetch isn't reachable/mocked.\n> - `real/*.real.test.js` make live provider calls — need credentials, skip otherwise.\n- `*.real.test.js` under `tests/translator/real/` make live provider calls — skip unless credentials are set.\n- Regression baselines: `tests/__baseline__/verify-*.mjs` compare against committed snapshots (providers, aliases, OAuth URLs). Run these after touching provider registry / alias logic.\n\n## Architecture\n\nTwo authoritative docs already exist — read them before working in these areas rather than re-deriving:\n- `docs/ARCHITECTURE.md` — full system: request lifecycle, combo/account fallback, OAuth + token refresh, cloud sync, data model.\n- `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/`.**\n\n### Request flow (the thing to understand first)\n`src/app/api/v1/*` route (Next rewrite maps `/v1/*` → `/api/v1/*` in `next.config.mjs`)\n→ `src/sse/handlers/chat.js` (parse, combo expansion, account-selection loop)\n→ `open-sse/handlers/chatCore.js` (detect source format, translate request, dispatch to executor, retry/refresh, stream setup)\n→ `open-sse/executors/*` (per-provider upstream call; `default.js` handles any OpenAI-compatible provider)\n→ `open-sse/translator/*` (client format ↔ provider format)\n→ SSE back to client.\n\n`src/sse/` is the app-side entry glue; `open-sse/` is the provider-agnostic engine (also usable standalone). Cross that boundary consciously.\n\n### Translator engine (`open-sse/translator/`)\n- 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`).\n- 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.\n- 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.\n\n### Provider registry (`open-sse/providers/registry/*`)\n- 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.\n- Add a provider: copy `providers/REGISTRY_TEMPLATE.js`, add models to `config/providerModels.js`. Only add an executor for non-OpenAI-compatible upstreams.\n\n### Persistence — IMPORTANT (ARCHITECTURE.md is stale here)\nState 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.\n- `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/`.\n- DB file location resolves via `src/lib/db/paths.js` (`DATA_DIR`, else `~/.9router/`).\n- Usage/logs (`src/lib/usageDb.js`, `usage.json` + `log.txt`) still live under `~/.9router` and do **not** follow `DATA_DIR`.\n\n### RTK token saver (`open-sse/rtk/`)\nPre-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.\n\n## Conventions & gotchas\n\n- Plain JavaScript (ESM), no TypeScript. `@/*` path alias → `src/*` (`jsconfig.json`).\n- `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.\n- 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.\n- 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.\n- **Security-first on PRs**: Security is the top priority when reviewing or creating PRs. Audit authentication, credential/token storage & leaks, header manipulation (`X-Forwarded-For`), and SSRF risks before functional logic. Always include explicit security warnings/notes when reporting PR reviews or changes to the user.\n- Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`).\n","docs/ARCHITECTURE.md":"# 9Router Architecture\n\n_Last updated: 2026-02-06_\n\n## Executive Summary\n\n9Router is a local AI routing gateway and dashboard built on Next.js.\nIt provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic across multiple upstream providers with translation, fallback, token refresh, and usage tracking.\n\nCore capabilities:\n\n- OpenAI-compatible API surface for CLI/tools\n- Request/response translation across provider formats\n- Model combo fallback (multi-model sequence)\n- Account-level fallback (multi-account per provider)\n- OAuth + API-key provider connection management\n- Local persistence for providers, keys, aliases, combos, settings, pricing\n- Usage/cost tracking and request logging\n- Optional cloud sync for multi-device/state sync\n\nPrimary runtime model:\n\n- Next.js app routes under `src/app/api/*` implement both dashboard APIs and compatibility APIs\n- A shared SSE/routing core in `src/sse/*` + `open-sse/*` handles provider execution, translation, streaming, fallback, and usage\n\n## Scope and Boundaries\n\n### In Scope\n\n- Local gateway runtime\n- Dashboard management APIs\n- Provider authentication and token refresh\n- Request translation and SSE streaming\n- Local state + usage persistence\n- Optional cloud sync orchestration\n\n### Out of Scope\n\n- Cloud service implementation behind `NEXT_PUBLIC_CLOUD_URL`\n- Provider SLA/control plane outside local process\n- External CLI binaries themselves (Claude CLI, Codex CLI, etc.)\n\n## High-Level System Context\n\n```mermaid\nflowchart LR\n    subgraph Clients[Developer Clients]\n        C1[Claude Code]\n        C2[Codex CLI]\n        C3[OpenClaw / Droid / Cline / Continue / Roo]\n        C4[Custom OpenAI-compatible clients]\n        BROWSER[Browser Dashboard]\n    end\n\n    subgraph Router[9Router Local Process]\n        API[V1 Compatibility API\\n/v1/*]\n        DASH[Dashboard + Management API\\n/api/*]\n        CORE[SSE + Translation Core\\nopen-sse + src/sse]\n        DB[(db.json)]\n        UDB[(usage.json + log.txt)]\n    end\n\n    subgraph Upstreams[Upstream Providers]\n        P1[OAuth Providers\\nClaude/Codex/Gemini/Qwen/iFlow/GitHub/Kiro/Cursor/Antigravity]\n        P2[API Key Providers\\nOpenAI/Anthropic/OpenRouter/GLM/Kimi/MiniMax]\n        P3[Compatible Nodes\\nOpenAI-compatible / Anthropic-compatible]\n    end\n\n    subgraph Cloud[Optional Cloud Sync]\n        CLOUD[Cloud Sync Endpoint\\nNEXT_PUBLIC_CLOUD_URL]\n    end\n\n    C1 --> API\n    C2 --> API\n    C3 --> API\n    C4 --> API\n    BROWSER --> DASH\n\n    API --> CORE\n    DASH --> DB\n    CORE --> DB\n    CORE --> UDB\n\n    CORE --> P1\n    CORE --> P2\n    CORE --> P3\n\n    DASH --> CLOUD\n```\n\n## Core Runtime Components\n\n## 1) API and Routing Layer (Next.js App Routes)\n\nMain directories:\n\n- `src/app/api/v1/*` and `src/app/api/v1beta/*` for compatibility APIs\n- `src/app/api/*` for management/configuration APIs\n- Next rewrites in `next.config.mjs` map `/v1/*` to `/api/v1/*`\n\nImportant compatibility routes:\n\n- `src/app/api/v1/chat/completions/route.js`\n- `src/app/api/v1/messages/route.js`\n- `src/app/api/v1/responses/route.js`\n- `src/app/api/v1/models/route.js`\n- `src/app/api/v1/messages/count_tokens/route.js`\n- `src/app/api/v1beta/models/route.js`\n- `src/app/api/v1beta/models/[...path]/route.js`\n\nManagement domains:\n\n- Auth/settings: `src/app/api/auth/*`, `src/app/api/settings/*`\n- Providers/connections: `src/app/api/providers*`\n- Provider nodes: `src/app/api/provider-nodes*`\n- OAuth: `src/app/api/oauth/*`\n- Keys/aliases/combos/pricing: `src/app/api/keys*`, `src/app/api/models/alias`, `src/app/api/combos*`, `src/app/api/pricing`\n- Usage: `src/app/api/usage/*`\n- Sync/cloud: `src/app/api/sync/*`, `src/app/api/cloud/*`\n- CLI tooling helpers: `src/app/api/cli-tools/*`\n\n## 2) SSE + Translation Core\n\nMain flow modules:\n\n- Entry: `src/sse/handlers/chat.js`\n- Core orchestration: `open-sse/handlers/chatCore.js`\n- Provider execution adapters: `open-sse/executors/*`\n- Format detection/provider config: `open-sse/services/provider.js`\n- Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js`\n- Account fallback logic: `open-sse/services/accountFallback.js`\n- Translation registry: `open-sse/translator/index.js`\n- Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js`\n- Usage extraction/normalization: `open-sse/utils/usageTracking.js`\n\n## 3) Persistence Layer\n\nPrimary state DB:\n\n- `src/lib/localDb.js`\n- file: `${DATA_DIR}/db.json` (or `~/.9router/db.json` when `DATA_DIR` is unset)\n- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing\n\nUsage DB:\n\n- `src/lib/usageDb.js`\n- files: `~/.9router/usage.json`, `~/.9router/log.txt`\n- note: currently independent from `DATA_DIR`\n\n## 4) Auth + Security Surfaces\n\n- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js`\n- API key generation/verification: `src/shared/utils/apiKey.js`\n- Provider secrets persisted in `providerConnections` entries\n- Optional proxy support for upstream calls via env proxy variables (`open-sse/utils/proxyFetch.js`)\n\n## 5) Cloud Sync\n\n- Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js`\n- Periodic task: `src/shared/services/cloudSyncScheduler.js`\n- Control route: `src/app/api/sync/cloud/route.js`\n\n## Request Lifecycle (`/v1/chat/completions`)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant Client as CLI/SDK Client\n    participant Route as /api/v1/chat/completions\n    participant Chat as src/sse/handlers/chat\n    participant Core as open-sse/handlers/chatCore\n    participant Model as Model Resolver\n    participant Auth as Credential Selector\n    participant Exec as Provider Executor\n    participant Prov as Upstream Provider\n    participant Stream as Stream Translator\n    participant Usage as usageDb\n\n    Client->>Route: POST /v1/chat/completions\n    Route->>Chat: handleChat(request)\n    Chat->>Model: parse/resolve model or combo\n\n    alt Combo model\n        Chat->>Chat: iterate combo models (handleComboChat)\n    end\n\n    Chat->>Auth: getProviderCredentials(provider)\n    Auth-->>Chat: active account + tokens/api key\n\n    Chat->>Core: handleChatCore(body, modelInfo, credentials)\n    Core->>Core: detect source format\n    Core->>Core: translate request to target format\n    Core->>Exec: execute(provider, transformedBody)\n    Exec->>Prov: upstream API call\n    Prov-->>Exec: SSE/JSON response\n    Exec-->>Core: response + metadata\n\n    alt 401/403\n        Core->>Exec: refreshCredentials()\n        Exec-->>Core: updated tokens\n        Core->>Exec: retry request\n    end\n\n    Core->>Stream: translate/normalize stream to client format\n    Stream-->>Client: SSE chunks / JSON response\n\n    Stream->>Usage: extract usage + persist history/log\n```\n\n## Combo + Account Fallback Flow\n\n```mermaid\nflowchart TD\n    A[Incoming model string] --> B{Is combo name?}\n    B -- Yes --> C[Load combo models sequence]\n    B -- No --> D[Single model path]\n\n    C --> E[Try model N]\n    E --> F[Resolve provider/model]\n    D --> F\n\n    F --> G[Select account credentials]\n    G --> H{Credentials available?}\n    H -- No --> I[Return provider unavailable]\n    H -- Yes --> J[Execute request]\n\n    J --> K{Success?}\n    K -- Yes --> L[Return response]\n    K -- No --> M{Fallback-eligible error?}\n\n    M -- No --> N[Return error]\n    M -- Yes --> O[Mark account unavailable cooldown]\n    O --> P{Another account for provider?}\n    P -- Yes --> G\n    P -- No --> Q{In combo with next model?}\n    Q -- Yes --> E\n    Q -- No --> R[Return all unavailable]\n```\n\nFallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics.\n\n## OAuth Onboarding and Token Refresh Lifecycle\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Dashboard UI\n    participant OAuth as /api/oauth/[provider]/[action]\n    participant ProvAuth as Provider Auth Server\n    participant DB as localDb\n    participant Test as /api/providers/[id]/test\n    participant Exec as Provider Executor\n\n    UI->>OAuth: GET authorize or device-code\n    OAuth->>ProvAuth: create auth/device flow\n    ProvAuth-->>OAuth: auth URL or device code payload\n    OAuth-->>UI: flow data\n\n    UI->>OAuth: POST exchange or poll\n    OAuth->>ProvAuth: token exchange/poll\n    ProvAuth-->>OAuth: access/refresh tokens\n    OAuth->>DB: createProviderConnection(oauth data)\n    OAuth-->>UI: success + connection id\n\n    UI->>Test: POST /api/providers/[id]/test\n    Test->>Exec: validate credentials / optional refresh\n    Exec-->>Test: valid or refreshed token info\n    Test->>DB: update status/tokens/errors\n    Test-->>UI: validation result\n```\n\nRefresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`.\n\n## Cloud Sync Lifecycle (Enable / Sync / Disable)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Endpoint Page UI\n    participant Sync as /api/sync/cloud\n    participant DB as localDb\n    participant Cloud as External Cloud Sync\n    participant Claude as ~/.claude/settings.json\n\n    UI->>Sync: POST action=enable\n    Sync->>DB: set cloudEnabled=true\n    Sync->>DB: ensure API key exists\n    Sync->>Cloud: POST /sync/{machineId} (providers/aliases/combos/keys)\n    Cloud-->>Sync: sync result\n    Sync->>Cloud: GET /{machineId}/v1/verify\n    Sync-->>UI: enabled + verification status\n\n    UI->>Sync: POST action=sync\n    Sync->>Cloud: POST /sync/{machineId}\n    Cloud-->>Sync: remote data\n    Sync->>DB: update newer local tokens/status\n    Sync-->>UI: synced\n\n    UI->>Sync: POST action=disable\n    Sync->>DB: set cloudEnabled=false\n    Sync->>Cloud: DELETE /sync/{machineId}\n    Sync->>Claude: switch ANTHROPIC_BASE_URL back to local (if needed)\n    Sync-->>UI: disabled\n```\n\nPeriodic sync is triggered by `CloudSyncScheduler` when cloud is enabled.\n\n## Data Model and Storage Map\n\n```mermaid\nerDiagram\n    SETTINGS ||--o{ PROVIDER_CONNECTION : controls\n    PROVIDER_NODE ||--o{ PROVIDER_CONNECTION : backs_compatible_provider\n    PROVIDER_CONNECTION ||--o{ USAGE_ENTRY : emits_usage\n\n    SETTINGS {\n      boolean cloudEnabled\n      number stickyRoundRobinLimit\n      boolean requireLogin\n      string password_hash\n    }\n\n    PROVIDER_CONNECTION {\n      string id\n      string provider\n      string authType\n      string name\n      number priority\n      boolean isActive\n      string apiKey\n      string accessToken\n      string refreshToken\n      string expiresAt\n      string testStatus\n      string lastError\n      string rateLimitedUntil\n      json providerSpecificData\n    }\n\n    PROVIDER_NODE {\n      string id\n      string type\n      string name\n      string prefix\n      string apiType\n      string baseUrl\n    }\n\n    MODEL_ALIAS {\n      string alias\n      string targetModel\n    }\n\n    COMBO {\n      string id\n      string name\n      string[] models\n    }\n\n    API_KEY {\n      string id\n      string name\n      string key\n      string machineId\n      boolean isActive\n    }\n\n    USAGE_ENTRY {\n      string provider\n      string model\n      number prompt_tokens\n      number completion_tokens\n      string connectionId\n      string timestamp\n    }\n```\n\nPhysical storage files:\n\n- main state: `${DATA_DIR}/db.json` (or `~/.9router/db.json`)\n- usage stats: `~/.9router/usage.json`\n- request log lines: `~/.9router/log.txt`\n- optional translator/request debug sessions: `<repo>/logs/...`\n\n## Deployment Topology\n\n```mermaid\nflowchart LR\n    subgraph LocalHost[Developer Host]\n        CLI[CLI Tools]\n        Browser[Dashboard Browser]\n    end\n\n    subgraph ContainerOrProcess[9Router Runtime]\n        Next[Next.js Server\\nPORT=20128]\n        Core[SSE Core + Executors]\n        MainDB[(db.json)]\n        UsageDB[(usage.json/log.txt)]\n    end\n\n    subgraph External[External Services]\n        Providers[AI Providers]\n        SyncCloud[Cloud Sync Service]\n    end\n\n    CLI --> Next\n    Browser --> Next\n    Next --> Core\n    Next --> MainDB\n    Core --> MainDB\n    Core --> UsageDB\n    Core --> Providers\n    Next --> SyncCloud\n```\n\n## Module Mapping (Decision-Critical)\n\n### Route and API Modules\n\n- `src/app/api/v1/*`, `src/app/api/v1beta/*`: compatibility APIs\n- `src/app/api/providers*`: provider CRUD, validation, testing\n- `src/app/api/provider-nodes*`: custom compatible node management\n- `src/app/api/oauth/*`: OAuth/device-code flows\n- `src/app/api/keys*`: local API key lifecycle\n- `src/app/api/models/alias`: alias management\n- `src/app/api/combos*`: fallback combo management\n- `src/app/api/pricing`: pricing overrides for cost calculation\n- `src/app/api/usage/*`: usage and logs APIs\n- `src/app/api/sync/*` + `src/app/api/cloud/*`: cloud sync and cloud-facing helpers\n- `src/app/api/cli-tools/*`: local CLI config writers/checkers\n\n### Routing and Execution Core\n\n- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop\n- `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup\n- `open-sse/executors/*`: provider-specific network and format behavior\n\n### Translation Registry and Format Converters\n\n- `open-sse/translator/index.js`: translator registry and orchestration\n- Request translators: `open-sse/translator/request/*`\n- Response translators: `open-sse/translator/response/*`\n- Format constants: `open-sse/translator/formats.js`\n\n### Persistence\n\n- `src/lib/localDb.js`: persistent config/state\n- `src/lib/usageDb.js`: usage history and rolling request logs\n\n## Provider Executor Coverage\n\nSpecialized executors:\n\n- `antigravity`\n- `gemini-cli`\n- `github`\n- `kiro`\n- `codex`\n- `cursor`\n\nDefault executor path:\n\n- all other providers (including compatible node providers) use `open-sse/executors/default.js`\n\n## Format Translation Coverage\n\nDetected source formats include:\n\n- `openai`\n- `openai-responses`\n- `claude`\n- `gemini`\n\nTarget formats include:\n\n- OpenAI chat/Responses\n- Claude\n- Gemini/Gemini-CLI/Antigravity envelope\n- Kiro\n- Cursor\n\nTranslations are selected dynamically based on source payload shape and provider target format.\n\n## Failure Modes and Resilience\n\n## 1) Account/Provider Availability\n\n- provider account cooldown on transient/rate/auth errors\n- account fallback before failing request\n- combo model fallback when current model/provider path is exhausted\n\n## 2) Token Expiry\n\n- pre-check and refresh with retry for refreshable providers\n- 401/403 retry after refresh attempt in core path\n\n## 3) Stream Safety\n\n- disconnect-aware stream controller\n- translation stream with end-of-stream flush and `[DONE]` handling\n- usage estimation fallback when provider usage metadata is missing\n\n## 4) Cloud Sync Degradation\n\n- sync errors are surfaced but local runtime continues\n- scheduler has retry-capable logic, but periodic execution currently calls single-attempt sync by default\n\n## 5) Data Integrity\n\n- DB shape migration/repair for missing keys\n- corrupt JSON reset safeguards for localDb and usageDb\n\n## Observability and Operational Signals\n\nRuntime visibility sources:\n\n- console logs from `src/sse/utils/logger.js`\n- per-request usage aggregates in `usage.json`\n- textual request status log in `log.txt`\n- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true`\n- dashboard usage endpoints (`/api/usage/*`) for UI consumption\n\n## Security-Sensitive Boundaries\n\n- JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing\n- Initial password fallback (`INITIAL_PASSWORD`, default `123456`) must be overridden in real deployments\n- API key HMAC secret (`API_KEY_SECRET`) secures generated local API key format\n- Provider secrets (API keys/tokens) are persisted in local DB and should be protected at filesystem level\n- Cloud sync endpoints rely on API key auth + machine id semantics\n\n## Environment and Runtime Matrix\n\nEnvironment variables actively used by code:\n\n- App/auth: `JWT_SECRET`, `INITIAL_PASSWORD`\n- Storage: `DATA_DIR`\n- Security hashing: `API_KEY_SECRET`, `MACHINE_ID_SALT`\n- Logging: `ENABLE_REQUEST_LOGS`\n- Sync/cloud URLing: `NEXT_PUBLIC_BASE_URL`, `NEXT_PUBLIC_CLOUD_URL`\n- Outbound proxy: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` and lowercase variants\n- Platform/runtime helpers (not app-specific config): `APPDATA`, `NODE_ENV`, `PORT`, `HOSTNAME`\n\n## Known Architectural Notes\n\n1. `usageDb` currently stores under `~/.9router` and does not follow `DATA_DIR`.\n2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`.\n3. Request logger writes full headers/body when enabled; treat log directory as sensitive.\n4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability.\n\n## Operational Verification Checklist\n\n- Build from source: `cd /root/dev/9router && npm run build`\n- Build Docker image: `cd /root/dev/9router && docker build -t 9router .`\n- Start service and verify:\n- `GET /api/settings`\n- `GET /api/v1/models`\n- CLI target base URL should be `http://<host>:20128/v1` when `PORT=20128`\n","open-sse/AGENTS.md":"# open-sse\n\nProvider-agnostic SSE engine: one OpenAI-style request → any provider (LLM chat, image, embedding, tts, stt, search), streamed back in the client's format.\n\n## Request lifecycle (chat)\n\n`handlers/chatCore.js` → `services/model.js` `parseModel` (resolve `provider/model`) → **RTK for `cursor`** (`rtk/` compresses the source-format `tool_result` / `role:tool` in-place — its translator rewrites those shapes, so this one provider must run **before** translate) → `translator/index.js` `translateRequest` (client format → provider format) → **post-translate savers** (`rtk/` compress for every other provider, `rtk/headroom.js` proxy compress, `rtk/caveman.js` / `rtk/ponytail.js` system inject — all fail-open) → `executors/index.js` `getExecutor(provider)` → `executor.execute()` (streams upstream) → `translateResponse` (provider chunks → client format) → SSE out.\n\n## Directory map\n\n- `config/` — ALL constants/config (no hardcode elsewhere). `providers.js`/`registry/` (provider defs), `providerModels.js` (alias→models matrix), `runtimeConfig.js` (timeouts, token limits), `*Constants.js`.\n- `translator/` — format conversion. `request/<from>-to-<to>.js`, `response/<from>-to-<to>.js`, `schema/` (enums: ROLE, CLAUDE_BLOCK…), `concerns/` (shared logic), `formats.js`+`formats/` (per-format). `index.js` is the registry/entry.\n- `executors/` — per-provider upstream call. `base.js` (BaseExecutor), one file per special provider, `index.js` map.\n- `providers/` — registry build + `capabilities.js` + `pricing.js`. Entry: `index.js` (PROVIDERS).\n- `handlers/` — per-modality cores (chat/image/embedding/tts/stt/search) + sub-provider folders. `chatCore/` has the streaming/non-streaming/sse-to-json handlers.\n- `rtk/` — request token-killer. `index.js` compresses `tool_result` content in-place (OpenAI/Claude/Kiro shapes); `filters/` per-tool compressors + `autodetect.js`; `headroom.js` external compress proxy; `caveman.js` system-prompt injector.\n- `transformer/` — `responsesTransformer.js` (Chat Completions SSE → Codex Responses API SSE), `streamToJsonConverter.js`.\n- `shared/` — cross-provider auth/identity: `clineAuth.js`, `machineId.js`, `qoder/`.\n- `services/` — `model.js`, `provider.js`, `accountFallback.js`, `combo.js`, `compact.js`, `tokenRefresh/`+`tokenRefresh.js`, `oauthCredentialManager.js`, `usage/`, `projectId.js`, `kiroModels.js`/`qoderModels.js`.\n- `utils/` — streamHandler, stream, sse, error, sessionManager, claudeCloaking, clientDetector, proxyFetch (patches global fetch), cursorProtobuf/cursorChecksum, ollamaTransform.\n\n## Conventions\n\n- Config-driven, DRY, camelCase. NEVER hardcode values, models, or block/role strings — use `config/` + `schema/` constants.\n- Translator pipeline pivots through OpenAI as the intermediate format. A translator registered on the exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop.\n- Translators self-register via `register(from, to, reqFn, resFn)` as an import side-effect — new files MUST be imported in `translator/index.js`.\n\n## How to add\n\n- **Provider**: copy `providers/REGISTRY_TEMPLATE.js` → `providers/registry/{id}.js`; add models to `config/providerModels.js`. Generic providers need no executor (DefaultExecutor handles OpenAI-compatible APIs).\n- **Executor** (only for non-standard upstream): subclass `BaseExecutor` (override `getBaseUrls`/`buildHeaders`/`buildUrl`/`execute`), register in `executors/index.js` map. `getExecutor` falls back to `DefaultExecutor` when absent.\n- **Translator**: add `request|response/<from>-to-<to>.js` calling `register(...)`, then import it in `translator/index.js`. Reuse `schema/` + `concerns/` — don't re-implement parsing.\n\n## Pitfalls\n\n- OpenAI bridge is lossy (thinking, non-base64 images, tool ids, is_error) — prefer a direct route for fragile pairs.\n- `registry/index.js` is an auto-generated static import list; regenerate it (don't hand-edit) after adding a `registry/{id}.js`. REGISTRY_TEMPLATE is excluded by design.\n- Special binary/protobuf formats (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — handle in their executor.\n- `rtk/` + `headroom.js` mutate the request body in-place and are **fail-open**: any error returns null and leaves the body untouched — never throw out of them. RTK skips `is_error`/`status:\"error\"` tool results to preserve traces.\n","skills/9router-chat/SKILL.md":"---\nname: 9router-chat\ndescription: Chat / code generation via 9Router using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through 9Router.\n---\n\n# 9Router — Chat\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Endpoints\n\n- `POST $NINEROUTER_URL/v1/chat/completions` — OpenAI format\n- `POST $NINEROUTER_URL/v1/messages` — Anthropic format\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models | jq '.data[].id'\n# Per-model metadata (contextWindow, params)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/gpt-4o\"\n```\n\nCombos (e.g. `vip`, `mycodex`) auto-fallback through multiple providers.\n\n## OpenAI format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/chat/completions \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/gpt-5\",\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}],\"stream\":false}'\n```\n\nJS (OpenAI SDK):\n\n```js\nimport OpenAI from \"openai\";\nconst client = new OpenAI({ baseURL: `${process.env.NINEROUTER_URL}/v1`, apiKey: process.env.NINEROUTER_KEY });\nconst res = await client.chat.completions.create({\n  model: \"openai/gpt-5\",\n  messages: [{ role: \"user\", content: \"Hi\" }],\n  stream: true,\n});\nfor await (const chunk of res) process.stdout.write(chunk.choices[0]?.delta?.content || \"\");\n```\n\n## Anthropic format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/messages \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"anthropic-version: 2023-06-01\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"cc/claude-opus-4-7\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}]}'\n```\n\n## Response shape\n\nOpenAI (`/v1/chat/completions`):\n```json\n{ \"id\": \"chatcmpl-...\", \"object\": \"chat.completion\", \"model\": \"openai/gpt-5\",\n  \"choices\": [{ \"index\": 0, \"message\": { \"role\": \"assistant\", \"content\": \"Hello!\" }, \"finish_reason\": \"stop\" }],\n  \"usage\": { \"prompt_tokens\": 8, \"completion_tokens\": 2, \"total_tokens\": 10 } }\n```\n\nStreaming (`stream:true`) emits SSE: `data: {choices:[{delta:{content:\"...\"}}]}\\n\\n` ... `data: [DONE]\\n\\n`.\n\nAnthropic (`/v1/messages`):\n```json\n{ \"id\": \"msg_...\", \"type\": \"message\", \"role\": \"assistant\", \"model\": \"cc/claude-opus-4-7\",\n  \"content\": [{ \"type\": \"text\", \"text\": \"Hello!\" }],\n  \"stop_reason\": \"end_turn\", \"usage\": { \"input_tokens\": 8, \"output_tokens\": 2 } }\n```\n","skills/9router-embeddings/SKILL.md":"---\nname: 9router-embeddings\ndescription: Generate vector embeddings via 9Router /v1/embeddings using OpenAI / Gemini / Mistral / Voyage / Nvidia / GitHub embedding models for RAG, semantic search, similarity. Use when the user wants embeddings, vectors, RAG, semantic search, or to embed text.\n---\n\n# 9Router — Embeddings\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/embedding | jq '.data[].id'\n# Per-model dimensions\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/text-embedding-3-small\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/embeddings`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/embedding` |\n| `input` | yes | string OR array of strings |\n| `encoding_format` | no | `float` (default) / `base64` |\n| `dimensions` | no | OpenAI v3 only |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/embeddings \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/text-embedding-3-small\",\"input\":[\"hello\",\"world\"]}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/embeddings`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/text-embedding-004\", input: \"RAG chunk text\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].embedding.length);  // dimension\n```\n\n## Response shape\n\n```json\n{ \"object\": \"list\", \"model\": \"openai/text-embedding-3-small\",\n  \"data\": [\n    { \"object\": \"embedding\", \"index\": 0, \"embedding\": [0.0123, -0.045, ...] },\n    { \"object\": \"embedding\", \"index\": 1, \"embedding\": [...] }\n  ],\n  \"usage\": { \"prompt_tokens\": 5, \"total_tokens\": 5 } }\n```\n\n## Provider quirks\n\n| Provider | Notes |\n|---|---|\n| `openai`, `openrouter`, `mistral`, `voyage-ai`, `fireworks`, `together`, `nebius`, `github`, `nvidia`, `jina-ai` | Native OpenAI shape — `dimensions` works only on OpenAI v3 (`text-embedding-3-*`) |\n| `gemini`, `google_ai_studio` | Server auto-converts to `embedContent`/`batchEmbedContents` — send OpenAI shape |\n| `openai-compatible-*`, `custom-embedding-*` | Custom `baseUrl` from credentials |\n\nBatch (`input` as array) is faster; some providers cap batch size.\n","skills/9router-image/SKILL.md":"---\nname: 9router-image\ndescription: Generate images via 9Router /v1/images/generations using OpenAI / Gemini Imagen / DALL-E / FLUX / MiniMax / SDWebUI / ComfyUI / Codex models. Use when the user wants to create, generate, draw, or render an image, picture, or text-to-image (txt2img).\n---\n\n# 9Router — Image Generation\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/image | jq '.data[].id'\n# Per-model params/options (size enum, quality enum, capabilities like edit)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/dall-e-3\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/images/generations`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/image` |\n| `prompt` | yes | image description |\n| `n` | no | count (provider-dependent) |\n| `size` | no | `1024x1024`, `1792x1024`, ... |\n| `quality` | no | `standard` / `hd` (OpenAI) |\n| `response_format` | no | `url` (default) or `b64_json` |\n\nAdd query `?response_format=binary` to receive raw image bytes (handy for saving file).\n\n## Examples\n\nSave to file (binary):\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/images/generations?response_format=binary\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"gemini/gemini-3-pro-image-preview\",\"prompt\":\"watercolor mountains at sunrise\",\"size\":\"1024x1024\"}' \\\n  --output out.png\n```\n\nJS (URL response):\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/images/generations`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/gemini-3-pro-image-preview\", prompt: \"neon city\", size: \"1024x1024\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].url || data[0].b64_json.slice(0, 40));\n```\n\n## Response shape\n\nJSON (default `response_format=url`):\n```json\n{ \"created\": 1735000000, \"data\": [{ \"url\": \"https://...\" }] }\n```\n\n`response_format=b64_json`:\n```json\n{ \"created\": 1735000000, \"data\": [{ \"b64_json\": \"iVBORw0KGgo...\" }] }\n```\n\nQuery `?response_format=binary` returns raw image bytes (Content-Type `image/png` or `image/jpeg`).\n\n## Provider quirks\n\nCommon fields above work everywhere. These add/override:\n\n| Provider | Extra/changed fields | Notes |\n|---|---|---|\n| `openai`, `minimax`, `openrouter`, `recraft` | `quality`, `style`, `response_format` | Standard OpenAI shape |\n| `gemini` (nano-banana) | — | Only `prompt`; ignores `size`/`n` |\n| `codex` (gpt-5.4-image) | `image`, `images[]`, `image_detail`, `output_format`, `background` | SSE stream; **ChatGPT Plus/Pro required** |\n| `huggingface` | — | Only `prompt`; returns single image |\n| `nanobanana` | `image`, `images[]` (edit mode) | `size` → aspect ratio; async polling |\n| `fal-ai` | `image` (img2img) | `n` → `num_images`; `size` → ratio; async |\n| `stability-ai` | `style` (preset), `output_format` | `size` → `aspect_ratio` |\n| `black-forest-labs` (FLUX) | `image` (ref) | `size` → exact `width`/`height`; async |\n| `runwayml` | `image` (ref) | `size` → ratio; async; video models exist |\n| `sdwebui`, `comfyui` | — | Localhost noAuth (`:7860` / `:8188`) |\n","skills/9router-stt/SKILL.md":"---\nname: 9router-stt\ndescription: Speech-to-text via 9Router /v1/audio/transcriptions using OpenAI Whisper / Groq / Gemini / Deepgram / AssemblyAI / NVIDIA / HuggingFace models. Use when the user wants to transcribe audio, convert speech to text, or get subtitles from audio files.\n---\n\n# 9Router — Speech-to-Text\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/stt | jq '.data[].id'\n# Per-model params (language, response_format, prompt, temperature support)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/whisper-1\"\n```\n\n`model` = STT model ID (e.g. `openai/whisper-1`, `groq/whisper-large-v3`, `deepgram/nova-3`, `gemini/gemini-2.5-flash`).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/transcriptions` (OpenAI Whisper compatible, `multipart/form-data`)\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/stt` |\n| `file` | yes | audio file (mp3, wav, m4a, webm, ogg, flac) |\n| `language` | no | ISO-639-1 (e.g. `en`, `vi`) |\n| `prompt` | no | hint text to guide transcription |\n| `response_format` | no | `json` (default) / `text` / `verbose_json` / `srt` / `vtt` |\n| `temperature` | no | 0–1 |\n\n## Examples\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/transcriptions\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -F \"model=openai/whisper-1\" \\\n  -F \"file=@audio.mp3\" \\\n  -F \"language=vi\"\n```\n\nJS (Node):\n\n```js\nimport { createReadStream } from \"node:fs\";\nconst form = new FormData();\nform.append(\"model\", \"groq/whisper-large-v3-turbo\");\nform.append(\"file\", new Blob([await (await import(\"node:fs/promises\")).readFile(\"audio.mp3\")]), \"audio.mp3\");\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/transcriptions`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}` },\n  body: form,\n});\nconst { text } = await r.json();\nconsole.log(text);\n```\n\n## Response shape\n\nDefault (`response_format=json`):\n```json\n{ \"text\": \"Xin chào, đây là bản ghi âm.\" }\n```\n\n`verbose_json` adds `language`, `duration`, `segments[]` with timestamps.\n`srt` / `vtt` return subtitle text.\n\n## Provider quirks\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe` | Native OpenAI shape |\n| `groq` | `whisper-large-v3`, `whisper-large-v3-turbo`, `distil-whisper-large-v3-en` | Fastest; OpenAI shape |\n| `gemini` | `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-2.5-flash-lite` | Server converts to `generateContent` with audio inline |\n| `deepgram` | `nova-3`, `nova-2`, `whisper-large` | Token auth; server adapts response |\n| `assemblyai` | `universal-3-pro`, `universal-2` | Async upload+poll handled server-side |\n| `nvidia` | `nvidia/parakeet-ctc-1.1b-asr` | NIM endpoint |\n| `huggingface` | `openai/whisper-large-v3`, `openai/whisper-small` | HF Inference API |\n| `elevenlabs` | `scribe_v1`, `scribe_v2` | Whisper-compatible shape; `xi-api-key` auth, not Bearer. Extra params: `timestamps_granularity` (`word`/`character`/`none`), `tag_audio_events`, and speaker labelling via **either** `diarize=true` **or** `num_speakers` (1–32) — sending both makes the request invalid, so `diarize` wins and `num_speakers` is dropped. Blank `language` is omitted upstream for auto-detect. `srt`/`vtt` and `verbose_json` segments are served from Scribe's own `additional_formats` render, so `segments[]` is omitted when the upstream render is unavailable rather than synthesized. Does not accept `temperature` or `prompt` — those are not forwarded. |\n","skills/9router-tts/SKILL.md":"---\nname: 9router-tts\ndescription: Text-to-speech via 9Router /v1/audio/speech using OpenAI / ElevenLabs / Deepgram / Edge TTS / Google TTS / Hyperbolic / Inworld voices. Use when the user wants to convert text to speech, generate audio, voiceover, narrate, or read text aloud.\n---\n\n# 9Router — Text-to-Speech\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\n# 1) List models\ncurl $NINEROUTER_URL/v1/models/tts | jq '.data[].id'\n# 2) Per-model metadata (params, voicesUrl if voice-by-id)\ncurl \"$NINEROUTER_URL/v1/models/info?id=el/eleven_multilingual_v2\"\n# 3) List voices (elevenlabs, edge-tts, deepgram, inworld, local-device). Optional ?lang=vi\ncurl \"$NINEROUTER_URL/v1/audio/voices?provider=edge-tts&lang=vi\" | jq '.data[].model'\n```\n\n`model` field in `/v1/audio/speech` = voice ID directly (e.g. `edge-tts/vi-VN-HoaiMyNeural`, `el/<voice_id>`, or `openai/tts-1` model+default voice).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/speech`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | voice ID from `/v1/models/tts` |\n| `input` | yes | text to speak |\n\nQuery `?response_format=mp3` (default, raw bytes) or `?response_format=json` (`{audio: base64, format}`).\n\n## Examples\n\nSave MP3:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/speech\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/tts-1\",\"input\":\"Hello world\"}' \\\n  --output speech.mp3\n```\n\nJS (save file):\n\n```js\nimport { writeFile } from \"node:fs/promises\";\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/speech`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"el/eleven_multilingual_v2\", input: \"Xin chào\" }),\n});\nawait writeFile(\"speech.mp3\", Buffer.from(await r.arrayBuffer()));\n```\n\n## Response shape\n\nDefault → raw audio bytes (Content-Type `audio/mp3`).\n\n`?response_format=json`:\n```json\n{ \"audio\": \"SUQzBAAAA...\", \"format\": \"mp3\" }\n```\n\n## Provider quirks (model format)\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `tts-1/alloy` (model/voice) or just voice | Default model `gpt-4o-mini-tts` |\n| `elevenlabs` | `<model_id>/<voice_id>` or `<voice_id>` | Default model `eleven_flash_v2_5`; list voices in Dashboard |\n| `openrouter` | `openai/gpt-4o-mini-tts/alloy` | Streamed via chat-completions audio modality |\n| `edge-tts` | voice id e.g. `vi-VN-HoaiMyNeural` | **noAuth**; default `vi-VN-HoaiMyNeural` |\n| `google-tts` | language code e.g. `en`, `vi` | **noAuth** |\n| `local-device` | OS voice name (`say -v ?` / SAPI) | **noAuth**; needs `ffmpeg` |\n| `deepgram` | `aura-asteria-en` etc | Token auth |\n| `nvidia`, `inworld`, `cartesia`, `playht` | `model/voice` | Provider-specific auth header |\n| `coqui`, `tortoise` | speaker / voice id | Localhost noAuth |\n| `hyperbolic` | model id | Body = `{text}` only |\n","skills/9router-video/SKILL.md":"---\nname: 9router-video\ndescription: 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.\n---\n\n# 9Router — Video Generation (xAI Grok Imagine)\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\nRequires 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.\n\n## Endpoints (async job flow)\n\nVideo generation is **asynchronous**: the POST returns a `request_id` immediately, then you poll until the job is `done` or `failed`.\n\n| Endpoint | Purpose |\n|---|---|\n| `POST /v1/videos/generations` | text-to-video / image-to-video |\n| `POST /v1/videos/edits` | edit an existing video |\n| `POST /v1/videos/extensions` | extend an existing video |\n| `GET /v1/videos/{request_id}` | poll job status |\n\nRequest fields (passed through to xAI unchanged — see https://docs.x.ai/developers/rest-api-reference/inference/videos):\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | no | `xai/grok-imagine-video` (prefix is stripped before upstream) |\n| `prompt` | yes for T2V | video description |\n| `duration` | no | seconds |\n| `aspect_ratio` | no | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `3:2`, `2:3` |\n| `resolution` | no | `480p`, `720p`, `1080p` |\n| `image` | no | `{ \"url\": \"https://… or data:image/…;base64,…\" }` for image-to-video |\n| `video` | edits/extensions | `{ \"url\": \"…mp4\" }` or `{ \"file_id\": \"…\" }` |\n\n## Examples\n\nSubmit a job:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/videos/generations\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -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\"}'\n# → {\"request_id\":\"abc123\"}   (response header x-9router-connection-id: <id>)\n```\n\nPoll until done (echo the connection header back so the same account polls the job):\n\n```bash\ncurl \"$NINEROUTER_URL/v1/videos/abc123\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"x-connection-id: <id from create response>\"\n# → {\"status\":\"pending\",\"progress\":42}\n# → {\"status\":\"done\",\"video\":{\"url\":\"https://…mp4\",\"duration\":8},\"model\":\"grok-imagine-video\"}\n# → {\"status\":\"failed\",\"error\":{\"code\":\"…\",\"message\":\"…\"}}\n```\n\nDownload: fetch `video.url` from the `done` response.\n\n## CLI one-shot\n\n```bash\n9router xai video \\\n  --prompt \"A cinematic tracking shot through a neon city at night\" \\\n  --output video.mp4\n# options: --model --duration --aspect-ratio --resolution --image --timeout --port --api-key\n```\n\nSubmits, polls with progress, downloads to `video.mp4.part`, atomically renames on success. Ctrl+C cancels cleanly; non-zero exit on failure.\n\n## Notes & limits\n\n- 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`).\n- 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.\n- Video models are tagged `kind: \"video\"` and are excluded from chat model lists and chat fallback combos.\n- 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.\n","skills/9router-web-fetch/SKILL.md":"---\nname: 9router-web-fetch\ndescription: Fetch URL → markdown / text / HTML via 9Router /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown.\n---\n\n# 9Router — Web Fetch\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webFetch\") | .id'\n# Per-provider params\ncurl \"$NINEROUTER_URL/v1/models/info?id=firecrawl/fetch\"\n```\n\nIDs end in `/fetch` (e.g. `firecrawl/fetch`, `jina/fetch`). `fetch-combo` chains providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/web/fetch`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `firecrawl` or `jina-reader`) |\n| `url` | yes | URL to extract |\n| `format` | no | `markdown` (default) / `text` / `html` |\n| `max_characters` | no | truncate output |\n\n## Examples\n\n### Jina Reader\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"jina-reader\",\"url\":\"https://9router.com\",\"format\":\"markdown\"}'\n```\n\n### Exa\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"exa\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Firecrawl\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"firecrawl\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Tavily\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Ollama Cloud\n\nUses the API key from the existing `ollama` connection.\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"ollama\",\"url\":\"https://example.com\",\"format\":\"markdown\"}'\n```\n\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/web/fetch`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"fetch-combo\", url: \"https://example.com\", format: \"markdown\", max_characters: 5000 }),\n});\nconst { data } = await r.json();\nconsole.log(data.title, data.content.length);\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"jina-reader\",\n  \"url\": \"...\",\n  \"title\": \"...\",\n  \"content\": { \"format\": \"markdown\", \"text\": \"...\", \"length\": 1234 },\n  \"links\": [\"https://example.com/related\"],\n  \"metadata\": { \"author\": null, \"published_at\": null, \"language\": null },\n  \"usage\": { \"fetch_cost_usd\": 0 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700 }\n}\n```\n\n`links` is included when the upstream provider returns discovered page links (currently Ollama Cloud).\n\n## Provider quirks\n\n| Provider | Auth | Best for |\n|---|---|---|\n| `firecrawl` | Bearer | JS-rendered pages, `format=markdown/html` |\n| `jina-reader` | Bearer (optional) | Free tier (~1M chars/mo); fastest plain markdown |\n| `tavily` | Bearer | Bulk extract; returns `raw_content` |\n| `exa` | `x-api-key` | Pre-indexed pages; fast text extraction |\n| `ollama` | Bearer | Markdown plus page title and discovered links; uses the Ollama Cloud key |\n","skills/9router-web-search/SKILL.md":"---\nname: 9router-web-search\ndescription: Web and X search via 9Router /v1/search using Tavily / Exa / Brave / Serper / SearXNG / Google PSE / Linkup / SearchAPI / You.com / Perplexity / Xquik. Use when the user wants to search the web, find articles, or search public X posts.\n---\n\n# 9Router — Web Search\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webSearch\") | .id'\n# Per-provider params (searchTypes, maxResults, required options like cx for google-pse)\ncurl \"$NINEROUTER_URL/v1/models/info?id=tavily/search\"\n```\n\nIDs end in `/search` (e.g. `tavily/search`). Combos (`owned_by:\"combo\"`) chain providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/search`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `tavily` or `brave`) |\n| `query` | yes | search query |\n| `max_results` | no | default 5 |\n| `search_type` | no | `web` (default) / `news` / `x` for Xquik |\n| `country`, `language`, `time_range`, `domain_filter` | no | provider-dependent |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"query\":\"9Router open source\",\"max_results\":5}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/search`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"search-combo\", query: \"latest LLM benchmarks\", max_results: 10 }),\n});\nconsole.log(await r.json());\n```\n\nX search with Xquik:\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"xquik\",\"query\":\"from:github release\",\"max_results\":10,\"provider_options\":{\"queryType\":\"Latest\"}}'\n```\n\nAdd the Xquik API key in 9Router's provider settings. Xquik charges 1 credit per returned post. Continue a search by passing `pagination.next_cursor` as `provider_options.cursor`.\n\nXquik responses include provider pagination and credit usage:\n\n```json\n{\n  \"pagination\": { \"has_more\": true, \"next_cursor\": \"cursor-2\" },\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": null, \"provider_credits_used\": 10 }\n}\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"tavily\",\n  \"query\": \"9Router open source\",\n  \"results\": [\n    {\n      \"title\": \"...\", \"url\": \"https://...\", \"display_url\": \"github.com/...\",\n      \"snippet\": \"...\", \"position\": 1, \"score\": 0.92,\n      \"published_at\": null, \"favicon_url\": null, \"content\": null,\n      \"metadata\": { \"author\": null, \"language\": null, \"source_type\": null, \"image_url\": null },\n      \"citation\": { \"provider\": \"tavily\", \"retrieved_at\": \"2026-...\", \"rank\": 1 }\n    }\n  ],\n  \"answer\": null,\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": 0.008 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700, \"total_results_available\": 12 },\n  \"errors\": []\n}\n```\n\n## Provider quirks\n\nAll accept `query` + `max_results`. Optional fields vary:\n\n| Provider | Supports | Required extras |\n|---|---|---|\n| `tavily` | country, domain_filter, news topic | — |\n| `exa` | domain_filter (incl/excl), news category | — |\n| `brave-search` | country, language | — |\n| `serper` | country, language, news endpoint | — |\n| `perplexity` | country, language, domain_filter | — |\n| `linkup` | domain_filter, time_range | `depth: fast/standard/deep` (option) |\n| `google-pse` | country, language, time_range, offset | **`cx` required** (providerOptions) |\n| `searchapi` | country, language, pagination | — |\n| `youcom` | country, language, time_range, domain_filter, full_page | — |\n| `searxng` | language, time_range | Self-hosted, **noAuth** |\n| `xquik` | X/Twitter search operators, language, cursor pagination | `queryType: Latest/Top`, `cursor` (options) |\n\nProvider IS the model — `\"provider\":\"tavily\" ≡ \"model\":\"tavily\"`.\n","skills/9router/SKILL.md":"---\nname: 9router\ndescription: Entry point for 9Router — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions 9Router, NINEROUTER_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed.\n---\n\n# 9Router\n\nLocal/remote AI gateway exposing OpenAI-compatible REST. One key, many providers, auto-fallback.\n\n## Setup\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"      # or VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                      # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nAll requests: `${NINEROUTER_URL}/v1/...` with header `Authorization: Bearer ${NINEROUTER_KEY}` (omit if auth disabled).\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`\n\n## Discover models\n\n```bash\ncurl $NINEROUTER_URL/v1/models                  # chat/LLM (default)\ncurl $NINEROUTER_URL/v1/models/image            # image-gen\ncurl $NINEROUTER_URL/v1/models/tts              # text-to-speech\ncurl $NINEROUTER_URL/v1/models/embedding        # embeddings\ncurl $NINEROUTER_URL/v1/models/web              # web search + fetch (entries have `kind` field)\ncurl $NINEROUTER_URL/v1/models/stt              # speech-to-text\ncurl $NINEROUTER_URL/v1/models/image-to-text    # vision\n```\n\nUse `data[].id` as `model` field in requests. Combos appear with `owned_by:\"combo\"`.\n\nResponse shape:\n```json\n{ \"object\": \"list\", \"data\": [\n  { \"id\": \"openai/gpt-5\", \"object\": \"model\", \"owned_by\": \"openai\", \"created\": 1735000000 },\n  { \"id\": \"tavily/search\", \"object\": \"model\", \"kind\": \"webSearch\", \"owned_by\": \"tavily\", \"created\": 1735000000 }\n]}\n```\n\n## Capability skills\n\nWhen the user needs a specific capability, fetch that skill's `SKILL.md` from its raw URL:\n\n| Capability | Raw URL |\n|---|---|\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## Errors\n\n- 401 → set/refresh `NINEROUTER_KEY` (Dashboard → Keys)\n- 400 `Invalid model format` → check `model` exists in `/v1/models/<kind>`\n- 503 `All accounts unavailable` → wait `retry-after` or add another provider account\n","skills/README.md":"# 9Router — Agent Skills\n\nDrop-in skills for any AI agent (Claude, Cursor, ChatGPT, custom SDK). Just **copy a link** below and paste it to your AI — it will fetch the skill and use 9Router for you.\n\n> Tip: start with the **9router** entry skill — it covers setup and links to all capability skills.\n\n## Skills\n\n| Capability | Copy link below and paste to your AI |\n|---|---|\n| **Entry / Setup** (start here) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md |\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Video generation (xAI Grok Imagine) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-video/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## How to use\n\nPaste to your AI (Claude, Cursor, ChatGPT, …):\n\n```\nRead this skill and use it: https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md\n```\n\nThen ask normally — *\"generate an image of a cat\"*, *\"transcribe this URL\"*, etc.\n\n## Configure your shell once\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"   # local default, or your VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                   # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`.\n\n## Links\n\n- Source: https://github.com/decolua/9router\n- Dashboard: https://9router.com\n"},"items":[{"name":"AGENTS.md","path":"open-sse/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/open-sse/AGENTS.md","title":"open-sse - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# open-sse\n\nProvider-agnostic SSE engine: one OpenAI-style request → any provider (LLM chat, image, embedding, tts, stt, search), streamed back in the client's format.\n\n## Request lifecycle (chat)\n\n`handlers/chatCore.js` → `services/model.js` `parseModel` (resolve `provider/model`) → **RTK for `cursor`** (`rtk/` compresses the source-format `tool_result` / `role:tool` in-place — its translator rewrites those shapes, so this one provider must run **before** translate) → `translator/index.js` `translateRequest` (client format → provider format) → **post-translate savers** (`rtk/` compress for every other provider, `rtk/headroom.js` proxy compress, `rtk/caveman.js` / `rtk/ponytail.js` system inject — all fail-open) → `executors/index.js` `getExecutor(provider)` → `executor.execute()` (streams upstream) → `translateResponse` (provider chunks → client format) → SSE out.\n\n## Directory map\n\n- `config/` — ALL constants/config (no hardcode elsewhere). `providers.js`/`registry/` (provider defs), `providerModels.js` (alias→models matrix), `runtimeConfig.js` (timeouts, token limits), `*Constants.js`.\n- `translator/` — format conversion. `request/<from>-to-<to>.js`, `response/<from>-to-<to>.js`, `schema/` (enums: ROLE, CLAUDE_BLOCK…), `concerns/` (shared logic), `formats.js`+`formats/` (per-format). `index.js` is the registry/entry.\n- `executors/` — per-provider upstream call. `base.js` (BaseExecutor), one file per special provider, `index.js` map.\n- `providers/` — registry build + `capabilities.js` + `pricing.js`. Entry: `index.js` (PROVIDERS).\n- `handlers/` — per-modality cores (chat/image/embedding/tts/stt/search) + sub-provider folders. `chatCore/` has the streaming/non-streaming/sse-to-json handlers.\n- `rtk/` — request token-killer. `index.js` compresses `tool_result` content in-place (OpenAI/Claude/Kiro shapes); `filters/` per-tool compressors + `autodetect.js`; `headroom.js` external compress proxy; `caveman.js` system-prompt injector.\n- `transformer/` — `responsesTransformer.js` (Chat Completions SSE → Codex Responses API SSE), `streamToJsonConverter.js`.\n- `shared/` — cross-provider auth/identity: `clineAuth.js`, `machineId.js`, `qoder/`.\n- `services/` — `model.js`, `provider.js`, `accountFallback.js`, `combo.js`, `compact.js`, `tokenRefresh/`+`tokenRefresh.js`, `oauthCredentialManager.js`, `usage/`, `projectId.js`, `kiroModels.js`/`qoderModels.js`.\n- `utils/` — streamHandler, stream, sse, error, sessionManager, claudeCloaking, clientDetector, proxyFetch (patches global fetch), cursorProtobuf/cursorChecksum, ollamaTransform.\n\n## Conventions\n\n- Config-driven, DRY, camelCase. NEVER hardcode values, models, or block/role strings — use `config/` + `schema/` constants.\n- Translator pipeline pivots through OpenAI as the intermediate format. A translator registered on the exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop.\n- Translators self-register via `register(from, to, reqFn, resFn)` as an import side-effect — new files MUST be imported in `translator/index.js`.\n\n## How to add\n\n- **Provider**: copy `providers/REGISTRY_TEMPLATE.js` → `providers/registry/{id}.js`; add models to `config/providerModels.js`. Generic providers need no executor (DefaultExecutor handles OpenAI-compatible APIs).\n- **Executor** (only for non-standard upstream): subclass `BaseExecutor` (override `getBaseUrls`/`buildHeaders`/`buildUrl`/`execute`), register in `executors/index.js` map. `getExecutor` falls back to `DefaultExecutor` when absent.\n- **Translator**: add `request|response/<from>-to-<to>.js` calling `register(...)`, then import it in `translator/index.js`. Reuse `schema/` + `concerns/` — don't re-implement parsing.\n\n## Pitfalls\n\n- OpenAI bridge is lossy (thinking, non-base64 images, tool ids, is_error) — prefer a direct route for fragile pairs.\n- `registry/index.js` is an auto-generated static import list; regenerate it (don't hand-edit) after adding a `registry/{id}.js`. REGISTRY_TEMPLATE is excluded by design.\n- Special binary/protobuf formats (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — handle in their executor.\n- `rtk/` + `headroom.js` mutate the request body in-place and are **fail-open**: any error returns null and leaves the body untouched — never throw out of them. RTK skips `is_error`/`status:\"error\"` tool results to preserve traces.\n","isInternal":false,"tokens":1171,"sizeBytes":4463},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## What this is\n\n9Router (`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.\n\nTwo published artifacts live in this one repo:\n- The **dashboard + gateway** (root `package.json`, `9router-app`) — the Next.js server that does the actual routing.\n- 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.\n\nThe code lives in `src/` (Next.js app + dashboard/compat APIs), `open-sse/` (the provider-agnostic routing/translation engine), `cli/` (the launcher package), and `tests/`.\n\n## Commands\n\nDashboard/gateway (run from repo root):\n```bash\ncp .env.example .env\nnpm install\nPORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev   # dev (webpack, port 20127 by default via next dev)\nnpm run build && PORT=20128 HOSTNAME=0.0.0.0 npm run start           # production\n```\n- Bun variants: `npm run dev:bun` / `build:bun` / `start:bun`.\n- Default runtime port is **20128** (dashboard at `/dashboard`, API at `/v1`).\n- Lint: `npx eslint .` (config `eslint.config.mjs`, extends `eslint-config-next`).\n\nCLI package (`cli/`):\n```bash\nnpm run cli:pack       # build + npm pack from root\ncd cli && npm run dev  # nodemon watch\n```\n\nTests (vitest, in `tests/`, an **independent** ESM package — not wired into root `npm test`):\n```bash\nnpm install                             # ROOT deps first — tests import from src/ which needs `open`, `undici`, etc.\ncd tests && npm install                 # then tests' own deps (vitest) → tests/node_modules (allowed by tests/.gitignore)\nnpx vitest run                          # all tests; auto-discovers tests/vitest.config.js\nnpx vitest run unit/capabilities.test.js   # single file (path relative to tests/)\n```\n> 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.\n>\n> **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:\n> - 26 catalogued in `tests/__baseline__/known-fails.txt` (rtk, oauth-cursor-auto-import, translator-request-normalization, …).\n> - `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.\n> - `unit/xai-oauth-service.test.js` times out (5s) when the xAI endpoint-discovery fetch isn't reachable/mocked.\n> - `real/*.real.test.js` make live provider calls — need credentials, skip otherwise.\n- `*.real.test.js` under `tests/translator/real/` make live provider calls — skip unless credentials are set.\n- Regression baselines: `tests/__baseline__/verify-*.mjs` compare against committed snapshots (providers, aliases, OAuth URLs). Run these after touching provider registry / alias logic.\n\n## Architecture\n\nTwo authoritative docs already exist — read them before working in these areas rather than re-deriving:\n- `docs/ARCHITECTURE.md` — full system: request lifecycle, combo/account fallback, OAuth + token refresh, cloud sync, data model.\n- `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/`.**\n\n### Request flow (the thing to understand first)\n`src/app/api/v1/*` route (Next rewrite maps `/v1/*` → `/api/v1/*` in `next.config.mjs`)\n→ `src/sse/handlers/chat.js` (parse, combo expansion, account-selection loop)\n→ `open-sse/handlers/chatCore.js` (detect source format, translate request, dispatch to executor, retry/refresh, stream setup)\n→ `open-sse/executors/*` (per-provider upstream call; `default.js` handles any OpenAI-compatible provider)\n→ `open-sse/translator/*` (client format ↔ provider format)\n→ SSE back to client.\n\n`src/sse/` is the app-side entry glue; `open-sse/` is the provider-agnostic engine (also usable standalone). Cross that boundary consciously.\n\n### Translator engine (`open-sse/translator/`)\n- 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`).\n- 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.\n- 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.\n\n### Provider registry (`open-sse/providers/registry/*`)\n- 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.\n- Add a provider: copy `providers/REGISTRY_TEMPLATE.js`, add models to `config/providerModels.js`. Only add an executor for non-OpenAI-compatible upstreams.\n\n### Persistence — IMPORTANT (ARCHITECTURE.md is stale here)\nState 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.\n- `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/`.\n- DB file location resolves via `src/lib/db/paths.js` (`DATA_DIR`, else `~/.9router/`).\n- Usage/logs (`src/lib/usageDb.js`, `usage.json` + `log.txt`) still live under `~/.9router` and do **not** follow `DATA_DIR`.\n\n### RTK token saver (`open-sse/rtk/`)\nPre-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.\n\n## Conventions & gotchas\n\n- Plain JavaScript (ESM), no TypeScript. `@/*` path alias → `src/*` (`jsconfig.json`).\n- `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.\n- 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.\n- 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.\n- **Security-first on PRs**: Security is the top priority when reviewing or creating PRs. Audit authentication, credential/token storage & leaks, header manipulation (`X-Forwarded-For`), and SSRF risks before functional logic. Always include explicit security warnings/notes when reporting PR reviews or changes to the user.\n- Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`).\n","isInternal":false,"tokens":2035,"sizeBytes":8186},{"name":"SKILL.md","path":"skills/9router-chat/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-chat/SKILL.md","title":"9router-chat","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-chat\ndescription: Chat / code generation via 9Router using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through 9Router.\n---\n\n# 9Router — Chat\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Endpoints\n\n- `POST $NINEROUTER_URL/v1/chat/completions` — OpenAI format\n- `POST $NINEROUTER_URL/v1/messages` — Anthropic format\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models | jq '.data[].id'\n# Per-model metadata (contextWindow, params)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/gpt-4o\"\n```\n\nCombos (e.g. `vip`, `mycodex`) auto-fallback through multiple providers.\n\n## OpenAI format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/chat/completions \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/gpt-5\",\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}],\"stream\":false}'\n```\n\nJS (OpenAI SDK):\n\n```js\nimport OpenAI from \"openai\";\nconst client = new OpenAI({ baseURL: `${process.env.NINEROUTER_URL}/v1`, apiKey: process.env.NINEROUTER_KEY });\nconst res = await client.chat.completions.create({\n  model: \"openai/gpt-5\",\n  messages: [{ role: \"user\", content: \"Hi\" }],\n  stream: true,\n});\nfor await (const chunk of res) process.stdout.write(chunk.choices[0]?.delta?.content || \"\");\n```\n\n## Anthropic format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/messages \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"anthropic-version: 2023-06-01\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"cc/claude-opus-4-7\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}]}'\n```\n\n## Response shape\n\nOpenAI (`/v1/chat/completions`):\n```json\n{ \"id\": \"chatcmpl-...\", \"object\": \"chat.completion\", \"model\": \"openai/gpt-5\",\n  \"choices\": [{ \"index\": 0, \"message\": { \"role\": \"assistant\", \"content\": \"Hello!\" }, \"finish_reason\": \"stop\" }],\n  \"usage\": { \"prompt_tokens\": 8, \"completion_tokens\": 2, \"total_tokens\": 10 } }\n```\n\nStreaming (`stream:true`) emits SSE: `data: {choices:[{delta:{content:\"...\"}}]}\\n\\n` ... `data: [DONE]\\n\\n`.\n\nAnthropic (`/v1/messages`):\n```json\n{ \"id\": \"msg_...\", \"type\": \"message\", \"role\": \"assistant\", \"model\": \"cc/claude-opus-4-7\",\n  \"content\": [{ \"type\": \"text\", \"text\": \"Hello!\" }],\n  \"stop_reason\": \"end_turn\", \"usage\": { \"input_tokens\": 8, \"output_tokens\": 2 } }\n```\n","frontmatter":{"name":"9router-chat","description":"Chat / code generation via 9Router using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through 9Router."},"isInternal":false,"tokens":801,"sizeBytes":2543},{"name":"SKILL.md","path":"skills/9router-embeddings/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-embeddings/SKILL.md","title":"9router-embeddings","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-embeddings\ndescription: Generate vector embeddings via 9Router /v1/embeddings using OpenAI / Gemini / Mistral / Voyage / Nvidia / GitHub embedding models for RAG, semantic search, similarity. Use when the user wants embeddings, vectors, RAG, semantic search, or to embed text.\n---\n\n# 9Router — Embeddings\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/embedding | jq '.data[].id'\n# Per-model dimensions\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/text-embedding-3-small\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/embeddings`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/embedding` |\n| `input` | yes | string OR array of strings |\n| `encoding_format` | no | `float` (default) / `base64` |\n| `dimensions` | no | OpenAI v3 only |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/embeddings \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/text-embedding-3-small\",\"input\":[\"hello\",\"world\"]}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/embeddings`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/text-embedding-004\", input: \"RAG chunk text\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].embedding.length);  // dimension\n```\n\n## Response shape\n\n```json\n{ \"object\": \"list\", \"model\": \"openai/text-embedding-3-small\",\n  \"data\": [\n    { \"object\": \"embedding\", \"index\": 0, \"embedding\": [0.0123, -0.045, ...] },\n    { \"object\": \"embedding\", \"index\": 1, \"embedding\": [...] }\n  ],\n  \"usage\": { \"prompt_tokens\": 5, \"total_tokens\": 5 } }\n```\n\n## Provider quirks\n\n| Provider | Notes |\n|---|---|\n| `openai`, `openrouter`, `mistral`, `voyage-ai`, `fireworks`, `together`, `nebius`, `github`, `nvidia`, `jina-ai` | Native OpenAI shape — `dimensions` works only on OpenAI v3 (`text-embedding-3-*`) |\n| `gemini`, `google_ai_studio` | Server auto-converts to `embedContent`/`batchEmbedContents` — send OpenAI shape |\n| `openai-compatible-*`, `custom-embedding-*` | Custom `baseUrl` from credentials |\n\nBatch (`input` as array) is faster; some providers cap batch size.\n","frontmatter":{"name":"9router-embeddings","description":"Generate vector embeddings via 9Router /v1/embeddings using OpenAI / Gemini / Mistral / Voyage / Nvidia / GitHub embedding models for RAG, semantic search, similarity. Use when the user wants embeddings, vectors, RAG, semantic search, or to embed text."},"isInternal":false,"tokens":725,"sizeBytes":2421},{"name":"SKILL.md","path":"skills/9router-image/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-image/SKILL.md","title":"9router-image","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-image\ndescription: Generate images via 9Router /v1/images/generations using OpenAI / Gemini Imagen / DALL-E / FLUX / MiniMax / SDWebUI / ComfyUI / Codex models. Use when the user wants to create, generate, draw, or render an image, picture, or text-to-image (txt2img).\n---\n\n# 9Router — Image Generation\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/image | jq '.data[].id'\n# Per-model params/options (size enum, quality enum, capabilities like edit)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/dall-e-3\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/images/generations`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/image` |\n| `prompt` | yes | image description |\n| `n` | no | count (provider-dependent) |\n| `size` | no | `1024x1024`, `1792x1024`, ... |\n| `quality` | no | `standard` / `hd` (OpenAI) |\n| `response_format` | no | `url` (default) or `b64_json` |\n\nAdd query `?response_format=binary` to receive raw image bytes (handy for saving file).\n\n## Examples\n\nSave to file (binary):\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/images/generations?response_format=binary\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"gemini/gemini-3-pro-image-preview\",\"prompt\":\"watercolor mountains at sunrise\",\"size\":\"1024x1024\"}' \\\n  --output out.png\n```\n\nJS (URL response):\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/images/generations`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/gemini-3-pro-image-preview\", prompt: \"neon city\", size: \"1024x1024\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].url || data[0].b64_json.slice(0, 40));\n```\n\n## Response shape\n\nJSON (default `response_format=url`):\n```json\n{ \"created\": 1735000000, \"data\": [{ \"url\": \"https://...\" }] }\n```\n\n`response_format=b64_json`:\n```json\n{ \"created\": 1735000000, \"data\": [{ \"b64_json\": \"iVBORw0KGgo...\" }] }\n```\n\nQuery `?response_format=binary` returns raw image bytes (Content-Type `image/png` or `image/jpeg`).\n\n## Provider quirks\n\nCommon fields above work everywhere. These add/override:\n\n| Provider | Extra/changed fields | Notes |\n|---|---|---|\n| `openai`, `minimax`, `openrouter`, `recraft` | `quality`, `style`, `response_format` | Standard OpenAI shape |\n| `gemini` (nano-banana) | — | Only `prompt`; ignores `size`/`n` |\n| `codex` (gpt-5.4-image) | `image`, `images[]`, `image_detail`, `output_format`, `background` | SSE stream; **ChatGPT Plus/Pro required** |\n| `huggingface` | — | Only `prompt`; returns single image |\n| `nanobanana` | `image`, `images[]` (edit mode) | `size` → aspect ratio; async polling |\n| `fal-ai` | `image` (img2img) | `n` → `num_images`; `size` → ratio; async |\n| `stability-ai` | `style` (preset), `output_format` | `size` → `aspect_ratio` |\n| `black-forest-labs` (FLUX) | `image` (ref) | `size` → exact `width`/`height`; async |\n| `runwayml` | `image` (ref) | `size` → ratio; async; video models exist |\n| `sdwebui`, `comfyui` | — | Localhost noAuth (`:7860` / `:8188`) |\n","frontmatter":{"name":"9router-image","description":"Generate images via 9Router /v1/images/generations using OpenAI / Gemini Imagen / DALL-E / FLUX / MiniMax / SDWebUI / ComfyUI / Codex models. Use when the user wants to create, generate, draw, or render an image, picture, or text-to-image (txt2img)."},"isInternal":false,"tokens":1051,"sizeBytes":3338},{"name":"SKILL.md","path":"skills/9router-stt/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-stt/SKILL.md","title":"9router-stt","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-stt\ndescription: Speech-to-text via 9Router /v1/audio/transcriptions using OpenAI Whisper / Groq / Gemini / Deepgram / AssemblyAI / NVIDIA / HuggingFace models. Use when the user wants to transcribe audio, convert speech to text, or get subtitles from audio files.\n---\n\n# 9Router — Speech-to-Text\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/stt | jq '.data[].id'\n# Per-model params (language, response_format, prompt, temperature support)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/whisper-1\"\n```\n\n`model` = STT model ID (e.g. `openai/whisper-1`, `groq/whisper-large-v3`, `deepgram/nova-3`, `gemini/gemini-2.5-flash`).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/transcriptions` (OpenAI Whisper compatible, `multipart/form-data`)\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/stt` |\n| `file` | yes | audio file (mp3, wav, m4a, webm, ogg, flac) |\n| `language` | no | ISO-639-1 (e.g. `en`, `vi`) |\n| `prompt` | no | hint text to guide transcription |\n| `response_format` | no | `json` (default) / `text` / `verbose_json` / `srt` / `vtt` |\n| `temperature` | no | 0–1 |\n\n## Examples\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/transcriptions\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -F \"model=openai/whisper-1\" \\\n  -F \"file=@audio.mp3\" \\\n  -F \"language=vi\"\n```\n\nJS (Node):\n\n```js\nimport { createReadStream } from \"node:fs\";\nconst form = new FormData();\nform.append(\"model\", \"groq/whisper-large-v3-turbo\");\nform.append(\"file\", new Blob([await (await import(\"node:fs/promises\")).readFile(\"audio.mp3\")]), \"audio.mp3\");\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/transcriptions`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}` },\n  body: form,\n});\nconst { text } = await r.json();\nconsole.log(text);\n```\n\n## Response shape\n\nDefault (`response_format=json`):\n```json\n{ \"text\": \"Xin chào, đây là bản ghi âm.\" }\n```\n\n`verbose_json` adds `language`, `duration`, `segments[]` with timestamps.\n`srt` / `vtt` return subtitle text.\n\n## Provider quirks\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe` | Native OpenAI shape |\n| `groq` | `whisper-large-v3`, `whisper-large-v3-turbo`, `distil-whisper-large-v3-en` | Fastest; OpenAI shape |\n| `gemini` | `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-2.5-flash-lite` | Server converts to `generateContent` with audio inline |\n| `deepgram` | `nova-3`, `nova-2`, `whisper-large` | Token auth; server adapts response |\n| `assemblyai` | `universal-3-pro`, `universal-2` | Async upload+poll handled server-side |\n| `nvidia` | `nvidia/parakeet-ctc-1.1b-asr` | NIM endpoint |\n| `huggingface` | `openai/whisper-large-v3`, `openai/whisper-small` | HF Inference API |\n| `elevenlabs` | `scribe_v1`, `scribe_v2` | Whisper-compatible shape; `xi-api-key` auth, not Bearer. Extra params: `timestamps_granularity` (`word`/`character`/`none`), `tag_audio_events`, and speaker labelling via **either** `diarize=true` **or** `num_speakers` (1–32) — sending both makes the request invalid, so `diarize` wins and `num_speakers` is dropped. Blank `language` is omitted upstream for auto-detect. `srt`/`vtt` and `verbose_json` segments are served from Scribe's own `additional_formats` render, so `segments[]` is omitted when the upstream render is unavailable rather than synthesized. Does not accept `temperature` or `prompt` — those are not forwarded. |\n","frontmatter":{"name":"9router-stt","description":"Speech-to-text via 9Router /v1/audio/transcriptions using OpenAI Whisper / Groq / Gemini / Deepgram / AssemblyAI / NVIDIA / HuggingFace models. Use when the user wants to transcribe audio, convert speech to text, or get subtitles from audio files."},"isInternal":false,"tokens":1155,"sizeBytes":3671},{"name":"SKILL.md","path":"skills/9router-tts/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-tts/SKILL.md","title":"9router-tts","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-tts\ndescription: Text-to-speech via 9Router /v1/audio/speech using OpenAI / ElevenLabs / Deepgram / Edge TTS / Google TTS / Hyperbolic / Inworld voices. Use when the user wants to convert text to speech, generate audio, voiceover, narrate, or read text aloud.\n---\n\n# 9Router — Text-to-Speech\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\n# 1) List models\ncurl $NINEROUTER_URL/v1/models/tts | jq '.data[].id'\n# 2) Per-model metadata (params, voicesUrl if voice-by-id)\ncurl \"$NINEROUTER_URL/v1/models/info?id=el/eleven_multilingual_v2\"\n# 3) List voices (elevenlabs, edge-tts, deepgram, inworld, local-device). Optional ?lang=vi\ncurl \"$NINEROUTER_URL/v1/audio/voices?provider=edge-tts&lang=vi\" | jq '.data[].model'\n```\n\n`model` field in `/v1/audio/speech` = voice ID directly (e.g. `edge-tts/vi-VN-HoaiMyNeural`, `el/<voice_id>`, or `openai/tts-1` model+default voice).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/speech`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | voice ID from `/v1/models/tts` |\n| `input` | yes | text to speak |\n\nQuery `?response_format=mp3` (default, raw bytes) or `?response_format=json` (`{audio: base64, format}`).\n\n## Examples\n\nSave MP3:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/speech\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/tts-1\",\"input\":\"Hello world\"}' \\\n  --output speech.mp3\n```\n\nJS (save file):\n\n```js\nimport { writeFile } from \"node:fs/promises\";\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/speech`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"el/eleven_multilingual_v2\", input: \"Xin chào\" }),\n});\nawait writeFile(\"speech.mp3\", Buffer.from(await r.arrayBuffer()));\n```\n\n## Response shape\n\nDefault → raw audio bytes (Content-Type `audio/mp3`).\n\n`?response_format=json`:\n```json\n{ \"audio\": \"SUQzBAAAA...\", \"format\": \"mp3\" }\n```\n\n## Provider quirks (model format)\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `tts-1/alloy` (model/voice) or just voice | Default model `gpt-4o-mini-tts` |\n| `elevenlabs` | `<model_id>/<voice_id>` or `<voice_id>` | Default model `eleven_flash_v2_5`; list voices in Dashboard |\n| `openrouter` | `openai/gpt-4o-mini-tts/alloy` | Streamed via chat-completions audio modality |\n| `edge-tts` | voice id e.g. `vi-VN-HoaiMyNeural` | **noAuth**; default `vi-VN-HoaiMyNeural` |\n| `google-tts` | language code e.g. `en`, `vi` | **noAuth** |\n| `local-device` | OS voice name (`say -v ?` / SAPI) | **noAuth**; needs `ffmpeg` |\n| `deepgram` | `aura-asteria-en` etc | Token auth |\n| `nvidia`, `inworld`, `cartesia`, `playht` | `model/voice` | Provider-specific auth header |\n| `coqui`, `tortoise` | speaker / voice id | Localhost noAuth |\n| `hyperbolic` | model id | Body = `{text}` only |\n","frontmatter":{"name":"9router-tts","description":"Text-to-speech via 9Router /v1/audio/speech using OpenAI / ElevenLabs / Deepgram / Edge TTS / Google TTS / Hyperbolic / Inworld voices. Use when the user wants to convert text to speech, generate audio, voiceover, narrate, or read text aloud."},"isInternal":false,"tokens":990,"sizeBytes":3063},{"name":"SKILL.md","path":"skills/9router-video/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-video/SKILL.md","title":"9router-video","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-video\ndescription: 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.\n---\n\n# 9Router — Video Generation (xAI Grok Imagine)\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\nRequires 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.\n\n## Endpoints (async job flow)\n\nVideo generation is **asynchronous**: the POST returns a `request_id` immediately, then you poll until the job is `done` or `failed`.\n\n| Endpoint | Purpose |\n|---|---|\n| `POST /v1/videos/generations` | text-to-video / image-to-video |\n| `POST /v1/videos/edits` | edit an existing video |\n| `POST /v1/videos/extensions` | extend an existing video |\n| `GET /v1/videos/{request_id}` | poll job status |\n\nRequest fields (passed through to xAI unchanged — see https://docs.x.ai/developers/rest-api-reference/inference/videos):\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | no | `xai/grok-imagine-video` (prefix is stripped before upstream) |\n| `prompt` | yes for T2V | video description |\n| `duration` | no | seconds |\n| `aspect_ratio` | no | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `3:2`, `2:3` |\n| `resolution` | no | `480p`, `720p`, `1080p` |\n| `image` | no | `{ \"url\": \"https://… or data:image/…;base64,…\" }` for image-to-video |\n| `video` | edits/extensions | `{ \"url\": \"…mp4\" }` or `{ \"file_id\": \"…\" }` |\n\n## Examples\n\nSubmit a job:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/videos/generations\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -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\"}'\n# → {\"request_id\":\"abc123\"}   (response header x-9router-connection-id: <id>)\n```\n\nPoll until done (echo the connection header back so the same account polls the job):\n\n```bash\ncurl \"$NINEROUTER_URL/v1/videos/abc123\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"x-connection-id: <id from create response>\"\n# → {\"status\":\"pending\",\"progress\":42}\n# → {\"status\":\"done\",\"video\":{\"url\":\"https://…mp4\",\"duration\":8},\"model\":\"grok-imagine-video\"}\n# → {\"status\":\"failed\",\"error\":{\"code\":\"…\",\"message\":\"…\"}}\n```\n\nDownload: fetch `video.url` from the `done` response.\n\n## CLI one-shot\n\n```bash\n9router xai video \\\n  --prompt \"A cinematic tracking shot through a neon city at night\" \\\n  --output video.mp4\n# options: --model --duration --aspect-ratio --resolution --image --timeout --port --api-key\n```\n\nSubmits, polls with progress, downloads to `video.mp4.part`, atomically renames on success. Ctrl+C cancels cleanly; non-zero exit on failure.\n\n## Notes & limits\n\n- 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`).\n- 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.\n- Video models are tagged `kind: \"video\"` and are excluded from chat model lists and chat fallback combos.\n- 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.\n","frontmatter":{"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."},"isInternal":false,"tokens":1092,"sizeBytes":4004},{"name":"SKILL.md","path":"skills/9router-web-fetch/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-web-fetch/SKILL.md","title":"9router-web-fetch","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-web-fetch\ndescription: Fetch URL → markdown / text / HTML via 9Router /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown.\n---\n\n# 9Router — Web Fetch\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webFetch\") | .id'\n# Per-provider params\ncurl \"$NINEROUTER_URL/v1/models/info?id=firecrawl/fetch\"\n```\n\nIDs end in `/fetch` (e.g. `firecrawl/fetch`, `jina/fetch`). `fetch-combo` chains providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/web/fetch`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `firecrawl` or `jina-reader`) |\n| `url` | yes | URL to extract |\n| `format` | no | `markdown` (default) / `text` / `html` |\n| `max_characters` | no | truncate output |\n\n## Examples\n\n### Jina Reader\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"jina-reader\",\"url\":\"https://9router.com\",\"format\":\"markdown\"}'\n```\n\n### Exa\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"exa\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Firecrawl\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"firecrawl\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Tavily\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Ollama Cloud\n\nUses the API key from the existing `ollama` connection.\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"ollama\",\"url\":\"https://example.com\",\"format\":\"markdown\"}'\n```\n\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/web/fetch`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"fetch-combo\", url: \"https://example.com\", format: \"markdown\", max_characters: 5000 }),\n});\nconst { data } = await r.json();\nconsole.log(data.title, data.content.length);\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"jina-reader\",\n  \"url\": \"...\",\n  \"title\": \"...\",\n  \"content\": { \"format\": \"markdown\", \"text\": \"...\", \"length\": 1234 },\n  \"links\": [\"https://example.com/related\"],\n  \"metadata\": { \"author\": null, \"published_at\": null, \"language\": null },\n  \"usage\": { \"fetch_cost_usd\": 0 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700 }\n}\n```\n\n`links` is included when the upstream provider returns discovered page links (currently Ollama Cloud).\n\n## Provider quirks\n\n| Provider | Auth | Best for |\n|---|---|---|\n| `firecrawl` | Bearer | JS-rendered pages, `format=markdown/html` |\n| `jina-reader` | Bearer (optional) | Free tier (~1M chars/mo); fastest plain markdown |\n| `tavily` | Bearer | Bulk extract; returns `raw_content` |\n| `exa` | `x-api-key` | Pre-indexed pages; fast text extraction |\n| `ollama` | Bearer | Markdown plus page title and discovered links; uses the Ollama Cloud key |\n","frontmatter":{"name":"9router-web-fetch","description":"Fetch URL → markdown / text / HTML via 9Router /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown."},"isInternal":false,"tokens":1142,"sizeBytes":3768},{"name":"SKILL.md","path":"skills/9router-web-search/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router-web-search/SKILL.md","title":"9router-web-search","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router-web-search\ndescription: Web and X search via 9Router /v1/search using Tavily / Exa / Brave / Serper / SearXNG / Google PSE / Linkup / SearchAPI / You.com / Perplexity / Xquik. Use when the user wants to search the web, find articles, or search public X posts.\n---\n\n# 9Router — Web Search\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webSearch\") | .id'\n# Per-provider params (searchTypes, maxResults, required options like cx for google-pse)\ncurl \"$NINEROUTER_URL/v1/models/info?id=tavily/search\"\n```\n\nIDs end in `/search` (e.g. `tavily/search`). Combos (`owned_by:\"combo\"`) chain providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/search`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `tavily` or `brave`) |\n| `query` | yes | search query |\n| `max_results` | no | default 5 |\n| `search_type` | no | `web` (default) / `news` / `x` for Xquik |\n| `country`, `language`, `time_range`, `domain_filter` | no | provider-dependent |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"query\":\"9Router open source\",\"max_results\":5}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/search`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"search-combo\", query: \"latest LLM benchmarks\", max_results: 10 }),\n});\nconsole.log(await r.json());\n```\n\nX search with Xquik:\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"xquik\",\"query\":\"from:github release\",\"max_results\":10,\"provider_options\":{\"queryType\":\"Latest\"}}'\n```\n\nAdd the Xquik API key in 9Router's provider settings. Xquik charges 1 credit per returned post. Continue a search by passing `pagination.next_cursor` as `provider_options.cursor`.\n\nXquik responses include provider pagination and credit usage:\n\n```json\n{\n  \"pagination\": { \"has_more\": true, \"next_cursor\": \"cursor-2\" },\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": null, \"provider_credits_used\": 10 }\n}\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"tavily\",\n  \"query\": \"9Router open source\",\n  \"results\": [\n    {\n      \"title\": \"...\", \"url\": \"https://...\", \"display_url\": \"github.com/...\",\n      \"snippet\": \"...\", \"position\": 1, \"score\": 0.92,\n      \"published_at\": null, \"favicon_url\": null, \"content\": null,\n      \"metadata\": { \"author\": null, \"language\": null, \"source_type\": null, \"image_url\": null },\n      \"citation\": { \"provider\": \"tavily\", \"retrieved_at\": \"2026-...\", \"rank\": 1 }\n    }\n  ],\n  \"answer\": null,\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": 0.008 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700, \"total_results_available\": 12 },\n  \"errors\": []\n}\n```\n\n## Provider quirks\n\nAll accept `query` + `max_results`. Optional fields vary:\n\n| Provider | Supports | Required extras |\n|---|---|---|\n| `tavily` | country, domain_filter, news topic | — |\n| `exa` | domain_filter (incl/excl), news category | — |\n| `brave-search` | country, language | — |\n| `serper` | country, language, news endpoint | — |\n| `perplexity` | country, language, domain_filter | — |\n| `linkup` | domain_filter, time_range | `depth: fast/standard/deep` (option) |\n| `google-pse` | country, language, time_range, offset | **`cx` required** (providerOptions) |\n| `searchapi` | country, language, pagination | — |\n| `youcom` | country, language, time_range, domain_filter, full_page | — |\n| `searxng` | language, time_range | Self-hosted, **noAuth** |\n| `xquik` | X/Twitter search operators, language, cursor pagination | `queryType: Latest/Top`, `cursor` (options) |\n\nProvider IS the model — `\"provider\":\"tavily\" ≡ \"model\":\"tavily\"`.\n","frontmatter":{"name":"9router-web-search","description":"Web and X search via 9Router /v1/search using Tavily / Exa / Brave / Serper / SearXNG / Google PSE / Linkup / SearchAPI / You.com / Perplexity / Xquik. Use when the user wants to search the web, find articles, or search public X posts."},"isInternal":false,"tokens":1246,"sizeBytes":4157},{"name":"SKILL.md","path":"skills/9router/SKILL.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/9router/SKILL.md","title":"9router","category":"anthropic-skill","format":"markdown","content":"---\nname: 9router\ndescription: Entry point for 9Router — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions 9Router, NINEROUTER_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed.\n---\n\n# 9Router\n\nLocal/remote AI gateway exposing OpenAI-compatible REST. One key, many providers, auto-fallback.\n\n## Setup\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"      # or VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                      # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nAll requests: `${NINEROUTER_URL}/v1/...` with header `Authorization: Bearer ${NINEROUTER_KEY}` (omit if auth disabled).\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`\n\n## Discover models\n\n```bash\ncurl $NINEROUTER_URL/v1/models                  # chat/LLM (default)\ncurl $NINEROUTER_URL/v1/models/image            # image-gen\ncurl $NINEROUTER_URL/v1/models/tts              # text-to-speech\ncurl $NINEROUTER_URL/v1/models/embedding        # embeddings\ncurl $NINEROUTER_URL/v1/models/web              # web search + fetch (entries have `kind` field)\ncurl $NINEROUTER_URL/v1/models/stt              # speech-to-text\ncurl $NINEROUTER_URL/v1/models/image-to-text    # vision\n```\n\nUse `data[].id` as `model` field in requests. Combos appear with `owned_by:\"combo\"`.\n\nResponse shape:\n```json\n{ \"object\": \"list\", \"data\": [\n  { \"id\": \"openai/gpt-5\", \"object\": \"model\", \"owned_by\": \"openai\", \"created\": 1735000000 },\n  { \"id\": \"tavily/search\", \"object\": \"model\", \"kind\": \"webSearch\", \"owned_by\": \"tavily\", \"created\": 1735000000 }\n]}\n```\n\n## Capability skills\n\nWhen the user needs a specific capability, fetch that skill's `SKILL.md` from its raw URL:\n\n| Capability | Raw URL |\n|---|---|\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## Errors\n\n- 401 → set/refresh `NINEROUTER_KEY` (Dashboard → Keys)\n- 400 `Invalid model format` → check `model` exists in `/v1/models/<kind>`\n- 503 `All accounts unavailable` → wait `retry-after` or add another provider account\n","frontmatter":{"name":"9router","description":"Entry point for 9Router — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions 9Router, NINEROUTER_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed."},"isInternal":false,"tokens":837,"sizeBytes":2992},{"name":"README.md","path":"skills/README.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/skills/README.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# 9Router — Agent Skills\n\nDrop-in skills for any AI agent (Claude, Cursor, ChatGPT, custom SDK). Just **copy a link** below and paste it to your AI — it will fetch the skill and use 9Router for you.\n\n> Tip: start with the **9router** entry skill — it covers setup and links to all capability skills.\n\n## Skills\n\n| Capability | Copy link below and paste to your AI |\n|---|---|\n| **Entry / Setup** (start here) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md |\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Video generation (xAI Grok Imagine) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-video/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## How to use\n\nPaste to your AI (Claude, Cursor, ChatGPT, …):\n\n```\nRead this skill and use it: https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md\n```\n\nThen ask normally — *\"generate an image of a cat\"*, *\"transcribe this URL\"*, etc.\n\n## Configure your shell once\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"   # local default, or your VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                   # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`.\n\n## Links\n\n- Source: https://github.com/decolua/9router\n- Dashboard: https://9router.com\n","isInternal":false,"tokens":598,"sizeBytes":2170},{"name":"ARCHITECTURE.md","path":"docs/ARCHITECTURE.md","rawUrl":"https://raw.githubusercontent.com/decolua/9router/HEAD/docs/ARCHITECTURE.md","title":"docs - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# 9Router Architecture\n\n_Last updated: 2026-02-06_\n\n## Executive Summary\n\n9Router is a local AI routing gateway and dashboard built on Next.js.\nIt provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic across multiple upstream providers with translation, fallback, token refresh, and usage tracking.\n\nCore capabilities:\n\n- OpenAI-compatible API surface for CLI/tools\n- Request/response translation across provider formats\n- Model combo fallback (multi-model sequence)\n- Account-level fallback (multi-account per provider)\n- OAuth + API-key provider connection management\n- Local persistence for providers, keys, aliases, combos, settings, pricing\n- Usage/cost tracking and request logging\n- Optional cloud sync for multi-device/state sync\n\nPrimary runtime model:\n\n- Next.js app routes under `src/app/api/*` implement both dashboard APIs and compatibility APIs\n- A shared SSE/routing core in `src/sse/*` + `open-sse/*` handles provider execution, translation, streaming, fallback, and usage\n\n## Scope and Boundaries\n\n### In Scope\n\n- Local gateway runtime\n- Dashboard management APIs\n- Provider authentication and token refresh\n- Request translation and SSE streaming\n- Local state + usage persistence\n- Optional cloud sync orchestration\n\n### Out of Scope\n\n- Cloud service implementation behind `NEXT_PUBLIC_CLOUD_URL`\n- Provider SLA/control plane outside local process\n- External CLI binaries themselves (Claude CLI, Codex CLI, etc.)\n\n## High-Level System Context\n\n```mermaid\nflowchart LR\n    subgraph Clients[Developer Clients]\n        C1[Claude Code]\n        C2[Codex CLI]\n        C3[OpenClaw / Droid / Cline / Continue / Roo]\n        C4[Custom OpenAI-compatible clients]\n        BROWSER[Browser Dashboard]\n    end\n\n    subgraph Router[9Router Local Process]\n        API[V1 Compatibility API\\n/v1/*]\n        DASH[Dashboard + Management API\\n/api/*]\n        CORE[SSE + Translation Core\\nopen-sse + src/sse]\n        DB[(db.json)]\n        UDB[(usage.json + log.txt)]\n    end\n\n    subgraph Upstreams[Upstream Providers]\n        P1[OAuth Providers\\nClaude/Codex/Gemini/Qwen/iFlow/GitHub/Kiro/Cursor/Antigravity]\n        P2[API Key Providers\\nOpenAI/Anthropic/OpenRouter/GLM/Kimi/MiniMax]\n        P3[Compatible Nodes\\nOpenAI-compatible / Anthropic-compatible]\n    end\n\n    subgraph Cloud[Optional Cloud Sync]\n        CLOUD[Cloud Sync Endpoint\\nNEXT_PUBLIC_CLOUD_URL]\n    end\n\n    C1 --> API\n    C2 --> API\n    C3 --> API\n    C4 --> API\n    BROWSER --> DASH\n\n    API --> CORE\n    DASH --> DB\n    CORE --> DB\n    CORE --> UDB\n\n    CORE --> P1\n    CORE --> P2\n    CORE --> P3\n\n    DASH --> CLOUD\n```\n\n## Core Runtime Components\n\n## 1) API and Routing Layer (Next.js App Routes)\n\nMain directories:\n\n- `src/app/api/v1/*` and `src/app/api/v1beta/*` for compatibility APIs\n- `src/app/api/*` for management/configuration APIs\n- Next rewrites in `next.config.mjs` map `/v1/*` to `/api/v1/*`\n\nImportant compatibility routes:\n\n- `src/app/api/v1/chat/completions/route.js`\n- `src/app/api/v1/messages/route.js`\n- `src/app/api/v1/responses/route.js`\n- `src/app/api/v1/models/route.js`\n- `src/app/api/v1/messages/count_tokens/route.js`\n- `src/app/api/v1beta/models/route.js`\n- `src/app/api/v1beta/models/[...path]/route.js`\n\nManagement domains:\n\n- Auth/settings: `src/app/api/auth/*`, `src/app/api/settings/*`\n- Providers/connections: `src/app/api/providers*`\n- Provider nodes: `src/app/api/provider-nodes*`\n- OAuth: `src/app/api/oauth/*`\n- Keys/aliases/combos/pricing: `src/app/api/keys*`, `src/app/api/models/alias`, `src/app/api/combos*`, `src/app/api/pricing`\n- Usage: `src/app/api/usage/*`\n- Sync/cloud: `src/app/api/sync/*`, `src/app/api/cloud/*`\n- CLI tooling helpers: `src/app/api/cli-tools/*`\n\n## 2) SSE + Translation Core\n\nMain flow modules:\n\n- Entry: `src/sse/handlers/chat.js`\n- Core orchestration: `open-sse/handlers/chatCore.js`\n- Provider execution adapters: `open-sse/executors/*`\n- Format detection/provider config: `open-sse/services/provider.js`\n- Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js`\n- Account fallback logic: `open-sse/services/accountFallback.js`\n- Translation registry: `open-sse/translator/index.js`\n- Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js`\n- Usage extraction/normalization: `open-sse/utils/usageTracking.js`\n\n## 3) Persistence Layer\n\nPrimary state DB:\n\n- `src/lib/localDb.js`\n- file: `${DATA_DIR}/db.json` (or `~/.9router/db.json` when `DATA_DIR` is unset)\n- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing\n\nUsage DB:\n\n- `src/lib/usageDb.js`\n- files: `~/.9router/usage.json`, `~/.9router/log.txt`\n- note: currently independent from `DATA_DIR`\n\n## 4) Auth + Security Surfaces\n\n- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js`\n- API key generation/verification: `src/shared/utils/apiKey.js`\n- Provider secrets persisted in `providerConnections` entries\n- Optional proxy support for upstream calls via env proxy variables (`open-sse/utils/proxyFetch.js`)\n\n## 5) Cloud Sync\n\n- Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js`\n- Periodic task: `src/shared/services/cloudSyncScheduler.js`\n- Control route: `src/app/api/sync/cloud/route.js`\n\n## Request Lifecycle (`/v1/chat/completions`)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant Client as CLI/SDK Client\n    participant Route as /api/v1/chat/completions\n    participant Chat as src/sse/handlers/chat\n    participant Core as open-sse/handlers/chatCore\n    participant Model as Model Resolver\n    participant Auth as Credential Selector\n    participant Exec as Provider Executor\n    participant Prov as Upstream Provider\n    participant Stream as Stream Translator\n    participant Usage as usageDb\n\n    Client->>Route: POST /v1/chat/completions\n    Route->>Chat: handleChat(request)\n    Chat->>Model: parse/resolve model or combo\n\n    alt Combo model\n        Chat->>Chat: iterate combo models (handleComboChat)\n    end\n\n    Chat->>Auth: getProviderCredentials(provider)\n    Auth-->>Chat: active account + tokens/api key\n\n    Chat->>Core: handleChatCore(body, modelInfo, credentials)\n    Core->>Core: detect source format\n    Core->>Core: translate request to target format\n    Core->>Exec: execute(provider, transformedBody)\n    Exec->>Prov: upstream API call\n    Prov-->>Exec: SSE/JSON response\n    Exec-->>Core: response + metadata\n\n    alt 401/403\n        Core->>Exec: refreshCredentials()\n        Exec-->>Core: updated tokens\n        Core->>Exec: retry request\n    end\n\n    Core->>Stream: translate/normalize stream to client format\n    Stream-->>Client: SSE chunks / JSON response\n\n    Stream->>Usage: extract usage + persist history/log\n```\n\n## Combo + Account Fallback Flow\n\n```mermaid\nflowchart TD\n    A[Incoming model string] --> B{Is combo name?}\n    B -- Yes --> C[Load combo models sequence]\n    B -- No --> D[Single model path]\n\n    C --> E[Try model N]\n    E --> F[Resolve provider/model]\n    D --> F\n\n    F --> G[Select account credentials]\n    G --> H{Credentials available?}\n    H -- No --> I[Return provider unavailable]\n    H -- Yes --> J[Execute request]\n\n    J --> K{Success?}\n    K -- Yes --> L[Return response]\n    K -- No --> M{Fallback-eligible error?}\n\n    M -- No --> N[Return error]\n    M -- Yes --> O[Mark account unavailable cooldown]\n    O --> P{Another account for provider?}\n    P -- Yes --> G\n    P -- No --> Q{In combo with next model?}\n    Q -- Yes --> E\n    Q -- No --> R[Return all unavailable]\n```\n\nFallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics.\n\n## OAuth Onboarding and Token Refresh Lifecycle\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Dashboard UI\n    participant OAuth as /api/oauth/[provider]/[action]\n    participant ProvAuth as Provider Auth Server\n    participant DB as localDb\n    participant Test as /api/providers/[id]/test\n    participant Exec as Provider Executor\n\n    UI->>OAuth: GET authorize or device-code\n    OAuth->>ProvAuth: create auth/device flow\n    ProvAuth-->>OAuth: auth URL or device code payload\n    OAuth-->>UI: flow data\n\n    UI->>OAuth: POST exchange or poll\n    OAuth->>ProvAuth: token exchange/poll\n    ProvAuth-->>OAuth: access/refresh tokens\n    OAuth->>DB: createProviderConnection(oauth data)\n    OAuth-->>UI: success + connection id\n\n    UI->>Test: POST /api/providers/[id]/test\n    Test->>Exec: validate credentials / optional refresh\n    Exec-->>Test: valid or refreshed token info\n    Test->>DB: update status/tokens/errors\n    Test-->>UI: validation result\n```\n\nRefresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`.\n\n## Cloud Sync Lifecycle (Enable / Sync / Disable)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Endpoint Page UI\n    participant Sync as /api/sync/cloud\n    participant DB as localDb\n    participant Cloud as External Cloud Sync\n    participant Claude as ~/.claude/settings.json\n\n    UI->>Sync: POST action=enable\n    Sync->>DB: set cloudEnabled=true\n    Sync->>DB: ensure API key exists\n    Sync->>Cloud: POST /sync/{machineId} (providers/aliases/combos/keys)\n    Cloud-->>Sync: sync result\n    Sync->>Cloud: GET /{machineId}/v1/verify\n    Sync-->>UI: enabled + verification status\n\n    UI->>Sync: POST action=sync\n    Sync->>Cloud: POST /sync/{machineId}\n    Cloud-->>Sync: remote data\n    Sync->>DB: update newer local tokens/status\n    Sync-->>UI: synced\n\n    UI->>Sync: POST action=disable\n    Sync->>DB: set cloudEnabled=false\n    Sync->>Cloud: DELETE /sync/{machineId}\n    Sync->>Claude: switch ANTHROPIC_BASE_URL back to local (if needed)\n    Sync-->>UI: disabled\n```\n\nPeriodic sync is triggered by `CloudSyncScheduler` when cloud is enabled.\n\n## Data Model and Storage Map\n\n```mermaid\nerDiagram\n    SETTINGS ||--o{ PROVIDER_CONNECTION : controls\n    PROVIDER_NODE ||--o{ PROVIDER_CONNECTION : backs_compatible_provider\n    PROVIDER_CONNECTION ||--o{ USAGE_ENTRY : emits_usage\n\n    SETTINGS {\n      boolean cloudEnabled\n      number stickyRoundRobinLimit\n      boolean requireLogin\n      string password_hash\n    }\n\n    PROVIDER_CONNECTION {\n      string id\n      string provider\n      string authType\n      string name\n      number priority\n      boolean isActive\n      string apiKey\n      string accessToken\n      string refreshToken\n      string expiresAt\n      string testStatus\n      string lastError\n      string rateLimitedUntil\n      json providerSpecificData\n    }\n\n    PROVIDER_NODE {\n      string id\n      string type\n      string name\n      string prefix\n      string apiType\n      string baseUrl\n    }\n\n    MODEL_ALIAS {\n      string alias\n      string targetModel\n    }\n\n    COMBO {\n      string id\n      string name\n      string[] models\n    }\n\n    API_KEY {\n      string id\n      string name\n      string key\n      string machineId\n      boolean isActive\n    }\n\n    USAGE_ENTRY {\n      string provider\n      string model\n      number prompt_tokens\n      number completion_tokens\n      string connectionId\n      string timestamp\n    }\n```\n\nPhysical storage files:\n\n- main state: `${DATA_DIR}/db.json` (or `~/.9router/db.json`)\n- usage stats: `~/.9router/usage.json`\n- request log lines: `~/.9router/log.txt`\n- optional translator/request debug sessions: `<repo>/logs/...`\n\n## Deployment Topology\n\n```mermaid\nflowchart LR\n    subgraph LocalHost[Developer Host]\n        CLI[CLI Tools]\n        Browser[Dashboard Browser]\n    end\n\n    subgraph ContainerOrProcess[9Router Runtime]\n        Next[Next.js Server\\nPORT=20128]\n        Core[SSE Core + Executors]\n        MainDB[(db.json)]\n        UsageDB[(usage.json/log.txt)]\n    end\n\n    subgraph External[External Services]\n        Providers[AI Providers]\n        SyncCloud[Cloud Sync Service]\n    end\n\n    CLI --> Next\n    Browser --> Next\n    Next --> Core\n    Next --> MainDB\n    Core --> MainDB\n    Core --> UsageDB\n    Core --> Providers\n    Next --> SyncCloud\n```\n\n## Module Mapping (Decision-Critical)\n\n### Route and API Modules\n\n- `src/app/api/v1/*`, `src/app/api/v1beta/*`: compatibility APIs\n- `src/app/api/providers*`: provider CRUD, validation, testing\n- `src/app/api/provider-nodes*`: custom compatible node management\n- `src/app/api/oauth/*`: OAuth/device-code flows\n- `src/app/api/keys*`: local API key lifecycle\n- `src/app/api/models/alias`: alias management\n- `src/app/api/combos*`: fallback combo management\n- `src/app/api/pricing`: pricing overrides for cost calculation\n- `src/app/api/usage/*`: usage and logs APIs\n- `src/app/api/sync/*` + `src/app/api/cloud/*`: cloud sync and cloud-facing helpers\n- `src/app/api/cli-tools/*`: local CLI config writers/checkers\n\n### Routing and Execution Core\n\n- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop\n- `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup\n- `open-sse/executors/*`: provider-specific network and format behavior\n\n### Translation Registry and Format Converters\n\n- `open-sse/translator/index.js`: translator registry and orchestration\n- Request translators: `open-sse/translator/request/*`\n- Response translators: `open-sse/translator/response/*`\n- Format constants: `open-sse/translator/formats.js`\n\n### Persistence\n\n- `src/lib/localDb.js`: persistent config/state\n- `src/lib/usageDb.js`: usage history and rolling request logs\n\n## Provider Executor Coverage\n\nSpecialized executors:\n\n- `antigravity`\n- `gemini-cli`\n- `github`\n- `kiro`\n- `codex`\n- `cursor`\n\nDefault executor path:\n\n- all other providers (including compatible node providers) use `open-sse/executors/default.js`\n\n## Format Translation Coverage\n\nDetected source formats include:\n\n- `openai`\n- `openai-responses`\n- `claude`\n- `gemini`\n\nTarget formats include:\n\n- OpenAI chat/Responses\n- Claude\n- Gemini/Gemini-CLI/Antigravity envelope\n- Kiro\n- Cursor\n\nTranslations are selected dynamically based on source payload shape and provider target format.\n\n## Failure Modes and Resilience\n\n## 1) Account/Provider Availability\n\n- provider account cooldown on transient/rate/auth errors\n- account fallback before failing request\n- combo model fallback when current model/provider path is exhausted\n\n## 2) Token Expiry\n\n- pre-check and refresh with retry for refreshable providers\n- 401/403 retry after refresh attempt in core path\n\n## 3) Stream Safety\n\n- disconnect-aware stream controller\n- translation stream with end-of-stream flush and `[DONE]` handling\n- usage estimation fallback when provider usage metadata is missing\n\n## 4) Cloud Sync Degradation\n\n- sync errors are surfaced but local runtime continues\n- scheduler has retry-capable logic, but periodic execution currently calls single-attempt sync by default\n\n## 5) Data Integrity\n\n- DB shape migration/repair for missing keys\n- corrupt JSON reset safeguards for localDb and usageDb\n\n## Observability and Operational Signals\n\nRuntime visibility sources:\n\n- console logs from `src/sse/utils/logger.js`\n- per-request usage aggregates in `usage.json`\n- textual request status log in `log.txt`\n- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true`\n- dashboard usage endpoints (`/api/usage/*`) for UI consumption\n\n## Security-Sensitive Boundaries\n\n- JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing\n- Initial password fallback (`INITIAL_PASSWORD`, default `123456`) must be overridden in real deployments\n- API key HMAC secret (`API_KEY_SECRET`) secures generated local API key format\n- Provider secrets (API keys/tokens) are persisted in local DB and should be protected at filesystem level\n- Cloud sync endpoints rely on API key auth + machine id semantics\n\n## Environment and Runtime Matrix\n\nEnvironment variables actively used by code:\n\n- App/auth: `JWT_SECRET`, `INITIAL_PASSWORD`\n- Storage: `DATA_DIR`\n- Security hashing: `API_KEY_SECRET`, `MACHINE_ID_SALT`\n- Logging: `ENABLE_REQUEST_LOGS`\n- Sync/cloud URLing: `NEXT_PUBLIC_BASE_URL`, `NEXT_PUBLIC_CLOUD_URL`\n- Outbound proxy: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` and lowercase variants\n- Platform/runtime helpers (not app-specific config): `APPDATA`, `NODE_ENV`, `PORT`, `HOSTNAME`\n\n## Known Architectural Notes\n\n1. `usageDb` currently stores under `~/.9router` and does not follow `DATA_DIR`.\n2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`.\n3. Request logger writes full headers/body when enabled; treat log directory as sensitive.\n4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability.\n\n## Operational Verification Checklist\n\n- Build from source: `cd /root/dev/9router && npm run build`\n- Build Docker image: `cd /root/dev/9router && docker build -t 9router .`\n- Start service and verify:\n- `GET /api/settings`\n- `GET /api/v1/models`\n- CLI target base URL should be `http://<host>:20128/v1` when `PORT=20128`\n","isInternal":false,"tokens":4203,"sizeBytes":16939}],"systemPromptSnippet":"<yakaai_skills repo=\"decolua/9router\">\n<!-- File: open-sse/AGENTS.md (Tokens: ~1171 | Category: root-instruction) -->\n# open-sse\n\nProvider-agnostic SSE engine: one OpenAI-style request → any provider (LLM chat, image, embedding, tts, stt, search), streamed back in the client's format.\n\n## Request lifecycle (chat)\n\n`handlers/chatCore.js` → `services/model.js` `parseModel` (resolve `provider/model`) → **RTK for `cursor`** (`rtk/` compresses the source-format `tool_result` / `role:tool` in-place — its translator rewrites those shapes, so this one provider must run **before** translate) → `translator/index.js` `translateRequest` (client format → provider format) → **post-translate savers** (`rtk/` compress for every other provider, `rtk/headroom.js` proxy compress, `rtk/caveman.js` / `rtk/ponytail.js` system inject — all fail-open) → `executors/index.js` `getExecutor(provider)` → `executor.execute()` (streams upstream) → `translateResponse` (provider chunks → client format) → SSE out.\n\n## Directory map\n\n- `config/` — ALL constants/config (no hardcode elsewhere). `providers.js`/`registry/` (provider defs), `providerModels.js` (alias→models matrix), `runtimeConfig.js` (timeouts, token limits), `*Constants.js`.\n- `translator/` — format conversion. `request/<from>-to-<to>.js`, `response/<from>-to-<to>.js`, `schema/` (enums: ROLE, CLAUDE_BLOCK…), `concerns/` (shared logic), `formats.js`+`formats/` (per-format). `index.js` is the registry/entry.\n- `executors/` — per-provider upstream call. `base.js` (BaseExecutor), one file per special provider, `index.js` map.\n- `providers/` — registry build + `capabilities.js` + `pricing.js`. Entry: `index.js` (PROVIDERS).\n- `handlers/` — per-modality cores (chat/image/embedding/tts/stt/search) + sub-provider folders. `chatCore/` has the streaming/non-streaming/sse-to-json handlers.\n- `rtk/` — request token-killer. `index.js` compresses `tool_result` content in-place (OpenAI/Claude/Kiro shapes); `filters/` per-tool compressors + `autodetect.js`; `headroom.js` external compress proxy; `caveman.js` system-prompt injector.\n- `transformer/` — `responsesTransformer.js` (Chat Completions SSE → Codex Responses API SSE), `streamToJsonConverter.js`.\n- `shared/` — cross-provider auth/identity: `clineAuth.js`, `machineId.js`, `qoder/`.\n- `services/` — `model.js`, `provider.js`, `accountFallback.js`, `combo.js`, `compact.js`, `tokenRefresh/`+`tokenRefresh.js`, `oauthCredentialManager.js`, `usage/`, `projectId.js`, `kiroModels.js`/`qoderModels.js`.\n- `utils/` — streamHandler, stream, sse, error, sessionManager, claudeCloaking, clientDetector, proxyFetch (patches global fetch), cursorProtobuf/cursorChecksum, ollamaTransform.\n\n## Conventions\n\n- Config-driven, DRY, camelCase. NEVER hardcode values, models, or block/role strings — use `config/` + `schema/` constants.\n- Translator pipeline pivots through OpenAI as the intermediate format. A translator registered on the exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop.\n- Translators self-register via `register(from, to, reqFn, resFn)` as an import side-effect — new files MUST be imported in `translator/index.js`.\n\n## How to add\n\n- **Provider**: copy `providers/REGISTRY_TEMPLATE.js` → `providers/registry/{id}.js`; add models to `config/providerModels.js`. Generic providers need no executor (DefaultExecutor handles OpenAI-compatible APIs).\n- **Executor** (only for non-standard upstream): subclass `BaseExecutor` (override `getBaseUrls`/`buildHeaders`/`buildUrl`/`execute`), register in `executors/index.js` map. `getExecutor` falls back to `DefaultExecutor` when absent.\n- **Translator**: add `request|response/<from>-to-<to>.js` calling `register(...)`, then import it in `translator/index.js`. Reuse `schema/` + `concerns/` — don't re-implement parsing.\n\n## Pitfalls\n\n- OpenAI bridge is lossy (thinking, non-base64 images, tool ids, is_error) — prefer a direct route for fragile pairs.\n- `registry/index.js` is an auto-generated static import list; regenerate it (don't hand-edit) after adding a `registry/{id}.js`. REGISTRY_TEMPLATE is excluded by design.\n- Special binary/protobuf formats (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — handle in their executor.\n- `rtk/` + `headroom.js` mutate the request body in-place and are **fail-open**: any error returns null and leaves the body untouched — never throw out of them. RTK skips `is_error`/`status:\"error\"` tool results to preserve traces.\n\n<!-- File: CLAUDE.md (Tokens: ~2035 | Category: claude-rule) -->\n# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## What this is\n\n9Router (`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.\n\nTwo published artifacts live in this one repo:\n- The **dashboard + gateway** (root `package.json`, `9router-app`) — the Next.js server that does the actual routing.\n- 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.\n\nThe code lives in `src/` (Next.js app + dashboard/compat APIs), `open-sse/` (the provider-agnostic routing/translation engine), `cli/` (the launcher package), and `tests/`.\n\n## Commands\n\nDashboard/gateway (run from repo root):\n```bash\ncp .env.example .env\nnpm install\nPORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev   # dev (webpack, port 20127 by default via next dev)\nnpm run build && PORT=20128 HOSTNAME=0.0.0.0 npm run start           # production\n```\n- Bun variants: `npm run dev:bun` / `build:bun` / `start:bun`.\n- Default runtime port is **20128** (dashboard at `/dashboard`, API at `/v1`).\n- Lint: `npx eslint .` (config `eslint.config.mjs`, extends `eslint-config-next`).\n\nCLI package (`cli/`):\n```bash\nnpm run cli:pack       # build + npm pack from root\ncd cli && npm run dev  # nodemon watch\n```\n\nTests (vitest, in `tests/`, an **independent** ESM package — not wired into root `npm test`):\n```bash\nnpm install                             # ROOT deps first — tests import from src/ which needs `open`, `undici`, etc.\ncd tests && npm install                 # then tests' own deps (vitest) → tests/node_modules (allowed by tests/.gitignore)\nnpx vitest run                          # all tests; auto-discovers tests/vitest.config.js\nnpx vitest run unit/capabilities.test.js   # single file (path relative to tests/)\n```\n> 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.\n>\n> **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:\n> - 26 catalogued in `tests/__baseline__/known-fails.txt` (rtk, oauth-cursor-auto-import, translator-request-normalization, …).\n> - `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.\n> - `unit/xai-oauth-service.test.js` times out (5s) when the xAI endpoint-discovery fetch isn't reachable/mocked.\n> - `real/*.real.test.js` make live provider calls — need credentials, skip otherwise.\n- `*.real.test.js` under `tests/translator/real/` make live provider calls — skip unless credentials are set.\n- Regression baselines: `tests/__baseline__/verify-*.mjs` compare against committed snapshots (providers, aliases, OAuth URLs). Run these after touching provider registry / alias logic.\n\n## Architecture\n\nTwo authoritative docs already exist — read them before working in these areas rather than re-deriving:\n- `docs/ARCHITECTURE.md` — full system: request lifecycle, combo/account fallback, OAuth + token refresh, cloud sync, data model.\n- `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/`.**\n\n### Request flow (the thing to understand first)\n`src/app/api/v1/*` route (Next rewrite maps `/v1/*` → `/api/v1/*` in `next.config.mjs`)\n→ `src/sse/handlers/chat.js` (parse, combo expansion, account-selection loop)\n→ `open-sse/handlers/chatCore.js` (detect source format, translate request, dispatch to executor, retry/refresh, stream setup)\n→ `open-sse/executors/*` (per-provider upstream call; `default.js` handles any OpenAI-compatible provider)\n→ `open-sse/translator/*` (client format ↔ provider format)\n→ SSE back to client.\n\n`src/sse/` is the app-side entry glue; `open-sse/` is the provider-agnostic engine (also usable standalone). Cross that boundary consciously.\n\n### Translator engine (`open-sse/translator/`)\n- 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`).\n- 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.\n- 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.\n\n### Provider registry (`open-sse/providers/registry/*`)\n- 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.\n- Add a provider: copy `providers/REGISTRY_TEMPLATE.js`, add models to `config/providerModels.js`. Only add an executor for non-OpenAI-compatible upstreams.\n\n### Persistence — IMPORTANT (ARCHITECTURE.md is stale here)\nState 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.\n- `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/`.\n- DB file location resolves via `src/lib/db/paths.js` (`DATA_DIR`, else `~/.9router/`).\n- Usage/logs (`src/lib/usageDb.js`, `usage.json` + `log.txt`) still live under `~/.9router` and do **not** follow `DATA_DIR`.\n\n### RTK token saver (`open-sse/rtk/`)\nPre-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.\n\n## Conventions & gotchas\n\n- Plain JavaScript (ESM), no TypeScript. `@/*` path alias → `src/*` (`jsconfig.json`).\n- `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.\n- 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.\n- 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.\n- **Security-first on PRs**: Security is the top priority when reviewing or creating PRs. Audit authentication, credential/token storage & leaks, header manipulation (`X-Forwarded-For`), and SSRF risks before functional logic. Always include explicit security warnings/notes when reporting PR reviews or changes to the user.\n- Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`).\n\n<!-- File: skills/9router-chat/SKILL.md (Tokens: ~801 | Category: anthropic-skill) -->\n---\nname: 9router-chat\ndescription: Chat / code generation via 9Router using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through 9Router.\n---\n\n# 9Router — Chat\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Endpoints\n\n- `POST $NINEROUTER_URL/v1/chat/completions` — OpenAI format\n- `POST $NINEROUTER_URL/v1/messages` — Anthropic format\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models | jq '.data[].id'\n# Per-model metadata (contextWindow, params)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/gpt-4o\"\n```\n\nCombos (e.g. `vip`, `mycodex`) auto-fallback through multiple providers.\n\n## OpenAI format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/chat/completions \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/gpt-5\",\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}],\"stream\":false}'\n```\n\nJS (OpenAI SDK):\n\n```js\nimport OpenAI from \"openai\";\nconst client = new OpenAI({ baseURL: `${process.env.NINEROUTER_URL}/v1`, apiKey: process.env.NINEROUTER_KEY });\nconst res = await client.chat.completions.create({\n  model: \"openai/gpt-5\",\n  messages: [{ role: \"user\", content: \"Hi\" }],\n  stream: true,\n});\nfor await (const chunk of res) process.stdout.write(chunk.choices[0]?.delta?.content || \"\");\n```\n\n## Anthropic format\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/messages \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"anthropic-version: 2023-06-01\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"cc/claude-opus-4-7\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}]}'\n```\n\n## Response shape\n\nOpenAI (`/v1/chat/completions`):\n```json\n{ \"id\": \"chatcmpl-...\", \"object\": \"chat.completion\", \"model\": \"openai/gpt-5\",\n  \"choices\": [{ \"index\": 0, \"message\": { \"role\": \"assistant\", \"content\": \"Hello!\" }, \"finish_reason\": \"stop\" }],\n  \"usage\": { \"prompt_tokens\": 8, \"completion_tokens\": 2, \"total_tokens\": 10 } }\n```\n\nStreaming (`stream:true`) emits SSE: `data: {choices:[{delta:{content:\"...\"}}]}\\n\\n` ... `data: [DONE]\\n\\n`.\n\nAnthropic (`/v1/messages`):\n```json\n{ \"id\": \"msg_...\", \"type\": \"message\", \"role\": \"assistant\", \"model\": \"cc/claude-opus-4-7\",\n  \"content\": [{ \"type\": \"text\", \"text\": \"Hello!\" }],\n  \"stop_reason\": \"end_turn\", \"usage\": { \"input_tokens\": 8, \"output_tokens\": 2 } }\n```\n\n<!-- File: skills/9router-embeddings/SKILL.md (Tokens: ~725 | Category: anthropic-skill) -->\n---\nname: 9router-embeddings\ndescription: Generate vector embeddings via 9Router /v1/embeddings using OpenAI / Gemini / Mistral / Voyage / Nvidia / GitHub embedding models for RAG, semantic search, similarity. Use when the user wants embeddings, vectors, RAG, semantic search, or to embed text.\n---\n\n# 9Router — Embeddings\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/embedding | jq '.data[].id'\n# Per-model dimensions\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/text-embedding-3-small\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/embeddings`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/embedding` |\n| `input` | yes | string OR array of strings |\n| `encoding_format` | no | `float` (default) / `base64` |\n| `dimensions` | no | OpenAI v3 only |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/embeddings \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/text-embedding-3-small\",\"input\":[\"hello\",\"world\"]}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/embeddings`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/text-embedding-004\", input: \"RAG chunk text\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].embedding.length);  // dimension\n```\n\n## Response shape\n\n```json\n{ \"object\": \"list\", \"model\": \"openai/text-embedding-3-small\",\n  \"data\": [\n    { \"object\": \"embedding\", \"index\": 0, \"embedding\": [0.0123, -0.045, ...] },\n    { \"object\": \"embedding\", \"index\": 1, \"embedding\": [...] }\n  ],\n  \"usage\": { \"prompt_tokens\": 5, \"total_tokens\": 5 } }\n```\n\n## Provider quirks\n\n| Provider | Notes |\n|---|---|\n| `openai`, `openrouter`, `mistral`, `voyage-ai`, `fireworks`, `together`, `nebius`, `github`, `nvidia`, `jina-ai` | Native OpenAI shape — `dimensions` works only on OpenAI v3 (`text-embedding-3-*`) |\n| `gemini`, `google_ai_studio` | Server auto-converts to `embedContent`/`batchEmbedContents` — send OpenAI shape |\n| `openai-compatible-*`, `custom-embedding-*` | Custom `baseUrl` from credentials |\n\nBatch (`input` as array) is faster; some providers cap batch size.\n\n<!-- File: skills/9router-image/SKILL.md (Tokens: ~1051 | Category: anthropic-skill) -->\n---\nname: 9router-image\ndescription: Generate images via 9Router /v1/images/generations using OpenAI / Gemini Imagen / DALL-E / FLUX / MiniMax / SDWebUI / ComfyUI / Codex models. Use when the user wants to create, generate, draw, or render an image, picture, or text-to-image (txt2img).\n---\n\n# 9Router — Image Generation\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/image | jq '.data[].id'\n# Per-model params/options (size enum, quality enum, capabilities like edit)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/dall-e-3\"\n```\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/images/generations`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/image` |\n| `prompt` | yes | image description |\n| `n` | no | count (provider-dependent) |\n| `size` | no | `1024x1024`, `1792x1024`, ... |\n| `quality` | no | `standard` / `hd` (OpenAI) |\n| `response_format` | no | `url` (default) or `b64_json` |\n\nAdd query `?response_format=binary` to receive raw image bytes (handy for saving file).\n\n## Examples\n\nSave to file (binary):\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/images/generations?response_format=binary\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"gemini/gemini-3-pro-image-preview\",\"prompt\":\"watercolor mountains at sunrise\",\"size\":\"1024x1024\"}' \\\n  --output out.png\n```\n\nJS (URL response):\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/images/generations`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"gemini/gemini-3-pro-image-preview\", prompt: \"neon city\", size: \"1024x1024\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].url || data[0].b64_json.slice(0, 40));\n```\n\n## Response shape\n\nJSON (default `response_format=url`):\n```json\n{ \"created\": 1735000000, \"data\": [{ \"url\": \"https://...\" }] }\n```\n\n`response_format=b64_json`:\n```json\n{ \"created\": 1735000000, \"data\": [{ \"b64_json\": \"iVBORw0KGgo...\" }] }\n```\n\nQuery `?response_format=binary` returns raw image bytes (Content-Type `image/png` or `image/jpeg`).\n\n## Provider quirks\n\nCommon fields above work everywhere. These add/override:\n\n| Provider | Extra/changed fields | Notes |\n|---|---|---|\n| `openai`, `minimax`, `openrouter`, `recraft` | `quality`, `style`, `response_format` | Standard OpenAI shape |\n| `gemini` (nano-banana) | — | Only `prompt`; ignores `size`/`n` |\n| `codex` (gpt-5.4-image) | `image`, `images[]`, `image_detail`, `output_format`, `background` | SSE stream; **ChatGPT Plus/Pro required** |\n| `huggingface` | — | Only `prompt`; returns single image |\n| `nanobanana` | `image`, `images[]` (edit mode) | `size` → aspect ratio; async polling |\n| `fal-ai` | `image` (img2img) | `n` → `num_images`; `size` → ratio; async |\n| `stability-ai` | `style` (preset), `output_format` | `size` → `aspect_ratio` |\n| `black-forest-labs` (FLUX) | `image` (ref) | `size` → exact `width`/`height`; async |\n| `runwayml` | `image` (ref) | `size` → ratio; async; video models exist |\n| `sdwebui`, `comfyui` | — | Localhost noAuth (`:7860` / `:8188`) |\n\n<!-- File: skills/9router-stt/SKILL.md (Tokens: ~1155 | Category: anthropic-skill) -->\n---\nname: 9router-stt\ndescription: Speech-to-text via 9Router /v1/audio/transcriptions using OpenAI Whisper / Groq / Gemini / Deepgram / AssemblyAI / NVIDIA / HuggingFace models. Use when the user wants to transcribe audio, convert speech to text, or get subtitles from audio files.\n---\n\n# 9Router — Speech-to-Text\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/stt | jq '.data[].id'\n# Per-model params (language, response_format, prompt, temperature support)\ncurl \"$NINEROUTER_URL/v1/models/info?id=openai/whisper-1\"\n```\n\n`model` = STT model ID (e.g. `openai/whisper-1`, `groq/whisper-large-v3`, `deepgram/nova-3`, `gemini/gemini-2.5-flash`).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/transcriptions` (OpenAI Whisper compatible, `multipart/form-data`)\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/stt` |\n| `file` | yes | audio file (mp3, wav, m4a, webm, ogg, flac) |\n| `language` | no | ISO-639-1 (e.g. `en`, `vi`) |\n| `prompt` | no | hint text to guide transcription |\n| `response_format` | no | `json` (default) / `text` / `verbose_json` / `srt` / `vtt` |\n| `temperature` | no | 0–1 |\n\n## Examples\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/transcriptions\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -F \"model=openai/whisper-1\" \\\n  -F \"file=@audio.mp3\" \\\n  -F \"language=vi\"\n```\n\nJS (Node):\n\n```js\nimport { createReadStream } from \"node:fs\";\nconst form = new FormData();\nform.append(\"model\", \"groq/whisper-large-v3-turbo\");\nform.append(\"file\", new Blob([await (await import(\"node:fs/promises\")).readFile(\"audio.mp3\")]), \"audio.mp3\");\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/transcriptions`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}` },\n  body: form,\n});\nconst { text } = await r.json();\nconsole.log(text);\n```\n\n## Response shape\n\nDefault (`response_format=json`):\n```json\n{ \"text\": \"Xin chào, đây là bản ghi âm.\" }\n```\n\n`verbose_json` adds `language`, `duration`, `segments[]` with timestamps.\n`srt` / `vtt` return subtitle text.\n\n## Provider quirks\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe` | Native OpenAI shape |\n| `groq` | `whisper-large-v3`, `whisper-large-v3-turbo`, `distil-whisper-large-v3-en` | Fastest; OpenAI shape |\n| `gemini` | `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-2.5-flash-lite` | Server converts to `generateContent` with audio inline |\n| `deepgram` | `nova-3`, `nova-2`, `whisper-large` | Token auth; server adapts response |\n| `assemblyai` | `universal-3-pro`, `universal-2` | Async upload+poll handled server-side |\n| `nvidia` | `nvidia/parakeet-ctc-1.1b-asr` | NIM endpoint |\n| `huggingface` | `openai/whisper-large-v3`, `openai/whisper-small` | HF Inference API |\n| `elevenlabs` | `scribe_v1`, `scribe_v2` | Whisper-compatible shape; `xi-api-key` auth, not Bearer. Extra params: `timestamps_granularity` (`word`/`character`/`none`), `tag_audio_events`, and speaker labelling via **either** `diarize=true` **or** `num_speakers` (1–32) — sending both makes the request invalid, so `diarize` wins and `num_speakers` is dropped. Blank `language` is omitted upstream for auto-detect. `srt`/`vtt` and `verbose_json` segments are served from Scribe's own `additional_formats` render, so `segments[]` is omitted when the upstream render is unavailable rather than synthesized. Does not accept `temperature` or `prompt` — those are not forwarded. |\n\n<!-- File: skills/9router-tts/SKILL.md (Tokens: ~990 | Category: anthropic-skill) -->\n---\nname: 9router-tts\ndescription: Text-to-speech via 9Router /v1/audio/speech using OpenAI / ElevenLabs / Deepgram / Edge TTS / Google TTS / Hyperbolic / Inworld voices. Use when the user wants to convert text to speech, generate audio, voiceover, narrate, or read text aloud.\n---\n\n# 9Router — Text-to-Speech\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\n# 1) List models\ncurl $NINEROUTER_URL/v1/models/tts | jq '.data[].id'\n# 2) Per-model metadata (params, voicesUrl if voice-by-id)\ncurl \"$NINEROUTER_URL/v1/models/info?id=el/eleven_multilingual_v2\"\n# 3) List voices (elevenlabs, edge-tts, deepgram, inworld, local-device). Optional ?lang=vi\ncurl \"$NINEROUTER_URL/v1/audio/voices?provider=edge-tts&lang=vi\" | jq '.data[].model'\n```\n\n`model` field in `/v1/audio/speech` = voice ID directly (e.g. `edge-tts/vi-VN-HoaiMyNeural`, `el/<voice_id>`, or `openai/tts-1` model+default voice).\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/audio/speech`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | voice ID from `/v1/models/tts` |\n| `input` | yes | text to speak |\n\nQuery `?response_format=mp3` (default, raw bytes) or `?response_format=json` (`{audio: base64, format}`).\n\n## Examples\n\nSave MP3:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/audio/speech\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"openai/tts-1\",\"input\":\"Hello world\"}' \\\n  --output speech.mp3\n```\n\nJS (save file):\n\n```js\nimport { writeFile } from \"node:fs/promises\";\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/speech`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"el/eleven_multilingual_v2\", input: \"Xin chào\" }),\n});\nawait writeFile(\"speech.mp3\", Buffer.from(await r.arrayBuffer()));\n```\n\n## Response shape\n\nDefault → raw audio bytes (Content-Type `audio/mp3`).\n\n`?response_format=json`:\n```json\n{ \"audio\": \"SUQzBAAAA...\", \"format\": \"mp3\" }\n```\n\n## Provider quirks (model format)\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `tts-1/alloy` (model/voice) or just voice | Default model `gpt-4o-mini-tts` |\n| `elevenlabs` | `<model_id>/<voice_id>` or `<voice_id>` | Default model `eleven_flash_v2_5`; list voices in Dashboard |\n| `openrouter` | `openai/gpt-4o-mini-tts/alloy` | Streamed via chat-completions audio modality |\n| `edge-tts` | voice id e.g. `vi-VN-HoaiMyNeural` | **noAuth**; default `vi-VN-HoaiMyNeural` |\n| `google-tts` | language code e.g. `en`, `vi` | **noAuth** |\n| `local-device` | OS voice name (`say -v ?` / SAPI) | **noAuth**; needs `ffmpeg` |\n| `deepgram` | `aura-asteria-en` etc | Token auth |\n| `nvidia`, `inworld`, `cartesia`, `playht` | `model/voice` | Provider-specific auth header |\n| `coqui`, `tortoise` | speaker / voice id | Localhost noAuth |\n| `hyperbolic` | model id | Body = `{text}` only |\n\n<!-- File: skills/9router-video/SKILL.md (Tokens: ~1092 | Category: anthropic-skill) -->\n---\nname: 9router-video\ndescription: 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.\n---\n\n# 9Router — Video Generation (xAI Grok Imagine)\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\nRequires 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.\n\n## Endpoints (async job flow)\n\nVideo generation is **asynchronous**: the POST returns a `request_id` immediately, then you poll until the job is `done` or `failed`.\n\n| Endpoint | Purpose |\n|---|---|\n| `POST /v1/videos/generations` | text-to-video / image-to-video |\n| `POST /v1/videos/edits` | edit an existing video |\n| `POST /v1/videos/extensions` | extend an existing video |\n| `GET /v1/videos/{request_id}` | poll job status |\n\nRequest fields (passed through to xAI unchanged — see https://docs.x.ai/developers/rest-api-reference/inference/videos):\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | no | `xai/grok-imagine-video` (prefix is stripped before upstream) |\n| `prompt` | yes for T2V | video description |\n| `duration` | no | seconds |\n| `aspect_ratio` | no | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `3:2`, `2:3` |\n| `resolution` | no | `480p`, `720p`, `1080p` |\n| `image` | no | `{ \"url\": \"https://… or data:image/…;base64,…\" }` for image-to-video |\n| `video` | edits/extensions | `{ \"url\": \"…mp4\" }` or `{ \"file_id\": \"…\" }` |\n\n## Examples\n\nSubmit a job:\n\n```bash\ncurl -X POST \"$NINEROUTER_URL/v1/videos/generations\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -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\"}'\n# → {\"request_id\":\"abc123\"}   (response header x-9router-connection-id: <id>)\n```\n\nPoll until done (echo the connection header back so the same account polls the job):\n\n```bash\ncurl \"$NINEROUTER_URL/v1/videos/abc123\" \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"x-connection-id: <id from create response>\"\n# → {\"status\":\"pending\",\"progress\":42}\n# → {\"status\":\"done\",\"video\":{\"url\":\"https://…mp4\",\"duration\":8},\"model\":\"grok-imagine-video\"}\n# → {\"status\":\"failed\",\"error\":{\"code\":\"…\",\"message\":\"…\"}}\n```\n\nDownload: fetch `video.url` from the `done` response.\n\n## CLI one-shot\n\n```bash\n9router xai video \\\n  --prompt \"A cinematic tracking shot through a neon city at night\" \\\n  --output video.mp4\n# options: --model --duration --aspect-ratio --resolution --image --timeout --port --api-key\n```\n\nSubmits, polls with progress, downloads to `video.mp4.part`, atomically renames on success. Ctrl+C cancels cleanly; non-zero exit on failure.\n\n## Notes & limits\n\n- 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`).\n- 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.\n- Video models are tagged `kind: \"video\"` and are excluded from chat model lists and chat fallback combos.\n- 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.\n\n<!-- File: skills/9router-web-fetch/SKILL.md (Tokens: ~1142 | Category: anthropic-skill) -->\n---\nname: 9router-web-fetch\ndescription: Fetch URL → markdown / text / HTML via 9Router /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown.\n---\n\n# 9Router — Web Fetch\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webFetch\") | .id'\n# Per-provider params\ncurl \"$NINEROUTER_URL/v1/models/info?id=firecrawl/fetch\"\n```\n\nIDs end in `/fetch` (e.g. `firecrawl/fetch`, `jina/fetch`). `fetch-combo` chains providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/web/fetch`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `firecrawl` or `jina-reader`) |\n| `url` | yes | URL to extract |\n| `format` | no | `markdown` (default) / `text` / `html` |\n| `max_characters` | no | truncate output |\n\n## Examples\n\n### Jina Reader\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"jina-reader\",\"url\":\"https://9router.com\",\"format\":\"markdown\"}'\n```\n\n### Exa\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"exa\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Firecrawl\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"firecrawl\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Tavily\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Ollama Cloud\n\nUses the API key from the existing `ollama` connection.\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/web/fetch \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"ollama\",\"url\":\"https://example.com\",\"format\":\"markdown\"}'\n```\n\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/web/fetch`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"fetch-combo\", url: \"https://example.com\", format: \"markdown\", max_characters: 5000 }),\n});\nconst { data } = await r.json();\nconsole.log(data.title, data.content.length);\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"jina-reader\",\n  \"url\": \"...\",\n  \"title\": \"...\",\n  \"content\": { \"format\": \"markdown\", \"text\": \"...\", \"length\": 1234 },\n  \"links\": [\"https://example.com/related\"],\n  \"metadata\": { \"author\": null, \"published_at\": null, \"language\": null },\n  \"usage\": { \"fetch_cost_usd\": 0 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700 }\n}\n```\n\n`links` is included when the upstream provider returns discovered page links (currently Ollama Cloud).\n\n## Provider quirks\n\n| Provider | Auth | Best for |\n|---|---|---|\n| `firecrawl` | Bearer | JS-rendered pages, `format=markdown/html` |\n| `jina-reader` | Bearer (optional) | Free tier (~1M chars/mo); fastest plain markdown |\n| `tavily` | Bearer | Bulk extract; returns `raw_content` |\n| `exa` | `x-api-key` | Pre-indexed pages; fast text extraction |\n| `ollama` | Bearer | Markdown plus page title and discovered links; uses the Ollama Cloud key |\n\n<!-- File: skills/9router-web-search/SKILL.md (Tokens: ~1246 | Category: anthropic-skill) -->\n---\nname: 9router-web-search\ndescription: Web and X search via 9Router /v1/search using Tavily / Exa / Brave / Serper / SearXNG / Google PSE / Linkup / SearchAPI / You.com / Perplexity / Xquik. Use when the user wants to search the web, find articles, or search public X posts.\n---\n\n# 9Router — Web Search\n\nRequires `NINEROUTER_URL` (and `NINEROUTER_KEY` if auth enabled). See https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $NINEROUTER_URL/v1/models/web | jq '.data[] | select(.kind==\"webSearch\") | .id'\n# Per-provider params (searchTypes, maxResults, required options like cx for google-pse)\ncurl \"$NINEROUTER_URL/v1/models/info?id=tavily/search\"\n```\n\nIDs end in `/search` (e.g. `tavily/search`). Combos (`owned_by:\"combo\"`) chain providers with auto-fallback.\n\n## Endpoint\n\n`POST $NINEROUTER_URL/v1/search`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `tavily` or `brave`) |\n| `query` | yes | search query |\n| `max_results` | no | default 5 |\n| `search_type` | no | `web` (default) / `news` / `x` for Xquik |\n| `country`, `language`, `time_range`, `domain_filter` | no | provider-dependent |\n\n## Examples\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"tavily\",\"query\":\"9Router open source\",\"max_results\":5}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.NINEROUTER_URL}/v1/search`, {\n  method: \"POST\",\n  headers: { \"Authorization\": `Bearer ${process.env.NINEROUTER_KEY}`, \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ model: \"search-combo\", query: \"latest LLM benchmarks\", max_results: 10 }),\n});\nconsole.log(await r.json());\n```\n\nX search with Xquik:\n\n```bash\ncurl -X POST $NINEROUTER_URL/v1/search \\\n  -H \"Authorization: Bearer $NINEROUTER_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"xquik\",\"query\":\"from:github release\",\"max_results\":10,\"provider_options\":{\"queryType\":\"Latest\"}}'\n```\n\nAdd the Xquik API key in 9Router's provider settings. Xquik charges 1 credit per returned post. Continue a search by passing `pagination.next_cursor` as `provider_options.cursor`.\n\nXquik responses include provider pagination and credit usage:\n\n```json\n{\n  \"pagination\": { \"has_more\": true, \"next_cursor\": \"cursor-2\" },\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": null, \"provider_credits_used\": 10 }\n}\n```\n\n## Response shape\n\n```json\n{\n  \"provider\": \"tavily\",\n  \"query\": \"9Router open source\",\n  \"results\": [\n    {\n      \"title\": \"...\", \"url\": \"https://...\", \"display_url\": \"github.com/...\",\n      \"snippet\": \"...\", \"position\": 1, \"score\": 0.92,\n      \"published_at\": null, \"favicon_url\": null, \"content\": null,\n      \"metadata\": { \"author\": null, \"language\": null, \"source_type\": null, \"image_url\": null },\n      \"citation\": { \"provider\": \"tavily\", \"retrieved_at\": \"2026-...\", \"rank\": 1 }\n    }\n  ],\n  \"answer\": null,\n  \"usage\": { \"queries_used\": 1, \"search_cost_usd\": 0.008 },\n  \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700, \"total_results_available\": 12 },\n  \"errors\": []\n}\n```\n\n## Provider quirks\n\nAll accept `query` + `max_results`. Optional fields vary:\n\n| Provider | Supports | Required extras |\n|---|---|---|\n| `tavily` | country, domain_filter, news topic | — |\n| `exa` | domain_filter (incl/excl), news category | — |\n| `brave-search` | country, language | — |\n| `serper` | country, language, news endpoint | — |\n| `perplexity` | country, language, domain_filter | — |\n| `linkup` | domain_filter, time_range | `depth: fast/standard/deep` (option) |\n| `google-pse` | country, language, time_range, offset | **`cx` required** (providerOptions) |\n| `searchapi` | country, language, pagination | — |\n| `youcom` | country, language, time_range, domain_filter, full_page | — |\n| `searxng` | language, time_range | Self-hosted, **noAuth** |\n| `xquik` | X/Twitter search operators, language, cursor pagination | `queryType: Latest/Top`, `cursor` (options) |\n\nProvider IS the model — `\"provider\":\"tavily\" ≡ \"model\":\"tavily\"`.\n\n<!-- File: skills/9router/SKILL.md (Tokens: ~837 | Category: anthropic-skill) -->\n---\nname: 9router\ndescription: Entry point for 9Router — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions 9Router, NINEROUTER_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed.\n---\n\n# 9Router\n\nLocal/remote AI gateway exposing OpenAI-compatible REST. One key, many providers, auto-fallback.\n\n## Setup\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"      # or VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                      # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nAll requests: `${NINEROUTER_URL}/v1/...` with header `Authorization: Bearer ${NINEROUTER_KEY}` (omit if auth disabled).\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`\n\n## Discover models\n\n```bash\ncurl $NINEROUTER_URL/v1/models                  # chat/LLM (default)\ncurl $NINEROUTER_URL/v1/models/image            # image-gen\ncurl $NINEROUTER_URL/v1/models/tts              # text-to-speech\ncurl $NINEROUTER_URL/v1/models/embedding        # embeddings\ncurl $NINEROUTER_URL/v1/models/web              # web search + fetch (entries have `kind` field)\ncurl $NINEROUTER_URL/v1/models/stt              # speech-to-text\ncurl $NINEROUTER_URL/v1/models/image-to-text    # vision\n```\n\nUse `data[].id` as `model` field in requests. Combos appear with `owned_by:\"combo\"`.\n\nResponse shape:\n```json\n{ \"object\": \"list\", \"data\": [\n  { \"id\": \"openai/gpt-5\", \"object\": \"model\", \"owned_by\": \"openai\", \"created\": 1735000000 },\n  { \"id\": \"tavily/search\", \"object\": \"model\", \"kind\": \"webSearch\", \"owned_by\": \"tavily\", \"created\": 1735000000 }\n]}\n```\n\n## Capability skills\n\nWhen the user needs a specific capability, fetch that skill's `SKILL.md` from its raw URL:\n\n| Capability | Raw URL |\n|---|---|\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## Errors\n\n- 401 → set/refresh `NINEROUTER_KEY` (Dashboard → Keys)\n- 400 `Invalid model format` → check `model` exists in `/v1/models/<kind>`\n- 503 `All accounts unavailable` → wait `retry-after` or add another provider account\n\n<!-- File: skills/README.md (Tokens: ~598 | Category: anthropic-skill) -->\n# 9Router — Agent Skills\n\nDrop-in skills for any AI agent (Claude, Cursor, ChatGPT, custom SDK). Just **copy a link** below and paste it to your AI — it will fetch the skill and use 9Router for you.\n\n> Tip: start with the **9router** entry skill — it covers setup and links to all capability skills.\n\n## Skills\n\n| Capability | Copy link below and paste to your AI |\n|---|---|\n| **Entry / Setup** (start here) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md |\n| Chat / code-gen | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-image/SKILL.md |\n| Video generation (xAI Grok Imagine) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-video/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router-web-fetch/SKILL.md |\n\n## How to use\n\nPaste to your AI (Claude, Cursor, ChatGPT, …):\n\n```\nRead this skill and use it: https://raw.githubusercontent.com/decolua/9router/refs/heads/master/skills/9router/SKILL.md\n```\n\nThen ask normally — *\"generate an image of a cat\"*, *\"transcribe this URL\"*, etc.\n\n## Configure your shell once\n\n```bash\nexport NINEROUTER_URL=\"http://localhost:20128\"   # local default, or your VPS / tunnel URL\nexport NINEROUTER_KEY=\"sk-...\"                   # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nVerify: `curl $NINEROUTER_URL/api/health` → `{\"ok\":true}`.\n\n## Links\n\n- Source: https://github.com/decolua/9router\n- Dashboard: https://9router.com\n\n<!-- File: docs/ARCHITECTURE.md (Tokens: ~4203 | Category: project-spec) -->\n# 9Router Architecture\n\n_Last updated: 2026-02-06_\n\n## Executive Summary\n\n9Router is a local AI routing gateway and dashboard built on Next.js.\nIt provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic across multiple upstream providers with translation, fallback, token refresh, and usage tracking.\n\nCore capabilities:\n\n- OpenAI-compatible API surface for CLI/tools\n- Request/response translation across provider formats\n- Model combo fallback (multi-model sequence)\n- Account-level fallback (multi-account per provider)\n- OAuth + API-key provider connection management\n- Local persistence for providers, keys, aliases, combos, settings, pricing\n- Usage/cost tracking and request logging\n- Optional cloud sync for multi-device/state sync\n\nPrimary runtime model:\n\n- Next.js app routes under `src/app/api/*` implement both dashboard APIs and compatibility APIs\n- A shared SSE/routing core in `src/sse/*` + `open-sse/*` handles provider execution, translation, streaming, fallback, and usage\n\n## Scope and Boundaries\n\n### In Scope\n\n- Local gateway runtime\n- Dashboard management APIs\n- Provider authentication and token refresh\n- Request translation and SSE streaming\n- Local state + usage persistence\n- Optional cloud sync orchestration\n\n### Out of Scope\n\n- Cloud service implementation behind `NEXT_PUBLIC_CLOUD_URL`\n- Provider SLA/control plane outside local process\n- External CLI binaries themselves (Claude CLI, Codex CLI, etc.)\n\n## High-Level System Context\n\n```mermaid\nflowchart LR\n    subgraph Clients[Developer Clients]\n        C1[Claude Code]\n        C2[Codex CLI]\n        C3[OpenClaw / Droid / Cline / Continue / Roo]\n        C4[Custom OpenAI-compatible clients]\n        BROWSER[Browser Dashboard]\n    end\n\n    subgraph Router[9Router Local Process]\n        API[V1 Compatibility API\\n/v1/*]\n        DASH[Dashboard + Management API\\n/api/*]\n        CORE[SSE + Translation Core\\nopen-sse + src/sse]\n        DB[(db.json)]\n        UDB[(usage.json + log.txt)]\n    end\n\n    subgraph Upstreams[Upstream Providers]\n        P1[OAuth Providers\\nClaude/Codex/Gemini/Qwen/iFlow/GitHub/Kiro/Cursor/Antigravity]\n        P2[API Key Providers\\nOpenAI/Anthropic/OpenRouter/GLM/Kimi/MiniMax]\n        P3[Compatible Nodes\\nOpenAI-compatible / Anthropic-compatible]\n    end\n\n    subgraph Cloud[Optional Cloud Sync]\n        CLOUD[Cloud Sync Endpoint\\nNEXT_PUBLIC_CLOUD_URL]\n    end\n\n    C1 --> API\n    C2 --> API\n    C3 --> API\n    C4 --> API\n    BROWSER --> DASH\n\n    API --> CORE\n    DASH --> DB\n    CORE --> DB\n    CORE --> UDB\n\n    CORE --> P1\n    CORE --> P2\n    CORE --> P3\n\n    DASH --> CLOUD\n```\n\n## Core Runtime Components\n\n## 1) API and Routing Layer (Next.js App Routes)\n\nMain directories:\n\n- `src/app/api/v1/*` and `src/app/api/v1beta/*` for compatibility APIs\n- `src/app/api/*` for management/configuration APIs\n- Next rewrites in `next.config.mjs` map `/v1/*` to `/api/v1/*`\n\nImportant compatibility routes:\n\n- `src/app/api/v1/chat/completions/route.js`\n- `src/app/api/v1/messages/route.js`\n- `src/app/api/v1/responses/route.js`\n- `src/app/api/v1/models/route.js`\n- `src/app/api/v1/messages/count_tokens/route.js`\n- `src/app/api/v1beta/models/route.js`\n- `src/app/api/v1beta/models/[...path]/route.js`\n\nManagement domains:\n\n- Auth/settings: `src/app/api/auth/*`, `src/app/api/settings/*`\n- Providers/connections: `src/app/api/providers*`\n- Provider nodes: `src/app/api/provider-nodes*`\n- OAuth: `src/app/api/oauth/*`\n- Keys/aliases/combos/pricing: `src/app/api/keys*`, `src/app/api/models/alias`, `src/app/api/combos*`, `src/app/api/pricing`\n- Usage: `src/app/api/usage/*`\n- Sync/cloud: `src/app/api/sync/*`, `src/app/api/cloud/*`\n- CLI tooling helpers: `src/app/api/cli-tools/*`\n\n## 2) SSE + Translation Core\n\nMain flow modules:\n\n- Entry: `src/sse/handlers/chat.js`\n- Core orchestration: `open-sse/handlers/chatCore.js`\n- Provider execution adapters: `open-sse/executors/*`\n- Format detection/provider config: `open-sse/services/provider.js`\n- Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js`\n- Account fallback logic: `open-sse/services/accountFallback.js`\n- Translation registry: `open-sse/translator/index.js`\n- Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js`\n- Usage extraction/normalization: `open-sse/utils/usageTracking.js`\n\n## 3) Persistence Layer\n\nPrimary state DB:\n\n- `src/lib/localDb.js`\n- file: `${DATA_DIR}/db.json` (or `~/.9router/db.json` when `DATA_DIR` is unset)\n- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing\n\nUsage DB:\n\n- `src/lib/usageDb.js`\n- files: `~/.9router/usage.json`, `~/.9router/log.txt`\n- note: currently independent from `DATA_DIR`\n\n## 4) Auth + Security Surfaces\n\n- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js`\n- API key generation/verification: `src/shared/utils/apiKey.js`\n- Provider secrets persisted in `providerConnections` entries\n- Optional proxy support for upstream calls via env proxy variables (`open-sse/utils/proxyFetch.js`)\n\n## 5) Cloud Sync\n\n- Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js`\n- Periodic task: `src/shared/services/cloudSyncScheduler.js`\n- Control route: `src/app/api/sync/cloud/route.js`\n\n## Request Lifecycle (`/v1/chat/completions`)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant Client as CLI/SDK Client\n    participant Route as /api/v1/chat/completions\n    participant Chat as src/sse/handlers/chat\n    participant Core as open-sse/handlers/chatCore\n    participant Model as Model Resolver\n    participant Auth as Credential Selector\n    participant Exec as Provider Executor\n    participant Prov as Upstream Provider\n    participant Stream as Stream Translator\n    participant Usage as usageDb\n\n    Client->>Route: POST /v1/chat/completions\n    Route->>Chat: handleChat(request)\n    Chat->>Model: parse/resolve model or combo\n\n    alt Combo model\n        Chat->>Chat: iterate combo models (handleComboChat)\n    end\n\n    Chat->>Auth: getProviderCredentials(provider)\n    Auth-->>Chat: active account + tokens/api key\n\n    Chat->>Core: handleChatCore(body, modelInfo, credentials)\n    Core->>Core: detect source format\n    Core->>Core: translate request to target format\n    Core->>Exec: execute(provider, transformedBody)\n    Exec->>Prov: upstream API call\n    Prov-->>Exec: SSE/JSON response\n    Exec-->>Core: response + metadata\n\n    alt 401/403\n        Core->>Exec: refreshCredentials()\n        Exec-->>Core: updated tokens\n        Core->>Exec: retry request\n    end\n\n    Core->>Stream: translate/normalize stream to client format\n    Stream-->>Client: SSE chunks / JSON response\n\n    Stream->>Usage: extract usage + persist history/log\n```\n\n## Combo + Account Fallback Flow\n\n```mermaid\nflowchart TD\n    A[Incoming model string] --> B{Is combo name?}\n    B -- Yes --> C[Load combo models sequence]\n    B -- No --> D[Single model path]\n\n    C --> E[Try model N]\n    E --> F[Resolve provider/model]\n    D --> F\n\n    F --> G[Select account credentials]\n    G --> H{Credentials available?}\n    H -- No --> I[Return provider unavailable]\n    H -- Yes --> J[Execute request]\n\n    J --> K{Success?}\n    K -- Yes --> L[Return response]\n    K -- No --> M{Fallback-eligible error?}\n\n    M -- No --> N[Return error]\n    M -- Yes --> O[Mark account unavailable cooldown]\n    O --> P{Another account for provider?}\n    P -- Yes --> G\n    P -- No --> Q{In combo with next model?}\n    Q -- Yes --> E\n    Q -- No --> R[Return all unavailable]\n```\n\nFallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics.\n\n## OAuth Onboarding and Token Refresh Lifecycle\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Dashboard UI\n    participant OAuth as /api/oauth/[provider]/[action]\n    participant ProvAuth as Provider Auth Server\n    participant DB as localDb\n    participant Test as /api/providers/[id]/test\n    participant Exec as Provider Executor\n\n    UI->>OAuth: GET authorize or device-code\n    OAuth->>ProvAuth: create auth/device flow\n    ProvAuth-->>OAuth: auth URL or device code payload\n    OAuth-->>UI: flow data\n\n    UI->>OAuth: POST exchange or poll\n    OAuth->>ProvAuth: token exchange/poll\n    ProvAuth-->>OAuth: access/refresh tokens\n    OAuth->>DB: createProviderConnection(oauth data)\n    OAuth-->>UI: success + connection id\n\n    UI->>Test: POST /api/providers/[id]/test\n    Test->>Exec: validate credentials / optional refresh\n    Exec-->>Test: valid or refreshed token info\n    Test->>DB: update status/tokens/errors\n    Test-->>UI: validation result\n```\n\nRefresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`.\n\n## Cloud Sync Lifecycle (Enable / Sync / Disable)\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant UI as Endpoint Page UI\n    participant Sync as /api/sync/cloud\n    participant DB as localDb\n    participant Cloud as External Cloud Sync\n    participant Claude as ~/.claude/settings.json\n\n    UI->>Sync: POST action=enable\n    Sync->>DB: set cloudEnabled=true\n    Sync->>DB: ensure API key exists\n    Sync->>Cloud: POST /sync/{machineId} (providers/aliases/combos/keys)\n    Cloud-->>Sync: sync result\n    Sync->>Cloud: GET /{machineId}/v1/verify\n    Sync-->>UI: enabled + verification status\n\n    UI->>Sync: POST action=sync\n    Sync->>Cloud: POST /sync/{machineId}\n    Cloud-->>Sync: remote data\n    Sync->>DB: update newer local tokens/status\n    Sync-->>UI: synced\n\n    UI->>Sync: POST action=disable\n    Sync->>DB: set cloudEnabled=false\n    Sync->>Cloud: DELETE /sync/{machineId}\n    Sync->>Claude: switch ANTHROPIC_BASE_URL back to local (if needed)\n    Sync-->>UI: disabled\n```\n\nPeriodic sync is triggered by `CloudSyncScheduler` when cloud is enabled.\n\n## Data Model and Storage Map\n\n```mermaid\nerDiagram\n    SETTINGS ||--o{ PROVIDER_CONNECTION : controls\n    PROVIDER_NODE ||--o{ PROVIDER_CONNECTION : backs_compatible_provider\n    PROVIDER_CONNECTION ||--o{ USAGE_ENTRY : emits_usage\n\n    SETTINGS {\n      boolean cloudEnabled\n      number stickyRoundRobinLimit\n      boolean requireLogin\n      string password_hash\n    }\n\n    PROVIDER_CONNECTION {\n      string id\n      string provider\n      string authType\n      string name\n      number priority\n      boolean isActive\n      string apiKey\n      string accessToken\n      string refreshToken\n      string expiresAt\n      string testStatus\n      string lastError\n      string rateLimitedUntil\n      json providerSpecificData\n    }\n\n    PROVIDER_NODE {\n      string id\n      string type\n      string name\n      string prefix\n      string apiType\n      string baseUrl\n    }\n\n    MODEL_ALIAS {\n      string alias\n      string targetModel\n    }\n\n    COMBO {\n      string id\n      string name\n      string[] models\n    }\n\n    API_KEY {\n      string id\n      string name\n      string key\n      string machineId\n      boolean isActive\n    }\n\n    USAGE_ENTRY {\n      string provider\n      string model\n      number prompt_tokens\n      number completion_tokens\n      string connectionId\n      string timestamp\n    }\n```\n\nPhysical storage files:\n\n- main state: `${DATA_DIR}/db.json` (or `~/.9router/db.json`)\n- usage stats: `~/.9router/usage.json`\n- request log lines: `~/.9router/log.txt`\n- optional translator/request debug sessions: `<repo>/logs/...`\n\n## Deployment Topology\n\n```mermaid\nflowchart LR\n    subgraph LocalHost[Developer Host]\n        CLI[CLI Tools]\n        Browser[Dashboard Browser]\n    end\n\n    subgraph ContainerOrProcess[9Router Runtime]\n        Next[Next.js Server\\nPORT=20128]\n        Core[SSE Core + Executors]\n        MainDB[(db.json)]\n        UsageDB[(usage.json/log.txt)]\n    end\n\n    subgraph External[External Services]\n        Providers[AI Providers]\n        SyncCloud[Cloud Sync Service]\n    end\n\n    CLI --> Next\n    Browser --> Next\n    Next --> Core\n    Next --> MainDB\n    Core --> MainDB\n    Core --> UsageDB\n    Core --> Providers\n    Next --> SyncCloud\n```\n\n## Module Mapping (Decision-Critical)\n\n### Route and API Modules\n\n- `src/app/api/v1/*`, `src/app/api/v1beta/*`: compatibility APIs\n- `src/app/api/providers*`: provider CRUD, validation, testing\n- `src/app/api/provider-nodes*`: custom compatible node management\n- `src/app/api/oauth/*`: OAuth/device-code flows\n- `src/app/api/keys*`: local API key lifecycle\n- `src/app/api/models/alias`: alias management\n- `src/app/api/combos*`: fallback combo management\n- `src/app/api/pricing`: pricing overrides for cost calculation\n- `src/app/api/usage/*`: usage and logs APIs\n- `src/app/api/sync/*` + `src/app/api/cloud/*`: cloud sync and cloud-facing helpers\n- `src/app/api/cli-tools/*`: local CLI config writers/checkers\n\n### Routing and Execution Core\n\n- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop\n- `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup\n- `open-sse/executors/*`: provider-specific network and format behavior\n\n### Translation Registry and Format Converters\n\n- `open-sse/translator/index.js`: translator registry and orchestration\n- Request translators: `open-sse/translator/request/*`\n- Response translators: `open-sse/translator/response/*`\n- Format constants: `open-sse/translator/formats.js`\n\n### Persistence\n\n- `src/lib/localDb.js`: persistent config/state\n- `src/lib/usageDb.js`: usage history and rolling request logs\n\n## Provider Executor Coverage\n\nSpecialized executors:\n\n- `antigravity`\n- `gemini-cli`\n- `github`\n- `kiro`\n- `codex`\n- `cursor`\n\nDefault executor path:\n\n- all other providers (including compatible node providers) use `open-sse/executors/default.js`\n\n## Format Translation Coverage\n\nDetected source formats include:\n\n- `openai`\n- `openai-responses`\n- `claude`\n- `gemini`\n\nTarget formats include:\n\n- OpenAI chat/Responses\n- Claude\n- Gemini/Gemini-CLI/Antigravity envelope\n- Kiro\n- Cursor\n\nTranslations are selected dynamically based on source payload shape and provider target format.\n\n## Failure Modes and Resilience\n\n## 1) Account/Provider Availability\n\n- provider account cooldown on transient/rate/auth errors\n- account fallback before failing request\n- combo model fallback when current model/provider path is exhausted\n\n## 2) Token Expiry\n\n- pre-check and refresh with retry for refreshable providers\n- 401/403 retry after refresh attempt in core path\n\n## 3) Stream Safety\n\n- disconnect-aware stream controller\n- translation stream with end-of-stream flush and `[DONE]` handling\n- usage estimation fallback when provider usage metadata is missing\n\n## 4) Cloud Sync Degradation\n\n- sync errors are surfaced but local runtime continues\n- scheduler has retry-capable logic, but periodic execution currently calls single-attempt sync by default\n\n## 5) Data Integrity\n\n- DB shape migration/repair for missing keys\n- corrupt JSON reset safeguards for localDb and usageDb\n\n## Observability and Operational Signals\n\nRuntime visibility sources:\n\n- console logs from `src/sse/utils/logger.js`\n- per-request usage aggregates in `usage.json`\n- textual request status log in `log.txt`\n- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true`\n- dashboard usage endpoints (`/api/usage/*`) for UI consumption\n\n## Security-Sensitive Boundaries\n\n- JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing\n- Initial password fallback (`INITIAL_PASSWORD`, default `123456`) must be overridden in real deployments\n- API key HMAC secret (`API_KEY_SECRET`) secures generated local API key format\n- Provider secrets (API keys/tokens) are persisted in local DB and should be protected at filesystem level\n- Cloud sync endpoints rely on API key auth + machine id semantics\n\n## Environment and Runtime Matrix\n\nEnvironment variables actively used by code:\n\n- App/auth: `JWT_SECRET`, `INITIAL_PASSWORD`\n- Storage: `DATA_DIR`\n- Security hashing: `API_KEY_SECRET`, `MACHINE_ID_SALT`\n- Logging: `ENABLE_REQUEST_LOGS`\n- Sync/cloud URLing: `NEXT_PUBLIC_BASE_URL`, `NEXT_PUBLIC_CLOUD_URL`\n- Outbound proxy: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` and lowercase variants\n- Platform/runtime helpers (not app-specific config): `APPDATA`, `NODE_ENV`, `PORT`, `HOSTNAME`\n\n## Known Architectural Notes\n\n1. `usageDb` currently stores under `~/.9router` and does not follow `DATA_DIR`.\n2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`.\n3. Request logger writes full headers/body when enabled; treat log directory as sensitive.\n4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability.\n\n## Operational Verification Checklist\n\n- Build from source: `cd /root/dev/9router && npm run build`\n- Build Docker image: `cd /root/dev/9router && docker build -t 9router .`\n- Start service and verify:\n- `GET /api/settings`\n- `GET /api/v1/models`\n- CLI target base URL should be `http://<host>:20128/v1` when `PORT=20128`\n</yakaai_skills>"}