Skip to content

Commit ec98c11

Browse files
committed
fix(billing): charge a continued Function call's failure once, through the journal
1 parent 79c5435 commit ec98c11

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎apps/sim/providers/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ export async function executeProviderRequest(
351351
const settledBeforeContinuation = settledCost.total
352352
await continuePendingConversationCalls(modelSafeRequest, session)
353353
settledCost.total = settledBeforeContinuation
354+
failedFunctionToolCost.total = 0
354355
const currentUserMessage = [...(modelSafeRequest.messages ?? [])]
355356
.reverse()
356357
.find((message) => message.role === 'user' && !isConversationHistoryNotice(message))

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ vi.mock('@/providers/registry', () => ({
1919
vi.mock('@/lib/billing/core/usage-gate-cache', () => billingUsageGateCacheMock)
2020
vi.mock('@/tools', () => toolsMock)
2121
vi.mock('@/lib/billing/core/billing', () => billingCoreMock)
22-
vi.mock('@/providers/cost-policy', () => ({
22+
vi.mock('@/providers/cost-policy', async (importOriginal) => ({
23+
...(await importOriginal<typeof import('@/providers/cost-policy')>()),
2324
resolveModelCostPolicy: (_model: string, isBYOK = false) => ({
2425
billable: !isBYOK,
2526
multiplier: 1,
@@ -298,6 +299,63 @@ describe('run usage limit', () => {
298299
expect(readTrustedExecutionCost(failure)).toEqual({ input: 1, output: 1, total: 2.25 })
299300
})
300301

302+
it("charges a continued Function call's failure once, through the journal that records it", async () => {
303+
mockCheckExecutionUsageLimits.mockResolvedValue({
304+
isExceeded: false,
305+
payerUsage: { currentUsage: 0, limit: 5 },
306+
})
307+
const tool: ProviderToolConfig = {
308+
id: 'function_execute',
309+
description: 'run code',
310+
params: {},
311+
parameters: { type: 'object', properties: {}, required: [] },
312+
}
313+
const session = new AgentTurnStateMachine(
314+
{ save: vi.fn() },
315+
{
316+
version: 1,
317+
steps: [
318+
{
319+
id: 'interrupted-step',
320+
assistant: { role: 'assistant', content: '' },
321+
calls: [
322+
{
323+
invocationId: 'call-1',
324+
toolId: 'function_execute',
325+
arguments: '{}',
326+
configuredToolBinding: getConfiguredConversationToolBinding(tool),
327+
},
328+
],
329+
results: [],
330+
},
331+
],
332+
}
333+
)
334+
mockExecuteTool.mockResolvedValueOnce({
335+
success: false,
336+
output: { cost: { total: 0.5 } },
337+
error: 'Function failed',
338+
})
339+
mockExecuteRequest.mockResolvedValue({
340+
content: 'Done',
341+
model: 'model-a',
342+
cost: { input: 0, output: 0, total: 0 },
343+
})
344+
345+
const response = await executeProviderRequest(
346+
'anthropic',
347+
{
348+
model: 'model-a',
349+
workflowId: 'workflow-1',
350+
messages: [{ role: 'user', content: 'Continue' }],
351+
tools: [tool],
352+
},
353+
{ executionContext: startRun(), agentConversation: session }
354+
)
355+
356+
expect(response).toMatchObject({ cost: { total: 0.5 } })
357+
})
358+
301359
it('counts every block of the run, not only the request that is running', async () => {
302360
const run = startRun()
303361
await modelStep(startRequest(run), 1, 1)

0 commit comments

Comments
 (0)