import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' const logger = createLogger('OpenRouterUtils') interface OpenRouterModelData { id: string supported_parameters?: string[] } interface ModelCapabilities { supportsStructuredOutputs: boolean supportsTools: boolean } let modelCapabilitiesCache: Map | null = null let cacheTimestamp = 0 const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes async function fetchModelCapabilities(): Promise> { try { const response = await fetch('https://openrouter.ai/api/v1/models', { headers: { 'Content-Type': 'application/json' }, }) if (!response.ok) { await response.text().catch(() => {}) logger.warn('Failed to fetch OpenRouter model capabilities', { status: response.status, }) return new Map() } const data = await response.json() const capabilities = new Map() for (const model of (data.data ?? []) as OpenRouterModelData[]) { const supportedParams = model.supported_parameters ?? [] capabilities.set(model.id, { supportsStructuredOutputs: supportedParams.includes('structured_outputs'), supportsTools: supportedParams.includes('tools'), }) } logger.info('Cached OpenRouter model capabilities', { modelCount: capabilities.size, withStructuredOutputs: Array.from(capabilities.values()).filter( (c) => c.supportsStructuredOutputs ).length, }) return capabilities } catch (error) { logger.error('Error fetching OpenRouter model capabilities', { error: toError(error).message, }) return new Map() } } /** * Gets capabilities for a specific OpenRouter model. * Fetches from API if cache is stale or empty. */ export async function getOpenRouterModelCapabilities( modelId: string ): Promise { const now = Date.now() if (!modelCapabilitiesCache || now - cacheTimestamp > CACHE_TTL_MS) { modelCapabilitiesCache = await fetchModelCapabilities() cacheTimestamp = now } const normalizedId = modelId.replace(/^openrouter\//, '') return modelCapabilitiesCache.get(normalizedId) ?? null } export async function supportsNativeStructuredOutputs(modelId: string): Promise { const capabilities = await getOpenRouterModelCapabilities(modelId) return capabilities?.supportsStructuredOutputs ?? false } export function createReadableStreamFromOpenAIStream( openaiStream: AsyncIterable, onComplete?: (content: string, usage: CompletionUsage) => void ): ReadableStream { return createOpenAICompatibleStream(openaiStream, 'OpenRouter', onComplete) } export function checkForForcedToolUsage( response: any, toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, forcedTools: string[], usedForcedTools: string[] ): { hasUsedForcedTool: boolean; usedForcedTools: string[] } { return checkForForcedToolUsageOpenAI( response, toolChoice, 'OpenRouter', forcedTools, usedForcedTools ) }