/** * Resolves where a model call should go: the server's Ollama (as before) or a * cloud provider the browser sent a key for. Shared by the model routes so * every flow (chat, decks, homework, grading) can run on either backend. */ import { authErrorMessage, authErrorKind, resolveAuth, type OllamaAuth, type ServerConfig } from "./config"; import { sanitiseApiKey } from "./http-guard"; import { PROVIDER_BASE_HEADER, PROVIDER_HEADER, PROVIDER_KEY_HEADER, providerInfo } from "./providers"; import type { ProviderTarget } from "./openai-compat"; import type { Locale } from "./i18n-dict"; export type ModelTarget = | { kind: "ollama"; auth: OllamaAuth } | { kind: "provider"; provider: string; label: string; target: ProviderTarget }; export type TargetResolution = | { target: ModelTarget } | { error: { message: string; kind: string; status: number } }; /** * Inert credentials for provider runs: the ai.ts helpers still take an * `auth`, but it is never used once a provider target is set. */ export const INERT_OLLAMA_AUTH: OllamaAuth = { baseUrl: "", apiKey: "", directCloud: false, authMode: "local" }; function safeBase(raw: string): string | null { const value = raw.trim().slice(0, 300); if (!value) return null; try { const url = new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") return null; if (!url.hostname) return null; return value.replace(/\/+$/, ""); } catch { return null; } } export function resolveModelTarget(config: ServerConfig, request: Request, locale: Locale): TargetResolution { const requested = (request.headers.get(PROVIDER_HEADER) ?? "ollama").trim().toLowerCase(); if (!requested || requested === "ollama") { const auth = resolveAuth(config, request); if ("error" in auth) { const kind = authErrorKind(auth.error); return { error: { message: authErrorMessage(locale, auth.error), kind, status: kind === "needs_key" ? 401 : 500 } }; } return { target: { kind: "ollama", auth: auth.auth } }; } const info = providerInfo(requested); if (!info) { return { error: { message: `Unknown model provider "${requested}".`, kind: "bad_provider", status: 400 } }; } // A browser-saved key is the point of providers: the server never holds it. const apiKey = sanitiseApiKey(request.headers.get(PROVIDER_KEY_HEADER) ?? ""); if (!apiKey) { return { error: { message: `Add your ${info.label} API key in Settings → Providers.`, kind: "needs_key", status: 401, }, }; } const baseUrl = requested === "custom" ? safeBase(request.headers.get(PROVIDER_BASE_HEADER) ?? "") : info.baseUrl; if (!baseUrl) { return { error: { message: `Add the base URL for your custom OpenAI-compatible endpoint in Settings → Providers.`, kind: "bad_provider", status: 400, }, }; } return { target: { kind: "provider", provider: requested, label: info.label, target: { baseUrl, apiKey, label: info.label }, }, }; }