Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/app/api/openrouter/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
isDataCollectionRequiredOnKiloCodeOnly,
isDeadFreeModel,
isSlackbotOnlyModel,
isReviewOnlyModel,
isReviewPromotionActive,
isRateLimitedModel,
} from '@/lib/models';
import {
Expand Down Expand Up @@ -250,6 +252,14 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
return modelDoesNotExistResponse();
}

// Review-only promotional models are only available through Code Reviewer (internalApiUse)
// and only during the active promotion window
if (isReviewOnlyModel(originalModelIdLowerCased)) {
if (!internalApiUse || !isReviewPromotionActive(originalModelIdLowerCased)) {
return modelDoesNotExistResponse();
}
}

// Extract properties for usage context
const tokenEstimates = estimateChatTokens(requestBodyParsed);
const promptInfo = extractPromptInfo(requestBodyParsed);
Expand Down
20 changes: 17 additions & 3 deletions src/lib/code-reviews/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@
* Constants used throughout the code review system.
*/

import { getActiveReviewPromotionModel } from '@/lib/models';

// ============================================================================
// Review Configuration
// ============================================================================

/**
* Default model for code reviews
* Using Claude Sonnet 4.5 as specified in the plan
* Default model for code reviews.
* Falls back to Claude Sonnet 4.5 when no promotion is active.
*/
export const DEFAULT_CODE_REVIEW_MODEL = 'anthropic/claude-sonnet-4.5';
const BASE_CODE_REVIEW_MODEL = 'anthropic/claude-sonnet-4.5';

/**
* Returns the effective default model for code reviews.
* If a review-only promotional model is currently active, it takes precedence.
*/
export function getDefaultCodeReviewModel(): string {
const promoModel = getActiveReviewPromotionModel();
return promoModel?.internal_id ?? BASE_CODE_REVIEW_MODEL;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: getDefaultCodeReviewModel() returns internal_id instead of public_id, which means the promotional model will not be recognized as free by the API route.

The API route at src/app/api/openrouter/[...path]/route.ts uses the model ID to check isFreeModel(), isReviewOnlyModel(), and isRateLimitedModel() — all of which match against public_id. The applyProviderSpecificLogic() function in src/lib/providers/index.ts:243 then rewrites requestToMutate.model from public_idinternal_id before sending to OpenRouter.

By returning internal_id here, the model bypasses the review-only access gate and isn't recognized as free, so users will be charged credits for what's supposed to be a free promotional model.

For reference, the Slack integration correctly uses public_id (opus_46_free_slackbot_model.public_id) in src/lib/integrations/slack-service.ts:20.

Suggested change
return promoModel?.internal_id ?? BASE_CODE_REVIEW_MODEL;
return promoModel?.public_id ?? BASE_CODE_REVIEW_MODEL;

}

/** @deprecated Use getDefaultCodeReviewModel() for promotion-aware model selection */
export const DEFAULT_CODE_REVIEW_MODEL = BASE_CODE_REVIEW_MODEL;

/**
* Default mode for cloud agent sessions
Expand Down
22 changes: 19 additions & 3 deletions src/lib/code-reviews/triggers/prepare-review-payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import type {
} from '../prompts/generate-prompt';
import { getIntegrationById } from '@/lib/integrations/db/platform-integrations';
import { getCodeReviewById } from '../db/code-reviews';
import { DEFAULT_CODE_REVIEW_MODEL, DEFAULT_CODE_REVIEW_MODE } from '../core/constants';
import { DEFAULT_CODE_REVIEW_MODE, getDefaultCodeReviewModel } from '../core/constants';
import { getActiveReviewPromotionModel } from '@/lib/models';
import type { Owner } from '../core';
import { generateReviewPrompt } from '../prompts/generate-prompt';
import type { CodeReviewAgentConfig } from '@/lib/agent-config/core/types';
Expand Down Expand Up @@ -341,7 +342,7 @@ export async function prepareReviewPayload(
kilocodeOrganizationId: owner.type === 'org' ? owner.id : undefined,
prompt,
mode: DEFAULT_CODE_REVIEW_MODE as 'code',
model: config.model_slug || DEFAULT_CODE_REVIEW_MODEL,
model: config.model_slug || getDefaultCodeReviewModel(),
upstreamBranch: review.head_ref,
}
: {
Expand All @@ -351,7 +352,7 @@ export async function prepareReviewPayload(
kilocodeOrganizationId: owner.type === 'org' ? owner.id : undefined,
prompt,
mode: DEFAULT_CODE_REVIEW_MODE as 'code',
model: config.model_slug || DEFAULT_CODE_REVIEW_MODEL,
model: config.model_slug || getDefaultCodeReviewModel(),
upstreamBranch: review.head_ref,
};

Expand All @@ -365,6 +366,21 @@ export async function prepareReviewPayload(
});
}

// 6b. Log if a promotional model is being used for tracking
const activePromoModel = getActiveReviewPromotionModel();
const effectiveModel = sessionInput.model;
if (activePromoModel && effectiveModel === activePromoModel.internal_id) {
logExceptInTest('[prepareReviewPayload] Using promotional model for code review', {
reviewId,
promotionModel: activePromoModel.public_id,
effectiveModel,
promotionStart: activePromoModel.promotion_start,
promotionEnd: activePromoModel.promotion_end,
ownerType: owner.type,
ownerId: owner.id,
});
}

// 7. Build complete payload
const payload: CodeReviewPayload = {
reviewId,
Expand Down
40 changes: 40 additions & 0 deletions src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CLAUDE_OPUS_CURRENT_MODEL_ID,
CLAUDE_SONNET_CURRENT_MODEL_ID,
opus_46_free_slackbot_model,
sonnet_46_free_review_model,
} from '@/lib/providers/anthropic';
import { corethink_free_model } from '@/lib/providers/corethink';
import { giga_potato_model } from '@/lib/providers/gigapotato';
Expand Down Expand Up @@ -65,6 +66,7 @@ export const kiloFreeModels = [
minimax_m21_free_model,
minimax_m25_free_model,
opus_46_free_slackbot_model,
sonnet_46_free_review_model,
grok_code_fast_1_optimized_free_model,
zai_glm47_free_model,
zai_glm5_free_model,
Expand Down Expand Up @@ -96,3 +98,41 @@ export function isDeadFreeModel(model: string): boolean {
export function isSlackbotOnlyModel(model: string): boolean {
return !!kiloFreeModels.find(m => m.public_id === model && m.slackbot_only);
}

/**
* Check if a model is a review-only promotional model.
* These models are hidden from the public model list and only available in Code Reviewer.
*/
export function isReviewOnlyModel(model: string): boolean {
return !!kiloFreeModels.find(m => m.public_id === model && m.review_only);
}

