eros-engine exposes an opaque OpenRouter passthrough on the streaming
chat endpoint. Three caller-supplied fields ride to
openrouter.ai/api/v1/chat/completions unchanged, three OpenRouter wire
echoes come back on the SSE done frame, and two deployer-set env vars
add app-attribution headers to every outbound call.
The engine never inspects content. PII scrubbing, hashing, and metadata semantics are the caller's responsibility.
POST /comp/chat/{session_id}/message/stream accepts an optional audit
object alongside the required content / client_msg_id:
Caps enforced at the engine before forwarding:
| Field | Cap |
|---|---|
audit.user |
chars ≤ 256 |
audit.session_id |
chars ≤ 256 |
audit.metadata keys |
≤ 16 |
audit.metadata key |
regex ^[A-Za-z0-9_.-]{1,64}$ |
audit.metadata value |
JSON string, chars ≤ 512 |
Violations return 400 BadRequest as a pre-stream error, and no user
message row is persisted.
Privacy: do not put raw email / wallet address / real name in
user — send a hash. OpenRouter retains request metadata (token
counts, latency) but not prompts / responses by default.
The streaming endpoint's done frame carries three optional fields:
| Field | Type | Meaning |
|---|---|---|
usage |
object? |
OpenRouter usage block verbatim (tokens / cost / cached / reasoning). Engine does not flatten. |
generation_id |
string? |
OpenRouter response.id. Query /api/v1/generation with it for full request metadata later. |
model |
string? |
Model OpenRouter actually served. When fallback_model was hit, this is the fallback. |
These fields appear on the done frame (the per-turn terminal frame
before final). Background paths (dreaming / post_process) do not
surface them to clients.
Deployers can strip specific top-level keys from the usage echo by
setting OPENROUTER_USAGE_HIDDEN_KEYS (comma-separated) on the server.
Typical use: hide wholesale cost / cost_details from downstream
customers without losing visibility for the operator.
OPENROUTER_USAGE_HIDDEN_KEYS=cost,cost_detailsBehaviour:
- Applies to the SSE streaming
doneframe (/comp/chat/{id}/message/stream). - The full unfiltered
usageis still persisted to the DB; only the client-facing payload is filtered. - Does not affect
tracing::info!output — operator observability stays intact regardless of this setting. - Background paths (dreaming / post_process) already don't return
usageto clients, so the env var has no effect on them. - Only top-level keys are stripped; to suppress a whole subtree, list
its parent key (
cost_detailsremoves the entire object, not just its members). - Unset or empty → today's pass-through behaviour.
Background paths (pipeline::dreaming, pipeline::post_process) emit
usage only as tracing::info! fields:
openrouter: call completed session=… generation_id=… model=…
prompt_tokens=… completion_tokens=… total_tokens=… cost=…
Two optional env vars add headers to every outbound OpenRouter call:
| Env | Header | Purpose |
|---|---|---|
OPENROUTER_APP_REFERER |
HTTP-Referer |
App identifier on OpenRouter dashboards |
OPENROUTER_APP_TITLE |
X-Title |
Display name in OpenRouter app analytics |
Both unset → today's behaviour (no attribution headers). They are set
per deployment, not per request — App-Attribution is intended for
app-level aggregation. Per-user attribution belongs in audit.user.
Invalid values (control characters, non-ASCII outside header rules)
are dropped at construction time with a tracing::warn!; the client
still works.
- Persist. No DB column stores
audit,usage, or attribution. Surface fields only. - Hash. The engine does not transform
user— callers are responsible for sending a hash. - Sanitise.
metadatakeys and values are size / shape-checked, not content-checked. - Interpret. The engine does not group, aggregate, or alert on any audit field. Callers wire that themselves.
When audit is supplied, the engine logs an info-level event with
audit_user_present, audit_session_present, and audit_metadata_keys
(keys only — never values). On every successful OpenRouter call the
engine also logs generation_id, model, and best-effort parsed
token/cost fields from usage.
The engine's persona / chat / affinity tables are the long-lived
contract. Audit context is intentionally ephemeral so callers can
experiment with user hashing, metadata schemas, and per-deployment
analytics without engine-side migrations or business logic.
{ "content": "...", "client_msg_id": "01J3333333333333333333333A", "audit": { "user": "u_<hash-of-internal-id>", // optional "session_id": "conv_xyz", // optional, ≠ URL session UUID "metadata": { // optional "feature": "chat", "plan": "pro" } } }