# 📡 AIRouter — Documentación de la API

> Base URL: **`https://airouter.dilithium3.com/v1`**
> Formato **OpenAI-compatible**: si ya usas el SDK de OpenAI, solo cambias `baseURL` y `apiKey`.

---

## 🔑 Autenticación

Hay **dos tipos** de autenticación, según el endpoint:

1. **API key de inferencia** (para `/v1/chat/completions` y `/v1/models`):
   ```http
   Authorization: Bearer sk-airouter-xxxxxxxxxxxxxxxxxxxxxxxx
   ```
2. **Token de sesión JWT** (para gestionar tu cuenta: keys, saldo, recargas).
   Ver [sección 3](#3-autenticación-de-usuario-jwt).

- La key se genera **una sola vez** en el panel (solo se guarda su hash).
- Si la pierdes, genera otra; la anterior queda revocable en el dashboard.
- **Sin saldo = `402 Payment Required`**. Recarga antes de consumir.

---

## 1. Chat completions

### `POST /v1/chat/completions`

El endpoint principal. Acepta el mismo cuerpo que OpenAI.

**Petición**

```bash
curl https://airouter.dilithium3.com/v1/chat/completions \
  -H "Authorization: Bearer $AIROUTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-3-7-sonnet",
    "messages": [
      { "role": "system", "content": "Eres un asistente conciso." },
      { "role": "user", "content": "Explica qué es un gateway de IA en una frase." }
    ],
    "max_tokens": 256
  }'
```

**Respuesta**

```json
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "claude-3-7-sonnet",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Es un servicio que..." },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 21,
    "completion_tokens": 12,
    "total_tokens": 33
  }
}
```

### Streaming (SSE)

Añade `"stream": true` y recibirás un flujo **Server-Sent Events** token a token,
idéntico al de OpenAI:

```js
const stream = await ai.chat.completions.create({
  model: "gpt-4o-mini",
  stream: true,
  messages: [{ role: "user", content: "Cuéntame un chiste" }],
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content || "");
}
```

---

## 2. Listar modelos

### `GET /v1/models`

```json
{
  "object": "list",
  "data": [
    { "id": "gpt-4o", "object": "model", "owned_by": "openai" },
    { "id": "claude-3-7-sonnet", "object": "model", "owned_by": "anthropic" }
  ]
}
```

---

## 3. Autenticación de usuario (JWT)

Los endpoints de gestión (crear keys, recargar saldo, consultar tu cuenta) usan un
**token de sesión (JWT)** distinto de la API key de inferencia.

### `POST /v1/auth/register`

```json
// petición
{ "email": "dev@ejemplo.com", "password": "una-clave-segura" }

// respuesta
{ "token": "eyJhbGciOi...", "user_id": "123e4567-..." }
```

### `POST /v1/auth/login`

```json
// petición
{ "email": "dev@ejemplo.com", "password": "una-clave-segura" }

// respuesta
{ "token": "eyJhbGciOi...", "user_id": "123e4567-..." }
```

### `GET /v1/me`

```http
Authorization: Bearer <jwt>
```

```json
{ "user_id": "123e4567-...", "balance_usd": 14.5 }
```

### `GET /v1/usage/summary`

Consumo del usuario autenticado (totales, diario de 7 días y desglose por modelo).

```http
Authorization: Bearer <jwt>
```

```json
{
  "total_cost_usd": 1.234,
  "total_tokens": 152000,
  "daily": [
    { "day": "2026-08-20", "cost_usd": 0.12, "tokens": 8000 },
    { "day": "2026-08-21", "cost_usd": 0.45, "tokens": 31000 }
  ],
  "by_model": [
    { "model": "gpt-4o-mini", "cost_usd": 0.9 },
    { "model": "claude-3-7-sonnet", "cost_usd": 0.33 }
  ]
}
```

- El JWT se firma con `JWT_SECRET` y expira tras `JWT_TTL_SECS` (7 días por defecto).
- Las contraseñas se guardan con **Argon2** (nunca en texto plano).
- Usa el token así: `Authorization: Bearer <jwt>` en `/v1/keys`, `/v1/billing/checkout`, `/v1/me` y `/v1/usage/summary`.

---

## 4. API Keys

### `POST /v1/keys` *(requiere JWT)*

Genera una key nueva. **Se muestra una sola vez.**

```json
// petición (body)
{ "name": "producción" }

// respuesta
{ "key": "sk-airouter-...", "name": "producción" }
```

> Seguridad: solo guardamos `SHA-256(salt + key)`. La original jamás toca la BD.

---

## 5. Facturación (saldo prepago)

### `POST /v1/billing/checkout` *(requiere JWT)*

Crea una sesión de Stripe Checkout y devuelve su URL (rediriges al usuario ahí).

```json
// petición
{ "amount_usd": 20 }

// respuesta
{ "stripe_checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..." }
```

**Flujo completo:**

1. El cliente elige un paquete (`$10`, `$20`, `$50`, `$100`).
2. El frontend llama a `/v1/billing/checkout` y recibe la URL.
3. Redirige al usuario a Stripe (tarjeta / Apple Pay / Google Pay).
4. Stripe redirige de vuelta a `/dashboard?success=true`.
5. **Por detrás**, Stripe envía `POST /webhooks/stripe` (firma `whsec_...` verificada)
   y el gateway **acredita el saldo** en Redis + lo registra en Postgres.

### `POST /webhooks/stripe` *(solo Stripe)*

- Verifica `Stripe-Signature` criptográficamente (nunca confíes en el body).
- Procesa `checkout.session.completed` → `credit_balance`.
- Idempotente: si el pago ya se registró, se ignora.
- Responde `200 OK` rápido (Stripe reenvía si tarda más de lo esperado).

---

## 6. Errores

Formato estándar (OpenAI-compatible):

```json
{
  "error": {
    "message": "Insufficient funds",
    "type": "airouter_error",
    "code": 402
  }
}
```

| Código | Significado |
|:--:|--|
| `401` | API key faltante o inválida |
| `402` | **Saldo insuficiente** — recarga para continuar |
| `404` | Modelo no encontrado |
| `502` | Error del proveedor upstream |
| `500` | Error interno |

---

## 7. Precios y margen

Cobras por **1M de tokens** (entrada y salida por separado), con margen incluido:

```
coste_total = (prompt_tokens / 1e6) * precio_prompt
            + (completion_tokens / 1e6) * precio_completion
```

- Los precios viven en la tabla `ai_models` (Postgres) y se cachean en Redis
  (`model:price:<model_key>`), por lo que el cobro es de microsegundos.
- Cambiar tarifas = `UPDATE` en Postgres, **sin recompilar** el gateway.

---

## 8. SDKs oficiales (drop-in)

```js
// Node.js — cambia solo baseURL y apiKey
import OpenAI from "openai";
const ai = new OpenAI({
  baseURL: "https://airouter.dilithium3.com/v1",
  apiKey: process.env.AIROUTER_KEY,
});
```

```python
# Python
from openai import OpenAI
client = OpenAI(
    base_url="https://airouter.dilithium3.com/v1",
    api_key=os.environ["AIROUTER_KEY"],
)
```

```bash
# cURL
curl https://airouter.dilithium3.com/v1/chat/completions \
  -H "Authorization: Bearer $AIROUTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hola"}]}'
```

---

## 9. Arquitectura del cobro (detalle)

```
1. auth            → hash(API key) → user_id (Redis, caché de Postgres)
2. fondos          → user:<id>:balance > 0 (Redis, µs)
3. delegación      → LiteLLM (formato + conteo de tokens)
4. respuesta+usage → LiteLLM devuelve tokens y respuesta
5. cobro           → script Lua atómico (INCRBYFLOAT -coste) — nunca saldo negativo
6. registro        → usage_logs (Postgres, asíncrono, fire-and-forget)
7. entrega         → respuesta al cliente (o flujo SSE)
```

- **Redis AOF** (`appendfsync everysec`): los saldos sobreviven reinicios.
- **PostgreSQL** = fuente de verdad (users, keys hasheadas, transactions, usage_logs).
- **Dilithium3** = sellado inmutable de recibos mensuales (auditoría).

---

## 10. Notas de producción

- El **cobro en streaming** ya está implementado: el gateway pide
  `stream_options: { "include_usage": true }` a LiteLLM, captura el `usage` del último
  chunk mientras reenvía el flujo, y al terminar (o si el cliente corta) descuenta el
  coste de forma atómica y lo registra en `usage_logs`.
- `/v1/keys`, `/v1/billing/checkout`, `/v1/me` y `/v1/usage/summary` están protegidos
  con **JWT** (extractor `JwtUser`). El token se emite en `/v1/auth/register` y `/v1/auth/login`.
- Un **worker en segundo plano** sincroniza los saldos Redis → Postgres cada
  `SYNC_INTERVAL_SECS` y sella los recibos mensuales en Dilithium3 (diario, idempotente).
