diff --git a/docs/superpowers/specs/2026-04-30-log-chatid-upstreamid-design.md b/docs/superpowers/specs/2026-04-30-log-chatid-upstreamid-design.md new file mode 100644 index 0000000..937bbbc --- /dev/null +++ b/docs/superpowers/specs/2026-04-30-log-chatid-upstreamid-design.md @@ -0,0 +1,236 @@ +# Log Chat ID And Upstream ID Design + +## Summary + +This design adds two new usage-log fields: + +- `chat_id`: extracted from the incoming request body +- `upstream_id`: extracted from the upstream response headers + +The existing `logs.request_id` field keeps its current meaning and continues to represent the internal new-api request ID. + +The extraction logic is centralized and writes normalized values into relay context first, then both consume logs and error logs read from the same context when persisting to `logs`. + +## Problem + +The current system has only one stable request identifier in usage logs: the internal `logs.request_id`. + +For reconciliation work, that is not enough: + +- the business request may already carry a `chat_id` +- the upstream provider may return its own request ID in response headers + +Today these values are not recorded consistently in consume logs. Error logs have partial upstream request-id support, but consume logs do not have a unified path. + +## Goals + +- Persist request-body `chat_id` into usage logs +- Persist upstream response request ID into usage logs +- Keep success and error logs consistent +- Keep the extraction logic generic enough to support future upstream header changes +- Keep streaming support without buffering the full stream body +- Keep performance impact negligible + +## Non-Goals + +- No change to the meaning of existing `logs.request_id` +- No backfill of existing historical logs +- No first-version parsing of response body IDs +- No provider-specific per-channel logging branches unless the generic extractor cannot cover them + +## Canonical Field Semantics + +The `logs` table will have three distinct request identifiers: + +- `request_id`: internal new-api request ID, already existing +- `chat_id`: business ID extracted from the incoming request body +- `upstream_id`: upstream request ID extracted from response headers + +Field semantics must not overlap. + +## Data Model + +Add two nullable string columns to `logs`: + +- `chat_id` +- `upstream_id` + +Constraints: + +- both fields default to empty string +- both fields use ordinary indexes +- neither field is unique + +Reasoning: + +- reconciliation queries need direct filtering and export +- uniqueness cannot be guaranteed across providers or retries + +## Extraction Architecture + +### 1. Request-side extraction + +When the request body is parsed into the relay request object, the system performs a best-effort extraction of a top-level `chat_id`. + +Behavior: + +- extract only the business-level request `chat_id` +- do not deeply traverse nested objects +- do not fail the request if `chat_id` is missing +- store the result in relay context as `RelayInfo.ChatID` + +### 2. Response-side extraction + +When an upstream `http.Response` is received, before body consumption begins, the system performs a best-effort extraction of the upstream request ID and stores it in `RelayInfo.UpstreamID`. + +First-version default rule chain: + +1. `x-request-id` +2. `request-id` + +This rule chain is intentionally generic. The extractor should be implemented as a reusable module with: + +- a default candidate-header chain +- a future extension point for provider or channel-specific overrides + +### 3. Persistence + +Consume logs and error logs do not parse request bodies or response headers directly. + +They only read: + +- internal `request_id` +- `RelayInfo.ChatID` +- `RelayInfo.UpstreamID` + +and persist them into `logs`. + +This keeps extraction and logging decoupled. + +## Data Flow + +### Non-stream requests + +1. request enters gateway +2. internal request ID is created as today +3. request parser extracts `chat_id` +4. relay runs normally +5. upstream response is received +6. response extractor reads `x-request-id` / `request-id` +7. final consume log or error log persists `request_id`, `chat_id`, `upstream_id` + +### Stream requests + +1. request parser extracts `chat_id` before relay starts +2. upstream response headers are received before stream body forwarding +3. response extractor reads `upstream_id` from headers +4. stream body is forwarded as usual +5. final consume log or error log persists `request_id`, `chat_id`, `upstream_id` + +No per-chunk ID parsing is required in the first version. + +## Retry Semantics + +Retries must not collapse multiple upstream attempts into one combined ID set. + +Rules: + +- consume log records only the final successful attempt's `upstream_id` +- each failed attempt may still produce its own error log with its own `upstream_id` +- the final consume log should not store a list of retry upstream IDs + +This preserves clean reconciliation semantics for billed requests. + +## Performance Requirements + +The design must not materially affect relay throughput or stream latency. + +Allowed work: + +- read response headers once +- best-effort request-side `chat_id` extraction while request parsing already happens + +Forbidden first-version approaches: + +- buffering the full response body only to find IDs +- parsing every SSE chunk to search for IDs +- adding extra database writes per request + +Expected impact: + +- request-side `chat_id` extraction: negligible, because request parsing already occurs +- response-side `upstream_id` extraction: negligible, because headers are already available +- database overhead: one existing log insert with two extra fields + +## Error Handling + +The feature is best-effort only. + +Rules: + +- missing `chat_id` does not fail the request +- missing upstream header does not fail the request +- malformed request payload does not add extra parsing failure beyond current validation behavior +- logs may store empty values for either field + +## Query And UI Scope + +First version should support: + +- backend filtering by `chat_id` +- backend filtering by `upstream_id` +- frontend display in log detail or an equivalent low-risk display path + +The first version does not need both fields as default main table columns. + +## Testing Scope + +### Backend tests + +- non-stream success: request body includes `chat_id`, upstream returns `x-request-id`, consume log stores both +- stream success: request body includes `chat_id`, upstream stream response includes `x-request-id`, consume log stores both +- error response: error log stores both when available +- retry then success: consume log stores only the final successful `upstream_id` +- missing fields: request still succeeds or fails normally, log fields remain empty +- header fallback: if `x-request-id` is absent and `request-id` exists, `upstream_id` is still stored + +### Regression focus + +- no change to existing `logs.request_id` behavior +- no extra stream buffering +- no change to billing semantics + +## Implementation Notes + +Recommended implementation shape: + +- extend `relay/common.RelayInfo` with `ChatID` and `UpstreamID` +- add a small extraction helper for request-side `chat_id` +- add a reusable response-header extractor for upstream IDs +- extend `model.Log` and log persistence functions to carry `chat_id` and `upstream_id` +- update log query/filter surfaces to support the two new fields + +## Risks + +- some providers may later rename or stop returning `x-request-id` +- some request formats may not include top-level `chat_id` +- stream and retry paths can drift if the extractor is not wired at a shared layer + +Mitigation: + +- use one shared response extractor with a candidate chain +- use one shared relay context as the single source of truth +- test both normal and retry flows explicitly + +## Decision Summary + +Approved design decisions: + +- keep `logs.request_id` unchanged as the internal request ID +- add `logs.chat_id` +- add `logs.upstream_id` +- extract `chat_id` from the request body +- extract `upstream_id` from response headers, defaulting to `x-request-id` and falling back to `request-id` +- centralize extraction into shared relay context +- support both stream and non-stream requests without full-body buffering +- record only the final successful `upstream_id` in consume logs during retry scenarios