mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
Step 2 of the provider refactor plan. lib/providers.ts holds one record per provider (SDK kind, credential kind, env prefix, default URL, picker group, notes and base URL hints in four languages, and the short model list of the six providers that cannot list models). ProviderName is now the record's key set and lib/types/model-config.ts re-exports the record as PROVIDER_INFO, so the twelve importers are unchanged. Derived from the record, under their old names and in their old files: FIXED_CRED_PROVIDERS, PROVIDER_LOGO_MAP and chatRequestUrl's own-path list (model-config.ts), SINGLE_SYSTEM_PROVIDERS, PROVIDER_ENV_VARS and SDK_KNOWS_ENDPOINT (ai-providers.ts), PROVIDER_GROUPS and OPTIONS_OPEN (provider-setup.ts), NO_KEY_NEEDED (provider-models route). tests/unit/providers.test.ts freezes the old tables' values and checks the derived ones against them. The record is written in the picker's order, so the provider lists in two 'No AI provider configured' messages change order; those two snapshot rows are the only snapshot change.
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
import { NextResponse } from "next/server"
|
|
import { checkAccessCode, rejectCrossSite } from "@/lib/access-code"
|
|
import { classifyLLMError } from "@/lib/llm-errors"
|
|
import {
|
|
canListModels,
|
|
listProviderModels,
|
|
ModelListError,
|
|
} from "@/lib/provider-models"
|
|
import { PROVIDER_NAMES, PROVIDERS, type ProviderName } from "@/lib/providers"
|
|
import {
|
|
allowPrivateUrls,
|
|
isPrivateUrl,
|
|
RedirectRefusedError,
|
|
redirectGuardedFetch,
|
|
} from "@/lib/ssrf-protection"
|
|
|
|
export const runtime = "nodejs"
|
|
|
|
// Public lists need no key
|
|
const NO_KEY_NEEDED = new Set<ProviderName>(
|
|
PROVIDER_NAMES.filter((p) => PROVIDERS[p].listNeedsNoKey),
|
|
)
|
|
|
|
/**
|
|
* The models a provider offers, for the "Fetch models" button in model
|
|
* settings. Answers { models: null } for providers that cannot list them,
|
|
* so the dialog keeps its suggested models.
|
|
*/
|
|
export async function POST(req: Request) {
|
|
const crossSite = rejectCrossSite(req)
|
|
if (crossSite) return crossSite
|
|
// Sends requests to a URL the client chose, so require the access code
|
|
const accessError = checkAccessCode(req)
|
|
if (accessError) return accessError
|
|
|
|
const { provider, apiKey, baseUrl } = (await req.json()) as {
|
|
provider: ProviderName
|
|
apiKey?: string
|
|
baseUrl?: string
|
|
}
|
|
if (!canListModels(provider)) {
|
|
return NextResponse.json({ models: null })
|
|
}
|
|
// SECURITY: Block SSRF attacks via custom baseUrl
|
|
if (baseUrl && !allowPrivateUrls() && (await isPrivateUrl(baseUrl))) {
|
|
return NextResponse.json({ error: "Invalid base URL" }, { status: 400 })
|
|
}
|
|
if (!apiKey && !NO_KEY_NEEDED.has(provider)) {
|
|
return NextResponse.json(
|
|
{ error: "API key is required" },
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
|
|
try {
|
|
const models = await listProviderModels(
|
|
provider,
|
|
{ apiKey, baseUrl },
|
|
(baseUrl && redirectGuardedFetch()) || fetch,
|
|
)
|
|
return NextResponse.json({ models })
|
|
} catch (error) {
|
|
console.warn("[provider-models] Listing failed:", error)
|
|
// Only our own explanations go back: the URL may be an internal
|
|
// address, whose answer or host names must not reach the caller.
|
|
// The Gateway SDK wraps them, keeping ours as the cause.
|
|
const isOwn = (e: unknown): e is Error =>
|
|
e instanceof ModelListError || e instanceof RedirectRefusedError
|
|
const cause = (error as { cause?: unknown })?.cause
|
|
const own = isOwn(error) ? error : isOwn(cause) ? cause : null
|
|
const { code } = classifyLLMError(own ?? error)
|
|
return NextResponse.json({
|
|
code,
|
|
error: own?.message ?? "The model list request failed.",
|
|
})
|
|
}
|
|
}
|