This repo uses a hybrid Cloudflare Worker plus Container deployment. The Worker serves health checks, roleplay session orchestration, native LinkAPI traffic, Codex Everywhere OpenAI traffic, and the Kimi Code model catalog directly. OpenCode provider traffic, Kimi Code chat, and the remaining Flask routes run through a Container.
Cloudflare's Python Workers runtime runs on Pyodide. Cloudflare's docs note that threading, multiprocessing, and sockets are not functional there. This app uses:
- Flask and WSGI-style request handling
requestsfor upstream provider calls- threaded execution and SSE streaming
- service-account JSON support for the
googleaiprovider
That makes Cloudflare Containers the safer target for the Python application. /v1/roleplay, /linkapi/*, and /codex-easy/* keep their orchestration in the Worker. The roleplay route uses one SQLite-backed Durable Object per session and sends only selected OpenCode provider calls through Container egress. The Worker serves /kimi-code/v1/models locally and rejects unauthorized or invalid Kimi requests without waking the Container; valid Kimi chat streams through Flask because Kimi's edge rejects Worker-origin egress.
The main Worker now binds the private multillm-knowledge service. Before its
first deployment with that binding, provision and deploy the
Knowledge Worker. Knowledge starts disabled;
provider secrets and allowances are configured separately from chat.
- The target Cloudflare account must have the Workers Paid plan because Containers are only available there.
- Authenticate Wrangler against the student roll-number account:
npx wrangler login- Install the JavaScript dependencies used by the Worker:
npm ciWrangler vars in wrangler.jsonc already set:
FLASK_ENV=productionSERVER_HOST=0.0.0.0SERVER_PORT=8080APP_NAME=MultiLLM Proxy
RUNTIME_FEATURE_FLAGS groups eight nonsecret settings into one JSON binding to
leave room for provider secrets. Worker vars and secrets share a limit of 128
text bindings, and Cloudflare rejects an upload that exceeds it. The Container,
usage ledger, roleplay settings and roleplay session read these grouped
settings. An individual binding with the same setting name takes precedence,
including an explicit disabled value. Keep credentials in separate Worker
secrets; the grouped binding accepts only the ten names listed in
worker/runtime-feature-flags.mjs. Add a new nonsecret setting to the group
rather than as a separate var. It can hold the model price table
(MODEL_PRICING_USD_PER_MILLION); keep the group under Cloudflare's 5 KB limit
for one variable.
Public HTTP requests receive a 308 redirect to the same HTTPS URL before any
route or container work. For Container requests, the Worker overwrites
X-Forwarded-Proto, X-Forwarded-Host, and the internal
X-MultiLLM-External-Origin header, then enables
MULTILLM_TRUST_PROXY_HEADERS inside the Container. The internal origin header
survives Container runtime header normalization, so Flask generates public
HTTPS dashboard, documentation, and login URLs without trusting caller-supplied
forwarding headers in direct local deployments.
Set secrets for anything sensitive:
npx wrangler secret put ADMIN_API_KEY
npx wrangler secret put FLASK_SECRET_KEY
npx wrangler secret put JWT_SECRET
npx wrangler secret put CODEX_EASY_API_KEY
npx wrangler secret put KIMI_CODE_API_KEY
npx wrangler secret put LINKAPI_KEY
npx wrangler secret put OPENCODE_GO_API_KEY
npx wrangler secret put NANOGPT_API_KEY
npx wrangler secret put NANOGPT_API_KEY_1
npx wrangler secret put NAVYAI_API_KEY
npx wrangler secret put AIHUBMIX_API_KEY
npx wrangler secret put GEMINI_API_KEY
npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put OPENROUTER_API_KEYSet only the providers you actually use. The Worker forwards these directly
into the container. It also forwards numbered GROQ_API_KEY_N,
NANOGPT_API_KEY_N, and compatibility NANO_GPT_KEY_N secrets automatically.
The committed Worker variables set NANOGPT_BILLING_MODE=subscription and
NANOGPT_SUBSCRIPTION_BASE_URL=https://nano-gpt.com/api/subscription. These are
non-secret routing settings; keep the API keys in Wrangler secrets. Do not add
an X-Billing-Mode: paygo override when the NanoGPT account is subscription
only. NANOGPT_PREFERRED_KEY_INDEX=0 makes the primary NANOGPT_API_KEY
the first choice while retaining numbered credentials as automatic fallbacks.
POST /v1/roleplay and /roleplay/v1/chat/completions authenticate at the
edge and route directly to ROLEPLAY_SESSION, a SQLite-backed Durable Object
binding. One object per session serializes turns without creating a global
bottleneck. It stores bounded continuity memory, idempotency guards, and
Kimi/GLM latency and reliability statistics.
NanoGPT subscription GLM-5.3-Flash is the first production GLM choice,
followed by uncensored Flash and GLM-5.2 in the 1x pool. Full GLM-5.3 remains
an explicit 2x selector. roleplay:glm-speed ranks the 1x choices using
same-session streaming TPS; OpenCode and NavyAI remain later provider tiers.
LinkAPI is Kimi-only and OpenRouter is omitted from roleplay. See
the roleplay guide for the request schema, compaction policy,
safe fallback boundary, metrics route, and all ROLEPLAY_* variables.
NanoGPT and NavyAI routes are Container-backed raw gateways. The Worker forwards their keys, optional base-URL overrides, provider request limits, and the credential-splitting CORS headers into Flask. See NanoGPT gateway and NavyAI gateway.
CODEX_EASY_API_KEY is the preferred Codex Everywhere secret; CODEX_API_KEY remains a fallback alias. Both the Worker and Flask always use the fixed upstream origin https://codex-easy.ai.
Use $PROXY_BASE_URL/codex-easy when the client appends /v1 itself. Use $PROXY_BASE_URL/codex-easy/v1 when the client expects its configured base URL to include /v1 already.
| Operation | Direct Worker route | Caller authentication |
|---|---|---|
| Key-group model catalog | /codex-easy/v1/models |
Authorization: Bearer $ADMIN_API_KEY |
| OpenAI Responses | /codex-easy/v1/responses |
Authorization: Bearer $ADMIN_API_KEY |
| Chat Completions | /codex-easy/v1/chat/completions |
Authorization: Bearer $ADMIN_API_KEY |
| Images | /codex-easy/v1/images/* |
Authorization: Bearer $ADMIN_API_KEY |
The Worker authenticates direct callers only against ADMIN_API_KEY, removes that caller credential, and sends CODEX_EASY_API_KEY or its alias upstream. These routes bypass Flask dashboard-user authentication, application-level request-size checks, RPM/TPM/daily limits, Flask request/rate-limit accounting, and request metrics. Use the Container-backed /v1/responses or /v1/chat/completions route with a codex-easy:<model> model ID when those controls are required.
The model catalog is API-key-group-specific; query /codex-easy/v1/models instead of relying on a hard-coded catalog. /v1/images/* works only for image-generation key groups. Raw request and response bytes are preserved for JSON, SSE, binary images, and multipart bodies, except that a JSON GPT Image generation missing moderation is normalized once to insert "low".
On the Codex Everywhere and LinkAPI raw OpenAI fast paths, a Responses prompt_cache_key remains in the untouched body and the Chat X-Grok-Conv-Id header is forwarded. For Grok requests, xAI recommends those fields with a stable conversation ID to improve cache routing. They do not guarantee a cache hit; caching remains upstream behavior and stable request prefixes still matter. Generation POSTs are single-attempt, and this proxy does not provide idempotency.
A Codex Everywhere key belongs to one group, so each pool is its own provider with its own Worker secret. All pools use https://codex-everywhere.com. Model IDs take the pool's prefix, such as ce-gpt-pro:gpt-6.1-sol.
| Provider | Secret | Models | Price |
|---|---|---|---|
ce-gpt-plus |
CODEX_EVERYWHERE_API_KEY_GPT_PLUS_POOL |
GPT-6 Astra, 6.1 Sol, Sol, Luna; GPT-5.6 | 0.03× OpenAI |
ce-gpt-pro |
CODEX_EVERYWHERE_API_KEY_GPT_PRO_POOL |
Same as Plus | 0.05× OpenAI |
ce-grok-heavy |
CODEX_EVERYWHERE_API_KEY_GROK_HEAVY |
Grok 4.7, 4.6, 4.5 | 0.06× xAI |
ce-claude-kiro-cheap |
CODEX_EVERYWHERE_API_KEY_CLAUDE_UNSTABLE |
Claude Opus 5.5 and earlier | 0.045× Anthropic |
ce-claude-kiro |
CODEX_EVERYWHERE_API_KEY_CLAUDE_KIRO |
Claude Opus 5.5 and earlier | 0.08× Anthropic |
ce-claude-max |
CODEX_EVERYWHERE_API_KEY_CLAUDE_MAX |
Claude Fable 5.1 and the rest; Claude Code only | 0.24× Anthropic |
ce-image |
CODEX_EVERYWHERE_API_KEY_GPT_IMAGE |
GPT Image 2 and 2.5 Sunburst and Flare | $0.015 per image |
The GPT and Grok pools accept Chat Completions and Responses, and can be auto:intelligence candidates. The Claude pools accept Anthropic Messages; unified /v1/chat/completions requests to them are translated to Messages, and they are not intelligence candidates. ce-image is an image relay. Raw routes such as /ce-gpt-pro/v1/responses and /ce-claude-kiro/v1/messages accept only each protocol's documented paths, and each pool's /v1/models lists its own key's catalog. A pool with no secret is skipped. CE answers 403 INSUFFICIENT_BALANCE while the account has no credit.
Each pool can hold spare keys under the same name with _1, _2 and so on, such as CODEX_EVERYWHERE_API_KEY_GPT_PRO_POOL_1. Requests use the base key first, then the spares in number order. A key that CE refuses (401, 402 or 403) rests for five minutes, and a rate-limited key (429) for one minute. In auto:intelligence a refused key retries the same model on the next key within the request, because a refusal generates nothing. Other routes switch on the next request. The rests are kept per Container process.
CE's GPT API runs on Codex and uses Codex's default system prompt unless a request sets one (CE docs). Check how a pool treats a system message before adding it to a chain.
The Gemini via Antigravity group serves Google's native API at /v1beta and is not integrated.
Set KIMI_CODE_API_KEY as a Worker secret. The upstream base is fixed to https://api.kimi.com/coding/v1.
| Operation | Proxy route | Execution path | Caller authentication |
|---|---|---|---|
| Model catalog | /kimi-code/v1/models |
Worker-local configured catalog | Authorization: Bearer $ADMIN_API_KEY |
| Chat Completions | /kimi-code/v1/chat/completions |
Edge auth, then Container raw pass-through | Authorization: Bearer $ADMIN_API_KEY |
Kimi Code generation is Chat Completions only in this integration. Use model k3 on the raw route. Send reasoning_effort: "max" for K3's strongest reasoning setting, and keep prompt_cache_key stable for stable conversation prefixes when seeking upstream cache affinity. Cache hits are not guaranteed.
The Worker authenticates the caller with ADMIN_API_KEY before forwarding valid chat requests to Flask. Flask replaces the caller credential with KIMI_CODE_API_KEY and preserves the OpenAI-compatible request/response stream. The raw route bypasses unified request-size checks, rate limits, and accounting; use /v1/chat/completions with model kimi-code:k3 when those controls are required. Chat generation is single-attempt to avoid duplicated work and billing.
LINKAPI_KEY is used both by the direct Worker fast path and the Container
fallback. LINKAPI_API_KEY is supported as an alias, but LINKAPI_KEY is
preferred. The optional, non-secret LINKAPI_BASE_URL variable defaults to
LinkAPI's HK direct origin, https://hk.linkapi.ai, and is restricted to the
Worker's allowlist of official LinkAPI hosts; arbitrary HTTPS origins are
rejected.
Use the deployed Worker URL as the base and keep the native path after /linkapi:
| Protocol | Route | Caller authentication |
|---|---|---|
| Claude Messages | /linkapi/v1/messages |
x-api-key: $ADMIN_API_KEY and anthropic-version |
| OpenAI Responses | /linkapi/v1/responses |
Authorization: Bearer $ADMIN_API_KEY |
| OpenAI compatible | /linkapi/v1/chat/completions |
Authorization: Bearer $ADMIN_API_KEY |
| OpenAI model catalog | /linkapi/v1/models |
Authorization: Bearer $ADMIN_API_KEY |
| OpenAI image generation | /linkapi/v1/images/generations |
Authorization: Bearer $ADMIN_API_KEY |
| OpenAI image editing | /linkapi/v1/images/edits |
Authorization: Bearer $ADMIN_API_KEY |
| Gemini native | /linkapi/v1beta/models/{model}:generateContent |
Prefer x-goog-api-key: $ADMIN_API_KEY; ?key=$ADMIN_API_KEY is compatibility-only |
These direct routes authenticate only with ADMIN_API_KEY. They intentionally bypass Flask dashboard-user authentication, application-level request-size checks, RPM/TPM/daily limits, Flask request/rate-limit accounting, and request metrics. The Worker removes the caller credential and sends LINKAPI_KEY upstream. When the Flask controls are required, use the Container-backed /v1/chat/completions endpoint with a linkapi:<model> model ID, or /v1/images/generations with an image-capable linkapi:<model> model ID.
Gemini clients should prefer x-goog-api-key. Query-string ?key= remains available for compatibility, but it places the caller key in the URL, where clients and intermediaries may retain it, even though automatic Worker invocation logs are disabled.
Request and response bodies are streamed without changing native SSE event frames. A JSON GPT Image generation missing moderation is normalized once to insert "low"; explicit auto and low values remain unchanged. The raw OpenAI routes preserve Responses prompt_cache_key and Chat X-Grok-Conv-Id; for Grok, xAI recommends those shapes for cache routing, but neither this proxy nor LinkAPI guarantees a cache hit. Generation POSTs are single-attempt: the proxy never retries them and does not provide idempotency, because a retry can duplicate upstream work and billing. A caller should retry only when the selected upstream protocol and endpoint explicitly document an idempotency guarantee.
LinkAPI's live pricing page lists gpt-image-2-c for /linkapi/v1/images/generations and /linkapi/v1/images/edits. Use native Gemini generateContent for Gemini Flash Image models such as gemini-2.5-flash-image. See the LinkAPI image guide for request examples and live-catalog guidance.
Set AIHUBMIX_API_KEY as a Worker secret. The committed non-secret variables
use https://aihubmix.com first and https://api.inferera.com as the trusted
backup. The Worker forwards all /aihubmix/* routes to the Container so Flask
can enforce the path allowlist, preserve multipart image edits, normalize
Gemini and Doubao image responses, and record provider metrics.
| Operation | Route |
|---|---|
| Live provider catalog | /aihubmix/v1/models |
| Native chat | /aihubmix/v1/chat/completions |
| Native GPT image generation | /aihubmix/v1/images/generations |
| Native GPT image edit | /aihubmix/v1/images/edits |
| Unified GPT, Gemini, or Doubao image generation | /v1/images/generations with aihubmix:<model> |
See the AIHubMix guide for the seeded free catalog, examples, and the bounded backup-origin policy.
The Container-backed normal /v1/chat/completions route safely preprocesses GLM-5.2 context above GLM_AUTO_OPTIMIZE_TRIGGER_TOKENS (8,000 by default). It only replaces older high-confidence image-prompt blocks, preserving surrounding story text, the newest prompt, protected directives, recent turns, media, tools, and reasoning structures. It makes no extra provider call and never summarizes ordinary text. /v1/responses and provider-specific routes remain unchanged.
Prompt classification uses a bounded process-local cache keyed by SHA-256. Values contain prompt-span offsets only, not conversation text or responses. Configure CONTEXT_ANALYSIS_CACHE_ENABLED, CONTEXT_ANALYSIS_CACHE_TTL_SECONDS, and CONTEXT_ANALYSIS_CACHE_MAX_ENTRIES; each Container keeps its own cache.
POST /optimize/v1/chat/completions remains the explicit route for every unified model and uses the unified namespace, authentication, request limits, rate limits, accounting, and metrics.
The default deterministic mode does not call another model. Once the request exceeds optimization.trigger_input_tokens, it can replace older high-confidence detailed image prompts while retaining the newest detailed image prompt and safety-critical structures such as system/developer messages, recent turns, media exchanges, tool chains, and reasoning/thinking blocks. target_input_tokens defaults to 75% of the selected provider's configured prompt limit; both token counts are provider-neutral byte-based estimates rather than exact tokenizer or billing usage.
summarize mode requires optimization.summary_model in provider:model form. If safe older plain-text history must be condensed, the route makes exactly one additional billed and separately rate-limited summary call. One-attempt summary transport is limited to codex-easy, kimi-code, and linkapi; calls use a bounded 45-second read timeout and two-slot per-process pool, and failure or local saturation falls back without retry. The response body and SSE bytes remain upstream-native, while X-MultiLLM-Optimization*, X-MultiLLM-Estimated-Input-*, X-MultiLLM-Image-Prompts-Compacted, X-MultiLLM-Messages-Summarized, X-MultiLLM-Optimization-Cache-Hits, X-MultiLLM-Optimization-Cache-Misses, and X-MultiLLM-Summary headers report what happened and are exposed through CORS.
Summary models are same-provider by default. Eligible historical user/assistant plaintext is transmitted verbatim to the selected summary provider, so it may contain sensitive text. Prefer the same provider, remove secrets, and use preserve_message_indices for history that must stay out of the summary. Cross-provider transfer requires the explicit optimization.allow_cross_provider_summary: true disclosure opt-in; otherwise the request returns 400. The digest is inserted as an untrusted historical assistant message.
OPTIMIZER_MAX_REQUEST_BYTES controls the pre-parse optimizer ingress cap (16 MiB by default), while the transformed final body must still fit the selected provider's MAX_REQUEST_BYTES. OPTIMIZER_SUMMARY_TIMEOUT_SECONDS defaults to 45 seconds and is clamped to 5-120 seconds. The target model/key, output cap, and RPM/daily capacity are checked before a paid summary.
If you only use the Gemini/Gemma provider paths, set:
npx wrangler secret put GEMINI_API_KEYIf you also use the googleai provider that depends on Google Cloud auth, set:
npx wrangler secret put GOOGLE_APPLICATION_CREDENTIALS_JSON
npx wrangler secret put PROJECT_ID
npx wrangler secret put LOCATION
npx wrangler secret put GOOGLE_ENDPOINTGOOGLE_APPLICATION_CREDENTIALS_JSON should contain the full service account JSON as a single secret value. The container entrypoint writes it to /tmp/google-credentials.json and sets GOOGLE_APPLICATION_CREDENTIALS. The app uses that JSON directly through google-auth; gcloud is only a local fallback when no service-account JSON/path is configured.
Pushes to main deploy through GitHub Actions once ci passes: D1 migrations first,
then the Knowledge Worker, then this Worker and its Container. See
How deploys work for the pipeline, the
one-time token and secret setup, and the choice between it and Workers Builds.
For a manual deploy, use the ordered script, never a bare wrangler deploy:
npm run deploy # scripts/deploy.shIt applies intelligence-migrations/ to the multillm-intelligence D1 database,
stops unless every migration file is recorded as applied, then deploys the
Knowledge Worker (wrangler.knowledge.jsonc) and finally this Worker and its
Container. Extra arguments go to the final deploy, for example
npm run deploy -- --containers-rollout immediate for a one-shot rollout of a
changed Container image. /ready answers 503 with d1_schema_missing and the
missing tables until the schema is migrated; npm run cf:status shows it with the
deployed versions and pending migrations.
Alerts. Worker logs are sampled at 100% (head_sampling_rate: 1): the Worker
writes little besides structured failure lines such as account_storage_failed,
knowledge_edge_account_lookup_failed and d1_schema_missing, about 50,000 events
a month at current traffic. In Observability, save queries on those events and
on HTTP 5xx for multillm-proxy and multillm-knowledge, and create a
notification (or a monitor on /ready) so an outage pages someone instead of being
found by users.
The final wrangler deploy builds the container image from Dockerfile, pushes it
to Cloudflare, and deploys the Worker, Durable Objects and container binding.
- Worker entry:
cloudflare-worker.mjs - Direct Worker route:
/codex-easy/* - Direct Worker route:
/kimi-code/* - Direct Worker route:
/linkapi/* - Direct Worker route:
/v1/roleplay - Durable Object class:
RoleplaySession - Durable Object / Container class:
MultiLLMProxyContainer - Direct Worker route: signed
/v1/media/files/*links, served from R2 - Workflow class:
MediaJobWorkflow, and the optionalMEDIA_BUCKETandMEDIA_JOBSbindings (media storage) - Container port:
8080 - Gunicorn entrypoint:
app:create_app() - Gunicorn default:
GUNICORN_WORKERS=1,GUNICORN_THREADS=8
- The deployment is pinned to a single named container instance (
primary), andwrangler.jsoncsetsmax_instances=1. Control-plane state is shared through D1, so more instances are possible; see what running more than one requires. - Container disk is ephemeral. Dashboard accounts (with
AUTH_STORAGE_BACKEND=d1), automatic routes, rate-limit usage, login throttling, model-disable overrides, free-route cooldowns, workbench records and provider catalog snapshots are stored in D1 (details) and survive Container restarts. KeepADMIN_API_KEYas the bootstrap credential; it authenticates even while D1 is unavailable.