feat: show per-step OpenRouter cost on the Trajectory tab
Extend the openRouterCost projection with a per-step priced-cost map keyed by turn:step so surfaces can render spend without re-pricing, and have the Trajectory view read it through the framework useProjection seat: each priced assistant step shows its USD cost on the request boundary chip and in the inspector summary, while unpriced steps stay blank. Also fix a pre-existing exactOptionalPropertyTypes error in extractCacheRates that the host typecheck surfaced.
This commit is contained in:
@@ -871,4 +871,46 @@ describe('TrajectoryTable', () => {
|
||||
expect(screen.getByRole('row', { name: /TOOL/ }).getAttribute('aria-selected')).toBe('false')
|
||||
expect(onInspectApplied).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows a host-priced step cost on the request boundary and in the summary', () => {
|
||||
render(
|
||||
<TrajectoryTable
|
||||
turns={TURNS}
|
||||
requestNumbers={[{
|
||||
turn: 1, step: 1, group: 'Step 1', number: 1, seq: 1,
|
||||
stepCostUsd: 0.0028,
|
||||
}]}
|
||||
{...FOLD_PROPS}
|
||||
/>,
|
||||
)
|
||||
const chip = screen.getByRole('button', { name: 'Request #1 · $0.0028' })
|
||||
expect(chip).toBeTruthy()
|
||||
fireEvent.click(chip)
|
||||
expect(screen.getByText('Cost')).toBeTruthy()
|
||||
expect(screen.getByText('$0.0028')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('formats step costs above a cent with the currency format', () => {
|
||||
render(
|
||||
<TrajectoryTable
|
||||
turns={TURNS}
|
||||
requestNumbers={[{
|
||||
turn: 1, step: 1, group: 'Step 1', number: 1, seq: 1,
|
||||
stepCostUsd: 1.234,
|
||||
}]}
|
||||
{...FOLD_PROPS}
|
||||
/>,
|
||||
)
|
||||
const chip = screen.getByRole('button', { name: 'Request #1 · $1.23' })
|
||||
expect(chip).toBeTruthy()
|
||||
fireEvent.click(chip)
|
||||
expect(screen.getByText('Cost')).toBeTruthy()
|
||||
expect(screen.getByText('$1.23')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('omits the cost row when the step was not priced', () => {
|
||||
render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Request #1' }))
|
||||
expect(screen.queryByText('Cost')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -255,7 +255,8 @@ function mount(slots: SlotRegistry, nodes: ConversationSnapshot['nodes'] = NODES
|
||||
return (
|
||||
<View
|
||||
{...injectedProps}
|
||||
{...({ sessionId: SID, useSession, useSessions: emptySessions(), useWorkspaces: emptyWorkspaces() } as unknown as ConvViewProps)}
|
||||
{...({ sessionId: SID, useSession, useProjection: () => undefined,
|
||||
useSessions: emptySessions(), useWorkspaces: emptyWorkspaces() } as unknown as ConvViewProps)}
|
||||
key={key}
|
||||
/>
|
||||
)
|
||||
@@ -1199,6 +1200,32 @@ describe('TrajectoryView state', () => {
|
||||
)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('attaches the per-step OpenRouter cost from the openRouterCost projection', () => {
|
||||
render(
|
||||
<TrajectoryView
|
||||
{...standaloneProps(NODES)}
|
||||
{...standaloneHistory(historySnapshot(NODES))}
|
||||
{...standaloneDuration()}
|
||||
useProjection={((key: string) => (key === 'openRouterCost'
|
||||
? {
|
||||
totalUsd: 0.00336,
|
||||
pricedSteps: 2,
|
||||
unknownModelSteps: 0,
|
||||
steps: { '1:1': 0.0028 },
|
||||
currency: 'USD',
|
||||
}
|
||||
: undefined)) as never}
|
||||
/>,
|
||||
)
|
||||
// The turn-1/step-1 request is priced; the turn-2/step-1 one is not.
|
||||
expect(screen.getByRole('button', { name: 'Request #1 · $0.0028' })).toBeTruthy()
|
||||
expect(screen.queryByRole('button', { name: 'Request #2 · $0.0028' })).toBeNull()
|
||||
// The summary of the priced step shows its cost.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Request #1 · $0.0028' }))
|
||||
expect(screen.getByText('Cost')).toBeTruthy()
|
||||
expect(screen.getByText('$0.0028')).toBeTruthy()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('node half', () => {
|
||||
|
||||
Reference in New Issue
Block a user