Skip to content

Commit 4206937

Browse files
Update OpenRouter => Vercel model id mapping (#2996)
* Update OpenRouter => Vercel model id mapping * Apply suggestions from code review Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> * test(ai-gateway): add tests for mapModelIdToVercel * refactor(ai-gateway): drop unused model-prefix helpers --------- Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
1 parent f514efc commit 4206937

11 files changed

Lines changed: 179 additions & 56 deletions

File tree

apps/web/src/lib/ai-gateway/models.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ import {
2121
} from '@/lib/ai-gateway/providers/minimax';
2222
import { KIMI_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/moonshotai';
2323
import { morph_warp_grep_free_model } from '@/lib/ai-gateway/providers/morph';
24-
import { gemma_4_26b_a4b_it_free_model } from '@/lib/ai-gateway/providers/google';
24+
import {
25+
GEMINI_PRO_CURRENT_MODEL_ID,
26+
gemma_4_26b_a4b_it_free_model,
27+
} from '@/lib/ai-gateway/providers/google';
2528
import { alibabaDirectModels, qwen36_plus_model } from '@/lib/ai-gateway/providers/qwen';
2629
import { stepfun_35_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
2730
import {
2831
grok_code_fast_1_optimized_free_model,
2932
isGrok4Model,
3033
} from '@/lib/ai-gateway/providers/xai';
3134
import { isClaudeModel } from '@/lib/ai-gateway/providers/anthropic.constants';
32-
import { isOpenAiModel } from '@/lib/ai-gateway/providers/openai';
35+
import { GPT_CURRENT_MODEL_ID, isOpenAiModel } from '@/lib/ai-gateway/providers/openai';
36+
import { GLM_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/zai';
3337

3438
export const PRIMARY_DEFAULT_MODEL = CLAUDE_SONNET_CURRENT_MODEL_ID;
3539

@@ -51,11 +55,11 @@ export const preferredModels = [
5155
CLAUDE_OPUS_CURRENT_MODEL_ID,
5256
KIMI_CURRENT_MODEL_ID,
5357
CLAUDE_SONNET_CURRENT_MODEL_ID,
54-
'openai/gpt-5.5',
55-
'google/gemini-3.1-pro-preview',
58+
GPT_CURRENT_MODEL_ID,
59+
GEMINI_PRO_CURRENT_MODEL_ID,
5660
MINIMAX_CURRENT_MODEL_ID,
5761
qwen36_plus_model.public_id,
58-
'z-ai/glm-5.1',
62+
GLM_CURRENT_MODEL_ID,
5963
];
6064

6165
export function isFreeModel(model: string): boolean {

apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model';
22

33
export const CLAUDE_SONNET_CURRENT_MODEL_ID = 'anthropic/claude-sonnet-4.6';
4-
5-
export const CLAUDE_SONNET_CURRENT_MODEL_NAME = 'Claude Sonnet 4.6';
6-
74
export const CLAUDE_OPUS_CURRENT_MODEL_ID = 'anthropic/claude-opus-4.7';
5+
export const CLAUDE_HAIKU_CURRENT_MODEL_ID = 'anthropic/claude-haiku-4.5';
86

9-
export const CLAUDE_OPUS_CURRENT_MODEL_NAME = 'Claude Opus 4.7';
7+
export const CLAUDE_SONNET_CURRENT_VERCEL_MODEL_ID = CLAUDE_SONNET_CURRENT_MODEL_ID;
8+
export const CLAUDE_OPUS_CURRENT_VERCEL_MODEL_ID = CLAUDE_OPUS_CURRENT_MODEL_ID;
9+
export const CLAUDE_HAIKU_CURRENT_VERCEL_MODEL_ID = CLAUDE_HAIKU_CURRENT_MODEL_ID;
1010

1111
export const claude_sonnet_clawsetup_model: KiloExclusiveModel = {
1212
public_id: CLAUDE_SONNET_CURRENT_MODEL_ID + ':clawsetup',

apps/web/src/lib/ai-gateway/providers/google.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,7 @@ export function applyGoogleModelSettings(provider: ProviderId, requestToMutate:
6868
delete lineRanges.items;
6969
}
7070
}
71+
72+
export const GEMINI_PRO_CURRENT_MODEL_ID = 'google/gemini-3.1-pro-preview';
73+
74+
export const GEMINI_PRO_CURRENT_VERCEL_MODEL_ID = GEMINI_PRO_CURRENT_MODEL_ID;

apps/web/src/lib/ai-gateway/providers/model-prefix.test.ts

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { modelStartsWith, stripModelTilde } from './model-prefix';
21
import { isClaudeModel, isHaikuModel, isOpusModel } from './anthropic.constants';
32
import { isOpenAiModel, isGptOssModel } from './openai';
43
import { isGeminiModel, isGemmaModel, isGemini3Model } from './google';
@@ -10,31 +9,6 @@ import { isStepModel } from './stepfun';
109
import { isCodestralModel, isMistralModel } from './mistral';
1110
import { inferVercelFirstPartyInferenceProviderForModel } from './openrouter/inference-provider-id';
1211

13-
describe('modelStartsWith', () => {
14-
test('matches the bare prefix', () => {
15-
expect(modelStartsWith('anthropic/claude-sonnet-4.5', 'anthropic/')).toBe(true);
16-
});
17-
18-
test('matches the tilde-prefixed variant', () => {
19-
expect(modelStartsWith('~anthropic/claude-sonnet-4.5', 'anthropic/')).toBe(true);
20-
});
21-
22-
test('rejects unrelated prefixes', () => {
23-
expect(modelStartsWith('openai/gpt-5', 'anthropic/')).toBe(false);
24-
expect(modelStartsWith('~openai/gpt-5', 'anthropic/')).toBe(false);
25-
});
26-
});
27-
28-
describe('stripModelTilde', () => {
29-
test('removes a leading tilde', () => {
30-
expect(stripModelTilde('~anthropic/claude-sonnet-4.5')).toBe('anthropic/claude-sonnet-4.5');
31-
});
32-
33-
test('leaves untilded ids alone', () => {
34-
expect(stripModelTilde('anthropic/claude-sonnet-4.5')).toBe('anthropic/claude-sonnet-4.5');
35-
});
36-
});
37-
3812
describe('provider predicates match substrings, regardless of prefix', () => {
3913
test('isClaudeModel / isHaikuModel / isOpusModel', () => {
4014
expect(isClaudeModel('~anthropic/claude-sonnet-4.5')).toBe(true);
@@ -102,11 +76,11 @@ describe('provider predicates match substrings, regardless of prefix', () => {
10276
expect(isCodestralModel('mistralai/devstral-2')).toBe(false);
10377
});
10478

105-
test('inferVercelFirstPartyInferenceProviderForModel strips tilde', () => {
106-
expect(inferVercelFirstPartyInferenceProviderForModel('~anthropic/claude-sonnet-4.5')).toBe(
79+
test('inferVercelFirstPartyInferenceProviderForModel', () => {
80+
expect(inferVercelFirstPartyInferenceProviderForModel('anthropic/claude-sonnet-4.5')).toBe(
10781
'anthropic'
10882
);
109-
expect(inferVercelFirstPartyInferenceProviderForModel('~openai/gpt-5-nano')).toBe('openai');
110-
expect(inferVercelFirstPartyInferenceProviderForModel('~openai/gpt-oss')).toBe(null);
83+
expect(inferVercelFirstPartyInferenceProviderForModel('openai/gpt-5-nano')).toBe('openai');
84+
expect(inferVercelFirstPartyInferenceProviderForModel('openai/gpt-oss')).toBe(null);
11185
});
11286
});

apps/web/src/lib/ai-gateway/providers/model-prefix.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

apps/web/src/lib/ai-gateway/providers/moonshotai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ export function applyMoonshotModelSettings(requestToMutate: GatewayRequest) {
1313

1414
export const KIMI_CURRENT_MODEL_ID = 'moonshotai/kimi-k2.6';
1515

16-
export const KIMI_CURRENT_MODEL_NAME = 'Kimi K2.6';
16+
export const KIMI_CURRENT_VERCEL_MODEL_ID = KIMI_CURRENT_MODEL_ID;

apps/web/src/lib/ai-gateway/providers/openai.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ export function isOpenAiModel(requestedModel: string) {
88
export function isGptOssModel(requestedModel: string) {
99
return requestedModel.includes('gpt-oss');
1010
}
11+
12+
export const GPT_CURRENT_MODEL_ID = 'openai/gpt-5.5';
13+
14+
export const GPT_CURRENT_VERCEL_MODEL_ID = GPT_CURRENT_MODEL_ID;

apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as z from 'zod';
2-
import { stripModelTilde } from '@/lib/ai-gateway/providers/model-prefix';
32
import { isGptOssModel } from '@/lib/ai-gateway/providers/openai';
43

54
export const OpenRouterInferenceProviderIdSchema = z.enum([
@@ -211,7 +210,7 @@ export function inferVercelFirstPartyInferenceProviderForModel(
211210
): VercelInferenceProviderId | null {
212211
return isGptOssModel(model)
213212
? null
214-
: (modelPrefixToVercelInferenceProviderMapping[stripModelTilde(model).split('/')[0]] ?? null);
213+
: (modelPrefixToVercelInferenceProviderMapping[model.split('/')[0]] ?? null);
215214
}
216215

217216
export const AwsCredentialsSchema = z.object({
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { describe, it, expect } from '@jest/globals';
2+
import {
3+
CLAUDE_HAIKU_CURRENT_VERCEL_MODEL_ID,
4+
CLAUDE_OPUS_CURRENT_VERCEL_MODEL_ID,
5+
CLAUDE_SONNET_CURRENT_VERCEL_MODEL_ID,
6+
} from '@/lib/ai-gateway/providers/anthropic.constants';
7+
import { GEMINI_PRO_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/google';
8+
import { KIMI_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/moonshotai';
9+
import { GPT_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/openai';
10+
import { mapModelIdToVercel } from '@/lib/ai-gateway/providers/vercel/mapModelIdToVercel';
11+
12+
describe('mapModelIdToVercel', () => {
13+
describe('tilde-prefixed latest aliases', () => {
14+
it.each([
15+
['~anthropic/claude-opus-latest', CLAUDE_OPUS_CURRENT_VERCEL_MODEL_ID],
16+
['~anthropic/claude-sonnet-latest', CLAUDE_SONNET_CURRENT_VERCEL_MODEL_ID],
17+
['~anthropic/claude-haiku-latest', CLAUDE_HAIKU_CURRENT_VERCEL_MODEL_ID],
18+
['~openai/gpt-latest', GPT_CURRENT_VERCEL_MODEL_ID],
19+
['~moonshotai/kimi-latest', KIMI_CURRENT_VERCEL_MODEL_ID],
20+
['~google/gemini-pro-latest', GEMINI_PRO_CURRENT_VERCEL_MODEL_ID],
21+
])('maps %s to the current Vercel model id', (input, expected) => {
22+
expect(mapModelIdToVercel(input, false)).toBe(expected);
23+
});
24+
25+
it('does not map a latest alias that is missing the leading tilde', () => {
26+
expect(mapModelIdToVercel('anthropic/claude-opus-latest', false)).toBe(
27+
'anthropic/claude-opus-latest'
28+
);
29+
});
30+
});
31+
32+
describe('hardcoded OpenRouter → Vercel mapping', () => {
33+
it.each([
34+
['arcee-ai/trinity-large-preview:free', 'arcee-ai/trinity-large-preview'],
35+
['mistralai/codestral-2508', 'mistral/codestral'],
36+
['mistralai/devstral-2512', 'mistral/devstral-2'],
37+
['mistralai/mistral-embed-2312', 'mistral/mistral-embed'],
38+
['mistralai/codestral-embed-2505', 'mistral/codestral-embed'],
39+
['mistralai/ministral-14b-2512', 'mistral/ministral-14b'],
40+
['mistralai/ministral-3b-2512', 'mistral/ministral-3b'],
41+
['mistralai/ministral-8b-2512', 'mistral/ministral-8b'],
42+
['mistralai/mistral-large-2512', 'mistral/mistral-large-3'],
43+
['mistralai/mistral-medium-3', 'mistral/mistral-medium'],
44+
['mistralai/mistral-medium-3.1', 'mistral/mistral-medium'],
45+
['mistralai/mistral-small-2603', 'mistral/mistral-small'],
46+
['mistralai/pixtral-large-2411', 'mistral/pixtral-large'],
47+
['qwen/qwen3-14b', 'alibaba/qwen-3-14b'],
48+
['qwen/qwen3-235b-a22b', 'alibaba/qwen-3-235b'],
49+
['qwen/qwen3-30b-a3b', 'alibaba/qwen-3-30b'],
50+
['qwen/qwen3-32b', 'alibaba/qwen-3-32b'],
51+
])('maps %s to %s', (input, expected) => {
52+
expect(mapModelIdToVercel(input, false)).toBe(expected);
53+
});
54+
});
55+
56+
describe('grok reasoning/non-reasoning toggle', () => {
57+
it.each([
58+
['x-ai/grok-4-fast', 'xai/grok-4-fast-reasoning'],
59+
['x-ai/grok-4.1-fast', 'xai/grok-4.1-fast-reasoning'],
60+
['x-ai/grok-4.20-beta', 'xai/grok-4.20-reasoning'],
61+
])('maps %s to %s when reasoning is not explicitly disabled', (input, expected) => {
62+
expect(mapModelIdToVercel(input, false)).toBe(expected);
63+
});
64+
65+
it.each([
66+
['x-ai/grok-4-fast', 'xai/grok-4-fast-non-reasoning'],
67+
['x-ai/grok-4.1-fast', 'xai/grok-4.1-fast-non-reasoning'],
68+
['x-ai/grok-4.20-beta', 'xai/grok-4.20-non-reasoning'],
69+
])('maps %s to %s when reasoning is explicitly disabled', (input, expected) => {
70+
expect(mapModelIdToVercel(input, true)).toBe(expected);
71+
});
72+
73+
it('does not rewrite a mapped target that does not end with -reasoning', () => {
74+
expect(mapModelIdToVercel('mistralai/codestral-2508', true)).toBe('mistral/codestral');
75+
});
76+
});
77+
78+
describe('first-party inference provider inference', () => {
79+
it('rewrites the anthropic/ prefix unchanged', () => {
80+
expect(mapModelIdToVercel('anthropic/claude-sonnet-4.5', false)).toBe(
81+
'anthropic/claude-sonnet-4.5'
82+
);
83+
});
84+
85+
it('rewrites the mistralai/ prefix to mistral/', () => {
86+
// not covered by the hardcoded mapping
87+
expect(mapModelIdToVercel('mistralai/some-new-model', false)).toBe('mistral/some-new-model');
88+
});
89+
90+
it('rewrites the qwen/ prefix to alibaba/', () => {
91+
expect(mapModelIdToVercel('qwen/some-new-qwen-model', false)).toBe(
92+
'alibaba/some-new-qwen-model'
93+
);
94+
});
95+
96+
it('rewrites x-ai/ to xai/', () => {
97+
expect(mapModelIdToVercel('x-ai/some-new-grok', false)).toBe('xai/some-new-grok');
98+
});
99+
100+
it('rewrites z-ai/ to zai/', () => {
101+
expect(mapModelIdToVercel('z-ai/glm-5.1', false)).toBe('zai/glm-5.1');
102+
});
103+
104+
it('leaves gpt-oss models unchanged', () => {
105+
expect(mapModelIdToVercel('openai/gpt-oss-20b', false)).toBe('openai/gpt-oss-20b');
106+
});
107+
108+
it('leaves a model with an unknown provider prefix unchanged', () => {
109+
expect(mapModelIdToVercel('deepseek/deepseek-v3.2', false)).toBe('deepseek/deepseek-v3.2');
110+
});
111+
112+
it('returns the model id as-is when it contains no slash', () => {
113+
expect(mapModelIdToVercel('some-model-without-slash', false)).toBe(
114+
'some-model-without-slash'
115+
);
116+
});
117+
});
118+
119+
describe('kilo-exclusive models', () => {
120+
it('maps a hidden openrouter-gateway exclusive to its internal id', () => {
121+
// google/gemma-4-26b-a4b-it:free is registered in kiloExclusiveModels
122+
// with gateway 'openrouter' and internal_id 'google/gemma-4-26b-a4b-it'.
123+
expect(mapModelIdToVercel('google/gemma-4-26b-a4b-it:free', false)).toBe(
124+
'google/gemma-4-26b-a4b-it'
125+
);
126+
});
127+
128+
it('does not use internal_id for exclusives that are not routed via openrouter', () => {
129+
// x-ai/grok-code-fast-1:optimized:free has gateway 'martian',
130+
// so the mapping should pass the public id through the prefix rewrite.
131+
expect(mapModelIdToVercel('x-ai/grok-code-fast-1:optimized:free', false)).toBe(
132+
'xai/grok-code-fast-1:optimized:free'
133+
);
134+
});
135+
});
136+
});

apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
import { kiloExclusiveModels } from '@/lib/ai-gateway/models';
2+
import {
3+
CLAUDE_HAIKU_CURRENT_VERCEL_MODEL_ID,
4+
CLAUDE_OPUS_CURRENT_VERCEL_MODEL_ID,
5+
CLAUDE_SONNET_CURRENT_VERCEL_MODEL_ID,
6+
} from '@/lib/ai-gateway/providers/anthropic.constants';
7+
import { GEMINI_PRO_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/google';
8+
import { KIMI_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/moonshotai';
9+
import { GPT_CURRENT_VERCEL_MODEL_ID } from '@/lib/ai-gateway/providers/openai';
210
import { inferVercelFirstPartyInferenceProviderForModel } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id';
311

412
const vercelModelIdMapping: Record<string, string | undefined> = {
13+
'~anthropic/claude-opus-latest': CLAUDE_OPUS_CURRENT_VERCEL_MODEL_ID,
14+
'~anthropic/claude-sonnet-latest': CLAUDE_SONNET_CURRENT_VERCEL_MODEL_ID,
15+
'~anthropic/claude-haiku-latest': CLAUDE_HAIKU_CURRENT_VERCEL_MODEL_ID,
16+
'~openai/gpt-latest': GPT_CURRENT_VERCEL_MODEL_ID,
17+
'~moonshotai/kimi-latest': KIMI_CURRENT_VERCEL_MODEL_ID,
18+
'~google/gemini-pro-latest': GEMINI_PRO_CURRENT_VERCEL_MODEL_ID,
519
'arcee-ai/trinity-large-preview:free': 'arcee-ai/trinity-large-preview',
620
'mistralai/codestral-2508': 'mistral/codestral',
721
'mistralai/devstral-2512': 'mistral/devstral-2',
@@ -10,7 +24,6 @@ const vercelModelIdMapping: Record<string, string | undefined> = {
1024
'x-ai/grok-4-fast': 'xai/grok-4-fast-reasoning',
1125
'x-ai/grok-4.1-fast': 'xai/grok-4.1-fast-reasoning',
1226
'x-ai/grok-4.20-beta': 'xai/grok-4.20-reasoning',
13-
// Mistral date-suffixed → Vercel clean names
1427
'mistralai/ministral-14b-2512': 'mistral/ministral-14b',
1528
'mistralai/ministral-3b-2512': 'mistral/ministral-3b',
1629
'mistralai/ministral-8b-2512': 'mistral/ministral-8b',
@@ -19,7 +32,6 @@ const vercelModelIdMapping: Record<string, string | undefined> = {
1932
'mistralai/mistral-medium-3.1': 'mistral/mistral-medium',
2033
'mistralai/mistral-small-2603': 'mistral/mistral-small',
2134
'mistralai/pixtral-large-2411': 'mistral/pixtral-large',
22-
// Qwen name format: qwen3-Xb → qwen-3-Xb
2335
'qwen/qwen3-14b': 'alibaba/qwen-3-14b',
2436
'qwen/qwen3-235b-a22b': 'alibaba/qwen-3-235b',
2537
'qwen/qwen3-30b-a3b': 'alibaba/qwen-3-30b',

0 commit comments

Comments
 (0)