Skip to content

feat(collectors): sessionless LiteLLM usage collector and normalizer - #53

Open
kumanday wants to merge 2 commits into
mainfrom
leonardogonzalez/coe-373-implement-sessionless-litellm-usage-collector-and-normalizer
Open

kumanday wants to merge 2 commits into
mainfrom
leonardogonzalez/coe-373-implement-sessionless-litellm-usage-collector-and-normalizer

Conversation

@kumanday

@kumanday kumanday commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a sessionless LiteLLM usage collection path (COE-373). All-key /spend/logs rows are normalized into usage_requests, attributed to the proxy_keys registry when possible, optionally linked to benchmark sessions, and reported through a reconciliation report. The existing session-bound LiteLLMCollector / RequestNormalizer path is unchanged.

Changes

  • collectors/extraction.py: shared pure helpers for timestamps, latency (seconds→ms, request_duration_ms, endTime-startTime), TTFT (seconds→ms or derived from completion_start_time/completionStartTime), tokens, cache (incl. string "True", additional_usage_values), cost, and status/error (incl. metadata.error_information).
  • UsageRequestNormalizer (normalize_requests.py): maps fixture and native LiteLLM SpendLogs rows to UsageRequest ORM rows; stable ID is request_id → call_id; rows without either are skipped with missing_stable_request_id; rows missing best-effort fields are accepted as partial with LiteLLM source field names; optional join fields (benchmark_session_id, experiment_id, variant_id, task_card_id, harness_profile, trace_id) are read from metadata.spend_logs_metadata, metadata.requester_metadata, metadata, and request_tags, supporting both credential-service benchmark_* tags and fixture short names. Metadata is allowlisted + RedactionFilter-ed; content fields and raw key references are never persisted.
  • collectors/key_attribution.py: ProxyKeyAttributionResolver resolves by registry litellm_key_id, optional ephemeral virtual-key→alias mapping (ADR-002), then reported alias; cached lookups; unresolved keys logged/reported only by alias or sha256: fingerprint.
  • LiteLLMUsageCollector (litellm_collector.py): GET /spend/logs?start_date&end_date&summarize=false with no session ID, half-open window filtering, known-session FK linking (via SessionRepository.exists), dry-run, idempotent writes through SQLUsageRequestRepository.create_many, categorized errors (auth_error, http_error, connection_error, invalid_response, repository_error).
  • UsageReconciliationReport: mapped/partial/skipped/out-of-window/attributed/unattributed/session-linked/written/duplicate counts, missing-field counts, skip reasons, unattributed keys, per-row diagnostics (capped at 100); to_dict() and to_markdown().
  • Docs: collector flow in docs/data-model-and-observability.md; metadata redaction boundary in docs/security-and-operations.md.

Evidence

Test output

$ pytest tests/unit/test_usage_normalizer.py tests/unit/test_collectors.py tests/unit/test_normalize_requests.py tests/unit/test_spend_log_fixtures.py
129 passed

$ make lint format-check type-check test
ruff: All checks passed
mypy: Success: no issues found
701 passed, 3 warnings   (baseline before change: 617 passed)

Verification

  • Fixture tests for success, failure, cached, streaming, non-streaming TTFT derivation, sparse, and call_id fallback rows assert key alias, model, provider, tokens, cost, latency, TTFT, errors, and timestamps.
  • Missing request_id/call_id rows are skipped with diagnostics; rows with no session metadata are accepted; rows with session metadata preserve join fields; UUID sessions that exist are linked via FK, unknown/non-UUID ones stay in metadata only.
  • Redaction tests: messages/response/proxy_server_request, arbitrary metadata, secret-like values, and raw api_key never appear in persisted rows; error messages are redacted.
  • Mocked /spend/logs (httpx MockTransport) tests cover query params (summarize=false, date span, no session_id), SQLite idempotent re-collection, dry-run, window filtering, wrapped payloads, and error categories.
  • Report markdown reviewed in tests for actionable field names (missing_stable_request_id, endTime, spend, proxy_key_id) and absence of raw key values.

Checklist

  • Tests pass (make test)
  • Code is formatted (make format-check)
  • Lint/type-check clean (make lint type-check)
  • Documentation updated (if behavior changed)
  • AGENTS.md updated (if repo knowledge changed) — not needed
  • Evidence section filled out (for substantive changes)

Type of change

  • Bug fix
  • New feature
  • Performance improvement
  • Refactoring
  • Documentation
  • Other

Breaking changes

None. Session-bound benchmark collection APIs are unchanged; new symbols are additive exports from collectors.

Related issues

  • COE-373 (parent COE-363); unblocks COE-374 (watermarks/CLI) and COE-375 (reconciliation with benchmark ingestion).

Additional notes

  • Watermarks and CLI commands are intentionally left to COE-374; time-window rollups and dashboards are out of scope.
  • The legacy RequestNormalizer was not refactored onto collectors.extraction to avoid changing benchmark-path behavior in this PR; the helpers follow the same documented unit conventions.

Summary:
- Add collectors.extraction with shared helpers for timestamps,
  latency, TTFT, tokens, cache, cost, and status/error fields.
- Add UsageRequestNormalizer that maps LiteLLM spend-log rows
  (fixture and native SpendLogs shapes) into usage_requests without a
  session, skips rows lacking request_id/call_id with diagnostics, and
  preserves optional benchmark join fields in redacted metadata.
- Add ProxyKeyAttributionResolver that resolves spend-log key
  references to proxy_keys by litellm_key_id, ephemeral virtual-key
  mapping, or alias; raw key references are never persisted.
- Add LiteLLMUsageCollector that fetches /spend/logs for all keys
  (summarize=false), filters to the window, links known benchmark
  sessions, and writes idempotently via SQLUsageRequestRepository.
- Add UsageReconciliationReport with mapped/partial/skipped,
  attribution, session-link, duplicate, and missing-field counts.
- Document the collector flow and metadata redaction boundary.

Rationale:
- COE-373: usage ingestion must work for all keys, not only
  benchmark sessions, while keeping the session-bound benchmark
  collector unchanged.
- ADR-002 forbids persisting raw virtual key values; unattributed
  keys are reported by alias or sha256 fingerprint.

Tests:
- pytest tests/unit/test_usage_normalizer.py tests/unit/test_collectors.py
- make lint format-check type-check test (701 passed)

Co-Authored-By: bot_apk <apk@cognition.ai>
@kumanday kumanday added the symphony Symphony orchestrated task label Oct 7, 2026
Live /spend/logs dry run showed cache_key is the placeholder 'Cache OFF'
on every row when caching is disabled, adding noise without join value.

Co-Authored-By: bot_apk <apk@cognition.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-this Trigger AI PR review symphony Symphony orchestrated task

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant