diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9040ba915a..0921b54954 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,3 +108,39 @@ jobs: - run: npx playwright install --with-deps chromium - run: npm run build - run: npm run test:e2e + + test-integration: + name: Integration Tests + runs-on: ubuntu-latest + needs: build + env: + JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation + API_KEY_SECRET: ci-test-api-key-secret-long + INITIAL_PASSWORD: ci-test-password-for-integration + DATA_DIR: /tmp/omniroute-ci + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run test:integration + continue-on-error: true + + test-security: + name: Security Tests + runs-on: ubuntu-latest + needs: build + env: + JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation + API_KEY_SECRET: ci-test-api-key-secret-long + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run test:security + continue-on-error: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 014e4c7c44..8b20d06fe2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,7 +159,7 @@ src/ # TypeScript (.ts / .tsx) │ ├── cacheLayer.ts # LRU cache │ ├── semanticCache.ts # Semantic response cache │ ├── idempotencyLayer.ts # Request deduplication -│ └── localDb.ts # LowDB (JSON) storage +│ └── localDb.ts # Settings facade (LowDB for config, SQLite for domain data) ├── shared/ │ ├── components/ # React components (.tsx) │ ├── middleware/ # Correlation IDs, etc. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5ceb57ebe5..3e0b9fd41a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -771,8 +771,8 @@ Environment variables actively used by code: ## Operational Verification Checklist -- Build from source: `cd /root/dev/omniroute && npm run build` -- Build Docker image: `cd /root/dev/omniroute && docker build -t omniroute .` +- Build from source: `npm run build` +- Build Docker image: `docker build -t omniroute .` - Start service and verify: - `GET /api/settings` - `GET /api/v1/models` diff --git a/docs/adr/ADR-001-nextjs-foundation.md b/docs/adr/ADR-001-nextjs-foundation.md new file mode 100644 index 0000000000..8bb7637b18 --- /dev/null +++ b/docs/adr/ADR-001-nextjs-foundation.md @@ -0,0 +1,37 @@ +# ADR-001: Next.js as the Foundation for an AI Gateway + +## Status: Accepted + +## Context + +OmniRoute is an AI routing gateway that translates, forwards, and manages requests across 20+ LLM providers. We needed a framework that could serve both the API proxy layer and a management dashboard from a single codebase. + +**Alternatives considered:** + +- **Express.js only** — Simpler proxy, but requires separate frontend tooling +- **Fastify** — Fast, but no built-in SSR/dashboard support +- **Next.js** — Unified full-stack framework with API routes, SSR, and static pages + +## Decision + +We chose Next.js because: + +1. **Single deployment** — API routes (`/api/*`) and dashboard UI in one process +2. **Middleware layer** — Native request interception for auth guards and request tracing +3. **File-based routing** — Easy to map provider endpoints to handlers +4. **Built-in TypeScript** — Type safety across the entire codebase + +## Consequences + +**Positive:** + +- One `npm run build` produces both API and UI +- Middleware provides centralized auth and request tracing +- Dashboard gets automatic code splitting and optimization + +**Negative:** + +- Next.js middleware has limitations (no heavy imports, edge runtime constraints) +- Serverless deployment model doesn't align with persistent WebSocket/SSE connections +- Build times are longer than Express-only setups +- The SSE proxy layer (`open-sse/`) operates outside Next.js conventions diff --git a/docs/adr/ADR-002-hub-spoke-translation.md b/docs/adr/ADR-002-hub-spoke-translation.md new file mode 100644 index 0000000000..bca315769c --- /dev/null +++ b/docs/adr/ADR-002-hub-spoke-translation.md @@ -0,0 +1,37 @@ +# ADR-002: Hub-and-Spoke Translation with OpenAI as Intermediate Format + +## Status: Accepted + +## Context + +OmniRoute routes requests across 20+ providers, each with its own API format (OpenAI, Anthropic Messages, Google Gemini, AWS Bedrock, etc.). Direct provider-to-provider translation would require O(n²) translators. + +**Alternatives considered:** + +- **Direct translation** — Each pair needs a dedicated translator (n² complexity) +- **Common intermediate format** — Translate to/from a canonical format (2n complexity) +- **Protocol buffers** — Strong typing but heavy overhead for a proxy + +## Decision + +We use the **OpenAI Chat Completions format** as the canonical intermediate representation. All incoming requests are normalized to OpenAI format, processed, then translated to the target provider's format. + +``` +Client → [any format] → OpenAI canonical → [target format] → Provider +Provider → [response] → OpenAI canonical → [original format] → Client +``` + +## Consequences + +**Positive:** + +- Only 2 translators per provider (inbound + outbound) instead of n² pairs +- OpenAI format is the de facto standard — most clients already use it +- Adding a new provider requires only implementing one translator pair +- Streaming (SSE) works consistently through the canonical format + +**Negative:** + +- Some provider-specific features may be lost in translation +- The double translation adds latency (typically < 5ms) +- OpenAI format changes require updating the canonical representation diff --git a/docs/adr/ADR-003-dual-storage-sqlite.md b/docs/adr/ADR-003-dual-storage-sqlite.md new file mode 100644 index 0000000000..5b68c15fbb --- /dev/null +++ b/docs/adr/ADR-003-dual-storage-sqlite.md @@ -0,0 +1,39 @@ +# ADR-003: Dual Storage — SQLite Primary with JSON Migration Path + +## Status: Accepted + +## Context + +OmniRoute originally used LowDB (JSON file) for all persistence. As the project grew, JSON-based storage became a bottleneck for concurrent access, querying, and data integrity. + +**Alternatives considered:** + +- **LowDB only** — Simple but no concurrent access, no ACID, no querying +- **SQLite only** — Fast, ACID-compliant, but breaks existing deployments +- **PostgreSQL** — Production-grade but requires external dependency +- **Dual storage with migration** — SQLite primary + automatic JSON migration + +## Decision + +We migrated to **SQLite as the primary store** with an automatic one-time migration from `db.json`: + +1. On startup, if `db.json` exists and SQLite is empty, auto-migrate all data +2. All new reads/writes go through SQLite +3. The `db.json` file is preserved but no longer written to + +Settings remain in a hybrid model where LowDB handles simple key-value configuration for backward compatibility. + +## Consequences + +**Positive:** + +- ACID transactions for provider connections, API keys, and usage data +- Proper SQL queries for analytics and log filtering +- Concurrent read/write safety via WAL mode +- Zero-downtime migration from JSON — users upgrade transparently + +**Negative:** + +- Two storage engines to maintain (SQLite + LowDB for settings) +- Migration code must handle edge cases and partial data +- SQLite binary dependency needed in deployment environments diff --git a/eslint.config.mjs b/eslint.config.mjs index 0ec4f3d652..62d334fcb4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,7 @@ import nextVitals from "eslint-config-next/core-web-vitals"; /** @type {import("eslint").Linter.Config[]} */ const eslintConfig = [ ...nextVitals, - // FASE-02: Security rules + // FASE-02: Security rules (strict everywhere) { rules: { "no-eval": "error", @@ -11,18 +11,25 @@ const eslintConfig = [ "no-new-func": "error", }, }, - // Global ignores + // Relaxed rules for open-sse and tests (incremental adoption) + { + files: ["open-sse/**/*.ts", "tests/**/*.mjs", "tests/**/*.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "warn", + "@next/next/no-assign-module-variable": "off", + "react-hooks/rules-of-hooks": "off", + }, + }, + // Global ignores (open-sse and tests REMOVED — now linted) { ignores: [ ".next/**", "out/**", "build/**", "next-env.d.ts", - "tests/**", "scripts/**", "bin/**", "node_modules/**", - "open-sse/**", ], }, ]; diff --git a/src/app/(dashboard)/dashboard/analytics/loading.tsx b/src/app/(dashboard)/dashboard/analytics/loading.tsx new file mode 100644 index 0000000000..167555442d --- /dev/null +++ b/src/app/(dashboard)/dashboard/analytics/loading.tsx @@ -0,0 +1,15 @@ +"use client"; + +export default function AnalyticsLoading() { + return ( +
+ {error.message || "An unexpected error occurred while loading provider data."} +
+ ++ {error.message || "An unexpected error occurred while loading settings."} +
+ +