LLM-OTA · technical plan

Turn a Grok subscription into an OpenAI- and Anthropic-compatible API

A Cloudflare Worker on llm.e2e.one that accepts standard LLM SDK traffic, authenticates with issued gateway keys, and forwards to xAI using credentials extracted from the official grok-cli OAuth session. An admin UI shows config, token health, and usage including cache reads and writes.

POC provider: Grok / xAI only Host: llm.e2e.one Zone: e2e.one Worker: llm-ota Subscription OAuth · ToS-sensitive

1. Goal

LLM-OTA (Over-The-Air LLM gateway) is a single Cloudflare Worker that looks like OpenAI and Anthropic to clients, and like a Grok Build / SuperGrok subscriber to xAI. The POC implements one provider only: Grok. Later providers (Claude Max, ChatGPT Plus, Gemini, etc.) plug into the same gateway interface.

Clients see

  • POST https://llm.e2e.one/v1/chat/completions
  • POST https://llm.e2e.one/v1/responses
  • POST https://llm.e2e.one/v1/messages
  • GET https://llm.e2e.one/v1/models
  • Gateway Authorization: Bearer sk-ota-... or x-api-key

Operators see

  • Admin at https://llm.e2e.one/admin
  • Provider status and token expiry
  • Per-key request and token usage
  • Cache hit / write / miss breakdown
  • Model aliases and rate limits

2. Research findings

2.1 grok-cli does not use grok.com cookies

The official Grok Build CLI (this machine: ~/.grok/bin/grok) authenticates through SpaceXAI OAuth at auth.x.ai, not the unofficial sso / sso-rw cookie scrape used by older grok.com wrappers. Credentials live in ~/.grok/auth.json with mode 0600.

Observed local schema (values redacted):

{
  "https://auth.x.ai::<principal-uuid>": {
    "auth_mode": "...",
    "key": "<access token — Bearer for api.x.ai>",
    "refresh_token": "<rotating refresh token>",
    "expires_at": "<ISO-8601>",
    "oidc_issuer": "https://auth.x.ai",
    "oidc_client_id": "...",
    "user_id": "...",
    "team_id": "...",
    "email": "...",
    "principal_id": "...",
    "principal_type": "..."
  }
}

2.2 xAI already speaks OpenAI; Anthropic native is deprecated

2.3 Prompt cache on xAI is automatic prefix cache

From xAI prompt caching:

2.4 What “full” OpenAI / Anthropic cache compat means

ClientRequestResponse
OpenAI Chat Completions Stable prefix; optional prompt_cache_key; pass-through x-grok-conv-id usage.prompt_tokens_details.cached_tokens, cache_write_tokens (GPT-5.6-style, synthesized)
OpenAI Responses prompt_cache_key usage.input_tokens_details.cached_tokens + write counterpart
Anthropic Messages Block-level cache_control: {type:"ephemeral", ttl:"5m"|"1h"}; top-level automatic cache_control; anthropic-beta: prompt-caching-2024-07-31 cache_creation_input_tokens, cache_read_input_tokens, optional cache_creation.ephemeral_5m_input_tokens / ephemeral_1h_input_tokens

3. Key decisions

DecisionChoiceWhy
Runtime One Cloudflare Worker + D1 + KV + one Durable Object Already on the e2e.one zone. Custom Domain gives TLS without extra DNS work.
Public hostname llm.e2e.one ai.e2e.one is taken by kiro-otai. api.e2e.one points at an origin A record.
POC provider Grok via grok-cli OAuth against api.x.ai User request. Official console API keys remain a fallback auth mode.
Not using grok.com cookies OAuth session only CLI no longer uses sso cookies; cookie scrapers are brittle and more ToS-hostile.
Anthropic backend Translate to xAI Chat Completions / Responses xAI native /v1/messages is deprecated; cache writes are unsupported there.
Refresh lock Durable Object TokenLease Refresh tokens rotate. Concurrent refreshes invalidate each other.
Client auth Gateway-issued keys, hashed in D1 Never expose the grok-cli access token to downstream tools.
Admin auth (POC) ADMIN_PASSWORD cookie session Fast. Swap for Cloudflare Access before any shared use.

4. Architecture

  OpenAI SDK / Claude SDK / Cursor / Cline
                 |
                 |  sk-ota-...  or  x-api-key
                 v
        https://llm.e2e.one          Cloudflare Worker  llm-ota
        /v1/chat/completions         - protocol detect
        /v1/responses                - key auth
        /v1/messages                 - model alias
        /v1/models                   - cache-key derivation
        /admin                       - usage log (waitUntil → D1)
                 |
                 |  Bearer <rotated grok-cli access token>
                 |  x-grok-conv-id / prompt_cache_key
                 v
              api.x.ai
                 |
        TokenLease DO <-- refresh_token grant --> auth.x.ai
        KV: current access token + expiry (cache of DO state)
        D1: api_keys, requests, usage_daily, config

