💄 style: Support OpenRouter Claude 4 reasoning (#8087)

* Add Claude Sonnet 4 and Claude Opus 4 to OpenRouter provider

* Calculate budget_token count
This commit is contained in:
Koell
2025-06-08 15:12:30 +08:00
committed by GitHub
parent b91ca8c903
commit 039be1d1b6
2 changed files with 99 additions and 43 deletions
+44
View File
@@ -660,6 +660,50 @@ const openrouterChatModels: AIChatModelCard[] = [
},
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
vision: true,
},
contextWindowTokens: 200_000,
description:
'Claude Sonnet 4 可以产生近乎即时的响应或延长的逐步思考,用户可以清晰地看到这些过程。API 用户还可以对模型思考的时间进行细致的控制',
displayName: 'Claude Sonnet 4',
id: 'anthropic/claude-sonnet-4',
maxOutput: 64_000,
pricing: {
input: 3,
output: 15,
},
releasedAt: '2025-05-23',
settings: {
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
},
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
vision: true,
},
contextWindowTokens: 200_000,
description:
'Claude Opus 4 是 Anthropic 用于处理高度复杂任务的最强大模型。它在性能、智能、流畅性和理解力方面表现卓越。',
displayName: 'Claude Opus 4',
id: 'anthropic/claude-opus-4',
maxOutput: 32_000,
pricing: {
input: 15,
output: 75,
},
releasedAt: '2025-05-23',
settings: {
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
},
type: 'chat',
},
{
abilities: {
functionCall: true,
+55 -43
View File
@@ -1,3 +1,4 @@
import OpenRouterModels from '@/config/aiModels/openrouter';
import type { ChatModelCard } from '@/types/llm';
import { ModelProvider } from '../types';
@@ -14,12 +15,27 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
baseURL: 'https://openrouter.ai/api/v1',
chatCompletion: {
handlePayload: (payload) => {
const { thinking } = payload;
const { thinking, model, max_tokens } = payload;
let reasoning: OpenRouterReasoning = {};
if (thinking?.type === 'enabled') {
const modelConfig = OpenRouterModels.find((m) => m.id === model);
const defaultMaxOutput = modelConfig?.maxOutput;
// 配置优先级:用户设置 > 模型配置 > 硬编码默认值
const getMaxTokens = () => {
if (max_tokens) return max_tokens;
if (defaultMaxOutput) return defaultMaxOutput;
return undefined;
};
const maxTokens = getMaxTokens() || 32_000; // Claude Opus 4 has minimum maxOutput
reasoning = {
max_tokens: thinking.budget_tokens,
max_tokens: thinking?.budget_tokens
? Math.min(thinking.budget_tokens, maxTokens - 1)
: 1024,
};
}
@@ -43,7 +59,7 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
models: async ({ client }) => {
const modelsPage = (await client.models.list()) as any;
const modelList: OpenRouterModelCard[] = modelsPage.data;
const modelsExtraInfo: OpenRouterModelExtraInfo[] = [];
try {
const response = await fetch('https://openrouter.ai/api/frontend/models');
@@ -54,49 +70,45 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
} catch (error) {
console.error('Failed to fetch OpenRouter frontend models:', error);
}
// 解析模型能力
const baseModels = await processMultiProviderModelList(modelList);
// 合并 OpenRouter 获取的模型信息
return baseModels.map((baseModel) => {
const model = modelList.find(m => m.id === baseModel.id);
const extraInfo = modelsExtraInfo.find(
(m) => m.slug.toLowerCase() === baseModel.id.toLowerCase(),
);
if (!model) return baseModel;
return {
...baseModel,
contextWindowTokens: model.context_length,
description: model.description,
displayName: model.name,
functionCall:
baseModel.functionCall ||
model.description.includes('function calling') ||
model.description.includes('tools') ||
extraInfo?.endpoint?.supports_tool_parameters ||
false,
maxTokens:
typeof model.top_provider.max_completion_tokens === 'number'
? model.top_provider.max_completion_tokens
: undefined,
pricing: {
input: formatPrice(model.pricing.prompt),
output: formatPrice(model.pricing.completion),
},
reasoning:
baseModel.reasoning ||
extraInfo?.endpoint?.supports_reasoning ||
false,
releasedAt: new Date(model.created * 1000).toISOString().split('T')[0],
vision:
baseModel.vision ||
model.architecture.modality.includes('image') ||
false,
};
}).filter(Boolean) as ChatModelCard[];
return baseModels
.map((baseModel) => {
const model = modelList.find((m) => m.id === baseModel.id);
const extraInfo = modelsExtraInfo.find(
(m) => m.slug.toLowerCase() === baseModel.id.toLowerCase(),
);
if (!model) return baseModel;
return {
...baseModel,
contextWindowTokens: model.context_length,
description: model.description,
displayName: model.name,
functionCall:
baseModel.functionCall ||
model.description.includes('function calling') ||
model.description.includes('tools') ||
extraInfo?.endpoint?.supports_tool_parameters ||
false,
maxTokens:
typeof model.top_provider.max_completion_tokens === 'number'
? model.top_provider.max_completion_tokens
: undefined,
pricing: {
input: formatPrice(model.pricing.prompt),
output: formatPrice(model.pricing.completion),
},
reasoning: baseModel.reasoning || extraInfo?.endpoint?.supports_reasoning || false,
releasedAt: new Date(model.created * 1000).toISOString().split('T')[0],
vision: baseModel.vision || model.architecture.modality.includes('image') || false,
};
})
.filter(Boolean) as ChatModelCard[];
},
provider: ModelProvider.OpenRouter,
});