diff --git a/.env.example b/.env.example
index 16a71dc9f6..c7e4911e4a 100644
--- a/.env.example
+++ b/.env.example
@@ -37,6 +37,8 @@ INITIAL_PASSWORD=CHANGEME
# Base directory for all persistent data (SQLite DB, logs, backups).
# Used by: src/lib/db/core.ts — resolves the SQLite database file path.
# Default: ~/.omniroute/ | Override for Docker or custom installations.
+# Hint: When running in Docker, consider mounting a host directory here for data persistence across container restarts
+# also if you want to share the same database as "npm run dev" use "./data"
# DATA_DIR=/var/lib/omniroute
# Encryption key for SQLite database encryption at rest.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3b92bb1d4..3fa907b355 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,15 +15,25 @@
- **docker:** rebuild `better-sqlite3` native bindings after hardened install to resolve container startup crash (#2772 — thanks @thanet-s)
- **combos:** make combo target timeout configurable, inheriting resolved request timeout by default and clamping values so they only shorten fallback latency (#2775 — thanks @rdself)
+- **oauth:** use public callbacks for remote Google OAuth with custom creds (#2787 — thanks @akarray)
+- **combos:** allow rate-limited provider connections after transient 429s (#2786 — thanks @JxnLexn)
+- **logs:** keep database log settings in sync with the pipeline toggle (#2785 — thanks @JxnLexn)
+- **docker:** speedup docker creation by reducing steps and bunch up copy operations (#2784 — thanks @hartmark)
+- **codex:** apply global service tiers to combo request bodies (#2783 — thanks @JxnLexn)
### ⚡ Performance / CI
- **ci:** build Docker platforms on native runners (linux/amd64 on ubuntu-24.04 and linux/arm64 on ubuntu-24.04-arm) instead of emulated QEMU, reducing build times significantly (#2774 — thanks @thanet-s)
+### 📝 Documentation
+
+- **docs:** fix broken documentation links in README after Fumadocs migration (#2782 — thanks @kjhq)
+
### 🏆 Hall of Contributors
A special thanks to everyone who contributed code, reviews, and tests for this release:
-@hartmark, @hijak, @rdself, @thanet-s
+@akarray, @hartmark, @hijak, @JxnLexn, @kjhq, @rdself, @thanet-s
+
---
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f125ea29da..d16220c52c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -232,18 +232,25 @@ tests/
├── translator/ # Translator-specific tests
└── load/ # Load tests
-docs/ # Documentation
-├── ARCHITECTURE.md # System architecture
-├── API_REFERENCE.md # All endpoints
-├── USER_GUIDE.md # Provider setup, CLI integration
-├── TROUBLESHOOTING.md # Common issues
-├── MCP-SERVER.md # MCP server (25 tools)
-├── A2A-SERVER.md # A2A agent protocol
-├── AUTO-COMBO.md # Auto-combo engine
-├── CLI-TOOLS.md # CLI tools integration
-├── COVERAGE_PLAN.md # Test coverage improvement plan
-├── openapi.yaml # OpenAPI specification
-└── adr/ # Architecture Decision Records
+docs/
+├── adr/ # Architecture Decision Records
+├── architecture/ # System architecture & resilience
+├── comparison/ # OmniRoute vs alternatives
+├── compression/ # Compression guides & rules
+├── dev/ # Development guides
+├── diagrams/ # Architecture diagrams
+├── frameworks/ # MCP, A2A, OpenCode, Memory, Skills
+├── guides/ # User guide, Docker, setup, troubleshooting
+├── i18n/ # Internationalized README translations
+├── marketing/ # Marketing materials
+├── ops/ # Deployment, proxy, coverage, releases
+├── providers/ # Provider-specific docs
+├── reference/ # API reference, env vars, CLI tools, free tiers
+├── releases/ # Release notes
+├── routing/ # Auto-combo engine, reasoning replay
+├── screenshots/ # Dashboard screenshots
+├── security/ # Guardrails, compliance, stealth, tokens
+└── specs/ # Design specs
```
---
diff --git a/Dockerfile b/Dockerfile
index 853d9dc4e9..f23cd7b074 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,22 @@
-FROM node:24-trixie-slim AS builder
+# ── Common base with runtime deps ──────────────────────────────────────────
+FROM node:24-trixie-slim AS base
WORKDIR /app
-RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
- --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
+RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
+ --mount=type=cache,target=/var/lib/apt/lists,sharing=shared \
apt-get update \
- && apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates python3 make g++ \
+ && apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+# ── Builder ────────────────────────────────────────────────────────────────
+FROM base AS builder
+
+# Build tools for native module compilation
+# apt-get update needed here because base's rm -rf clears the shared cache
+RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
+ --mount=type=cache,target=/var/lib/apt/lists,sharing=shared \
+ apt-get update \
+ && apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
COPY package*.json ./
@@ -26,12 +38,15 @@ RUN --mount=type=cache,target=/root/.npm \
&& npm rebuild better-sqlite3 \
&& node -e "require('better-sqlite3')(':memory:').close()"
+# Use Turbopack for significant build speedup
+ENV OMNIROUTE_USE_TURBOPACK=1
+
COPY . ./
RUN --mount=type=cache,target=/app/.next/cache \
- mkdir -p /app/data && npm run build -- --webpack
+ mkdir -p /app/data && npm run build
-FROM node:24-trixie-slim AS runner-base
-WORKDIR /app
+# ── Runner base ────────────────────────────────────────────────────────────
+FROM base AS runner-base
LABEL org.opencontainers.image.title="omniroute" \
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint" \
@@ -46,15 +61,11 @@ ENV NODE_OPTIONS="--max-old-space-size=256"
# Data directory inside Docker — must match the volume mount in docker-compose.yml
ENV DATA_DIR=/app/data
-RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
- --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
- apt-get update \
- && apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
- && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/data
-COPY --from=builder /app/public ./public
-COPY --from=builder /app/.next/static ./.next/static
+# The standalone build + syncStandaloneExtraModules bundles all runtime files
+# (.next, node_modules, migrations, scripts, docs, etc.) into .next/standalone/.
+# Explicit overrides below cover modules that NFT tracing may miss.
COPY --from=builder /app/.next/standalone ./
# Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime
COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers
@@ -70,18 +81,6 @@ COPY --from=builder /app/node_modules/split2 ./node_modules/split2
# traced by Next.js standalone output — copy them explicitly.
COPY --from=builder /app/src/lib/db/migrations ./migrations
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
-# MITM server.cjs is spawned at runtime via child_process — not traced by nft
-COPY --from=builder /app/src/mitm/server.cjs ./src/mitm/server.cjs
-# Runtime docs are pruned by .dockerignore to English markdown + OpenAPI.
-# Next.js standalone tracing does not include docs read via fs.
-COPY --from=builder /app/.next/standalone/docs ./docs
-
-COPY --from=builder /app/scripts/dev/run-standalone.mjs ./dev/run-standalone.mjs
-COPY --from=builder /app/scripts/build/runtime-env.mjs ./build/runtime-env.mjs
-COPY --from=builder /app/scripts/build/bootstrap-env.mjs ./build/bootstrap-env.mjs
-COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
-
-RUN node -e "require('better-sqlite3')(':memory:').close()"
# Hand /app over to the baked-in `node` non-root user (UID/GID 1000) so the
# runtime process never holds root privileges. The chown happens after all
diff --git a/README.md b/README.md
index 084ca20ea1..321bc6e807 100644
--- a/README.md
+++ b/README.md
@@ -275,7 +275,7 @@ Result: 4 layers of fallback = zero downtime
+ also works with · Cline · Antigravity · Windsurf · AMP · Hermes · Qwen CLI · Roo · Continue · any OpenAI-compatible tool
-📖 Per-tool setup for all 16+ tools → [`docs/CLI-TOOLS.md`](docs/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)
+📖 Per-tool setup for all 16+ tools → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)
@@ -332,7 +332,7 @@ Result: 4 layers of fallback = zero downtime
| 🧩 **OpenCode plugin** | `@omniroute/opencode-provider` | Native OpenCode integration |
| 🛠️ **From source** | `npm install && npm run dev` | Hack on it, contribute |
-📖 [Docker Guide](docs/DOCKER_GUIDE.md) · [Desktop](electron/README.md) · [Termux](docs/TERMUX_GUIDE.md) · [PWA](docs/PWA_GUIDE.md) · [OpenCode](docs/frameworks/OPENCODE.md)
+📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) · [Desktop](electron/README.md) · [Termux](docs/guides/TERMUX_GUIDE.md) · [PWA](docs/guides/PWA_GUIDE.md) · [OpenCode](docs/frameworks/OPENCODE.md)
@@ -440,7 +440,7 @@ range = 78.4 – 94.6%
Code blocks, URLs, JSON and structured data are **always protected** by the preservation engine. Auto-trigger compression by token threshold, or assign a compression pipeline per routing combo.
-📖 [`COMPRESSION_GUIDE.md`](docs/COMPRESSION_GUIDE.md) · [`RTK_COMPRESSION.md`](docs/RTK_COMPRESSION.md) · [`COMPRESSION_ENGINES.md`](docs/COMPRESSION_ENGINES.md)
+📖 [`COMPRESSION_GUIDE.md`](docs/compression/COMPRESSION_GUIDE.md) · [`RTK_COMPRESSION.md`](docs/compression/RTK_COMPRESSION.md) · [`COMPRESSION_ENGINES.md`](docs/compression/COMPRESSION_ENGINES.md)
@@ -509,7 +509,7 @@ pnpm install -g omniroute && pnpm approve-builds -g && omniroute
yay -S omniroute-bin && systemctl --user enable --now omniroute.service
```
-📖 [Docker Guide](docs/DOCKER_GUIDE.md) — Compose profiles, Caddy HTTPS, Cloudflare tunnels.
+📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) — Compose profiles, Caddy HTTPS, Cloudflare tunnels.
@@ -580,7 +580,7 @@ yay -S omniroute-bin && systemctl --user enable --now omniroute.service
> 💡 The dashboard "cost" is a **savings tracker**, not a bill — OmniRoute never charges you. A "$290 total cost" using free models means **$290 saved**.
-📖 Complete free directory → [`docs/FREE_TIERS.md`](docs/FREE_TIERS.md) — 25+ providers, quotas, base URLs.
+📖 Complete free directory → [`docs/reference/FREE_TIERS.md`](docs/reference/FREE_TIERS.md) — 25+ providers, quotas, base URLs.
@@ -616,7 +616,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
- **🆓 1proxy marketplace** — hundreds of free validated proxies, quality scores, auto-rotation
- **Anti-detection** — TLS fingerprint spoofing (`wreq-js`), CLI fingerprint matching, proxy IP preservation
-📖 [`docs/PROXY_GUIDE.md`](docs/PROXY_GUIDE.md)
+📖 [`docs/ops/PROXY_GUIDE.md`](docs/ops/PROXY_GUIDE.md)
@@ -631,7 +631,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
**Quality & Ops:** built-in **Evals** (golden-set: exact/contains/regex/custom) · guardrails (PII, injection, vision) · health dashboard · p50/p95/p99 telemetry · webhooks · compliance audit.
**AI Agent Skills:** drop-in markdown manifests — point any agent at `skills/omniroute/SKILL.md`. 10 skills available.
-📖 [MCP Server](open-sse/mcp-server/README.md) · [A2A Server](src/lib/a2a/README.md) · [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) · [Features Gallery](docs/FEATURES.md)
+📖 [MCP Server](open-sse/mcp-server/README.md) · [A2A Server](src/lib/a2a/README.md) · [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) · [Features Gallery](docs/guides/FEATURES.md)
@@ -651,7 +651,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 177 providers.
-📖 [User Guide](docs/USER_GUIDE.md) · [API Reference](docs/API_REFERENCE.md) · [Environment Config](docs/ENVIRONMENT.md)
+📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
@@ -669,7 +669,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
| Docker SQLite locks | Use `--stop-timeout 40` for clean WAL checkpoint |
| Node runtime errors | Use Node `>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24 <25` |
-🐛 **Reporting a bug?** Run `npm run system-info` and attach `system-info.txt`. 📖 [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md)
+🐛 **Reporting a bug?** Run `npm run system-info` and attach `system-info.txt`. 📖 [`docs/guides/TROUBLESHOOTING.md`](docs/guides/TROUBLESHOOTING.md)
@@ -740,50 +740,50 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
| Document | Description |
| ------------------------------------- | ----------------------------------------------------------------------------- |
-| [User Guide](docs/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
-| [Setup Guide](docs/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
-| [CLI Tools Guide](docs/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
+| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
+| [Setup Guide](docs/guides/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
+| [CLI Tools Guide](docs/reference/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
| [Quick Start](README.md#-quick-start) | 3-step install → connect → configure |
### 🔧 Operations & Deployment
| Document | Description |
| ---------------------------------------------------- | -------------------------------------------------------------- |
-| [Docker Guide](docs/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
-| [VM Deployment](docs/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
-| [Fly.io Deployment](docs/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage |
-| [Termux Guide](docs/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |
-| [PWA Guide](docs/PWA_GUIDE.md) | Progressive Web App install, caching, architecture |
-| [Uninstall Guide](docs/UNINSTALL.md) | Clean removal for all install methods |
-| [Environment Config](docs/ENVIRONMENT.md) | Complete `.env` variables and references |
+| [Docker Guide](docs/guides/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
+| [VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
+| [Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage |
+| [Termux Guide](docs/guides/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |
+| [PWA Guide](docs/guides/PWA_GUIDE.md) | Progressive Web App install, caching, architecture |
+| [Uninstall Guide](docs/guides/UNINSTALL.md) | Clean removal for all install methods |
+| [Environment Config](docs/reference/ENVIRONMENT.md) | Complete `.env` variables and references |
### 🧠 Features & Architecture
| Document | Description |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------- |
-| [Architecture](docs/ARCHITECTURE.md) | System architecture, data flow, and internals |
-| [Compression Guide](docs/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked |
-| [RTK Compression](docs/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery |
-| [Compression Engines](docs/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces |
-| [Compression Rules Format](docs/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
-| [Compression Language Packs](docs/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
-| [Resilience Guide](docs/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
-| [Auto-Combo Engine](docs/AUTO-COMBO.md) | 6-factor scoring, mode packs, self-healing |
-| [Proxy Guide](docs/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
-| [Free Tiers](docs/FREE_TIERS.md) | 25+ free API providers consolidated directory |
-| [Features Gallery](docs/FEATURES.md) | Visual dashboard tour with screenshots |
-| [Codebase Documentation](docs/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
+| [Architecture](docs/architecture/ARCHITECTURE.md) | System architecture, data flow, and internals |
+| [Compression Guide](docs/compression/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked |
+| [RTK Compression](docs/compression/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery |
+| [Compression Engines](docs/compression/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces |
+| [Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
+| [Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
+| [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
+| [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) | 6-factor scoring, mode packs, self-healing |
+| [Proxy Guide](docs/ops/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
+| [Free Tiers](docs/reference/FREE_TIERS.md) | 25+ free API providers consolidated directory |
+| [Features Gallery](docs/guides/FEATURES.md) | Visual dashboard tour with screenshots |
+| [Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
### 🤖 Protocols & APIs
| Document | Description |
| ------------------------------------------- | --------------------------------------------------- |
-| [API Reference](docs/API_REFERENCE.md) | All endpoints with examples |
-| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification |
+| [API Reference](docs/reference/API_REFERENCE.md) | All endpoints with examples |
+| [OpenAPI Spec](docs/reference/openapi.yaml) | OpenAPI 3.0 specification |
| [MCP Server](open-sse/mcp-server/README.md) | 29 MCP tools, IDE configs, Python/TS/Go clients |
-| [MCP Server Guide](docs/MCP-SERVER.md) | MCP installation, transports, and tool reference |
+| [MCP Server Guide](docs/frameworks/MCP-SERVER.md) | MCP installation, transports, and tool reference |
| [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt |
-| [A2A Server Guide](docs/A2A-SERVER.md) | A2A agent card, tasks, skills, and streaming |
+| [A2A Server Guide](docs/frameworks/A2A-SERVER.md) | A2A agent card, tasks, skills, and streaming |
### 📋 Project & Quality
@@ -791,9 +791,9 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
| ---------------------------------------------- | ----------------------------------------------- |
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
-| [i18n Guide](docs/I18N.md) | 40+ language support, translation workflow, RTL |
-| [Release Checklist](docs/RELEASE_CHECKLIST.md) | Pre-release validation steps |
-| [Coverage Plan](docs/COVERAGE_PLAN.md) | Test coverage strategy and 4,690+ test suite |
+| [i18n Guide](docs/guides/I18N.md) | 40+ language support, translation workflow, RTL |
+| [Release Checklist](docs/ops/RELEASE_CHECKLIST.md) | Pre-release validation steps |
+| [Coverage Plan](docs/ops/COVERAGE_PLAN.md) | Test coverage strategy and 4,690+ test suite |
diff --git a/docs/AUTO-COMBO.md b/docs/AUTO-COMBO.md
deleted file mode 100644
index c042204b81..0000000000
--- a/docs/AUTO-COMBO.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# OmniRoute Auto-Combo Engine
-
-> Self-managing model chains with adaptive scoring + zero-config auto-routing
-
-## Zero-Config Auto-Routing (`auto/` prefix)
-
-> **NEW:** No combo creation required. Use `auto/` prefix directly in any client.
-
-### Quick Examples
-
-| Model ID | Variant | Behavior |
-| -------------- | ------- | ------------------------------------------------------------------------ |
-| `auto` | default | All connected providers, LKGP strategy, balanced weights |
-| `auto/coding` | coding | Quality-first weights, suitable for code generation |
-| `auto/fast` | fast | Low-latency weighted selection |
-| `auto/cheap` | cheap | Cost-optimized routing (lowest cost first) |
-| `auto/offline` | offline | Favors providers with highest quota availability |
-| `auto/smart` | smart | Quality-first + higher exploration rate (10%) for better model discovery |
-| `auto/lkgp` | lkgp | Explicit LKGP (same as default `auto`) |
-
-**How to use:**
-
-```bash
-# Any IDE or CLI tool that supports OpenAI format
-Base URL: http://localhost:20128/v1
-API Key:
-
-# In your code/config, set model to:
-model: "auto" # balanced default
-model: "auto/coding" # best for coding tasks
-model: "auto/fast" # fastest available
-model: "auto/cheap" # cheapest per token
-```
-
-**What happens:**
-
-1. OmniRoute detects `auto/` prefix in `src/sse/handlers/chat.ts`
-2. Queries all **active provider connections** from the database
-3. Filters to those with valid credentials (API key or OAuth token)
-4. Determines the model per connection (`connection.defaultModel` or provider's first model)
-5. Builds a **virtual combo** in-memory (not stored in DB)
-6. Routes using the selected variant's weight profile + LKGP strategy
-
-**Key properties:**
-
-- ✅ **Always-on:** No toggle, no combo creation, no configuration needed
-- ✅ **Dynamic:** Reflects current connected providers automatically
-- ✅ **Session stickiness:** LKGP ensures last successful provider is prioritized
-- ✅ **Multi-account aware:** Each provider connection becomes a separate candidate
-- ✅ **No DB writes:** Virtual combo exists only for the request, zero persistence overhead
-
-**Behind the scenes:**
-
-```txt
-Request: { model: "auto/coding" }
- ↓
-src/sse/handlers/chat.ts detects prefix
- ↓
-createVirtualAutoCombo('coding') → candidatePool from active connections
- ↓
-handleComboChat (same engine as persisted combos)
- ↓
-Auto-scoring selects best provider/model per request
-```
-
-**Implementation files:**
-
-| File | Purpose |
-| --------------------------------------------------------- | ----------------------------------------- |
-| `open-sse/services/autoCombo/autoPrefix.ts` | Prefix parser (`parseAutoPrefix`) |
-| `open-sse/services/autoCombo/virtualFactory.ts` | Creates virtual `AutoComboConfig` objects |
-| `open-sse/services/autoCombo/providerRegistryAccessor.ts` | Test hook for mocking provider registry |
-| `src/sse/handlers/chat.ts` | Integration: auto prefix short-circuit |
-| `src/shared/constants/providers.ts` | `SYSTEM_PROVIDERS.auto` system entry |
-
-## How It Works (Persisted Auto-Combos)
-
-The Auto-Combo Engine dynamically selects the best provider/model for each request using a **6-factor scoring function**:
-
-| Factor | Weight | Description |
-| :--------- | :----- | :---------------------------------------------- |
-| Quota | 0.20 | Remaining capacity [0..1] |
-| Health | 0.25 | Circuit breaker: CLOSED=1.0, HALF=0.5, OPEN=0.0 |
-| CostInv | 0.20 | Inverse cost (cheaper = higher score) |
-| LatencyInv | 0.15 | Inverse p95 latency (faster = higher) |
-| TaskFit | 0.10 | Model × task type fitness score |
-| Stability | 0.10 | Low variance in latency/errors |
-
-## Mode Packs
-
-| Pack | Focus | Key Weight |
-| :---------------------- | :----------- | :--------------- |
-| 🚀 **Ship Fast** | Speed | latencyInv: 0.35 |
-| 💰 **Cost Saver** | Economy | costInv: 0.40 |
-| 🎯 **Quality First** | Best model | taskFit: 0.40 |
-| 📡 **Offline Friendly** | Availability | quota: 0.40 |
-
-## Self-Healing
-
-- **Temporary exclusion**: Score < 0.2 → excluded for 5 min (progressive backoff, max 30 min)
-- **Circuit breaker awareness**: OPEN → auto-excluded; HALF_OPEN → probe requests
-- **Incident mode**: >50% OPEN → disable exploration, maximize stability
-- **Cooldown recovery**: After exclusion, first request is a "probe" with reduced timeout
-
-## Bandit Exploration
-
-5% of requests (configurable) are routed to random providers for exploration. Disabled in incident mode.
-
-## API
-
-```bash
-# Create auto-combo
-curl -X POST http://localhost:20128/api/combos/auto \
- -H "Content-Type: application/json" \
- -d '{"id":"my-auto","name":"Auto Coder","candidatePool":["anthropic","google","openai"],"modePack":"ship-fast"}'
-
-# List auto-combos
-curl http://localhost:20128/api/combos/auto
-```
-
-## Task Fitness
-
-30+ models scored across 6 task types (`coding`, `review`, `planning`, `analysis`, `debugging`, `documentation`). Supports wildcard patterns (e.g., `*-coder` → high coding score).
-
-## Files
-
-| File | Purpose |
-| :------------------------------------------- | :------------------------------------ |
-| `open-sse/services/autoCombo/scoring.ts` | Scoring function & pool normalization |
-| `open-sse/services/autoCombo/taskFitness.ts` | Model × task fitness lookup |
-| `open-sse/services/autoCombo/engine.ts` | Selection logic, bandit, budget cap |
-| `open-sse/services/autoCombo/selfHealing.ts` | Exclusion, probes, incident mode |
-| `open-sse/services/autoCombo/modePacks.ts` | 4 weight profiles |
-| `src/app/api/combos/auto/route.ts` | REST API |
diff --git a/scripts/build/build-next-isolated.mjs b/scripts/build/build-next-isolated.mjs
index 97e6792d7e..c251de747e 100644
--- a/scripts/build/build-next-isolated.mjs
+++ b/scripts/build/build-next-isolated.mjs
@@ -147,6 +147,18 @@ export async function syncStandaloneNativeAssets(
sourcePath: path.join(rootDir, "node_modules", "wreq-js", "rust"),
destinationPath: path.join(rootDir, ".next", "standalone", "node_modules", "wreq-js", "rust"),
},
+ {
+ label: "better-sqlite3 native binary",
+ sourcePath: path.join(rootDir, "node_modules", "better-sqlite3", "build"),
+ destinationPath: path.join(
+ rootDir,
+ ".next",
+ "standalone",
+ "node_modules",
+ "better-sqlite3",
+ "build"
+ ),
+ },
];
let changed = false;
@@ -171,6 +183,90 @@ export async function syncStandaloneNativeAssets(
return changed;
}
+export async function syncStandaloneExtraModules(
+ rootDir = projectRoot,
+ fsImpl = fs,
+ log = console
+) {
+ const entries = [
+ {
+ label: "@swc/helpers",
+ sourcePath: path.join(rootDir, "node_modules", "@swc", "helpers"),
+ destRelative: path.join("node_modules", "@swc", "helpers"),
+ },
+ {
+ label: "pino-abstract-transport",
+ sourcePath: path.join(rootDir, "node_modules", "pino-abstract-transport"),
+ destRelative: path.join("node_modules", "pino-abstract-transport"),
+ },
+ {
+ label: "pino-pretty",
+ sourcePath: path.join(rootDir, "node_modules", "pino-pretty"),
+ destRelative: path.join("node_modules", "pino-pretty"),
+ },
+ {
+ label: "split2",
+ sourcePath: path.join(rootDir, "node_modules", "split2"),
+ destRelative: path.join("node_modules", "split2"),
+ },
+ {
+ label: "migrations",
+ sourcePath: path.join(rootDir, "src", "lib", "db", "migrations"),
+ destRelative: "migrations",
+ },
+ {
+ label: "MITM server",
+ sourcePath: path.join(rootDir, "src", "mitm", "server.cjs"),
+ destRelative: path.join("src", "mitm", "server.cjs"),
+ },
+ {
+ label: "run-standalone script",
+ sourcePath: path.join(rootDir, "scripts", "dev", "run-standalone.mjs"),
+ destRelative: path.join("dev", "run-standalone.mjs"),
+ },
+ {
+ label: "runtime-env script",
+ sourcePath: path.join(rootDir, "scripts", "build", "runtime-env.mjs"),
+ destRelative: path.join("build", "runtime-env.mjs"),
+ },
+ {
+ label: "bootstrap-env script",
+ sourcePath: path.join(rootDir, "scripts", "build", "bootstrap-env.mjs"),
+ destRelative: path.join("build", "bootstrap-env.mjs"),
+ },
+ {
+ label: "healthcheck script",
+ sourcePath: path.join(rootDir, "scripts", "dev", "healthcheck.mjs"),
+ destRelative: "healthcheck.mjs",
+ },
+ {
+ label: "public directory",
+ sourcePath: path.join(rootDir, "public"),
+ destRelative: "public",
+ },
+ {
+ label: "playwright-core (dynamic import by gemini-web executor)",
+ sourcePath: path.join(rootDir, "node_modules", "playwright-core"),
+ destRelative: path.join("node_modules", "playwright-core"),
+ },
+ ];
+
+ let changed = false;
+ const standaloneRoot = path.join(rootDir, ".next", "standalone");
+
+ for (const entry of entries) {
+ if (!(await exists(entry.sourcePath))) continue;
+
+ const destPath = path.join(standaloneRoot, entry.destRelative);
+ await fsImpl.mkdir(path.dirname(destPath), { recursive: true });
+ await fsImpl.cp(entry.sourcePath, destPath, { recursive: true, force: true });
+ log.log(`[build-next-isolated] Synced standalone module: ${entry.label}`);
+ changed = true;
+ }
+
+ return changed;
+}
+
export async function main() {
const movedPaths = [];
const transientBuildPaths = getTransientBuildPaths();
@@ -225,6 +321,15 @@ export async function main() {
nativeAssetErr
);
}
+
+ try {
+ await syncStandaloneExtraModules(projectRoot);
+ } catch (extraModuleErr) {
+ console.warn(
+ "[build-next-isolated] Non-fatal error syncing extra modules:",
+ extraModuleErr
+ );
+ }
}
process.exitCode = result.code;
} catch (error) {
diff --git a/src/app/api/logs/detail/route.ts b/src/app/api/logs/detail/route.ts
index 4a85321e42..90fa93ca2d 100644
--- a/src/app/api/logs/detail/route.ts
+++ b/src/app/api/logs/detail/route.ts
@@ -9,6 +9,7 @@ import {
getRequestDetailLogCount,
isDetailedLoggingEnabled,
} from "@/lib/db/detailedLogs";
+import { getUserDatabaseSettings, updateDatabaseSettings } from "@/lib/db/databaseSettings";
import { updateSettings } from "@/lib/db/settings";
export const dynamic = "force-dynamic";
@@ -36,6 +37,14 @@ export async function POST(req: NextRequest) {
const enabled = body.enabled === true || body.enabled === "1";
await updateSettings({ call_log_pipeline_enabled: enabled });
+ const databaseSettings = getUserDatabaseSettings();
+ updateDatabaseSettings({
+ logs: {
+ ...databaseSettings.logs,
+ detailedLogsEnabled: enabled,
+ callLogPipelineEnabled: enabled,
+ },
+ });
return NextResponse.json({
success: true,
diff --git a/src/app/api/oauth/[provider]/[action]/route.ts b/src/app/api/oauth/[provider]/[action]/route.ts
index 194601c698..56c74dd71b 100755
--- a/src/app/api/oauth/[provider]/[action]/route.ts
+++ b/src/app/api/oauth/[provider]/[action]/route.ts
@@ -6,6 +6,7 @@ import {
exchangeTokens,
requestDeviceCode,
pollForToken,
+ resolveBrowserOAuthRedirectUri,
} from "@/lib/oauth/providers";
import {
createProviderConnection,
@@ -80,7 +81,9 @@ export async function GET(
const { searchParams } = new URL(request.url);
if (action === "authorize") {
- const redirectUri = searchParams.get("redirect_uri") || "http://localhost:8080/callback";
+ const requestedRedirectUri =
+ searchParams.get("redirect_uri") || "http://localhost:8080/callback";
+ const redirectUri = resolveBrowserOAuthRedirectUri(provider, requestedRedirectUri);
const authData = generateAuthData(provider, redirectUri);
if (provider === "qoder" && !authData.authUrl) {
return NextResponse.json({
diff --git a/src/lib/db/databaseSettings.ts b/src/lib/db/databaseSettings.ts
index f9fa21b109..4b67b073b2 100644
--- a/src/lib/db/databaseSettings.ts
+++ b/src/lib/db/databaseSettings.ts
@@ -83,6 +83,17 @@ function parseStoredValue(rawValue: unknown): unknown {
}
}
+function toBooleanSetting(value: unknown): boolean | null {
+ if (typeof value === "boolean") return value;
+ if (typeof value === "number") return !Number.isNaN(value) && value !== 0;
+ if (typeof value !== "string") return null;
+
+ const normalized = value.trim().toLowerCase();
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
+ return null;
+}
+
function readNamespace(namespace: string): Record {
const db = getDbInstance();
const rows = db
@@ -119,6 +130,18 @@ function mergeTopLevelSections(target: UserDatabaseSettings, values: Record) {
+ const pipelineEnabled = toBooleanSetting(values.call_log_pipeline_enabled);
+ if (pipelineEnabled !== null) {
+ target.logs.callLogPipelineEnabled = pipelineEnabled;
+ }
+
+ const legacyDetailedEnabled = toBooleanSetting(values.detailed_logs_enabled);
+ if (legacyDetailedEnabled !== null) {
+ target.logs.detailedLogsEnabled = legacyDetailedEnabled;
+ }
+}
+
function mergeDatabaseSettingsNamespace(
target: UserDatabaseSettings,
values: Record
@@ -195,6 +218,7 @@ export function getUserDatabaseSettings(): UserDatabaseSettings {
mergeTopLevelSections(settings, mainSettings);
mergeDatabaseSettingsNamespace(settings, readNamespace(DATABASE_SETTINGS_NAMESPACE));
+ mergeRuntimeLogSettings(settings, mainSettings);
return settings;
}
@@ -236,6 +260,14 @@ export function updateDatabaseSettings(
const insert = db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)"
);
+ const settingsInsert = db.prepare(
+ "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', ?, ?)"
+ );
+
+ const requestedLogs = updates.logs as Partial | undefined;
+ const pipelineEnabled = requestedLogs?.callLogPipelineEnabled;
+ const detailedEnabled = requestedLogs?.detailedLogsEnabled;
+
const tx = db.transaction(() => {
for (const section of DATABASE_SETTINGS_SECTIONS) {
const sectionValues = nextSettings[section] as Record;
@@ -244,6 +276,10 @@ export function updateDatabaseSettings(
insert.run(DATABASE_SETTINGS_NAMESPACE, `${section}.${key}`, JSON.stringify(value));
}
}
+
+ if (pipelineEnabled !== undefined) {
+ settingsInsert.run("call_log_pipeline_enabled", JSON.stringify(Boolean(pipelineEnabled)));
+ }
});
tx();
diff --git a/src/lib/oauth/providers.ts b/src/lib/oauth/providers.ts
index 5709042630..3689b4aa47 100644
--- a/src/lib/oauth/providers.ts
+++ b/src/lib/oauth/providers.ts
@@ -10,6 +10,66 @@
import { generatePKCE, generateState } from "./utils/pkce";
import { PROVIDERS } from "./providers/index";
+const GOOGLE_BROWSER_PROVIDERS = new Set(["antigravity", "gemini-cli"]);
+
+function normalizeBaseUrl(value) {
+ const trimmed = typeof value === "string" ? value.trim() : "";
+ if (!trimmed) return "";
+ return trimmed.replace(/\/+$/, "");
+}
+
+function hasCustomGoogleOAuthCredentials(providerName, env = process.env) {
+ if (providerName === "antigravity") {
+ return !!env.ANTIGRAVITY_OAUTH_CLIENT_ID?.trim();
+ }
+
+ if (providerName === "gemini-cli") {
+ return !!env.GEMINI_CLI_OAUTH_CLIENT_ID?.trim() || !!env.GEMINI_OAUTH_CLIENT_ID?.trim();
+ }
+
+ return false;
+}
+
+/**
+ * Google providers default to localhost redirects so the embedded public
+ * credentials keep working on out-of-the-box local installs. When operators
+ * provide their own Google OAuth client IDs for a remote deployment, prefer the
+ * public callback URL documented in .env.example / docs/README so the popup can
+ * navigate back to OmniRoute instead of stalling on localhost.
+ */
+export function resolveBrowserOAuthRedirectUri(
+ providerName,
+ redirectUri,
+ env = process.env
+) {
+ if (!GOOGLE_BROWSER_PROVIDERS.has(providerName)) {
+ return redirectUri;
+ }
+
+ if (!hasCustomGoogleOAuthCredentials(providerName, env)) {
+ return redirectUri;
+ }
+
+ const publicBaseUrl =
+ normalizeBaseUrl(env.NEXT_PUBLIC_BASE_URL) || normalizeBaseUrl(env.OMNIROUTE_PUBLIC_BASE_URL);
+
+ if (!publicBaseUrl) {
+ return redirectUri;
+ }
+
+ try {
+ const requested = new URL(redirectUri);
+ const isLocalhostRedirect = /^(localhost|127\.0\.0\.1)$/i.test(requested.hostname);
+ if (!isLocalhostRedirect) {
+ return redirectUri;
+ }
+ } catch {
+ return redirectUri;
+ }
+
+ return `${publicBaseUrl}/callback`;
+}
+
/**
* Get provider handler
*/
diff --git a/src/lib/providers/codexFastTier.ts b/src/lib/providers/codexFastTier.ts
index de796f2087..9fe973e467 100644
--- a/src/lib/providers/codexFastTier.ts
+++ b/src/lib/providers/codexFastTier.ts
@@ -130,8 +130,8 @@ export interface ApplyCodexGlobalFastServiceTierOptions {
*/
model?: string | null;
/**
- * Outbound request body. Per-request body.service_tier is left untouched if
- * already set.
+ * Outbound request body. A valid per-request body.service_tier is left untouched
+ * when already set.
*/
body?: Record | null;
}
@@ -188,12 +188,11 @@ export function applyCodexGlobalFastServiceTier global mode > connection defaults.
diff --git a/src/shared/components/OAuthModal.tsx b/src/shared/components/OAuthModal.tsx
index 8f021c54d4..df0c59b69f 100644
--- a/src/shared/components/OAuthModal.tsx
+++ b/src/shared/components/OAuthModal.tsx
@@ -381,9 +381,9 @@ export default function OAuthModal({
// - Codex/OpenAI: always port 1455 (registered in OAuth app)
// - Windsurf/Devin CLI (remote fallback): use localhost with OmniRoute port + /auth/callback
// (on true localhost the callback server handles it; this is only reached on remote)
- // - Google OAuth providers (antigravity, gemini-cli): always localhost, regardless of
- // where OmniRoute is hosted — Google only accepts pre-registered localhost URIs with
- // the built-in credentials. Remote users must configure their own credentials.
+ // - Google OAuth providers (antigravity, gemini-cli): default to localhost so the
+ // bundled credentials keep working. The authorize route upgrades this to the public
+ // callback when custom Google credentials + NEXT_PUBLIC_BASE_URL are configured.
// - Other providers on remote: use actual origin (supports PUBLIC_URL env var)
// - Localhost: use localhost:port
let redirectUri: string;
@@ -433,7 +433,7 @@ export default function OAuthModal({
);
}
- setAuthData({ ...data, redirectUri });
+ setAuthData({ ...data, redirectUri: data.redirectUri || redirectUri });
// For non-true-localhost (LAN IPs, remote) or manual fallback: use manual input mode (user pastes callback URL)
if (!isTrueLocalhost || forceManual) {
diff --git a/tests/integration/chat-pipeline.test.ts b/tests/integration/chat-pipeline.test.ts
index 7476240b2c..2773023a11 100644
--- a/tests/integration/chat-pipeline.test.ts
+++ b/tests/integration/chat-pipeline.test.ts
@@ -606,6 +606,47 @@ test("chat pipeline persists Codex responses cache and reasoning tokens to call
assert.equal(callLog.tokens.reasoning, 13);
});
+test("chat pipeline applies global Codex priority service tier inside combos", async () => {
+ await seedConnection("codex", { apiKey: "sk-codex-combo-priority" });
+ await settingsDb.updateSettings({
+ codexServiceTier: { enabled: true, tier: "priority" },
+ });
+ await combosDb.createCombo({
+ name: "codex-priority-combo",
+ strategy: "priority",
+ config: { maxRetries: 0, retryDelayMs: 0 },
+ models: ["codex/gpt-5.5"],
+ });
+ const fetchCalls = [];
+
+ globalThis.fetch = async (url, init: RequestInit = {}) => {
+ fetchCalls.push({
+ url: String(url),
+ headers: toPlainHeaders(init.headers),
+ body: init.body ? JSON.parse(String(init.body)) : null,
+ });
+ return buildOpenAIResponsesSSE({ text: "combo priority ok", model: "gpt-5.5" });
+ };
+
+ const response = await handleChat(
+ buildRequest({
+ body: {
+ model: "codex-priority-combo",
+ stream: false,
+ messages: [{ role: "user", content: "Use Codex combo priority" }],
+ },
+ })
+ );
+
+ const json = (await response.json()) as any;
+ assert.equal(response.status, 200);
+ assert.equal(fetchCalls.length, 1);
+ assert.match(fetchCalls[0].url, /\/responses$/);
+ assert.equal(fetchCalls[0].headers.Authorization, "Bearer sk-codex-combo-priority");
+ assert.equal(fetchCalls[0].body.service_tier, "priority");
+ assert.equal(json.choices[0].message.content, "combo priority ok");
+});
+
test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests", async () => {
setCliCompatProviders(["codex"]);
await seedConnection("codex", {
@@ -950,12 +991,7 @@ test("chat pipeline sends Gemini CLI OAuth requests with native Cloud Code trans
assert.equal(generateCall.body.requestId, undefined);
assert.equal(generateCall.body.user_prompt_id, generateCall.body.request.session_id);
const keys = Object.keys(generateCall.body).slice(0, 4);
- assert.deepEqual(keys.sort(), [
- "model",
- "project",
- "request",
- "user_prompt_id",
- ]);
+ assert.deepEqual(keys.sort(), ["model", "project", "request", "user_prompt_id"]);
assert.equal(generateCall.body.request.sessionId, undefined);
assert.match(generateCall.body.request.session_id, /^[0-9a-f-]{36}$/i);
assert.equal(generateCall.body.request.contents.at(-1).parts[0].text, "Hello Gemini CLI");
diff --git a/tests/integration/combo-provider-exhaustion.test.ts b/tests/integration/combo-provider-exhaustion.test.ts
index 8957695b8b..2d20c96dbb 100644
--- a/tests/integration/combo-provider-exhaustion.test.ts
+++ b/tests/integration/combo-provider-exhaustion.test.ts
@@ -495,3 +495,79 @@ test.skip("round-robin path fast-skip: round-robin combo also skips exhausted pr
assert.equal(openaiCalls, 1, "round-robin should skip second openai target");
assert.equal(anthropicCalls, 1);
});
+
+test("allow rate-limited connections after transient 429 on subsequent targets in same combo", async () => {
+ const now = Date.now();
+ await seedConnection("openai", {
+ name: "openai-rate-limited-reused",
+ apiKey: "sk-openai-rate-limited-reused",
+ rateLimitedUntil: new Date(now + 60000).toISOString(),
+ });
+
+ await seedConnection("openai", {
+ name: "openai-fresh-429",
+ apiKey: "sk-openai-fresh-429",
+ });
+
+ await settingsDb.updateSettings({
+ requestRetry: 0,
+ maxRetryIntervalSec: 0,
+ });
+
+ await combosDb.createCombo({
+ name: "rate-limit-reuse-combo",
+ strategy: "priority",
+ config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
+ models: [
+ "openai/gpt-4o-mini",
+ "openai/gpt-3.5-turbo",
+ ],
+ });
+
+ let openaiCalls = 0;
+ let usedApiKeys: string[] = [];
+
+ globalThis.fetch = async (_url: string, init: any = {}) => {
+ const headers = toPlainHeaders(init.headers);
+ const authHeader = headers.authorization ?? headers.Authorization;
+
+ openaiCalls += 1;
+ if (authHeader === "Bearer sk-openai-fresh-429") {
+ usedApiKeys.push("fresh");
+ return new Response(JSON.stringify({ error: { message: "Too many requests" } }), {
+ status: 429,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ if (authHeader === "Bearer sk-openai-rate-limited-reused") {
+ usedApiKeys.push("rate-limited");
+ return new Response(
+ JSON.stringify({ choices: [{ message: { content: "rate limited reuse success" } }] }),
+ {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ }
+ );
+ }
+
+ throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
+ };
+
+ const response = await handleChat(
+ buildRequest({
+ body: {
+ model: "rate-limit-reuse-combo",
+ stream: false,
+ messages: [{ role: "user", content: "test rate limit reuse" }],
+ },
+ })
+ );
+
+ const body = (await response.json()) as any;
+ assert.equal(response.status, 200);
+ assert.equal(body.choices[0].message.content, "rate limited reuse success");
+ assert.equal(openaiCalls, 2);
+ assert.deepEqual(usedApiKeys, ["fresh", "rate-limited"]);
+});
+
diff --git a/tests/unit/codex-fast-tier.test.ts b/tests/unit/codex-fast-tier.test.ts
index 4aea837ccb..f411574345 100644
--- a/tests/unit/codex-fast-tier.test.ts
+++ b/tests/unit/codex-fast-tier.test.ts
@@ -106,15 +106,17 @@ test("Codex global service tier injects selected mode and can override connectio
});
test("Codex global service tier matches provider-prefixed combo model ids", () => {
+ const body: Record = {};
assert.deepEqual(
applyCodexGlobalFastServiceTier(
"codex",
{ providerSpecificData: {} },
{ codexServiceTier: { enabled: true, tier: "priority" } },
- { model: "codex/gpt-5.5" }
+ { model: "codex/gpt-5.5", body }
),
{ providerSpecificData: { requestDefaults: { serviceTier: "priority" } } }
);
+ assert.equal(body.service_tier, "priority");
const unsupported = { providerSpecificData: {} };
assert.equal(
@@ -155,7 +157,7 @@ test("Codex global service tier only short-circuits on valid body service_tier",
assert.deepEqual(injected, {
providerSpecificData: { requestDefaults: { serviceTier: "priority" } },
});
- assert.equal(invalidBody.service_tier, "invalid");
+ assert.equal(invalidBody.service_tier, "priority");
const validBody: Record = { service_tier: " Flex " };
const unchanged = { providerSpecificData: {} };
diff --git a/tests/unit/database-settings-maintenance.test.ts b/tests/unit/database-settings-maintenance.test.ts
index 299c7fa370..fa553eb687 100644
--- a/tests/unit/database-settings-maintenance.test.ts
+++ b/tests/unit/database-settings-maintenance.test.ts
@@ -11,6 +11,7 @@ process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../src/lib/db/core.ts");
const databaseSettings = await import("../../src/lib/db/databaseSettings.ts");
const databaseSettingsRoute = await import("../../src/app/api/settings/database/route.ts");
+const settingsDb = await import("../../src/lib/db/settings.ts");
const cleanup = await import("../../src/lib/db/cleanup.ts");
const aggregateHistory = await import("../../src/lib/usage/aggregateHistory.ts");
@@ -101,6 +102,23 @@ test("database settings reader supports legacy flat keys and lets nested saves w
assert.equal(databaseSettings.getUserDatabaseSettings().retention.callLogs, 7);
});
+test("database log settings mirror the runtime pipeline toggle", async () => {
+ await settingsDb.updateSettings({ call_log_pipeline_enabled: false });
+
+ assert.equal(databaseSettings.getUserDatabaseSettings().logs.callLogPipelineEnabled, false);
+
+ databaseSettings.updateDatabaseSettings({
+ logs: {
+ ...databaseSettings.getUserDatabaseSettings().logs,
+ callLogPipelineEnabled: true,
+ },
+ });
+
+ const settings = await settingsDb.getSettings();
+ assert.equal(settings.call_log_pipeline_enabled, true);
+ assert.equal(databaseSettings.getUserDatabaseSettings().logs.callLogPipelineEnabled, true);
+});
+
test("purgeDetailedLogs deletes request_detail_logs", async () => {
const db = core.getDbInstance();
db.prepare("INSERT INTO request_detail_logs (id, timestamp, duration_ms) VALUES (?, ?, ?)").run(
diff --git a/tests/unit/oauth-providers-config.test.ts b/tests/unit/oauth-providers-config.test.ts
index c277c5aea3..dc0c0d3103 100644
--- a/tests/unit/oauth-providers-config.test.ts
+++ b/tests/unit/oauth-providers-config.test.ts
@@ -18,8 +18,10 @@ const providersModule = await import("../../src/lib/oauth/providers/index.ts");
const oauthModule = await import("../../src/lib/oauth/constants/oauth.ts");
const registryModule = await import("../../open-sse/config/providerRegistry.ts");
const antigravityHeadersModule = await import("../../open-sse/services/antigravityHeaders.ts");
+const oauthHelpersModule = await import("../../src/lib/oauth/providers.ts");
const PROVIDERS = providersModule.default;
+const { resolveBrowserOAuthRedirectUri } = oauthHelpersModule;
const {
ANTIGRAVITY_CONFIG,
CLAUDE_CONFIG,
@@ -316,6 +318,44 @@ test("browser-based providers expose buildAuthUrl and return provider-specific a
assert.equal(clineUrl.origin, "https://api.cline.bot");
});
+test("custom Google OAuth credentials switch Antigravity remote callbacks to NEXT_PUBLIC_BASE_URL", () => {
+ const redirectUri = resolveBrowserOAuthRedirectUri(
+ "antigravity",
+ "http://localhost:20128/callback",
+ {
+ NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com/",
+ ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-antigravity.apps.googleusercontent.com",
+ }
+ );
+
+ assert.equal(redirectUri, "https://omniroute.example.com/callback");
+});
+
+test("custom Google OAuth credentials switch Gemini remote callbacks to OMNIROUTE_PUBLIC_BASE_URL", () => {
+ const redirectUri = resolveBrowserOAuthRedirectUri(
+ "gemini-cli",
+ "http://127.0.0.1:20128/callback",
+ {
+ OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com",
+ GEMINI_CLI_OAUTH_CLIENT_ID: "custom-gemini.apps.googleusercontent.com",
+ }
+ );
+
+ assert.equal(redirectUri, "https://omniroute.example.com/callback");
+});
+
+test("Google OAuth callbacks stay on localhost when no custom credentials are configured", () => {
+ const redirectUri = resolveBrowserOAuthRedirectUri(
+ "antigravity",
+ "http://localhost:20128/callback",
+ {
+ NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
+ }
+ );
+
+ assert.equal(redirectUri, "http://localhost:20128/callback");
+});
+
test("device and import-token providers expose the flow-specific fields expected by their configs", () => {
const deviceProviders = ["qwen", "kimi-coding", "github", "kiro", "amazon-q", "kilocode"];
diff --git a/tests/unit/sse-auth.test.ts b/tests/unit/sse-auth.test.ts
index f72348303e..5b57edbe46 100644
--- a/tests/unit/sse-auth.test.ts
+++ b/tests/unit/sse-auth.test.ts
@@ -608,6 +608,22 @@ test("getProviderCredentials retains rate-limited accounts when allowSuppressedC
assert.equal(bypassed.connectionId, connection.id);
});
+test("getProviderCredentials retains rate-limited accounts when allowRateLimitedConnections is enabled", async () => {
+ const connection = await seedConnection("openai", {
+ name: "allow-rate-limit-option",
+ rateLimitedUntil: futureIso(),
+ });
+
+ const blocked = await auth.getProviderCredentials("openai");
+ const bypassed = await auth.getProviderCredentials("openai", null, null, null, {
+ allowRateLimitedConnections: true,
+ });
+
+ assert.equal(blocked.allRateLimited, true);
+ assert.equal(bypassed.connectionId, connection.id);
+});
+
+
test("getProviderCredentials retains terminal accounts for combo live tests", async () => {
const connection = await seedConnection("openai", {
name: "suppressed-terminal",