Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions apps/web/app/api/explorer/ask/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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" } },
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = "";
Expand Down
84 changes: 84 additions & 0 deletions apps/web/lib/explorer/provider.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined> = 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);
}