Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions docs/AI-GATEWAY-CONFIG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AI Gateway Proxy — Configuration Guide

The AI Gateway Worker proxies LLM requests from the OpenClaw gateway to Anthropic and OpenAI. Provider credentials are stored per-user in Cloudflare KV and managed via the self-service `/config` UI.
The AI Gateway Worker proxies LLM requests from the OpenClaw gateway to upstream LLM providers. Supports Anthropic, OpenAI, and 11 additional OpenAI-compatible providers (DeepSeek, Groq, Mistral, Together, xAI, OpenRouter, Perplexity, Cohere, Fireworks, MiniMax, Moonshot). Provider credentials are stored per-user in Cloudflare KV and managed via the self-service `/config` UI.

The worker supports two upstream routing modes, auto-detected based on which secrets are configured.

Expand Down Expand Up @@ -32,18 +32,21 @@ The worker auto-detects which mode to use: if `CF_AI_GATEWAY_TOKEN`, `CF_AI_GATE

Visit the config UI at `https://<AI_GATEWAY_WORKER_URL>/config` and authenticate with your gateway token.

The config page supports four credential types:
The config page supports four credential types for the legacy providers, plus API key fields for 11 additional providers under the collapsible "Additional Providers" section:

| Provider | Credential | Field | Notes |
|----------|-----------|-------|-------|
| Anthropic | API Key | `sk-ant-api-*` | Standard API key |
| Anthropic | OAuth Token | `sk-ant-oat-*` | Claude Code subscription token (takes priority over API key) |
| OpenAI | API Key | `sk-*` | Standard API key |
| OpenAI | Codex OAuth | Paste `.codex/auth.json` | Codex subscription (takes priority over API key, auto-refreshes) |
| Additional | API Key | Per-provider | Cohere, DeepSeek, Fireworks, Groq, MiniMax, Mistral, Moonshot, OpenRouter, Perplexity, Together, xAI |

Credentials are stored in Cloudflare KV — they never touch the VPS. Changes take effect immediately.

For each provider, OAuth/subscription credentials take priority over static API keys. You can have both configured as a fallback.
For Anthropic and OpenAI, OAuth/subscription credentials take priority over static API keys. You can have both configured as a fallback.

Additional providers use `/proxy/{provider}/v1/...` routes (e.g., `/proxy/deepseek/v1/chat/completions`).

---

