Luigit
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,
	);
}