Skip to content

Latest commit

 

History

History
970 lines (825 loc) · 70.9 KB

api-reference.md

File metadata and controls

970 lines (825 loc) · 70.9 KB

API reference

ClawRouter serves discovery, proxy, session, and administration APIs from one Worker. This page lists the HTTP surface; deployment and resource configuration live in Deploy ClawRouter on Cloudflare, and state ownership and failure behavior live in Architecture.

Authentication

Proxy credentials use Authorization: Bearer <clawrouter-key>. Authentication resolves the issued credential and its policy from the serialized ACCESS_CONTROL Durable Object authority before any provider secret is used.

Browser session, playground, and OAuth routes require a verified Cloudflare Access session. Admin routes accept that verified session for configured admins or Authorization: Bearer <admin-token> against CLAWROUTER_ADMIN_TOKEN_SHA256. Admin status does not grant provider access, and provider access does not grant admin status. A pool-submission route accepts only its one-time scoped ticket; neither proxy nor admin credentials are interpreted there.

The Docker self-hosting profile has no Cloudflare Access identity. Its admin API uses the bearer token, and clients use normal proxy credentials.

The optional /private/v1/{models,catalog,responses} facade has its own pinned owner or isolated-workload authentication and never accepts generic proxy/admin credentials. See the private alias contract for exact paths, configuration, protocol limits, and isolation requirements. It is absent from public discovery.

Discovery and client routes

Method Path Purpose
GET /v1/health Service health, application version, and observability mode
GET /v1/providers Compiled provider snapshot
GET /v1/routes Compiled OpenAI-compatible, manifest, and native route catalog
GET /v1/models Model list scoped to the authenticated credential or session
GET /v1/catalog Authorized configured providers and operation/model/transport offers
GET /v1/me Proxy-credential identity and policy summary
GET /v1/usage Caller policy or principal budget and usage summary
GET /v1/pool Opt-in policy pool inventory, quota eligibility and seven-day grant/lane totals
GET /v1/key/inspect Proxy-credential verification and readiness status

Usage summaries, provider totals, and daily totals remain shared across each authorized policy. usage.personal on /v1/usage and /v1/session/usage repeats those summary, provider, and daily totals for the caller's own events only, using the same ownership rule as recent events below. usage.principals on /v1/admin/usage lists thirty-day summary totals per principal; events without a principal are grouped by credential ID (principalId: null). The budget follows the policy's configured policy or principal scope. Recent usage.events on /v1/usage and /v1/session/usage contain only events attributed to the authenticated principal. A service key without a principal sees only events with its exact credential ID and no principal. Unattributed historical events without that credential ID remain admin-only; reassigning a credential does not transfer a previous principal's event history. This tightens earlier releases' policy-wide recent-event visibility. Administrators use /v1/admin/usage for the complete audit; their personal session endpoint still returns only their own events. Retained request content remains admin-only.

GET /v1/catalog is the client integration contract. It reports scope (auth type, credential, principal) and observedAt. Only authorized configured providers appear; configured but unavailable rows remain inspectable. Static /v1/providers and /v1/routes describe registration, not caller eligibility. Use those endpoints for setup inventory; client catalogs can omit unconfigured providers.

nativeBaseUrl preserves the v1 route-location string: /v1/native/<provider>, or /v1 for Fusion, including in session catalogs. It is legacy location metadata, not authorization or transport eligibility. Consumers should select a matching eligible offer under the returned scope, rather than infer access from nativeBaseUrl, routes, or provider-wide readiness.

Each provider's offers identifies an endpoint, model (or an operation form without a selected model), route, transport, selected policy and generation, eligible, and an optional reasonCode. affordability is exact-covered, exact-blocked, or request-dependent: fixed tariffs can be compared with observed balances, while token-priced requests depend on their actual input and parameters. Free token counting and declared zero-price operations remain available at exhausted positive limits. A configured zero limit still blocks ordinary requests. Affordability describes the plain operation; request parameters and hosted tools are assessed again at dispatch. A form without a selected model remains request-dependent when an eligible declared model is available; it does not make a model-less or opaque request priced.

When a provider declares products, an eligible offer can also include productOffers: [{ id, productId }]. These references identify compatible declarations for that exact operation and the selected policy's active grant candidates. They do not select an account or prove a subscription entitlement. The provider's optional products array supplies each referenced product's id, displayName, commercialModel, and authenticationMethod. Missing fields mean no product annotation, not that the existing offer is unsupported. Blocked offers do not carry these references; dispatch remains authoritative.

The projection observes the actual principal's policy ledger and the provider ledger without reserving budget, selecting credentials, refreshing accounts, or probing upstream. Observations are advisory; HTTP dispatch and every WebSocket create authenticate, check current grants, and reserve budget independently. Policy order is chosen by grant/transport eligibility before price or budget; an exhausted selected policy never causes a switch to a richer policy. Within that policy, deterministic credential/configuration checks precede grant priority, so an unusable account cannot displace a configured sibling.

Session offers target HTTP playground routes only. A session catalog cannot certify an issued key's native or WebSocket access; fetch the catalog with that key. The same session projection appears in /v1/session's entitlements.catalog and in /v1/entitlements's catalog. Policies without tenantId now use the dispatch/ledger tenant default consistently during Access selection and discovery, instead of inheriting the session tenant only in those two views.

/v1/models and client-catalog.v1 share a scoped model projection. Authorized, configured model identity, capabilities, request parameters and supported native routes (including supported key-only WebSocket metadata) survive temporary grant cooldowns or quota exhaustion with a known recovery time. Retention requires a recovering grant that supports the exact operation and still passes credential, configuration, scope, stale-state, model and budget checks. A generic upstream_grant_pool_unavailable reason is not sufficient: authentication failures, denied grants, removed configuration and unsupported transports do not retain otherwise blocked metadata.

Retained inventory is not execution eligibility. Cooling offers remain eligible: false, affordability: "exact-blocked", with reasonCode: "upstream_grant_pool_unavailable"; provider executable and readiness continue to describe current availability. Inference still returns HTTP 429 with Retry-After until the earliest eligible grant recovery, or 503 without Retry-After when recovery is unknown. Grant-pool ownership stays unchanged: discovery never reopens environment authorization for an unavailable configured pool. Fusion's virtual model still requires concrete aggregator eligibility.

Fusion emits one HTTP clawrouter/fusion offer for the synthesizer's selected policy and generation. Its route is /v1/chat/completions for a key or /v1/playground/v1/chat/completions for a session. A sibling native or WebSocket operation cannot make that Chat offer eligible. Fixed-price observations account for the synthesizer first and shared adviser budgets; if only a subset of advisers may fit, the usable offer remains request-dependent. Unavailable advisers fail open. These observations do not replace request-time admission or the separate administrator preview of an unsaved Fusion configuration.

Proxy (including native), admin, and pool-submission route identifiers are decoded once. Invalid percent escapes or invalid percent-encoded UTF-8 return HTTP 400 with invalid_path_encoding, after applicable authentication checks. Semantic identifier and path validation still applies after decoding.

Proxy routes

