Skip to content
Closed
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: 8 additions & 2 deletions apps/web/src/lib/ai-gateway/auto-model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,23 @@ export const KILO_AUTO_BALANCED_MODEL: AutoModel = {
opencode_settings: undefined,
};

// INVARIANT: kilo-auto/small metadata must stay the lowest common denominator
// of every model it can resolve to (google/gemma-4-26b-a4b-it with balance,
// otherwise the kilo-auto/free rotation). ling-3.0-flash and laguna-s-2.1 are
// text-only, so supports_images must stay false; max_completion_tokens is
// bounded by gemma's 16384. Re-check all resolution targets before raising
// any of these values.
export const KILO_AUTO_SMALL_MODEL: AutoModel = {
id: 'kilo-auto/small',
name: 'Auto Small',
description: 'Automatically routes your request to a small model.',
context_length: 262144,
max_completion_tokens: 32768,
max_completion_tokens: 16384,
prompt_price: '0.00000005',
completion_price: '0.0000004',
input_cache_read_price: '0.000000005',
input_cache_write_price: undefined,
supports_images: true,
supports_images: false,
supports_pdf: false,
opencode_settings: undefined,
};
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/lib/ai-gateway/auto-model/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import {
BALANCED_QWEN_MODEL,
FRONTIER_MODE_TO_MODEL,
KILO_AUTO_EFFICIENT_MODEL,
KILO_AUTO_FREE_MODEL,
KILO_AUTO_SMALL_MODEL,
ORG_AUTO_MODEL,
} from '@/lib/ai-gateway/auto-model';
import { GEMMA_4_26B_A4B_IT_ID } from '@/lib/ai-gateway/providers/google';
import { stepfun_37_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
import type { AutoRoutingDecision } from '@kilocode/auto-routing-contracts';

const baseParams = {
Expand Down Expand Up @@ -386,6 +390,44 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
});
});

describe('resolveAutoModel — kilo-auto/small branch', () => {
const smallParams = {
...baseParams,
model: KILO_AUTO_SMALL_MODEL.id,
apiKind: 'chat_completions' as const,
};

it('resolves to the paid Gemma model when the user has balance', async () => {
const result = await resolveAutoModel(smallParams, nullUserPromise, Promise.resolve(100));

expect(result).toEqual({ kind: 'ok', resolved: { model: GEMMA_4_26B_A4B_IT_ID } });
});

it('falls back to the kilo-auto/free rotation when the user has no balance', async () => {
const params = { ...smallParams, sessionId: 'session-1' };
const smallResult = await resolveAutoModel(params, nullUserPromise, zeroBalancePromise);
const freeResult = await resolveAutoModel(
{ ...params, model: KILO_AUTO_FREE_MODEL.id },
nullUserPromise,
zeroBalancePromise
);

expect(smallResult).toEqual(freeResult);
expect(smallResult.kind).toBe('ok');
});

it('resolves to a free candidate when the user has no balance', async () => {
// The Redis mock returns no OpenRouter models, so the only candidate is
// the public Kilo-exclusive free model.
const result = await resolveAutoModel(smallParams, nullUserPromise, zeroBalancePromise);

expect(result).toEqual({
kind: 'ok',
resolved: { model: stepfun_37_flash_free_model.public_id },
});
});
});

