Skip to content

Ship a first cut of workspace + per-worker cost visibility#2303

Open
federicodeponte wants to merge 8 commits into
mainfrom
feat/workspace-spend-visibility
Open

Ship a first cut of workspace + per-worker cost visibility#2303
federicodeponte wants to merge 8 commits into
mainfrom
feat/workspace-spend-visibility

Conversation

@federicodeponte

@federicodeponte federicodeponte commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Relates to floomhq/workeros-cloud#1201. Live user test: a user seeing 500
runs/day asked twice "who pays for this, doesn't this cost a lot" and wanted
visible costs/limits (he praised Claude Code's statusline showing model +
percent-of-limit). This ships a full cut, not a stub.

  • GET /workspace/spend (new): a purpose-built, read-only readout of
    workspace day/month spend-to-date against the configured daily/monthly
    caps. Reuses the existing services.run_cost aggregation functions
    (_workspace_day_to_date_cost_usd, _workspace_month_to_date_cost_usd,
    _workspace_daily_spend_cap_usd, _workspace_monthly_spend_cap_usd) -
    the same math that already gates the spend-cap enforcement on run
    dispatch, so the number shown always agrees with what actually blocks a
    run. No mutation.
  • GET /workers/{id}/spend (new): a single worker's month-to-date
    spend + its configured monthly cap (null if uncapped). Reuses
    _worker_month_to_date_cost_usd + _spend_cap_for_config /
    get_worker_config_for_run. 404s for a worker the caller can't see (same
    visibility check as GET /workers/{id}).
  • Bug fix in GET /workspace/settings: its existing
    current_day_spend_usd / current_month_spend_usd mirrors were computed
    by calling _workspace_day_to_date_cost_usd() / _workspace_month_to_date_cost_usd()
    with no repos/user_id arguments. Without those, the aggregation always
    fell back to the engine's local sqlite file, which is empty on any
    deployment using a different Repositories backend (e.g. Supabase on
    cloud) - so cloud always rendered $0.00 regardless of real spend. This
    PR passes repos=repos, user_id=auth.user_id through, matching the
    pattern already used by the spend-cap enforcement path in
    run_service._enforce_run_spend_caps.
  • Settings page: ModelDefaults now renders "$X of $Y spent this month" (plus a daily figure when a daily cap is set) directly under the
    existing "Monthly spend cap (USD)" field, sourced from the new
    GET /workspace/spend endpoint.
  • Worker detail page: the Overview tab's stat row now includes a
    "Spend (mo)" tile (month-to-date spend, or "spend / cap" when the worker
    has a configured monthly cap), sourced from GET /workers/{id}/spend via
    a cache-first useWorkerSpendQuery hook. Fails soft (omitted if the
    fetch errors), same as every other stat on that row.
  • Test fix: test_797_workspace_defaults_enforcement.py's
    test_settings_returns_current_month_spend seeded a run at a hardcoded
    day-05 and then asserted current_day_spend_usd >= 4.25 - a
    date-dependent flake that fails on any day after the 5th of the month
    (confirmed failing on main before this PR, unrelated to this change;
    fixed here since it's directly in the code path this PR touches).

Deferred

  • floomhq/workeros-cloud#1183 ("Not reported" cost/tokens on some
    runs).
    Investigated the code path
    (services/ai_observability.py + cost.py's
    resolved_cost_usd_from_transcript): a run's cost/tokens read
    "Not reported" only when no {"type":"usage",...} row exists in its
    transcript at all - correct behavior for genuinely non-agent
    (pure-script) runs that made no LLM calls, since there's nothing to sum.
    Whether the specific repro run (run_16145da453f5) is that case, or a
    genuine instrumentation gap, needs looking at the actual run's
    transcript/runner type, which isn't reproducible from this local
    investigation. Deeper root-causing (and possibly changing "no usage data"
    vs "confirmed zero cost" semantics in the persisted total_cost_usd)
    is a separate, more involved change and is out of scope for this pass -
    filed as a follow-up rather than rabbit-holed here.

(Per-worker spend UI, originally scoped as a possible defer, shipped in
this PR instead - see the worker detail bullet above.)

Files touched

  • apps/api/routers/workspace.py - GET /workspace/spend + settings bug fix
  • apps/api/routers/worker_admin.py - GET /workers/{id}/spend
  • apps/web/app/settings/page.tsx - spend readout in ModelDefaults
  • apps/web/app/workers/WorkersCollection.tsx - "Spend (mo)" stat on worker detail
  • apps/web/lib/api.ts, apps/web/lib/types.ts - client + types
  • apps/web/lib/query/hooks.ts - useWorkerSpendQuery + query key
  • apps/api/tests/test_1201_worker_spend_endpoint.py (new)
  • apps/api/tests/test_797_workspace_defaults_enforcement.py - new
    endpoint tests + flaky-date fix
  • apps/web/tests/model-defaults-spend-1201.dom.test.tsx (new)
  • apps/web/tests/worker-detail-spend-1201.dom.test.tsx (new)
  • apps/web/tests/model-defaults-797.dom.test.tsx - mock update for the
    new api.workspace.getSpend call
  • docs/API.md - documents the two new endpoints