Method Path Contract
POST /v1/chat/completions OpenAI-compatible chat routing
POST /v1/responses OpenAI Responses routing
POST /v1/decisions OpenAI Decisions: predicates, choices, and rubric scores
GET /v1/responses/<response-id> Retrieve or resume an originally authorized response
POST /v1/responses/<response-id>/cancel Bodyless cancellation on the original response owner
GET upgrade /v1/responses, qualified native Responses paths Authenticated, bounded Responses WebSocket sessions
POST /v1/embeddings OpenAI-compatible embeddings routing
POST /v1/messages Anthropic Messages routing
POST /v1/messages/count_tokens Anthropic token counting
manifest-defined /v1/proxy/<provider>/<endpoint> Manifest request, query, header, path, and auth mapping
manifest-defined /v1/native/<provider>/<provider-native-path> Provider-native request and response formats

OpenAI-compatible requests select a provider-qualified model in the request body, for example openai/gpt-4.1-mini. Native and manifest routes resolve the provider and endpoint from the compiled snapshot instead of accepting arbitrary upstream URLs.

Unified routes require matching OpenAI request and response formats. A provider's Chat or embeddings capability alone is insufficient: use its native route when the protocol differs, such as Cohere's /v2/embed with texts input.

Native routes preserve the selected provider's upstream model namespace. For example, openai/gpt-6-astra in an OpenRouter request remains an OpenRouter model identifier. Known models must support the selected endpoint. Caller-supplied unknown models require that endpoint's explicit modelPassthrough declaration; this preserves opaque model selection on the bundled model-provider routes without claiming model availability or copying another model's capabilities. Such models remain absent from discovery and unpriced unless the endpoint declares an applicable pricing reference. The local Chat endpoint explicitly retains its zero API charge. Either a policy or provider budget requires declared pricing or an explicit fixed request tariff before dispatch.

Opaque native IDs are preserved even when they begin with the provider's routing prefix. For example, use openrouter/free on the native OpenRouter Chat route; the same model on unified Chat is openrouter/openrouter/free.

Native Responses JSON and SSE routes include:

Provider ClawRouter path Upstream contract
Azure OpenAI /v1/native/azure-openai/openai/v1/responses Deployment name in model; endpoint and API key required; no inherited dated api-version
OpenRouter /v1/native/openrouter/v1/responses OpenRouter model identifier in model; bearer credential required; OPENROUTER_SITE_URL attribution optional

Azure's legacy deployment chat and embeddings routes still require AZURE_OPENAI_API_VERSION. The default azure-openai/deployment model is listed only when AZURE_OPENAI_DEPLOYMENT is configured; explicit native deployment routes remain available without that default. The placeholder Azure deployment and OpenRouter auto catalog entries do not attest a particular model's Responses support or price. See the upstream Azure Responses contract and OpenRouter Responses contract; operators must verify model access with their own provider account.

A manifest proxy request for Tavily looks like this:

curl "$CLAWROUTER_BASE_URL/v1/proxy/tavily/search" \
  -H "authorization: Bearer $CLAWROUTER_KEY" \
  -H "content-type: application/json" \
  --data '{"body":{"query":"openclaw"},"query":{"topic":"news"}}'

clawrouter/fusion is an optional virtual model on /v1/chat/completions. It fans a bounded text-only prompt out to configured adviser models and asks one configured synthesizer for the final response. Every subrequest uses normal policy, budget, readiness, retention, and usage-accounting paths. See Fusion routing.

Media request sizes

Claude Messages and token counting, OpenAI-format Responses (create, compact, input tokens) and Chat Completions accept up to 32 MB (32,000,000 bytes) through unified, native, and manifest routes (including the manifest envelope). Other JSON routes keep 8 MiB. Bodies within 8 MiB keep the existing JSON parser; above it the Worker authenticates before expanded parsing and applies structural limits. Bodies of 1 MiB or more lease twice their bytes from a 96 MiB per-isolate body budget through response completion or observed cancellation. Requests past the budget receive a nonbillable HTTP 503 with Retry-After: 1 before budget reservation or provider dispatch: Claude routes return {"type":"error","error":{"type":"overloaded_error","message":"…"}}, other routes error.code: server_overloaded. A conversation that resends its screenshots every turn fits roughly 60 text-heavy or 12 photo-heavy 2048 px screenshots; see what fits, and what does not for measured capacity and limitations, and Media JSON request limits for byte counting, transport copies, structural safeguards and platform caveats.

OpenAI Decisions

Use POST /v1/decisions with model: "openai/gpt-6-luna" and an ordinary ClawRouter API key whose policy allows that model. With OpenAI JavaScript SDK 7.30.0 or later, set baseURL to https://<router-host>/v1 and call client.decisions.create(...). Native clients may instead use base URL https://<router-host>/v1/native/openai/v1 with upstream model gpt-6-luna. The manifest route is POST /v1/proxy/openai/decisions with the request in the usual body envelope.

{
  "model": "openai/gpt-6-luna",
  "input": "The product arrived damaged.",
  "questions": [{
    "type": "predicate",
    "name": "damaged",
    "instructions": "Is the product described as damaged?"
  }]
}

The upstream Decisions API is a public beta for text/image input and named predicate, choice, or score questions (1–400 per request; choice questions accept 2–255 typed options). The full response, including model, usage, probabilities, confidence values, and per-question refusals, passes through unchanged. Inline PNG/JPEG/WebP and remote image URLs use the upstream input format; prompt-cache controls and safety_identifier also pass through. Upstream owns input validation and its error response. Catalog native facts describe the Decisions endpoint separately from Luna's conversational limits. It is a non-streaming JSON contract, not a Responses alias: no SSE, WebSocket, background lifecycle, or subscription/Codex transport is advertised. Only the declared Luna model is accepted.

Decisions uses its own endpoint-specific price card: $0.10 per million input tokens, including cached and cache-written input, doubled for the entire request above 272,000 input tokens. There is no output charge. Ordinary Luna Chat/Responses pricing is unchanged. Reservations include the questions and conservatively bound opaque image input; completed JSON settles from upstream usage. Missing, oversized, malformed, or interrupted usage retains the reservation. Policy-fixed tariffs retain precedence. Existing authentication, revocation, model allowlists, budgets, and request-retention policy apply. The bundled route uses OpenAI's default global API endpoint; regional processing endpoints/premiums are not configured by this feature.

Anthropic Messages pricing

Native, unified and manifest Anthropic Messages routes apply the published 2× fast-mode price on supported Opus models and the 1.1× US inference price across input, output, cache reads and cache writes. The multipliers compose.

Omitted or null inference_geo can inherit the upstream workspace default, so geo-capable models reserve at 1.1×: 10% more than global pricing, which can cause HTTP 402 near a budget boundary. Explicit global reserves at 1×. Settlement prefers response usage. Null modifier fields and response geography not_available are unreported: use the request's rate or the conservative admission rate (1.1× on geo-capable models, base on older models), not zero. SSE deltas cannot erase earlier reported modifiers with unreported values.

Omitted, null or auto service tier remains admitted at the standard rate, without rewriting the upstream request. Unsupported request modifiers return HTTP 400 pricing_required under hard budgets before dispatch. Explicit response priority, batch or unknown modifiers without a published tariff instead return HTTP 502 pricing_required; SSE emits an error frame in place of message_stop, possibly after HTTP 200 headers and partial content. The dispatched reservation is retained as unpriced_usage, not a provider-invoice claim. Legacy Priority accounts should request standard_only or use a fixed policy price; a failed response cannot undo paid work, and blind retries can pay again.

