From 754bc4cfce549bc45ad7db65c9ecfa0bd8cefe7c Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:58:57 +0000 Subject: [PATCH 1/7] feat: make Sonnet 4.6 free for one week in review mode - Add claude-sonnet-4-6-20250514 as a free promotional model for Code Reviewer - Promotion window: 2026-02-18 to 2026-02-25 (7 days) - Model is hidden from public model list (review_only flag) - Only accessible through internal API (Code Reviewer) during promotion - Add promotion tracking admin endpoint (getReviewPromotionStats) - Add structured logging for promotion model usage --- src/app/api/openrouter/[...path]/route.ts | 10 +++ src/lib/code-reviews/core/constants.ts | 20 +++++- .../triggers/prepare-review-payload.ts | 22 +++++- src/lib/models.ts | 40 +++++++++++ src/lib/providers/anthropic.ts | 18 +++++ src/lib/providers/kilo-free-model.ts | 6 ++ src/lib/providers/openrouter/index.ts | 6 +- src/routers/admin-code-reviews-router.ts | 71 ++++++++++++++++++- 8 files changed, 185 insertions(+), 8 deletions(-) diff --git a/src/app/api/openrouter/[...path]/route.ts b/src/app/api/openrouter/[...path]/route.ts index adf45e227..c903347b8 100644 --- a/src/app/api/openrouter/[...path]/route.ts +++ b/src/app/api/openrouter/[...path]/route.ts @@ -14,6 +14,8 @@ import { isDataCollectionRequiredOnKiloCodeOnly, isDeadFreeModel, isSlackbotOnlyModel, + isReviewOnlyModel, + isReviewPromotionActive, isRateLimitedModel, } from '@/lib/models'; import { @@ -250,6 +252,14 @@ export async function POST(request: NextRequest): Promise 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 + ); +} diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index fe82295f6..26dbd1794 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -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; diff --git a/src/lib/providers/kilo-free-model.ts b/src/lib/providers/kilo-free-model.ts index 29c81f5b0..06fd4ac07 100644 --- a/src/lib/providers/kilo-free-model.ts +++ b/src/lib/providers/kilo-free-model.ts @@ -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) { diff --git a/src/lib/providers/openrouter/index.ts b/src/lib/providers/openrouter/index.ts index ec8e6471a..e015e610c 100644 --- a/src/lib/providers/openrouter/index.ts +++ b/src/lib/providers/openrouter/index.ts @@ -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 = diff --git a/src/routers/admin-code-reviews-router.ts b/src/routers/admin-code-reviews-router.ts index 212f69f47..138d80b1b 100644 --- a/src/routers/admin-code-reviews-router.ts +++ b/src/routers/admin-code-reviews-router.ts @@ -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. @@ -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`COUNT(*)`, + completed_promo_reviews: sql`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'completed')`, + failed_promo_reviews: sql`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'failed')`, + unique_users: sql`COUNT(DISTINCT COALESCE(${cloud_agent_code_reviews.owned_by_user_id}, ${cloud_agent_code_reviews.owned_by_organization_id}))`, + unique_orgs: sql`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) + ) + ); + + // Daily breakdown + const dailyBreakdown = await db + .select({ + day: sql`DATE_TRUNC('day', ${cloud_agent_code_reviews.created_at})::date::text`, + total: sql`COUNT(*)`, + completed: sql`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'completed')`, + unique_users: sql`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, + })), + }; + }), }); From 8532000258ad1dd10331da700a9e62cea6db7ea5 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 18 Feb 2026 09:58:33 +0200 Subject: [PATCH 2/7] fix: Fix promo review stats query Use leftJoin instead of innerJoin for cli_sessions to avoid dropping reviews without a session. Fix COALESCE type mismatch, default empty stats result, exclude review_only models from rate limiting, and remove deprecated DEFAULT_CODE_REVIEW_MODEL constant. --- src/lib/code-reviews/core/constants.ts | 3 --- src/lib/models.ts | 4 +++- src/lib/providers/anthropic.ts | 16 ++++++++-------- src/routers/admin-code-reviews-router.ts | 21 ++++++++++++++------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/lib/code-reviews/core/constants.ts b/src/lib/code-reviews/core/constants.ts index bc284c6ef..18a9f07a6 100644 --- a/src/lib/code-reviews/core/constants.ts +++ b/src/lib/code-reviews/core/constants.ts @@ -25,9 +25,6 @@ export function getDefaultCodeReviewModel(): string { return promoModel?.internal_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 */ diff --git a/src/lib/models.ts b/src/lib/models.ts index c077fb3a4..65cbfead8 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -53,7 +53,9 @@ export function isFreeModel(model: string): boolean { } export function isRateLimitedModel(model: string): boolean { - return kiloFreeModels.some(m => m.public_id === model && m.is_enabled && !m.slackbot_only); + return kiloFreeModels.some( + m => m.public_id === model && m.is_enabled && !m.slackbot_only && !m.review_only + ); } export function isDataCollectionRequiredOnKiloCodeOnly(model: string): boolean { diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index 78f18b06d..15b97f32c 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -14,12 +14,12 @@ export const opus_46_free_slackbot_model = { context_length: 1_000_000, max_completion_tokens: 32000, is_enabled: false, - flags: ['reasoning', 'prompt_cache', 'vision'], - gateway: 'openrouter', + flags: ['reasoning', 'prompt_cache', 'vision'] as KiloFreeModel['flags'], + gateway: 'openrouter' as const, internal_id: 'anthropic/claude-opus-4.6', - inference_providers: [], + inference_providers: [] as KiloFreeModel['inference_providers'], slackbot_only: true, -} as KiloFreeModel; +} satisfies KiloFreeModel; export const SONNET_46_REVIEW_PROMO_MODEL_ID = 'anthropic/claude-sonnet-4-6-20250514:review'; @@ -30,14 +30,14 @@ export const sonnet_46_free_review_model = { context_length: 200_000, max_completion_tokens: 16384, is_enabled: true, - flags: ['reasoning', 'prompt_cache', 'vision'], - gateway: 'openrouter', + flags: ['reasoning', 'prompt_cache', 'vision'] as KiloFreeModel['flags'], + gateway: 'openrouter' as const, internal_id: 'anthropic/claude-sonnet-4-6-20250514', - inference_providers: ['anthropic'], + inference_providers: ['anthropic'] as KiloFreeModel['inference_providers'], review_only: true, promotion_start: '2026-02-18T00:00:00Z', promotion_end: '2026-02-25T00:00:00Z', -} as KiloFreeModel; +} satisfies KiloFreeModel; const ENABLE_ANTHROPIC_STRICT_TOOL_USE = false; diff --git a/src/routers/admin-code-reviews-router.ts b/src/routers/admin-code-reviews-router.ts index 138d80b1b..2df64ffc5 100644 --- a/src/routers/admin-code-reviews-router.ts +++ b/src/routers/admin-code-reviews-router.ts @@ -380,18 +380,19 @@ export const adminCodeReviewsRouter = createTRPCRouter({ 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 + // Query code reviews that used the promotional model during the promotion window. + // Left join with cli_sessions to check last_model (cli_session_id is nullable). + // Rows without a cli_session are naturally excluded by the eq() predicate on last_model. const result = await db .select({ total_promo_reviews: sql`COUNT(*)`, completed_promo_reviews: sql`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'completed')`, failed_promo_reviews: sql`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'failed')`, - unique_users: sql`COUNT(DISTINCT COALESCE(${cloud_agent_code_reviews.owned_by_user_id}, ${cloud_agent_code_reviews.owned_by_organization_id}))`, + unique_users: sql`COUNT(DISTINCT COALESCE(${cloud_agent_code_reviews.owned_by_user_id}, ${cloud_agent_code_reviews.owned_by_organization_id}::text))`, unique_orgs: sql`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)) + .leftJoin(cliSessions, eq(cloud_agent_code_reviews.cli_session_id, cliSessions.session_id)) .where( and( gte(cloud_agent_code_reviews.created_at, promoStart), @@ -406,10 +407,10 @@ export const adminCodeReviewsRouter = createTRPCRouter({ day: sql`DATE_TRUNC('day', ${cloud_agent_code_reviews.created_at})::date::text`, total: sql`COUNT(*)`, completed: sql`COUNT(*) FILTER (WHERE ${cloud_agent_code_reviews.status} = 'completed')`, - unique_users: sql`COUNT(DISTINCT COALESCE(${cloud_agent_code_reviews.owned_by_user_id}, ${cloud_agent_code_reviews.owned_by_organization_id}))`, + unique_users: sql`COUNT(DISTINCT COALESCE(${cloud_agent_code_reviews.owned_by_user_id}, ${cloud_agent_code_reviews.owned_by_organization_id}::text))`, }) .from(cloud_agent_code_reviews) - .innerJoin(cliSessions, eq(cloud_agent_code_reviews.cli_session_id, cliSessions.session_id)) + .leftJoin(cliSessions, eq(cloud_agent_code_reviews.cli_session_id, cliSessions.session_id)) .where( and( gte(cloud_agent_code_reviews.created_at, promoStart), @@ -420,7 +421,13 @@ export const adminCodeReviewsRouter = createTRPCRouter({ .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]; + const stats = result[0] ?? { + total_promo_reviews: 0, + completed_promo_reviews: 0, + failed_promo_reviews: 0, + unique_users: 0, + unique_orgs: 0, + }; return { promotionModelId: promoModel.public_id, promotionInternalModelId: promoModelId, From 70e9ea2151609be1a2ab8176a823ae04fbbf1f36 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 18 Feb 2026 10:19:56 +0200 Subject: [PATCH 3/7] fix: Fix sonnet 4.6 model id --- src/lib/providers/anthropic.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index 15b97f32c..d3352698a 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -21,18 +21,18 @@ export const opus_46_free_slackbot_model = { slackbot_only: true, } satisfies KiloFreeModel; -export const SONNET_46_REVIEW_PROMO_MODEL_ID = 'anthropic/claude-sonnet-4-6-20250514:review'; +export const SONNET_46_REVIEW_PROMO_MODEL_ID = 'anthropic/claude-sonnet-4.6: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, + context_length: 1_000_000, max_completion_tokens: 16384, is_enabled: true, flags: ['reasoning', 'prompt_cache', 'vision'] as KiloFreeModel['flags'], gateway: 'openrouter' as const, - internal_id: 'anthropic/claude-sonnet-4-6-20250514', + internal_id: 'anthropic/claude-sonnet-4.6', inference_providers: ['anthropic'] as KiloFreeModel['inference_providers'], review_only: true, promotion_start: '2026-02-18T00:00:00Z', From cd9d73128eef378cf9f8082cceef702d19224ae1 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 18 Feb 2026 10:30:33 +0200 Subject: [PATCH 4/7] fix: Revert old changes --- src/lib/providers/anthropic.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index d3352698a..1fe4b9dc4 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -14,10 +14,10 @@ export const opus_46_free_slackbot_model = { context_length: 1_000_000, max_completion_tokens: 32000, is_enabled: false, - flags: ['reasoning', 'prompt_cache', 'vision'] as KiloFreeModel['flags'], - gateway: 'openrouter' as const, + flags: ['reasoning', 'prompt_cache', 'vision'], + gateway: 'openrouter', internal_id: 'anthropic/claude-opus-4.6', - inference_providers: [] as KiloFreeModel['inference_providers'], + inference_providers: [], slackbot_only: true, } satisfies KiloFreeModel; From e19190f6ddf95eb9ecbdfed69e5b866dd9018196 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 18 Feb 2026 11:49:13 +0200 Subject: [PATCH 5/7] refactor: Migrate slackbot_only/review_only booleans to allowed_uses array --- src/app/api/openrouter/[...path]/route.ts | 15 ++++--- .../hooks/useResumeConfigModal.ts | 11 +++-- .../cloud-agent/hooks/useResumeConfigModal.ts | 11 +++-- src/lib/models.ts | 42 ++++++------------- src/lib/providers/anthropic.ts | 4 +- src/lib/providers/kilo-free-model.ts | 7 ++-- src/lib/providers/openrouter/index.ts | 2 +- 7 files changed, 36 insertions(+), 56 deletions(-) diff --git a/src/app/api/openrouter/[...path]/route.ts b/src/app/api/openrouter/[...path]/route.ts index c903347b8..ff2bc4861 100644 --- a/src/app/api/openrouter/[...path]/route.ts +++ b/src/app/api/openrouter/[...path]/route.ts @@ -13,8 +13,7 @@ import { isFreeModel, isDataCollectionRequiredOnKiloCodeOnly, isDeadFreeModel, - isSlackbotOnlyModel, - isReviewOnlyModel, + kiloFreeModels, isReviewPromotionActive, isRateLimitedModel, } from '@/lib/models'; @@ -247,14 +246,14 @@ export async function POST(request: NextRequest): Promise m.public_id === originalModelIdLowerCased + )?.allowed_uses; + + if (modelAllowedUses?.includes('slackbot') && !internalApiUse) { return modelDoesNotExistResponse(); } - - // Review-only promotional models are only available through Code Reviewer (internalApiUse) - // and only during the active promotion window - if (isReviewOnlyModel(originalModelIdLowerCased)) { + if (modelAllowedUses?.includes('review')) { if (!internalApiUse || !isReviewPromotionActive(originalModelIdLowerCased)) { return modelDoesNotExistResponse(); } diff --git a/src/components/cloud-agent-next/hooks/useResumeConfigModal.ts b/src/components/cloud-agent-next/hooks/useResumeConfigModal.ts index a1d72f330..e7f8c3f56 100644 --- a/src/components/cloud-agent-next/hooks/useResumeConfigModal.ts +++ b/src/components/cloud-agent-next/hooks/useResumeConfigModal.ts @@ -1,5 +1,5 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; -import { isSlackbotOnlyModel } from '@/lib/models'; +import { kiloFreeModels } from '@/lib/models'; import { type DbSessionDetails, type IndexedDbSessionData } from '../store/db-session-atoms'; import { extractRepoFromGitUrl } from '../utils/git-utils'; import type { ResumeConfig, StreamResumeConfig } from '../types'; @@ -65,11 +65,10 @@ export function needsResumeConfigModal(params: { if (!loadedDbSession) return false; - if ( - loadedDbSession.last_model && - isSlackbotOnlyModel(loadedDbSession.last_model) && - !currentIndexedDbSession?.resumeConfig - ) + const lastModelRestricted = kiloFreeModels.find(m => m.public_id === loadedDbSession.last_model) + ?.allowed_uses?.length; + + if (loadedDbSession.last_model && lastModelRestricted && !currentIndexedDbSession?.resumeConfig) return true; const isCliSession = !loadedDbSession.cloud_agent_session_id; diff --git a/src/components/cloud-agent/hooks/useResumeConfigModal.ts b/src/components/cloud-agent/hooks/useResumeConfigModal.ts index bb127ace2..abb1fa2e5 100644 --- a/src/components/cloud-agent/hooks/useResumeConfigModal.ts +++ b/src/components/cloud-agent/hooks/useResumeConfigModal.ts @@ -1,5 +1,5 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; -import { isSlackbotOnlyModel } from '@/lib/models'; +import { kiloFreeModels } from '@/lib/models'; import { type DbSessionDetails, type IndexedDbSessionData } from '../store/db-session-atoms'; import { extractRepoFromGitUrl } from '../utils/git-utils'; import type { ResumeConfig, StreamResumeConfig } from '../types'; @@ -65,11 +65,10 @@ export function needsResumeConfigModal(params: { if (!loadedDbSession) return false; - if ( - loadedDbSession.last_model && - isSlackbotOnlyModel(loadedDbSession.last_model) && - !currentIndexedDbSession?.resumeConfig - ) + const lastModelRestricted = kiloFreeModels.find(m => m.public_id === loadedDbSession.last_model) + ?.allowed_uses?.length; + + if (loadedDbSession.last_model && lastModelRestricted && !currentIndexedDbSession?.resumeConfig) return true; const isCliSession = !loadedDbSession.cloud_agent_session_id; diff --git a/src/lib/models.ts b/src/lib/models.ts index 65cbfead8..4a5ef704d 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -11,7 +11,7 @@ import { } from '@/lib/providers/anthropic'; import { corethink_free_model } from '@/lib/providers/corethink'; import { giga_potato_model } from '@/lib/providers/gigapotato'; -import type { KiloFreeModel } from '@/lib/providers/kilo-free-model'; +import type { KiloFreeModel, KiloFreeModelUse } from '@/lib/providers/kilo-free-model'; import { minimax_m21_free_model, minimax_m25_free_model } from '@/lib/providers/minimax'; import { grok_code_fast_1_optimized_free_model } from '@/lib/providers/xai'; import { zai_glm47_free_model, zai_glm5_free_model } from '@/lib/providers/zai'; @@ -53,9 +53,7 @@ export function isFreeModel(model: string): boolean { } export function isRateLimitedModel(model: string): boolean { - return kiloFreeModels.some( - m => m.public_id === model && m.is_enabled && !m.slackbot_only && !m.review_only - ); + return kiloFreeModels.some(m => m.public_id === model && m.is_enabled && !m.allowed_uses?.length); } export function isDataCollectionRequiredOnKiloCodeOnly(model: string): boolean { @@ -92,30 +90,19 @@ export function isDeadFreeModel(model: string): boolean { return !!kiloFreeModels.find(m => m.public_id === model && !m.is_enabled); } -/** - * Check if a model is only available through Kilo for Slack (internalApiUse). - * These models are hidden from the public model list and return "model does not exist" - * when accessed outside of the Slack integration. - */ -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); +export function isFreeModelAllowedForUse(model: string, use: KiloFreeModelUse): boolean { + return kiloFreeModels.some( + m => + m.public_id === model && + m.is_enabled && + (!m.allowed_uses?.length || m.allowed_uses.includes(use)) + ); } -/** - * 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. - */ +/** Returns true if the model has allowed_uses including 'review' and is within its promotion window. */ export function isReviewPromotionActive(model: string, now = new Date()): boolean { const freeModel = kiloFreeModels.find( - m => m.public_id === model && m.review_only && m.is_enabled + m => m.public_id === model && m.allowed_uses?.includes('review') && m.is_enabled ); if (!freeModel) return false; if (freeModel.promotion_start && now < new Date(freeModel.promotion_start)) return false; @@ -123,15 +110,12 @@ export function isReviewPromotionActive(model: string, now = new Date()): boolea return true; } -/** - * Get the active review promotion model, if any. - * Returns the model config if a review-only promotion is currently active, null otherwise. - */ +/** Returns the first review-use model whose promotion window is currently active, or null. */ export function getActiveReviewPromotionModel(now = new Date()): KiloFreeModel | null { return ( kiloFreeModels.find( m => - m.review_only && + m.allowed_uses?.includes('review') && m.is_enabled && (!m.promotion_start || now >= new Date(m.promotion_start)) && (!m.promotion_end || now < new Date(m.promotion_end)) diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index 1fe4b9dc4..30e3df18a 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -18,7 +18,7 @@ export const opus_46_free_slackbot_model = { gateway: 'openrouter', internal_id: 'anthropic/claude-opus-4.6', inference_providers: [], - slackbot_only: true, + allowed_uses: ['slackbot'], } satisfies KiloFreeModel; export const SONNET_46_REVIEW_PROMO_MODEL_ID = 'anthropic/claude-sonnet-4.6:review'; @@ -34,7 +34,7 @@ export const sonnet_46_free_review_model = { gateway: 'openrouter' as const, internal_id: 'anthropic/claude-sonnet-4.6', inference_providers: ['anthropic'] as KiloFreeModel['inference_providers'], - review_only: true, + allowed_uses: ['review'], promotion_start: '2026-02-18T00:00:00Z', promotion_end: '2026-02-25T00:00:00Z', } satisfies KiloFreeModel; diff --git a/src/lib/providers/kilo-free-model.ts b/src/lib/providers/kilo-free-model.ts index 06fd4ac07..c7f079e36 100644 --- a/src/lib/providers/kilo-free-model.ts +++ b/src/lib/providers/kilo-free-model.ts @@ -3,6 +3,8 @@ import type { ProviderId } from '@/lib/providers/provider-id'; export type KiloFreeModelFlag = 'reasoning' | 'prompt_cache' | 'vision'; +export type KiloFreeModelUse = 'slackbot' | 'review'; + export type KiloFreeModel = { public_id: string; display_name: string; @@ -14,10 +16,7 @@ export type KiloFreeModel = { gateway: ProviderId; internal_id: string; 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; + allowed_uses?: KiloFreeModelUse[]; /** 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. */ diff --git a/src/lib/providers/openrouter/index.ts b/src/lib/providers/openrouter/index.ts index e015e610c..624536621 100644 --- a/src/lib/providers/openrouter/index.ts +++ b/src/lib/providers/openrouter/index.ts @@ -62,7 +62,7 @@ function enhancedModelList(models: OpenRouterModel[]) { ) .concat( kiloFreeModels - .filter(m => m.is_enabled && !m.slackbot_only && !m.review_only) + .filter(m => m.is_enabled && !m.allowed_uses?.length) .map(model => convertFromKiloModel(model)) ) .concat([autoModel]) From 7105f7226a2d38ba91e3b44af7aad63b5a510140 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 18 Feb 2026 12:00:08 +0200 Subject: [PATCH 6/7] fix: remove redundant as-casts and unused isFreeModelAllowedForUse --- src/lib/models.ts | 11 +---------- src/lib/providers/anthropic.ts | 6 +++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/lib/models.ts b/src/lib/models.ts index 4a5ef704d..0461fb681 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -11,7 +11,7 @@ import { } from '@/lib/providers/anthropic'; import { corethink_free_model } from '@/lib/providers/corethink'; import { giga_potato_model } from '@/lib/providers/gigapotato'; -import type { KiloFreeModel, KiloFreeModelUse } from '@/lib/providers/kilo-free-model'; +import type { KiloFreeModel } from '@/lib/providers/kilo-free-model'; import { minimax_m21_free_model, minimax_m25_free_model } from '@/lib/providers/minimax'; import { grok_code_fast_1_optimized_free_model } from '@/lib/providers/xai'; import { zai_glm47_free_model, zai_glm5_free_model } from '@/lib/providers/zai'; @@ -90,15 +90,6 @@ export function isDeadFreeModel(model: string): boolean { return !!kiloFreeModels.find(m => m.public_id === model && !m.is_enabled); } -export function isFreeModelAllowedForUse(model: string, use: KiloFreeModelUse): boolean { - return kiloFreeModels.some( - m => - m.public_id === model && - m.is_enabled && - (!m.allowed_uses?.length || m.allowed_uses.includes(use)) - ); -} - /** Returns true if the model has allowed_uses including 'review' and is within its promotion window. */ export function isReviewPromotionActive(model: string, now = new Date()): boolean { const freeModel = kiloFreeModels.find( diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index 30e3df18a..0401320d3 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -30,10 +30,10 @@ export const sonnet_46_free_review_model = { context_length: 1_000_000, max_completion_tokens: 16384, is_enabled: true, - flags: ['reasoning', 'prompt_cache', 'vision'] as KiloFreeModel['flags'], - gateway: 'openrouter' as const, + flags: ['reasoning', 'prompt_cache', 'vision'], + gateway: 'openrouter', internal_id: 'anthropic/claude-sonnet-4.6', - inference_providers: ['anthropic'] as KiloFreeModel['inference_providers'], + inference_providers: ['anthropic'], allowed_uses: ['review'], promotion_start: '2026-02-18T00:00:00Z', promotion_end: '2026-02-25T00:00:00Z', From 8166221b31bbb559b1af1f5e7d6cfa46053652cb Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 18 Feb 2026 12:16:13 +0200 Subject: [PATCH 7/7] feat: Update promotion start --- src/lib/providers/anthropic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index eb91510cc..6195d40d9 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -35,8 +35,8 @@ export const sonnet_46_free_review_model = { internal_id: 'anthropic/claude-sonnet-4.6', inference_providers: ['anthropic'], allowed_uses: ['review'], - promotion_start: '2026-02-18T00:00:00Z', - promotion_end: '2026-02-25T00:00:00Z', + promotion_start: '2026-02-18T11:00:00Z', // 6 AM East Coast + promotion_end: '2026-02-25T11:00:00Z', } satisfies KiloFreeModel; const ENABLE_ANTHROPIC_STRICT_TOOL_USE = false;