mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
LLMs: OpenRouter: auto-detection of capabilities (i/o modalities, features, etc). Thanks OpenRouter, you're the best!
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Reasoning, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
import { LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json, LLM_IF_OAI_PromptCaching, LLM_IF_OAI_Reasoning, LLM_IF_OAI_Vision, LLM_IF_Outputs_Audio, LLM_IF_Outputs_Image } from '~/common/stores/llms/llms.types';
|
||||
|
||||
import type { ModelDescriptionSchema } from '../../llm.server.types';
|
||||
import { fromManualMapping } from './models.data';
|
||||
@@ -10,24 +12,13 @@ import { wireOpenrouterModelsListOutputSchema } from '../openrouter.wiretypes';
|
||||
// - models list API: https://openrouter.ai/docs/models
|
||||
|
||||
|
||||
// [EDITORIAL] - OpenRouter Anthropic thinking models - add a "reasoning" variant for these models
|
||||
const antThinkingModels: string[] = [
|
||||
'anthropic/claude-opus-4.1',
|
||||
'anthropic/claude-opus-4',
|
||||
'anthropic/claude-sonnet-4.5',
|
||||
'anthropic/claude-sonnet-4',
|
||||
'anthropic/claude-haiku-4.5',
|
||||
'anthropic/claude-3-7-sonnet',
|
||||
] as const;
|
||||
|
||||
|
||||
const orModelFamilyOrder = [
|
||||
// Leading models/organizations (based on capabilities and popularity)
|
||||
'anthropic/', 'deepseek/', 'google/', 'openai/', 'x-ai/',
|
||||
// Other major providers
|
||||
'mistralai/', 'meta-llama/', 'amazon/', 'cohere/',
|
||||
// Specialized/AI companies
|
||||
'perplexity/', 'phind/', 'qwen/', 'inflection/',
|
||||
'moonshotai/', 'perplexity/', 'qwen/', 'inflection/',
|
||||
// Research/open models
|
||||
'nvidia/', 'microsoft/', 'nousresearch/', 'openchat/', // 'huggingfaceh4/',
|
||||
// Community/other providers
|
||||
@@ -64,48 +55,115 @@ export function openRouterModelToModelDescription(wireModel: object): ModelDescr
|
||||
// parse the model
|
||||
const { data: model, error } = wireOpenrouterModelsListOutputSchema.safeParse(wireModel);
|
||||
if (error) {
|
||||
console.warn(`openRouterModelToModelDescription: Failed to parse model data`, { error });
|
||||
console.warn('[DEV] openRouterModelToModelDescription: parser fail', z.prettifyError(error), wireModel);
|
||||
return null;
|
||||
}
|
||||
|
||||
// parse pricing
|
||||
|
||||
// -- Label --
|
||||
|
||||
let label = model.name || model.id.replace('/', ' · ');
|
||||
|
||||
|
||||
// -- Pricing --
|
||||
|
||||
const inputPrice = parseFloat(model.pricing.prompt);
|
||||
const outputPrice = parseFloat(model.pricing.completion);
|
||||
const cacheWritePrice = model.pricing.input_cache_write ? parseFloat(model.pricing.input_cache_write) : undefined;
|
||||
const cacheReadPrice = model.pricing.input_cache_read ? parseFloat(model.pricing.input_cache_read) : undefined;
|
||||
|
||||
const chatPrice: ModelDescriptionSchema['chatPrice'] = {
|
||||
input: inputPrice ? inputPrice * 1000 * 1000 : 'free',
|
||||
output: outputPrice ? outputPrice * 1000 * 1000 : 'free',
|
||||
// image...
|
||||
// request...
|
||||
};
|
||||
const seemsFree = chatPrice.input === 'free' && chatPrice.output === 'free';
|
||||
|
||||
// openrouter provides the fields we need as part of the model object
|
||||
let label = model.name || model.id.replace('/', ' · ');
|
||||
if (cacheWritePrice && cacheReadPrice) {
|
||||
// if writing, assume anthropic-style
|
||||
chatPrice.cache = {
|
||||
cType: 'ant-bp',
|
||||
read: cacheReadPrice * 1000 * 1000,
|
||||
write: cacheWritePrice * 1000 * 1000,
|
||||
duration: 300, // 5 minutes default
|
||||
};
|
||||
} else if (cacheReadPrice) {
|
||||
// if only reading, assume openai-style
|
||||
chatPrice.cache = {
|
||||
cType: 'oai-ac',
|
||||
read: cacheReadPrice * 1000 * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
// -- Pricing: free --
|
||||
const seemsFree = chatPrice.input === 'free' && chatPrice.output === 'free';
|
||||
if (seemsFree)
|
||||
label += ' · 🎁'; // Free? Discounted?
|
||||
// label = label.replace('(self-moderated)', '🔓');
|
||||
|
||||
|
||||
// -- Interfaces --
|
||||
const interfaces = [LLM_IF_OAI_Chat];
|
||||
|
||||
// input: vision
|
||||
if (model.architecture?.input_modalities?.includes('image'))
|
||||
interfaces.push(LLM_IF_OAI_Vision);
|
||||
|
||||
// output: image
|
||||
if (model.architecture?.output_modalities?.includes('image'))
|
||||
interfaces.push(LLM_IF_Outputs_Image);
|
||||
// output: audio
|
||||
if (model.architecture?.output_modalities?.includes('audio'))
|
||||
interfaces.push(LLM_IF_Outputs_Audio);
|
||||
|
||||
// FC
|
||||
if (model.supported_parameters?.includes('tools'))
|
||||
interfaces.push(LLM_IF_OAI_Fn);
|
||||
// Json
|
||||
if (model.supported_parameters?.includes('response_format') || model.supported_parameters?.includes('structured_outputs'))
|
||||
interfaces.push(LLM_IF_OAI_Json);
|
||||
// Reasoning
|
||||
if (model.supported_parameters?.includes('reasoning'))
|
||||
interfaces.push(LLM_IF_OAI_Reasoning);
|
||||
|
||||
// Prompt caching support: check pricing fields
|
||||
if (model.pricing?.input_cache_read !== undefined || model.pricing?.input_cache_write !== undefined)
|
||||
interfaces.push(LLM_IF_OAI_PromptCaching);
|
||||
|
||||
|
||||
// -- Parameters --
|
||||
|
||||
const parameterSpecs: ModelDescriptionSchema['parameterSpecs'] = [
|
||||
{ paramId: 'llmVndOrtWebSearch' }, // OpenRouter web search is available for all models
|
||||
];
|
||||
|
||||
if (model.id.startsWith('anthropic/') && interfaces.includes(LLM_IF_OAI_Reasoning))
|
||||
parameterSpecs.push({ paramId: 'llmVndAntThinkingBudget', initialValue: null });
|
||||
|
||||
if (model.id.startsWith('google/') && interfaces.includes(LLM_IF_OAI_Reasoning))
|
||||
parameterSpecs.push({ paramId: 'llmVndGeminiThinkingBudget' });
|
||||
|
||||
if (model.id.startsWith('openai/') && interfaces.includes(LLM_IF_OAI_Reasoning))
|
||||
parameterSpecs.push({ paramId: 'llmVndOaiReasoningEffort' });
|
||||
|
||||
|
||||
// -- Hidden --
|
||||
|
||||
// hidden: hide by default older models or models not in known families; match with startsWith for both orOldModelIDs and orModelFamilyOrder
|
||||
const hidden = orOldModelIDs.some(prefix => model.id.startsWith(prefix))
|
||||
|| !orModelFamilyOrder.some(prefix => model.id.startsWith(prefix));
|
||||
|
||||
return fromManualMapping([], model.id, undefined, undefined, {
|
||||
|
||||
return fromManualMapping([], model.id, model?.created, undefined, {
|
||||
idPrefix: model.id,
|
||||
// latest: ...
|
||||
label,
|
||||
// created: ...
|
||||
// updated: ...
|
||||
description: model.description,
|
||||
description: model.description?.length > 280 ? model.description.slice(0, 277) + '...' : model.description,
|
||||
contextWindow: model.context_length || 4096,
|
||||
maxCompletionTokens: model.top_provider.max_completion_tokens || undefined,
|
||||
// trainingDataCutoff: ...
|
||||
interfaces: [LLM_IF_OAI_Chat],
|
||||
interfaces,
|
||||
// benchmark: ...
|
||||
chatPrice,
|
||||
hidden,
|
||||
parameterSpecs: [
|
||||
{ paramId: 'llmVndOrtWebSearch' }, // [OpenRouter, 2025-10-22] OpenRouter web search is available for all models
|
||||
],
|
||||
parameterSpecs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,20 +172,23 @@ export function openRouterInjectVariants(models: ModelDescriptionSchema[], model
|
||||
models.push(model);
|
||||
|
||||
// inject thinking variants for Anthropic thinking models
|
||||
if (antThinkingModels.includes(model.id)) {
|
||||
// NOTE: we flipped the logic of some thinking/non-thinking models
|
||||
if (model.id.includes('anthropic/') && model.interfaces.includes(LLM_IF_OAI_Reasoning)) {
|
||||
|
||||
// create a thinking variant for the model, by setting 'idVariant' and modifying the label/description
|
||||
const thinkingVariant: ModelDescriptionSchema = {
|
||||
...model,
|
||||
idVariant: 'thinking',
|
||||
label: `${model.label} (thinking)`,
|
||||
description: `(extended thinking mode) ${model.description}`,
|
||||
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_OAI_Reasoning],
|
||||
label: `${model.label.replace(' (thinking)', '')} (thinking)`,
|
||||
description: `(configurable thinking) ${model.description}`,
|
||||
interfaces: model.interfaces.filter(i => i !== LLM_IF_OAI_Reasoning),
|
||||
// this is what makes it a thinking variant
|
||||
parameterSpecs: [
|
||||
...(model.parameterSpecs || []),
|
||||
{ paramId: 'llmVndAntThinkingBudget', initialValue: 1024 },
|
||||
],
|
||||
parameterSpecs: model.parameterSpecs?.map(param =>
|
||||
param.paramId !== 'llmVndAntThinkingBudget' ? param : {
|
||||
...param,
|
||||
initialValue: 8192,
|
||||
// initialValue: null, // disable thinking
|
||||
}),
|
||||
};
|
||||
|
||||
models.push(thinkingVariant);
|
||||
|
||||
Reference in New Issue
Block a user