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
2 changes: 1 addition & 1 deletion apps/web/src/app/api/openrouter/embeddings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
const effectiveProvider = provider;

if (userByok && userByok.length > 0 && provider.id === 'vercel') {
requestBodyParsed.model = mapModelIdToVercel(requestBodyParsed.model, false);
requestBodyParsed.model = mapModelIdToVercel(requestBodyParsed.model);
}

const upstreamBody = buildUpstreamBody(requestBodyParsed, requestedModelLowerCased);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/byok/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function getModelUserByokProviders(modelId: string): Promise<UserBy
return [];
}
const providers: UserByokProviderId[] =
vercelModelMetadata[mapModelIdToVercel(modelId, false)]?.endpoints
vercelModelMetadata[mapModelIdToVercel(modelId)]?.endpoints
.map(ep => VercelUserByokInferenceProviderIdSchema.safeParse(ep.provider_name ?? ep.tag).data)
.filter(providerId => providerId !== undefined) ?? [];
if (providers.length === 0) {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/lib/ai-gateway/providers/model-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isKimiModel } from '@/lib/ai-gateway/providers/moonshotai';
import { isOpenAiModel } from '@/lib/ai-gateway/providers/openai';
import { isQwenModel } from '@/lib/ai-gateway/providers/qwen';
import { seed_20_code_free_model } from '@/lib/ai-gateway/providers/seed';
import { isGrokModel, isGrokToggleableReasoningModel } from '@/lib/ai-gateway/providers/xai';
import { isGrokModel, isGrok42Model, isGrok45Model } from '@/lib/ai-gateway/providers/xai';
import { isGlmModel } from '@/lib/ai-gateway/providers/zai';
import type {
CustomLlmProvider,
Expand Down Expand Up @@ -101,7 +101,7 @@ export function getModelVariants(model: string): OpenCodeSettings['variants'] {
if (
isMinimaxModel(model) ||
isKimiModel(model) ||
isGrokToggleableReasoningModel(model) ||
isGrok42Model(model) ||
isQwenModel(model) ||
isGemmaModel(model) ||
model.includes('mimo')
Expand All @@ -114,7 +114,7 @@ export function getModelVariants(model: string): OpenCodeSettings['variants'] {
if (model.startsWith('inception/mercury-2')) {
return REASONING_VARIANTS_INSTANT_LOW_MEDIUM_HIGH;
}
if (isStepModel(model)) {
if (isStepModel(model) || isGrok45Model(model)) {
return REASONING_VARIANTS_LOW_MEDIUM_HIGH;
}
if (isDeepseekModel(model) || isGlmModel(model)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ function injectExtraUserByokModels(
}
}
for (const model of openRouterModels.values()) {
const vercelModel = vercelModels[mapModelIdToVercel(model.slug, false)];
const vercelModel = vercelModels[mapModelIdToVercel(model.slug)];
if (!vercelModel) continue;

const vercelInferenceProviders = new Set(
Expand Down
8 changes: 2 additions & 6 deletions apps/web/src/lib/ai-gateway/providers/vercel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
VercelInferenceProviderConfig,
VercelProviderConfig,
} from '@/lib/ai-gateway/providers/openrouter/types';
import { isReasoningExplicitlyDisabled } from '@/lib/ai-gateway/providers/openrouter/request-helpers';
import { mapModelIdToVercel } from '@/lib/ai-gateway/providers/vercel/mapModelIdToVercel';
import { redisClient } from '@/lib/redis';
import { createCachedFetch } from '@/lib/cached-fetch';
Expand Down Expand Up @@ -92,7 +91,7 @@ export async function shouldRouteToVercel(
}

const vercelModels = await getVercelModelsFromRedis();
const vercelModelId = mapModelIdToVercel(requestedModel, isReasoningExplicitlyDisabled(request));
const vercelModelId = mapModelIdToVercel(requestedModel);
if (!vercelModels.has(vercelModelId)) {
console.debug(`[shouldRouteToVercel] model not found in Vercel model list`);
return false;
Expand Down Expand Up @@ -186,10 +185,7 @@ export function applyVercelSettings(
requestToMutate: GatewayRequest,
userByok: BYOKResult[] | null
) {
requestToMutate.body.model = mapModelIdToVercel(
requestedModel,
isReasoningExplicitlyDisabled(requestToMutate)
);
requestToMutate.body.model = mapModelIdToVercel(requestedModel);

if (userByok) {
if (userByok.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ describe('mapModelIdToVercel', () => {
['~google/gemini-flash-latest', GEMINI_FLASH_CURRENT_VERCEL_MODEL_ID],
['~x-ai/grok-latest', GROK_CURRENT_VERCEL_MODEL_ID],
])('maps %s to the current Vercel model id', (input, expected) => {
expect(mapModelIdToVercel(input, false)).toBe(expected);
expect(mapModelIdToVercel(input)).toBe(expected);
});

it('does not map a latest alias that is missing the leading tilde', () => {
expect(mapModelIdToVercel('anthropic/claude-opus-latest', false)).toBe(
expect(mapModelIdToVercel('anthropic/claude-opus-latest')).toBe(
'anthropic/claude-opus-latest'
);
});
Expand All @@ -59,83 +59,54 @@ describe('mapModelIdToVercel', () => {
['qwen/qwen3-30b-a3b', 'alibaba/qwen-3-30b'],
['qwen/qwen3-32b', 'alibaba/qwen-3-32b'],
])('maps %s to %s', (input, expected) => {
expect(mapModelIdToVercel(input, false)).toBe(expected);
});
});

describe('grok 4.20 reasoning/non-reasoning toggle', () => {
it('maps x-ai/grok-4.20 to the reasoning variant when reasoning is not explicitly disabled', () => {
expect(mapModelIdToVercel('x-ai/grok-4.20', false)).toBe('xai/grok-4.20-reasoning');
});

it('maps x-ai/grok-4.20 to the non-reasoning variant when reasoning is explicitly disabled', () => {
expect(mapModelIdToVercel('x-ai/grok-4.20', true)).toBe('xai/grok-4.20-non-reasoning');
});

it('does not rewrite other hardcoded mapping targets when reasoning is disabled', () => {
// Only grok-4.20 has a reasoning/non-reasoning split; other hardcoded
// mappings must pass through unchanged regardless of the flag.
expect(mapModelIdToVercel('mistralai/codestral-2508', true)).toBe('mistral/codestral');
expect(mapModelIdToVercel('qwen/qwen3-32b', true)).toBe('alibaba/qwen-3-32b');
});

it('does not apply the reasoning toggle to grok models outside the hardcoded mapping', () => {
// A grok id that is not in vercelModelIdMapping should fall through to
// the generic prefix rewrite even when reasoning is explicitly disabled.
expect(mapModelIdToVercel('x-ai/grok-4.20-reasoning', true)).toBe('xai/grok-4.20-reasoning');
expect(mapModelIdToVercel(input)).toBe(expected);
});
});

describe('first-party inference provider inference', () => {
it('rewrites the anthropic/ prefix unchanged', () => {
expect(mapModelIdToVercel('anthropic/claude-sonnet-4.5', false)).toBe(
'anthropic/claude-sonnet-4.5'
);
expect(mapModelIdToVercel('anthropic/claude-sonnet-4.5')).toBe('anthropic/claude-sonnet-4.5');
});

it('rewrites the mistralai/ prefix to mistral/', () => {
// not covered by the hardcoded mapping
expect(mapModelIdToVercel('mistralai/some-new-model', false)).toBe('mistral/some-new-model');
expect(mapModelIdToVercel('mistralai/some-new-model')).toBe('mistral/some-new-model');
});

it('rewrites the qwen/ prefix to alibaba/', () => {
expect(mapModelIdToVercel('qwen/some-new-qwen-model', false)).toBe(
'alibaba/some-new-qwen-model'
);
expect(mapModelIdToVercel('qwen/some-new-qwen-model')).toBe('alibaba/some-new-qwen-model');
});

it('rewrites x-ai/ to xai/', () => {
expect(mapModelIdToVercel('x-ai/some-new-grok', false)).toBe('xai/some-new-grok');
expect(mapModelIdToVercel('x-ai/some-new-grok')).toBe('xai/some-new-grok');
});

it('rewrites z-ai/ to zai/', () => {
expect(mapModelIdToVercel('z-ai/glm-5.1', false)).toBe('zai/glm-5.1');
expect(mapModelIdToVercel('z-ai/glm-5.1')).toBe('zai/glm-5.1');
});

it('leaves gpt-oss models unchanged', () => {
expect(mapModelIdToVercel('openai/gpt-oss-20b', false)).toBe('openai/gpt-oss-20b');
expect(mapModelIdToVercel('openai/gpt-oss-20b')).toBe('openai/gpt-oss-20b');
});

it('leaves a model with an unknown provider prefix unchanged', () => {
expect(mapModelIdToVercel('deepseek/deepseek-v3.2', false)).toBe('deepseek/deepseek-v3.2');
expect(mapModelIdToVercel('deepseek/deepseek-v3.2')).toBe('deepseek/deepseek-v3.2');
});

it('returns the model id as-is when it contains no slash', () => {
expect(mapModelIdToVercel('some-model-without-slash', false)).toBe(
'some-model-without-slash'
);
expect(mapModelIdToVercel('some-model-without-slash')).toBe('some-model-without-slash');
});
});

describe('kilo-exclusive models', () => {
it('maps a Vercel-exclusive model to its internal id', () => {
expect(mapModelIdToVercel('meta/muse-spark-1.1', false)).toBe('meta/muse-spark-1.1');
expect(mapModelIdToVercel('meta/muse-spark-1.1')).toBe('meta/muse-spark-1.1');
});

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', false)).toBe(
expect(mapModelIdToVercel('google/gemma-4-26b-a4b-it:free')).toBe(
'google/gemma-4-26b-a4b-it'
);
});
Expand All @@ -144,7 +115,7 @@ describe('mapModelIdToVercel', () => {
// claude_sonnet_clawsetup_model has gateway 'openrouter' and no
// 'vercel-routing' flag, so the mapping must pass the public id through
// the generic prefix rewrite instead of substituting internal_id.
expect(mapModelIdToVercel('anthropic/claude-sonnet-4.6:clawsetup', false)).toBe(
expect(mapModelIdToVercel('anthropic/claude-sonnet-4.6:clawsetup')).toBe(
'anthropic/claude-sonnet-4.6:clawsetup'
);
});
Expand All @@ -153,9 +124,7 @@ describe('mapModelIdToVercel', () => {
// minimax_m25_free_model has the 'vercel-routing' flag but status
// 'disabled', so it must not be substituted by internal_id and instead
// pass the public id through the generic prefix rewrite.
expect(mapModelIdToVercel('minimax/minimax-m2.5:free', false)).toBe(
'minimax/minimax-m2.5:free'
);
expect(mapModelIdToVercel('minimax/minimax-m2.5:free')).toBe('minimax/minimax-m2.5:free');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const vercelModelIdMapping: Record<string, string | undefined> = {
'mistralai/devstral-2512': 'mistral/devstral-2',
'mistralai/mistral-embed-2312': 'mistral/mistral-embed',
'mistralai/codestral-embed-2505': 'mistral/codestral-embed',
'x-ai/grok-4.20': 'xai/grok-4.20-reasoning',
'mistralai/ministral-14b-2512': 'mistral/ministral-14b',
'mistralai/ministral-3b-2512': 'mistral/ministral-3b',
'mistralai/ministral-8b-2512': 'mistral/ministral-8b',
Expand All @@ -46,12 +45,10 @@ const vercelModelIdMapping: Record<string, string | undefined> = {
'qwen/qwen3-32b': 'alibaba/qwen-3-32b',
};

export function mapModelIdToVercel(modelId: string, reasoningExplicitlyDisabled: boolean) {
export function mapModelIdToVercel(modelId: string) {
const hardcodedVercelId = vercelModelIdMapping[modelId];
if (hardcodedVercelId) {
return hardcodedVercelId === 'xai/grok-4.20-reasoning' && reasoningExplicitlyDisabled
? 'xai/grok-4.20-non-reasoning'
: hardcodedVercelId;
return hardcodedVercelId;
}

const internalId =
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/lib/ai-gateway/providers/xai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export function isGrokModel(requestedModel: string) {
return requestedModel.includes('grok');
}

export function isGrokToggleableReasoningModel(model: string) {
export function isGrok42Model(model: string) {
return model.includes('grok-4.2');
}

export function isGrok45Model(model: string) {
return model.includes('grok-4.5');
}
2 changes: 1 addition & 1 deletion apps/web/src/routers/byok-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async function fetchSupportedModels(): Promise<Record<string, string[]>> {

for (const openRouterModel of Object.values(openRouterModelMetadata)) {
if (isKiloExclusiveModel(openRouterModel.id)) continue;
const vercelModel = vercelModelMetadata[mapModelIdToVercel(openRouterModel.id, false)];
const vercelModel = vercelModelMetadata[mapModelIdToVercel(openRouterModel.id)];
if (!vercelModel) continue;
if (vercelModel.type !== 'language') continue;
for (const endpoint of vercelModel.endpoints) {
Expand Down