feat: add CSV export for AI spend data (#27491)

## Description

Adds `GET /api/v2/organizations/{organization}/ai/spend/export`,
returning `text/csv` with per-user, per-group, per-model, per-provider
aggregated AI spend. The data is built from the raw AI Gateway token
usage tables rather than the `ai_user_daily_spend` rollup, but stays
consistent with it: spend is attributed through the token usage's
effective group and bucketed by the token usage `created_at`, the same
values the daily rollup derives from.

The period defaults to the current UTC month, narrowed to the configured
AI Gateway retention window when the month begins before retained data
does. Explicit `period_start`/`period_end` params must be provided
together, are interpreted as UTC, and may span at most 31 days. Unlike
the default period, an explicit period that begins before the retention
window is rejected rather than narrowed. Every row echoes the applied
bounds, so a narrowed window is visible in the export.

The endpoint requires organization-level admin permissions.

## Changes

- Add the `ExportOrganizationAISpend` query aggregating
`aibridge_token_usages` joined to `aibridge_interceptions`, scoped to
the organization via the effective group, resolving the username, group
name, and organization name alongside their IDs.
- Add the `exportOrganizationAISpend` handler and route, gated by the
`aigateway-cost-control` experiment and the `AIBridge` feature,
returning the CSV in a single response.
- Add the `ExportOrganizationAISpend` codersdk client method.
- Require organization-wide `ResourceGroupMember` read, since the export
aggregates every user in the organization. The per-row filter stays in
`dbauthz` as defence in depth.
- Escape leading formula characters in the free-text columns, so a model
or provider name recorded from an intercepted request cannot be
evaluated when the CSV is opened in a spreadsheet.
- Add an index on `aibridge_token_usages (effective_group_id,
created_at)`, which the period and group predicates otherwise cannot
use.

Closes
https://linear.app/codercom/issue/AIGOV-293/add-csv-export-for-ai-spend-data

> [!NOTE]
> Generated by Coder Agents on behalf of @ssncferreira
This commit is contained in:
Susana Ferreira
2026-07-28 10:58:38 +01:00
committed by GitHub
parent 16cadcf2c8
commit c3895ff9c0
19 changed files with 1558 additions and 2 deletions
+40
View File
@@ -305,3 +305,43 @@ FROM user_spend
WHERE current_spend_micros >= spend_limit_micros
GROUP BY effective_group_id
ORDER BY effective_group_id;
-- name: ExportOrganizationAISpend :many
-- Returns per-user, per-group, per-model, per-provider aggregated AI spend for
-- @organization_id over the [period_start, period_end) window. Spend is
-- attributed through the token usage's effective group, and rows are bucketed
-- by the token usage created_at, matching how ai_user_daily_spend is derived.
SELECT
ai.initiator_id AS user_id,
users.username AS username,
tu.effective_group_id AS group_id,
groups.name AS group_name,
groups.organization_id AS organization_id,
organizations.name AS organization_name,
ai.model AS model,
ai.provider AS provider,
ai.provider_name AS provider_name,
COALESCE(SUM(tu.input_tokens), 0)::BIGINT AS input_tokens,
COALESCE(SUM(tu.output_tokens), 0)::BIGINT AS output_tokens,
COALESCE(SUM(tu.cache_read_input_tokens), 0)::BIGINT AS cache_read_tokens,
COALESCE(SUM(tu.cache_write_input_tokens), 0)::BIGINT AS cache_write_tokens,
COALESCE(SUM(tu.cost_micros), 0)::BIGINT AS cost_micros
FROM aibridge_token_usages tu
JOIN aibridge_interceptions ai ON ai.id = tu.interception_id
JOIN users ON users.id = ai.initiator_id
JOIN groups ON groups.id = tu.effective_group_id
JOIN organizations ON organizations.id = groups.organization_id
WHERE groups.organization_id = @organization_id
AND tu.created_at >= @period_start::timestamptz
AND tu.created_at < @period_end::timestamptz
GROUP BY
ai.initiator_id,
users.username,
tu.effective_group_id,
groups.name,
groups.organization_id,
organizations.name,
ai.model,
ai.provider,
ai.provider_name
ORDER BY ai.initiator_id, tu.effective_group_id, ai.provider, ai.provider_name, ai.model;