Repository navigation
Commit 056349e
authored
🤖 feat: count AI calls from bash commands in workspace costs (opt-in) (#5772)
## Summary
Bash tool commands can now send their Anthropic and OpenAI calls through
Xum, so the spend shows up in the workspace's Cost tab and in Analytics.
Turn on **Settings → Providers → Bash commands** (off by default). Each
Local or Worktree bash command then gets `ANTHROPIC_BASE_URL`,
`OPENAI_BASE_URL` and a per-workspace `xum-proxy-…` key that point at a
loopback proxy. The proxy forwards with the provider keys from Xum's
settings and records the reported usage.
This is PR 1 of 4 in a stack. PR 2 keeps the port and keys stable across
restarts, PR 3 adds SSH and Coder hosts, and PR 4 adds a bug-bash
scenario for the feature.
## Background
Test harnesses (`make bug-bash`, e2e explorers, SDK scripts) that the
agent runs in bash call providers directly. Today that spend never
reaches Xum's ledger, so the Cost tab and Analytics undercount.
## Implementation
- `src/node/services/bashAiProxy/bashAiProxyService.ts`: a loopback HTTP
proxy. Allowed paths: Messages, Responses, Chat Completions, token
counting and model listing. It replaces the proxy key with the real key
and provider headers, adds `stream_options.include_usage` to streamed
Chat Completions, drops `Expect`, and does not follow redirects. It
builds the upstream URL from the configured base URL (normalized like
the chat path, query kept) and keeps headers configured in
providers.jsonc, such as `OpenAI-Project`.
- `usageExtract.ts` reads usage from JSON and SSE bodies (Anthropic and
OpenAI). The proxy records usage through
`SessionUsageService.recordHeadlessUsage` with analytics source
`bash_proxy`, and emits a live `session-usage-delta`.
- `TurnRequestBuilder` adds the variables to `xumEnv` on a best-effort
basis. It adds none when:
- the switch is off,
- a project secret names any of that provider's variables,
- Xum has no API key for that provider,
- the workspace is not trusted for shared execution (untrusted project
or `XUM_DISABLE_PROJECT_AUTOMATION=1`). The bash tool blanks provider
keys there, so a proxy URL would pair with a blank key.
- The proxy checks each request. If the switch is off, it returns 503.
If the key's workspace was removed, it returns 401. If the project lost
trust, it returns 403. So commands that already hold a key lose access
at once.
## Validation
- Unit tests with a fake upstream: both Anthropic SDK path styles,
OpenAI chat and responses, SSE usage, header handling, the refusals
above, and the env rules. `aiService.test.ts` covers the trust gating
for trusted, untrusted and kill-switch workspaces. I mutation-checked it
and the 403 test.
- Dogfood with a fake provider and no real spend: a bash `curl` through
the proxy shows up in the Cost tab rows next to the chat turns.

- Four agent bug-bash rounds (Opus 5.5 and Sonnet 5.5 explorers, effort
high, PR 4 scenario) against the top of the stack. The last round found
no defect in branch code. Earlier rounds found unclear setting copy and
two trust gaps. This stack fixes them.
## Risks
- With the switch on, agent CLIs such as `claude -p` bill the Xum API
key instead of a subscription login. That is why the switch is opt-in.
The docs and the setting text say so.
- Usage comes from what the provider reports. If a client disconnects
mid-stream, Xum records only the usage seen before the disconnect.
Related issues filed from the bug bashes (not fixed here): #5766, #5767,
#5768, #5769, #5771.
---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$32.63`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=32.63 -->1 parent ffac032 commit 056349e
24 files changed
Lines changed: 1606 additions & 2 deletions
File tree
- docs/config
- src
- browser
- features/Settings/Sections
- stories/mocks
- utils
- commands
- common
- config/schemas
- orpc/schemas
- types
- node
- config
- orpc
- services
- agentSkills
- bashAiProxy
- di
- layers
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
76 | 97 | | |
77 | 98 | | |
78 | 99 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
463 | 464 | | |
464 | 465 | | |
465 | 466 | | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
466 | 472 | | |
467 | 473 | | |
468 | 474 | | |
| |||
3444 | 3450 | | |
3445 | 3451 | | |
3446 | 3452 | | |
| 3453 | + | |
| 3454 | + | |
| 3455 | + | |
| 3456 | + | |
| 3457 | + | |
| 3458 | + | |
| 3459 | + | |
| 3460 | + | |
| 3461 | + | |
| 3462 | + | |
| 3463 | + | |
| 3464 | + | |
| 3465 | + | |
3447 | 3466 | | |
3448 | 3467 | | |
3449 | 3468 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
600 | 600 | | |
601 | 601 | | |
602 | 602 | | |
| 603 | + | |
603 | 604 | | |
604 | 605 | | |
605 | 606 | | |
| |||
850 | 851 | | |
851 | 852 | | |
852 | 853 | | |
| 854 | + | |
853 | 855 | | |
854 | 856 | | |
855 | 857 | | |
| |||
963 | 965 | | |
964 | 966 | | |
965 | 967 | | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
966 | 973 | | |
967 | 974 | | |
968 | 975 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
| 164 | + | |
164 | 165 | | |
165 | 166 | | |
166 | 167 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| 108 | + | |
108 | 109 | | |
109 | 110 | | |
110 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1352 | 1352 | | |
1353 | 1353 | | |
1354 | 1354 | | |
| 1355 | + | |
| 1356 | + | |
| 1357 | + | |
| 1358 | + | |
| 1359 | + | |
| 1360 | + | |
| 1361 | + | |
| 1362 | + | |
| 1363 | + | |
| 1364 | + | |
| 1365 | + | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
| 1369 | + | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
1355 | 1379 | | |
1356 | 1380 | | |
1357 | 1381 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2043 | 2043 | | |
2044 | 2044 | | |
2045 | 2045 | | |
| 2046 | + | |
| 2047 | + | |
| 2048 | + | |
| 2049 | + | |
| 2050 | + | |
| 2051 | + | |
| 2052 | + | |
| 2053 | + | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
2046 | 2063 | | |
2047 | 2064 | | |
2048 | 2065 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
177 | 177 | | |
178 | 178 | | |
179 | 179 | | |
| 180 | + | |
| 181 | + | |
180 | 182 | | |
181 | 183 | | |
182 | 184 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3149 | 3149 | | |
3150 | 3150 | | |
3151 | 3151 | | |
| 3152 | + | |
3152 | 3153 | | |
3153 | 3154 | | |
3154 | 3155 | | |
| |||
3287 | 3288 | | |
3288 | 3289 | | |
3289 | 3290 | | |
| 3291 | + | |
3290 | 3292 | | |
3291 | 3293 | | |
3292 | 3294 | | |
| |||
0 commit comments