Skip to content

Commit 0b19d0c

Browse files
authored
feat(auto-model): route balanced tier to Kimi when images detected (#1604)
## Summary - Kilo Auto Balanced now supports image inputs by detecting images in user messages across all three gateway request formats (`chat_completions`, `responses`, `messages`) - When the balanced tier would normally route to MiniMax (for `build`, `explore`, `code` modes or as fallback) and the request contains images, it overrides to Kimi K2.5 instead, since MiniMax does not support image inputs - Sets `supports_images: true` on the balanced auto model so clients know images are accepted ## Verification - [x] `pnpm typecheck` passes (exit code 0) - [x] Pre-push hooks (lint, format, typecheck) pass - [x] Snapshot test `openrouter-models-sorting.approved.json` updated to include `image` in balanced model's `input_modalities` ## Visual Changes N/A ## Reviewer Notes - The image detection function `requestContainsImages` handles all three gateway request kinds with their respective image discriminants: `image_url` (chat_completions), `input_image` (responses), `image` (Anthropic messages) - The override only triggers when the resolved model is MiniMax — modes that already route to Kimi (plan, general, architect, etc.) are unaffected - `BALANCED_IMAGE_MODEL` uses reasoning enabled, consistent with all other Kimi routes in the balanced tier
2 parents b6ca683 + 39e52df commit 0b19d0c

File tree

4 files changed

+59
-5
lines changed

4 files changed

+59
-5
lines changed

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

@@ -104,6 +105,11 @@ const BALANCED_CODE_MODEL: ResolvedAutoModel = {
104105
model: MINIMAX_CURRENT_MODEL_ID,
105106
};
106107

108+
const BALANCED_IMAGE_MODEL: ResolvedAutoModel = {
109+
model: KIMI_CURRENT_MODEL_ID,
110+
reasoning: { enabled: true },
111+
};
112+
107113
const BALANCED_MODE_TO_MODEL: Record<string, ResolvedAutoModel> = {
108114
plan: { model: KIMI_CURRENT_MODEL_ID, reasoning: { enabled: true } },
109115
general: { model: KIMI_CURRENT_MODEL_ID, reasoning: { enabled: true } },
@@ -156,7 +162,7 @@ export const KILO_AUTO_BALANCED_MODEL: AutoModel = {
156162
max_completion_tokens: 131072,
157163
prompt_price: '0.0000006',
158164
completion_price: '0.000003',
159-
supports_images: false,
165+
supports_images: true,
160166
roocode_settings: {
161167
included_tools: ['edit_file'],
162168
excluded_tools: ['apply_diff'],
@@ -202,7 +208,8 @@ const legacyMapping: Record<string, AutoModel | undefined> = {
202208
export async function resolveAutoModel(
203209
model: string,
204210
modeHeader: string | null,
205-
balancePromise: Promise<number>
211+
balancePromise: Promise<number>,
212+
hasImages: boolean
206213
): Promise<ResolvedAutoModel> {
207214
const mappedModel =
208215
(Object.hasOwn(legacyMapping, model) ? legacyMapping[model] : null)?.id ?? model;
@@ -216,6 +223,9 @@ export async function resolveAutoModel(
216223
}
217224
const mode = modeHeader?.trim().toLowerCase() ?? '';
218225
if (mappedModel === KILO_AUTO_BALANCED_MODEL.id) {
226+
if (hasImages) {
227+
return BALANCED_IMAGE_MODEL;
228+
}
219229
return (
220230
(Object.hasOwn(BALANCED_MODE_TO_MODEL, mode) ? BALANCED_MODE_TO_MODEL[mode] : null) ??
221231
BALANCED_CODE_MODEL
@@ -234,10 +244,12 @@ export async function applyResolvedAutoModel(
234244
featureHeader: FeatureValue | null,
235245
balancePromise: Promise<number>
236246
) {
247+
const hasImages = requestContainsImages(request);
237248
const resolved = await resolveAutoModel(
238249
model,
239250
featureHeader === 'kiloclaw' ? 'plan' : modeHeader,
240-
balancePromise
251+
balancePromise,
252+
hasImages
241253
);
242254
request.body.model = resolved.model;
243255
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
@@ -50,7 +50,8 @@
5050
"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.",
5151
"architecture": {
5252
"input_modalities": [
53-
"text"
53+
"text",
54+
"image"
5455
],
5556
"output_modalities": [
5657
"text"

0 commit comments

Comments
 (0)