import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { GatewayFetchError } from '~~/server/utils/ai-provider'; import { Err, Ok } from '~~/types/result'; import { SupportedModalities } from '~/types/model'; import type { ProviderModule } from '.'; import { lshDecimal } from './helpers'; export default { id: 'openrouter', baseUrl: 'https://openrouter.ai/api/v1', modelsEndpoint: '/models', createGateway({ apiKey }) { if (apiKey === undefined) { return Err(GatewayFetchError.NoProviderApiKey); } return Ok({ gateway: createOpenRouter({ apiKey, headers: { 'HTTP-Referer': 'https://localhost:3000', 'X-Title': 'Veridian', }, }), streamTransformer: undefined, textTransformer: undefined, }); }, getAuthHeaders(apiKey) { return apiKey ? { Authorization: `Bearer ${apiKey}` } : {}; }, async fetchModels({ baseURL, apiKey }) { const res = await fetch(`${baseURL}/models`, { method: 'GET', headers: { Authorization: `Bearer ${apiKey}` }, }); const data = await res.json(); if (!res.ok) { throw createError({ statusCode: res.status, message: JSON.stringify(data) }); } const models = []; for (const model of data.data) { const capabilities = new Set(); for (const capability of model.supported_parameters) { switch (capability) { case 'reasoning': capabilities.add('reasoning'); break; case 'tools': capabilities.add('tools'); break; } } const pricing = {} as Record; for (const key of Object.keys(model.pricing)) { switch (key) { case 'prompt': case 'completion': case 'image': case 'audio': case 'discount': pricing[key] = lshDecimal(model.pricing[key], 6); break; case 'request': pricing['request'] = lshDecimal(model.pricing[key], 3); break; case 'image_tokens': pricing['imageTokens'] = lshDecimal(model.pricing[key], 6); break; case 'image_output': pricing['imageOutput'] = lshDecimal(model.pricing[key], 6); break; case 'audio_output': pricing['audioOutput'] = lshDecimal(model.pricing[key], 6); break; case 'input_audio_cache': pricing['inputAudioCache'] = lshDecimal(model.pricing[key], 6); break; case 'web_search': pricing['webSearch'] = lshDecimal(model.pricing[key], 3); break; case 'internal_reasoning': pricing['internalReasoning'] = lshDecimal(model.pricing[key], 6); break; case 'input_cache_read': pricing['inputCacheRead'] = lshDecimal(model.pricing[key], 6); break; case 'input_cache_write': pricing['inputCacheWrite'] = lshDecimal(model.pricing[key], 6); break; } } models.push({ id: model.id as string, name: model.name as string, cost: pricing, attributes: { inputModalities: model.architecture.input_modalities.filter((modality: string) => (SupportedModalities as Readonly).includes(modality)), outputModalities: model.architecture.output_modalities.filter((modality: string) => (SupportedModalities as Readonly).includes(modality)), capabilities: Array.from(capabilities), contextWindow: model.context_length, supported_parameters: model.supported_parameters, }, releasedAt: model.created * 1000, }); } return models; }, } satisfies ProviderModule;