Skip to content
Merged
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
19 changes: 7 additions & 12 deletions src/app/api/openrouter/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@ import {
} from '@/lib/anonymous';
import { checkFreeModelRateLimit, logFreeModelRequest } from '@/lib/free-model-rate-limiter';
import { classifyAbuse } from '@/lib/abuse-service';
import { KILO_AUTO_MODEL_ID } from '@/lib/kilo-auto-model';

const MAX_TOKENS_LIMIT = 99999999999; // GPT4.1 default is ~32k

const AUTO_MODEL_ID = 'kilo/auto';

const OPUS = 'anthropic/claude-opus-4.5';
const SONNET = 'anthropic/claude-sonnet-4.5';

Expand Down Expand Up @@ -119,7 +118,7 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
return modelDoesNotExistResponse();
}

const requestedAutoModel = requestBodyParsed.model.trim().toLowerCase() === AUTO_MODEL_ID;
const requestedAutoModel = requestBodyParsed.model.trim().toLowerCase() === KILO_AUTO_MODEL_ID;

// "kilo/auto" is a quasi-model id that resolves to a real model based on x-kilocode-mode.
// After this resolution, the rest of the proxy flow behaves as if the client requested
Expand Down Expand Up @@ -283,15 +282,11 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
}

// Organization model allow list check.
// For `kilo/auto`, we intentionally skip model allow list enforcement so that teams can
// enable auto-mode routing without maintaining per-model allow lists.
if (!requestedAutoModel) {
const modelRestrictionError = checkOrganizationModelRestrictions({
modelId: originalModelIdLowerCased,
settings,
});
if (modelRestrictionError) return modelRestrictionError;
}
const modelRestrictionError = checkOrganizationModelRestrictions({
modelId: requestedAutoModel ? KILO_AUTO_MODEL_ID : originalModelIdLowerCased,
settings,
});
if (modelRestrictionError) return modelRestrictionError;