Hard-budget JSON responses and individual SSE events exceeding the 2 MiB price inspection bound also fail with pricing_required. Fixed-price contracts and soft/unmetered passthrough are unchanged; truly unknown soft/unmetered prices report unpriced_usage without inventing a charge. See Anthropic price modifiers for model support, sources and limitations.

Responses continuation contract

Responses HTTP/SSE and WebSocket routes preserve upstream previous_response_id and x-codex-turn-state bytes. The router binds returned identities to the caller's authorization scope, provider route, and credential owner before publishing them. Every continuation still checks current authorization, grant eligibility, and budgets. It cannot fail over to another account. Ordinary stateless requests keep their configured pool routing and failover behavior.

Owner-controlled token refresh preserves the binding. Replacing credentials or account identity, revoking a grant, changing the route, or losing the original owner requires a restart. HTTP 409 continuation_restart_required means resend full input without either continuation field. Unknown or expired identities are rejected before upstream dispatch, including environment/API-key routes. This also applies after upgrading: state created before this binding contract must be restarted with full input. Stateless requests and known environment/API-key continuations remain supported.

Bindings expire 30 days after first publication, independently of upstream state retention. Each authorization scope admits up to one million hashed identities; it never evicts a live binding to admit another. Binding-store failure or capacity exhaustion returns continuation_unavailable before headers, or terminates an already-started stream. The upstream call can still incur charges. Response IDs on WebSockets can appear before their binding commits; every binding and pricing evidence write finishes before the terminal frame and next same-lane dispatch. If binding fails, the socket closes with continuation_unavailable without delivering the terminal; unbound IDs require a restart with full input. IDs are limited to 256 UTF-8 bytes and turn state to 8 KiB. Output/frame size is not limited by HTTP identity observation, and raw identities and model output are not stored in this index.

HTTP/SSE and WebSocket requests with previous_response_id consume the parent's durable tool-pricing evidence before budget reservation. Only a final token_only fact allows ordinary token pricing, subject to the current request. Retained hosted fees or server work and unknown ancestry return pricing_required under either policy or provider monthly budgets unless a fixed policy tariff applies. Fixed tariffs and unmetered requests retain their existing behavior.

Deploy the proof producer before the consumer; upgrading does not establish that old writers have drained. Legacy, pending, corrupt, future-version, and opaque history remain unqualified, and a later proof cannot clear the unknown fact already captured by a request. Turn-state aliases establish routing affinity, not tool pricing. Full-input conversation and item_reference state, encrypted compaction witnesses, and complete compaction metering remain separate work; no compact route is added. See spend control.

Reusing a response identity belonging to another producer fails publication with continuation_unavailable; resend full input. This also applies to older claim-less writers encountering qualified-producer rows. Transient proof-finalization failure after a confirmed identity claim can preserve delivery and accounting while leaving evidence unqualified.

This contract covers previous_response_id and Codex turn state. Responses conversation selectors remain an unpinned, separate contract gap; do not rely on pooled account affinity for them. WebSockets also use the connection contract below.

HTTP endpoint deadlines start after preflight and end after response normalization, before continuation-header registration and body delivery. For streaming SSE requests, normalization includes bounded first-event inspection; for successful JSON responses, it ends at the response headers. These deadlines do not impose a total or idle timeout on body delivery. Caller cancellation remains active through EOF. Fusion advisers retain their separate consumption deadline. Caller cancellation, router deadline, and upstream or publication failure retain the first observed cause in usage receipts. The selected HTTP status stays separate from that outcome. An accepted HTTP rejection keeps its status when JSON or SSE error details cannot be read; later cancellation still ends delivery. A parsed terminal usage event retains its measured charge if delivery later stops; dispatched work without final usage retains its estimate. This does not guarantee that every transport reports an idle client disconnect.

HTTP cancellation diagnostics

Cancellation accounting starts when the runtime reports an ingress abort or response-body cancellation. A client-local abort alone does not guarantee a prompt server notification or a receipt within ten seconds for an indefinitely idle HTTP response.

pnpm test:scripts includes the seven affirmative deadline and cancellation cases: progressing JSON/SSE delivery, initial-response deadlines, active-delivery cancellation, and cancellation observed after independently delayed JSON output. Each cancellation case requires upstream shutdown, one receipt, and settlement in both real budget ledgers. The strict idle-disconnect reproduction is explicit:

pnpm diagnostic:http-idle-disconnect

