diff --git a/.github/pr-assets/session-cost/01-admin-session-trace-cost.png b/.github/pr-assets/session-cost/01-admin-session-trace-cost.png new file mode 100644 index 0000000000..511c8633fc Binary files /dev/null and b/.github/pr-assets/session-cost/01-admin-session-trace-cost.png differ diff --git a/.github/pr-assets/session-cost/02-code-review-detail-cost.png b/.github/pr-assets/session-cost/02-code-review-detail-cost.png new file mode 100644 index 0000000000..42e396c7dc Binary files /dev/null and b/.github/pr-assets/session-cost/02-code-review-detail-cost.png differ diff --git a/.github/pr-assets/session-cost/03-code-review-detail-fallback-cost.png b/.github/pr-assets/session-cost/03-code-review-detail-fallback-cost.png new file mode 100644 index 0000000000..0a67cdeb15 Binary files /dev/null and b/.github/pr-assets/session-cost/03-code-review-detail-fallback-cost.png differ diff --git a/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx b/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx index 90e1ff5dc1..0d59249711 100644 --- a/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx +++ b/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx @@ -9,6 +9,10 @@ import { CouncilGovernancePanel } from '@/components/code-reviews/CouncilGoverna import { formatTokenCount } from '@/lib/code-reviews/summary/usage-footer'; import { getCodeReviewJobsHref } from '@/lib/code-reviews/code-review-links'; import { getCodeReviewTerminalReasonCopy } from '@/lib/code-reviews/terminal-reason-copy'; +import { + formatSessionCost, + isRenderableSessionCost, +} from '@/components/cloud-agent-next/session-cost-breakdown'; import { ExternalLink, GitPullRequest, Loader2, ArrowLeft, RotateCcw, Ban } from 'lucide-react'; import { getCodeReviewStatusIcon } from '@/components/code-reviews/code-review-status-icons'; import { useTRPC } from '@/lib/trpc/utils'; @@ -128,6 +132,10 @@ export function CodeReviewDetailClient({ reviewId }: CodeReviewDetailClientProps const canCancel = isCancellableReviewStatus(status); const prLabel = review.platform === 'gitlab' ? 'MR' : 'PR'; const jobsHref = getCodeReviewJobsHref(review.platform, review.owned_by_organization_id); + // Billing-derived session cost is authoritative when available; fall back to the + // orchestrator-accumulated review cost when the billing lookup has nothing (no + // linked session, an in-flight review, or a failed lookup). + const sessionCostMusd = data.sessionCostMusd ?? review.total_cost_musd; const isSupersededCancellation = status === 'cancelled' && (review.terminal_reason === 'superseded' || @@ -264,10 +272,10 @@ export function CodeReviewDetailClient({ reviewId }: CodeReviewDetailClientProps )} - {review.total_cost_musd != null && review.total_cost_musd > 0 && ( + {sessionCostMusd != null && isRenderableSessionCost(sessionCostMusd / 1_000_000) && (
Cost
-
${(review.total_cost_musd / 1_000_000).toFixed(4)}
+
{formatSessionCost(sessionCostMusd / 1_000_000)}
)}
diff --git a/apps/web/src/app/admin/components/SessionTraceViewer.tsx b/apps/web/src/app/admin/components/SessionTraceViewer.tsx index f60a2cfff5..02b3cc6f92 100644 --- a/apps/web/src/app/admin/components/SessionTraceViewer.tsx +++ b/apps/web/src/app/admin/components/SessionTraceViewer.tsx @@ -13,6 +13,10 @@ import { MessageErrorBoundary as V1MessageErrorBoundary } from '@/components/clo import { convertToCloudMessages } from '@/components/cloud-agent/store/db-session-atoms'; import { MessageBubble as V2MessageBubble } from '@/components/cloud-agent-next/MessageBubble'; import { MessageErrorBoundary as V2MessageErrorBoundary } from '@/components/cloud-agent-next/MessageErrorBoundary'; +import { + formatSessionCost, + isRenderableSessionCost, +} from '@/components/cloud-agent-next/session-cost-breakdown'; import { isNewSession } from '@/lib/cloud-agent/session-type'; import { useAdminSessionTrace, @@ -31,6 +35,7 @@ import { Download, Server, Tag, + DollarSign, } from 'lucide-react'; import type { CloudMessage, Message } from '@/components/cloud-agent/types'; import type { StoredMessage } from '@/components/cloud-agent-next/types'; @@ -403,6 +408,18 @@ export function SessionTraceViewer() {
)} + {sessionQuery.data.total_cost_microdollars != null && + isRenderableSessionCost(sessionQuery.data.total_cost_microdollars / 1_000_000) && ( +
+ + + Cost:{' '} + + {formatSessionCost(sessionQuery.data.total_cost_microdollars / 1_000_000)} + + +
+ )} {latestContainerInterval?.cloudflareInstanceId && (
diff --git a/apps/web/src/routers/admin-router.ts b/apps/web/src/routers/admin-router.ts index 051291cc72..e7ba313896 100644 --- a/apps/web/src/routers/admin-router.ts +++ b/apps/web/src/routers/admin-router.ts @@ -2444,8 +2444,9 @@ export const adminRouter = createTRPCRouter({ return { ...session, - // V1 doesn't have git_branch — null it out for a consistent shape + // V1 doesn't have git_branch or per-session cost — null them out for a consistent shape git_branch: null, + total_cost_microdollars: null, user: user ? { id: user.id, diff --git a/apps/web/src/routers/code-reviews-router.test.ts b/apps/web/src/routers/code-reviews-router.test.ts index c849546f1c..6ac882f8f9 100644 --- a/apps/web/src/routers/code-reviews-router.test.ts +++ b/apps/web/src/routers/code-reviews-router.test.ts @@ -589,6 +589,66 @@ describe('codeReviewRouter.get role-gated ids', () => { await db.delete(cloud_agent_code_reviews).where(eq(cloud_agent_code_reviews.id, review.id)); } }); + + it('returns the billing-derived session cost for a terminal review', async () => { + const sessionId = `ses_get_cost_${crypto.randomUUID()}`; + const usageId = crypto.randomUUID(); + const [review] = await db + .insert(cloud_agent_code_reviews) + .values({ + owned_by_organization_id: null, + owned_by_user_id: ownerUser.id, + platform_integration_id: null, + repo_full_name: 'test-org/get-cost-repo', + pr_number: 2, + pr_url: 'https://github.com/test-org/get-cost-repo/pull/2', + pr_title: 'Get cost PR', + pr_author: 'octocat', + base_ref: 'main', + head_ref: 'feature/get-cost', + head_sha: 'sha-get-cost', + status: 'completed', + session_id: `agent_${crypto.randomUUID()}`, + cli_session_id: sessionId, + // The orchestrator never accumulated usage for this review, so the review's + // own total is 0 and the billing aggregate is the only cost available. + total_cost_musd: 0, + created_at: '2026-06-18T09:00:00.000Z', + completed_at: '2026-06-18T11:00:00.000Z', + }) + .returning({ id: cloud_agent_code_reviews.id }); + + await db.insert(microdollar_usage).values({ + id: usageId, + kilo_user_id: ownerUser.id, + cost: 1234, + input_tokens: 1000, + output_tokens: 100, + cache_write_tokens: 0, + cache_hit_tokens: 0, + created_at: '2026-06-18T10:00:00.000Z', + model: 'anthropic/claude-sonnet-4.6', + }); + await db.insert(microdollar_usage_metadata).values({ + id: usageId, + message_id: `msg_${usageId}`, + session_id: sessionId, + created_at: '2026-06-18T10:00:00.000Z', + }); + + try { + const ownerCaller = await createCallerForUser(ownerUser.id); + const ownerResult = await ownerCaller.codeReviews.get({ reviewId: review.id }); + expect(ownerResult.success).toBe(true); + if (!ownerResult.success) throw new Error('expected owner get success'); + expect(ownerResult.sessionCostMusd).toBe(1234); + expect(ownerResult.review.total_cost_musd).toBe(0); + } finally { + await db.delete(microdollar_usage_metadata).where(eq(microdollar_usage_metadata.id, usageId)); + await db.delete(microdollar_usage).where(eq(microdollar_usage.id, usageId)); + await db.delete(cloud_agent_code_reviews).where(eq(cloud_agent_code_reviews.id, review.id)); + } + }); }); describe('personalReviewAgent.createManualReviewJob', () => { diff --git a/apps/web/src/routers/code-reviews/code-reviews-router.ts b/apps/web/src/routers/code-reviews/code-reviews-router.ts index d25d75a0a4..15867df49d 100644 --- a/apps/web/src/routers/code-reviews/code-reviews-router.ts +++ b/apps/web/src/routers/code-reviews/code-reviews-router.ts @@ -456,6 +456,7 @@ export const codeReviewRouter = createTRPCRouter({ }, attempts: visibleAttempts, tokenUsage, + sessionCostMusd: billingUsage?.totalCostMusd ?? null, }); } catch (error) { if (error instanceof TRPCError) {