Overview
See also:
- docs/api/ingest.md
- docs/api/metrics.md
- docs/api/projects.md
- docs/api/internal-pg-sync.md
- docs/api/badge-experiment.md
POST /api/ingest¶
- Auth (OSS template):
Authorization: Bearer <INGEST_API_KEY>or headerx-api-key: <INGEST_API_KEY>projectIdis required in the request body.- TODO: Switch to per‑project API keys (see
convex/apiKeys.ts) and derive project from the key. - Rate limiting:
RF_RATE_LIMIT_PER_MINUTEper project (default 1000/min). - Privacy: do not send prompts or outputs; only usage/cost/latency/metadata.
- Request supports single or batch:
// excerpt from app/api/ingest/route.ts const LLMRunInputSchema = z.object({ id: z.string().optional(), provider: z.string(), model: z.string(), // privacy: do not send prompt/completion prompt: z.string().optional(), completion: z.string().optional(), tokens: z.object({ input: z.number(), output: z.number() }).optional(), cost: z.number().optional(), latency: z.number().optional(), timestamp: z.string().optional(), metadata: z.record(z.string(), z.any()).optional(), inputTokens: z.number().int().nonnegative().optional(), outputTokens: z.number().int().nonnegative().optional(), costUSD: z.number().nonnegative().optional(), latencyMs: z.number().int().nonnegative().optional(), status: z.enum(["success","error","timeout"]).optional(), errorCode: z.string().optional(), promptHash: z.string().optional(), promptPreview: z.string().max(160).optional(), experimentId: z.string().optional(), traceId: z.string().optional(), idempotencyKey: z.string().optional(), runId: z.string().optional(), // cost tracking costSource: z.enum(["provider", "catalog", "estimated"]).optional(), costEstimated: z.boolean().optional(), }) - Cost recompute behavior (server):
- OpenRouter: if
providerProvidesDirectCost(provider)andcostUSD>0, trust provider cost →costSource="provider",costEstimated=false. - Others or when cost missing: compute from registry →
calculateCostFromTokens(model, inputTokens, outputTokens)→costSource="catalog",costEstimatedreflects pricing entry. - Responses:
- Single:
{ ok: true, id } - Batch:
{ ok: true, processed, ids }
Example (single):
curl -X POST "$BASE/api/ingest" \
-H "Authorization: Bearer $INGEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "<projectId>",
"provider": "openai", "model": "gpt-4o",
"inputTokens": 100, "outputTokens": 50,
"costUSD": 0, "latencyMs": 1200, "status": "success"
}'
GET /api/metrics¶
- Auth: Bearer project key (dev mocked in
lib/auth.tswhenRUNFORGE_DEV_MODE=1). - Query params:
range=24h|7d|30d(default7d)projectId=<id>(optional; required when auth cannot infer project)aggregate=1|true(optional; when set, aggregates across all projects)- Response shape:
{ "points": [ { "t": "2025-01-01T01:00:00.000Z", "costUSD": 0.12345, "errorRate": 0.01, "p95LatencyMs": 850, "runs": 42, "tokensIn": 1200, "tokensOut": 800 } ], "costByProvider": { "openai": 1.23, "openrouter": 0.45 }, "costByModel": { "gpt-4o-mini": 0.67 }, "series": [ { "t": "2025-01-01T01:00:00.000Z", "costUSD": 0.12345, "errorRate": 0.01, "p95LatencyMs": 850 } ] }
POST /api/internal/pg-sync¶
- Purpose: Convex→Postgres durable sync webhook
- Auth: HMAC header
x-rf-signaturecomputed withRUNFORGE_SYNC_SIGN - Body: JSON of a single
runs_livedoc (validated)
GET /api/badge/experiment/[id]¶
- Returns SVG badge or
{ status: "pass"|"fail" }(JSON)
See also: api/ingest.md, api/metrics.md, convex/*, 07-backend.md.
- OTel: optional
traceIdpropagation — include in request metadata and it will be stored astraceId. - Privacy: RunForge never stores prompts or outputs. Only usage/cost/latency/metadata is ingested. Prefer
promptHashoverpromptPreview.