Ship a first cut of workspace + per-worker cost visibility#2303
Ship a first cut of workspace + per-worker cost visibility#2303federicodeponte wants to merge 8 commits into
Conversation
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>
#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>
|
Merge-queue FAIL resolved. |
|
Freshness recheck completed after Floom main advanced.
The original local-default lookup blocker remains resolved on the current-main-containing head. This PR was not merged. |
|
Final current-main refresh completed.
The original blocker is resolved. This PR was not merged. |
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 ofworkspace day/month spend-to-date against the configured daily/monthly
caps. Reuses the existing
services.run_costaggregation 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-datespend + 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 (samevisibility check as
GET /workers/{id}).GET /workspace/settings: its existingcurrent_day_spend_usd/current_month_spend_usdmirrors were computedby calling
_workspace_day_to_date_cost_usd()/_workspace_month_to_date_cost_usd()with no
repos/user_idarguments. Without those, the aggregation alwaysfell back to the engine's local sqlite file, which is empty on any
deployment using a different
Repositoriesbackend (e.g. Supabase oncloud) - so cloud always rendered
$0.00regardless of real spend. ThisPR passes
repos=repos, user_id=auth.user_idthrough, matching thepattern already used by the spend-cap enforcement path in
run_service._enforce_run_spend_caps.ModelDefaultsnow renders "$X of $Y spent this month" (plus a daily figure when a daily cap is set) directly under theexisting "Monthly spend cap (USD)" field, sourced from the new
GET /workspace/spendendpoint."Spend (mo)" tile (month-to-date spend, or "spend / cap" when the worker
has a configured monthly cap), sourced from
GET /workers/{id}/spendviaa cache-first
useWorkerSpendQueryhook. Fails soft (omitted if thefetch errors), same as every other stat on that row.
test_797_workspace_defaults_enforcement.py'stest_settings_returns_current_month_spendseeded a run at a hardcodedday-05 and then asserted
current_day_spend_usd >= 4.25- adate-dependent flake that fails on any day after the 5th of the month
(confirmed failing on
mainbefore this PR, unrelated to this change;fixed here since it's directly in the code path this PR touches).
Deferred
runs). Investigated the code path
(
services/ai_observability.py+cost.py'sresolved_cost_usd_from_transcript): a run's cost/tokens read"Not reported" only when no
{"type":"usage",...}row exists in itstranscript 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 agenuine 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 fixapps/api/routers/worker_admin.py-GET /workers/{id}/spendapps/web/app/settings/page.tsx- spend readout inModelDefaultsapps/web/app/workers/WorkersCollection.tsx- "Spend (mo)" stat on worker detailapps/web/lib/api.ts,apps/web/lib/types.ts- client + typesapps/web/lib/query/hooks.ts-useWorkerSpendQuery+ query keyapps/api/tests/test_1201_worker_spend_endpoint.py(new)apps/api/tests/test_797_workspace_defaults_enforcement.py- newendpoint 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 thenew
api.workspace.getSpendcalldocs/API.md- documents the two new endpointsTest plan
cd apps/api && python -m pytest tests/test_797_workspace_defaults_enforcement.py tests/test_1201_worker_spend_endpoint.py -q- 27 passedcd apps/web && npm run lint- 0 errors (21 pre-existing warnings, none in touched files)cd apps/web && npm run lint:emdash- cleancd apps/web && npx tsc --noEmit- no new errors in touched filescd apps/web && npx vitest run- 204 files / 1015 passed, 1 pre-existing skiptest_1185_session_storage.py::test_cli_command_panel_uses_session_storagefails identically onmainbefore this change (missing file, unrelated pre-existing issue)apps/apipytest suite twice in parallel (this branch vs. a cleanorigin/mainworktree, 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