Test plan

  • cd apps/api && python -m pytest tests/test_797_workspace_defaults_enforcement.py tests/test_1201_worker_spend_endpoint.py -q - 27 passed
  • cd apps/web && npm run lint - 0 errors (21 pre-existing warnings, none in touched files)
  • cd apps/web && npm run lint:emdash - clean
  • cd apps/web && npx tsc --noEmit - no new errors in touched files
  • cd apps/web && npx vitest run - 204 files / 1015 passed, 1 pre-existing skip
  • Confirmed test_1185_session_storage.py::test_cli_command_panel_uses_session_storage fails identically on main before this change (missing file, unrelated pre-existing issue)
  • Ran the full apps/api pytest suite twice in parallel (this branch vs. a clean origin/main worktree, on this shared/loaded box) to separate real regressions from environmental noise: both runs hit failures/errors at the same percentage markers on the same clusters of tests, confirming the noise is pre-existing/load-induced on this box, not introduced by this diff. Re-ran several of the affected tests in isolation afterward and they passed cleanly.

Generated with Claude Code

Ships a first honest cut of cost visibility: a "who pays for this" test
user asked twice why Floom might cost a lot and wanted visible spend
against a cap, like Claude Code's statusline.

- GET /workspace/spend: purpose-built read of workspace day/month
  spend-to-date against the configured daily/monthly caps. Reuses the
  existing services.run_cost aggregation (the same functions that already
  gate the spend-cap enforcement), never writes anything.
- GET /workers/{id}/spend: a single worker's month-to-date spend + its
  configured monthly cap, 404s for a worker the caller can't see.
- Fixes GET /workspace/settings: its existing current_day_spend_usd /
  current_month_spend_usd mirrors were computed without passing
  repos/user_id, so on any deployment with a non-sqlite Repositories
  backend they silently fell back to the engine's local (empty) sqlite
  file and always read $0.00 regardless of real spend.
- Settings page: renders "$X of $Y spent this month" next to the
  existing Monthly spend cap field, sourced from the new endpoint.
- Fixes a pre-existing date-dependent flaky test in
  test_797_workspace_defaults_enforcement.py (asserted day-spend against
  a hardcoded day-05 seed, so it failed after the 5th of any month).

Deferred (see PR body): per-worker spend UI on the worker detail page,
and the #1183 "Not reported" cost root-cause investigation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 4 commits July 20, 2026 12:11
#1201)

Adds a "Spend (mo)" stat to the worker-detail Overview summary row
(useWorkerSpendQuery, GET /workers/{id}/spend), next to Last run / Runs /
Success / Schedule. Cache-first via the existing TanStack Query
conventions in lib/query/hooks.ts; fails soft (the stat is simply omitted
if the fetch errors, same pattern as every other stat on that row).

This was called out as deferred in the original PR body; wiring it in
turned out to be a small, low-risk addition once the endpoint existed, so
shipping it now rather than leaving a stub follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@federicodeponte

Copy link
Copy Markdown
Member Author

Merge-queue FAIL resolved. GET /workspace/spend now resolves the request active workspace with _active_workspace_id(request) and passes that ID into daily and monthly cap lookups. Added a regression test that creates a non-default workspace with caps 21/84 while local-default remains 10/50, then verifies the endpoint returns 21/84 for the active workspace. Merged current Floom main so the head retains #2299 and the current engine commits. Local verification passed: Ruff, bytecode compilation, and 28 related tests. The first Windows pass hit an unrelated current-main workspace-chat timeout after 885 tests passed; rerun on the unchanged head passed: https://github.com/floomhq/floom/actions/runs/29761676968/job/88419133882. All seven remote checks are green on db76a4a97beee9e38d98f32b2b2ff0e49b060279.

@federicodeponte

Copy link
Copy Markdown
Member Author

Freshness recheck completed after Floom main advanced.

  • Merged current Floom main 8d512f63bac062f34af9d4999e8d18648c39c625 into the existing PR branch without conflicts.
  • The spend endpoint still resolves _active_workspace_id(request) and passes it to daily and monthly cap lookups.
  • The cross-workspace regression still proves that a non-default active workspace receives its own caps.
  • All seven remote checks are green on final head a7f8cef2653b650d5f9376bdb24f612da859027b.

The original local-default lookup blocker remains resolved on the current-main-containing head. This PR was not merged.

@federicodeponte

Copy link
Copy Markdown
Member Author

Final current-main refresh completed.

  • Merged Floom main a8e4208bea1df57b788dbc3e55fecf977261e643 into the existing branch.
  • The active-workspace spend-cap implementation and cross-workspace regression remain intact.
  • All seven remote checks are green on head 0b9b9bf5ec3d628a1933ec98b9fee857146fb1d2.

The original blocker is resolved. This PR was not merged.

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.

2 participants