/**
* Check if a review-only promotional model is currently within its active promotion window.
* Returns false for non-review-only models or if the promotion has expired.
*/
export function isReviewPromotionActive(model: string, now = new Date()): boolean {
const freeModel = kiloFreeModels.find(
m => m.public_id === model && m.review_only && m.is_enabled
);
if (!freeModel) return false;
if (freeModel.promotion_start && now < new Date(freeModel.promotion_start)) return false;
if (freeModel.promotion_end && now >= new Date(freeModel.promotion_end)) return false;
return true;
}

/**
* Get the active review promotion model, if any.
* Returns the model config if a review-only promotion is currently active, null otherwise.
*/
export function getActiveReviewPromotionModel(now = new Date()): KiloFreeModel | null {
return (
kiloFreeModels.find(
m =>
m.review_only &&
m.is_enabled &&
(!m.promotion_start || now >= new Date(m.promotion_start)) &&
(!m.promotion_end || now < new Date(m.promotion_end))
) ?? null
);
}
18 changes: 18 additions & 0 deletions src/lib/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ export const opus_46_free_slackbot_model = {
slackbot_only: true,
} as KiloFreeModel;

export const SONNET_46_REVIEW_PROMO_MODEL_ID = 'anthropic/claude-sonnet-4-6-20250514:review';

export const sonnet_46_free_review_model = {
public_id: SONNET_46_REVIEW_PROMO_MODEL_ID,
display_name: 'Anthropic: Claude Sonnet 4.6 (Free for Code Reviewer)',
description: 'Claude Sonnet 4.6 — free for one week in Code Reviewer (review mode)',
context_length: 200_000,
max_completion_tokens: 16384,
is_enabled: true,
flags: ['reasoning', 'prompt_cache', 'vision'],
gateway: 'openrouter',
internal_id: 'anthropic/claude-sonnet-4-6-20250514',
inference_providers: ['anthropic'],
review_only: true,
promotion_start: '2026-02-18T00:00:00Z',
promotion_end: '2026-02-25T00:00:00Z',
} as KiloFreeModel;

const ENABLE_ANTHROPIC_STRICT_TOOL_USE = false;

const ENABLE_ANTHROPIC_AUTOMATIC_CACHING = true;
Expand Down
6 changes: 6 additions & 0 deletions src/lib/providers/kilo-free-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export type KiloFreeModel = {
inference_providers: OpenRouterInferenceProviderId[];
/** If true, this model is only available through Kilo for Slack (internalApiUse) and hidden from public model list */
slackbot_only?: boolean;
/** If true, this model is only available in Code Reviewer (review mode) and hidden from the public model list */
review_only?: boolean;
/** Promotion start date (ISO 8601). If set, the model is only active after this date. */
promotion_start?: string;
/** Promotion end date (ISO 8601). If set, the model is disabled after this date. */
promotion_end?: string;
};