That command replays the original eight-case sequence, including the final whitespace-only response. It retains every strict assertion and exits nonzero when the limitation reproduces. Its nested entry point is outside the default test/*.test.mjs script-test glob; no result is suppressed or treated as a pass.

At commit 750a4093c9685829e9423f124c56022e7a2ce838, the hosted diagnostic run used Node 24.21.0, Miniflare 5.20260918.0-alpha and workerd 1.20260918.1. The delayed-output case received HTTP 200/gzip headers and zero decoded body bytes before the client aborted at 1.048 seconds. Independent payload output started at 2.044 seconds; ingress abort followed at 2.452 seconds, before EOF. It recorded one 200/client_error receipt with unknown tokens and a fixed charge of 7 micros in both ledgers. The unchanged whitespace-only case recorded no ingress abort or receipt, and both reservations remained unsettled during the observation window. That limitation remains unresolved.

The pinned KJ disconnect contract allows detection to remain pending without a write; its HTTP implementation discusses the half-close tradeoff. Node 24.21.0 Fetch abort reaches HTTP/1 socket destruction, whose ordinary-close path is distinct from reset. The fixture does not establish physical FIN/RST behavior, compression causality, or deployed HTTP/2 behavior. No idle or total-delivery timeout is added.

Background Responses

Declared OpenAI API Responses routes support durable recovery for background: true JSON or SSE creation when the selected transport supports creation, retrieval and cancellation. Retrieve with GET /v1/responses/<response-id>; resume SSE with stream=true&starting_after=<last-sequence-number>. Resumption requires an originally streamed background create. Cancel with bodyless POST /v1/responses/<response-id>/cancel. These controls also use the native /v1/native/openai/v1/responses/... routes. Manifest controls use responses_retrieve and responses_cancel: provide pathParams.response_id and an empty body in a POST envelope, or response_id in the retrieval URL. Repeated include[] values are preserved; manifest query arrays are supported.

Controls reauthenticate the original policy scope and credential route. They do not select another account or reserve new budget. If creation used OpenAI-Organization or OpenAI-Project, public controls must resupply the same headers to reconstruct the original route pin. Each is limited to 256 printable ASCII bytes. The collector alone temporarily retains these headers. The initial declared create path has no caller path parameters; create options belong in JSON, and caller-supplied create query parameters are rejected before reservation or generation dispatch when durable recovery applies. Foreground and creation-only transport query behavior is unchanged.

Creation keeps the ordinary policy and grant selection. A transport that supports only creation retains request-bound accounting: observed usage settles that request, and missing usage retains its conservative estimate. It receives no recovery locator, job or collection polling. Permitted creation failover keeps that accounting mode even if an alternate supports the full lifecycle. A full-capable initial transport with invalid control configuration fails before reservation; configuration failure does not downgrade it to request-bound accounting.

Verified browser sessions use the corresponding /v1/playground/v1/responses/... or /v1/playground/proxy/openai/<endpoint> controls. Existing response ownership must match exactly one currently authorized policy scope. Ambiguous, revoked or replaced ownership is rejected rather than falling back to another policy. After route preparation, controls recheck the original key, policy generation, policy tenant and current user bindings before upstream admission. Key rotation invalidates an already queued control. Access checks use current stored groups; local logout is checked separately against the session store. Cloudflare Access retains the verified assertion's expiry, without claiming live IdP revocation. A revocation after admission does not cancel an already dispatched control or the router's accounting collection for an admitted generation.

curl "$CLAWROUTER_BASE_URL/v1/responses" \
  -H "authorization: Bearer $CLAWROUTER_KEY" \
  -H "content-type: application/json" \
  --data '{"model":"openai/gpt-6-astra","input":"Explain durable accounting.","background":true,"store":false}'
curl "$CLAWROUTER_BASE_URL/v1/responses/$RESPONSE_ID" \
  -H "authorization: Bearer $CLAWROUTER_KEY"
curl -X POST "$CLAWROUTER_BASE_URL/v1/responses/$RESPONSE_ID/cancel" \
  -H "authorization: Bearer $CLAWROUTER_KEY"

Keep response.id for controls; for streaming resumption, also keep the last observed sequence_number. The upstream background contract permits store: false with temporary polling retention of roughly ten minutes. The router's one-hour collection limit is not an upstream retention guarantee. Cancellation is idempotent upstream; accepting a cancellation request is not proof of final usage or a refund. Existing creation-only ChatGPT subscription transport admission is preserved; this does not prove upstream acceptance or add retrieval/cancellation capabilities to that transport. This feature does not establish native Codex background adoption; WebSocket behavior remains separate below.

Creation responses carry x-clawrouter-background-recovery after a durable admission is attempted, including when its acknowledgement is lost. Its value is <64-hex-scope-digest>.bg_<32-hex-job-id>. It is a locator, not authentication, completion proof or a settlement receipt. It is never forwarded upstream. An absent record after an admission failure does not prove settlement.

Administrators use POST /v1/admin/usage/recovery behind the existing admin and same-origin boundary. The JSON envelope is closed:

Action Exact fields Result
Inspect action: "inspect", locator Sanitized record and independent sink dispositions
List action: "list", scope, optional after job ID At most 16 records from one scope, with nextAfter
Replay action: "replay", locator Retry only an unresolved, frozen financial receipt before its original deadline

Replay never recreates the upstream generation. The API exposes original deadlines, event/request IDs, amount/basis and independent financial/usage acknowledgements; it omits credentials, raw response IDs and retained route headers. expired_by_retention closes only usage delivery after the 30-day history window. It does not establish a financial acknowledgement or an available history row. HTTP 404 means absent/retired, HTTP 503 means unavailable, and neither means paid. After a durable admission, retain a recovery-aware Worker and owner while obligations or inventory completeness are unknown. See spend control for recovery bounds, the rollback floor and conservative accounting.

WebSocket contract

Send authenticated upgrades to /v1/responses or /v1/native/openai/v1/responses. Unified upgrades select their model on the first response.create, so an HTTP 101 alone does not prove model access or upstream readiness. Every create rechecks credential, policy, provider, grant, retention, and budget before dispatch. The connection pins its provider route and grant revision; changing either requires a new connection. Create admission and grant selection share HTTP's isolate-local snapshot (fresh 10 s, maximum age 60 s by default); CLAWROUTER_HOT_CACHE_MAX_AGE_SECONDS=0 restores authoritative checks. A pinned grant that is missing, ineligible or changed returns 409 upstream_grant_changed, or the continuation restart error below. Local selection never substitutes another grant on an existing socket. Credential authentication errors take precedence over per-create model or route selection errors, as they do for HTTP requests.

Response IDs and response.metadata turn state are bound to each create's fresh authorization scope and actual upstream owner before the terminal frame is forwarded and before the next same-lane dispatch. Earlier frames, including response.created, metadata and deltas, may expose IDs before binding commits. Codex can then reconnect with client_metadata["x-codex-turn-state"] or fall back to HTTP with the same turn state and account. Conflicting header and metadata tokens are rejected; every supplied continuation identity must name the same owner. Each socket retains at most 32 recently committed identities with their owner and final pricing evidence. A fresh create can use these witnesses only when every supplied identity matches its authorization scope, policy generation, provider/endpoint and the socket's grant key/revision. Witnesses are recorded after successful binding and pricing evidence commits and cleared on close. Unknown, pending, failed or evicted identities, reconnects, HTTP requests, and changed scope/generation/grant pins use the authoritative continuation lookup. Header/metadata conflicts are still rejected before any lookup. A continuation naming a different grant from the connection's pin returns 409 continuation_restart_required before dispatch; restart with full input on a new connection. The rejected create is not billed and does not close the socket. An upgrade header alone does not create a synthetic metadata event. Metadata may precede response.created; it identifies the response without proving execution started. Binding and pricing evidence writes run concurrently with non-terminal delivery, in per-operation order. The terminal waits for all those writes; pending publication preserves wire order and counts toward output limits. Binding failure closes with 503 continuation_unavailable before terminal delivery. After a terminal outcome is claimed and its frame is forwarded, the next same-lane create may begin admission while settlement finishes; dispatch and the final soft-budget preflight wait for successful settlement on that lane, without blocking other lanes' admission or dispatch. An accounting failure still closes the connection with accounting_unavailable before new work is sent. Closing the connection suppresses late publication acknowledgments and queued output.

response.interrupt with response_id and mode: "discard_partial_items" bypasses the create queue on the same upstream socket. Its optional stream_id must match the response's lane. The ID must belong to a sent active response, the lane's most recent terminal response, or a response previously interrupted on that connection. No other control fields or modes are accepted. Repeated interrupts are forwarded; they do not create usage receipts or new generations. Upstream response.interrupt.accepted and response.interrupt.failed frames preserve wire order, including after settlement or a successor's start. They never bind continuation ownership or settle the generation. The router does not fabricate acknowledgements or require one before processing a normal terminal. Early cancellation and acknowledgements depend on the upstream. The router forwards the control without promising that it stops generation. Uncorrelated upstream error events after an interrupt close the connection conservatively, retaining estimates for running work rather than treating a control rejection as a generation terminal.

Worker deployments retain structured responses_websocket_control diagnostics for forwarded interrupts and unsupported events (at most 16 per connection). Fields are a router-generated connection ID, a fixed event-type category, forwarded/rejected outcome, and whether background was explicitly enabled. Request/response bodies, response IDs, lane IDs, credentials, and arbitrary event names are excluded. Wrangler enables retained logs with invocation logs disabled.

The bridge forwards native response IDs, errors, metadata, tool results, previous_response_id, and stream_options. Prewarm generate: false requests receive normal admission and accounting. It never replays requests or switches grants after dispatch. A terminal response with usable usage settles once. Reserve policies retain the reservation when sent requests disconnect or time out without final usage; unsent admitted requests release it. Soft policies use local pre-dispatch denials and parallel budget checks, then charge observed usage or an explicit fixed tariff, marking unknown prices as unpriced_usage without inventing a missing-usage charge. Budget exhaustion never aborts an in-flight soft turn; it denies the next create in this isolate. Queued requests have no receipt. The first terminal outcome or close cause owns settlement, including when admission finishes after cancellation. A client disconnect records client_error with status_code: null; only the response whose deadline expired records timeout/504. Other active lanes closed with that connection do not inherit its timeout. If budget settlement/charge and its durable recovery both fail, or usage publication fails, the socket reports accounting_unavailable and closes before sending more work.

Limits per connection are 16 active responses (including those still settling), 32 named lanes plus the default lane, 48 buffered creates, 32 MB per incoming frame (the HTTP media envelope; Cloudflare delivers at most 32 MiB), 32 MB total buffered create bytes, 8 MiB per upstream frame, at most 48 output frames/8 MiB awaiting publication, 16 MiB cumulative downstream output, and 128 forwarded interrupts (including repeats). Interrupt identities remain bounded by this lifetime cap to correlate late acknowledgements. The output limit bounds a slow reader because Workers' WebSocket API has no supported drain/queue metric. Connections last at most 60 minutes; each response uses its endpoint deadline, capped at 600 seconds. Clients must reconnect after a limit or deadline closes the connection. Binary frames, steering events, and background execution are rejected visibly. This contract runs on the Worker deployment; other hosts must qualify native upgrade forwarding before advertising it.

See the upstream Responses WebSocket contract. An authenticated key catalog route advertises websocket: "openai.responses" only for a supported endpoint and configured grant transport, retaining that metadata during a known temporary grant cooldown. Consult eligible offers for current availability; the metadata never authorizes an upgrade.

Access session routes

Method Path Purpose
GET /v1/session Verified Access identity, entitlements, and readiness
GET /v1/session/avatar Proxied user avatar
GET /v1/session/usage Session quota and legacy shared-policy usage summary; ?budgets_only=true skips usage ledger reads
GET /v1/session/usage/report Bounded personal-only tokens, price coverage, daily trend and model/provider/key groups
GET /v1/session/usage/events Metadata-only personal event pages with a query-bound cursor
GET /v1/entitlements Compatibility entitlement response
GET /v1/session/credentials Credentials owned by the signed-in user
POST /v1/session/credentials Create a caller-owned credential; reject an existing ID
PUT /v1/session/credentials/<credential-id> Create or rotate a caller-owned credential
POST /v1/session/credentials/<credential-id>/rotate Replace only an active credential's secret hash
POST /v1/session/credentials/<credential-id>/rename Change only a caller-owned credential's display name
POST /v1/session/credentials/<credential-id>/revoke Revoke a caller-owned credential
POST /v1/playground/<route> Run a console playground request through an allowed route
GET /v1/playground/v1/responses/<response-id> Retrieve/resume the session's original response
GET /v1/oauth/callback Complete a provider-approved browser OAuth flow

The Worker redirects / to /dashboard, and /dashboard to /dashboard/home. A production Cloudflare Access application protects /dashboard/*, /v1/session*, /v1/playground/*, /v1/admin/*, and /v1/oauth/callback before the request reaches the Worker.

Admin reads

Method Path Purpose
GET /v1/admin/bootstrap Coherent authority, configuration, and readiness snapshot
GET /v1/admin/overview Policy, credential, tenant, provider, and budget totals
GET /v1/admin/tenants Tenant summaries
GET /v1/admin/usage Budget rows, aggregate usage, and recent request audit
GET /v1/admin/content?tenant=<id>&ref=<ref> Retained request content from the separate archive
GET /v1/admin/policies Access policies
GET /v1/admin/credentials Issued proxy credentials without raw secrets
GET /v1/admin/connections Global provider connections and kill switches
GET /v1/admin/access-users Materialized Access users
GET /v1/admin/policy-bindings User and group policy bindings
GET /v1/admin/provider-status Policy-aware provider readiness
GET /v1/admin/provider-health Persisted provider smoke status
GET /v1/admin/upstream-grants Sanitized policy- and tenant-scoped upstream grants
GET /v1/admin/upstream-grants/usage?scope=policies&scopeId=<policy-id>&days=<1..31> Per-grant/lane totals and daily model usage
GET /v1/admin/assignment-rules Access identity assignment rules
GET /v1/admin/fusion Fusion configuration

Admin mutations

Method Path Purpose
PUT /v1/admin/access-users/<email> Update a materialized user's tenant, status, groups, or retention exemption
PUT /v1/admin/access-user-grants/<email> Update a user and atomically replace direct policy grants
PUT /v1/admin/policy-bindings Create or update a user or group binding
PUT /v1/admin/policies/<policy-id> Create or update a policy
POST /v1/admin/policies/<policy-id>/revoke Disable a policy and every credential bound to it
POST /v1/admin/credentials Create an issued credential; reject an existing ID
PUT /v1/admin/credentials/<credential-id> Create or update an issued credential
POST /v1/admin/credentials/<credential-id>/rotate Replace only an active credential's secret hash
POST /v1/admin/credentials/<credential-id>/rename Change only an existing credential's display name
POST /v1/admin/credentials/<credential-id>/revoke Revoke one issued credential
PUT /v1/admin/connections/<provider-id> Update a global provider connection
PATCH /v1/admin/connections/<provider-id> Update only supplied connection fields
PUT /v1/admin/upstream-grants/<policies|tenants>/<scope-id>/<token-ref> Create or update a scoped upstream grant
POST /v1/admin/upstream-grants/<policies|tenants>/<scope-id>/<token-ref>/revoke Revoke a scoped upstream grant and remove its secrets
POST /v1/admin/upstream-grants/<policies|tenants>/<scope-id>/<token-ref>/refresh Refresh an OAuth grant
POST /v1/admin/upstream-grants/<policies|tenants>/<scope-id>/<token-ref>/quota-refresh Refresh provider-reported grant quota state
POST /v1/admin/upstream-grants/<policies|tenants>/<scope-id>/<token-ref>/authorize Begin a browser OAuth authorization
POST /v1/admin/pool-submission-tickets Issue a short-lived, one-time credential contribution ticket for one pool slot
PUT /v1/admin/assignment-rules/<rule-id> Create or update an identity assignment rule
POST /v1/admin/assignment-rules/reconcile Reconcile materialized users against assignment rules
PUT /v1/admin/fusion Update Fusion routing configuration
POST /v1/admin/fusion/preview Evaluate Fusion readiness and estimated reservations for a policy
POST /v1/admin/usage/recovery Inspect/list one recovery scope or replay one immutable financial receipt

The legacy GET|PUT /v1/admin/keys..., POST /v1/admin/keys/<kid>/revoke, and GET /v1/admin/users routes remain compatibility aliases. New control-plane clients use policies, credentials, and tenants directly. Legacy top-level console and /api/* aliases redirect or normalize to their /dashboard/* and /v1/* equivalents.

Upstream grant PUT preserves unspecified credentials by default. Add ?mode=replace to replace the complete grant, clearing omitted credentials and account or refresh metadata. Replacement requires a fresh primary credential; credential-presence flags are insufficient. Other mode values return HTTP 400. The cf:oauth:put CLI uses replacement mode.

Grant revocation accepts an optional JSON object with kind, provider, and label hints for legacy grants that have no credential owner. Existing owners ignore these hints and retain their canonical identity. Revocation stores a secretless, disabled tombstone and cancels maintenance; an unknown grant returns HTTP 404. Retrying revocation preserves the same tombstone generation.

Explicit replacement and revocation can recover an existing legacy KV grant with invalid JSON or metadata larger than the 3 MiB migration limit. Corrupt or oversized bytes are discarded, never imported into the credential owner. Supply fresh credentials through cf:oauth:put or PUT ...?mode=replace, or use cf:oauth:revoke to remove the account. Ordinary edits, OAuth callbacks, contributions, refresh and automatic migration remain strict. A failed KV read is unavailable state, not proof of an absent account. Recovery preserves retained pool generations; a newer index than an existing owner requires operator recovery instead of resetting ownership. When both the owner and KV record are missing, either PUT mode creates an account only after confirming an empty attachment index. Retained legacy membership or generation history returns HTTP 409, including generation-zero legacy rows.

Policy model access

Policies optionally set models to an array of full catalog IDs, for example ["anthropic/claude-opus-5-5"]. Omitted or null preserves access to every model of the allowed providers; [] denies every model. Provider restrictions still apply. Writes accept at most 1,024 entries, reject unknown IDs, and normalize duplicates. Omitting either models or poolStatus from an update preserves the existing value.

/v1/catalog, /v1/models (including Anthropic format), session entitlements, and Fusion offers honor these scopes. Access sessions can use models allowed by any held policy, with the normal ordered policy choice among matching policies. Every concrete inference call checks the resolved catalog ID before grant selection, budget reservation or upstream contact. A denied model returns HTTP 403 model_not_allowed on native Messages and count_tokens, manifest routes, OpenAI chat/embeddings/speech, HTTP Responses, and each WebSocket response.create. Fusion checks its synthesizer and each adviser separately; denied advisers fail open like other unavailable advisers. Its virtual name does not grant access to underlying models. Restricted policies reject opaque universal gateway requests because their downstream model selections cannot be verified.

Policy pool status

GET /v1/pool accepts a proxy key or verified Access session. Policy poolStatus: "visible" opts in; omitted or "hidden" returns HTTP 404 pool_status_hidden. Responses use Cache-Control: private, no-store. A proxy key always selects its own policy, regardless of query parameters. An Access session may select one of its held policies with ?policyId=<id>; without it, the first active policy is used. An unheld policy returns HTTP 403.

The response contains version: "clawrouter.pool.v1", ISO observedAt, policyId, routing (strategy, includedQuotaReservePercent, environmentFallback), allowed catalog models, grants, and usage. Each grant includes only its token-reference key, label, provider, kind, enabled and credential status, plan, account (email, rateLimitTier, subscriptionStatus), quota windows (id, window, models, remainingPercent, resetAt, observedAt), extra usage (enabled, usedCredits, monthlyLimit, currency, spendLimitReached), per-model eligibility, lastSelectedAt, and selectedCount. Unknown facts are null. Account emails are intentionally visible to holders when the administrator opts in. No access/refresh tokens, credential fields, raw provider payloads, or full storage keys are returned.

Inventory includes the selected policy's grants, shared tenant grants and explicit grantRouting.sharedGrants subscription owners authorized by an administrator, filtered by its provider and eligible-grant restrictions. It is bounded to 1,024 scanned grant records (HTTP 422 pool_too_large beyond that limit), plus at most 32 explicitly shared owners. Foreign-scope owners use stable opaque shared-<digest> keys in both inventory and usage, so equal token references do not collide and source policy names remain private. usage contains days: 7 and the policy's aggregate lanes and grants from the existing grant usage ledger, with token references or opaque shared IDs replacing storage keys; it contains no individual request events. These are policy-wide totals, including other credentials on the same policy, and retain historical attribution.

Eligibility shares the Subscriptions page and dispatch quota rules: scoped windows match upstream model family names, reserve applies to subscriptions, active cooldown blocks selection, and stale observations block only under staleState: "deny". Status is eligible, exhausted, cooldown, stale, or reauth_required. Models belonging to another provider use exhausted (no usable capacity); disabled grants are never eligible, and missing or invalid credentials use reauth_required. Eligibility reports quota/credential availability, not priority selection, transport readiness or a budget reservation. Observations and usage are advisory snapshots, not an atomic upstream quota guarantee.

Subscription routing and quota

Policy grantRouting accepts two opt-in controls in addition to the existing strategy, stickiness, failover, priority and stale-state settings:

Field Default Contract
includedQuotaReservePercent 0 Integer 0–50. A subscription is ineligible when any applicable, unexpired window has this percentage or less remaining.
environmentFallback false If no scoped grant is eligible, allow the provider's configured environment credential with normal transport, pricing and budget enforcement. Missing environment credentials retain the pool-unavailable error.

Scoped quota windows in GET /v1/admin/upstream-grants optionally carry models: ["fable"] (lowercase family tokens) and per-window observedAt timestamps. A scope applies when its token occurs in the requested upstream model ID; an absent scope applies to all models. Exhausted unscoped windows cool down the whole grant. Scoped exhaustion excludes only matching models. most_remaining and threshold compare only applicable live windows. threshold keeps the last reading of each open window regardless of staleAfterSeconds; a grant with no open-window reading is an unmeasured replacement of last resort. Freshness uses each applicable window's own timestamp, so partial header updates cannot refresh an older model window. Concurrent feedback merges by window observation time and preserves active inference cooldowns. Reset-passed readings are unknown. A reserve protects against extra-usage billing but cannot prevent one in-flight request crossing the boundary. It does not turn polling into an atomic upstream quota reservation.

Grant responses also expose optional sanitized account fields (email, displayName, rateLimitTier, subscriptionStatus, extraUsageEnabled) and extraUsage fields (enabled, usedCredits, monthlyLimit, utilization, currency, spendLimitReached). Credits are minor units, e.g. USD cents. Only manifest-declared scalar fields are retained. Tokens and raw provider payloads are excluded. A probe HTTP 429 keeps the last reading and honors Retry-After independently per endpoint; HTTP 401 marks reauthorization required.

When a configured pool has no eligible grant, HTTP requests fail with upstream_grant_pool_unavailable. If every grant that could serve the operation is excluded only by an active rate-limit or quota cooldown or by quota windows with published resets, the status is HTTP 429 with Retry-After set to the whole seconds until the earliest such grant becomes eligible. Otherwise (for example eligibility restrictions, unsupported transports, exhausted windows without a reset, stale observations under staleState: "deny", or grants whose credentials failed with HTTP 401/403) it remains HTTP 503 without Retry-After. The JSON error body is the same in both cases.

Retry-After is ClawRouter's own re-admission time, not a provider promise. A provider 429 without timing cools a grant for ClawRouter's 60-second default, observed cooldowns are capped at 24 hours, and published quota resets are used as reported without a cap. Clients should apply their own maximum wait and fail over beyond it. An authentication cooldown never yields a Retry-After, because waiting does not repair a credential. CORS responses expose Retry-After to browser clients.

Proxied HTTP responses carry x-clawrouter-grant-lane: subscription for subscription/OAuth grants, api_key for scoped API-key grants, and environment for provider environment credentials. One-shot 401/403/429 failover can use the environment lane when explicitly enabled and no alternate grant remains. Continuation pins, provider authorization and budget limits remain authoritative. An existing environment pin can reuse that environment credential while fallback remains enabled; a scoped pin never switches lanes. Usage events carry nullable grant_key and grant_lane for the final attempt; events created before this change retain unknown attribution.

Upstream grant usage

GET /v1/admin/upstream-grants/usage?scope=policies&scopeId=<policy-id>&days=7 requires the same admin authorization as the grant inventory. days is an integer 1–31 (default 7). The policy must exist; tenant-only scopes are not accepted. The Worker resolves the policy's canonical tenant before addressing its usage shard, independent of the administrator's tenant.

The response contains days, since/until epoch milliseconds, retentionDays (currently 30), grants, lanes, and daily. Each metrics row contains requests, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, and costMicros. grants groups by provider, nullable grantKey, and nullable grantLane; lanes groups by provider and nullable grantLane. daily adds UTC day (YYYY-MM-DD) and nullable model, retaining the same grant/lane keys. Cost follows existing accounting prices (API-equivalent manifest value, or the policy's fixed tariff), not subscription invoice spend. Failed recorded requests also count; usage remains a single final-attempt event per request.

Only retained events within the rolling requested range are read. A 31-day request cannot restore data beyond 30-day retention. Legacy null attribution remains a separate group. The response is bounded to 8,192 daily groups and returns HTTP 422 if exceeded; request a shorter range rather than relying on partial totals. Invalid scope/range returns HTTP 400; an unknown policy returns HTTP 404. The additive SQLite migration preserves every existing event.

Credential expiry, daily/weekly budgets, and service tiers

New credentials accept expiresAt (future ISO-8601 UTC) and serviceTiers (canonical lowercase list or null to inherit). Expiry defaults to 180 days; personal maximum is min(default TTL, max TTL), admin maximum 366 days. Configure these using CLAWROUTER_CREDENTIAL_DEFAULT_TTL_DAYS and CLAWROUTER_CREDENTIAL_MAX_TTL_DAYS. An admin PUT omitting expiry preserves it; new IDs always receive a finite expiry. Admin PUT may omit secretSha256 to preserve an existing secret while updating expiry. Rotation resets expiry and works for expired enabled keys under the other existing preconditions.

Session/admin credential lists return expiresAt, expired, serviceTiers, and active (false when expired). Key inspect adds the same expiry fields, serviceTiers, effectiveServiceTiers keyed by visible catalog model ID, dailyBudgetMicros, weeklyBudgetMicros, limitHint, budget.daily and budget.weekly. An expired key still gets HTTP 401 on inspect, with expiresAt, expired: true, and active: false beside the error. Legacy absent/null expiry is accepted.

Policies add nullable dailyBudgetMicros and weeklyBudgetMicros (nonnegative safe integers), serviceTiers: {allowed: string[], models?: Record<string, string[]>}, and limitHint (trimmed plain text, at most 300 characters, no controls). Tier lists are deduplicated, bounded to eight IDs, with at most 32 known catalog-model overrides. Overrides replace global tiers; credential tiers intersect them. Personal tier lists may only narrow the policy. The provisioning script can patch the team server and openclaw_stable policies to allow Ultrafast only on openai/gpt-6.1-sol. Without configuration, Default, Flex and Fast are allowed; Ultrafast/unknown tiers are denied, even without budgets.

Existing monthly status fields remain unchanged. /v1/usage, session policy entitlements and admin usage rows add budget.daily and budget.weekly with configured, limitMicros, spentMicros, remainingMicros, windowKey, and resetsAt (next midnight UTC for daily; next Monday 00:00 UTC for weekly). Weekly window keys end in /week/YYYY-Www using the ISO week-year. The monthly status also includes resetsAt. Principal scopes share daily, weekly and monthly balances across that principal's keys. Unavailable status amounts remain null.

HTTP Error code Meaning
401 proxy_key_expired Key expired; message names its date and dashboard renewal path.
402 daily_budget_exhausted Daily capacity exhausted; message names the limit, next UTC reset and policy hint.
402 weekly_budget_exhausted Weekly capacity exhausted; message names the limit, next Monday 00:00 UTC reset and policy hint.
402 budget_exhausted Policy monthly capacity exhausted; includes policy hint.
400 service_tier_not_allowed Requested OpenAI tier not in the policy/model/key intersection; names tier, model, allowed tiers and hint.
403 model_not_allowed Model outside policy scope; includes policy hint.

OpenAI-format omitted/default/auto map to default, fast maps to priority, and other strings become lowercase for admission. Other request formats retain existing behavior. Pricing gaps under a daily-only or weekly-only budget return pricing_required just as with monthly budgets. Dispatched work completes after key expiry; subsequent requests, WebSocket turns, background creations and continuations fail.

Credential creation, naming, rotation, and revocation

Use POST /v1/admin/credentials or POST /v1/session/credentials with { "credentialId": "my_key", "policyId": "my_policy", "secretSha256": "<64 hex characters>" } to create a key. IDs must contain 4–128 letters, digits, or underscores so the key can be authenticated. Success returns 201; an existing ID returns 409 credential_exists without replacing the key or pruning retained records. Admin creation also accepts enabled (default true) and principalId (an email or null, default null). Personal creation always enables the key and assigns the signed-in user as owner.

Creation also accepts optional name (string or null). Names are trimmed, at most 80 characters, and reject control characters. Empty or whitespace-only strings and null clear a name; other types are rejected. Session/admin credential lists and mutation responses return name, null when unnamed, without exposing secret hashes.

Use POST .../credentials/<credential-id>/rename with exactly { "name": "Work laptop" } or { "name": null }. Other fields are rejected. Rename requires an existing credential and, for personal keys, the current enabled signed-in owner. It remains available for owned revoked or expired keys and after policy/group/binding access is removed. It changes only the latest stored name: no rotation, secret recovery, reactivation, owner/policy rebinding, expiry renewal, generation update, or tier changes. Missing keys return 404; keys owned by another principal return 403. Ownership and actor state are rechecked at the serialized write boundary, including after concurrent writes.

Use POST .../credentials/<credential-id>/rotate with only { "secretSha256": "<64 hex characters>" } to replace a key's hash and reset its expiry. Rotation preserves its name, owner, policy, generation, and enabled state. It requires an enabled key, an enabled policy with the same generation, and an owner who is not disabled. An inactive key returns 409 credential_inactive; a missing key returns 404. Rotation cannot reactivate a revoked key or renew a revoked policy generation.

The existing PUT .../credentials/<credential-id> remains an upsert: it can replace the owner or policy on an admin key, reenable a key, and bind the current policy generation. Personal PUT keeps its existing forced-enabled, caller-owned behavior. PUT omission preserves an existing name; an explicit null clears it. Use create and rotate for operations that must not overwrite an existing key or change its authorization. Secret hashes are lowercase SHA-256 hex; responses never include a raw secret or hash and describe the record committed by that operation.

Personal create, PUT, and rotate recheck the current enabled user, groups, bindings, and policy at the serialized write boundary. Personal revoke requires ownership but remains available after group, binding, or policy access is removed. Revoke disables the latest record, preserves any intervening hash rotation, and is idempotent for an existing key. Local administrator disable or demotion is also rechecked before credential writes; Cloudflare administrator status comes from the verified Access configuration. Personal keys remain limited to 10 enabled and 100 retained records; creating another key can prune revoked records in ID order at the retention limit. Rotation and rename consume no additional slot.

Pool contribution

POST /v1/pool-submissions/<ticket-id>/consume accepts a ticket bearer secret and one credential bundle. The ticket, not the request body, supplies the pool scope, token reference, provider, grant kind, priority, and weight. Accepted body fields are credential, credentials, accessToken, refreshToken, tokenType, expiresAt, scopes, accountId, and bounded subscription metadata. Contributor-defined refresh endpoints and OAuth client settings are not accepted. Keep-warm follows the provider manifest default; Claude subscription tickets default to enabled. The administrator may set keepWarm: false on the ticket to opt out. Contributors cannot alter maintenance behavior in their submission.

The route consumes the ticket once, but an identical retry can recover an interrupted submission and returns the same receipt after completion. The response exposes only the grant key and submission timestamp. Raw credential material is stored in the grant's Durable Object; the KV grant record contains only routing metadata and credential-presence flags.

Routing and authorization behavior

Before forwarding a request, the Worker authenticates the credential, resolves its policy, checks the provider connection and scoped grant readiness, and reserves a conservative budget. Policies can select grants by priority, round robin, least used, reported remaining quota, consume-until-threshold, or weighted random choice. Threshold routing owns pool affinity; other modes may use identity or session stickiness.

Provider grants can be scoped to a policy or tenant. When several token references are eligible, ClawRouter applies the policy strategy within the active priority tier. An upstream 401, 403, or 429 records sanitized grant state and can trigger same-provider alternates when policy allows failover, within one budget of four distinct eligible grants shared with credential preparation and Fusion reservation. Anthropic's explicit credit-balance HTTP 400 also excludes that grant for five minutes and can trigger the same bounded pre-delivery failover. Healthy grants retain stickiness; unrelated 400s and errors after streaming begins do not trigger credit failover. Exclusions use the full scoped grant key, including explicitly shared owners. If no further attempt is available, the last upstream rejection is returned; subsequent requests with no eligible grant return the pool-unavailable error with the earliest Retry-After.

Unified, native, and manifest HTTP proxy routes refuse upstream 3xx redirects on both initial and alternate-grant dispatch. They cancel the redirect body and return a nonbillable 502 provider_unavailable without Location; configure the final API endpoint rather than a redirect-dependent URL.

Once HTTP dispatch begins, the endpoint deadline and observed caller cancellation also bound waiting for alternate-grant preparation. If interrupted there, the Worker cancels the original rejection body, returns 502 provider_unavailable, and never dispatches a late alternate. An ordinary selection failure still returns the original rejection. Already-issued authority or credential operations may finish; this does not cancel them or extend the deadline to initial admission.

Disable one credential to revoke one key, disable a policy to revoke every credential bound to it, or disable a provider connection to stop that provider globally. Policy and credential generations must match, so incomplete rotations and stale migration records fail closed.

Budgets, usage, and retention

Budgeted requests reserve an upper-bound token cost before the upstream call when the selected model has versioned pricing. A policy requestCostMicros value is a fixed-cost override; budgeted routes without versioned pricing or an override fail closed.

requestCostsMicros maps capabilities to fixed request prices in non-negative integer microUSD (at most 64 entries). A matching entry overrides requestCostMicros, including zero; otherwise the policy-wide tariff or manifest pricing applies. Standalone search additionally requires an explicit search.web entry for eligibility in both discovery and dispatch; a policy-wide tariff alone returns 400 pricing_required. Removing that entry disables standalone search, while an explicit zero enables it at zero router charge. For example, {"requestCostMicros":null,"requestCostsMicros":{"search.web":1000}} fixes standalone search charges while retaining token-priced inference. Policy updates preserve an omitted map; null removes it. Free counting, model discovery and moderation remain free. These prices govern admission, settlement and catalog affordability. The API and cf:key:put --request-costs-micros use integer microUSD. The admin policy form displays and accepts both request-price fields in US dollars, converting exactly on load/save; its equivalent capability JSON is {"search.web":0.001}. Prices support six decimal places, including explicit zero; blank removes the override.

Successful responses settle to reported usage, including cached input where available, or the explicit fixed policy tariff. Known-unsent work and received non-2xx responses settle at zero with cost_basis: none. Transport failures after dispatch, and missing or interrupted usage, retain the qualified estimate or fixed tariff because upstream work may have occurred. Streaming responses are metered without buffering the client stream.

Claude Messages server web_search settles $0.01 per search plus measured token costs on soft-budget and unmetered keys, using the authoritative usage.server_tool_use.web_search_requests counter in JSON or cumulative SSE usage. Content blocks and the caller's max_uses are not billed as observed searches. Missing, malformed, or incomplete search usage does not invent a fee: known token costs are still recorded and the receipt reports incomplete/unpriced usage. Known web_fetch versions have token costs only, with no per-call fee. See Anthropic's web search pricing and web fetch pricing.

Hard-budget policy or provider limits require an explicit fixed policy price (requestCostMicros or the llm.messages capability tariff) for these Claude server web tools; otherwise dispatch returns 400 pricing_required, even when max_uses is supplied. That field bounds tool calls, not the cumulative token cost of repeated server-side model iterations, for which Anthropic publishes no synchronous iteration ceiling. A fixed policy tariff remains the configured charge rather than measured token-plus-search pricing. OpenAI hosted-search accounting is unchanged.

Declared Responses JSON and SSE bodies are inspected incrementally for usage and terminal status, including fields after large output. The observer retains only bounded metadata; excessive nesting or oversized selected fields leave usage unknown and preserve the applicable estimate or fixed tariff. These inspection limits do not impose a response-size limit or change delivered bytes.

Usage events are delivered through USAGE_QUEUE to tenant- and policy-sharded USAGE_LEDGER Durable Objects. Settlement and audit delivery retry independently, and exhausted messages move to the configured usage dead-letter queue. Ledgers keep bounded identity, route, timing, outcome, token, cost, request ID, and trace metadata; they do not store prompts or completions.

Policies can retain LLM request bodies in the separate CONTENT_ARCHIVE R2 binding, including whole documents/PDFs and inline media in large Claude requests (not just extracted text). Retention failure is fail-closed before upstream traffic. See Request content retention for the policy and disclosure contract.

Every Worker-owned response returns the canonical X-Request-ID, and CORS exposes it to clients. Valid W3C trace and span IDs are preserved in usage metadata. Agent attribution headers, pricing fields, and reservation semantics are documented in Agent spend control.

Image generation

POST /v1/images/generations supports the OpenAI SDK's images.generate call. Use an explicit registered model; there is no legacy DALL-E default. See OpenAI image generation for models, bounded JSON options, modality-specific accounting and unsupported surfaces.

The same contract is available at POST /v1/native/openai/v1/images/generations (native model slugs) and POST /v1/proxy/openai/images_generations (manifest JSON envelope). Generated base64 image bytes are forwarded unchanged and never archived.