Skip to content

Commit 0038a64

Browse files
authored
💄 style: Optimize OpenRouter modelFetch endpoint (lobehub#9671)
* 🔧 refactor: Update OpenRouter API endpoint and enhance model data structure * 🐛 fix: 修正模型名称处理逻辑以避免不必要的前缀去除
1 parent 02096ea commit 0038a64

File tree

3 files changed

+60
-47
lines changed

3 files changed

+60
-47
lines changed

packages/model-runtime/src/providers/openrouter/index.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ testProvider({
2121
});
2222

2323
// Mock the console.error to avoid polluting test output
24-
vi.spyOn(console, 'error').mockImplementation(() => {});
24+
vi.spyOn(console, 'error').mockImplementation(() => { });
2525

2626
let instance: LobeOpenAICompatibleRuntime;
2727

@@ -154,8 +154,8 @@ describe('LobeOpenRouterAI', () => {
154154

155155
const list = await instance.models();
156156

157-
// 验证在当前实现中,当 frontend fetch 返回非 ok 时,会返回空列表
158-
expect(fetch).toHaveBeenCalledWith('https://openrouter.ai/api/frontend/models');
157+
// 验证在当前实现中,当 model fetch 返回非 ok 时,会返回空列表
158+
expect(fetch).toHaveBeenCalledWith('https://openrouter.ai/api/v1/models');
159159
expect(list.length).toBe(0);
160160
expect(list).toEqual([]);
161161
});

packages/model-runtime/src/providers/openrouter/index.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
5858
let modelList: OpenRouterModelCard[] = [];
5959

6060
try {
61-
const response = await fetch('https://openrouter.ai/api/frontend/models');
61+
const response = await fetch('https://openrouter.ai/api/v1/models');
6262
if (response.ok) {
6363
const data = await response.json();
6464
modelList = data['data'];
@@ -70,44 +70,56 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
7070

7171
// 处理前端获取的模型信息,转换为标准格式
7272
const formattedModels = modelList.map((model) => {
73-
const { endpoint } = model;
74-
const endpointModel = endpoint?.model;
73+
const { top_provider, architecture, pricing, supported_parameters } = model;
7574

76-
const inputModalities = endpointModel?.input_modalities || model.input_modalities;
75+
const inputModalities = architecture.input_modalities || [];
7776

78-
let displayName = model.slug?.toLowerCase().includes('deepseek') && !model.short_name?.toLowerCase().includes('deepseek')
79-
? (model.name ?? model.slug)
80-
: (model.short_name ?? model.name ?? model.slug);
77+
// 处理 name,默认去除冒号及其前面的内容
78+
let displayName = model.name;
79+
const colonIndex = displayName.indexOf(':');
80+
if (colonIndex !== -1) {
81+
const prefix = displayName.substring(0, colonIndex).trim();
82+
const suffix = displayName.substring(colonIndex + 1).trim();
8183

82-
const inputPrice = formatPrice(endpoint?.pricing?.prompt);
83-
const outputPrice = formatPrice(endpoint?.pricing?.completion);
84-
const cachedInputPrice = formatPrice(endpoint?.pricing?.input_cache_read);
85-
const writeCacheInputPrice = formatPrice(endpoint?.pricing?.input_cache_write);
84+
const isDeepSeekPrefix = prefix.toLowerCase() === 'deepseek';
85+
const suffixHasDeepSeek = suffix.toLowerCase().includes('deepseek');
86+
87+
if (isDeepSeekPrefix && !suffixHasDeepSeek) {
88+
displayName = model.name;
89+
} else {
90+
displayName = suffix;
91+
}
92+
}
93+
94+
const inputPrice = formatPrice(pricing.prompt);
95+
const outputPrice = formatPrice(pricing.completion);
96+
const cachedInputPrice = formatPrice(pricing.input_cache_read);
97+
const writeCacheInputPrice = formatPrice(pricing.input_cache_write);
8698

8799
const isFree = (inputPrice === 0 || outputPrice === 0) && !displayName.endsWith('(free)');
88100
if (isFree) {
89101
displayName += ' (free)';
90102
}
91103

92104
return {
93-
contextWindowTokens: endpoint?.context_length || model.context_length,
94-
description: endpointModel?.description || model.description,
105+
contextWindowTokens: top_provider.context_length || model.context_length,
106+
description: model.description,
95107
displayName,
96-
functionCall: endpoint?.supports_tool_parameters || false,
97-
id: endpoint?.model_variant_slug || model.slug,
108+
functionCall: supported_parameters.includes('tools'),
109+
id: model.id,
98110
maxOutput:
99-
typeof endpoint?.max_completion_tokens === 'number'
100-
? endpoint.max_completion_tokens
111+
typeof top_provider.max_completion_tokens === 'number'
112+
? top_provider.max_completion_tokens
101113
: undefined,
102114
pricing: {
103115
input: inputPrice,
104116
cachedInput: cachedInputPrice,
105117
writeCacheInput: writeCacheInputPrice,
106118
output: outputPrice,
107119
},
108-
reasoning: endpoint?.supports_reasoning || false,
109-
releasedAt: new Date(model.created_at).toISOString().split('T')[0],
110-
vision: Array.isArray(inputModalities) && inputModalities.includes('image'),
120+
reasoning: supported_parameters.includes('reasoning'),
121+
releasedAt: new Date(model.created * 1000).toISOString().split('T')[0],
122+
vision: inputModalities.includes('image'),
111123
};
112124
});
113125

packages/model-runtime/src/providers/openrouter/type.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,38 @@ interface ModelPricing {
55
input_cache_write?: string;
66
prompt: string;
77
request?: string;
8+
web_search?: string;
9+
internal_reasoning?: string;
810
}
911

10-
export interface OpenRouterModelCard {
12+
interface TopProvider {
1113
context_length: number;
12-
created_at: number;
13-
description?: string;
14-
endpoint: OpenRouterModelEndpoint;
15-
input_modalities?: string[];
16-
name?: string;
17-
output_modalities?: string[];
18-
per_request_limits?: any | null;
19-
short_name?: string;
20-
slug: string;
14+
max_completion_tokens: number | null;
15+
is_moderated: boolean;
2116
}
2217

23-
interface OpenRouterModelEndpoint {
24-
context_length?: number;
25-
max_completion_tokens: number | null;
26-
model?: {
27-
description?: string;
28-
input_modalities?: string[];
29-
name?: string;
30-
short_name?: string;
31-
slug: string;
32-
};
33-
model_variant_slug?: string;
18+
interface Architecture {
19+
modality: string;
20+
input_modalities: string[];
21+
output_modalities: string[];
22+
tokenizer: string;
23+
instruct_type: string | null;
24+
}
25+
26+
export interface OpenRouterModelCard {
27+
id: string;
28+
canonical_slug: string;
29+
hugging_face_id?: string;
30+
name: string;
31+
created: number;
32+
description?: string;
33+
context_length: number;
34+
architecture: Architecture;
3435
pricing: ModelPricing;
36+
top_provider: TopProvider;
37+
per_request_limits?: any | null;
3538
supported_parameters: string[];
36-
supports_reasoning?: boolean;
37-
supports_tool_parameters?: boolean;
38-
variant?: 'free' | 'standard' | 'unknown';
39+
default_parameters?: any | null;
3940
}
4041

4142
interface OpenRouterOpenAIReasoning {

0 commit comments

Comments
 (0)