Lincoln-cn/JAiRouter
Lincoln-cn/JAiRouter has 26 GitHub stars and 11 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-14 18:46 UTC. Last push 2026-11-13.
Measured star growth
- in 24h
- 0
- in 7 days
- Collecting history
- in 15 days
- Collecting history
- in 1 month
- Collecting history
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
About
Self-hosted LLM gateway for Java and Spring Boot teams: one OpenAI-compatible endpoint in front of Ollama, vLLM, GPUStack and cloud models, with RBAC, rate limiting, circuit breaking, audit and a visual console.
AIREADME.md
OriginalJAiRouter
One OpenAI-compatible endpoint in front of every model backend you run.
Ollama · vLLM · GPUStack · Xinference · LocalAI · OpenAI · Claude · Gemini
Routing · load balancing · rate limiting · circuit breaking · failover · RBAC · audit · visual console
Java + Spring Boot — built for teams whose stack already speaks JVM.
中文 • Docs • Issues • Discussions
This README is written to be read by people and AI agents alike: the facts block below and the Repository map / Commands / HTTP surfaces sections are the machine-actionable core. Where the code and this file disagree, the code wins — please open an issue so we can fix the file.
product: JAiRouter
repository: Lincoln-cn/JAiRouter
checkout_dir: model-router # the local/git directory name differs from the product name
artifact: model-router # Maven artifactId
project_version: 3.8.6 # pom.xml
latest_release: v3.8.6 (2026-11-14)
stack:
backend: Java 17 + Spring Boot 4.1.2 (WebFlux / reactive)
frontend: Vue 3 + TypeScript + Vite, bilingual zh-CN / en-US
build: Maven wrapper — ./mvnw (Unix) · .\mvnw.cmd (Windows)
runtime:
default_port: 8080
console: /admin/
swagger_ui: dev profile only
required_env_in_prod: [JWT_SECRET, INITIAL_ADMIN_PASSWORD]
required_env_in_staging: [JWT_SECRET]
surfaces:
- /v1/** # client surface: OpenAI-native + Anthropic-compatible
- /api/** # management + legacy surface (RBAC-governed)
- /actuator/** # health, readiness, Prometheus
tests:
command: ./mvnw test -o
scale: ~5,100 test methods across ~600 test classes
license: Apache-2.0
Why this one
Most LLM gateways are Python (LiteLLM) or an existing gateway stack you have to adopt whole (Kong, APISIX, Envoy). If your services are Spring Boot and your ops team already runs JVM workloads in containers, JAiRouter is the same job done in a stack you already know how to deploy, monitor and debug: one jar or one image, PostgreSQL or H2, Flyway migrations, Micrometer and Prometheus, and a console that reads like an admin page.
It is not a universal proxy. It is an operable gateway for a small-to-mid deployment: authorization down to permission codes, audit that survives review, and configuration you can change without a restart.
What it is
JAiRouter is a self-hosted AI model gateway. It puts one OpenAI-compatible endpoint in front of
the model servers you already run — Ollama, vLLM, GPUStack, Xinference, LocalAI — plus cloud
providers (OpenAI, Anthropic, Gemini, and any OpenAI-compatible vendor). Existing OpenAI SDK,
LangChain and LlamaIndex code keeps working: point base_url at the gateway and nothing else changes.
It then adds the layer you would otherwise build yourself: multi-instance load balancing, rate limiting, circuit breaking with failover to a different healthy instance, RBAC, audit logging, record governance and observability — all configurable at runtime from a web console, without restarting the process.
| Problem | What JAiRouter does |
|---|---|
| Many model endpoints to manage | One unified API surface |
| Manual failover when a backend dies | Circuit breaker + request-level failover |
| Auth implemented per service | JWT + API key with data-driven RBAC |
| Logs and metrics scattered per backend | Central metrics, tracing, call history |
| Restart required for config changes | Hot reload from the console |
Quick start
5-minute demo — no model service needed
Two containers and one script show the gateway end to end: request forwarding, model switching, response cache, and reversible sanitization. A mock upstream stands in for a real model, so there is no GPU to provision, no API key to obtain, and no JDK to install.
docker compose -f deploy/demo/docker-compose.yml pull # v3.8.1 or newer
docker compose -f deploy/demo/docker-compose.yml up -d # wait until both are healthy
node scripts/demo/run-demo.mjs
Full walkthrough, including how to check reversible sanitization from the upstream side: 5-minute demo.
Development profile — no secrets required
docker run -d --name jairouter -p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=dev \
sodlinken/jairouter:latest
# Console: http://localhost:8080/admin/
# Default login: admin / ChangeMeOnFirstStartup123456
The dev profile intentionally falls back to built-in development secrets, so it starts with no configuration at all. Never expose a dev-profile instance to an untrusted network.
Production profile — secrets are required
docker run -d --name jairouter -p 8080:8080 \
-e JWT_SECRET="$(openssl rand -base64 48)" \
-e INITIAL_ADMIN_PASSWORD='<your-strong-admin-password>' \
sodlinken/jairouter:latest
Both variables are mandatory. The prod profile performs a startup security gate and
refuses to boot when either is missing or still at its factory default — see
Production startup gate for the exact rules. A container started with
only JWT_SECRET exits during startup with IllegalStateException.
If you boot the JAR directly rather than through Docker, generate strong values first:
java -jar target/model-router-*.jar --generate-key # JWT secret
java -jar target/model-router-*.jar --generate-password # admin password
Point a client at it
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/v1", # OpenAI-native surface (raw JSON / SSE)
api_key="not-needed", # Authorization is forwarded downstream
default_headers={"X-API-Key": "<your-api-key>"} # gateway credential (Management API key)
)
response = client.chat.completions.create(
model="llama3.2", # any model from your configured backends
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
Registered backends are managed from the console under Instance Management; changes apply by hot reload, with no restart.
Production startup gate
The gate lives in StartupSecretKeyChecker (a CommandLineRunner) and
StartupSecurityGate. It runs on every start and reads environment variables directly — setting
the equivalent YAML property is not enough to satisfy it:
| Check | Rule when the profile is prod / production |
Rule otherwise |
|---|---|---|
| JWT enabled | jairouter.security.jwt.enabled must be true, otherwise fail |
not checked |
JWT_SECRET |
must be set, not a known factory/doc default, and not rate VERY_WEAK / WEAK |
warn only |
INITIAL_ADMIN_PASSWORD |
must be set, except for the factory default ChangeMeOnFirstStartup123456, and not rate VERY_WEAK / WEAK |
warn only |
Details worth knowing:
- A missing
INITIAL_ADMIN_PASSWORDis a hard failure in production. It is the only name the configuration binding reads — the legacyADMIN_PASSWORDalias is gone — and nothing is generated for you. - No password is auto-generated in
prod. If neither variable is set, startup aborts; it does not fall back to the documented default. (The only generators are the explicit CLI flags--generate-key/--generate-password.) Outsideprod, the base configuration falls back to a random value rather than a public one, and prints it once in the startup log. - A
MEDIUM-strength value logs a warning and allows startup;VERY_WEAK/WEAKblocks it. - Escape hatch for trusted isolated networks:
JAIRouter_SKIP_AUTH_WARNING=true. It must be set explicitly and it disables the gate for all of the checks above. - The
adminaccount itself comes fromjairouter.security.jwt.accountsinsrc/main/resources/config/auth/jwt.yml, whose password is{noop}${INITIAL_ADMIN_PASSWORD:${random.uuid}}(random fallback outsideprod, no public literal). On first start those YAML accounts are copied into thejwt_accountstable only when that table is empty (JpaDatabaseInitializer.initializeJwtAccounts); later starts leave the table untouched, so rotatingINITIAL_ADMIN_PASSWORDdoes not change an account that already exists in the database — change it in the console instead.
Repository map
| Path | What lives there |
|---|---|
src/main/java/org/unreal/modelrouter/router/ |
Gateway core: adapters, request processors, load balancers, rule engine, circuit breaker, cache |
src/main/java/org/unreal/modelrouter/auth/ |
Authentication (JWT, API key), sanitization, security audit, RBAC, console-facing auth APIs |
src/main/java/org/unreal/modelrouter/monitor/ |
Metrics, tracing, call history, slow-query analysis, error tracking, security alerts |
src/main/java/org/unreal/modelrouter/persistence/ |
Store abstractions plus JPA entities/repositories and schema initialization |
src/main/java/org/unreal/modelrouter/config/ |
Configuration service: hot reload, versioning/rollback, validation, console API |
src/main/resources/config/** |
Modular YAML config, imported via spring.config.import in application.yml |
src/main/resources/db/migration/{h2,postgres}/ |
Versioned schema migrations (Flyway), one dialect directory each |
src/test/java/ |
JUnit 5 + Mockito + Reactor StepVerifier; ~5,100 test methods |
frontend/ |
Vue 3 + TypeScript console (Vite base: /admin/) |
docs/zh/, docs/en/ |
MkDocs documentation, kept symmetrical |
deploy/ |
Kubernetes manifests, compose files, deployment scripts |
.cspell.json |
Spell-check word list — the docs workflow fails on unknown words |
innerdoc/ |
Internal design notes, not published |
Commands
# Build (skipping tests)
./mvnw clean package -DskipTests
# Run locally
java -jar target/model-router-*.jar --spring.profiles.active=dev
# Full Java test suite (offline works; -Dcheckstyle.skip etc. speed up iteration)
./mvnw test -o
# Focused test run — comma-separated class filter, no wildcards
./mvnw test -Dtest=FooTest,BarTest -DfailIfNoTests=false -o
# Frontend (type-check + build)
cd frontend && npm ci && npm run type-check && npm run build
Windows: substitute .\mvnw.cmd for ./mvnw.
HTTP surfaces
| Surface | Prefix | Protocol | Auth |
|---|---|---|---|
| Client — OpenAI-native | /v1/** |
OpenAI JSON + SSE (/v1/chat/completions, /v1/models, /v1/embeddings, /v1/rerank) |
JWT or API key |
| Client — Anthropic-compatible | /v1/messages, /v1/messages/count_tokens |
Anthropic Messages (streaming SSE, tool use) | JWT or API key |
| Management | /api/** |
JSON, RouterResponse envelope |
JWT/API key + RBAC URL matrix |
| Operations | /actuator/** |
Health, readiness, /actuator/prometheus |
depends on profile |
Error bodies are protocol-shaped per surface: on /v1/messages an Anthropic-shaped
{"type":"error","error":{...}}, on the other /v1/** paths an OpenAI-shaped
{"error":{message,type,code}}, and /api/** keeps its legacy shape. Streaming failures on the
/v1 surface emit a protocol-shaped error frame and still terminate the stream with an error —
[DONE] is never appended to a failed stream, because clients treat [DONE] as success.
Authentication and authorization
- JWT — header name is configurable, default
Jairouter_Token. - API key — header
X-API-Key. Gateway credentials are separate from downstream provider keys:x-api-keyis consumed by the gateway and never forwarded, so per-instance downstream credentials belong in the instance's ownheaders.Authorization. - RBAC — data-driven: 65 permission codes, 4 role templates (
ADMIN/OPERATOR/USER/VIEWER), a URL rule matrix plus method-level@PreAuthorize. Requests that match no rule followjairouter.security.rbac.unmatched-policy, whose default isDENY_ALL;ROLE_ADMINshort-circuits everything./v1/**is authenticated but intentionally outside the permission matrix.
Feature inventory
Implemented and exercised by the test suite:
- Adapters — Ollama, vLLM, GPUStack, Xinference, LocalAI, OpenAI, Anthropic (native), Gemini
(native), plus any OpenAI-compatible provider declared in
adapter-definitions. - Load balancing — round-robin, weighted, least-connections, IP hash, consistent hash, EWMA latency-aware, random.
- Rule engine — conditional routing on model, service type, header, client IP, weight and instance tags, with hit statistics; visual editing in the console.
- Resilience — circuit breaker per instance, request-level failover to another healthy instance, rate limiting (token bucket / leaky bucket / sliding window), response cache for deterministic requests (opt-in, tenant-isolated keys).
- Quota — multi-dimensional, multi-window ledger (MINUTE/HOUR/DAY/MONTH) with Redis distributed
counting and local fallback (
jairouter.quota.enabled, off by default). - Record governance — call history at three levels: metadata only, desensitized summary, or AES-256-GCM-encrypted full content; ADMIN-only decryption endpoint.
- Observability — Prometheus metrics, OpenTelemetry tracing, slow-query analysis, error tracking, security alerts. Alert outlets are deliberately limited to Prometheus metrics + structured logs — the repository implements no push channels (email/webhook/IM); wire those up in Alertmanager or your log platform.
- Persistence — H2 by default; PostgreSQL (with
Flywaymigrations) whenDATABASE_URLpoints at it; Redis for shared state in multi-replica deployments. - Multi-replica — cross-replica event broadcast, shared rate-limit/quota counters, and a distributed lock for scheduled tasks (including the alert cooldown).
- Console — Vue 3 SPA at
/admin/: services, instances, adapters, rules, versions & rollback, quota, call history, tracing, alerts, API keys, RBAC, dark/light theme, zh-CN / en-US.
Not implemented: in-repo notification delivery. A semantic cache (SimHash near-match reuse of
non-streaming responses) exists but is off by default — see jairouter.semantic-cache.enabled.
See the changelog for per-release notes.
Architecture
Request flow
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
│ (OpenAI SDK / LangChain / LlamaIndex) │
└─────────────────────────────────┬───────────────────────────────┘
│
▼ OpenAI-Compatible API
┌─────────────────────────────────────────────────────────────────┐
│ JAiRouter Gateway │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Routing │ │ Balance │ │ Limit │ │ Circuit │ │ Auth │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Observability & Persistence │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────┬───────────────────────────────┘
│
┌──────────────┬───────────┼───────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌──────────┐ ┌───────┐
│Ollama │ │ vLLM │ │GPUStack│ │Xinference│ │ OpenAI │
└───────┘ └───────┘ └───────┘ └──────────┘ └───────┘
Console screenshots
Dashboard — real-time service, instance and system metrics
Service & instance management — adapters and load-balancing strategies per service
Visual routing rules with hit statistics, priority drag-and-drop and templates
Call history analytics — success rate, latency and token usage trends
Slow-query analysis — hotspots, alert status and operation breakdown
Playground — chat, embedding, rerank, audio and image test benches
Quota runtime configuration — restart-required fields are read-only with a badge
Quota monitoring — backend type, degradation status and multi-dimensional filtering
Client access guide — copy-ready snippets for the OpenAI-compatible and Anthropic base URLs
Built-in dark theme — the console is themeable and bilingual
Status
Latest release: v3.8.6 (2026-11-14). Per-release notes, upgrade notes and known limits live in the changelog; downloads and container tags are on Releases.
This file deliberately does not repeat release history. It used to, and the copy fell five releases behind the changelog — one source of truth is the only version that stays true.
Documentation
| Resource | Link |
|---|---|
| Full documentation | https://jairouter.com |
| Deployment | https://jairouter.com/en/deployment/ |
| Configuration | https://jairouter.com/en/configuration/ |
| Monitoring | https://jairouter.com/en/monitoring/ |
| Interactive API reference | http://localhost:8080/swagger-ui (dev profile) |
Why JAiRouter?
- vs calling backends directly — no shared resilience, no central credentials, no usage visibility. JAiRouter gives one endpoint, automatic failover and a full audit trail.
- vs One-API / new-api — those distribute and bill API keys for shared accounts. JAiRouter is the gateway in front of your own inference cluster: multi-instance balancing, circuit breaking, rule routing and RBAC for self-hosted Ollama/vLLM/GPUStack. The two complement each other.
- vs Nginx — Nginx is a general-purpose web server. JAiRouter is purpose-built for model workloads: OpenAI-compatible routing, model-aware balancing, streaming semantics.
- vs LangChain — LangChain is an application framework. JAiRouter is the infrastructure layer beneath it.
| Feature | JAiRouter | Nginx | One-API | LangChain |
|---|---|---|---|---|
| OpenAI compatible | ✅ | ❌ | ✅ | ✅ |
| Load balancing | ✅ | ✅ | ✅ | ❌ |
| Circuit breaker | ✅ | ❌ | ❌ | ❌ |
| Rate limiting | ✅ | ✅ | ✅ | ❌ |
| Web console | ✅ | ❌ | ✅ | ❌ |
| Config hot reload | ✅ | ❌ | ✅ | ❌ |
| Version control | ✅ | ❌ | ❌ | ❌ |
| OpenTelemetry | ✅ | ❌ | ❌ | ✅ |
Contributing
Contributions are welcome — see the Contributing Guide.
Security issues: please do not open a public issue — report them privately per SECURITY.md.
Before opening a pull request:
./mvnw test -o # ~600 test classes, ~5,100 test methods, must stay green
cd frontend && npm run type-check # the frontend type gate
House conventions worth knowing: commits use <type>(<scope>): <summary>; every fix/feature branch
targets master and is squash-merged; documentation exists in both docs/zh/ and docs/en/ and is
expected to stay symmetrical; docs are spell-checked against .cspell.json, so new technical terms
may need to be added there.
git clone https://github.com/Lincoln-cn/JAiRouter.git
cd JAiRouter
./mvnw clean package -DskipTests
java -jar target/model-router-*.jar
Support
- Documentation: https://jairouter.com
Issues or Discussions — where should I post?
GitHub gives us two channels and we keep them separate on purpose. Pick by what you have:
| What you have | Where to post | How we handle it |
|---|---|---|
| A reproducible defect — crash, wrong result, regression, wrong doc | Issues | Triaged quickly; confirmed defects are fixed or scheduled |
| A requirement or idea — feature request, new adapter, design discussion, usage question | Discussions | Collected, voted on and folded into the roadmap |
In one line: a defect goes to Issues; a requirement or a question goes to Discussions.
Not sure which one fits? Post in Discussions — if it turns out to be a reproducible defect, we will move it to Issues for you.
Discussions is organized by category:
| Category | Use it for |
|---|---|
| 💡 Ideas | Feature requests and improvement suggestions |
| 🗳 Polls | Community votes on direction |
| 🙏 Q&A | How-to questions for the community |
| 🙌 Show and tell | Share what you built on JAiRouter |
| 💬 General | Anything else |
| 📣 Announcements | Release notes from maintainers |
License
JAiRouter is released under the Apache 2.0 License.
If JAiRouter saves you time, star it on GitHub ⭐
Found a bug? Open an issue. Have an idea or a question? Start a discussion.
Maintained by Lincoln-cn · Apache-2.0