The supported Cloudflare target is a hybrid Worker plus Container deployment. Most routes need Flask, normal Python packages, SQLite file access, and Gunicorn, so they run in the Container. Roleplay under /v1/roleplay, native LinkAPI traffic under /linkapi/*, and Codex Everywhere OpenAI traffic under /codex-easy/* take direct Worker paths. Kimi model discovery is Worker-local; valid Kimi chat is authenticated at the edge and streamed through the Container because Kimi's edge rejects Worker-origin egress.
wrangler.jsonc deploys:
- Worker entry:
cloudflare-worker.mjs - Container class:
MultiLLMProxyContainer - Container image:
Dockerfile - Durable Object binding:
MULTILLM_PROXY_CONTAINER - Container instance count:
max_instances=1 - Roleplay Durable Object:
RoleplaySession, one instance per session - Observability: enabled
OpenCode routes are authenticated at the Worker edge and then forwarded
through the Container. OpenCode's edge rejects Worker-origin HTTP signatures
with Cloudflare error 1010, while the Container's normal HTTP client is
accepted. Roleplay sessions use the same Container egress path whenever an
OpenCode model is selected; other configured roleplay providers remain direct
Worker fetches.
POST /v1/roleplay and its OpenAI-compatible
/roleplay/v1/chat/completions alias keep authentication, validation, session
state, memory, and adaptive selection in the Worker. The Worker authenticates
the caller with ROLEPLAY_API_KEY or the bootstrap ADMIN_API_KEY, validates
a bounded JSON request, and sends it to the session's ROLEPLAY_SESSION
Durable Object. An OpenCode generation wakes the Container only for accepted
upstream egress; other providers remain direct Worker fetches. Each session
stores recent dialogue, a compact continuity digest, idempotency keys, and
per-model EWMA latency/reliability statistics.
Production GLM routing starts generic requests on NanoGPT
z-ai/glm-5.3-flash, followed by uncensored Flash and GLM-5.2 inside the 1x
subscription pool. Full GLM-5.3 is excluded from automatic subscription
routing and remains available through the explicit roleplay:5.3 selector.
roleplay:glm-speed ranks the 1x pool by observed streaming TPS after bounded
exploration. OpenCode and NavyAI remain later provider tiers. LinkAPI is
Kimi-only and OpenRouter is omitted from the roleplay chain. Explicit 400,
401, 402, 403, 404, 413,
415, 422, 429, and 503 responses trigger automatic fallback.
Transport errors and other 5xx responses stop because their generation
outcome is ambiguous.
At the local compaction threshold, the selected model decides whether older dialogue should become a bounded continuity digest. At the hard input limit, compaction is required; failure stops before final generation. This can create one additional billed request, but never silently drops history.
JanitorAI-origin roleplay streams ignore the client's numeric output ceiling.
When a provider reports finish_reason: length, the Worker joins as many as
ROLEPLAY_MAX_AUTO_CONTINUATIONS continuation legs (eight by default) into one
SSE response with a single terminal [DONE]. A provider finish_reason: stop
uses the separate ROLEPLAY_MAX_OUTPUT_CONTRACT_REPAIRS budget (one by default)
when a caller-mandated response lacks visible story content or its final
IMAGE PROMPT: block lacks canonical required fields. An image-only leg is
discarded and regenerated from the beginning; current and legacy labels are
aliases. No-progress repair text is suppressed, multiple image blocks are
collapsed to one, and only the cleaned answer is stored.
Use GET /v1/roleplay/models for configured tiers and
GET /v1/roleplay/metrics?session_id=... for bounded per-session routing
metrics. Neither route returns stored dialogue. See the roleplay guide.
The Worker routes Container-backed app traffic to the named primary container. Keeping one container instance avoids stale auth/model state from multiple Python worker processes until durable storage is introduced.
Configure the preferred upstream credential as a Worker secret. The existing CODEX_API_KEY secret name is supported as a fallback alias.
npx wrangler secret put CODEX_EASY_API_KEYThe upstream origin is fixed to https://codex-easy.ai. If a client appends /v1, configure $PROXY_BASE_URL/codex-easy; if it expects /v1 in the configured base, use $PROXY_BASE_URL/codex-easy/v1.
| Operation | Direct Worker route | Caller credential |
|---|---|---|
| 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 fast path accepts only the bootstrap ADMIN_API_KEY and replaces it with CODEX_EASY_API_KEY or its alias before contacting Codex Everywhere. It bypasses 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.
Catalog results are specific to the API-key group, so discover current model IDs through /codex-easy/v1/models. Image routes work only for image-generation key groups. The Worker preserves raw JSON, SSE, binary, 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, it retains a Responses prompt_cache_key and forwards Chat's X-Grok-Conv-Id. For Grok requests, xAI recommends these fields for cache routing, but neither the proxy nor either provider guarantees a cache hit. Generation POSTs are single-attempt; the proxy does not provide idempotency.
Configure the upstream credential as a Worker secret:
npx wrangler secret put KIMI_CODE_API_KEYThe upstream base is fixed to https://api.kimi.com/coding/v1.
| Operation | Proxy route | Execution path | Caller credential |
|---|---|---|---|
| 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 k3 on the raw route, reasoning_effort: "max" for K3's strongest reasoning setting, and a stable prompt_cache_key when the conversation prefix is stable. Cache affinity is upstream behavior and a hit is not guaranteed.
The Worker validates ADMIN_API_KEY before waking the Container. It serves model discovery locally; for valid chat, Flask removes the caller credential and sends KIMI_CODE_API_KEY upstream. The raw route bypasses unified request-size checks, RPM/TPM/daily limits, and accounting. Use /v1/chat/completions with model kimi-code:k3 when those controls are needed. Requests and streams are preserved, and generation is single-attempt to avoid duplicate work and billing.
Configure the upstream credential as a Worker secret:
npx wrangler secret put LINKAPI_KEYLINKAPI_BASE_URL is an optional, non-secret Worker variable. It defaults to
LinkAPI's HK direct origin, https://hk.linkapi.ai, for both Worker and
Container requests. It is restricted to the Worker's allowlist of official
LinkAPI hosts; arbitrary HTTPS origins are rejected. LINKAPI_API_KEY remains
a compatibility alias, but LINKAPI_KEY is the preferred name.
Native routes keep their upstream protocol shape:
| Protocol | Worker route | Caller credential |
|---|---|---|
| Claude Messages | /linkapi/v1/messages |
x-api-key: $ADMIN_API_KEY |
| 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 |
The fast path accepts only the bootstrap ADMIN_API_KEY and replaces the caller credential with LINKAPI_KEY before contacting LinkAPI. It intentionally bypasses Flask dashboard-user authentication, application-level request-size checks, RPM/TPM/daily limits, Flask request/rate-limit accounting, and request metrics. When those 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.
The Worker streams request and response bodies without translating native SSE frames. A JSON GPT Image generation missing moderation is normalized once to insert "low"; explicit auto and low values remain unchanged. Its 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. The proxy never retries generation POSTs and does not provide idempotency, avoiding accidental duplicate generations and billing. A caller should retry only when the selected upstream protocol and endpoint explicitly document an idempotency guarantee, using its own retry policy.
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 complete examples.
Configure the upstream credential as a Worker secret:
npx wrangler secret put AIHUBMIX_API_KEYwrangler.jsonc sets AIHUBMIX_BASE_URL=https://aihubmix.com and
AIHUBMIX_BACKUP_BASE_URL=https://api.inferera.com. Both values are validated
against the fixed official-host allowlist before use. The Worker forwards
/aihubmix/* into Flask rather than contacting AIHubMix directly, preserving
the native multipart body and the unified image translators.
Use /aihubmix/v1/models for the live provider catalog. Use
/v1/images/generations with aihubmix:gpt-image-2-free,
aihubmix:gemini-3.1-flash-image-preview-free, or
aihubmix:doubao-seedream-4-0 for the normalized OpenAI Images interface. See
the AIHubMix guide for native routes and request examples.
Normal Container-backed GLM-5.2 requests on /v1/chat/completions safely preprocess context above GLM_AUTO_OPTIMIZE_TRIGGER_TOKENS (8,000 by default). Only old high-confidence image-prompt blocks are eligible; surrounding assistant story text, the newest full prompt, protected directives, recent turns, media, tools, and reasoning data remain intact. This automatic path never summarizes ordinary text or makes an extra provider request. /v1/responses and provider-specific routes remain unchanged.
The process-local prompt-analysis cache stores SHA-256 keys and prompt-span offsets only, never conversation text or generated replies. Configure it with CONTEXT_ANALYSIS_CACHE_ENABLED, CONTEXT_ANALYSIS_CACHE_TTL_SECONDS (300), and CONTEXT_ANALYSIS_CACHE_MAX_ENTRIES (2,048). Each Container has an independent cache.
POST /optimize/v1/chat/completions remains the explicit route for any unified provider:model and uses the normal Flask authentication, request-size, rate-limit, accounting, and metrics path.
The default deterministic mode makes no additional provider call. Above optimization.trigger_input_tokens, it can compact high-confidence older detailed image prompts while retaining the newest detailed image prompt, recent turns, system/developer instructions, multimodal exchanges, tool chains, and reasoning/thinking data. target_input_tokens defaults to 75% of the target provider's configured prompt limit. Reported token counts are provider-neutral byte-based estimates, not exact tokenizer usage or billing values.
summarize mode requires an explicit optimization.summary_model in provider:model form. When summarization is needed, it makes one additional billed and separately rate-limited summary call before the final request. Summary providers are restricted to codex-easy, kimi-code, or linkapi to keep transport single-attempt; calls use a bounded 45-second read timeout and two-slot per-process pool, and failed or capacity-denied summaries are not retried. Inspect X-MultiLLM-Optimization, X-MultiLLM-Optimization-Mode, X-MultiLLM-Estimated-Input-Before, X-MultiLLM-Estimated-Input-After, X-MultiLLM-Image-Prompts-Compacted, X-MultiLLM-Messages-Summarized, X-MultiLLM-Optimization-Target-Met, X-MultiLLM-Optimization-Cache-Hits, X-MultiLLM-Optimization-Cache-Misses, and X-MultiLLM-Summary headers for optimization metadata. Upstream JSON and SSE response bodies remain unchanged, and browser clients can read these headers through CORS.
The summary model must use the final model's provider by default. Eligible historical user/assistant plaintext is sent verbatim to that summary provider and can contain sensitive text. Prefer the same provider, remove secrets, and protect excluded history with optimization.preserve_message_indices. Cross-provider history transfer requires optimization.allow_cross_provider_summary: true; without that explicit disclosure opt-in the route returns 400. The bounded digest is inserted as an untrusted historical assistant message.
Set OPTIMIZER_MAX_REQUEST_BYTES to control the pre-parse ingress cap (default 16 MiB) and OPTIMIZER_SUMMARY_TIMEOUT_SECONDS for the summary read timeout (default 45 seconds, clamped to 5-120). The transformed final body still must satisfy the selected provider's body/prompt/output limits. The target model/key, output cap, and RPM/daily capacity are checked before a paid summary.
Container disk is ephemeral: it is reset whenever the Container sleeps (after 15 idle minutes) or a deploy replaces it. Durable state therefore lives outside it:
- D1 (
INTELLIGENCE_DB). Dashboard accounts and key hashes (withAUTH_STORAGE_BACKEND=d1, whichwrangler.jsoncsets), automatic routes, request usage for rate limits and daily budgets, login throttling, disabled models, free-route cooldowns, workbench profiles and comparison results, the last good provider model catalog, and the intelligence gateway's policy, reservations and integration keys. See control-plane persistence for how each is cached and what happens during a D1 outage. - Durable Objects. Roleplay sessions (
RoleplaySession) survive isolate and Container restarts, with a 30-day inactivity TTL by default.
The Worker still passes SQLite paths under /tmp (AUTH_DB_PATH,
RATE_LIMIT_DB_PATH, MODEL_REGISTRY_DB_PATH). With D1 they hold only this
Container's login-throttling record, which backs up the shared one, and nothing there
needs to survive a restart. Keep ADMIN_API_KEY configured as the bootstrap admin
key: it authenticates even while D1 is unavailable, and the direct LinkAPI and Codex
Everywhere fast paths authenticate against it because they do not wake the Container.
Apply migration 0006_control_state.sql before deploying code that uses it
(npm run deploy does). Until it exists, /ready reports the missing tables, the
Container keeps that state in memory with a logged warning, and admin saves to it
answer 503.
Each Container process keeps the following in memory only. It is rebuilt after a restart and is not shared between instances:
- NanoGPT key-pool health (the active key and keys cooling down after a rejection) and the NanoGPT speed-suffix breaker.
- Provider circuit breakers.
- Caches: responses, prompt analysis, provider usage figures, and verified API keys (up to 60 seconds).
- Dashboard metrics and the recent-request history.
- Groq per-key token accounting and the image-relay catalog refresh schedule.
- The in-memory side of the shared state: usage and cooldowns not yet sent to D1, and the cached copies described in control-plane persistence.
wrangler.jsonc keeps max_instances: 1, and the Worker sends every Container
request to one named instance (getContainer(env.MULTILLM_PROXY_CONTAINER, "primary")).
The shared D1 state makes more instances safe, but running them is a deployment
decision that needs:
- Migration
0006applied andAUTH_STORAGE_BACKEND=d1, so accounts and all control-plane state are in D1 rather than on one instance's disk. - The same
FLASK_SECRET_KEYandJWT_SECRETon every instance. They are Worker secrets, so this holds by default; dashboard sessions, CSRF tokens, video job IDs and the keyed identity hashes in D1 depend on them. - A Worker change that spreads requests, for example
getRandom(env.MULTILLM_PROXY_CONTAINER, N)from@cloudflare/containers, withmax_instancesraised to at least N. Raisingmax_instancesalone changes nothing. - Accepting per-instance behavior. Limits can briefly admit more than configured
(by what other instances admitted within one
RATE_LIMIT_SYNC_SECONDSinterval); each instance learns provider failures and NanoGPT key health on its own; metrics cover one instance each; a disabled model reaches the other instances within 30 seconds, and an account change made on another instance within 60. - Budgeting D1 load. Each busy instance makes one usage sync every
RATE_LIMIT_SYNC_SECONDS(5 by default), a cooldown refresh every 10 seconds while the free pool is in use, and a model-override read every 30 seconds.
npx wrangler secret put ADMIN_API_KEY
npx wrangler secret put FLASK_SECRET_KEY
npx wrangler secret put JWT_SECRETProvider secrets are optional and should be set only when used:
npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put ANTHROPIC_API_KEY
npx wrangler secret put ANTHROPIC_API_KEY_1
npx wrangler secret put OPENROUTER_API_KEY
npx wrangler secret put OPENCODE_GO_API_KEY
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 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 GROQ_API_KEY_1
npx wrangler secret put CHUTES_API_TOKENFor Vertex-style Google auth:
npx wrangler secret put GOOGLE_APPLICATION_CREDENTIALS_JSON
npx wrangler secret put PROJECT_ID
npx wrangler secret put LOCATION
npx wrangler secret put GOOGLE_ENDPOINTOptional dashboard single sign-on through Cloudflare Access needs a Worker secret and a few variables; see Dashboard single sign-on:
npx wrangler secret put CF_ACCESS_PROOF_SECRET # at least 32 random characters
# Variables: CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD, CF_ACCESS_ALLOWED_EMAILS,
# optionally DASHBOARD_SSO_ONLY=true and ADMIN_USERNAMES.The Worker verifies the Access token and forwards only a signed identity assertion to
the Container; CF_ACCESS_AUD stays on the Worker. Migration 0010 adds the
control_audit_events table behind the administrator audit log at /admin/audit.
Do not place API keys in wrangler.jsonc vars. Use Worker secrets.
NanoGPT's non-secret deployment default is subscription-only text routing via
NANOGPT_BILLING_MODE=subscription; wrangler.jsonc supplies the corresponding
subscription base URL to both the Worker roleplay path and the Container.
Local checks before a change reaches main:
npm ci
python scripts/validate_sqlite_schema.py
python scripts/check_static_secrets.py
python -m pytest -q
npm run test:worker
npm run test:integration
npm run cf:migrations:check
node scripts/key_data_preflight.mjs --check
node scripts/check_worker_bundle.mjs --config wrangler.jsonc --outdir /tmp/main-workerPushing to main deploys; see How deploys work. Never deploy
with a bare wrangler deploy: its preflight verifies prerequisites but does not apply migrations.
A push to main runs the ci workflow. When it passes, the deploy workflow
(.github/workflows/deploy.yml) deploys the commit that ci tested:
- Select the commit. The
gatejob skips the commit ifmainhas moved on, because the newer commit's run deploys both; an older commit never overwrites a newer one. Deploy runs never overlap (concurrency: deploy-production), and a newer queued run replaces an older queued one. Run workflow on thedeployworkflow redeploys the head ofmainonce ci has passed for it. - Apply migrations.
scripts/verify_d1_migrations.mjs --checkchecks the files, the job lists the pending ones,wrangler d1 migrations apply multillm-intelligence --remoteapplies them, and the remote check fails unless every file is recorded as applied. Any failure stops the run before new code ships. - Deploy the Knowledge Worker, then the main Worker. The Knowledge Worker goes
first because the main Worker binds it (
KNOWLEDGE_SERVICE) and may call what the new version adds. The main Worker and its Container go last, and only when the repository variableDEPLOY_MAIN_WORKERistrue(see Workers Builds). Before upload, the main build verifies the migrated account schema, required bindings and secret names, and the private Worker's tagged expansion capability. Main activation uses--containers-rollout=nonefirst, followed by the Container rollout. Each version is tagged with the full commit SHA, and its message links to the run. - Verify.
scripts/deploy_status.mjs --verifyretries for up to ten minutes until no migration is pending,GET /readyanswers 200 (every D1 table exists and the Container is healthy), the Knowledge Worker's live version carries the commit, and the main Worker's live version carries the commit with a bundle release fingerprint (BUILD_IDfromscripts/build_release_metadata.py) equal to the one computed from the checkout. When Workers Builds deploys the main Worker, the check waits for it to publish the same commit or fingerprint and only warns if it has not. - Summarize. The job summary lists the pending migrations, each step's outcome, the deployed state and every check.
The job installs dependencies from the frozen lockfile exactly as the cloudflare-build
ci job does, so Wrangler is the locked version, and every action is pinned to a commit
SHA.
Migrations are applied before the code that uses them. The main build refuses upload until they are recorded and the required account columns exist. The schema must still support old and new code during rollout:
- Make only additive changes: new tables, new nullable or defaulted columns, new
indexes, using
IF NOT EXISTS. Add each new table toREQUIRED_D1_TABLESinworker/d1-schema.mjsso/readyreports it until it exists. - Name files
NNNN_lowercase_words.sql, numbered from0001without gaps or duplicates. When two branches add the same number, the one merged second renumbers its file to the next free number; ci fails until it does. Never rename an applied migration: Wrangler tracks them by file name and would apply it again. - To drop or rename a table or column, first ship code that no longer uses it, then add
a migration containing a line
-- destructive-migration: <reason>. Without that linenpm run cf:migrations:checkfails in ci.
Workers Builds is connected to multillm-proxy and runs npx wrangler deploy on every
push to main, without migrations and without waiting for ci. If it and the deploy
workflow both deploy the main Worker, they race. Choose one option.
(a) GitHub Actions deploys everything (recommended). Migrations always land before the code, and nothing ships unless ci passed.
- In the Cloudflare dashboard, open Workers & Pages → multillm-proxy → Settings →
Builds and select Disconnect. To keep builds (and preview builds of other
branches) as versions that are never promoted, set the deploy command to
npx wrangler versions uploadinstead; those versions do not update the Container image. - Then, in GitHub Settings → Secrets and variables → Actions → Variables, add the
repository variable
DEPLOY_MAIN_WORKERwith the valuetrue. Doing it after step 1 means no push is deployed by both. - Add Containers Edit to the API token, and run the
deployworkflow once from the Actions tab to deploy the current head ofmain.
(b) Keeping automatic promotion is not supported for the key-controls rollout.
Workers Builds can publish the main Worker and Container before migrations or the
private Worker, even when GitHub CI fails. Disconnect it or use wrangler versions upload
without automatic promotion, and deploy through scripts/deploy.sh. The preflight's
D1, secret-list and deployments-read permissions inside Builds have not been verified.
See the staged rollout and rollback procedure.
-
Create the API token. In the Cloudflare dashboard, go to Manage Account → Account API Tokens → Create Token, create a custom token, and limit it to this account:
Permission Needed for Workers Editor, scoped to the Workers multillm-proxyandmultillm-knowledge(legacy name Workers Scripts Edit)Deploying both Workers with their Durable Objects, Workflow and cron trigger; reading deployments, versions and the deployed bundle D1 Edit Applying migrations and reading the migration table Containers Edit Option (a) only: pushing the Container image and rolling it out The main Worker runs on
workers.devand the Knowledge Worker has no public URL, so no zone permission is needed; add Zone → Workers Routes → Edit for a zone only if you attach a route there. If a deploy is refused for the Knowledge Worker's Workflow or the account'sworkers.devsubdomain, widen the Workers scope to all Workers, still as Editor.deploy_status.mjsalso asks the Workers Builds API which commit it deployed; if the token cannot read builds, it compares the bundle fingerprint instead. Set an expiry date and rotate the token. -
Create the environment. In GitHub Settings → Environments, create
production. Optionally add Required reviewers to approve each deploy, and limit Deployment branches and tags tomain. -
Add the secrets
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_IDto theproductionenvironment (find the account ID). Only the deploy job uses that environment, the workflow passes the secrets only to the steps that call Cloudflare, and nothing prints them. Until they exist, every deploy run stops at its first step with a message pointing here. -
Choose option (a) or (b) under Workers Builds.
-
Optionally set the repository variable
DEPLOY_STATUS_URLto check/readyon another origin thanWORKBENCH_WORKER_URLinwrangler.jsonc.
npm run cf:status # for people
npm run cf:status -- --json # for agentsscripts/deploy_status.mjs prints each Worker's live version, where it came from and
the commit it was built from (its version tag, or the Workers Builds record), the local
checkout's commit and fingerprint, pending migrations and /ready. It needs
wrangler login or CLOUDFLARE_API_TOKEN; with CLOUDFLARE_API_TOKEN and
CLOUDFLARE_ACCOUNT_ID set it also reads the deployed bundle fingerprint and Workers
Builds commits. It prints no secrets. wrangler secret put creates a new live version,
which can show no commit until the next deploy.
Set D1_DATABASE explicitly to the database in both selected Worker configurations,
then run npm run deploy (scripts/deploy.sh). For another environment set
MAIN_WORKER_CONFIG, KNOWLEDGE_WORKER_CONFIG and DEPLOY_STATUS_URL as well. The
script checks migrations, applies them, deploys the private Worker, then the main
Worker with Container rollout disabled, then the Container. Preflight runs before
each main upload. From a clean checkout it tags both versions with the commit.
Extra arguments go to the final deploy. Never run it concurrently with another deploy.
Each release uses a fresh build-only image identifier. The command does not finish
successfully until application detail shows a newer Container version and image, and
/ready returns matching Worker/Container artifact fingerprints. "No changes" is not
rollout proof. See deployment verification.
Status reads also require --database or D1_DATABASE; alternate configs are selected
with --config and --knowledge-config. No default database is queried.
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.
Defaults:
GUNICORN_WORKERS=1
GUNICORN_THREADS=8
GUNICORN_TIMEOUT=120
GUNICORN_GRACEFUL_TIMEOUT=30With D1, more Gunicorn worker processes behave like more instances: each keeps its own ledger and shares usage through D1. Read State and Container Instances before adding either.
Use the Container-first rollback procedure until A/B rollback has passed real staging. A Worker-only rollback across earlier strict RPC decoders can fail unrestricted requests. Keep migrations and stored restrictions; A refuses controlled dispatch while preserving unrestricted and management access. Do not clear policies or roll back D1 migrations to make a rollback appear healthy.
- Cloudflare Containers overview: https://developers.cloudflare.com/containers/
- Cloudflare Containers lifecycle and ephemeral disk: https://developers.cloudflare.com/containers/platform-details/architecture/
- Cloudflare Containers environment variables: https://developers.cloudflare.com/containers/platform-details/environment-variables/
- Cloudflare Workers secrets: https://developers.cloudflare.com/workers/configuration/secrets/
- Cloudflare database connectivity: https://developers.cloudflare.com/workers/databases/connecting-to-databases/
- Deploying Workers from GitHub Actions: https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/
- Workers roles and API token permissions: https://developers.cloudflare.com/workers/authorization/workers/
- API token permission names: https://developers.cloudflare.com/fundamentals/api/reference/permissions/
- Workers Builds, including disconnecting it: https://developers.cloudflare.com/workers/ci-cd/builds/
- Deploying Containers: https://developers.cloudflare.com/containers/guides/deploy/
- D1 migrations: https://developers.cloudflare.com/d1/reference/migrations/