OpenAI-compatible multi-provider router
Quota-aware routing โข OAuth onboarding โข Persistent storage โข Request tracing โข Automatic model discovery
MultiVibe acts as an OpenAI-compatible gateway that lets you route requests across multiple provider accounts while keeping a single API surface. The same proxy routes are exposed under /v1 and at the root path for clients that expect either style.
- OpenAI-compatible API
GET /v1/modelsGET /v1/models/:idPOST /v1/chat/completionsPOST /v1/responsesPOST /v1/responses/compact- root-path aliases:
/models,/chat/completions,/responses,/responses/compact - compatibility endpoints:
/api/v1/models,/api/tags,/version,/props,/v1/props
- Streaming over SSE or WebSocket
- HTTP streaming uses plain
POSTwithstream: true - HTTP response stream is
text/event-stream /v1/responsesalso acceptsws:///wss://and Codex-style JSONresponse.createframes/v1/chat/completionsand/v1/responses/compactremain HTTP-only
- HTTP streaming uses plain
- Multi-account routing with quota-aware failover across OpenAI, OpenAI-compatible, Mistral, and z.ai accounts
- Model aliases (for example
small) with ordered fallback across providers/models, including optional effort-qualified targets likehigh:gpt-5.3-codex - Image-aware routing override that can route image-bearing requests to a chosen exposed model or alias while preserving the originally requested model in traces
- OAuth onboarding from dashboard with browser callback or device-code flow
- Manual OpenAI-compatible connections with custom
baseUrl+ API key - Default OpenAI passthrough account for root-path requests that are not handled by the OpenAI-compatible endpoints
- Persistent account storage across container restarts
- Request tracing v2 (configurable recent-trace retention, server pagination, trace export, tokens/model/error/latency stats, optional full payload, and image payload diagnostics)
- Usage stats endpoint with global + per-account + per-route aggregates over full history
- Time-range stats (
sinceMs/untilMs) while keeping lightweight history for long-term aggregates - zstd-compressed JSON request bodies for compatible clients
Screenshots below are taken in sanitized mode (
?sanitized=1).
When a request arrives, MultiVibe resolves the requested model to a provider and chooses an account with this strategy:
- Prefer accounts untouched on both windows (5h + weekly)
- Otherwise prefer account with nearest weekly reset
- Fallback by priority
- On
429/quota-like errors, temporarily block the account+model and retry on the next candidate
When the requested model is an alias, MultiVibe resolves it to ordered target models and automatically falls back across target models/providers as quotas are hit.
Aliases may also intentionally reuse an already exposed provider model name. In that case, the alias overrides the provider model and routes requests using the alias target order instead.
Alias targets can optionally be prefixed with a reasoning-effort tier: minimal:, low:, medium:, high:, or xhigh:. Requests using Chat Completions reasoning_effort or Responses reasoning.effort select the closest matching target tier before falling back.
If a request contains images and imageRequestModelOverride is set in admin settings, routing uses that model or alias when it is currently exposed. The upstream payload keeps image parts when translating between Chat Completions image_url content and Responses input_image content.
Everything important is file-based and survives restart (if /data is mounted):
/data/accounts.json/data/oauth-state.json/data/requests-trace.jsonl/data/requests-stats-history.jsonl
Recent trace retention defaults to the latest 1000 entries and can be changed with TRACE_RETENTION_MAX.
Stats history is append-only and keeps lightweight request metadata for long-term cost/volume tracking.
Docker compose already mounts
./data:/data.
docker compose up -d --build- Dashboard:
http://localhost:1455 - Health:
http://localhost:1455/health
Because this is often deployed remotely (Unraid/VPS), OpenAI onboarding supports both browser callback and device-code flows. The browser callback flow uses a manual redirect paste step:
- Open dashboard
- For OpenAI accounts, enter the account email
- Choose Browser callback and click Start OAuth
- Complete login in browser
- Copy the full redirect URL shown after the callback completes
- Paste that URL in the dashboard and click Complete OAuth
For headless or remote setups, choose Device code instead. The dashboard opens the verification page, shows a one-time code, and completes automatically after you approve the login.
Mistral, z.ai, and generic OpenAI-compatible accounts use manual token/API-key entry in the dashboard. Generic OpenAI-compatible accounts also require a baseUrl.
Default expected redirect URI:
http://localhost:1455/auth/callback
curl http://localhost:1455/v1/modelsExample model object returned:
{
"id": "gpt-5.3-codex",
"object": "model",
"created": 1730000000,
"owned_by": "multivibe",
"metadata": {
"context_window": null,
"max_output_tokens": null,
"supports_reasoning": true,
"supports_tools": true,
"supported_tool_types": ["function"]
}
}curl -X POST http://localhost:1455/v1/chat/completions \
-H "content-type: application/json" \
-d '{
"model": "gpt-5.3-codex",
"messages": [{"role":"user","content":"hello"}]
}'curl -N -X POST http://localhost:1455/v1/responses \
-H "content-type: application/json" \
-d '{
"model": "gpt-5.3-codex",
"input": "hello",
"stream": true
}'const ws = new WebSocket("ws://localhost:1455/v1/responses", {
headers: {
Authorization: "Bearer YOUR_TOKEN",
},
});
ws.onmessage = (event) => {
console.log(JSON.parse(event.data));
};
ws.onopen = () => {
ws.send(
JSON.stringify({
type: "response.create",
model: "gpt-5.3-codex",
input: [
{ role: "user", content: [{ type: "input_text", text: "hello" }] },
],
stream: true,
}),
);
};curl -X POST http://localhost:1455/admin/model-aliases \
-H "x-admin-token: change-me" \
-H "content-type: application/json" \
-d '{
"id": "small",
"targets": ["gpt-5.1-codex-mini", "devstral-small-latest"],
"enabled": true,
"description": "Small coding model pool"
}'Targets may also be effort-qualified:
{
"id": "reasoning-coder",
"targets": ["low:gpt-5.3-codex", "high:gpt-5.3-pro"],
"enabled": true
}curl -X PATCH http://localhost:1455/admin/settings \
-H "x-admin-token: change-me" \
-H "content-type: application/json" \
-d '{
"defaultPassthroughAccountId": "openai-account-id",
"imageRequestModelOverride": "vision-model-or-alias"
}'Use an empty string for either field to clear it.
# Paginated API (recommended)
curl -H "x-admin-token: change-me" \
"http://localhost:1455/admin/traces?page=1&pageSize=100"# Legacy compatibility mode
curl -H "x-admin-token: change-me" \
"http://localhost:1455/admin/traces?limit=50"curl -H "x-admin-token: change-me" \
"http://localhost:1455/admin/stats/usage?sinceMs=1735689600000&untilMs=1738291200000"curl -H "x-admin-token: change-me" \
"http://localhost:1455/admin/stats/traces?sinceMs=1735689600000&untilMs=1738291200000"curl -H "x-admin-token: change-me" \
"http://localhost:1455/admin/traces/export.zip?sinceMs=1735689600000&untilMs=1738291200000" \
-o traces-export.zipOptional filters:
accountId=<id>route=/v1/chat/completionssinceMs=<epoch_ms>untilMs=<epoch_ms>
Model alias admin endpoints:
GET /admin/model-aliasesPOST /admin/model-aliasesPATCH /admin/model-aliases/:idDELETE /admin/model-aliases/:id
Settings endpoints:
GET /admin/settingsPATCH /admin/settings
OAuth admin endpoints:
POST /admin/oauth/startGET /admin/oauth/status/:flowIdPOST /admin/oauth/completePOST /admin/oauth/device/poll
| Variable | Default | Description |
|---|---|---|
PORT |
1455 |
HTTP server port |
STORE_PATH |
/data/accounts.json |
Accounts, aliases, and settings store |
OAUTH_STATE_PATH |
/data/oauth-state.json |
OAuth flow state |
TRACE_FILE_PATH |
/data/requests-trace.jsonl |
Recent request trace file |
TRACE_STATS_HISTORY_PATH |
/data/requests-stats-history.jsonl |
Lightweight request history for long-term stats |
TRACE_RETENTION_MAX |
1000 |
Number of recent full traces to retain; minimum effective value is 100 |
TRACE_INCLUDE_BODY |
false |
Persist full request payloads when explicitly enabled; trace stats still work when disabled |
REQUEST_BODY_LIMIT |
100mb |
Max accepted JSON or decompressed zstd request body size |
PROXY_MODELS |
gpt-5.3-codex,gpt-5.2-codex,gpt-5-codex |
Fallback comma-separated model list for /v1/models |
MODELS_CLIENT_VERSION |
0.144.1 |
Codex version sent to OpenAI model discovery and runtime requests |
MODELS_CACHE_MS |
600000 |
Model discovery cache duration (ms) |
ADMIN_TOKEN |
empty | Admin endpoints auth token; empty disables the admin-token check |
CHATGPT_BASE_URL |
https://chatgpt.com |
OpenAI/ChatGPT upstream base URL |
UPSTREAM_PATH |
/backend-api/codex/responses |
OpenAI upstream request path |
UPSTREAM_COMPACT_PATH |
/backend-api/codex/responses/compact |
OpenAI upstream path for /v1/responses/compact |
MISTRAL_BASE_URL |
https://api.mistral.ai |
Mistral upstream base URL |
MISTRAL_UPSTREAM_PATH |
/v1/responses |
Mistral upstream path for responses |
MISTRAL_COMPACT_UPSTREAM_PATH |
/v1/responses/compact |
Mistral upstream path for compact responses |
ZAI_BASE_URL |
https://api.z.ai |
z.ai upstream base URL |
ZAI_UPSTREAM_PATH |
/v1/chat/completions |
z.ai upstream path for responses routed through chat completions |
ZAI_COMPACT_UPSTREAM_PATH |
/v1/chat/completions |
z.ai upstream path for compact responses |
OAUTH_CLIENT_ID |
app_EMoamEEZ73f0CkXaXp7hrann |
OpenAI OAuth client id |
OAUTH_AUTHORIZATION_URL |
https://auth.openai.com/oauth/authorize |
OAuth authorize endpoint |
OAUTH_TOKEN_URL |
https://auth.openai.com/oauth/token |
OAuth token endpoint |
OAUTH_DEVICE_AUTHORIZATION_URL |
https://auth.openai.com/api/accounts/deviceauth/usercode |
OAuth device-code start endpoint |
OAUTH_DEVICE_TOKEN_URL |
https://auth.openai.com/api/accounts/deviceauth/token |
OAuth device-code polling endpoint |
OAUTH_DEVICE_VERIFICATION_URL |
https://auth.openai.com/codex/device |
OAuth device-code verification page |
OAUTH_DEVICE_REDIRECT_URI |
https://auth.openai.com/deviceauth/callback |
OAuth device-code token exchange redirect URI |
OAUTH_SCOPE |
openid profile email offline_access |
OAuth scope |
OAUTH_AUDIENCE |
empty | Optional OAuth audience |
OAUTH_REDIRECT_URI |
http://localhost:1455/auth/callback |
Redirect URI |
TOKEN_REFRESH_MARGIN_MS |
60000 |
Refresh OAuth tokens this long before expiry |
ACCOUNT_FLUSH_INTERVAL_MS |
5000 |
Debounce interval for writing modified account state to disk |
MAX_ACCOUNT_RETRY_ATTEMPTS |
10 |
Max accounts to try on quota/rate-limit errors |
MAX_UPSTREAM_RETRIES |
5 |
Retries per upstream request (429/5xx) |
UPSTREAM_BASE_DELAY_MS |
2000 |
Base backoff delay for upstream retries (ms) |
HANG_RETRY_INTERVAL_MS |
10000 |
Delay between retry cycles when all accounts are exhausted (ms) |
HANG_RETRY_MAX_DURATION_MS |
120000 |
Max total time to hang-and-retry before returning 429 to client (ms) |
RATE_LIMIT_BLOCK_MS |
60000 |
Duration to block an account+model after a 429 response (ms) |
EXCLUDED_PROVIDER_MODELS |
empty | Comma-separated provider:model list to prevent routing a model to specific providers |
EMPTY_RESPONSE_BLOCK_THRESHOLD |
3 |
Empty assistant outputs before temporarily blocking account+model |
EMPTY_RESPONSE_BLOCK_DURATION_MS |
30000 |
Duration of an empty-response account+model block (ms) |
EMPTY_RESPONSE_WINDOW_MS |
300000 |
Time window for counting empty assistant outputs (ms) |
SENTRY_DSN |
empty | Optional Sentry DSN; unset disables Sentry |
SENTRY_ENVIRONMENT |
NODE_ENV or production |
Sentry environment |
SENTRY_TRACES_SAMPLE_RATE |
0.1 |
Sentry performance sampling rate |
npm install
npm --prefix web install
npm run devFor a production-style local run:
npm run build
npm run startPRs and issues are welcome.
If you open a PR:
- keep it focused
- include before/after behavior
- include screenshots for UI changes




