{"id":51370984,"url":"https://github.com/aikazu/kelola-router","last_synced_at":"2026-07-03T06:33:53.189Z","repository":{"id":361838088,"uuid":"1256032486","full_name":"aikazu/kelola-router","owner":"aikazu","description":"Local-first API router for MiniMax + Kiro (AWS CodeWhisperer / Amazon Q) upstreams — OpenAI \u0026 Anthropic compatible, multi-account fallback, switchable Kiro IDE/CLI persona, RTK + Caveman compression, built-in dashboard (Hono + SQLite + Preact)","archived":false,"fork":false,"pushed_at":"2026-06-16T14:51:26.000Z","size":2394,"stargazers_count":4,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-16T16:21:04.007Z","etag":null,"topics":["amazon-q","anthropic","api-router","aws-codewhisperer","hono","kiro","llm-proxy","minimax","openai","preact","sqlite","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aikazu.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-01T11:56:05.000Z","updated_at":"2026-06-16T14:54:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aikazu/kelola-router","commit_stats":null,"previous_names":["aikazu/kelola-router"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/aikazu/kelola-router","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aikazu%2Fkelola-router","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aikazu%2Fkelola-router/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aikazu%2Fkelola-router/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aikazu%2Fkelola-router/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aikazu","download_url":"https://codeload.github.com/aikazu/kelola-router/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aikazu%2Fkelola-router/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35075804,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-03T02:00:05.635Z","response_time":110,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["amazon-q","anthropic","api-router","aws-codewhisperer","hono","kiro","llm-proxy","minimax","openai","preact","sqlite","typescript"],"created_at":"2026-07-03T06:33:52.596Z","updated_at":"2026-07-03T06:33:53.170Z","avatar_url":"https://github.com/aikazu.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kelola Router\n\n\u003e Local-first API router for **MiniMax**, **Kiro (AWS CodeWhisperer / Amazon Q)**, **CodeBuddy**, **Pioneer**, **Notion**, and **Z.AI**: provider-prefix routing, multi-account pool, combo fallback, prompt caching, RTK + Caveman compression, live request-flow console, and a built-in Preact dashboard.\n\n[![Node \u003e=20](https://img.shields.io/badge/node-%E2%89%A520-339933?logo=node.js\u0026logoColor=white)](https://nodejs.org)\n[![TypeScript 5.x](https://img.shields.io/badge/typescript-5.x-3178C6?logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org)\n[![Hono](https://img.shields.io/badge/hono-server-E36002?logo=hono\u0026logoColor=white)](https://hono.dev)\n[![SQLite (WAL)](https://img.shields.io/badge/sqlite-WAL-003B57?logo=sqlite\u0026logoColor=white)](https://www.sqlite.org/wal.html)\n[![License MIT](https://img.shields.io/badge/license-MIT-c9a352)](#license)\n[![Release v0.22.0](https://img.shields.io/badge/release-v0.22.0-c9a352?logo=github)](https://github.com/aikazu/kelola-router/releases/tag/v0.22.0)\n[![Tests 1000+](https://img.shields.io/badge/tests-1000%2B-22c55e)](./tests)\n[![UI Obsidian Gold](https://img.shields.io/badge/ui-obsidian%20gold-c9a352)](#dashboard)\n\n`#c9a352` is the accent gold used across the dashboard, badges, and log highlights.\n\n---\n\n## Architecture at a glance\n\n```\n                          +---------------------+\n   OpenAI / Anthropic --\u003e |   Hono proxy        | \u003c-- /v1/chat/completions\n   /v1/messages client    |   (src/server.ts)   |     /v1/messages\n                          +----------+----------+\n                                     |\n        +--------------+-------------+----------------+---------------+\n        |              |             |                |               |\n        v              v             v                v               v\n  +-----------+  +-----------+ +-----------+   +-----------+   +-----------+\n  | provider  |  | account   | | transport |   | streaming |   | RTK +     |\n  | selection |  | state +   | | (proxy/   |   | SSE pipe  |   | Caveman   |\n  | (sticky,  |  | backoff + | | relay/    |   | + usage   |   | compress  |\n  | round-    |  | locks)    | | direct)   |   | extractor |   | filters   |\n  | robin)    |  |           | |           |   |           |   |           |\n  +-----------+  +-----------+ +-----------+   +-----------+   +-----------+\n        |              |             |                |               |\n        +------+-------+-------------+-----+----------+-------+\n               |                                 |\n               v                                 v\n        +---------------+                +----------------+\n        | format/       |                | upstream       |\n        | transform +   |                | HTTP via       |\n        | negotiate     |                | proxyAwareFetch|\n        | (OpenAI\u003c-\u003e    |                |                |\n        |  Anthropic)   |                +-------+--------+\n        +---------------+                        |\n                                                 v\n                            +----------------------------------+\n                            |   Upstream providers (6)         |\n                            |                                  |\n                            |   🟧 MiniMax        (mx/)         |\n                            |   🟣 Kiro (AWS Q)   (kr/)        |\n                            |   🟦 CodeBuddy      (cb/)        |\n                            |   🟢 Pioneer        (pio/)       |\n                            |   🌸 Notion         (nt/)        |\n                            |   ⚪  Z.AI          (zai/)       |\n                            +----------------------------------+\n```\n\nAll six upstreams live behind a single OpenAI-compatible or Anthropic-compatible endpoint. Account selection, transport, prompt caching, RTK + Caveman compression, and live request-flow events are handled inside the router.\n\n---\n\n## Features\n\n- **Six upstream providers**: MiniMax, Kiro (AWS CodeWhisperer / Amazon Q), CodeBuddy, Pioneer, Notion, and Z.AI behind a single URL.\n- **Provider-prefix routing**: `mm/MiniMax-M3`, `kr/claude-sonnet-4.5`, `cb/gpt-5`, `pio/...`, `nt/...`, `zai/...`. Aliases and combos translate user-facing model names to any of the six upstreams.\n- **Multi-account pool with state machine**: sticky + round-robin selection per provider, exponential backoff (1s → 4min cap), per-(account, model) cooldown locks, and error rules for 429 / 2056 / 2061 / 5xx cascades.\n- **OpenAI ↔ Anthropic wire-format negotiation**: clients speak either protocol; the router translates per upstream (Kiro EventStream binary, Notion CRDT NDJSON, Z.AI dual API).\n- **RTK compression pipeline**: `compressMessages` with `dedupLog` + `smartTruncate` filters, runtime registry, autodetect, and per-request RTK log surfaced in the console.\n- **Caveman terse system-prompt injection**: optional off / light / strong prompt mutation per request.\n- **Dual cache_control + auto-breakpoints**: Anthropic-style prompt caching, automatic breakpoint insertion when callers omit markers, respects caller-provided markers.\n- **Live request-flow console**: every step (selection, transport, upstream, error) emits a flow event you can tail in the dashboard or via `CONSOLE_FLOW=stdout`.\n- **Scheduler / quota puller**: periodic `POST /v1/token_plan/remains` for Kiro-style quota tracking, persisted in `quota_snapshots`.\n- **Auth + security**: scrypt password hashing + session cookies, CSRF, 5-fail-per-15min rate limit, optional SQLCipher encryption-at-rest, admin audit log, security banner.\n- **Built-in Preact dashboard**: Obsidian-Gold-themed SPA (Vite) with Overview, Usage, Client keys, Upstream accounts, Models, Aliases, Combos, Quota, Transports, Console, and Settings.\n- **Transport flexibility**: direct / SOCKS proxy / HTTP proxy / upstream relay, per-account assignment with geoip-aware defaulting, dispatcher cache.\n- **Docker-ready**: multi-stage Dockerfile, `docker-compose.yml`, bind-mount friendly, `recover-db.ts` for WAL race recovery.\n- **1000+ tests** across server (Vitest, 160 files), client (Vitest, 83 tests), and the audit-fixes suite (14 new tests).\n- **Audit-fixes v0.22.0**: quota uses `Promise.allSettled` for parallel per-account fetch with per-account error shape, settings GET returns `null` for missing keys (client merges UI defaults), admin cache 250ms TTL with explicit `bumpAdminCacheVersion` invalidation from `flushDb`, combo/alias name uniqueness enforced both directions (`checkComboConflict` exported), and `upsertAlias` UPDATE branch now sets `source`.\n\n---\n\n## Quick start\n\n### Prerequisites\n\n- Node.js \u003e= 20\n- A SQLite build (libsqlite3), bundled via `better-sqlite3`\n- Accounts for at least one upstream:\n  - **MiniMax**: MiniMax API key\n  - **Kiro (AWS CodeWhisperer / Amazon Q)**: Social auth or IdC, Kiro refresh token\n  - **CodeBuddy**: CodeBuddy bearer cookie\n  - **Pioneer**: Pioneer API key (Anthropic-compatible)\n  - **Notion**: Notion internal integration token\n  - **Z.AI**: Z.AI API key (OpenAI-compatible)\n\n### Bootstrap\n\n```bash\ngit clone https://github.com/aikazu/kelola-router.git\ncd kelola-router\nnpm install\ncp .env.example .env\nnpm run dev          # starts Hono server + Vite dashboard in parallel\n```\n\nOpen the dashboard at `http://localhost:5173`, set the admin password on first run, and add at least one account per provider you plan to use:\n\n- `Upstream -\u003e Accounts -\u003e + Add` (MiniMax / CodeBuddy / Pioneer / Z.AI)\n- `Upstream -\u003e Accounts -\u003e + Add (Kiro)` for Kiro social or device-flow\n- `Upstream -\u003e Accounts -\u003e + Add (Notion)` for Notion internal integration\n\n### Send a request\n\n```bash\ncurl http://localhost:8787/v1/chat/completions \\\n  -H \"Authorization: Bearer \u003cclient_key\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"mm/MiniMax-M3\",\n    \"messages\": [{\"role\": \"user\", \"content\": \"Hello!\"}]\n  }'\n```\n\nSwap `mm/` for `kr/`, `cb/`, `pio/`, `nt/`, or `zai/` to target a different upstream.\n\n---\n\n## Request pipeline (per request)\n\n1. **Auth**: `client_key` middleware validates the bearer token; `admin_key` is reserved for `/api/admin/*`.\n2. **CSRF**: same-origin guard for state-changing admin requests.\n3. **Rate limit**: 5 failed auth attempts per IP per 15 minutes.\n4. **Model resolution**: `modelPrefix.ts` parses `\u003cprovider\u003e/\u003cmodel\u003e`; aliases and combos are resolved through `providers/alias.ts` + `providers/aliasCache.ts`.\n5. **Account selection**: `accounts/selection.ts` chooses per `(provider, model)` with sticky or round-robin strategy, respecting `accounts/locks.ts` cooldowns.\n6. **Transport resolution**: `transport/resolve.ts` picks proxy / relay / direct via `dispatcherCache.ts`; geoip defaults to intl/cn per `providers/baseUrl.ts`.\n7. **Caveman + RTK**: optional terse system-prompt injection and message compression through `caveman/` and `rtk/`.\n8. **Format transform**: `format/transform.ts` + `format/negotiate.ts` convert OpenAI\u003c-\u003eAnthropic to whatever the upstream expects (Kiro uses EventStream binary framing, Notion uses an internal JSON shape, etc.).\n9. **Upstream call**: `providers/upstreamFetch.ts` POSTs JSON via `proxyAwareFetch`, with streaming SSE assembled per-provider (Kiro uses `providers/kiro/assembler.ts`, CodeBuddy uses `providers/codebuddy/streamConvert.ts`).\n10. **Streaming + usage**: `streaming/pipeWithUsage.ts` tees the SSE upstream response back to the client while `streaming/extractUsage.ts` parses token usage; `tailBuffer.ts` buffers partial lines.\n\nEvery step emits a `console/` flow event for the dashboard Console page and the `request_logs` table.\n\n---\n\n## Directory layout\n\n```\nsrc/\n├── server.ts                     # Hono app + listener bootstrap\n├── auth/                         # client_key + admin_key + CSRF + rate-limit\n│   ├── password.ts               # scrypt hashing + session cookie\n│   ├── rateLimit.ts              # 5 fails / 15min per IP\n│   └── csrf.ts                   # same-origin guard\n├── util/\n│   ├── env.ts                    # typed env getters\n│   └── log.ts                    # pino instance\n├── accounts/                     # state machine + selection\n│   ├── backoff.ts                # exponential cooldown (1s -\u003e 4min cap)\n│   ├── errorRules.ts             # 429/2056/2061/5xx cascade\n│   ├── state.ts                  # apply/reset/filter/lock-checks\n│   ├── selection.ts              # sticky + round-robin per provider\n│   ├── locks.ts                  # per-(account, model) cooldown CRUD\n│   └── types.ts\n├── db/\n│   ├── index.ts                  # openDb (WAL, FK, busy_timeout, optional SQLCipher)\n│   ├── hooks.ts                  # bumpAdminCacheVersion re-export\n│   ├── migrations/               # 001-initial only (schema.sql / indexes.sql / seed.sql)\n│   └── repos/                    # accounts, aliases, auditLog, client_keys,\n│                                 #   combos, models, quotaSnapshots,\n│                                 #   requestLogs, requestLogsQueue, settings,\n│                                 #   transports (+ settings.types.ts)\n├── providers/\n│   ├── minimax.ts                # PROVIDER const, upstreamUrl/Headers\n│   ├── baseUrl.ts                # intl vs cn\n│   ├── headers.ts                # OpenAI Bearer vs Anthropic x-api-key\n│   ├── alias.ts + aliasCache.ts\n│   ├── listModels.ts             # /v1/models fetch + merge\n│   ├── modelPrefix.ts            # mm/kr/cb/pio/nt/zai parser\n│   ├── pricing.ts                # per-token cost calc (incl cache)\n│   ├── parseError.ts             # base_resp.status_code extraction\n│   ├── quota.ts                  # token-plan quota parser\n│   ├── upstreamFetch.ts          # JSON POST wrapper over proxyAwareFetch\n│   ├── common/                   # SseAssemblerBase.ts template-method\n│   ├── format/                   # transform.ts + negotiate.ts + messageTypes.ts\n│   ├── kiro/                     # AWS CodeWhisperer / Amazon Q\n│   ├── codebuddy/                # index, transform, streamConvert\n│   ├── notion/                   # auth, constants, extract, transform, manifest.json\n│   ├── pioneer/                  # index, models, transform\n│   └── zai/                      # index, transform\n├── proxy/                        # per-provider request proxies\n│   ├── minimax.ts, kiro.ts, codebuddy.ts, pioneer.ts, notion.ts, zai.ts\n├── rtk/                          # RTK compression pipeline\n│   ├── index.ts, applyFilter.ts, autodetect.ts, registry.ts, constants.ts, types.ts\n│   └── filters/                  # dedupLog, smartTruncate\n├── caveman/                      # terse system-prompt injection\n├── cache-injection.ts            # dual cache_control + auto-breakpoints\n├── streaming/\n│   ├── extractUsage.ts           # parse SSE -\u003e usage\n│   ├── pipeWithUsage.ts          # tee upstream SSE\n│   └── tailBuffer.ts             # SSE partial-line buffering\n├── transport/                    # proxy / relay resolution\n│   ├── proxyFetch.ts, dispatcherCache.ts, socksLoader.ts, types.ts\n│   ├── geoip.ts                  # ipapi.co country probe\n│   ├── resolve.ts                # resolveTransportForAccount\n│   └── resolvedCache.ts\n├── scheduler/\n│   └── quotaPull.ts              # periodic /v1/token_plan/remains puller\n├── console/                      # flow event bus\n└── security/status.ts            # GET /api/admin/security/status\n\nclient/                           # Preact SPA dashboard (Vite)\n├── src/\n│   ├── App.tsx, main.tsx\n│   ├── __tests__/                # 11 test files + setup\n│   ├── components/               # Card, Stat, Badge, Button, Modal, Toast,\n│   │                             #   CommandPalette, Icon, Pagination, Progress,\n│   │                             #   SelectionControls, SecurityBanner,\n│   │                             #   TransportAssignment, ToastProvider\n│   ├── components/accounts/      # AddAccountModal, EditAccountModal, KiroUsageModal,\n│   │                             #   ProviderAccountSection, AccountsTable,\n│   │                             #   NotionAuthForm, KiroAutoImportForm,\n│   │                             #   KiroDeviceFlowForm\n│   ├── components/models/        # AddModelModal, EditModelModal, ProviderModelsSection\n│   ├── components/transports/    # AddTransportModal, BulkImportModal,\n│   │                             #   EditTransportModal, FailureModeCard,\n│   │                             #   TransportsTable\n│   ├── hooks/                    # useKiroAutoImport, useKiroDeviceFlow, useNotionAuth\n│   ├── layout/                   # AppShell, Sidebar, TopBar\n│   ├── lib/                      # api.ts (fetch wrapper), queryClient,\n│   │                             #   relativeTime, types, providerPrefix\n│   ├── pages/                    # 15 pages (see Dashboard below)\n│   └── styles/                   # base.css (tokens+fonts), components.css, animations.css\n\nscripts/                          # CLI scripts\n├── add-account.ts + add-account.cliArgs.ts\n├── add-client-key.ts\n├── notion-add-account.ts\n├── recover-db.ts                 # WAL race recovery\n├── reset.ts\n├── seed-models.ts                # base models\n├── seed-kiro-models.ts\n├── seed-codebuddy-models.ts\n├── seed-notion-models.ts\n└── seed-zai-models.ts\n\ntests/                            # integration + API + console + DB + provider + proxy + bench\ndocker-compose.yml, Dockerfile, Caddyfile, .env.example\ndocs/roadmap.md, docs/zai/, docs/notion/\n```\n\n---\n\n## Dashboard\n\nBuilt-in Preact SPA (Vite). Sidebar order is fixed; pages route via the hash router. Theme accent: `#c9a352` (Obsidian Gold).\n\n| Page              | Route                  | Purpose                                                                      |\n| ----------------- | ---------------------- | ---------------------------------------------------------------------------- |\n| Login             | `#/login`              | Admin password + session cookie bootstrap                                    |\n| Overview          | `#/`                   | Counts, health, recent request log                                           |\n| Usage             | `#/usage`              | Per-model token + cost rollups                                               |\n| Client keys       | `#/client-keys`        | Issue / rotate bearer tokens for downstream callers                          |\n| Upstream          | `#/accounts`           | Add / edit / disable accounts per provider (incl. Kiro auth flows)          |\n| Models            | `#/models`             | Catalog + custom model registry, `family` field (audit-fix A4)              |\n| Aliases           | `#/aliases`            | User-facing name -\u003e upstream model (e.g. `mm-fast` -\u003e `mm/MiniMax-M3`)      |\n| Combos            | `#/combos`             | Ordered fallback lists across providers (symmetric uniqueness, audit-fix B1) |\n| Quota             | `#/quota`              | Kiro / per-account quota snapshots + per-account error shape (audit-fix A5) |\n| Proxies           | `#/transports`         | Manage transports (direct / SOCKS / HTTP / relay), bulk import              |\n| Console           | `#/console`            | Live request-flow event stream                                               |\n| Settings          | `#/settings`           | caveman / caching / rtk / minimax / version + per-provider selection        |\n| RequestDetail     | `#/request/:id`        | Drill-in for a single request (prompt, upstream, usage, RTK log)            |\n| Placeholder       | `#/...`                | Catch-all for unimplemented sections                                         |\n| 404               | `#/404`                | Not-found page                                                               |\n\n---\n\n## Configuration\n\n`GET /api/admin/settings` returns the current settings object. Missing keys return `null`; the dashboard merges UI defaults over the response (audit-fix A7). Seeded defaults come from migration `001-initial`.\n\n```jsonc\n{\n  \"caveman\":  { \"level\": \"off\" },                                 // \"off\" | \"light\" | \"strong\"\n  \"caching\":  { \"autoBreakpoints\": true, \"respectCallerMarkers\": true },\n  \"rtk\":      {\n    \"enabled\": true,\n    \"minCompressSize\": 500,\n    \"rawCap\": 10485760,\n    \"filters\": [\"dedupLog\", \"smartTruncate\"]\n  },\n  \"minimax\":  { \"upstreamFormat\": \"auto\", \"m3DefaultMaxCompletionTokens\": 131072 },\n  \"version\":  \"\u003cauto\u003e\"\n}\n```\n\n### Per-provider selection\n\n`settings.selection.\u003cprovider\u003e` controls account-pick strategy per provider.\n\n| Provider   | Default mode | Notes                                                              |\n| ---------- | ------------ | ------------------------------------------------------------------ |\n| `minimax`  | sticky       | Round-robin opt-in via `{mode: \"round-robin\", step: N}`            |\n| `kiro`     | sticky       | Same shape; Kiro accounts carry their own quota snapshot           |\n| `codebuddy`| sticky       | Round-robin available                                              |\n| `pioneer`  | sticky       | Round-robin available                                              |\n| `notion`   | sticky       | Notion internal integration only; no rotation across workspaces    |\n| `zai`      | sticky       | Round-robin available                                              |\n\n### Environment variables (selected)\n\n| Var                              | Purpose                                                              |\n| -------------------------------- | -------------------------------------------------------------------- |\n| `HOST` / `PORT`                  | Bind address (default `0.0.0.0:8787`)                                |\n| `REGION`                         | `intl` vs `cn` for `providers/baseUrl.ts`                            |\n| `DB_PATH`                        | SQLite file path (default `./data/router.db`)                        |\n| `ROUTER_DB_KEY`                  | SQLCipher encryption key (optional)                                  |\n| `LOG_LEVEL`                      | pino level (`debug` / `info` / `warn` / `error`)                     |\n| `ROUTER_UPSTREAM_FORMAT`         | Force `openai` or `anthropic` wire format                            |\n| `CONSOLE_FLOW`                   | `stdout` to mirror flow events to terminal                           |\n\n---\n\n## Development\n\n```bash\nnpm run dev                # server + client in parallel\nnpm run dev:server         # Hono on :8787 only\nnpm run dev:client         # Vite on :5173 only\nnpm run build              # tsc + Vite build\nnpm run start              # node dist/server.js\nnpm run lint               # biome check .\nnpm run lint:fix\nnpm run typecheck\nnpm run test               # server Vitest\nnpm run test:client        # client Vitest\nnpm run test:watch\n```\n\n### CLI scripts\n\nEach script has a `:docker` variant that runs the same command inside the compose container.\n\n| Command                       | Purpose                                              |\n| ----------------------------- | ---------------------------------------------------- |\n| `npm run add-account`         | Interactive account add (provider picked at prompt)  |\n| `npm run add-client-key`      | Mint a new client_key                                |\n| `npm run seed-models`         | Seed the base model catalog                          |\n| `npm run seed-kiro-models`    | Seed Kiro-specific models                            |\n| `npm run seed-codebuddy-models` | Seed CodeBuddy models                             |\n| `npm run seed-zai-models`     | Seed Z.AI models                                     |\n| `npm run seed-notion-models`  | Seed Notion models                                   |\n| `npm run seed-all`            | Run every `seed-*` script                            |\n| `npm run reset`               | Wipe DB and reseed (destructive)                     |\n| `npm run recover-db`          | Recover from a WAL race or interrupted checkpoint    |\n| `npx tsx scripts/notion-add-account.ts` | Notion-specific add (device code path)    |\n\n### Commit conventions\n\nConventional Commits, one logical unit per commit. Examples:\n\n- `feat(minimax): add round-robin selection step`\n- `fix(aliases): enforce combo-name uniqueness on insert`\n- `chore(release): v0.22.0`\n- `docs: sync with v0.21.0 (zai provider)`\n\nWIP commits are prefixed `wip:`. Never force-push or rewrite published history.\n\n---\n\n## Docker\n\n```bash\ndocker compose up -d --build\ndocker compose logs -f router\n```\n\n- The container expects `/data` to be bind-mounted. The SQLite file lives at `/data/router.db`.\n- **WAL race warning**: if the container was killed mid-write you may see `database disk image is malformed` on next start. Run `npm run recover-db` (or `docker compose run --rm router npm run recover-db`) to checkpoint and reattach.\n- The Caddyfile is provided as a reference for TLS termination; it is not started automatically by `docker-compose.yml`.\n- `.env.example` lists every env var the server reads at boot.\n\n---\n\n## VPS deploy\n\n1. Provision a small VPS (1 vCPU / 1 GB is enough for personal use).\n2. Clone the repo and `cp .env.example .env`. Fill in `HOST`, `PORT`, `DB_PATH`, `LOG_LEVEL`, and `ROUTER_DB_KEY` (if encrypting at rest).\n3. `npm ci \u0026\u0026 npm run build`.\n4. On first boot, open the dashboard at `http://\u003cvps\u003e:8787` and **enter the dashboard password you set** (password mode + session cookie; the legacy open-mode `admin_key` flow is gone). The password is hashed with scrypt and stored in `settings`.\n5. Put Caddy (or your reverse proxy of choice) in front of port `8787` for TLS, then point your local OpenAI/Anthropic client at `https://router.example.com`.\n\nIf you ever forget the password, the only recovery is to wipe `settings` from the DB and re-bootstrap; there is no backdoor.\n\n---\n\n## Transport\n\n`/api/admin/transports` lets you assign one transport per upstream account. Four modes:\n\n| Mode     | Settings key                          | Env override  | Notes                                                |\n| -------- | ------------------------------------- | ------------- | ---------------------------------------------------- |\n| Direct   | `transport.proxy = null`              | (n/a)         | Default; uses local NIC                              |\n| SOCKS    | `transport.proxy = \"socks5h://...\"`   | `ALL_PROXY`   | Lazy-loaded via `transport/socksLoader.ts`           |\n| HTTP     | `transport.proxy = \"http://...\"`      | `HTTP_PROXY`  | Standard HTTP CONNECT                                |\n| Relay    | `transport.relay = \"https://relay.example.com\"` | (n/a) | Hand-off to a remote proxy that fans out to upstreams |\n\n`resolveTransportForAccount` caches the resolved dispatcher per account in `transport/resolvedCache.ts`. Geoip defaults to `intl` vs `cn` via `transport/geoip.ts` (ipapi.co country probe) and `providers/baseUrl.ts`.\n\n---\n\n## Security\n\n- **Open mode (default, dev-only)**: no admin auth. Set a dashboard password before exposing the service.\n- **Password mode**: scrypt-hashed admin password, session cookie with CSRF guard. This is the production default after first boot.\n- **Encryption-at-rest**: set `ROUTER_DB_KEY` and the SQLite file is opened via SQLCipher. Losing the key loses the DB.\n- **Rate limit**: 5 failed auth attempts per IP per 15 minutes (`auth/rateLimit.ts`).\n- **Audit log**: every admin mutation is appended to `audit_log` (repos in `db/repos/auditLog.ts`); readable on the Settings page.\n- **Security banner**: top-of-dashboard warning when running in open mode or with no DB encryption (`components/SecurityBanner`).\n- **Security status endpoint**: `GET /api/admin/security/status` (`security/status.ts`) reports mode, key presence, and last audit entries.\n\n---\n\n## Roadmap\n\n| Version  | Theme                                                                                          | Status   |\n| -------- | ---------------------------------------------------------------------------------------------- | -------- |\n| v0.22.0  | Audit-fixes batch: quota parallel + per-account errors, settings null + client merge, admin cache TTL + explicit invalidation, combo/alias symmetry, upsertAlias source tracking | Shipped  |\n| v0.21.0  | Z.AI provider + provider-prefix model routing (`mm/`, `kr/`, `cb/`, `pio/`, `nt/`, `zai/`)    | Shipped  |\n| v0.20.0  | Pioneer provider, combo cross-provider fallback, Kiro quota snapshots                           | Shipped  |\n| v0.19.0  | Notion provider, request-flow console, RTK log surfacing                                       | Shipped  |\n| v0.18.x  | Kiro (AWS CodeWhisperer) provider, device-flow import                                          | Shipped  |\n| v0.17.x  | CodeBuddy provider, OpenAI\u003c-\u003eAnthropic negotiation                                            | Shipped  |\n| v0.16.x  | RTK pipeline + Caveman terse-prompt injection                                                  | Shipped  |\n| v0.15.x  | Built-in Preact dashboard (Obsidian Gold)                                                      | Shipped  |\n| v0.14.x  | SQLite + WAL + SQLCipher optional encryption                                                   | Shipped  |\n| v0.13.x  | Multi-account pool, exponential backoff, per-model locks                                      | Shipped  |\n| v0.12.x  | Transport layer (proxy / relay / direct)                                                       | Shipped  |\n| v0.11.x  | Initial MiniMax proxy + client_key auth                                                        | Shipped  |\n\nSee `docs/roadmap.md` for upcoming ideas and `CHANGELOG.md` for the full per-version list.\n\n---\n\n## License\n\nMIT. See `LICENSE` for the full text.\n\n---\n\n\u003csub\u003eBuilt with Hono, Preact, better-sqlite3, pino, and an unreasonable amount of provider-specific wire-format glue. Gold accent `#c9a352`. Stay sticky, stay cached, stay on a healthy cooldown.\u003c/sub\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faikazu%2Fkelola-router","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faikazu%2Fkelola-router","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faikazu%2Fkelola-router/lists"}