npx skills add ...
npx skills add reason-machines/codex-skills --skill codex2api-reverse-proxy
Codex2API is a production-ready reverse proxy that converts Codex account pools into OpenAI/Anthropic-compatible API gateways with account management, scheduling, and admin dashboard.
npx skills add reason-machines/codex-skills --skill codex2api-reverse-proxy
Skill by ara.so — Codex Skills collection.
Codex2API is a Go + Gin + React production gateway that transforms a pool of Codex accounts into observable, schedulable OpenAI/Anthropic-compatible API endpoints. It manages Refresh Token/Access Token lifecycles, health scoring, dynamic concurrency, rate-limit recovery, usage tracking, and admin operations through a built-in dashboard.
/v1/chat/completions, /v1/responses, /v1/messages, /v1/images/generations, /v1/images/edits, and /v1/models endpointsFrontend dev server:
Server:
PostgreSQL Mode:
SQLite Mode:
Redis TLS (Aiven, Upstash, etc.):
After first startup, configure via admin dashboard at /admin/settings:
MaxConcurrency: Global concurrent request limitGlobalRPM: Global requests per minuteTestModel: Model for account health checksTestConcurrency: Test request concurrencyProxyURL: Global proxy (e.g., http://proxy:port)PgMaxConns: PostgreSQL connection pool sizeRedisPoolSize: Redis connection pool sizeResponse:
The scheduler selects accounts based on:
Active (healthy) > Cooldown (recovering) > Inactive (failed)Set per-account proxy:
Global proxy via settings page or environment:
Configure in admin dashboard under "Prompt Filter":
PostgreSQL:
SQLite:
Symptom: API returns 503 or "No available account"
Solutions:
POST /api/admin/accounts/testPostgreSQL:
SQLite:
Symptom: 429 Too Many Requests
Solutions:
max_rpm or max_tpm for the keyGlobalRPM settingSymptom: Requests queue or timeout during high load
Solutions:
MaxConcurrency in settingsCommon issues:
gemini-*-image models work)Symptom: Login fails or /api/admin/* returns 401
Solutions:
ADMIN_SECRET in .env matches login passwordX-Admin-Key header in requestsADMIN_SECRETPostgreSQL mode:
SQLite mode:
Configure in admin settings:
AutoCleanupEnabled: Enable automatic cleanupLogRetentionDays: Keep request logs for N days (default 30)UsageRetentionDays: Keep usage records for N days (default 90)Multiple Codex2API instances can share PostgreSQL + Redis:
Use a load balancer (nginx, Caddy, Traefik) to distribute requests.
Response:
Use in Kubernetes liveness/readiness probes or Docker healthchecks.
codex2api)git clone https://github.com/james-6-23/codex2api.git
cd codex2api
cp .env.sqlite.example .env
# Edit .env if needed
docker compose -f docker-compose.sqlite.yml pull
docker compose -f docker-compose.sqlite.yml up -d
docker compose -f docker-compose.sqlite.yml logs -f codex2apicp .env.example .env
# Start PostgreSQL and Redis containers or configure local instances
cd frontend && npm ci && npm run build && cd ..
go run .cd frontend && npm ci && npm run dev
# Frontend runs at http://localhost:5173/admin/CODEX_PORT=8080
ADMIN_SECRET=your-secure-admin-password
TZ=Asia/ShanghaiDATABASE_DRIVER=postgres
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=codex2api
DATABASE_PASSWORD=secure-db-password
DATABASE_NAME=codex2api
DATABASE_SSLMODE=disable
CACHE_DRIVER=redis
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=secure-redis-password
REDIS_DB=0DATABASE_DRIVER=sqlite
DATABASE_PATH=/data/codex2api.db
CACHE_DRIVER=memory# Prefer rediss:// URL format
REDIS_ADDR=rediss://default:password@host:port/0
# OR for host:port format
REDIS_ADDR=host:port
REDIS_TLS=true
REDIS_INSECURE_SKIP_VERIFY=false # Set true only for self-signed certs
REDIS_USERNAME=default
REDIS_PASSWORD=your-passwordpackage main
import (
"context"
"fmt"
"os"
"github.com/sashabaranov/go-openai"
)
func main() {
config := openai.DefaultConfig(os.Getenv("CODEX2API_KEY"))
config.BaseURL = "http://localhost:8080/v1"
client := openai.NewClientWithConfig(config)
resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: "claude-code",
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: "Explain how Codex2API account scheduling works",
},
},
},
)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println(resp.Choices[0].Message.Content)
}import os
import anthropic
client = anthropic.Anthropic(
api_key=os.environ.get("CODEX2API_KEY"),
base_url="http://localhost:8080/v1"
)
message = client.messages.create(
model="claude-code",
max_tokens=1024,
messages=[
{"role": "user", "content": "Explain dynamic concurrency in Codex2API"}
]
)
print(message.content[0].text)curl -X POST http://localhost:8080/v1/responses \
-H "Authorization: Bearer $CODEX2API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-code",
"messages": [
{
"role": "user",
"content": "Write a hello world in Go"
}
],
"stream": false
}'curl -X POST http://localhost:8080/v1/images/generations \
-H "Authorization: Bearer $CODEX2API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A serene mountain landscape at sunset",
"model": "gemini-2.0-flash-exp-image",
"n": 1,
"size": "1024x1024"
}'curl -X POST http://localhost:8080/v1/images/edits \
-H "Authorization: Bearer $CODEX2API_KEY" \
-F "image=@original.png" \
-F "prompt=Add a rainbow in the sky" \
-F "model=gemini-2.0-flash-exp-image" \
-F "n=1"curl http://localhost:8080/v1/models \
-H "Authorization: Bearer $CODEX2API_KEY"curl -X POST http://localhost:8080/api/admin/accounts/upload \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"tokens": [
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
]
}'curl -X POST http://localhost:8080/api/admin/accounts/upload \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"tokens": [
"sess-abc123...",
"sess-def456..."
],
"type": "access_token"
}'# Test all accounts
curl -X POST http://localhost:8080/api/admin/accounts/test \
-H "X-Admin-Key: $ADMIN_SECRET"
# Test specific account
curl -X POST http://localhost:8080/api/admin/accounts/test \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"account_id": "550e8400-e29b-41d4-a716-446655440000"}'curl http://localhost:8080/api/admin/accounts \
-H "X-Admin-Key: $ADMIN_SECRET"curl -X DELETE http://localhost:8080/api/admin/accounts/550e8400-e29b-41d4-a716-446655440000 \
-H "X-Admin-Key: $ADMIN_SECRET"curl -X POST http://localhost:8080/api/admin/apikeys \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Client",
"key": "sk-custom-key-123",
"max_rpm": 100,
"max_tpm": 50000
}'{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production Client",
"key": "sk-custom-key-123",
"max_rpm": 100,
"max_tpm": 50000,
"enabled": true,
"created_at": "2026-05-16T10:30:00Z"
}curl http://localhost:8080/api/admin/apikeys \
-H "X-Admin-Key: $ADMIN_SECRET"curl -X PATCH http://localhost:8080/api/admin/apikeys/550e8400-e29b-41d4-a716-446655440000 \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'// Scheduler picks account with highest score among eligible candidates
// Example internal scoring (simplified):
score := (successRate * 0.5) +
(1.0 - recentFailureRate * 0.3) +
(timeSinceLastSuccess * 0.2)# Accounts are tested with TestModel (configured in settings)
# Default: claude-code
# Test sends minimal completion request and validates response
# Health states:
# - Active: Last test succeeded, ready for requests
# - Cooldown: Recent failure, waiting for recovery
# - Inactive: Multiple consecutive failures, excluded from schedulingcurl -X PATCH http://localhost:8080/api/admin/accounts/550e8400-e29b-41d4-a716-446655440000 \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"proxy_url": "http://proxy.example.com:8080"}'# .env
PROXY_URL=http://global-proxy:8080import os
import anthropic
client = anthropic.Anthropic(
api_key=os.environ.get("CODEX2API_KEY"),
base_url="http://localhost:8080/v1"
)
with client.messages.stream(
model="claude-code",
max_tokens=1024,
messages=[{"role": "user", "content": "Count to 10"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True){
"enabled": true,
"rules": [
{
"pattern": "(?i)nuclear",
"action": "block",
"message": "Content violates policy"
},
{
"pattern": "(?i)medical advice",
"action": "warn",
"message": "Consider consulting a professional"
}
]
}# Start
docker compose up -d
# View logs
docker compose logs -f codex2api
# Restart
docker compose restart codex2api
# Stop
docker compose down
# Update to latest
docker compose pull && docker compose up -d# Start
docker compose -f docker-compose.sqlite.yml up -d
# Logs
docker compose -f docker-compose.sqlite.yml logs -f codex2api
# Update
docker compose -f docker-compose.sqlite.yml pull
docker compose -f docker-compose.sqlite.yml up -d# Backup
docker exec codex2api-postgres pg_dump -U codex2api codex2api > backup_$(date +%Y%m%d_%H%M%S).sql
# Restore
docker exec -i codex2api-postgres psql -U codex2api codex2api < backup_20260516_103000.sql# Backup (requires running container)
docker exec codex2api sqlite3 /data/codex2api.db ".backup /data/backup_$(date +%Y%m%d_%H%M%S).db"
# Or copy from host (if /data is mounted)
cp /path/to/data/codex2api.db /path/to/backup/codex2api_$(date +%Y%m%d_%H%M%S).db# Check account status
curl http://localhost:8080/api/admin/accounts \
-H "X-Admin-Key: $ADMIN_SECRET" | jq '.[] | {id, email, status, health_tier}'
# Force test all accounts
curl -X POST http://localhost:8080/api/admin/accounts/test \
-H "X-Admin-Key: $ADMIN_SECRET"# Test connection
docker exec codex2api-postgres pg_isready -U codex2api
# Check logs
docker logs codex2api-postgres
# Verify .env settings match docker-compose.yml
grep DATABASE_ .env# Check file permissions
docker exec codex2api ls -la /data/codex2api.db
# Verify mount point
docker inspect codex2api | jq '.[0].Mounts'# Test Redis
docker exec codex2api-redis redis-cli ping
# Check authentication
docker exec codex2api-redis redis-cli -a "$REDIS_PASSWORD" ping
# For TLS issues with cloud Redis
# Ensure REDIS_ADDR uses rediss:// or REDIS_TLS=true
# Check REDIS_INSECURE_SKIP_VERIFY if using self-signed certs# Update API key limits
curl -X PATCH http://localhost:8080/api/admin/apikeys/YOUR_KEY_ID \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"max_rpm": 200, "max_tpm": 100000}'# Check current concurrency
curl http://localhost:8080/api/admin/stats \
-H "X-Admin-Key: $ADMIN_SECRET" | jq '.current_concurrency'# Verify image-capable models
curl http://localhost:8080/v1/models \
-H "Authorization: Bearer $CODEX2API_KEY" | jq '.data[] | select(.id | contains("image"))'# Check current admin secret source
docker exec codex2api env | grep ADMIN_SECRET
# Restart to apply .env changes
docker compose restart codex2api# Reduce connection pool size in settings
# Default PgMaxConns: 25, RedisPoolSize: 10
# Monitor resource usage
docker stats codex2api codex2api-postgres codex2api-redis# SQLite is single-threaded; for high concurrency use PostgreSQL
# Check database file size
docker exec codex2api du -h /data/codex2api.db
# Run VACUUM to reclaim space
docker exec codex2api sqlite3 /data/codex2api.db "VACUUM;"# Update via settings API
curl -X PATCH http://localhost:8080/api/admin/settings \
-H "X-Admin-Key: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"test_model": "claude-sonnet-4"}'# docker-compose.yml
services:
codex2api-1:
image: ghcr.io/james-6-23/codex2api:latest
environment:
- DATABASE_HOST=postgres
- REDIS_ADDR=redis:6379
codex2api-2:
image: ghcr.io/james-6-23/codex2api:latest
environment:
- DATABASE_HOST=postgres
- REDIS_ADDR=redis:6379curl http://localhost:8080/health{
"status": "healthy",
"database": "ok",
"cache": "ok",
"timestamp": "2026-05-16T10:30:00Z"
}