Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' ||
Expand Down Expand Up @@ -264,10 +272,10 @@ export function CodeReviewDetailClient({ reviewId }: CodeReviewDetailClientProps
</dd>
</div>
)}
{review.total_cost_musd != null && review.total_cost_musd > 0 && (
{sessionCostMusd != null && isRenderableSessionCost(sessionCostMusd / 1_000_000) && (
<div>
<dt className="text-muted-foreground">Cost</dt>
<dd>${(review.total_cost_musd / 1_000_000).toFixed(4)}</dd>
<dd>{formatSessionCost(sessionCostMusd / 1_000_000)}</dd>
</div>
)}
<div>
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/app/admin/components/SessionTraceViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -403,6 +408,18 @@ export function SessionTraceViewer() {
</span>
</div>
)}
{sessionQuery.data.total_cost_microdollars != null &&
isRenderableSessionCost(sessionQuery.data.total_cost_microdollars / 1_000_000) && (
<div className="flex items-center gap-2">
<DollarSign className="text-muted-foreground h-4 w-4" />
<span className="text-sm">
Cost:{' '}
<span className="font-mono tabular-nums">
{formatSessionCost(sessionQuery.data.total_cost_microdollars / 1_000_000)}
</span>
</span>
</div>
)}
{latestContainerInterval?.cloudflareInstanceId && (
<div className="flex min-w-0 items-center gap-2">
<Server className="text-muted-foreground h-4 w-4 shrink-0" />
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/routers/admin-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions apps/web/src/routers/code-reviews-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/routers/code-reviews/code-reviews-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ export const codeReviewRouter = createTRPCRouter({
},
attempts: visibleAttempts,
tokenUsage,
sessionCostMusd: billingUsage?.totalCostMusd ?? null,
});
} catch (error) {
if (error instanceof TRPCError) {
Expand Down