-
Notifications
You must be signed in to change notification settings - Fork 31
fix(code-reviews): show model and tokens in review summary for v2 reviews #978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,12 @@ | |
| */ | ||
|
|
||
| import { db } from '@/lib/drizzle'; | ||
| import { cloud_agent_code_reviews } from '@kilocode/db/schema'; | ||
| import { eq, and, desc, count, ne, inArray } from 'drizzle-orm'; | ||
| import { | ||
| cloud_agent_code_reviews, | ||
| microdollar_usage, | ||
| microdollar_usage_metadata, | ||
| } from '@kilocode/db/schema'; | ||
| import { eq, and, desc, count, ne, inArray, sql, sum } from 'drizzle-orm'; | ||
| import { captureException } from '@sentry/nextjs'; | ||
| import type { CreateReviewParams, CodeReviewStatus, ListReviewsParams, Owner } from '../core'; | ||
| import type { CloudAgentCodeReview } from '@kilocode/db/schema'; | ||
|
|
@@ -479,3 +483,76 @@ export async function userOwnsReview(reviewId: string, userId: string): Promise< | |
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Result of aggregating billing usage for a session. | ||
| */ | ||
| export type SessionUsageSummary = { | ||
| model: string; | ||
| totalTokensIn: number; | ||
| totalTokensOut: number; | ||
| totalCostMusd: number; | ||
| }; | ||
|
|
||
| /** | ||
| * Aggregates LLM usage from the billing tables for a given kilo session ID. | ||
| * | ||
| * This is the fallback path for v2 (cloud-agent-next) reviews where the | ||
| * orchestrator does not accumulate usage from SSE events. The billing | ||
| * system (processUsage → microdollar_usage) already records per-request | ||
| * usage keyed by session_id, so we aggregate here. | ||
| * | ||
| * Uses two queries: | ||
| * 1. Session-wide totals (tokens + cost across all models) | ||
| * 2. The model with the most tokens (the primary review model name) | ||
| * | ||
| * This avoids undercounting when a session uses more than one model. | ||
| */ | ||
| export async function getSessionUsageFromBilling( | ||
| cliSessionId: string | ||
| ): Promise<SessionUsageSummary | null> { | ||
| try { | ||
| const sessionFilter = eq(microdollar_usage_metadata.session_id, cliSessionId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: This fallback filters on an unindexed column
|
||
| const joinCondition = eq(microdollar_usage.id, microdollar_usage_metadata.id); | ||
|
|
||
| // 1. Session-wide totals (all models combined) | ||
| const [totals] = await db | ||
| .select({ | ||
| totalTokensIn: sum(microdollar_usage.input_tokens).mapWith(Number), | ||
| totalTokensOut: sum(microdollar_usage.output_tokens).mapWith(Number), | ||
| totalCostMusd: sum(microdollar_usage.cost).mapWith(Number), | ||
| }) | ||
| .from(microdollar_usage) | ||
| .innerJoin(microdollar_usage_metadata, joinCondition) | ||
| .where(sessionFilter); | ||
|
|
||
| if (totals?.totalTokensIn == null) return null; | ||
|
|
||
| // 2. Pick the model with the most tokens (the primary review model) | ||
| const [topModel] = await db | ||
| .select({ model: microdollar_usage.model }) | ||
| .from(microdollar_usage) | ||
| .innerJoin(microdollar_usage_metadata, joinCondition) | ||
| .where(sessionFilter) | ||
| .groupBy(microdollar_usage.model) | ||
|
alex-alecu marked this conversation as resolved.
|
||
| .orderBy( | ||
| sql`sum(${microdollar_usage.input_tokens} + ${microdollar_usage.output_tokens}) desc` | ||
| ) | ||
| .limit(1); | ||
|
|
||
| if (!topModel?.model) return null; | ||
|
|
||
| return { | ||
| model: topModel.model, | ||
| totalTokensIn: totals.totalTokensIn, | ||
| totalTokensOut: totals.totalTokensOut ?? 0, | ||
| totalCostMusd: totals.totalCostMusd ?? 0, | ||
| }; | ||
| } catch (error) { | ||
| captureException(error, { | ||
| tags: { operation: 'getSessionUsageFromBilling' }, | ||
| extra: { cliSessionId }, | ||
| }); | ||
| return null; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: This adds a fixed delay to every v2 completion callback
The comment above says cloud-agent-next reviews never receive orchestrator usage, but this loop still waits through the full exponential backoff whenever
review.modelis empty. For v2 reviews that means every completion path pays ~1.4s before we even try the billing fallback, which delays the reaction/comment update for every successful review.