Expand Down Expand Up @@ -116,6 +119,15 @@ curl -s https://<AI_GATEWAY_WORKER_URL>/openai/v1/chat/completions \
-d '{"model":"gpt-4o-mini","max_tokens":10,"messages":[{"role":"user","content":"Say hi"}]}'
```

### Test a generic provider (e.g., DeepSeek)

```bash
curl -s https://<AI_GATEWAY_WORKER_URL>/proxy/deepseek/v1/chat/completions \
-H "Authorization: Bearer <AI_GATEWAY_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-chat","max_tokens":10,"messages":[{"role":"user","content":"Say hi"}]}'
```

### Verify CF AI Gateway analytics (CF AI Gateway mode only)

1. Go to **Cloudflare Dashboard** -> **AI** -> **AI Gateway** -> your gateway
Expand Down
147 changes: 147 additions & 0 deletions docs/PROPOSAL-GENERIC-PROVIDERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Proposal: feat/generic-providers — Universal Multi-Provider LLM Gateway

**Status:** Implemented
**Branch:** `feat/generic-providers`
**Depends on:** None

---

## Problem

The AI Gateway Worker routes requests to 3 hardcoded providers (Anthropic, OpenAI, OpenAI-Codex). Adding a new provider requires code changes.

---

## Goal

Generalize the Worker into a universal authenticated LLM proxy — 11 additional OpenAI-compatible providers, zero code changes per provider.

---

## Value

- **11 additional providers** (DeepSeek, Groq, Mistral, Together, xAI, OpenRouter, Perplexity, Cohere, Fireworks, MiniMax, Moonshot) available through a single authenticated endpoint
- **Centralized credential management** — all provider API keys in Cloudflare KV, managed via the existing `/config` UI
- **Single gateway token** — clients authenticate once; the Worker resolves the real provider key per request
- **Unified telemetry** — all provider traffic logged through Llemtry, regardless of upstream provider
- **Security boundary** — real API keys never leave KV; clients only see the gateway token
- **No external dependencies** — works immediately with OpenClaw's existing `models.providers` config

Users can add a DeepSeek or Groq key via the Config UI and start using those providers from OpenClaw within minutes.

---

## Architecture

```
OpenClaw agent
→ AI Gateway Worker (Cloudflare)
[validates gateway token, fetches provider key from KV]
→ Real provider API (DeepSeek, Groq, Mistral, etc.)
```

OpenClaw's `models.providers` config points at `/proxy/{provider}` routes on the Worker:

```jsonc
// openclaw.jsonc
"models": {
"providers": {
"deepseek": {
"baseUrl": "${AI_GATEWAY_URL}/proxy/deepseek",
"models": []
}
}
}
```

---

## Provider Support

11 generic providers (all OpenAI-compatible). Z.AI excluded — its path (`/api/paas/v4/`) is incompatible with the standard `/v1/` pattern.

| Provider | Base URL | Auth | Notes |
|-------------|---------------------------------------|--------|-------------------------|
| cohere | `https://api.cohere.ai/compatibility` | Bearer | `/compatibility` prefix |
| deepseek | `https://api.deepseek.com` | Bearer | Standard |
| fireworks | `https://api.fireworks.ai/inference` | Bearer | `/inference` prefix |
| groq | `https://api.groq.com/openai` | Bearer | `/openai` prefix |
| minimax | `https://api.minimax.io` | Bearer | Standard |
| mistral | `https://api.mistral.ai` | Bearer | Standard |
| moonshot | `https://api.moonshot.ai` | Bearer | `.ai` not `.cn` |
| openrouter | `https://openrouter.ai/api` | Bearer | `/api` prefix |
| perplexity | `https://api.perplexity.ai` | Bearer | No `/v1/models` |
| together | `https://api.together.xyz` | Bearer | Standard |
| xai | `https://api.x.ai` | Bearer | Standard |

All 11 use `Authorization: Bearer <key>`. No provider-specific headers required.

Plus 3 existing legacy providers (anthropic, openai, openai-codex) on their existing static routes.

### URL Construction

```
Request: POST /proxy/groq/v1/chat/completions
──── ─────────────────────────
provider directPath

Target: https://api.groq.com/openai/v1/chat/completions
───────────────────────────── ─────────────────────
PROVIDER_DEFAULTS["groq"] directPath
```

Base URLs do NOT include `/v1` — the `directPath` from route matching provides it.

### Allowed Endpoints (whitelist)

- `POST /proxy/{provider}/v1/chat/completions`
- `POST /proxy/{provider}/v1/embeddings`
- `GET /proxy/{provider}/v1/models`

No `v1/responses` (OpenAI-specific). No `v1/messages` (Anthropic-specific).

---

## Implementation

### Files changed

| File | Change |
|------|--------|
| `types.ts` | `LegacyProvider` type alias, `GenericRouteMatch` export, `providers` field on `UserCredentials` |
| `routing.ts` | `GENERIC_PROVIDERS` set, `GENERIC_ENDPOINTS` whitelist, `matchGenericRoute()` |
| `config.ts` | `PROVIDER_DEFAULTS` record (11 base URLs), `getGenericProviderConfig()` |
| `keys.ts` | `getGenericApiKey()` reads from `creds.providers[provider].apiKey` |
| `providers/generic.ts` | New — OpenAI-compatible passthrough proxy |
| `index.ts` | Generic route handling: auth → key lookup → `proxyGeneric()` → Llemtry |
| `admin.ts` | `mergeCredentials`/`maskCredentials` extended for `providers` field |
| `config-ui.ts` | Collapsible "Additional Providers" section, 11 API key fields |
| `llemtry.ts` | `ReportOptions.provider` widened to `string` |

### KV Schema (additive, no migration)