describe('resolveAutoModel — Organization Auto branch', () => {
it('uses exact built-in alias routes before canonical fallback routes', async () => {
const result = await resolveAutoModel(
Expand Down
45 changes: 23 additions & 22 deletions apps/web/src/lib/ai-gateway/auto-model/resolution.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type { FeatureValue } from '@/lib/feature-detection';
import {
gemma_4_26b_a4b_it_free_model,
GEMMA_4_26B_A4B_IT_ID,
} from '@/lib/ai-gateway/providers/google';
import { GEMMA_4_26B_A4B_IT_ID } from '@/lib/ai-gateway/providers/google';
import type {
GatewayRequest,
OpenRouterChatCompletionRequest,
Expand Down Expand Up @@ -142,6 +139,23 @@ export type ResolveAutoModelResult =
| { kind: 'no_free_models_available' }
| { kind: 'organization_auto_configuration_error'; message: string };

async function resolveFreeRotation(
apiKind: GatewayRequest['kind'] | null,
sessionId: string | null,
clientIp: string | null,
userPromise: Promise<User | null>
): Promise<ResolveAutoModelResult> {
const candidates = await getAutoFreeCandidates(apiKind);
if (candidates.length === 0) {
return { kind: 'no_free_models_available' };
}
const randomNumber = getRandomNumber(
'free_routing_' + (sessionId ?? (await userPromise)?.id ?? clientIp),
candidates.length
);
return { kind: 'ok', resolved: { model: candidates[randomNumber] } };
}

async function resolveOrganizationAutoModel(
params: ResolveAutoModelParams,
userPromise: Promise<User | null>,
Expand Down Expand Up @@ -282,26 +296,13 @@ export async function resolveAutoModel(
return await resolveOrganizationAutoModel(params, userPromise, balancePromise);
}
if (model === KILO_AUTO_FREE_MODEL.id) {
const candidates = await getAutoFreeCandidates(apiKind);
if (candidates.length === 0) {
return { kind: 'no_free_models_available' };
}
const randomNumber = getRandomNumber(
'free_routing_' + (sessionId ?? (await userPromise)?.id ?? clientIp),
candidates.length
);
return { kind: 'ok', resolved: { model: candidates[randomNumber] } };
return resolveFreeRotation(apiKind, sessionId, clientIp, userPromise);
}
if (model === KILO_AUTO_SMALL_MODEL.id) {
return {
kind: 'ok',
resolved: {
model:
(await balancePromise) > 0
? GEMMA_4_26B_A4B_IT_ID
: gemma_4_26b_a4b_it_free_model.public_id,
},
};
if ((await balancePromise) > 0) {
return { kind: 'ok', resolved: { model: GEMMA_4_26B_A4B_IT_ID } };
}
return resolveFreeRotation(apiKind, sessionId, clientIp, userPromise);
Comment thread
chrarnoldus marked this conversation as resolved.
}
if (model === KILO_AUTO_EFFICIENT_MODEL.id) {
const decision = params.efficientDecision ? await params.efficientDecision() : null;
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/lib/ai-gateway/context-overflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
GatewayRequest,
OpenRouterChatCompletionRequest,
} from '@/lib/ai-gateway/providers/openrouter/types';
import { gemma_4_26b_a4b_it_free_model } from '@/lib/ai-gateway/providers/google';
import { stepfun_37_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
import { ProxyErrorType } from '@/lib/proxy-error-types';

function chatRequest(body: OpenRouterChatCompletionRequest): GatewayRequest {
Expand Down Expand Up @@ -158,16 +158,16 @@ describe('detectContextOverflow', () => {
});

it('triggers on a generic 500 when our estimate exceeds the window', async () => {
// Gemma 4 has context_length 262_144 and max_completion_tokens 32_768.
// Step 3.7 Flash has context_length 262_144.
// This request estimates to more than 282_000 tokens, exceeding the context window.
const hugeRequest = chatRequest({
model: gemma_4_26b_a4b_it_free_model.public_id,
model: stepfun_37_flash_free_model.public_id,
messages: [{ role: 'user', content: 'x'.repeat(1_000_000) }],
max_tokens: 32_768,
});

const result = await detectContextOverflow({
requestedModel: gemma_4_26b_a4b_it_free_model.public_id,
requestedModel: stepfun_37_flash_free_model.public_id,
request: hugeRequest,
response: new Response('Internal Server Error', { status: 500 }),
});
Expand All @@ -180,12 +180,12 @@ describe('detectContextOverflow', () => {

it('does not trigger on a 500 when the estimate fits the window', async () => {
const smallRequest = chatRequest({
model: gemma_4_26b_a4b_it_free_model.public_id,
model: stepfun_37_flash_free_model.public_id,
messages: [{ role: 'user', content: 'hi' }],
});

const result = await detectContextOverflow({
requestedModel: gemma_4_26b_a4b_it_free_model.public_id,
requestedModel: stepfun_37_flash_free_model.public_id,
request: smallRequest,
response: new Response('Internal Server Error', { status: 500 }),
});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/forbidden-free-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const forbiddenFreeModelIds: ReadonlySet<string> = new Set([
'google/gemma-3-4b-it:free',
'google/gemma-3n-e2b-it:free',
'google/gemma-3n-e4b-it:free',
'google/gemma-4-26b-a4b-it:free', // usable through kilo-auto
'google/gemma-4-26b-a4b-it:free',
'google/gemma-4-31b-it:free',
'kilo/auto-free', // discontinued variant of kilo-auto/free
'kwaipilot/kat-coder-pro-v2.5:free',
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/lib/ai-gateway/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusi
import { isMuseModel } from '@/lib/ai-gateway/providers/meta';
import { MINIMAX_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/minimax';
import { KIMI_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/moonshotai';
import { gemma_4_26b_a4b_it_free_model, isGeminiModel } from '@/lib/ai-gateway/providers/google';
import { isGeminiModel } from '@/lib/ai-gateway/providers/google';
import { QWEN37_PLUS_MODEL_ID, qwen36_plus_stealth_model } from '@/lib/ai-gateway/providers/qwen';
import { stepfun_37_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
import { isGrokModel } from '@/lib/ai-gateway/providers/xai';
Expand Down Expand Up @@ -85,7 +85,6 @@ export function isKiloExclusiveModel(model: string): boolean {
}

export const kiloExclusiveModels = [
gemma_4_26b_a4b_it_free_model,
...deepseekDiscountedModels,
qwen36_plus_stealth_model,
gpt_5_6_sol_stealth_model,
Expand Down
17 changes: 0 additions & 17 deletions apps/web/src/lib/ai-gateway/providers/google.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model';

export function isGemmaModel(model: string) {
return model.includes('gemma');
}

export const GEMMA_4_26B_A4B_IT_ID = 'google/gemma-4-26b-a4b-it';

export const gemma_4_26b_a4b_it_free_model: KiloExclusiveModel = {
public_id: 'google/gemma-4-26b-a4b-it:free',
display_name: 'Google: Gemma 4 26B A4B (free)',
description:
'Gemma 4 26B A4B IT is an instruction-tuned Mixture-of-Experts (MoE) model from Google DeepMind. Despite 25.2B total parameters, only 3.8B activate per token during inference — delivering near-31B quality at a fraction of the compute cost.',
context_length: 262144,
max_completion_tokens: 32768,
status: 'hidden', // usable through kilo-auto
flags: ['vision', 'vercel-routing'],
gateway: 'openrouter',
internal_id: GEMMA_4_26B_A4B_IT_ID,
pricing: null,
inference_provider_restriction: [],
};

export function isGeminiModel(model: string) {
return model.includes('gemini');
}
Expand Down
14 changes: 11 additions & 3 deletions apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import { createMockResponse, mockOpenRouterModels } from '@/tests/helpers/openrouter-models.helper';
import type { OpenRouterModel } from '@/lib/organizations/organization-types';
import { qwen36_plus_stealth_model } from '@/lib/ai-gateway/providers/qwen';
import { gemma_4_26b_a4b_it_free_model } from '@/lib/ai-gateway/providers/google';
import {
findKiloExclusiveModel,
isDeadFreeModel,
Expand Down Expand Up @@ -42,6 +41,15 @@ const disabledFreeModel = {
pricing: null,
} satisfies KiloExclusiveModel;

const hiddenFreeModel = {
...qwen36_plus_stealth_model,
public_id: 'vendor/hidden-free-model',
internal_id: 'vendor/hidden-free-model-internal',
display_name: 'Hidden Free Kilo Model',
status: 'hidden',
pricing: null,
} satisfies KiloExclusiveModel;

function buildModel(overrides: Partial<OpenRouterModel> = {}): OpenRouterModel {
return {
id: 'vendor/model',
Expand Down Expand Up @@ -192,8 +200,8 @@ describe('shouldSuppressOpenRouterModel', () => {
});

it('suppresses hidden Kilo-exclusive models from OpenRouter', () => {
expect(gemma_4_26b_a4b_it_free_model.status).toBe('hidden');
expect(shouldSuppressOpenRouterModel(gemma_4_26b_a4b_it_free_model)).toBe(true);
expect(hiddenFreeModel.status).toBe('hidden');
expect(shouldSuppressOpenRouterModel(hiddenFreeModel)).toBe(true);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,9 @@ describe('mapModelIdToVercel', () => {

describe('kilo-exclusive models', () => {
it('maps an exclusive flagged with vercel-routing to its internal id', () => {
// google/gemma-4-26b-a4b-it:free is registered in kiloExclusiveModels
// with the 'vercel-routing' flag and internal_id 'google/gemma-4-26b-a4b-it'.
expect(mapModelIdToVercel('google/gemma-4-26b-a4b-it:free')).toBe(
'google/gemma-4-26b-a4b-it'
);
// stepfun/step-3.7-flash:free is registered in kiloExclusiveModels
// with the 'vercel-routing' flag and internal_id 'stepfun/step-3.7-flash'.
expect(mapModelIdToVercel('stepfun/step-3.7-flash:free')).toBe('stepfun/step-3.7-flash');
});

it('does not use internal_id for exclusives that are not vercel-routed', () => {
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/lib/rewriteModelResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from './rewriteModelResponse';
import { isDynamicallyOptedIntoRequestLogging } from '@/lib/ai-gateway/request-logging-opt-ins';
import { QWEN37_PLUS_MODEL_ID } from '@/lib/ai-gateway/custom-pricing';
import { stepfun_37_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
import { KILO_ORGANIZATION_ID } from '@/lib/organizations/constants';
import { logExceptInTest } from '@/lib/utils.server';

Expand Down Expand Up @@ -815,18 +816,18 @@ describe('rewriteModelResponse', () => {
test('continues stripping cost for free models outside the Kilo organization', async () => {
const result = await rewriteModelResponse(
jsonResponse({
model: 'google/gemma-4-26b-a4b-it:free',
model: stepfun_37_flash_free_model.public_id,
usage: { cost: 0, is_byok: false },
}),
'google/gemma-4-26b-a4b-it:free',
stepfun_37_flash_free_model.public_id,
'openrouter',
'chat_completions',
makeLogging()
);

expect(result).not.toBeNull();
expect(await result?.json()).toEqual({
model: 'google/gemma-4-26b-a4b-it:free',
model: stepfun_37_flash_free_model.public_id,
usage: {},
});
});
Expand Down