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:
2026-08-20 22:14:09 +07:00
parent d2ad46b8aa
commit 99b63abde6
20 changed files with 234 additions and 29 deletions

View File

@@ -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', () => {