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
12 changes: 0 additions & 12 deletions apps/web/src/lib/ai-gateway/auto-model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
type OpenCodeSettings,
type Verbosity,
} from '@kilocode/db/schema-types';
import { KIMI_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/moonshotai';
import {
gemma_4_26b_a4b_it_free_model,
GEMMA_4_26B_A4B_IT_ID,
Expand Down Expand Up @@ -85,17 +84,6 @@ export const FRONTIER_MODE_TO_MODEL: Record<Mode, ResolvedAutoModel> = {
code: SONNET_FRONTIER,
};

// INVARIANT: the efficient static fallback must remain image-capable.
// The capability-aware routing filter relies on this guarantee to make
// image requests succeed even when no benchmark candidate is capable.
// Whoever changes this model constant must re-verify image support
// (via live OpenRouter data or the `model_stats` table) before
// swapping it — do not assume parity with the prior value.
export const BALANCED_FALLBACK_MODEL: ResolvedAutoModel = {
model: KIMI_CURRENT_MODEL_ID,
reasoning: { enabled: true },
};

const UNKNOWN_PRICING: AutoModelPricing = {
prompt: '-1',
completion: '-1',
Expand Down
38 changes: 18 additions & 20 deletions apps/web/src/lib/ai-gateway/auto-model/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ jest.mock('@/lib/ai-gateway/providers/gateway-models-cache', () => ({

import { resolveAutoModel } from './resolution';
import {
BALANCED_FALLBACK_MODEL,
FRONTIER_MODE_TO_MODEL,
KILO_AUTO_BALANCED_MODEL,
KILO_AUTO_EFFICIENT_MODEL,
KILO_AUTO_FREE_MODEL,
ORG_AUTO_MODEL,
} from '@/lib/ai-gateway/auto-model';
import type { AutoRoutingDecision } from '@kilocode/auto-routing-contracts';
import { PRIMARY_DEFAULT_MODEL } from '@/lib/ai-gateway/models';

const baseParams = {
model: KILO_AUTO_EFFICIENT_MODEL.id,
Expand All @@ -26,6 +26,7 @@ const baseParams = {

const nullUserPromise = Promise.resolve(null);
const zeroBalancePromise = Promise.resolve(0);
const primaryDefaultFallback = { model: PRIMARY_DEFAULT_MODEL };

const sampleDecision: AutoRoutingDecision = {
model: 'anthropic/claude-haiku-4',
Expand Down Expand Up @@ -100,38 +101,38 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
expect(result).toEqual({ kind: 'ok', resolved: { model: 'anthropic/claude-haiku-4' } });
});

it('falls back to BALANCED_FALLBACK_MODEL when no thunk is provided and apiKind=responses', async () => {
it('falls back to PRIMARY_DEFAULT_MODEL when no thunk is provided and apiKind=responses', async () => {
const result = await resolveAutoModel(
{ ...baseParams, apiKind: 'responses' },
nullUserPromise,
zeroBalancePromise
);

expect(result).toEqual({ kind: 'ok', resolved: BALANCED_FALLBACK_MODEL });
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
});

it('falls back to BALANCED_FALLBACK_MODEL when no thunk is provided and apiKind=messages', async () => {
it('falls back to PRIMARY_DEFAULT_MODEL when no thunk is provided and apiKind=messages', async () => {
const result = await resolveAutoModel(
{ ...baseParams, apiKind: 'messages' },
nullUserPromise,
zeroBalancePromise
);

expect(result).toEqual({ kind: 'ok', resolved: BALANCED_FALLBACK_MODEL });
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
});

it('falls back to BALANCED_FALLBACK_MODEL when no thunk is provided and apiKind=chat_completions', async () => {
it('falls back to PRIMARY_DEFAULT_MODEL when no thunk is provided and apiKind=chat_completions', async () => {
const result = await resolveAutoModel(
{ ...baseParams, apiKind: 'chat_completions' },
nullUserPromise,
zeroBalancePromise
);

expect(result).toEqual({ kind: 'ok', resolved: BALANCED_FALLBACK_MODEL });
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
});

it.each([KILO_AUTO_BALANCED_MODEL.id, KILO_AUTO_EFFICIENT_MODEL.id])(
'falls back to Kimi K3 with reasoning for %s when the worker returns no decision',
'falls back to PRIMARY_DEFAULT_MODEL for %s when the worker returns no decision',
async model => {
const result = await resolveAutoModel(
{
Expand All @@ -144,14 +145,11 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
zeroBalancePromise
);

expect(result).toEqual({
kind: 'ok',
resolved: { model: 'moonshotai/kimi-k3', reasoning: { enabled: true } },
});
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
}
);

it('falls back to BALANCED_FALLBACK_MODEL when the worker returns a virtual auto model', async () => {
it('falls back to PRIMARY_DEFAULT_MODEL when the worker returns a virtual auto model', async () => {
const result = await resolveAutoModel(
{
...baseParams,
Expand All @@ -165,7 +163,7 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
zeroBalancePromise
);

expect(result).toEqual({ kind: 'ok', resolved: BALANCED_FALLBACK_MODEL });
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
});

it('does not call the thunk more than once', async () => {
Expand Down Expand Up @@ -259,7 +257,7 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
});
});

it('falls back to BALANCED_FALLBACK_MODEL when variant is absent from the model catalog', async () => {
it('falls back to PRIMARY_DEFAULT_MODEL when variant is absent from the model catalog', async () => {
// Claude has no "thinking" key — only none/low/medium/high/xhigh/max
const result = await resolveAutoModel(
{
Expand All @@ -275,10 +273,10 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
zeroBalancePromise
);

expect(result).toEqual({ kind: 'ok', resolved: BALANCED_FALLBACK_MODEL });
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
});

it('falls back to BALANCED_FALLBACK_MODEL when the model exposes no variants but decision has a variant', async () => {
it('falls back to PRIMARY_DEFAULT_MODEL when the model exposes no variants but decision has a variant', async () => {
const result = await resolveAutoModel(
{
...baseParams,
Expand All @@ -293,7 +291,7 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
zeroBalancePromise
);

expect(result).toEqual({ kind: 'ok', resolved: BALANCED_FALLBACK_MODEL });
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
});

it('applies exact thinking and instant variant settings', async () => {
Expand Down Expand Up @@ -403,7 +401,7 @@ describe('resolveAutoModel — kilo-auto/efficient branch', () => {
zeroBalancePromise
);

expect(result).toEqual({ kind: 'ok', resolved: BALANCED_FALLBACK_MODEL });
expect(result).toEqual({ kind: 'ok', resolved: primaryDefaultFallback });
});
});

Expand Down Expand Up @@ -651,7 +649,7 @@ describe('resolveAutoModel — Organization Auto branch', () => {

expect(result).toEqual({
kind: 'ok',
resolved: BALANCED_FALLBACK_MODEL,
resolved: primaryDefaultFallback,
routingTarget: 'kilo-auto/balanced',
});
});
Expand Down
9 changes: 5 additions & 4 deletions apps/web/src/lib/ai-gateway/auto-model/resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
KILO_AUTO_BALANCED_MODEL,
KILO_AUTO_EFFICIENT_MODEL,
modeSchema,
BALANCED_FALLBACK_MODEL,
FRONTIER_MODE_TO_MODEL,
FRONTIER_CODE_MODEL,
type ResolvedAutoModel,
Expand All @@ -27,6 +26,7 @@ import {
autoFreeModels,
findKiloExclusiveModel,
isKiloExclusiveFreeModel,
PRIMARY_DEFAULT_MODEL,
selectAutoFreeCandidate,
} from '@/lib/ai-gateway/models';
import { getOpenRouterModelsFromDatabase } from '@/lib/ai-gateway/providers/gateway-models-cache';
Expand Down Expand Up @@ -317,18 +317,19 @@ export async function resolveAutoModel(
};
}
if (model === KILO_AUTO_EFFICIENT_MODEL.id || model === KILO_AUTO_BALANCED_MODEL.id) {
const fallbackModel = { model: PRIMARY_DEFAULT_MODEL };
const decision = params.efficientDecision ? await params.efficientDecision() : null;
if (decision && !isVirtualAutoModelId(decision.model)) {
const resolvedFromDecision = await resolveEfficientDecisionModel(decision);
if (resolvedFromDecision) {
return { kind: 'ok', resolved: resolvedFromDecision };
}
// Exact catalog variant missing or removed: never serve the chosen model
// with implicit defaults — same balanced fallback as the no-decision path.
return { kind: 'ok', resolved: BALANCED_FALLBACK_MODEL };
// with implicit defaults — use the same fallback as the no-decision path.
return { kind: 'ok', resolved: fallbackModel };
}
// Static fallback when the worker is slow or unavailable.
return { kind: 'ok', resolved: BALANCED_FALLBACK_MODEL };
return { kind: 'ok', resolved: fallbackModel };
}
const mode = resolveMode(modeHeader, featureHeader);
return {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/ai-gateway/latest-model-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const GEMINI_PRO_LATEST_MODEL_ALIAS = '~google/gemini-pro-latest';
export const GEMINI_FLASH_LATEST_MODEL_ALIAS = '~google/gemini-flash-latest';
export const GROK_LATEST_MODEL_ALIAS = '~x-ai/grok-latest';
export const GLM_LATEST_MODEL_ALIAS = '~z-ai/glm-latest';
export const GLM_FLASH_LATEST_MODEL_ALIAS = '~z-ai/glm-flash-latest';
export const DEEPSEEK_V4_FLASH_LATEST_MODEL_ALIAS = '~deepseek/deepseek-v4-flash-latest';

export const LATEST_MODEL_ALIASES = [
Expand All @@ -23,5 +24,6 @@ export const LATEST_MODEL_ALIASES = [
GEMINI_FLASH_LATEST_MODEL_ALIAS,
GROK_LATEST_MODEL_ALIAS,
GLM_LATEST_MODEL_ALIAS,
GLM_FLASH_LATEST_MODEL_ALIAS,
DEEPSEEK_V4_FLASH_LATEST_MODEL_ALIAS,
] as const;
10 changes: 5 additions & 5 deletions apps/web/src/lib/ai-gateway/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
claude_opus_4_7_stealth_model,
claude_sonnet_4_6_stealth_model,
claude_opus_4_6_stealth_model,
CLAUDE_SONNET_CURRENT_MODEL_ID,
CLAUDE_OPUS_CURRENT_MODEL_ID,
Comment thread
chrarnoldus marked this conversation as resolved.
} from '@/lib/ai-gateway/providers/anthropic.constants';
import { DEEPSEEK_V4_1_FLASH_MODEL_ID } from '@/lib/ai-gateway/providers/deepseek';
import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model';
import { isMuseModel } from '@/lib/ai-gateway/providers/meta';
import { MINIMAX_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/minimax';
Expand All @@ -25,12 +25,12 @@ import {
gpt_5_6_sol_discounted_model,
gpt_6_astra_flex_model,
} from '@/lib/ai-gateway/providers/openai-exclusive';
import { GLM_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/zai';
import { GLM_FLASH_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/zai';
import { type ProviderId } from '@/lib/ai-gateway/providers/types';
import type { OpenRouterReasoningConfig } from '@/lib/ai-gateway/providers/openrouter/types';
import { getRandomNumber } from '@/lib/ai-gateway/getRandomNumber';

export const PRIMARY_DEFAULT_MODEL = CLAUDE_SONNET_CURRENT_MODEL_ID;
export const PRIMARY_DEFAULT_MODEL = GLM_FLASH_CURRENT_MODEL_ID;

export type AutoFreeModel = {
model: string;
Expand Down Expand Up @@ -92,14 +92,14 @@ export const preferredModels = [

...autoFreeModels.map(({ model }) => model),

CLAUDE_SONNET_CURRENT_MODEL_ID,
CLAUDE_OPUS_CURRENT_MODEL_ID,
GPT_CURRENT_MODEL_ID,
...(gpt_5_6_sol_discounted_model.status === 'public'
? [gpt_5_6_sol_discounted_model.public_id]
: []),
...(gpt_6_astra_flex_model.status === 'public' ? [gpt_6_astra_flex_model.public_id] : []),
GLM_CURRENT_MODEL_ID,
DEEPSEEK_V4_1_FLASH_MODEL_ID,
GLM_FLASH_CURRENT_MODEL_ID,
KIMI_CURRENT_MODEL_ID,
MINIMAX_CURRENT_MODEL_ID,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const CLAUDE_SONNET_CURRENT_MODEL_ID = 'anthropic/claude-sonnet-5';
export const CLAUDE_OPUS_CURRENT_MODEL_ID = 'anthropic/claude-opus-5';
export const CLAUDE_OPUS_FALLBACK_MODEL_ID = 'anthropic/claude-opus-4.8';
export const CLAUDE_HAIKU_CURRENT_MODEL_ID = 'anthropic/claude-haiku-4.5';
export const CLAUDE_FABLE_CURRENT_MODEL_ID = 'anthropic/claude-fable-5';
export const CLAUDE_FABLE_CURRENT_MODEL_ID = 'anthropic/claude-fable-5.1';
export const CLAUDE_OPUS_4_8_STEALTH_MODEL_ID = 'stealth/claude-opus-4.8';
export const CLAUDE_OPUS_STEALTH_MODEL_ID = 'stealth/claude-opus-4.7';
export const CLAUDE_SONNET_STEALTH_MODEL_ID = 'stealth/claude-sonnet-4.6';
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/deepseek.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export function isDeepseekModel(model: string) {
return model.includes('deepseek');
}

export const DEEPSEEK_V4_1_FLASH_MODEL_ID = 'deepseek/deepseek-v4.1-flash';
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
} from '@/lib/ai-gateway/providers/openai';
import { mapModelIdToVercel } from '@/lib/ai-gateway/providers/vercel/mapModelIdToVercel';
import { GROK_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/xai';
import { GLM_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/zai';
import {
GLM_CURRENT_VERCEL_MODEL_ID,
GLM_FLASH_CURRENT_VERCEL_MODEL_ID,
} from '@/lib/ai-gateway/providers/zai';
import {
CLAUDE_FABLE_LATEST_MODEL_ALIAS,
CLAUDE_HAIKU_LATEST_MODEL_ALIAS,
Expand All @@ -27,6 +30,7 @@ import {
GEMINI_PRO_LATEST_MODEL_ALIAS,
GPT_LATEST_MODEL_ALIAS,
GPT_MINI_LATEST_MODEL_ALIAS,
GLM_FLASH_LATEST_MODEL_ALIAS,
GLM_LATEST_MODEL_ALIAS,
GROK_LATEST_MODEL_ALIAS,
KIMI_LATEST_MODEL_ALIAS,
Expand All @@ -47,6 +51,7 @@ describe('mapModelIdToVercel', () => {
[GEMINI_FLASH_LATEST_MODEL_ALIAS, GEMINI_FLASH_CURRENT_VERCEL_MODEL_ID],
[GROK_LATEST_MODEL_ALIAS, GROK_CURRENT_VERCEL_MODEL_ID],
[GLM_LATEST_MODEL_ALIAS, GLM_CURRENT_VERCEL_MODEL_ID],
[GLM_FLASH_LATEST_MODEL_ALIAS, GLM_FLASH_CURRENT_VERCEL_MODEL_ID],
[DEEPSEEK_V4_FLASH_LATEST_MODEL_ALIAS, 'deepseek/deepseek-v4-flash-0731'],
])('maps %s to the current Vercel model id', (input, expected) => {
expect(mapModelIdToVercel(input)).toBe(expected);
Expand All @@ -65,6 +70,7 @@ describe('mapModelIdToVercel', () => {
GEMINI_FLASH_LATEST_MODEL_ALIAS,
GROK_LATEST_MODEL_ALIAS,
GLM_LATEST_MODEL_ALIAS,
GLM_FLASH_LATEST_MODEL_ALIAS,
DEEPSEEK_V4_FLASH_LATEST_MODEL_ALIAS,
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
} from '@/lib/ai-gateway/providers/openai';
import { inferVercelFirstPartyInferenceProviderForModel } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id';
import { GROK_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/xai';
import { GLM_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/zai';
import {
GLM_CURRENT_VERCEL_MODEL_ID,
GLM_FLASH_CURRENT_VERCEL_MODEL_ID,
} from '@/lib/ai-gateway/providers/zai';
import {
CLAUDE_FABLE_LATEST_MODEL_ALIAS,
CLAUDE_HAIKU_LATEST_MODEL_ALIAS,
Expand All @@ -27,6 +30,7 @@ import {
GEMINI_PRO_LATEST_MODEL_ALIAS,
GPT_LATEST_MODEL_ALIAS,
GPT_MINI_LATEST_MODEL_ALIAS,
GLM_FLASH_LATEST_MODEL_ALIAS,
GLM_LATEST_MODEL_ALIAS,
GROK_LATEST_MODEL_ALIAS,
KIMI_LATEST_MODEL_ALIAS,
Expand All @@ -44,6 +48,7 @@ const vercelModelIdMapping: Record<string, string | undefined> = {
[GEMINI_FLASH_LATEST_MODEL_ALIAS]: GEMINI_FLASH_CURRENT_VERCEL_MODEL_ID,
[GROK_LATEST_MODEL_ALIAS]: GROK_CURRENT_VERCEL_MODEL_ID,
[GLM_LATEST_MODEL_ALIAS]: GLM_CURRENT_VERCEL_MODEL_ID,
[GLM_FLASH_LATEST_MODEL_ALIAS]: GLM_FLASH_CURRENT_VERCEL_MODEL_ID,
[DEEPSEEK_V4_FLASH_LATEST_MODEL_ALIAS]: 'deepseek/deepseek-v4-flash-0731',
'mistralai/codestral-2508': 'mistral/codestral',
'mistralai/devstral-2512': 'mistral/devstral-2',
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/zai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export function isGlmModel(model: string) {

export const GLM_CURRENT_MODEL_ID = 'z-ai/glm-5.3';
export const GLM_CURRENT_VERCEL_MODEL_ID = 'zai/glm-5.3';
export const GLM_FLASH_CURRENT_MODEL_ID = 'z-ai/glm-5.3-flash';
export const GLM_FLASH_CURRENT_VERCEL_MODEL_ID = 'zai/glm-5.3-flash';
Loading