Skip to content

costs.ts run-count aggregation blind to credits/fixed/unknown billing types #11422

Description

@dzianisv

Problem

server/src/services/costs.ts computes apiRunCount (billing_type = metered_api) and subscriptionRunCount (subscription_included + subscription_overage) in byAgent and byProvider (and the same pattern in byBiller). Every other billing type — credits, fixed, unknown — is counted by neither. Any agent billed purely via credits (e.g. GitHub Copilot/OpenRouter lanes, see #11421) shows apiRunCount: 0, subscriptionRunCount: 0 forever, even though costCents is correctly non-zero. Verified live: 528/529 cost events on the instance are correctly classified by billing_type, but both counters read 0 for agents on the credits lane.

Decision (VP Eng, AGE-352)

Ship the additive fix now: add creditRunCount alongside the existing two counters. Do not introduce a runCountsByBillingType map in this pass — that's a breaking API/UI change and isn't warranted for a 3-bucket problem. Revisit the map shape only when the UI is touched for other reasons.

fixed and unknown billing types have no confirmed production traffic today (grep BILLING_TYPES in packages/shared/src/constants.ts for the full enum) — fold them into creditRunCount's sibling: add otherRunCount covering fixed + unknown so the three existing buckets (api/subscription/credits) plus a catch-all account for 100% of billingType values with no silent drops. Do not add a fourth named counter per type — that's the option-1-grows-forever problem the report calls out.

Scope

  • server/src/services/costs.ts:
    • byAgent: add creditRunCount, otherRunCount (mirrors existing apiRunCount/subscriptionRunCount count-distinct-heartbeat-run pattern).
    • byProvider: same two fields.
    • byBiller: same two fields (not named in the original report but has the identical bug — same apiRunCount/subscriptionRunCount-only pattern).
    • byAgentModel: no run-count fields exist there today (confirmed) — out of scope, do not add net-new fields to that endpoint in this ticket.
  • packages/shared/src/types/cost.ts: add creditRunCount: number and otherRunCount: number to CostByAgent and CostByProviderModel (byProvider's return type) and CostByBiller.
  • UI: ui/src/pages/Costs.tsx (by-agent card run-count line), ui/src/components/BillerSpendCard.tsx, ui/src/components/ProviderQuotaCard.tsx — extend the existing api/subscription display pattern to also show credits/other counts when non-zero. Follow the existing row.apiRunCount > 0 ? ... : "0 api" idiom; do not restructure the layout.
  • storybook/stories/budget-finance.stories.tsx fixtures — add the two new fields (can default to 0 in existing stories, non-zero in at least one to visually confirm rendering).

Acceptance criteria (machine-checkable)

  1. server/src/__tests__/costs-service.test.ts gains a test that inserts cost_events rows with billing_type = 'credits' only (no metered_api/subscription rows for that agent) and asserts byAgent(...) returns creditRunCount > 0 for that agent, apiRunCount === 0, subscriptionRunCount === 0. Same assertion shape for byProvider and byBiller.
  2. A second fixture row with billing_type = 'fixed' or 'unknown' asserts otherRunCount > 0 on the same three endpoints.
  3. pnpm --filter server test costs-service passes.
  4. pnpm --filter @paperclipai/shared build (or the repo's type-check target covering packages/shared) passes with the new fields on CostByAgent, CostByProviderModel, CostByBiller.
  5. UI change is additive only — no existing snapshot/test for Costs.tsx, BillerSpendCard.tsx, or ProviderQuotaCard.tsx breaks group of tests, if any exist; if none exist, a manual screenshot in the PR showing the new count segment rendering for a credits-only row is acceptable evidence.
  6. No changes to byAgentModel's shape.

Out of scope / explicitly rejected

  • runCountsByBillingType map replacement (option 2 from AGE-352) — rejected for this pass, breaking change not justified yet.
  • Changing the UI to show cost+tokens per billing type instead of counters (option 3) — rejected, throws away the existing UI contract for no benefit over the additive fix.

Ref: AGE-352 (Paperclip issue), parent AGE-344.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions