A local-first multi-provider AI gateway that exposes one internal API and routes requests across free-tier LLM providers.
Provide a single, reusable AI endpoint for personal projects so client applications do not need to integrate Groq, Gemini, or Cerebras directly.
AI Gateway is a small TypeScript service built with Hono that:
- accepts a unified AI request shape,
- selects an available provider,
- falls back only for recoverable provider failures,
- exposes lightweight diagnostics for local development.
It is designed for local use first and to stay simple in v1.
The current v1 includes:
- main AI endpoints:
POST /v1/ai/respondandPOST /v1/ai/embed - provider adapters for Groq, Gemini, and Cerebras
- health-aware routing with basic fallback behavior
- in-memory provider state and metrics
- request validation
- structured logging
- diagnostic endpoints:
GET /v1/health,GET /v1/providers,GET /v1/metrics
The following are intentionally excluded from v1:
- streaming responses
- databases, Redis, or other required persistence
- authentication and authorization
- caching
- billing or usage accounting
- dashboards or UI
- deployment infrastructure or Docker requirements
- advanced distributed systems concerns
- zero-cost v1
- no mandatory database in v1
- no streaming in v1
- local-first development
- Groq
- Gemini
- Cerebras
- TypeScript
- Hono
- Zod
- Pino
- Vitest
- Node.js
>=24 - pnpm
>=10
pnpm installCopy the example file and configure any providers you want to enable:
cp .env.example .envAvailable variables:
| Variable | Required | Default | Purpose |
|---|---|---|---|
PORT |
No | 3000 |
HTTP server port |
NODE_ENV |
No | development |
Runtime environment |
LOG_LEVEL |
No | info |
Pino log level |
DEFAULT_TIMEOUT_MS |
No | 15000 |
Per-provider request timeout |
GROQ_API_KEY |
No | empty | Enables Groq when set |
GEMINI_API_KEY |
No | empty | Enables Gemini when set |
CEREBRAS_API_KEY |
No | empty | Enables Cerebras when set |
GROQ_MODEL |
No | llama-3.3-70b-versatile |
Default Groq model |
GEMINI_MODEL |
No | gemini-2.0-flash |
Default Gemini model |
GEMINI_EMBEDDING_MODEL |
No | gemini-embedding-001 |
Default Gemini embedding model |
CEREBRAS_MODEL |
No | llama3.1-8b |
Default Cerebras model |
DEFAULT_EMBEDDING_PROVIDER |
No | gemini |
Preferred provider for embeddings |
Environment source behavior:
pnpm devandpnpm startload.envand.env.local- Cloudflare Workers reads bindings and secrets provided by Wrangler or the Cloudflare dashboard
- the same variable names are used in both runtimes
Provider configuration behavior:
- the server still starts if some provider API keys are missing,
- providers without API keys are marked disabled,
- the gateway remains usable as long as at least one provider is configured correctly.
Start the development server:
pnpm devThis loads .env and .env.local automatically.
Build and run the compiled server:
pnpm build
pnpm startBy default the server listens on http://localhost:3000.
Install dependencies, then create a local Workers env file:
cp .env.example .dev.varsSet any provider API keys you want to enable inside .dev.vars, then run:
pnpm dev:workerWrangler serves the Worker locally and injects the bindings from .dev.vars.
- Authenticate with Cloudflare:
pnpm exec wrangler login- Add required provider secrets for any providers you want enabled:
pnpm exec wrangler secret put GROQ_API_KEY
pnpm exec wrangler secret put GEMINI_API_KEY
pnpm exec wrangler secret put CEREBRAS_API_KEY- Optionally set non-secret vars in the Cloudflare dashboard or with Wrangler for:
NODE_ENVLOG_LEVELDEFAULT_TIMEOUT_MSGROQ_MODELGEMINI_MODELGEMINI_EMBEDDING_MODELCEREBRAS_MODELDEFAULT_EMBEDDING_PROVIDER
- Deploy:
pnpm deploy:worker- At least one provider API key must be configured for the gateway to serve AI responses.
GROQ_API_KEY,GEMINI_API_KEY, andCEREBRAS_API_KEYshould be stored as Cloudflare Worker secrets when used in Workers.- Non-secret settings can rely on defaults unless you need to override them.
- provider state and metrics remain in memory, so they reset on Worker instance restarts
- local Node mode and Workers mode share the same request/response contract, but logs are emitted through
consolein Workers instead of Pino - this setup is intended for simple v1 deployment readiness, not advanced production features such as durable state, streaming, or auth
pnpm test
pnpm typecheck
pnpm buildpnpm dev- run the local development server with watch modepnpm dev:worker- run the Cloudflare Worker locally with Wranglerpnpm build- compile TypeScript intodist/pnpm start- run the compiled server fromdist/index.jspnpm deploy:worker- deploy the Worker with Wranglerpnpm test- run the Vitest suitepnpm typecheck- run TypeScript type checking without emitting filespnpm lint- same astypecheckin the current v1 setup
POST /v1/ai/respondPOST /v1/ai/embed
GET /v1/health- quick readiness-style snapshot of overall gateway/provider availabilityGET /v1/providers- current in-memory provider state for each configured providerGET /v1/metrics- chat/embedding request totals, provider attempt totals, and provider state snapshot
POST /v1/ai/respond accepts this JSON shape:
{
"task": "chat",
"messages": [
{
"role": "user",
"content": "Explain what this gateway does in one sentence."
}
],
"temperature": 0.2,
"maxTokens": 256,
"responseFormat": "text",
"priority": "normal",
"metadata": {
"source": "readme-example"
}
}Field notes:
taskmust be one ofchat,json,summary, orclassificationmessagesmust contain at least one message- each message requires
role(system,user,assistant) and non-emptycontent temperatureis optional and must be between0and2maxTokensis optional and must be a positive integer up to32768responseFormatis optional and must betextorjsonpriorityis optional and must below,normal, orhighmetadatais optional and accepts a JSON object
{
"input": ["first text", "second text"]
}Notes:
inputaccepts either a non-empty string or a non-empty array of non-empty strings- the gateway normalizes string input into a single-item array
- response vectors preserve input order and include an
indexfield per vector - only embedding-capable providers are considered for this route
curl -X POST http://localhost:3000/v1/ai/respond \
-H "content-type: application/json" \
-d '{
"task": "chat",
"messages": [
{
"role": "user",
"content": "Say hello from AI Gateway."
}
],
"temperature": 0.2,
"maxTokens": 64
}'{
"requestId": "req_01hxyzexample",
"data": {
"provider": "groq",
"model": "llama-3.3-70b-versatile",
"text": "Hello from AI Gateway.",
"latencyMs": 482,
"fallbackUsed": false,
"usage": {
"inputTokens": 12,
"outputTokens": 8,
"totalTokens": 20
}
}
}Validation error example:
{
"requestId": "req_01hxyzexample",
"error": {
"type": "BAD_REQUEST",
"message": "Invalid AI request payload",
"details": [
{
"path": "messages.0.content",
"message": "String must contain at least 1 character(s)"
}
]
}
}Malformed JSON example:
{
"requestId": "req_01hxyzexample",
"error": {
"type": "BAD_REQUEST",
"message": "Malformed JSON request body"
}
}Provider availability example:
{
"requestId": "req_01hxyzexample",
"error": {
"type": "UNAVAILABLE",
"message": "No AI providers are currently available"
}
}Successful POST /v1/ai/respond responses always return:
requestIddata.providerdata.modeldata.textdata.latencyMsdata.fallbackUsed- optional
data.usage
Common error types:
BAD_REQUEST- invalid request body or malformed JSONAUTH- provider credentials are invalidRATE_LIMIT- the selected provider rejected the request for quota or rate reasonsTIMEOUT- provider request exceeded timeoutUNAVAILABLE- no provider is currently eligible or all recoverable attempts failedSERVER_ERROR- unexpected internal or provider-side server failureUNKNOWN- provider returned an unmapped error
Practical fallback behavior:
- recoverable failures may trigger fallback to another provider,
- non-recoverable failures such as
BAD_REQUESTorAUTHdo not fall back, - if all recoverable attempts fail, the request returns
503 UNAVAILABLE.
{
"status": "ok",
"providers": {
"total": 3,
"available": 2
}
}status: "ok"means at least one provider is currently availablestatus: "degraded"means no provider is currently available
{
"providers": [
{
"name": "groq",
"enabled": true,
"status": "healthy",
"baseWeight": 4,
"model": "llama-3.3-70b-versatile",
"consecutiveFailures": 0,
"consecutiveSuccesses": 3,
"avgLatencyMs": 420,
"errorRate": 0,
"localRequestsLastMinute": 3,
"localEstimatedTokensLastMinute": 180,
"requestWindowStartedAt": 1760000000000,
"totalAttempts": 3,
"totalFailures": 0,
"totalSuccesses": 3
},
{
"name": "gemini",
"enabled": false,
"status": "disabled",
"baseWeight": 3,
"model": "gemini-2.0-flash",
"disabledReason": "Missing GEMINI_API_KEY",
"consecutiveFailures": 0,
"consecutiveSuccesses": 0,
"avgLatencyMs": 0,
"errorRate": 0,
"localRequestsLastMinute": 0,
"localEstimatedTokensLastMinute": 0,
"requestWindowStartedAt": 1760000000000,
"totalAttempts": 0,
"totalFailures": 0,
"totalSuccesses": 0
}
]
}{
"metrics": {
"startedAt": "2026-04-11T00:00:00.000Z",
"requests": {
"total": 4,
"successes": 3,
"failures": 1,
"fallbackSuccesses": 1
},
"providerAttempts": {
"total": 5,
"successes": 3,
"failures": 2,
"recoverableFailures": 1,
"nonRecoverableFailures": 1
},
"providers": {
"groq": {
"attempts": 3,
"successes": 2,
"failures": 1,
"recoverableFailures": 1,
"nonRecoverableFailures": 0,
"lastErrorType": "RATE_LIMIT"
}
}
},
"providers": []
}GET /v1/metrics is mainly intended for local debugging and inspection, not as a stable analytics API.