export function convertFromKiloModel(model: KiloFreeModel) {
Expand Down
6 changes: 5 additions & 1 deletion src/lib/providers/openrouter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ function enhancedModelList(models: OpenRouterModel[]) {
!kiloFreeModels.some(m => m.public_id === model.id && m.is_enabled) &&
!isRateLimitedToDeath(model.id)
)
.concat(kiloFreeModels.filter(m => m.is_enabled).map(model => convertFromKiloModel(model)))
.concat(
kiloFreeModels
.filter(m => m.is_enabled && !m.slackbot_only && !m.review_only)
.map(model => convertFromKiloModel(model))
)
.concat([autoModel])
.map((model: OpenRouterModel) => {
const preferredIndex =
Expand Down
71 changes: 70 additions & 1 deletion src/routers/admin-code-reviews-router.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init';
import { db } from '@/lib/drizzle';
import { cloud_agent_code_reviews, kilocode_users, organizations } from '@/db/schema';
import { cloud_agent_code_reviews, cliSessions, kilocode_users, organizations } from '@/db/schema';
import * as z from 'zod';
import { sql, and, gte, lt, eq, isNotNull, desc, ilike, or, type SQL } from 'drizzle-orm';
import { sonnet_46_free_review_model } from '@/lib/providers/anthropic';

/**
* SQL condition to exclude "Insufficient credits" errors from failure metrics.
Expand Down Expand Up @@ -371,4 +372,72 @@ export const adminCodeReviewsRouter = createTRPCRouter({

return result;
}),

// Sonnet 4.6 free review promotion tracking
getReviewPromotionStats: adminProcedure.query(async () => {
const promoModel = sonnet_46_free_review_model;
const promoStart = promoModel.promotion_start ?? '2026-02-18T00:00:00Z';
const promoEnd = promoModel.promotion_end ?? '2026-02-25T00:00:00Z';
const promoModelId = promoModel.internal_id;

// Query code reviews that used the promotional model during the promotion window
// Join with cli_sessions to check last_model
const result = await db
.select({
total_promo_reviews: sql<number>`COUNT(*)`,
completed_promo_reviews: sql<number>`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'completed')`,
failed_promo_reviews: sql<number>`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'failed')`,
unique_users: sql<number>`COUNT(DISTINCT COALESCE(${cloud_agent_code_reviews.owned_by_user_id}, ${cloud_agent_code_reviews.owned_by_organization_id}))`,
unique_orgs: sql<number>`COUNT(DISTINCT ${cloud_agent_code_reviews.owned_by_organization_id})`,
})
.from(cloud_agent_code_reviews)
.innerJoin(cliSessions, eq(cloud_agent_code_reviews.cli_session_id, cliSessions.session_id))
.where(
and(
gte(cloud_agent_code_reviews.created_at, promoStart),
lt(cloud_agent_code_reviews.created_at, promoEnd),
eq(cliSessions.last_model, promoModelId)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This query matches last_model against promoModel.internal_id. If the critical bug in getDefaultCodeReviewModel() is fixed to return public_id instead of internal_id, then last_model in the DB will store the public_id (e.g. anthropic/claude-sonnet-4-6-20250514:review), and this query will need to match on public_id as well.

Consider using promoModel.public_id here to stay consistent with the fix, or add a comment clarifying which ID format last_model stores.

)
);

// Daily breakdown
const dailyBreakdown = await db
.select({
day: sql<string>`DATE_TRUNC('day', ${cloud_agent_code_reviews.created_at})::date::text`,
total: sql<number>`COUNT(*)`,
completed: sql<number>`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'completed')`,
unique_users: sql<number>`COUNT(DISTINCT COALESCE(${cloud_agent_code_reviews.owned_by_user_id}, ${cloud_agent_code_reviews.owned_by_organization_id}))`,
})
.from(cloud_agent_code_reviews)
.innerJoin(cliSessions, eq(cloud_agent_code_reviews.cli_session_id, cliSessions.session_id))
.where(
and(
gte(cloud_agent_code_reviews.created_at, promoStart),
lt(cloud_agent_code_reviews.created_at, promoEnd),
eq(cliSessions.last_model, promoModelId)
)
)
.groupBy(sql`DATE_TRUNC('day', ${cloud_agent_code_reviews.created_at})`)
.orderBy(sql`DATE_TRUNC('day', ${cloud_agent_code_reviews.created_at})`);

const stats = result[0];
return {
promotionModelId: promoModel.public_id,
promotionInternalModelId: promoModelId,
promotionStart: promoStart,
promotionEnd: promoEnd,
isActive: new Date() >= new Date(promoStart) && new Date() < new Date(promoEnd),
totalPromoReviews: Number(stats.total_promo_reviews) || 0,
completedPromoReviews: Number(stats.completed_promo_reviews) || 0,
failedPromoReviews: Number(stats.failed_promo_reviews) || 0,
uniqueUsers: Number(stats.unique_users) || 0,
uniqueOrgs: Number(stats.unique_orgs) || 0,
dailyBreakdown: dailyBreakdown.map(row => ({
day: row.day,
total: Number(row.total) || 0,
completed: Number(row.completed) || 0,
uniqueUsers: Number(row.unique_users) || 0,
})),
};
}),
});