/** * Shared model resolution utilities for API routes. * * Extracts the repeated parseModelString → resolveApiKey → resolveBaseUrl → * resolveProxy → getModel boilerplate into a single call. */ import type { NextRequest } from 'next/server'; import { getModel, parseModelString, type ModelWithInfo } from '@/lib/ai/providers'; import type { ThinkingConfig } from '@/lib/types/provider'; import { resolveApiKey, resolveBaseUrl, resolveProxy } from '@/lib/server/provider-config'; import { validateUrlForSSRF } from '@/lib/server/ssrf-guard'; export interface ResolvedModel extends ModelWithInfo { /** Original model string (e.g. "openai/gpt-4o-mini") */ modelString: string; /** Resolved provider ID (e.g. "openai", "ollama") */ providerId: string; /** Effective API key after server-side fallback resolution */ apiKey: string; /** Optional per-request thinking configuration from the client. */ thinkingConfig?: ThinkingConfig; } /** * Resolve a language model from explicit parameters. * * Use this when model config comes from the request body. */ export async function resolveModel(params: { modelString?: string; apiKey?: string; baseUrl?: string; providerType?: string; thinkingConfig?: ThinkingConfig; }): Promise { const modelString = params.modelString || process.env.DEFAULT_MODEL || 'gpt-5.4-mini'; const { providerId, modelId } = parseModelString(modelString); // SSRF validation applies only to client-supplied base URLs. // Server-configured URLs (e.g. OLLAMA_BASE_URL from env/YAML) flow through // resolveBaseUrl() and bypass this check — they're trusted by the operator. const clientBaseUrl = params.baseUrl || undefined; if (clientBaseUrl && process.env.NODE_ENV === 'production') { const ssrfError = await validateUrlForSSRF(clientBaseUrl); if (ssrfError) { throw new Error(ssrfError); } } const apiKey = clientBaseUrl ? params.apiKey || '' : resolveApiKey(providerId, params.apiKey || ''); const baseUrl = clientBaseUrl ? clientBaseUrl : resolveBaseUrl(providerId, params.baseUrl); const proxy = resolveProxy(providerId); const { model, modelInfo } = getModel({ providerId, modelId, apiKey, baseUrl, proxy, providerType: params.providerType as 'openai' | 'anthropic' | 'google' | undefined, }); return { model, modelInfo, modelString, providerId, apiKey, thinkingConfig: params.thinkingConfig, }; } function getThinkingConfigFromBody(body: unknown): ThinkingConfig | undefined { if (!body || typeof body !== 'object') return undefined; const record = body as { thinkingConfig?: unknown; thinking?: unknown }; const config = record.thinkingConfig ?? record.thinking; return config && typeof config === 'object' ? (config as ThinkingConfig) : undefined; } /** * Resolve a language model from standard request headers. * * Reads: x-model, x-api-key, x-base-url, x-provider-type * Note: requiresApiKey is derived server-side from the provider registry, * never from client headers, to prevent auth bypass. */ export async function resolveModelFromHeaders(req: NextRequest): Promise { return resolveModel({ modelString: req.headers.get('x-model') || undefined, apiKey: req.headers.get('x-api-key') || undefined, baseUrl: req.headers.get('x-base-url') || undefined, providerType: req.headers.get('x-provider-type') || undefined, }); } /** * Resolve a language model from standard request headers plus body fields. * * Reads model credentials from headers and per-request thinking config from * the JSON body field `thinkingConfig` (or legacy/eval field `thinking`). */ export async function resolveModelFromRequest( req: NextRequest, body: unknown, ): Promise { const resolved = await resolveModelFromHeaders(req); return { ...resolved, thinkingConfig: getThinkingConfigFromBody(body) ?? resolved.thinkingConfig, }; }