@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
135 lines (114 loc) • 4.6 kB
text/typescript
import { ModelProvider, openrouter as OpenRouterModels } from 'model-bank';
import {
OpenAICompatibleFactoryOptions,
createOpenAICompatibleRuntime,
} from '../../core/openaiCompatibleFactory';
import { processMultiProviderModelList } from '../../utils/modelParse';
import { OpenRouterModelCard, OpenRouterReasoning } from './type';
const formatPrice = (price?: string) => {
if (price === undefined || price === '-1') return undefined;
return Number((Number(price) * 1e6).toPrecision(5));
};
export const params = {
baseURL: 'https://openrouter.ai/api/v1',
chatCompletion: {
handlePayload: (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
? Math.min(thinking.budget_tokens, maxTokens - 1)
: 1024,
};
}
return {
...payload,
model: payload.enabledSearch ? `${payload.model}:online` : payload.model,
reasoning,
stream: payload.stream ?? true,
} as any;
},
},
constructorOptions: {
defaultHeaders: {
'HTTP-Referer': 'https://lobehub.com',
'X-Title': 'LobeHub',
},
},
debug: {
chatCompletion: () => process.env.DEBUG_OPENROUTER_CHAT_COMPLETION === '1',
},
models: async () => {
let modelList: OpenRouterModelCard[] = [];
try {
const response = await fetch('https://openrouter.ai/api/v1/models');
if (response.ok) {
const data = await response.json();
modelList = data['data'];
}
} catch (error) {
console.error('Failed to fetch OpenRouter frontend models:', error);
return [];
}
// 处理前端获取的模型信息,转换为标准格式
const formattedModels = modelList.map((model) => {
const { top_provider, architecture, pricing, supported_parameters } = model;
const inputModalities = architecture.input_modalities || [];
// 处理 name,默认去除冒号及其前面的内容
let displayName = model.name;
const colonIndex = displayName.indexOf(':');
if (colonIndex !== -1) {
const prefix = displayName.slice(0, Math.max(0, colonIndex)).trim();
const suffix = displayName.slice(Math.max(0, colonIndex + 1)).trim();
const isDeepSeekPrefix = prefix.toLowerCase() === 'deepseek';
const suffixHasDeepSeek = suffix.toLowerCase().includes('deepseek');
if (isDeepSeekPrefix && !suffixHasDeepSeek) {
displayName = model.name;
} else {
displayName = suffix;
}
}
const inputPrice = formatPrice(pricing.prompt);
const outputPrice = formatPrice(pricing.completion);
const cachedInputPrice = formatPrice(pricing.input_cache_read);
const writeCacheInputPrice = formatPrice(pricing.input_cache_write);
const isFree = (inputPrice === 0 || outputPrice === 0) && !displayName.endsWith('(free)');
if (isFree) {
displayName += ' (free)';
}
return {
contextWindowTokens: top_provider.context_length || model.context_length,
description: model.description,
displayName,
functionCall: supported_parameters.includes('tools'),
id: model.id,
maxOutput:
typeof top_provider.max_completion_tokens === 'number'
? top_provider.max_completion_tokens
: undefined,
pricing: {
cachedInput: cachedInputPrice,
input: inputPrice,
output: outputPrice,
writeCacheInput: writeCacheInputPrice,
},
reasoning: supported_parameters.includes('reasoning'),
releasedAt: new Date(model.created * 1000).toISOString().split('T')[0],
vision: inputModalities.includes('image'),
};
});
return await processMultiProviderModelList(formattedModels, 'openrouter');
},
provider: ModelProvider.OpenRouter,
} satisfies OpenAICompatibleFactoryOptions;
export const LobeOpenRouterAI = createOpenAICompatibleRuntime(params);