Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add experimental FOCUS-format AI Gateway spend export - #29253

Draft
ItsAustinDlugosch wants to merge 1 commit into
mainfrom
focus-export-ai-gateway
Draft

feat: add experimental FOCUS-format AI Gateway spend export#29253
ItsAustinDlugosch wants to merge 1 commit into
mainfrom
focus-export-ai-gateway

Conversation

@ItsAustinDlugosch

Copy link
Copy Markdown

aibridge: add experimental FOCUS-format AI Gateway spend export

Summary

Implements Phase 1 of FOCUS-format Export Mode for AI Gateway logs (ENG-3125), per the linked Phase One Draft.

Adds a new, additive, experimental endpoint:

GET /api/experimental/organizations/{organization}/ai/spend/export/focus

that exports AI Gateway usage and cost data in a FOCUS v1.2-shaped CSV or Parquet document, alongside the existing stable /api/v2/.../ai/spend/export endpoint (unchanged, unaffected). Gated behind FeatureAIBridge (existing license gate) and a new focus-export experiment (CODER_EXPERIMENTS=focus-export), off by default — two independent signals, neither a Coder-controlled kill switch, matching the RFC's stated approach for keeping this out of documented, supported surface area until Phase 2's config/UI work lands.

What's implemented

  • New AGPL package coderd/aibridge/focus: UsageRecord/Row DTOs, vendor and billing-account resolution, SKU building, CSV writer (with the existing export's formula-injection escaping duplicated per the license-boundary convention already established for that endpoint), and a Parquet writer (github.com/parquet-go/parquet-go, gzip codec).
  • Two new SQL queries (GetOrganizationAIFOCUSUsage, GetOrganizationAIFOCUSUsageRollup), scoped to the organization via the same effective_group_id → groups.organization_id tenant-isolation signal the existing export already relies on.
  • The new handler mirrors exportOrganizationAISpend's authorization check (rbac.ResourceGroupMember.InOrg), period-parsing convention (period_start/period_end, UTC, 31-day max), and download-filename convention.
  • Response headers in place of a full Phase-2 manifest.json: X-Coder-Focus-Version, -Mapping-Version, -Generated-At, -Row-Count, -Checksum-Sha256, plus -Granularity-Seconds (see deviation below).
  • Tests: vendor/publisher/billing-account/SKU resolution and row-mapping unit tests against the RFC's four worked examples, CSV escaping, Parquet round-tripping, a reflection-based test pinning Row field order against the CSV header and Parquet tags, database-backed query tests (tenant isolation for both raw and rollup grains, soft-deleted-provider-name reuse, hourly rollup folding and its inverse, billing-period correctness at a month boundary, ResourceId determinism/collision/rename-stability), and HTTP-level tests (feature/experiment/org-read gating, format/granularity validation, cross-org access, real org-admin role, happy path for both formats).

Deviations from the RFC / Phase One Draft — please read

1. Granularity is a Phase-1 feature, not deferred to Phase 2. Both documents scope Phase 1 to the finest, unrolled per-response grain only, with configurable rollup explicitly deferred to Phase 2's admin configuration surface. Per product direction, this PR ships a granularity query parameter (seconds; 0 = raw per-response grain, default 3600/hourly, max 86400/one day) with full rollup support now. Rollup groups by every FOCUS-reportable dimension (nothing is summed across a column the export must report distinctly); response-level identifiers collapse to null when a bucket folds more than one distinct underlying value; rolled-up rows get a deterministic synthetic ResourceId.

One consequence worth flagging for FOCUS-conformance purposes: the RFC's declared capability profile states "Data Granularity: Supported... ResourceId is token_usages.id, unique per priced response" — true only at granularity=0. At granularity>0, ResourceId is the synthetic rollup UUID instead. Not a bug, just something to account for if/when this gets run through the FOCUS conformance validator.

2. New third-party dependency: github.com/parquet-go/parquet-go. Pure Go (CGO_ENABLED=0 compatible), Apache-2.0 licensed, actively maintained fork of the archived segmentio/parquet-go. Its own footprint is close to free (four of its dependencies were already in this repo's tree); it adds:

  • github.com/parquet-go/bitpack, github.com/parquet-go/jsonlite (both MIT/Apache-2.0, first-party helper modules of the same project)
  • github.com/twpayne/go-geom (BSD-2-Clause) — a geometry library pulled in transitively for Parquet's geospatial logical types, entirely unused by this code
  • a bump of the existing indirect github.com/pierrec/lz4/v4 from v4.1.18 → v4.1.21

All four are permissively licensed with no reciprocal obligation on either the AGPL or enterprise-licensed portions of this codebase. Flagging explicitly per the usual supply-chain sign-off, especially the unused go-geom transitive.

Known limitations (Phase 1, by design — see RFC for full detail)

  • BilledCost is always "0"; ContractedCost/EffectiveCost equal ListCost (no invoice-backed or negotiated-rate relationship exists in AI Gateway's data model).
  • BillingAccountId/SubAccountId are fixed-default policy (flat deployment value / BYOK surrogate; budget group), not yet admin-configurable — Phase 2.
  • No workspace/project/team attribution (AI Gateway's data model doesn't capture it yet; see the RFC's Attribution gaps section).
  • openai-compat (including self-hosted models) remains a collapsed vendor bucket with no further discriminator.
  • Vendor-type resolution is a live join at export time, not a snapshot — a provider instance reconfigured or deleted after the fact can produce a wrong/missing vendor identity for historical rows (RFC Open Question Q1, accepted as-is).
  • The response is fully buffered in memory before any bytes are written (to compute Content-Length/checksum); at the endpoint's 31-day max window this is a real memory-pressure consideration, not just a file-size one. Streaming is a known Phase-2-or-later follow-up.
  • No manifest.json, no file-drop delivery, no admin configuration UI — all Phase 2.

Testing

  • go build ./...
  • go vet ./coderd/aibridge/focus/... ./enterprise/coderd/... ./coderd/aibridgedserver/...
  • golangci-lint run ./coderd/aibridge/focus/... ./enterprise/coderd/... ./coderd/aibridgedserver/... — 0 issues
  • go test ./coderd/aibridge/focus/... — 23 tests
  • go test ./enterprise/coderd/ -run TestExportOrganizationAIFocus — feature/experiment/org-read gating, format/granularity validation, cross-org access, org-admin role, happy path (CSV + Parquet + hourly rollup)
  • go test ./coderd/database/dbauthz/... -run TestMethodTestSuite/TestAIBridge — RBAC wiring for both new queries

Not in this PR (tracked separately / Phase 2+)

Admin configuration surface, file-drop delivery, full manifest.json, FOCUS conformance test suite against the FinOps Foundation's validator, PII-focused test suite. See the RFC's Phases section.

@github-actions

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Implements Phase 1 of the FOCUS-format Export Mode for AI Gateway logs RFC:

- New coderd/aibridge/focus package: UsageRecord/Row DTOs, vendor and
  billing-account resolution, SKU building, CSV and Parquet writers
  (github.com/parquet-go/parquet-go).
- New GetOrganizationAIFOCUSUsage and GetOrganizationAIFOCUSUsageRollup
  queries. Raw grain matches the RFC's per-response, per-token-type design;
  the rollup query additionally supports admin-configurable time-bucket
  granularity (query param, in seconds; default 3600/hourly) per product
  direction, grouping on every FOCUS dimension so no column is summed away,
  and omitting response-level identifiers a bucket can't uniquely resolve.
- New GET /api/experimental/organizations/{organization}/ai/spend/export/focus
  endpoint, gated by the new focus-export experiment plus the existing
  FeatureAIBridge license check, mirroring exportOrganizationAISpend's
  authorization and period-parsing conventions. Surfaces FOCUS/mapping
  version, row count, and a checksum as response headers in place of a full
  manifest.json (Phase 2).
- Tests: unit tests for vendor/publisher/billing-account/SKU resolution and
  row mapping against the RFC's worked examples, CSV formula-injection
  escaping, Parquet round-tripping, database-backed query tests (tenant
  isolation, soft-deleted provider name reuse, hourly rollup folding), and
  HTTP-level enablement/validation/happy-path tests.

Not yet done: Phase 2 admin configuration surface, file-drop delivery, the
full manifest.json, and the FinOps conformance/PII test suite described in
the RFC's Phase 3.
@ItsAustinDlugosch ItsAustinDlugosch changed the title aibridge: add experimental FOCUS-format AI Gateway spend export feat: add experimental FOCUS-format AI Gateway spend export Sep 11, 2026
@ItsAustinDlugosch

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

cdrci2 added a commit to coder/cla that referenced this pull request Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant