commit 65eb285b668fa32a27bcad3e1866df77310e9c1d Author: Stéphane Tailland Date: Sun Apr 26 13:58:04 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..493e5ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Secrets +compose/.env + +# Claude Code +.claude/ + +# macOS +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..91c00aa --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# pm-aigateway + +Self-hosted AI gateway powered by [agentgateway](https://agentgateway.dev) — a unified, OpenAI-compatible proxy for LLM providers, MCP servers, and A2A agents. + +## Project goal + +Provide a single API endpoint that routes LLM requests to multiple providers (OpenAI, Anthropic, etc.) with centralized auth and observability. Stateless to start; extend to MCP and A2A as needed. + +## Deployment targets + +| Target | Status | +|--------|--------| +| Docker Compose | **current** — single stateless container | +| Kubernetes | planned | + +## Stack + +- **agentgateway** — the sole service; stateless, single binary in a container + +No database or cache needed for the current scope. + +## Repository layout + +``` +. +├── CLAUDE.md +├── compose/ +│ ├── docker-compose.yml +│ └── .env.example +└── config/ + └── config.yaml # model list and routing rules +``` + +## Key configuration + +All provider API keys are supplied via environment variables (never committed). +Copy `compose/.env.example` → `compose/.env` and fill in values. + +Required env vars: +- `OPENAI_API_KEY` +- `ANTHROPIC_API_KEY` + +agentgateway reads `config/config.yaml` on startup. Restart the container to pick up changes. + +## Common commands + +```bash +# Start +docker compose -f compose/docker-compose.yml --env-file compose/.env up -d + +# Tail logs +docker compose -f compose/docker-compose.yml logs -f + +# Restart after config change +docker compose -f compose/docker-compose.yml restart + +# Stop +docker compose -f compose/docker-compose.yml down +``` + +## Ports + +| Endpoint | Port | +|----------|------| +| OpenAI-compatible API | 4000 | +| agentgateway UI | 15000 | + +## Test + +```bash +curl -s http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "Hello"}] + }' | jq . +``` + +## Coding conventions + +- Secrets in `compose/.env` only — gitignored. `compose/.env.example` is committed. +- Pin the agentgateway image tag; avoid `latest` in production. +- Keep `config/config.yaml` as the single source of truth for model/routing config. + +## Future extensions + +- **MCP gateway**: add `mcp.servers` block to `config.yaml` to expose tools +- **A2A gateway**: add `a2a` block for agent-to-agent routing +- **Persistence**: add Postgres if spend tracking or virtual keys are needed +- **Kubernetes**: translate compose to k8s manifests under `k8s/` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8d62cd2 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +# Container runtime: override with `make COMMAND=podman` +COMMAND := docker + +COMPOSE_FILE := compose/docker-compose.yml +ENV_FILE := compose/.env +API_URL := http://localhost:4000 + +# ─── Docker Compose ─────────────────────────────────────────────────────────── + +docker-up: ## Start the stack (detached) + $(COMMAND) compose -f $(COMPOSE_FILE) --env-file $(ENV_FILE) up -d + +docker-down: ## Stop and remove containers + $(COMMAND) compose -f $(COMPOSE_FILE) --env-file $(ENV_FILE) down + +docker-restart: docker-down docker-up ## Full restart (re-reads env vars and config) + +docker-logs: ## Tail logs + $(COMMAND) compose -f $(COMPOSE_FILE) logs -f + +docker-ps: ## Show container status + $(COMMAND) compose -f $(COMPOSE_FILE) ps + +docker-test: ## Send a test request to each configured model + @echo "→ claude-sonnet-4-6 (Anthropic)" + @curl -sf $(API_URL)/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"ping"}]}' \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['choices'][0]['message']['content'])" + @echo "→ or-gpt-5.5 (OpenRouter)" + @curl -sf $(API_URL)/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"or-gpt-5.5","messages":[{"role":"user","content":"ping"}]}' \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['choices'][0]['message']['content'])" + +docker-ui: ## Open the agentgateway UI in the browser + open http://localhost:15000/ui + +# ─── Help ───────────────────────────────────────────────────────────────────── + +.PHONY: docker-up docker-down docker-restart docker-logs docker-ps docker-test docker-ui help + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) \ + | awk 'BEGIN {FS = ":.*##"}; {printf " %-20s %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help diff --git a/compose/.env.example b/compose/.env.example new file mode 100644 index 0000000..e6bc8db --- /dev/null +++ b/compose/.env.example @@ -0,0 +1,3 @@ +OPENAI_API_KEY=sk-... +ANTHROPIC_API_KEY=sk-ant-... +OPENROUTER_API_KEY=sk-or-... diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..d59713e --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,13 @@ +services: + agentgateway: + image: ghcr.io/agentgateway/agentgateway:v1.1.0 + ports: + - "4000:4000" # OpenAI-compatible API + - "15000:15000" # UI + volumes: + - ../config:/etc/agentgateway:ro + command: ["-f", "/etc/agentgateway/config-binds.yaml"] + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + restart: unless-stopped diff --git a/config/config-binds.yaml b/config/config-binds.yaml new file mode 100644 index 0000000..6b4428a --- /dev/null +++ b/config/config-binds.yaml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://agentgateway.dev/schema/config +# Format binds — workaround pour le bug UI "e.binds.forEach" +# Note: OpenRouter (hostOverride) incompatible avec le format binds, voir config.yaml + +config: + adminAddr: 0.0.0.0:15000 + +binds: + - port: 4000 + listeners: + - routes: + - backends: + - ai: + name: claude-sonnet-4-6 + provider: + anthropic: + model: claude-sonnet-4-6 + policies: + backendAuth: + key: "$ANTHROPIC_API_KEY" diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..440780e --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=https://agentgateway.dev/schema/config + +config: + adminAddr: 0.0.0.0:15000 + +llm: + models: + - name: claude-sonnet-4-6 + provider: anthropic + params: + model: claude-sonnet-4-6 + apiKey: "$ANTHROPIC_API_KEY" + + - name: or-gpt-5.5 + provider: openAI + params: + model: openai/gpt-5.5 + hostOverride: "openrouter.ai:443" + pathOverride: "/api/v1/chat/completions" + apiKey: "$OPENROUTER_API_KEY" + backendTLS: {}