Observability export

One /v1 endpoint for every provider. Point your OpenAI client at a new base URL and key, bring your own provider keys, and pay 0% markup on tokens.

After every request — chat completions, embeddings, images, audio, reranking, video and batch submissions — a record of it can go to destinations you configure: a signed webhook, an OpenTelemetry collector, an S3-compatible bucket, Langfuse or Datadog. Up to ten per organisation, each with its own sample rate, set under Observability in your workspace or with the API below.

What a destination receives

json
{
  "type": "request.completed",
  "request": {
    "id": "gen-...",
    "created_at": "2026-11-01T14:03:11.412Z",
    "finished_at": "2026-11-01T14:03:13.907Z",
    "requested_model": "anthropic/claude-sonnet-5",
    "served_model": "anthropic/claude-sonnet-5",
    "provider": "anthropic",
    "status": "completed",
    "finish_reason": "stop",
    "error_code": null,
    "streamed": true,
    "is_byok": true,
    "cache_hit": false,
    "usage": {"prompt_tokens": 812, "completion_tokens": 240, "cached_tokens": 0},
    "cost_usd": 0.00604,
    "latency_ms": {"ttft": 410, "generation": 2495, "routing_overhead": 18},
    "key_name": "production",
    "workspace_id": null,
    "agent": "claude-code",
    "session_id": null,
    "run_id": null,
    "app": null
  }
}
Metadata only. The record has no field for a prompt or a completion, so no destination can receive one. The gateway does not store request content (What we store), and a broadcast is not a way around that. The one exception is deliberate and yours: a hosted agent whose definition names an output.destination_id has each finished run’s result delivered there as an agent.run.completed envelope.

The record is read back a moment after the response ends, so it carries what settlement wrote: the served model, the cost, and cache_hit when the response cache answered. Nothing here is awaited by a request; a slow or failing destination costs its own deliveries and nothing else.

Webhooks

A POST of the JSON above to your https URL, with X-Autonomous-Relay-Event: request.completed and, when the destination was created with a signing secret (every webhook is), X-Autonomous-Relay-Signature:

t=1793000000,v1=5f1d...   # HMAC-SHA256 over "<t>.<raw body>", hex, keyed by the signing secret
javascript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret) {
  const t = /(?:^|,)t=(\d+)/.exec(header)?.[1];
  const v1 = /(?:^|,)v1=([0-9a-f]+)/.exec(header)?.[1];
  if (!t || !v1) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
  • The signing secret is returned once, in the response that created the destination, and never again.
  • Both headers are also sent under their former names, X-AgentsRouter-Event and X-AgentsRouter-Signature, with the same values.
  • header_name and header_value add one header of your own, such as a bearer token; the value is sealed on arrival and the API shows its last four characters.
  • A delivery the network, a 429 or a 5xx fails is tried again, at most three times in all, after a short jittered wait; any other refusal is final. The destination’s last_attempt_at, last_success_at, last_http_status and last_error report the final outcome.

OpenTelemetry

An otlp destination receives one span per request as OTLP/HTTP JSON at the URL you give, named chat <model> and attributed after the GenAI semantic conventions (gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.response.finish_reasons), with the rest of the record as autonomousrelay.* attributes and session.id. The trace id is derived from the request id, so a retried delivery is the same span, not a twin. A failed request is a span with error status.

S3-compatible storage

An s3 destination writes newline-delimited JSON objects — each line the record a webhook receives — to AWS S3, Cloudflare R2, Backblaze B2, MinIO or Google Cloud Storage’s XML API. Records are batched per destination and written when 500 have accumulated, or 4 MB, or 60 seconds after the first. url is the storage endpoint (e.g. https://s3.us-east-1.amazonaws.com), with s3_region, s3_bucket, an optional s3_prefix, s3_path_style where the endpoint needs it, and the access key pair (s3_access_key_id, s3_secret_access_key, sealed). Uploads are signed with SigV4.

Langfuse and Datadog

  • Langfuse: langfuse_public_key (pk-lf-…) and langfuse_secret_key (sk-lf-…), and url as the host — Langfuse’s EU cloud when left out, or us., jp., hipaa.cloud.langfuse.com, or your own. Each request becomes a trace with one generation, sent to the OpenTelemetry endpoint Langfuse documents; the generation has model, usage, cost, level and time to first token, and no input or output.
  • Datadog: datadog_site (datadoghq.com, us3., us5., datadoghq.eu, ap1., ap2., uk1. or the US government sites) and datadog_api_key. Each request becomes a log, service:autonomousrelay, tagged by model, provider and status, with the record as its attributes.

The API

GET/v1/broadcast
POST/v1/broadcast
PATCH/v1/broadcast/:id
DELETE/v1/broadcast/:id
POST/v1/broadcast/:id/test
curl
curl -X POST https://autonomousrelay.com/v1/broadcast \
  -H "Authorization: Bearer $AUTONOMOUSRELAY_MANAGEMENT_KEY" \
  -H 'content-type: application/json' \
  -d '{"kind": "webhook", "url": "https://hooks.example.com/relay", "name": "warehouse", "sample_rate": 1}'

kind is webhook, otlp, s3, langfuse or datadog; sample_rate (above 0, at most 1) sends that share of requests. PATCH changes disabled, sample_rate and name; everything else is a new destination. /test sends one synthetic record now and answers with {"delivered", "http_status", "error"}. Any role reads the list; creating, changing and removing destinations needs config:write, as they decide where request metadata leaves the gateway for.

Input and output logging

GET/v1/logging
PUT/v1/logging

Separate from broadcast, and off: storing the prompts and completions of chosen keys for your own later reading (GET /v1/generation/:id/content). Its settings — the keys, a retention of at least 90 days, a lock — can be prepared now, but enabled: true is refused in this release pending a privacy review, so no content is stored anywhere today. Management keys only: an inference key cannot change what is recorded about its own traffic.