Worker routes

MethodPathAuthRole
GET/publicStatus / this plan
GET/healthpublicliveness; no token leak
GET/v1/modelsgateway keyOpenAI model list
POST/v1/chat/completionsgateway keyOpenAI Chat Completions
POST/v1/responsesgateway keyOpenAI Responses
POST/v1/messagesx-api-key or BearerAnthropic Messages
POST/v1/messages/count_tokenssameAnthropic token count (best-effort)
*/admin*admin sessionConfig + usage UI
POST/admin/api/keysadminMint / revoke gateway keys
POST/admin/api/provider/grok/importadminAccept pasted auth.json blob (never logged)

5. Extracting grok-cli tokens

5.1 Local importer (preferred)

A repo script scripts/import-grok-auth.mjs reads ~/.grok/auth.json (or $GROK_HOME/auth.json), never prints secrets, and writes Wrangler secrets:

GROK_ACCESS_TOKEN
GROK_REFRESH_TOKEN
GROK_OIDC_ISSUER          # https://auth.x.ai
GROK_OIDC_CLIENT_ID
GROK_TOKEN_EXPIRES_AT     # ISO, optional
GROK_ACCOUNT_EMAIL        # non-secret, also a var

Refresh flow (inside the Durable Object, exclusive):

  1. Discover token_endpoint from {issuer}/.well-known/openid-configuration.
  2. POST grant_type=refresh_token, client_id, current refresh token. Public client, no secret (CLI is a PKCE public client).
  3. Persist the new access token, new expiry, and rotated refresh token via wrangler secret is not available at runtime — store the live pair in DO storage (authoritative) and a short-TTL copy in KV for hot reads.
  4. Seed DO storage from the Wrangler secrets only on first boot or when the admin hits “re-import”.
Do not let the Worker and the local grok-cli refresh the same refresh token. After import, either treat the Worker as the owner, or re-import after every local grok login. Document this on the admin “Provider” panel.

5.2 Admin paste import

The admin UI accepts the raw auth.json object (or the inner record). The Worker extracts key / refresh_token / issuer / client_id, seeds the DO, and returns only { email, expires_at, issuer }. The request body is not written to D1 or logs.

5.3 Fallback: console API key

If no session is loaded, XAI_API_KEY (secret) is used. Usage then bills the xAI console, not the SuperGrok quota. The admin UI labels the active mode: subscription-oauth vs console-api-key.

6. OpenAI-compatible surface

Accept the current OpenAI Chat Completions and Responses request bodies and stream shapes.

Must implement (POC)

Explicitly later

7. Anthropic-compatible surface

Clients such as Claude Code, Cline, and the official Anthropic SDK must work unchanged.

Headers

HeaderPOC behavior
x-api-keyGateway key. Also accept Bearer.
anthropic-versionAccept 2023-06-01; echo it.
anthropic-betaHonor prompt-caching-2024-07-31 and extended TTL names. Ignore unknown betas.

Request

Response

Translation to xAI

Anthropic messages + system + tools
        → OpenAI/xAI chat messages
        → POST api.x.ai/v1/chat/completions
        → map choices/tool_calls back to content blocks
        → map usage (see §8)

8. Cache reads and writes

Two different cache languages have to meet in the Worker. xAI only reports reads (cached_tokens). Anthropic and newer OpenAI also expect writes.

8.1 Driving xAI cache

  1. Keep the prompt prefix byte-stable. Never rewrite earlier messages.
  2. Derive a conversation id: sha256(gateway_key_id + ":" + (client prompt_cache_key || client x-grok-conv-id || hash(stable_prefix)))
  3. Send it as x-grok-conv-id on Chat Completions and as prompt_cache_key on Responses.
  4. For Anthropic explicit breakpoints: the cached prefix is everything up to and including the last block that carries cache_control. The suffix after that is the “hot” tail. We still send the full prompt to xAI (it only caches matching prefixes), but we use the breakpoint to decide how to report writes vs reads.

8.2 Mapping usage back

Let P = upstream prompt tokens, R = upstream cached_tokens.

Outbound fieldValue
OpenAI prompt_tokens / Responses input_tokensP
OpenAI prompt_tokens_details.cached_tokensR
OpenAI prompt_tokens_details.cache_write_tokensmax(P - R - tail, 0) on a miss-or-partial, else 0
Anthropic input_tokensP - R (Anthropic counts uncached input only)
Anthropic cache_read_input_tokensR
Anthropic cache_creation_input_tokenssame write estimate as OpenAI
Anthropic cache_creation.ephemeral_5m_input_tokenswrites whose breakpoint ttl is 5m / default
Anthropic cache_creation.ephemeral_1h_input_tokenswrites whose breakpoint ttl is 1h

tail is an estimate of tokens after the last cache_control marker (tiktoken-class count is fine for POC; exact xAI tokenization is not public). If the client sent no markers (pure OpenAI automatic cache), treat the entire prompt except the last user turn as the cacheable prefix.

