OpenMAIC-React / src /lib /server /resolve-model.ts
muthuk1's picture
Convert OpenMAIC from Next.js to React (Vite)
f56a29b verified
raw
history blame
3.99 kB
/**
* 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<ResolvedModel> {
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<ResolvedModel> {
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<ResolvedModel> {
const resolved = await resolveModelFromHeaders(req);
return {
...resolved,
thinkingConfig: getThinkingConfigFromBody(body) ?? resolved.thinkingConfig,
};
}