发布

  • [OPIK-6820] [BE] feat: add AI spend API (summary, composition, breakdown, users, recommendations) (#7020)

    frostbyte_neo 发布于 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

    下载附件