-
[OPIK-6820] [BE] feat: add AI spend API (summary, composition, breakdown, users, recommendations) (#7020)
发布于
2026-06-11 16:09:13 +00:00 - [OPIK-6820] [BE] feat: add AI spend API (summary, composition, breakdown, users, recommendations)
Read-only spend endpoints under /v1/private/ai-spend over ClickHouse: workspace
summary KPIs, token-flow composition, per-lane breakdowns (incl. output lanes),
user leaderboard with DB-side sort + pagination, and savings recommendations.
Queries render via StringTemplate with log_comment and UUIDv7 id-range pruning;
high-spend flag, typed user_id filter, and recommendation impact enum. Includes
endpoint test coverage.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- refactor(ai-spend): address backend review — query perf + structure
Performance:
- Prune spans queries by trace_id range (id is the 5th span sort-key column so
id BETWEEN can't prune; trace_id == the trace's UUIDv7 id and is the leading
span sort-key). No new binds, no migration. - Users query: extract the windowed trace rows once into a shared CTE and use
LEFT ANY JOIN (one row per user_uuid). - Merge composition output + cost into one span scan, extract JSON subtrees
once per row (composition tokens, counts, output), and collapse the tools
breakdown to a single traces scan.
Structure:
- Extract AiSpendQueryBuilder: SQL leaves the api enums (SpendLane/OutputLane
keep only API metadata) and the DAO; user/name filters become StringTemplate
conditionals resolved via a Consumer in the single render. - Extract AiSpendMapper: response building + the high-spend threshold; read
cost as BigDecimal in SQL (drop the double round-trip and filterNan). The DAO
is now execution + binding + row mapping only. - Share leaderboard sortable-field constants (SpendUserField), use
StringUtils.isNotBlank, add CostRow/CountsRow.empty() factories, reword the
project validation message.
Addresses review comments C1, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13.
AiSpendResourceTest 20/20.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- fix(ai-spend): drop empty labels from output breakdown
OUTPUT_BREAKDOWN built_in_tool_calls uses the raw span name as its label (the
other output lanes fall back to 'Unknown ...'), so a blank name could surface
as an empty-label row. Add HAVING label != '' to match the input breakdowns.Addresses review comment on AiSpendQueryBuilder. AiSpendResourceTest 20/20.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- perf(ai-spend): parse cc once with a typed JSONExtract in composition tokens
The previous JSONExtractRaw(metadata,'cc')-then-JSONExtractInt(cc,...) pattern
re-parsed the cc string on every lane (9 parses + an intermediate column),
which measured slower than the original direct extracts. Replace it with a
single typed JSONExtract(metadata,'cc','Tuple(...)') that parses the object
once, and read lanes via named tuple access (cc.prior_assistant.summary.
total_tokens, etc.). Same sums; one parse instead of nine.Addresses review comment on COMPOSITION_TOKENS. AiSpendResourceTest 20/20.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Co-authored-by: Claude Opus 4.8 noreply@anthropic.com
下载附件