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, z.ai, and Grok Build subscription 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, including xAI device OAuth for SuperGrok / X Premium+
- Manual OpenAI-compatible connections with custom
baseUrl+ API key - Optional local proxy API key for HTTP and WebSocket clients via
PROXY_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.
For Grok Build, choose Grok Build (subscription) and start the device login.
The proxy sends the resulting subscription bearer to
https://cli-chat-proxy.grok.com/v1, together with the same client headers as
the official Grok Build CLI. It does not use XAI_API_KEY and therefore does
not switch the account to pay-per-token API billing.
An existing official CLI session can instead be imported with Import
configured auth.json. The server reads XAI_AUTH_PATH (default
~/.grok/auth.json) and ignores xai::api_key entries. In Docker, mount the
file read-only and point XAI_AUTH_PATH at the container path. Deprecated
pre-OIDC web_login entries are also rejected because the current Grok CLI no
longer treats them as a reliable sampling credential:
services:
multivibe:
environment:
- XAI_AUTH_PATH=/run/secrets/grok-auth.json
volumes:
- ${HOME}/.grok/auth.json:/run/secrets/grok-auth.json:roDevice OAuth is preferred when the CLI and proxy may run concurrently. Refresh tokens rotate; importing the same session into independent stores can make one consumer stale after the other refreshes it.
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/modelsWhen PROXY_API_KEY is set, send it as either a Bearer token or x-api-key.
An authenticated dashboard session can still use the API. Configure
ADMIN_TOKEN as well when using the dashboard so it can establish that
session:
curl -H "Authorization: Bearer $PROXY_API_KEY" \
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/pollPOST /admin/grok/import
To start Grok Build device OAuth, call POST /admin/oauth/start with
{"provider":"xai","method":"device"}. Poll the returned flowId through the
same /admin/oauth/device/poll endpoint used by OpenAI device OAuth.
| 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 |
USAGE_STALE_WHILE_REVALIDATE |
true |
Serve an existing stale usage snapshot while refreshing it in the background |
USAGE_STALE_MAX_AGE_MS |
1800000 |
Maximum snapshot age eligible for stale-while-revalidate; older snapshots block for refresh |
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) |
MODELS_STALE_WHILE_REVALIDATE |
true |
Serve a bounded stale model catalog while refreshing it in the background |
MODELS_STALE_MAX_AGE_MS |
1800000 |
Maximum model-catalog age eligible for stale-while-revalidate |
ADMIN_TOKEN |
empty | Admin endpoints auth token; empty disables the admin-token check |
PROXY_API_KEY |
empty | Optional Bearer or x-api-key required by HTTP and WebSocket proxy endpoints |
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 |
XAI_BASE_URL |
https://cli-chat-proxy.grok.com/v1 |
Grok Build subscription upstream base URL |
XAI_RESPONSES_PATH |
/responses |
Grok Build Responses upstream path |
XAI_CHAT_COMPLETIONS_PATH |
/chat/completions |
Grok Build Chat Completions upstream path |
XAI_MODELS_PATH |
/models |
Grok Build model-discovery path |
XAI_AUTH_PATH |
~/.grok/auth.json |
Official Grok CLI credential file read by the import action |
XAI_OAUTH_ISSUER |
https://auth.x.ai |
Trusted xAI OAuth issuer |
XAI_OAUTH_CLIENT_ID |
official Grok Build client id | OAuth client used for device login and refresh |
XAI_OAUTH_SCOPES |
official Grok Build scope set | Comma- or space-separated xAI OAuth scopes |
XAI_CLIENT_VERSION |
0.2.114 |
Grok CLI version header sent to subscription endpoints |
XAI_CLIENT_IDENTIFIER |
grok-pager |
Grok CLI client identifier header |
XAI_TOKEN_AUTH |
xai-grok-cli |
xAI subscription token-auth selector |
XAI_USER_AGENT |
generated Grok CLI user agent | User-Agent sent to Grok Build endpoints |
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 for transient 5xx/transport errors; quota rotates accounts |
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 |
Grok Build subscription access is intended for the account owner or another trusted operator. Do not expose a subscription-backed instance as a public multi-tenant service, and review the current xAI terms before deployment.
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




