One service through which an org reaches models, tools, a browser and hosted agents. Today its Models area is the most complete: it pools your Claude, ChatGPT/Codex, Gemini/Vertex, Antigravity, xAI, Kimi and other accounts and provider keys behind one endpoint that speaks the OpenAI, Anthropic and Gemini APIs, with clients on API keys and a built-in control panel behind Cloudflare Access. Tools, Browser and Agents share the same org, API key and control panel.
Commissary comes in two forms: a self-hosted Commissary, which you deploy to your own Cloudflare account and whose people sign in through Cloudflare Access (this README), and Commissary Cloud, the paid service run for anyone, with sign-up and personal orgs (DEPLOY.md). A member's clients send an API key; the secrets Commissary holds for outside services are credentials, and a model provider's secret is a provider key.
- One endpoint, every client. Claude Code, Codex CLI, the OpenAI/Anthropic/Gemini SDKs and anything else that speaks those APIs (the client API); requests are translated between protocols, so Claude Code can run on a GPT model and vice versa.
- Use the subscriptions you already pay for. Log in with OAuth (Claude, Codex, Antigravity, xAI, Kimi, Devin, …) or add provider keys; credentials rotate round-robin with cooldowns on rate limits and automatic token refresh.
- Nothing to run. Three Workers (the app Worker for the panel and the management API, the API Worker for the client API, and the state Worker that holds the Durable Objects), KV and D1. No servers, no containers, and it scales to zero.
- Zero-trust by default. People sign in to the control panel and management API through Cloudflare Access with your identity provider; tools send an API key each member creates in the panel, stored only as a hash and revocable (a revoked key stops working within 30 seconds); scripts use management keys or Access service tokens. The Worker stores no client passwords.
- Or run it for others. With
COMMISSARY_MODE=cloudCommissary signs people in itself with GitHub or Google (an auth Worker on its own auth host), anyone may sign up and gets a Personal org, and platform admins limit and suspend people and orgs: this is Commissary Cloud (DEPLOY.md). - Control panel. Connect accounts, manage provider keys, watch quotas and usage, shape model routing and edit the
config at
/. - Infrastructure as code. The Worker, storage, Access application, policies and service tokens are declared in
alchemy.run.tsand deployed with one command.
Claude Code / Codex / SDKs ──────(API key)──────► Commissary Worker ──► Claude, OpenAI, Gemini, xAI, …
Browser (control panel) ──► Cloudflare Access ──► (translate, pick an (your OAuth logins and
(your IdP + tokens) account, refresh) provider keys)
- Deploy
- Connect a client
- Configuration
- Updating and removing
- Troubleshooting
- Provider support
- Known limitations
- Local development
- Documentation
- Acknowledgements
- A Cloudflare account on the Workers Paid plan (requests may use up to 300 s of CPU; see
COMMISSARY_CPU_MS). - A domain (zone) in that account. Commissary gets its own hostname on it, e.g.
llm.example.com. - Zero Trust enabled on the account, with an identity provider (the built-in one-time PIN works). Note your team
name:
<team>.cloudflareaccess.com. - Node.js 22+ and pnpm 12 (the version pinned in
package.jsondevEngines).
git clone https://github.com/spiritledsoftware/commissary.git
cd commissary
pnpm installpnpm install also clones the upstream Go server into .repos/ (a read-only reference for development; the deploy
does not use it).
pnpm exec alchemy profile edit --add CloudflareChoose browser OAuth or paste an API token. The profile is stored in ~/.alchemy/profiles.json.
cp .env.example .envFill in at least these (.env is git-ignored; shell variables override it):
| Variable | Example | Purpose |
|---|---|---|
COMMISSARY_DOMAIN |
llm.example.com |
Hostname (base domain) of Commissary; its zone must be in the account |
ACCESS_TEAM_DOMAIN |
myteam |
Your Zero Trust team name |
ACCESS_ALLOW_EMAILS / _EMAIL_DOMAINS |
you@example.com |
Who may sign in through the browser |
ACCESS_SERVICE_TOKENS |
laptop,ci |
One Access service token is created per name, for scripts that use the management API |
PLATFORM_ADMIN_EMAILS / _ADMIN_SERVICE_TOKENS |
you@example.com |
Platform admins: who may open the control panel and management API, and create orgs (emails, service token names) |
Every other setting is optional; see Configuration. The Worker needs no provider secrets in
.env: accounts and provider keys are added later through the control panel and stored in a Durable Object.
pnpm build # build the control panel into apps/web/dist/
pnpm run deploy --stage prod # shows the plan, asks for confirmation, deploysThe first deploy asks to bootstrap Alchemy's state store (a small Worker and Secrets Store in your account that keeps
the deploy state, shared by every machine that deploys; set ALCHEMY_STATE=local to keep it in .alchemy/ instead).
Use the same --stage for every later deploy (pnpm plan --stage prod previews changes).
The deploy prints the URL, the Access AUD tag and each service token's Client ID. Cloudflare returns the Client
Secrets only once: they are written to .alchemy/access-service-tokens.json (mode 0600) on the deploying machine.
export CF_ACCESS_CLIENT_ID='<id>.access' # from .alchemy/access-service-tokens.json
export CF_ACCESS_CLIENT_SECRET='<secret>'
curl https://llm.example.com/healthz \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
# {"status":"ok"}Open https://llm.example.com/ and sign in through Access with an admin email. Then:
- Connect account (top bar) logs in to Claude, Codex, Antigravity, xAI, Kimi, … or uploads existing auth files;
- Provider keys adds (Claude, OpenAI-compatible upstreams, Gemini, Vertex, OpenCode Zen/Go, …);
- API keys creates the key your clients send (shown once: copy it).
Coming from the Go CLIProxyAPI server? MIGRATION.md imports your config and auth files.
Clients send an API key (control panel, API keys) where they send a provider key: Authorization: Bearer,
x-api-key or x-goog-api-key/?key=. The client API is not behind Access, so no Access headers are needed.
export COMMISSARY_API_KEY='cms_…'
H=(-H "Authorization: Bearer $COMMISSARY_API_KEY")
curl "${H[@]}" https://llm.example.com/v1/models
curl "${H[@]}" https://llm.example.com/v1/chat/completions -H 'content-type: application/json' \
-d '{"model":"<model id from /v1/models>","messages":[{"role":"user","content":"hi"}]}'Claude Code:
export ANTHROPIC_BASE_URL=https://llm.example.com
export ANTHROPIC_AUTH_TOKEN="$COMMISSARY_API_KEY"
claudeCLIENTS.md covers Codex CLI, the OpenAI, Anthropic and Gemini SDKs and browser use. Clients that signed in through Access before API keys: MIGRATION.md.
Endpoints. POST /v1/chat/completions, /v1/completions, /v1/messages, /v1/messages/count_tokens,
/v1/responses (+ /compact, and a GET WebSocket upgrade), GET /v1/models, /v1beta/models/*
(:generateContent, :streamGenerateContent, …), POST /v1beta/interactions, images, videos and speech under /v1,
plus /openai/v1/videos and /backend-api/codex/{responses,responses/compact,alpha/search}. /chat/completions,
/completions and /responses are also served without /v1, so one base URL fits clients that append
/v1/messages for Anthropic models and /chat/completions for OpenAI models.
Deploy settings live in .env (template with comments: .env.example). Runtime settings (routing,
model aliases, retries, …) are edited in the control panel and stored in the Worker as config entries; a Go config.yaml
(payload rules included) is imported with POST /v9/management/settings/import (docs/MIGRATION.md).
| Variable | Default | Purpose |
|---|---|---|
COMMISSARY_DOMAIN |
(required) | Base domain, the app host is under it; the app host itself unless COMMISSARY_APP_DOMAIN is set |
COMMISSARY_APP_DOMAIN |
(base) | App host: the app Worker's and Access's hostname (docs/DEPLOY.md) |
COMMISSARY_API_DOMAIN |
A second hostname that alone serves the client API, outside Access (docs/DEPLOY.md) | |
ACCESS_TEAM_DOMAIN |
(required) | Zero Trust team name or <team>.cloudflareaccess.com |
ACCESS_ALLOW_EMAILS |
Emails allowed to sign in (comma separated) | |
ACCESS_ALLOW_EMAIL_DOMAINS |
Email domains allowed to sign in | |
ACCESS_SERVICE_TOKENS |
Names of the service tokens to create, one per script or machine using the management API | |
ACCESS_ALLOW_SERVICE_TOKEN_IDS |
Service tokens managed elsewhere (by token ID) to admit as well, e.g. CI's | |
PLATFORM_ADMIN_EMAILS |
Platform admins of the control panel and management API | |
PLATFORM_ADMIN_SERVICE_TOKENS |
Service token names (from ACCESS_SERVICE_TOKENS) or Client IDs that are platform admins |
|
ACCESS_SESSION_DURATION |
24h |
Access session length for browser sign-ins |
ACCESS_SERVICE_TOKEN_HEADER |
Accept service tokens as JSON in one header (e.g. x-api-key) for management scripts without custom headers |
|
ACCESS_AUD |
Use an Access application you manage yourself instead of creating one | |
COMMISSARY_CPU_MS |
300000 |
CPU limit per request in ms; lower it on plans with smaller limits |
COMMISSARY_MODE |
cloud for Commissary Cloud (own sign-in, no Access; its settings: DEPLOY.md) |
ACCESS.md explains what each Access setting creates, how the Worker validates requests, and how to extend the policies (IdP groups, device posture).
git pull && pnpm install && pnpm build && pnpm run deploy --stage prod # update
pnpm logs --stage prod --tail # stream Worker logs
pnpm destroy --stage prod # remove everythingD1 migrations run on every deploy. destroy deletes the Durable Objects holding your credentials and the usage
history: export accounts from the panel first if you want to keep them.
A cron trigger (0 */3 * * *) refreshes the model catalog from models.dev (also on demand from
the Models page), prunes old usage rows and re-arms credential refresh alarms.
Continuous deployment. .github/workflows/ci.yml deploys main to production and, once set up, to Commissary Cloud on
its own domain (stage cloud), and every pull request to its own preview stage on workers.dev. The CI credentials are code too (stacks/github.ts, pnpm ci:setup); see
DEPLOY.md.
| Symptom | Fix |
|---|---|
| Deploy fails on the custom domain | The app host (COMMISSARY_APP_DOMAIN, else COMMISSARY_DOMAIN) must be on a zone of the account |
Client gets 401 Missing API key |
Send an API key (Authorization: Bearer cms_…, x-api-key or x-goog-api-key) |
Client gets 401 Invalid API key |
The API key is wrong, revoked or expired: create a new one on the API keys page |
Client gets a 302 to cloudflareaccess.com |
Its path is not part of the client API, or the deploy's bypass application is missing (ACCESS_AUD) |
Client gets 404 Not found: … |
With COMMISSARY_API_DOMAIN set, send the client API to the API host the message names |
500 Authentication service error |
ACCESS_TEAM_DOMAIN is wrong |
403 Forbidden on the control panel |
Your email or token is not in PLATFORM_ADMIN_EMAILS / PLATFORM_ADMIN_SERVICE_TOKENS |
A model is missing from /v1/models |
No connected account or provider key serves it; check the Accounts and Models pages |
| Responses WebSocket closes with 1011/1012 | It hit the Worker CPU limit; clients reconnect, or use HTTP/SSE |
More Access-specific answers: ACCESS.md#troubleshooting.
| Provider (credential) | Client protocols served | Notes |
|---|---|---|
| Claude (OAuth login, provider key) | OpenAI, Responses, Claude, Gemini, Interactions | Claude Code cloaking and body signing; no uTLS (see limitations) |
| Codex / ChatGPT (OAuth browser or device, provider key) | OpenAI, Responses (HTTP + WebSocket), Claude, Gemini | /backend-api/codex/* aliases; no Codex live/realtime |
| Gemini (provider key), Vertex (provider key, service account upload) | OpenAI, Claude, Gemini, Responses, Interactions | No Gemini OAuth: Gemini-CLI auth files are rejected on import |
| Antigravity (OAuth) | OpenAI, Claude, Gemini, Interactions | |
| xAI (device login, provider key) | OpenAI, Responses (HTTP + WebSocket), images, video, TTS | |
| Kimi / Kimi.ai (device login), Meta, Devin | OpenAI, Claude, Responses | Devin: authorization-code login |
| OpenCode Zen / OpenCode Go (provider key) | every client protocol | Chat, Responses, Messages (and Zen Gemini) per model; Go keys show their usage quotas on the Accounts page |
| OpenAI-compatible upstreams (provider key) | every client protocol | Configured as api-keys.openai-compatibility groups |
The matrix is indicative: which (client protocol, provider) pairs work follows the registered translators in
packages/translation/src/translator/builtin.ts; an unsupported pair answers with a 4xx/501 error.
- No TLS fingerprint impersonation. Workers
fetchcannot control the TLS ClientHello or HTTP/2 settings, sowire-policy/tls-fingerprintsettings are ignored. Claude Code header and body cloaking still applies; providers that fingerprint TLS may treat the traffic differently. - Egress IPs are Cloudflare's. Requests leave from Cloudflare data-centre addresses, not your own IP, and cannot be pinned. Providers that bind sessions to an IP or block cloud ranges may reject or challenge them.
- No outbound proxies.
proxy-url(global, per credential, per key group) is read but not applied. - WebSocket CPU limit. A Responses WebSocket lives in the invocation that accepted it and is bounded by the CPU
limit (
COMMISSARY_CPU_MS). There are no ping keep-alives (streaming.keepalive-secondsis ignored). - Not supported: plugins, TUI, mDNS discovery, pprof, git/Postgres/object-store config backends, request-log files,
Codex live/realtime (WebRTC/SIP), the AI Studio
wsrelaygateway,/v0/managementand the Go server's client API keys (API keys replace them)./v1/responses/compactanswers 501 for Gemini/Vertex credentials.
Per-provider details: ARCHITECTURE.md; config keys without effect: MIGRATION.md.
pnpm install
pnpm build # or `pnpm --filter @commissary/web dev` for the panel with hot reload on :5173
ALCHEMY_STATE=local pnpm dev # panel on http://localhost:1337, client API on :1339, hot reloadalchemy dev runs the Workers in local workerd with emulated KV, D1 and Durable Objects. No Cloudflare resources are
created, and with ALCHEMY_STATE=local no Cloudflare login is needed. Access is not provisioned in dev: every request
to a loopback host is treated as the admin dev@example.com (override with ACCESS_DEV_BYPASS). The bypass is ignored
for any other host, refused whenever ACCESS_TEAM_DOMAIN or ACCESS_AUD is set, and never reaches a deployed Worker.
Open http://localhost:1337/, connect an account, then:
curl localhost:1339/v1/models
curl 'localhost:1339/cdn-cgi/handler/scheduled?cron=0+*/3+*+*+*' # run the cron jobsBefore sending a change: pnpm typecheck && pnpm lint && pnpm test && pnpm smoke. Scripts, module layout and
conventions are in DEVELOPMENT.md.
| Guide | Contents |
|---|---|
| ACCESS.md | Access application, policies, service tokens, management keys, Commissary Cloud sign-in, security model |
| CLIENTS.md | Claude Code, Codex CLI, SDKs, curl |
| DEPLOY.md | CI deploys, preview stages, Commissary Cloud, operations |
| MIGRATION.md | Moving from the Go CLIProxyAPI server |
| DEVELOPMENT.md | Scripts, module layout, conventions |
| ARCHITECTURE.md | Design decisions and per-subsystem behaviour |
| GLOSSARY.md | Domain terms |
Commissary began as a TypeScript + Effect port of CLIProxyAPI by Router-For.ME, and still uses it as the reference for translators, thinking, payload rules and provider behaviour. Thanks to its authors and contributors.
MIT, including the original CLIProxyAPI copyright notice.