/** * MiniMax Coding Plan API client * * Provides web search via MiniMax API. * API keys resolved from auth.json or models.json via ModelRegistry. * Works with any active model — only requires minimax provider to be configured. */ import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { minimaxApiHost } from "./settings.ts"; // ── Types ───────────────────────────────────────────────────────────────── export interface MinimaxSearchResult { title: string; link: string; snippet: string; date?: string; } interface MinimaxSearchResponse { organic: MinimaxSearchResult[]; related_searches: { query: string }[]; base_resp: { status_code: number; status_msg: string; }; } // ── API key resolution ──────────────────────────────────────────────────── /** * Resolve MiniMax API key from ModelRegistry. * Works whether minimax is configured in auth.json or models.json. * Returns undefined if minimax provider is not configured. */ export async function resolveMinimaxApiKey( ctx: ExtensionContext, ): Promise { // getApiKeyForProvider resolves from both auth.json and models.json. const key = await ctx.modelRegistry.getApiKeyForProvider("minimax"); return key || process.env.MINIMAX_API_KEY?.trim() || undefined; } /** * Resolve MiniMax base URL. Uses the provider config from models.json if available, * otherwise falls back to the web.minimax.apiHost setting. */ export function resolveMinimaxBaseUrl(ctx: ExtensionContext): string { // Try to find a minimax model to get its baseUrl const allModels = ctx.modelRegistry.getAll(); const minimaxModel = allModels.find( (m) => m.provider === "minimax" || m.provider === "minimax-cn", ); if (minimaxModel?.baseUrl) { return minimaxModel.baseUrl; } return minimaxApiHost(ctx); } /** * Check if minimax provider is configured (has an API key). */ export async function isMinimaxAvailable( ctx: ExtensionContext, ): Promise { const key = await resolveMinimaxApiKey(ctx); return !!key; } // ── API client ──────────────────────────────────────────────────────────── function normalizeBaseUrl(baseUrl: string): string { // Remove trailing slash if (baseUrl.endsWith("/")) { baseUrl = baseUrl.slice(0, -1); } // The coding_plan endpoints are at /v1/coding_plan/... // Normalize to ensure /v1 prefix exists if (baseUrl.includes("/anthropic")) { return baseUrl.replace("/anthropic", "/v1"); } if (baseUrl.includes("/v1")) { return baseUrl; } return `${baseUrl}/v1`; } async function minimaxPost( baseUrl: string, apiKey: string, endpoint: string, body: Record, signal?: AbortSignal, ): Promise { const normalizedBase = normalizeBaseUrl(baseUrl); const url = `${normalizedBase}${endpoint}`; const response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "MM-API-Source": "pi-extension", }, body: JSON.stringify(body), signal: signal ?? AbortSignal.timeout(30_000), }); if (!response.ok) { throw new Error( `MiniMax API error: HTTP ${response.status} ${response.statusText}`, ); } const data = (await response.json()) as T; // Check API-level errors const baseResp = (data as Record).base_resp as | { status_code: number; status_msg: string } | undefined; if (baseResp && baseResp.status_code !== 0) { if (baseResp.status_code === 1004) { throw new Error( `MiniMax API: ${baseResp.status_msg}. Check your API key and API host.`, ); } throw new Error( `MiniMax API error: ${baseResp.status_code} - ${baseResp.status_msg}`, ); } return data; } // ── Web search ──────────────────────────────────────────────────────────── export async function minimaxSearch( query: string, ctx: ExtensionContext, signal?: AbortSignal, ): Promise { const apiKey = await resolveMinimaxApiKey(ctx); if (!apiKey) { throw new Error( "MiniMax provider not configured. Add API key to auth.json or models.json.", ); } const baseUrl = resolveMinimaxBaseUrl(ctx); return minimaxPost( baseUrl, apiKey, "/coding_plan/search", { q: query }, signal, ); }