Skip to content

Commit ff2092a

Browse files
committed
fix lint
1 parent e863998 commit ff2092a

3 files changed

Lines changed: 79 additions & 15 deletions

File tree

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { AGENT, BlockType, isMcpTool } from '@/executor/constants'
2121
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
2222
import type { ExecutionContext, StreamingExecution } from '@/executor/types'
2323
import { executeProviderRequest } from '@/providers'
24+
import { installStreamingCostPolicy } from '@/providers/cost-policy'
2425
import { SIM_AUTO_MODEL_ID } from '@/providers/models'
2526
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
2627
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
@@ -348,6 +349,45 @@ describe('AgentBlockHandler', () => {
348349
expect(buildAutoRoutingSignalsFor({ files: [{ id: 'f3' }] }).mediaKind).toBe('file')
349350
})
350351

352+
it('overlays the routing charge on a streaming cost written after the fact', async () => {
353+
// Mirrors the real streaming shape: the policy accessor is installed at
354+
// provider-return time, the drain writes the final cost long after the
355+
// handler returned, and consumers read it at log time.
356+
const output: Record<string, unknown> = { cost: { input: 0, output: 0, total: 0 } }
357+
installStreamingCostPolicy(output as never, { billable: true, multiplier: 1 })
358+
const streaming = { stream: new ReadableStream(), execution: { output } }
359+
360+
;(
361+
handler as unknown as { applyRoutingCost: (r: unknown, c: number) => void }
362+
).applyRoutingCost(streaming, 0.002)
363+
364+
// The drain settles the model cost afterwards.
365+
366+
;(output as { cost: unknown }).cost = { input: 0.01, output: 0.02, total: 0.03 }
367+
368+
expect(output.cost).toEqual({
369+
input: 0.01,
370+
output: 0.02,
371+
total: expect.closeTo(0.032, 10),
372+
routing: 0.002,
373+
})
374+
})
375+
376+
it('adds the routing charge to a settled non-streaming cost', async () => {
377+
const result: Record<string, unknown> = { cost: { input: 0.01, output: 0.02, total: 0.03 } }
378+
379+
;(
380+
handler as unknown as { applyRoutingCost: (r: unknown, c: number) => void }
381+
).applyRoutingCost(result, 0.002)
382+
383+
expect(result.cost).toEqual({
384+
input: 0.01,
385+
output: 0.02,
386+
total: expect.closeTo(0.032, 10),
387+
routing: 0.002,
388+
})
389+
})
390+
351391
it('leaves the reported model alone for an explicitly selected model', async () => {
352392
const result = (await handler.execute(mockContext, mockBlock, {
353393
model: 'gpt-4o',

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,8 @@ export class AgentBlockHandler implements BlockHandler {
163163

164164
const result = await this.executeProviderRequest(ctx, providerRequest, block, responseFormat)
165165

166-
// Routing cost lands on non-streaming outputs only for now; streaming
167-
// outputs assemble cost at stream end where there is no hook yet. The
168-
// charge is gated server-side by mothership's bill-model-router flag
169-
// (default off), so this asymmetry currently bills nobody.
170-
if (autoRouting && autoRouting.billableRoutingCost > 0 && !this.isStreamingExecution(result)) {
171-
this.applyRoutingCost(result as BlockOutput, autoRouting.billableRoutingCost)
166+
if (autoRouting && autoRouting.billableRoutingCost > 0) {
167+
this.applyRoutingCost(result, autoRouting.billableRoutingCost)
172168
}
173169

174170
if (autoRouting) {
@@ -289,17 +285,44 @@ export class AgentBlockHandler implements BlockHandler {
289285
}
290286

291287
/**
292-
* Adds the billable sim-auto routing charge to a non-streaming output's
293-
* cost breakdown as a distinct `routing` component.
288+
* Adds the billable sim-auto routing charge to the output's cost breakdown
289+
* as a distinct `routing` component.
290+
*
291+
* A non-streaming cost is final, so it is mutated in place. A streaming
292+
* output's cost settles at stream end — written through the accessor
293+
* `installStreamingCostPolicy` installed — so the charge is layered as a
294+
* read-time overlay on the property instead: whatever the drain writes,
295+
* every later read (trace spans, cost summary, usage ledger) sees the
296+
* routing component on top.
294297
*/
295-
private applyRoutingCost(output: BlockOutput, routingCost: number): void {
298+
private applyRoutingCost(result: BlockOutput | StreamingExecution, routingCost: number): void {
299+
const output = this.isStreamingExecution(result)
300+
? (result as StreamingExecution).execution?.output
301+
: (result as BlockOutput)
302+
if (!output || typeof output !== 'object') return
296303
const target = output as { cost?: Record<string, number> }
297-
if (target.cost && typeof target.cost.total === 'number') {
298-
target.cost.routing = routingCost
299-
target.cost.total += routingCost
300-
} else {
301-
target.cost = { input: 0, output: 0, routing: routingCost, total: routingCost }
304+
305+
const withRouting = (cost: Record<string, number> | undefined): Record<string, number> =>
306+
cost && typeof cost.total === 'number'
307+
? { ...cost, routing: routingCost, total: cost.total + routingCost }
308+
: { input: 0, output: 0, routing: routingCost, total: routingCost }
309+
310+
if (!this.isStreamingExecution(result)) {
311+
target.cost = withRouting(target.cost)
312+
return
302313
}
314+
315+
const prior = Object.getOwnPropertyDescriptor(target, 'cost')
316+
let raw = prior?.get ? undefined : (target.cost as Record<string, number> | undefined)
317+
Object.defineProperty(target, 'cost', {
318+
get: () => withRouting(prior?.get ? (prior.get.call(target) as never) : raw),
319+
set: (value: Record<string, number> | undefined) => {
320+
if (prior?.set) prior.set.call(target, value)
321+
else raw = value
322+
},
323+
configurable: true,
324+
enumerable: true,
325+
})
303326
}
304327

305328
private async validateToolPermissions(ctx: ExecutionContext, tools: ToolInput[]): Promise<void> {

apps/sim/lib/model-router/resolve.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ const ctx = {
5151
} as unknown as ExecutionContext
5252

5353
/** Distinct-by-default signals so the module-level decision cache never collides across tests. */
54+
let signalSeq = 0
5455
function makeSignals(overrides: Partial<AutoRoutingSignals> = {}): AutoRoutingSignals {
5556
return {
56-
systemPrompt: `analyze the quarterly report ${Math.random()}`,
57+
systemPrompt: `analyze the quarterly report ${++signalSeq}`,
5758
lastMessage: 'here is the data to reconcile against the ledger',
5859
messageCount: 1,
5960
toolNames: ['exa_search'],

0 commit comments

Comments
 (0)