Skip to content

Commit ebabfe5

Browse files
committed
fix(billing): bill journaled attempts on the usage stop
1 parent 332c149 commit ebabfe5

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

‎apps/sim/providers/index.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ export async function executeProviderRequest(
294294
const modelSafeRequest = await prepareProviderFileAttachments(sanitizedRequest)
295295
const toolIdentities = assignProviderToolIdentities(modelSafeRequest.tools)
296296
const failedFunctionToolCost = { total: 0 }
297+
// A conversation summary charges its parent request, so a stop after compaction bills it.
298+
const settledCost = runtimeContext?.settledCost ?? { input: 0, output: 0, total: 0 }
297299
const requestRuntimeContext: ProviderRuntimeContext = {
298300
...runtimeContext,
299301
...(runtimeContext?.agentConversation
@@ -305,8 +307,7 @@ export async function executeProviderRequest(
305307
}
306308
: {}),
307309
failedFunctionToolCost,
308-
// A conversation summary charges its parent request, so a stop after compaction bills it.
309-
settledCost: runtimeContext?.settledCost ?? { input: 0, output: 0, total: 0 },
310+
settledCost,
310311
...(toolIdentities.toolIdByWireId.size > 0
311312
? {
312313
toolIdByWireId: new Map([
@@ -351,6 +352,10 @@ export async function executeProviderRequest(
351352
.find((message) => message.role === 'user' && !isConversationHistoryNotice(message))
352353
bindConversationGenerationPrompt(modelSafeRequest, currentUserMessage)
353354
priorConversationUsage = session.getUsage()
355+
// A stop bills what success would: this journaled usage plus the request's own steps.
356+
settledCost.input += priorConversationUsage.cost.input
357+
settledCost.output += priorConversationUsage.cost.output
358+
settledCost.total += priorConversationUsage.cost.total
354359
bindConversationGenerationCompactor(
355360
modelSafeRequest,
356361
createAgentConversationCompactor(

‎apps/sim/providers/run-usage-limit.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type { BillingAttributionSnapshot } from '@/lib/billing/core/attribution'
3838
import { resetMidRunUsageCaches } from '@/lib/billing/core/mid-run-usage'
3939
import { NEAR_LIMIT_USAGE_GATE_TTL_MS } from '@/lib/billing/core/usage-gate-cache'
4040
import { isNonRetryableExecutionError } from '@/lib/execution/non-retryable-error'
41+
import { AgentTurnStateMachine } from '@/lib/memory/turn-state'
4142
import type { ExecutionContext } from '@/executor/types'
4243
import { readTrustedExecutionCost } from '@/executor/utils/errors'
4344
import { executeProviderRequest } from '@/providers'
@@ -160,6 +161,40 @@ describe('run usage limit', () => {
160161
expect(parentCost).toEqual({ input: 0.5, output: 0.25, total: 0.75 })
161162
})
162163

164+
it('bills a stopped attempt for the journaled steps a successful one would have billed', async () => {
165+
const session = new AgentTurnStateMachine(
166+
{ save: vi.fn() },
167+
{
168+
version: 1,
169+
steps: [
170+
{
171+
id: 'failed-attempt-step',
172+
assistant: { role: 'assistant', content: 'Partial answer' },
173+
calls: [],
174+
results: [],
175+
usage: { input: 1, output: 1 },
176+
cost: { input: 1, output: 1, total: 2 },
177+
},
178+
],
179+
}
180+
)
181+
const run = startRun()
182+
run.unrecordedModelCost = { total: 2 }
183+
mockExecuteRequest.mockImplementation(async (request: ProviderRequest) => {
184+
await prepareConversationGeneration(request, 'anthropic', { messages: [] })
185+
return { content: '', model: request.model }
186+
})
187+
188+
const failure = await executeProviderRequest(
189+
'anthropic',
190+
{ model: 'model-a', messages: [{ role: 'user', content: 'Continue' }] },
191+
{ executionContext: run, agentConversation: session }
192+
).catch((caught: unknown) => caught)
193+
194+
expect(isNonRetryableExecutionError(failure)).toBe(true)
195+
expect(readTrustedExecutionCost(failure)).toEqual({ input: 1, output: 1, total: 2 })
196+
})
197+
163198
it('counts every block of the run, not only the request that is running', async () => {
164199
const run = startRun()
165200
await modelStep(startRequest(run), 1, 1)

0 commit comments

Comments
 (0)