Commit f607c01
feat(billing): track E2B and Daytona Function sandbox usage (#7184)
* feat(billing): record function sandbox usage
* fix(billing): charge function user-code failures
* fix(billing): correct sandbox trace cost boundaries
* fix(billing): tighten sandbox completion boundaries
* test(billing): check the metered sandbox amount against a real provider
The pricing unit test pins the arithmetic and the conformance suite proves a
cost is produced, attached to the right outcomes, and routed — but that suite
stubs the provider and mocks Date.now() with a counter advancing one
millisecond per call. Under that clock `total > 0` is the strongest claim
available, and it holds equally well if the metered window is anchored to the
wrong instants or the resource constants are wrong.
Bounds the charge between what the sleep must cost and what the wall clock
could justify, so a wrong rate, a wrong vCPU/memory constant, and a
mis-anchored window all fail. Provider-agnostic via resolveProvider, opt-in
behind SANDBOX_BILLING_SMOKE=1 like the sibling smoke suites.
Verified against both providers: E2B billed 9.031s of a 9.302s call at
$0.1656/hr, Daytona 8.435s of 8.721s at $0.16668/hr — both matching published
rates, both excluding ~275ms of Sim-side overhead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(billing): meter the sandbox a cloud Pi session runs in
Pi's own sandbox was never metered. withPiSandbox called createSandbox
without the meterUsage argument, so only sandboxes created through
executeFunctionRequest were charged — and Pi's is the larger consumer by an
order of magnitude. A Function block holds one for seconds; a Pi session
holds one for a minimum lifetime of 31 minutes.
The gap was worst exactly where it was least visible. A Pi coding agent
normally runs BYOK, so its model cost is zero by definition, and the ledger
bills a model row on total > 0. With the sandbox unmetered, such a run
produced a zero-cost model_unbilled row and Sim collected only the flat
execution fee while paying its provider for the whole session.
Threads a cost sink through PiRunContext, which is the seam backends
already receive and the only one that reaches all four cloud modes. The
handler owns one sink covering both sandbox sources — Function tools in
local mode, the agent's own sandbox in cloud mode — so neither can be
dropped where the cost is folded into the block's output. It rides in
toolCost for the same reason the Function tool cost already does: that is
what survives the BYOK zeroing.
Unlike the Function path this charges on creation rather than on a
completed session. A Function run is seconds long, so absorbing one the
provider failed to deliver is cheap and reads as fair; tens of minutes of
Pi compute is consumed whether the agent finished, errored, or was
cancelled, and billing only clean endings would mean paying for every other
one. A create that throws still costs nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(billing): check the Pi sandbox charge against a real provider
The handler test mocks the backend and writes into the cost sink by hand,
so it proves the wiring from a backend to the block's cost and nothing
more — it would still pass if withPiSandbox never metered at all, which is
precisely the bug that path had.
Holds a real Pi sandbox open for a known interval and bounds the charge
between what that interval must cost and what the whole session could
justify. Verified to fail against the original unmetered call with
"expected 0 to be greater than or equal to 0.00023", and to pass once the
sink is threaded: 5.949s billed of a 6.141s session on E2B.
The second case pins the other half of the contract — a caller that
supplies no sink is not charged, which is what keeps mothership and other
internal Pi sandboxes free.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(billing): charge a Pi session only when it completed
Aligns Pi with the outcome policy the Function path already applies rather
than keeping the divergence the previous commit introduced. A session that
ends by throwing — a provider crash, a lifetime limit, a cancellation — is
absorbed, because a charge nobody can tie to delivered work is not one
worth defending, and consistency across the two sandbox paths is worth more
than recovering the cost of runs that failed.
A command exiting non-zero is still billed: the callback returns normally
there and the agent produced its answer, which is the same reason the
Function path bills its own non-zero exits.
The window still closes at teardown, so a completed session is charged for
the whole time the provider held its sandbox.
Verified on both providers, including that the new case fails when the
charge is applied unconditionally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(billing): keep the charge on completed runs that fail after execution
Three paths dropped cost the sandbox had already earned.
A harvest that cannot return what the run produced — more files than the
export limit, nesting past the listing depth, or an output directory the
code deleted — was excluded from the billable-error set. All three arrive
only after the sandbox has executed and all three are the caller's to fix,
so they belong with the post-completion export failures the policy already
bills rather than the provider failures it absorbs. A completed run whose
code wrote one file too many went free.
That also left the route with nothing to read: it already consults
readTrustedSandboxOutputCost for these errors, so attaching the cost at the
sandbox layer is what carries it into the response.
Separately, a Function block whose handler succeeded could still fail in
the steps that follow it — base64 hydration, and large-value redaction that
throws rather than emit unredacted data. Those errors carry no cost of
their own, so the completed sandbox went unbilled. The handler's cost is
now held across that window, in the same way streamingPartialOutput already
is, and used only when the error has none.
The new conformance case was confirmed to fail against the narrower catch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(billing): carry the Pi charge onto a session its agent failed
A backend that returns a result carrying `totals.errorMessage` has already
run: the sandbox was billed and the sink holds the charge. But that path
throws instead of reaching `buildOutput`, which is what publishes the cost,
so the charge was accumulated and then dropped — lost revenue rather than
an over-charge.
Both failure paths now carry it on the error they raise, the same way the
Function handler carries its tool cost, so `handleBlockError` can pick it
up. An agent that ran and then reported a failure consumed the same tokens
and sandbox seconds as one that succeeded, which is why the cost
computation is now shared between the two rather than duplicated.
Also corrects the sink's doc comment. Local mode does fill it — the agent
runs on the caller's own machine and costs Sim nothing, but a
`function_execute` among the Sim tools it calls bills its own remote
sandbox into the same total.
The new case was confirmed to fail without the attach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 95d1969 commit f607c01
39 files changed
Lines changed: 1897 additions & 149 deletions
File tree
- apps/sim
- executor
- execution
- handlers
- function
- pi
- cloud
- authoring
- babysit
- plan
- review
- core
- local
- utils
- lib
- billing
- execution/remote-sandbox
- function-execution
- providers
- scripts
- tools
- function
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| 14 | + | |
13 | 15 | | |
14 | 16 | | |
15 | 17 | | |
| |||
137 | 139 | | |
138 | 140 | | |
139 | 141 | | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
140 | 204 | | |
141 | 205 | | |
142 | 206 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
52 | 58 | | |
53 | 59 | | |
54 | 60 | | |
| |||
76 | 82 | | |
77 | 83 | | |
78 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
79 | 99 | | |
80 | 100 | | |
81 | 101 | | |
| |||
229 | 249 | | |
230 | 250 | | |
231 | 251 | | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
232 | 263 | | |
233 | 264 | | |
234 | 265 | | |
| |||
241 | 272 | | |
242 | 273 | | |
243 | 274 | | |
| 275 | + | |
| 276 | + | |
244 | 277 | | |
245 | 278 | | |
246 | 279 | | |
| |||
416 | 449 | | |
417 | 450 | | |
418 | 451 | | |
419 | | - | |
| 452 | + | |
| 453 | + | |
420 | 454 | | |
421 | 455 | | |
422 | 456 | | |
| |||
506 | 540 | | |
507 | 541 | | |
508 | 542 | | |
| 543 | + | |
| 544 | + | |
509 | 545 | | |
510 | 546 | | |
511 | 547 | | |
512 | 548 | | |
513 | 549 | | |
514 | | - | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
515 | 564 | | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
516 | 572 | | |
517 | | - | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
518 | 577 | | |
519 | 578 | | |
520 | 579 | | |
| |||
528 | 587 | | |
529 | 588 | | |
530 | 589 | | |
531 | | - | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
532 | 594 | | |
533 | 595 | | |
534 | 596 | | |
| |||
548 | 610 | | |
549 | 611 | | |
550 | 612 | | |
551 | | - | |
| 613 | + | |
| 614 | + | |
552 | 615 | | |
553 | 616 | | |
554 | 617 | | |
| |||
620 | 683 | | |
621 | 684 | | |
622 | 685 | | |
| 686 | + | |
623 | 687 | | |
624 | 688 | | |
| 689 | + | |
625 | 690 | | |
626 | 691 | | |
627 | 692 | | |
| |||
Lines changed: 41 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
| |||
254 | 256 | | |
255 | 257 | | |
256 | 258 | | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
257 | 298 | | |
258 | 299 | | |
259 | 300 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
111 | 112 | | |
112 | 113 | | |
113 | 114 | | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
118 | 121 | | |
119 | 122 | | |
120 | 123 | | |
121 | 124 | | |
122 | 125 | | |
| 126 | + | |
123 | 127 | | |
124 | 128 | | |
125 | 129 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
435 | 435 | | |
436 | 436 | | |
437 | 437 | | |
438 | | - | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
439 | 442 | | |
440 | 443 | | |
441 | 444 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
784 | 784 | | |
785 | 785 | | |
786 | 786 | | |
787 | | - | |
| 787 | + | |
788 | 788 | | |
789 | 789 | | |
790 | 790 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
83 | | - | |
| 83 | + | |
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
| 221 | + | |
222 | 222 | | |
223 | 223 | | |
224 | 224 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
172 | 173 | | |
173 | 174 | | |
174 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
175 | 190 | | |
176 | 191 | | |
177 | 192 | | |
| |||
0 commit comments