Repository navigation
feat(harness): team scoped usage and spend chart - #145
Conversation
|
|
31476df to
fcf86b4
Compare
4a99542 to
ef77d99
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef77d99743
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case len(teams) == 0: | ||
| ctx.LogLine("No usage this month.") | ||
| return nil |
There was a problem hiding this comment.
Emit an empty JSON array when the user has no teams
When listing.Teams is empty and the command is invoked with --output json, this returns before NewJSONArrayWriter is created, so stdout contains no bytes rather than the valid [] produced by other streamed-array commands. Any automation parsing the documented JSON output receives an unexpected EOF; initialize/close the array writer before this branch or explicitly emit an empty array in JSON mode.
Useful? React with 👍 / 👎.
| var team managementapi.RouteTeamUserSettings | ||
| switch { | ||
| case f.Team != "": | ||
| i := slices.IndexFunc(teams, func(t managementapi.RouteTeamUserSettings) bool { return t.TeamId == f.Team || t.TeamName == f.Team }) |
There was a problem hiding this comment.
Reject ambiguous team names before selecting a team
When a user belongs to multiple teams with the same name, this predicate silently selects the first one and reports that team's spend even though the supplied name is ambiguous. It can also let an earlier name match override a later exact ID match. Team resolution elsewhere in the CLI explicitly gives IDs precedence and rejects duplicate-name matches, so this lookup should scan for an exact ID first and otherwise require exactly one matching name.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🟡 Changes recommended
Chart width overflow and ambiguous series identifiers can make the new visualization incorrect or unreadable.
2 open findings
What changed in this PR
Adds team-scoped monthly Harness usage reporting with spend limits, per-model totals, and a terminal chart.
Changes:
- Selects a team and fetches month-to-date usage.
- Renders spend limits, model shares, and daily charts.
- Updates usage tests and the management API dependency.
| File | Description |
|---|---|
internal/cmd/usage_chart.go |
Implements the terminal chart. |
internal/cmd/export_test.go |
Exposes chart rendering to tests. |
internal/cmd/command.route_usage_test.go |
Moves route usage flag coverage. |
internal/cmd/command.harness_usage.go |
Implements team-scoped usage reporting. |
internal/cmd/command.harness_usage_test.go |
Tests teams, output, and charts. |
cmd/command.harness.go |
Updates flags and help text. |
go.mod |
Updates the management API client. |
go.sum |
Updates dependency checksums. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| paint := func(s string, i int) string { | ||
| return ansi.Style{}.ForegroundColor(usageChartColors[i%len(usageChartColors)]).Styled(s) |
| axis := []byte(strings.Repeat(" ", labelWidth+2+cols+6)) | ||
| every := (8 + step - 1) / step | ||
| for d := (days - 1) % every; d < days; d += every { | ||
| label := first.AddDate(0, 0, d).Format("Jan 2") | ||
| copy(axis[labelWidth+2+max(0, d*step+(step-len(label))/2):], label) |
ef77d99 to
1597722
Compare
| { | ||
| Name: "usage", | ||
| Summary: "Show your routes spend and tokens, month to date by default (PRE-RELEASE)", | ||
| Summary: "Show your routes spend this month in one of your teams (PRE-RELEASE)", |
There was a problem hiding this comment.
in one of your teams
Can you confirm the cross-team-viewing limitation will exists in API and UI too?
There was a problem hiding this comment.
Everything in the UI is scoped down to the team you're drilled down into (except of course admins can look at cross team)
For the API, we did want to have the ability to return data for all teams. Do you feel like we should have parity between the three? I could scope down the API to also force passing in --team in a followup, but it felt right to make team_id optional in the API, open to having my mind changed
There was a problem hiding this comment.
except of course admins can look at cross team
If admins can do this in UI, how do they do it in API/CLI/SDKs/MCP/etc?
There was a problem hiding this comment.
sorry, i shoul've clarified: admins can do this in the API /SDK as well.
We're delaying shipping admin controls (e.g. setting spend limits) in the CLI for now, but parity will come.
MCP not supported at all for now
There was a problem hiding this comment.
We're delaying shipping admin controls (e.g. setting spend limits) in the CLI for now, but parity will come.
For this one, I was more curious about viewing usage than setting spend limits. Don't have to add now of course, I am just wondering what the command would look like. If a common API call is allowed to view single or multi-team usage, will that be the case for CLI? Specifically, what is the default when you don't provide a team in API/SDK vs CLI and why do they have to be different?
There was a problem hiding this comment.
I couldn't come up w a strong argument for why API and CLI should behave differently, so now behavior is, when --team is not passed in, it shows usages across all teams!
There was a problem hiding this comment.
Not sure this deserves its own file IMO, can just put in one of the two files it is used and reference from the other IMO (there is plenty of precedent on this)
There was a problem hiding this comment.
ah okay! sounds good lemme move this
|
|
||
| // RenderUsageChart exposes the chart renderer to tests, painting series i's | ||
| // characters as <i>…</i>. | ||
| func RenderUsageChart(first time.Time, series [][]float64, width int) string { |
There was a problem hiding this comment.
Sign of a code smell IMO, where is this used?
There was a problem hiding this comment.
ended up changing to call renderUsageChart from inside the package so no need for this shim anymore. thanks for catching
| Start time.Time `flag:"start" desc:"Start of the range, inclusive, snapped down to its UTC day. ISO 8601, local when no timezone is given. Defaults to the start of the current UTC month."` | ||
| End time.Time `flag:"end" desc:"End of the range, exclusive, rounded up to the end of its UTC day. ISO 8601, local when no timezone is given. Defaults to now."` | ||
| Since time.Duration `flag:"since" desc:"Window from a relative time ago until now (e.g. '7d'). Mutually exclusive with --start and --end."` |
There was a problem hiding this comment.
I can no longer give a range? Is it supported in API? UI?
There was a problem hiding this comment.
UI only allows picking by billing month. Let me actually add in the ability to do that in CLI now since it's cheap
There was a problem hiding this comment.
added baseten harness usage --month 2026-09
There was a problem hiding this comment.
But unlike UI, API and CLI are quite rigid, does API/SDK accept full date? Why not CLI? And reusing the start/end or since has consistency across CLI.
There was a problem hiding this comment.
that is very fair. supports full date just like API now baseten harness usage --start 2026-06-01T00:00:00Z --end 2026-06-15T00:00:00Z
1cc40ba to
40fffaa
Compare
…per day `baseten harness usage` now shows, for each team you're in, what you spent there this month of your limit there (with the team's default when yours differs, and "limit reached" or "team limit reached" when your requests are rejected), a chart of spend per day per model when stdout is a terminal, and each model's spend and share. It covers this month only, the period spend limits apply to: the window, filter, grouping and --user-id flags are gone, leaving --team and --output; `baseten route usage` keeps all of them. JSON output is the API's daily buckets as before, now with team_id. `baseten route usage` gains --group-by team, showing usage metered before it was attributed to a team as "(no team)". Requires GET /v1/routes/settings/users/me/teams from basetenlabs/baseten#32884 and the regenerated client. Claude-Session: https://claude.ai/code/session_01NWBCrJFG8msRVeZnny39uT
The JSON writer opened after the no-teams return, so --output json printed nothing. The last date label can stick out two columns past the plot, which the width check didn't count, so a chart at the threshold width wrapped. Claude-Session: https://claude.ai/code/session_01NWBCrJFG8msRVeZnny39uT
The renderer had one caller, and the export shim existed only because the tests are in an external package. A white-box test file needs neither. Claude-Session: https://claude.ai/code/session_01NWBCrJFG8msRVeZnny39uT
Mirrors the UI's month picker rather than a free range: limits are per month, so the window is a month. A past month shows what the usage adds up to with no limit, reset date, or blocked state, since the settings endpoint only knows this month's. Claude-Session: https://claude.ai/code/session_01NWBCrJFG8msRVeZnny39uT
Same flags and shared window parser as `baseten route usage`, so the CLI is consistent and any window the API takes works here. The limit, reset date, and blocked state are only shown for this month so far, since the settings endpoint only knows this month's; other windows show the usage summed and the window's dates. Daily spend is indexed from the window start, not the day of the month. Claude-Session: https://claude.ai/code/session_01NWBCrJFG8msRVeZnny39uT
313338c to
5e7435a
Compare
cretz
left a comment
There was a problem hiding this comment.
Looks good, but wanting to really resolve #145 (comment). I think understanding the intentional inconsistency with API/SDK and at least envisioning what a cross-team form of this command looks like is important for this one.
Chad asked why harness usage needed a team when the API shows everything you're allowed to see without one. Now --team only narrows: without it, one usage call grouped by team splits your spend into a block per team you're in, each with its line, chart, and models, sharing one pair of columns. Usage in a team you've left, or from before team attribution, has no settings to show it against and is left out of the text view. Claude-Session: https://claude.ai/code/session_01NWBCrJFG8msRVeZnny39uT

baseten harness usageshows what you spent on routes this month in each of your teams, against your limit there, with a chart of spend per day per model and each model's spend and share.--teamnarrows it to one team;--start/--end/--sincepick another window, likeroute usage.Every team, no
--team:One team,
--team FDE:Another window,
--since 7d(no limit, since limits are monthly):