```json
{
"anthropic": { "apiKey": "...", "oauthToken": "..." },
"openai": { "apiKey": "...", "oauth": { ... } },
"providers": {
"deepseek": { "apiKey": "..." },
"groq": { "apiKey": "..." }
}
}
```

### Security

- **Endpoint whitelist:** 3 paths only — no arbitrary upstream path probing
- **Provider whitelist:** 11 known providers — no proxying to arbitrary hosts
- **No key, no call:** 401 if `apiKey` is falsy after KV lookup
- **Token isolation:** Gateway token never forwarded — fresh `Authorization` built from KV key
- **KV isolation:** `providers.*` namespace can't collide with legacy keys

### Backward Compatibility

- Existing static routes unchanged
- Existing KV credential format unchanged — `providers` is additive
- Existing OAuth flows unchanged
- Legacy code paths untouched — generic uses separate functions
9 changes: 6 additions & 3 deletions workers/ai-gateway/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# AI Gateway Proxy Worker

Cloudflare Worker that proxies LLM API calls to Anthropic and OpenAI. Sits between the OpenClaw gateway and providers without changing request/response formats. Routes directly to provider APIs by default, or optionally through [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) for observability, caching, and rate limiting.
Cloudflare Worker that proxies LLM API calls to upstream providers. Supports 3 legacy providers (Anthropic, OpenAI, OpenAI-Codex) on static routes and 11 generic OpenAI-compatible providers (DeepSeek, Groq, Mistral, Together, xAI, OpenRouter, Perplexity, Cohere, Fireworks, MiniMax, Moonshot) via `/proxy/{provider}/...` routes. Routes directly to provider APIs by default, or optionally through [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) for observability, caching, and rate limiting.

```
Direct mode (default):
OpenClaw Gateway → Worker (auth, key swap) → Anthropic / OpenAI
OpenClaw Gateway → Worker (auth, key swap) → Provider API

CF AI Gateway mode (optional):
OpenClaw Gateway → Worker (auth, URL rewrite) → Cloudflare AI Gateway → Anthropic / OpenAI
OpenClaw Gateway → Worker (auth, URL rewrite) → Cloudflare AI Gateway → Provider API
```

Streaming works transparently — request and response bodies are passed through as `ReadableStream` without parsing.
Expand All @@ -29,6 +29,9 @@ Streaming works transparently — request and response bodies are passed through
| `/openai/v1/embeddings` | POST | User token | OpenAI proxy |
| `/openai/v1/models` | GET | User token | OpenAI proxy |
| `/anthropic/v1/messages` | POST | User token | Anthropic proxy |
| `/proxy/{provider}/v1/chat/completions` | POST | User token | Generic provider proxy |
| `/proxy/{provider}/v1/embeddings` | POST | User token | Generic provider proxy |
| `/proxy/{provider}/v1/models` | GET | User token | Generic provider proxy |

## Auth

Expand Down
35 changes: 35 additions & 0 deletions workers/ai-gateway/src/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,14 @@ function maskCredentials(creds: UserCredentials): Record<string, unknown> {
if (Object.keys(o).length > 0) result.openai = o
}

if (creds.providers) {
const p: Record<string, unknown> = {}
for (const [name, entry] of Object.entries(creds.providers)) {
if (entry.apiKey) p[name] = { apiKey: maskString(entry.apiKey) }
}
if (Object.keys(p).length > 0) result.providers = p
}

return result
}

Expand Down Expand Up @@ -437,6 +445,33 @@ function mergeCredentials(
}
}

if ('providers' in update) {
if (update.providers === null) {
delete result.providers
} else {
const u = update.providers as Record<string, unknown>
if (!result.providers) result.providers = {}
for (const [name, value] of Object.entries(u)) {
if (value === null) {
delete result.providers[name]
} else {
const entry = value as Record<string, unknown>
if ('apiKey' in entry) {
if (entry.apiKey === null) {
delete result.providers[name]
} else {
result.providers[name] = { apiKey: entry.apiKey as string }
}
}
}
}
// Clean up empty providers section
if (Object.keys(result.providers).length === 0) {
delete result.providers
}
}
}

return result
}

Expand Down
Loading