xAI does not honor Anthropic TTL. 5m vs 1h only affects how we label synthesized write tokens. The real TTL is whatever xAI evicts on that server. Document this on the admin cache panel so it is not mistaken for Claude-identical economics.

8.3 Streaming usage

9. Admin frontend

Served by the same Worker (no separate Pages project). Small server-rendered HTML plus vanilla JS. No client-side framework in the POC.

Config

  • Provider: Grok, mode, email, token expiry countdown, last refresh result
  • Import auth.json / re-seed secrets
  • Model catalog + aliases
  • Default model, max output tokens, streaming on/off
  • Gateway keys: create, label, last-used, revoke
  • Rate limit per key (requests / minute, tokens / day)

Usage

  • Requests last 24h / 7d / 30d
  • Prompt, completion, cache-read, cache-write tokens
  • Cache hit rate = read / (read + write + uncached)
  • Breakdown by key, model, protocol (openai | anthropic)
  • Recent request log: time, key, model, status, latency, tokens — never prompt text

10. Data model

D1

api_keys(
  id TEXT PK, label TEXT, hash TEXT UNIQUE,
  prefix TEXT, created_at, last_used_at, revoked_at,
  rpm INTEGER, daily_token_cap INTEGER
)
requests(
  id TEXT PK, ts, key_id, protocol, model, status, latency_ms,
  prompt_tokens, completion_tokens,
  cache_read_tokens, cache_write_tokens, error_code
)
usage_daily(
  day TEXT, key_id, model,
  requests, prompt_tokens, completion_tokens,
  cache_read_tokens, cache_write_tokens,
  PRIMARY KEY(day, key_id, model)
)
config(
  key TEXT PK, value TEXT, updated_at
)

KV

grok:access short-TTL copy of the live access token for request-path reads. Source of truth is the DO.

Durable Object TokenLease

Single instance. Methods: getAccessToken(), importSession(blob), status(). Serializes refresh.

Secrets / vars

ADMIN_PASSWORD          secret
GROK_*                  secrets (seed only)
XAI_API_KEY             secret, optional fallback
ALLOWED_ORIGINS         var, optional CORS

11. Security and ToS

Using a SuperGrok / X Premium OAuth session as a multi-client HTTP API is not an official xAI product. It can violate xAI terms, burn subscription quota, and get the account locked. The POC is for the account owner’s own tools. Do not resell or expose it on the public internet without Access in front.

12. Domain setup on e2e.one

Zone e2e.one is already active on this Cloudflare account (free plan, id e001b235077663f32cf15cca58c0aeec).

HostnameStatusAction
ai.e2e.oneAAAA 100:: + route → kiro-otaiLeave alone
api.e2e.oneA 84.8.145.139 (existing origin)Do not steal
llm.e2e.oneunusedWorker Custom Domain

Wrangler:

{
  "name": "llm-ota",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-13",
  "compatibility_flags": ["nodejs_compat"],
  "observability": { "enabled": true, "head_sampling_rate": 1 },
  "routes": [{ "pattern": "llm.e2e.one", "custom_domain": true }]
}

Custom Domain creates the DNS record and certificate. Do not also create a CNAME — that blocks Custom Domain attach.

Public URLs after attach:

13. Build order

#SliceShips
0 This plan + hostname PLAN.html, Worker stub, llm.e2e.one Custom Domain
1 Scaffold wrangler.jsonc, D1 schema, KV, TokenLease DO, health, admin password gate
2 Grok adapter Importer, DO refresh, fallback API key, GET /v1/models
3 OpenAI Chat Completions Non-stream + stream, tools, usage + cached_tokens + cache_write_tokens
4 Anthropic Messages Translate, stream events, cache_control in and cache_* usage out
5 Responses API Thin map onto the same adapter; prompt_cache_key
6 Admin UI Config, key mint, usage charts, cache rates
7 Harden Cloudflare Access on /admin, rate limits, request log retention

14. Verification (when implementing)

  1. curl -sS https://llm.e2e.one/health → 200.
  2. Import local auth (no token printed). Admin shows email + future expiry.
  3. OpenAI:
    curl https://llm.e2e.one/v1/chat/completions \
      -H "Authorization: Bearer $OTA_KEY" \
      -H "Content-Type: application/json" \
      -d '{"model":"grok-4.6","messages":[{"role":"user","content":"ping"}]}'
    Second identical call with the same x-grok-conv-id must show cached_tokens > 0 or a non-zero write then a read.
  4. Anthropic SDK pointed at https://llm.e2e.one with a cache_control system block. Response includes cache_creation_input_tokens then cache_read_input_tokens on the follow-up.
  5. Admin usage page increments for both protocols; prompt text is absent from D1.
  6. Force-expire the access token: next request still succeeds via DO refresh; a parallel burst does not lose the refresh token.

15. Open questions