if (settings) {
// Set up provider object with both allow list and data collection
Expand Down
42 changes: 41 additions & 1 deletion src/app/api/openrouter/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import {
type OpenRouterModel,
} from '@/lib/providers/openrouter/openrouter-types';
import * as z from 'zod';
import {
KILO_AUTO_MODEL_CONTEXT_LENGTH,
KILO_AUTO_MODEL_DESCRIPTION,
KILO_AUTO_MODEL_ID,
KILO_AUTO_MODEL_NAME,
} from '@/lib/kilo-auto-model';

interface OpenRouterProvider {
name: string;
Expand Down Expand Up @@ -72,6 +78,38 @@ interface OpenRouterData {
generated_at: string;
}

function buildKiloAutoModel(): OpenRouterModel {
const epochIso = new Date(0).toISOString();
return {
slug: KILO_AUTO_MODEL_ID,
hf_slug: null,
updated_at: epochIso,
created_at: epochIso,
hf_updated_at: null,
name: KILO_AUTO_MODEL_NAME,
short_name: KILO_AUTO_MODEL_NAME,
author: 'Kilo',
description: KILO_AUTO_MODEL_DESCRIPTION,
model_version_group_id: null,
context_length: KILO_AUTO_MODEL_CONTEXT_LENGTH,
input_modalities: ['text'],
output_modalities: ['text'],
has_text_output: true,
group: 'other',
instruct_type: null,
default_system: null,
default_stops: [],
hidden: false,
router: null,
warning_message: null,
permaslug: KILO_AUTO_MODEL_ID,
reasoning_config: null,
features: null,
default_parameters: null,
endpoint: null,
};
}

export function useOpenRouterModels() {
return useQuery<OpenRouterModelsResponse>({
queryKey: ['openrouter-models'],
Expand Down Expand Up @@ -169,7 +207,9 @@ export function useOpenRouterModelsAndProviders() {
}
}

return [...modelBySlug.values()];
const modelsWithEndpoints = [...modelBySlug.values()];
const hasAutoAlready = modelsWithEndpoints.some(model => model.slug === KILO_AUTO_MODEL_ID);
return hasAutoAlready ? modelsWithEndpoints : [buildKiloAutoModel(), ...modelsWithEndpoints];
}, [query.data]);

return {
Expand Down
13 changes: 13 additions & 0 deletions src/lib/kilo-auto-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const KILO_AUTO_MODEL_ID = 'kilo/auto';

export const KILO_AUTO_MODEL_NAME = 'Kilo: Auto';

export const KILO_AUTO_MODEL_DESCRIPTION =
'Automatically routes your request to the best model for the task.';

export const KILO_AUTO_MODEL_CONTEXT_LENGTH = 200_000;
export const KILO_AUTO_MODEL_MAX_COMPLETION_TOKENS = 64_000;

// Keep non-zero so "limited access" UIs don't treat it as free.
export const KILO_AUTO_MODEL_PROMPT_PRICE = '0.0000010';
export const KILO_AUTO_MODEL_COMPLETION_PRICE = '0.0000010';
46 changes: 44 additions & 2 deletions src/lib/providers/openrouter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import { errorExceptInTest } from '@/lib/utils.server';
import { captureException, captureMessage } from '@sentry/nextjs';
import { convertFromKiloModel } from '@/lib/providers/kilo-free-model';
import { getModelSettings, getVersionedModelSettings } from '@/lib/providers/recommended-models';
import {
KILO_AUTO_MODEL_COMPLETION_PRICE,
KILO_AUTO_MODEL_CONTEXT_LENGTH,
KILO_AUTO_MODEL_DESCRIPTION,
KILO_AUTO_MODEL_ID,
KILO_AUTO_MODEL_MAX_COMPLETION_TOKENS,
KILO_AUTO_MODEL_NAME,
KILO_AUTO_MODEL_PROMPT_PRICE,
} from '@/lib/kilo-auto-model';

// Re-export from shared module for backwards compatibility
export { normalizeModelId } from '@/lib/model-utils';
Expand All @@ -17,22 +26,55 @@ export function isRateLimitedToDeathFree(model: string) {
return model.endsWith(':free') && !isFreeModel(model);
}

function buildAutoModel(): OpenRouterModel {
return {
id: KILO_AUTO_MODEL_ID,
name: KILO_AUTO_MODEL_NAME,
created: 0,
description: KILO_AUTO_MODEL_DESCRIPTION,
architecture: {
input_modalities: ['text'],
output_modalities: ['text'],
tokenizer: 'Other',
},
top_provider: {
is_moderated: false,
context_length: KILO_AUTO_MODEL_CONTEXT_LENGTH,
max_completion_tokens: KILO_AUTO_MODEL_MAX_COMPLETION_TOKENS,
},
pricing: {
prompt: KILO_AUTO_MODEL_PROMPT_PRICE,
completion: KILO_AUTO_MODEL_COMPLETION_PRICE,
request: '0',
image: '0',
web_search: '0',
internal_reasoning: '0',
},
context_length: KILO_AUTO_MODEL_CONTEXT_LENGTH,
supported_parameters: ['max_tokens', 'temperature', 'tools', 'reasoning', 'include_reasoning'],
};
}

function enhancedModelList(models: OpenRouterModel[]) {
const autoModel = buildAutoModel();
const enhancedModels = models
.filter(
(model: OpenRouterModel) =>
!isRateLimitedToDeathFree(model.id) &&
!kiloFreeModels.some(m => m.public_id === model.id && m.is_enabled)
)
.concat(kiloFreeModels.filter(m => m.is_enabled).map(model => convertFromKiloModel(model)))
.concat([autoModel])
Comment thread
iscekic marked this conversation as resolved.
.map((model: OpenRouterModel) => {
const preferredIndex = preferredModels.indexOf(model.id);
const preferredIndex =
model.id === KILO_AUTO_MODEL_ID ? -1 : preferredModels.indexOf(model.id);
const ageDays = (Date.now() / 1_000 - model.created) / (24 * 3600);
const isNew = preferredIndex >= 0 && ageDays >= 0 && ageDays < 7;
return {
...model,
name: isNew ? model.name + ' (new)' : model.name,
preferredIndex: preferredIndex >= 0 ? preferredIndex : undefined,
preferredIndex:
preferredIndex >= 0 || model.id === KILO_AUTO_MODEL_ID ? preferredIndex : undefined,
settings: getModelSettings(model.id),
versioned_settings: getVersionedModelSettings(model.id),
};
Expand Down
6 changes: 6 additions & 0 deletions src/lib/providers/recommended-models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ModelSettings, VersionedSettings } from '@/lib/organizations/organization-types';
import { KILO_AUTO_MODEL_ID } from '@/lib/kilo-auto-model';
import { giga_potato_model } from '@/lib/providers/gigapotato';
import { minimax_m21_free_model } from '@/lib/providers/minimax';
import { zai_glm47_free_model } from '@/lib/providers/zai';
Expand All @@ -10,6 +11,11 @@ export type RecommendedModel = {
};

export const recommendedModels = [
{
public_id: KILO_AUTO_MODEL_ID,
tool_choice_required: false,
random_vercel_routing: true,
},
{
public_id: minimax_m21_free_model.is_enabled
? minimax_m21_free_model.public_id
Expand Down
45 changes: 41 additions & 4 deletions src/tests/openrouter-models-sorting.approved.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
{
"data": [
{
"id": "kilo/auto",
"name": "Kilo: Auto",
"created": 0,
"description": "Automatically routes your request to the best model for the task.",
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"text"
],
"tokenizer": "Other"
},
"top_provider": {
"is_moderated": false,
"context_length": 200000,
"max_completion_tokens": 64000
},
"pricing": {
"prompt": "0.0000010",
"completion": "0.0000010",
"request": "0",
"image": "0",
"web_search": "0",
"internal_reasoning": "0"
},
"context_length": 200000,
"supported_parameters": [
"max_tokens",
"temperature",
"tools",
"reasoning",
"include_reasoning"
],
"preferredIndex": -1
},
{
"id": "minimax/minimax-m2.1:free",
"canonical_slug": "minimax/minimax-m2.1:free",
Expand Down Expand Up @@ -41,7 +78,7 @@
"include_reasoning"
],
"default_parameters": {},
"preferredIndex": 0,
"preferredIndex": 1,
"settings": {
"included_tools": [
"search_and_replace"
Expand Down Expand Up @@ -93,7 +130,7 @@
"include_reasoning"
],
"default_parameters": {},
"preferredIndex": 1,
"preferredIndex": 2,
"versioned_settings": {
"4.146.0": {
"included_tools": [
Expand Down Expand Up @@ -149,7 +186,7 @@
"include_reasoning"
],
"default_parameters": {},
"preferredIndex": 3,
"preferredIndex": 4,
"versioned_settings": {
"4.146.0": {
"included_tools": [
Expand Down Expand Up @@ -201,7 +238,7 @@
"tools"
],
"default_parameters": {},
"preferredIndex": 4
"preferredIndex": 5
},
{
"id": "anthropic/claude-sonnet-4",
Expand Down