Modern proxy to route API requests through the Cloudflare AI Gateway.
- β
On-the-fly decryption of
ai.json.encwhen the worker starts (embedded in the bundle) - β
User validation using keys stored in KV (
userskey) - β Multi-provider routing (Groq, SambaNova, Anthropic, OpenAI, Gemini)
- β Backward compatibility with both legacy request formats
- β Forwarding through Cloudflare AI Gateway with automatic model ID prefixing
- β Optional rate limiting via Durable Objects
- β Preconfigured CORS
- β Transparent streaming support
- β
Build automation β
ai.json.encis automatically converted to TypeScript
cp .dev.vars.example .dev.vars
# Fill in the values:
# - CLOUDFLARE_ACCOUNT_ID
# - AI_JSON_CRYPTOKEN (decryption token for ai.json.enc)
# - CLOUDFLARE_AIG_TOKEN (Cloudflare AI Gateway token)The src/config/ai.json.enc file must be:
- Encrypted with
openssl enc -aes-256-cbc -a -pbkdf2 -iter 100000 - Using the same
AI_JSON_CRYPTOKENas theAI_JSON_CRYPTOKENenv variable - Containing valid JSON with the
AiConfigstructure:
{
"version": 1,
"providers": {
"groq": {
"protocol": "openai",
"endpoint": "https://api.groq.com/openai/v1",
"gatewayEndpoint": "https://gateway.ai.cloudflare.com/v1/{account}/default/compat",
"gatewayModelPrefix": "groq",
"gatewayKey": "optional_gateway_key",
"keys": [
{ "key": "gsk_xxx...", "owner": "ronan", "type": "paid" }
],
"models": [
{
"id": "llama-3.3-70b-versatile",
"contextWindow": 8192,
"maxOutputTokens": 2048,
"tpmLimit": null,
"priority": 1,
"tags": ["fast", "reasoning"]
}
]
},
"sambanova": {
"protocol": "openai",
"endpoint": "https://api.sambanova.ai/api/chat/completions",
"gatewayEndpoint": "https://gateway.ai.cloudflare.com/v1/{account}/default/compat",
"gatewayModelPrefix": "custom-sambanova",
"keys": [
{ "key": "xxxxxxxxxxxxxxxxx" }
],
"models": [
{
"id": "Meta-Llama-3.3-70B-Instruct",
"contextWindow": 4096,
"maxOutputTokens": 2048,
"tpmLimit": null,
"priority": 1
}
]
}
}
}Load valid users into KV (KV_AI_PROXY), key users:
wrangler kv:key put users '{"ronan":{"key":"AGE-SECRET-KEY-..."},"audrey":{"key":"AGE-SECRET-KEY-..."},...}' --namespace-id=0f6936bc4d9b4d5fa1cc85acd757e354For development, keys are read from users.json if KV is empty.
curl -X POST https://ai-proxy.inet.pp.ua/groq/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer AGE-SECRET-KEY-..." \
-d '{
"model": "llama-3.3-70b-versatile",
"messages": [{"role": "user", "content": "Hello!"}]
}'curl -X POST https://ai-proxy.inet.pp.ua/openai/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer AGE-SECRET-KEY-..." \
-H "X-Host-Final: api.groq.com" \
-d '{
"model": "llama-3.3-70b-versatile",
"messages": [{"role": "user", "content": "..."}]
}'The proxy detects the provider using:
- Path prefix (priority):
/groq/,/sambanova/,/anthropic/,/openai/,/gemini/ X-Host-Finalheader (fallback):api.groq.com,api.sambanova.ai, etc.
If neither can be determined, a 400 error is returned.
Client request
β
[Bearer token validation]
β
[ai.json.enc decryption] (cached)
β
[Provider detection]
β
[Provider API key selection] (round-robin)
β
[Model ID prefixing for gateway]
β
Cloudflare AI Gateway
β
Final provider (Groq, SambaNova, etc.)
npm run dev
# Listens on http://localhost:8787
# Automatically runs: scripts/embed-config.js -> src/lib/embedded-config.tsnpm run deploynpm testThe scripts/embed-config.js script runs automatically before every build/dev:
- Reads
src/config/ai.json.enc(encrypted binary file) - Converts it to a JSON string
- Generates
src/lib/embedded-config.tswith that content - Imports that content into
src/index.ts - Wrangler embeds everything into the worker bundle
This process avoids managing file assets at runtime.
Force regeneration:
node scripts/embed-config.jsThe sample_request.sh file contains two working examples:
/openai/v1/chat/completionsroute withX-Host-Final: api.groq.com/v1/chat/completionsroute withX-Host-Final: api.sambanova.ai
Run the examples:
source .dev.vars
./sample_request.sh(Replace keys with real user keys in users.json)
# 1. Create ai.json with the AiConfig structure
cat > ai.json << 'EOF'
{
"version": 1,
"providers": { ... }
}
EOF
# 2. Encrypt with openssl
AI_JSON_CRYPTOKEN="your_secret_token"
openssl enc -aes-256-cbc -a -pbkdf2 -iter 100000 -salt \
-in ai.json -out ai.json.enc -pass pass:"$AI_JSON_CRYPTOKEN"
# 3. Copy to src/config/
cp ai.json.enc src/config/ai.json.enc
# 4. Remove plaintext file
rm ai.jsonopenssl enc -d -aes-256-cbc -a -in ai.json.enc -pass pass:"$AI_JSON_CRYPTOKEN" -out ai.jsonai-proxy-cloudflare/
βββ src/
β βββ index.ts # Main Hono app
β βββ config/
β β βββ ai.json.enc # Encrypted config (bundled)
β βββ lib/
β βββ ai-enc.ts # Decryption & helpers
β βββ auth.ts # Bearer token validation
β βββ gateway.ts # Forwarding to Cloudflare AI Gateway
βββ wrangler.jsonc # Cloudflare Workers config
βββ package.json
βββ tsconfig.json
βββ .dev.vars.example
βββ sample_request.sh
| Var | Source | Description |
|---|---|---|
CLOUDFLARE_ACCOUNT_ID |
.dev.vars / Wrangler secret | Your Cloudflare account ID |
AI_JSON_CRYPTOKEN |
.dev.vars / Wrangler secret | Decryption token for ai.json.enc |
CLOUDFLARE_AIG_TOKEN |
.dev.vars / Wrangler secret | Cloudflare AI Gateway token |
DEBUG |
.dev.vars (optional) | true for verbose logs |
To deploy in production:
wrangler secret put CLOUDFLARE_ACCOUNT_ID
wrangler secret put AI_JSON_CRYPTOKEN
wrangler secret put CLOUDFLARE_AIG_TOKENSee vitest.config.mts for test configuration.
npm testAGPL-3.0-or-later
Copyright Β© 2024-2026 Ronan LE MEILLAT