diff --git a/apps/web/app/api/explorer/ask/route.ts b/apps/web/app/api/explorer/ask/route.ts index 64de2cf4b3..b998b91adc 100644 --- a/apps/web/app/api/explorer/ask/route.ts +++ b/apps/web/app/api/explorer/ask/route.ts @@ -6,6 +6,7 @@ import { normalizeExplorerChart, inferExplorerFields } from "@/lib/explorer/char import { explorerChartKinds, type ExplorerAnswer } from "@/lib/explorer/contracts"; import { buildExplorerPlanningPrompt } from "@/lib/explorer/prompt"; import { executeExplorerRows, explainExplorerRows } from "@/lib/explorer/query"; +import { createMinimaxModel, readMinimaxConfig } from "@/lib/explorer/provider"; export const runtime = "edge"; export const maxDuration = 30; @@ -53,6 +54,24 @@ const openai = createOpenAI({ apiKey: openaiApiKey, }); +const minimaxConfig = readMinimaxConfig(); + +function resolveExplorerModel() { + if (minimaxConfig) { + return createMinimaxModel(minimaxConfig); + } + return openai("gpt-5.2"); +} + +const explorerProviderOptions = minimaxConfig + ? undefined + : { + openai: { + reasoningEffort: "low" as const, + textVerbosity: "low" as const, + }, + }; + export async function POST(request: Request) { if (isExplorerUnderMaintenance()) { return Response.json(maintenanceResponse, { @@ -90,7 +109,7 @@ export async function POST(request: Request) { try { const body = requestSchema.parse(await request.json()); - if (!openaiApiKey) { + if (!minimaxConfig && !openaiApiKey) { return Response.json( { error: "Missing OPENAI_API_TOKEN (or OPENAI_API_KEY) for Data Explorer." }, { status: 500, headers: { "Cache-Control": "no-store" } }, @@ -125,18 +144,13 @@ export async function POST(request: Request) { async function generateExplorerPlan(question: string) { const { object: plan } = await generateObject({ - model: openai("gpt-5.2"), + model: resolveExplorerModel(), schemaName: "ExplorerPlan", schemaDescription: "A safe readonly SQL plan and chart plan for OSSInsight Data Explorer.", schema: planSchema, prompt: buildExplorerPlanningPrompt(question), - providerOptions: { - openai: { - reasoningEffort: "low", - textVerbosity: "low", - }, - }, + providerOptions: explorerProviderOptions, }); return plan; @@ -201,18 +215,13 @@ function streamExplorerAnswer(question: string, startedAt: number) { send({ type: "phase", phase: "generating" }); const result = streamObject({ - model: openai("gpt-5.2"), + model: resolveExplorerModel(), schemaName: "ExplorerPlan", schemaDescription: "A safe readonly SQL plan and chart plan for OSSInsight Data Explorer.", schema: planSchema, prompt: buildExplorerPlanningPrompt(question), - providerOptions: { - openai: { - reasoningEffort: "low", - textVerbosity: "low", - }, - }, + providerOptions: explorerProviderOptions, }); let lastPreview = ""; diff --git a/apps/web/lib/explorer/provider.ts b/apps/web/lib/explorer/provider.ts new file mode 100644 index 0000000000..a5a6dc5d21 --- /dev/null +++ b/apps/web/lib/explorer/provider.ts @@ -0,0 +1,84 @@ +import { createOpenAI } from "@ai-sdk/openai"; + +// Regional base URLs for the MiniMax OpenAI-compatible chat API. The Data +// Explorer can be pointed at either the global endpoint or the mainland-China +// endpoint through the MINIMAX_API_REGION environment variable. +export const MINIMAX_REGION_BASE_URLS = { + global_en: "https://api.minimax.io/v1", + cn_zh: "https://api.minimaxi.com/v1", +} as const; + +export type MinimaxRegion = keyof typeof MINIMAX_REGION_BASE_URLS; + +export const MINIMAX_DEFAULT_REGION: MinimaxRegion = "global_en"; + +// Supported MiniMax text models and their context windows (in tokens). +export const MINIMAX_MODELS = { + "MiniMax-M3": { contextWindow: 1_000_000 }, + "MiniMax-M2.7": { contextWindow: 204_800 }, +} as const; + +export type MinimaxModelId = keyof typeof MINIMAX_MODELS; + +export const MINIMAX_DEFAULT_MODEL: MinimaxModelId = "MiniMax-M3"; + +export function resolveMinimaxRegion( + value: string | undefined, +): MinimaxRegion { + return value && value in MINIMAX_REGION_BASE_URLS + ? (value as MinimaxRegion) + : MINIMAX_DEFAULT_REGION; +} + +export function resolveMinimaxModel( + value: string | undefined, +): MinimaxModelId { + return value && value in MINIMAX_MODELS + ? (value as MinimaxModelId) + : MINIMAX_DEFAULT_MODEL; +} + +export function resolveMinimaxBaseUrl( + region: MinimaxRegion, + override: string | undefined, +): string { + const url = override?.trim() || MINIMAX_REGION_BASE_URLS[region]; + return url.replace(/\/+$/, ""); +} + +export interface MinimaxConfig { + apiKey: string; + region: MinimaxRegion; + baseURL: string; + modelId: MinimaxModelId; +} + +// Read the MiniMax configuration from the environment. Returns null when no +// MiniMax API key is configured, so callers can keep their existing default +// provider behaviour untouched. +export function readMinimaxConfig( + env: Record = process.env, +): MinimaxConfig | null { + const apiKey = env.MINIMAX_API_KEY?.trim(); + if (!apiKey) { + return null; + } + + const region = resolveMinimaxRegion(env.MINIMAX_API_REGION); + return { + apiKey, + region, + baseURL: resolveMinimaxBaseUrl(region, env.MINIMAX_API_BASE_URL), + modelId: resolveMinimaxModel(env.MINIMAX_MODEL), + }; +} + +// Build a language model bound to the MiniMax OpenAI-compatible endpoint. +export function createMinimaxModel(config: MinimaxConfig) { + const provider = createOpenAI({ + name: "minimax", + apiKey: config.apiKey, + baseURL: config.baseURL, + }); + return provider.chat(config.modelId); +}