Skip to content

Commit 14e175f

Browse files
authored
Merge branch 'main' into feat/kilo-auto-model-cache-prices
2 parents e23fd2b + a9a6d2a commit 14e175f

File tree

6 files changed

+82
-6
lines changed

6 files changed

+82
-6
lines changed

src/app/api/internal/code-review-status/[reviewId]/route.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,5 +599,27 @@ describe('POST /api/internal/code-review-status/[reviewId]', () => {
599599
expect.stringContaining('https://app.kilo.ai/')
600600
);
601601
});
602+
603+
it('suggests switching to a free model in the billing notice', async () => {
604+
mockGetCodeReviewById.mockResolvedValue(makeReview());
605+
mockHasPRCommentWithMarker.mockResolvedValue(false);
606+
607+
await POST(
608+
makeRequest({
609+
status: 'failed',
610+
errorMessage: 'Insufficient credits',
611+
terminalReason: 'billing',
612+
}),
613+
makeParams(REVIEW_ID)
614+
);
615+
616+
expect(mockCreatePRComment).toHaveBeenCalledWith(
617+
'inst-1',
618+
'owner',
619+
'repo',
620+
1,
621+
expect.stringContaining('switch to a free model')
622+
);
623+
});
602624
});
603625
});

src/app/api/internal/code-review-status/[reviewId]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ const BILLING_NOTICE_MARKER = '<!-- kilo-billing-notice -->';
166166
const BILLING_NOTICE_BODY = `${BILLING_NOTICE_MARKER}
167167
**Kilo Code Review could not run — your account is out of credits.**
168168
169-
Add credits at [app.kilo.ai](https://app.kilo.ai/) to enable reviews on this change.`;
169+
[Add credits](https://app.kilo.ai/) or [switch to a free model](https://app.kilo.ai/code-reviews) to enable reviews on this change.`;
170170

171171
/**
172172
* Read a review's usage data.

src/lib/kilo-auto-model.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
OpenRouterChatCompletionRequest,
1515
OpenRouterReasoningConfig,
1616
} from '@/lib/providers/openrouter/types';
17+
import { requestContainsImages } from '@/lib/providers/openrouter/request-helpers';
1718
import type { ModelSettings, OpenCodeSettings, Verbosity } from '@kilocode/db/schema-types';
1819
import type OpenAI from 'openai';
1920

@@ -106,6 +107,11 @@ const BALANCED_CODE_MODEL: ResolvedAutoModel = {
106107
model: MINIMAX_CURRENT_MODEL_ID,
107108
};
108109

110+
const BALANCED_IMAGE_MODEL: ResolvedAutoModel = {
111+
model: KIMI_CURRENT_MODEL_ID,
112+
reasoning: { enabled: true },
113+
};
114+
109115
const BALANCED_MODE_TO_MODEL: Record<string, ResolvedAutoModel> = {
110116
plan: { model: KIMI_CURRENT_MODEL_ID, reasoning: { enabled: true } },
111117
general: { model: KIMI_CURRENT_MODEL_ID, reasoning: { enabled: true } },
@@ -164,7 +170,7 @@ export const KILO_AUTO_BALANCED_MODEL: AutoModel = {
164170
completion_price: '0.000003',
165171
input_cache_read_price: '0.000000225',
166172
input_cache_write_price: undefined,
167-
supports_images: false,
173+
supports_images: true,
168174
roocode_settings: {
169175
included_tools: ['edit_file'],
170176
excluded_tools: ['apply_diff'],
@@ -212,7 +218,8 @@ const legacyMapping: Record<string, AutoModel | undefined> = {
212218
export async function resolveAutoModel(
213219
model: string,
214220
modeHeader: string | null,
215-
balancePromise: Promise<number>
221+
balancePromise: Promise<number>,
222+
hasImages: boolean
216223
): Promise<ResolvedAutoModel> {
217224
const mappedModel =
218225
(Object.hasOwn(legacyMapping, model) ? legacyMapping[model] : null)?.id ?? model;
@@ -226,6 +233,9 @@ export async function resolveAutoModel(
226233
}
227234
const mode = modeHeader?.trim().toLowerCase() ?? '';
228235
if (mappedModel === KILO_AUTO_BALANCED_MODEL.id) {
236+
if (hasImages) {
237+
return BALANCED_IMAGE_MODEL;
238+
}
229239
return (
230240
(Object.hasOwn(BALANCED_MODE_TO_MODEL, mode) ? BALANCED_MODE_TO_MODEL[mode] : null) ??
231241
BALANCED_CODE_MODEL
@@ -244,10 +254,12 @@ export async function applyResolvedAutoModel(
244254
featureHeader: FeatureValue | null,
245255
balancePromise: Promise<number>
246256
) {
257+
const hasImages = requestContainsImages(request);
247258
const resolved = await resolveAutoModel(
248259
model,
249260
featureHeader === 'kiloclaw' ? 'plan' : modeHeader,
250-
balancePromise
261+
balancePromise,
262+
hasImages
251263
);
252264
request.body.model = resolved.model;
253265
if (resolved.reasoning) {

src/lib/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export async function getMonitoredModels() {
4848
const set = new Set<string>();
4949
for (const model of preferredModels) {
5050
if (isKiloAutoModel(model)) {
51-
set.add((await resolveAutoModel(model, null, Promise.resolve(0))).model);
51+
set.add((await resolveAutoModel(model, null, Promise.resolve(0), false)).model);
5252
} else {
5353
set.add(model);
5454
}

src/lib/providers/openrouter/request-helpers.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,44 @@ export function scrubOpenCodeSpecificProperties(request: OpenRouterChatCompletio
152152
delete body.usage;
153153
delete body.reasoningEffort;
154154
}
155+
156+
export function requestContainsImages(request: GatewayRequest): boolean {
157+
switch (request.kind) {
158+
case 'chat_completions':
159+
return request.body.messages.some(
160+
msg =>
161+
(msg.role === 'user' || msg.role === 'tool') &&
162+
Array.isArray(msg.content) &&
163+
msg.content.some(part => part.type === 'image_url')
164+
);
165+
case 'responses': {
166+
if (!Array.isArray(request.body.input)) return false;
167+
return request.body.input.some(item => {
168+
if (typeof item === 'string') return false;
169+
if (item.type === 'message') {
170+
return (
171+
Array.isArray(item.content) && item.content.some(part => part.type === 'input_image')
172+
);
173+
}
174+
if (item.type === 'function_call_output') {
175+
return (
176+
Array.isArray(item.output) && item.output.some(part => part.type === 'input_image')
177+
);
178+
}
179+
return false;
180+
});
181+
}
182+
case 'messages':
183+
return request.body.messages.some(
184+
msg =>
185+
Array.isArray(msg.content) &&
186+
msg.content.some(
187+
block =>
188+
block.type === 'image' ||
189+
(block.type === 'tool_result' &&
190+
Array.isArray(block.content) &&
191+
block.content.some(inner => inner.type === 'image'))
192+
)
193+
);
194+
}
195+
}

src/tests/openrouter-models-sorting.approved.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"description": "Great balance of price and capability. Uses Kimi K2.5 for plan, general, architect, orchestrator, ask, debug; MiniMax M2.7 for build, explore, code.",
5353
"architecture": {
5454
"input_modalities": [
55-
"text"
55+
"text",
56+
"image"
5657
],
5758
"output_modalities": [
5859
"text"

0 commit comments

Comments
 (0)