repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/web/minimax.ts
Raw/**
* 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<string | undefined> {
// 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<boolean> {
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<T>(
baseUrl: string,
apiKey: string,
endpoint: string,
body: Record<string, unknown>,
signal?: AbortSignal,
): Promise<T> {
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<string, unknown>).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<MinimaxSearchResponse> {
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<MinimaxSearchResponse>(
baseUrl,
apiKey,
"/coding_plan/search",
{ q: query },
signal,
);
}