From 83cdccd930eb902cee358d366c1a5f49154605d6 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Mon, 26 Jan 2026 13:24:47 -0800 Subject: [PATCH] feat: add MiniMax coding plans support to /connect (#684) Co-authored-by: Letta --- src/cli/commands/connect.ts | 159 +++++++++++++++++++++++++++++- src/providers/byok-providers.ts | 7 ++ src/providers/minimax-provider.ts | 150 ++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 src/providers/minimax-provider.ts diff --git a/src/cli/commands/connect.ts b/src/cli/commands/connect.ts index 573a2e4..6f40909 100644 --- a/src/cli/commands/connect.ts +++ b/src/cli/commands/connect.ts @@ -8,6 +8,12 @@ import { startLocalOAuthServer, startOpenAIOAuth, } from "../../auth/openai-oauth"; +import { + createOrUpdateMinimaxProvider, + getMinimaxProvider, + MINIMAX_PROVIDER_NAME, + removeMinimaxProvider, +} from "../../providers/minimax-provider"; import { checkOpenAICodexEligibility, createOrUpdateOpenAICodexProvider, @@ -110,18 +116,18 @@ export async function handleConnect( ctx.buffersRef, ctx.refreshDerived, msg, - "Usage: /connect [options]\n\nAvailable providers:\n \u2022 codex - Connect via OAuth to authenticate with ChatGPT Plus/Pro\n \u2022 zai - Connect to Zai with your API key", + "Usage: /connect [options]\n\nAvailable providers:\n \u2022 codex - Connect via OAuth to authenticate with ChatGPT Plus/Pro\n \u2022 zai - Connect to zAI with your API key\n \u2022 minimax - Connect to MiniMax with your API key", false, ); return; } - if (provider !== "codex" && provider !== "zai") { + if (provider !== "codex" && provider !== "zai" && provider !== "minimax") { addCommandResult( ctx.buffersRef, ctx.refreshDerived, msg, - `Error: Unknown provider "${provider}"\n\nAvailable providers: codex, zai\nUsage: /connect [options]`, + `Error: Unknown provider "${provider}"\n\nAvailable providers: codex, zai, minimax\nUsage: /connect [options]`, false, ); return; @@ -133,6 +139,12 @@ export async function handleConnect( return; } + // MiniMax is handled separately in App.tsx, but add a fallback just in case + if (provider === "minimax") { + await handleConnectMinimax(ctx, msg); + return; + } + // Handle /connect codex await handleConnectCodex(ctx, msg); } @@ -372,6 +384,137 @@ async function handleConnectCodex( } } +/** + * Handle /disconnect minimax + */ +async function handleDisconnectMinimax( + ctx: ConnectCommandContext, + msg: string, +): Promise { + // Check if MiniMax provider exists + const existing = await getMinimaxProvider(); + if (!existing) { + addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + "Not currently connected to MiniMax.\n\nUse /connect minimax to connect.", + false, + ); + return; + } + + // Show running status + const cmdId = addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + "Disconnecting from MiniMax...", + true, + "running", + ); + + ctx.setCommandRunning(true); + + try { + // Remove provider from Letta + await removeMinimaxProvider(); + + updateCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + cmdId, + msg, + `\u2713 Disconnected from MiniMax.\n\n` + + `Provider '${MINIMAX_PROVIDER_NAME}' removed from Letta.`, + true, + "finished", + ); + } catch (error) { + updateCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + cmdId, + msg, + `\u2717 Failed to disconnect from MiniMax: ${getErrorMessage(error)}`, + false, + "finished", + ); + } finally { + ctx.setCommandRunning(false); + } +} + +/** + * Handle /connect minimax command + * Usage: /connect minimax + * + * Creates the minimax-coding-plan provider with the provided API key + */ +export async function handleConnectMinimax( + ctx: ConnectCommandContext, + msg: string, +): Promise { + const parts = msg.trim().split(/\s+/); + // Join all remaining parts in case the API key got split + const apiKey = parts.slice(2).join(""); + + // If no API key provided, show usage + if (!apiKey || apiKey.length === 0) { + addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + "Usage: /connect minimax \n\n" + + "Connect to MiniMax by providing your API key.\n\n" + + "Example: /connect minimax ...", + false, + ); + return; + } + + // Show running status + const cmdId = addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + "Creating MiniMax coding plan provider...", + true, + "running", + ); + + ctx.setCommandRunning(true); + + try { + // Create or update the MiniMax provider with the API key + await createOrUpdateMinimaxProvider(apiKey); + + updateCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + cmdId, + msg, + `\u2713 Successfully connected to MiniMax!\n\n` + + `Provider '${MINIMAX_PROVIDER_NAME}' created in Letta.\n\n` + + `The models are populated in /model \u2192 "All Available Models"`, + true, + "finished", + ); + } catch (error) { + updateCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + cmdId, + msg, + `\u2717 Failed to create MiniMax provider: ${getErrorMessage(error)}`, + false, + "finished", + ); + } finally { + ctx.setCommandRunning(false); + } +} + /** * Handle /disconnect command * Usage: /disconnect @@ -389,7 +532,7 @@ export async function handleDisconnect( ctx.buffersRef, ctx.refreshDerived, msg, - "Usage: /disconnect \n\nAvailable providers: codex, claude, zai", + "Usage: /disconnect \n\nAvailable providers: codex, claude, zai, minimax", false, ); return; @@ -401,6 +544,12 @@ export async function handleDisconnect( return; } + // Handle /disconnect minimax + if (provider === "minimax") { + await handleDisconnectMinimax(ctx, msg); + return; + } + // Handle /disconnect codex if (provider === "codex") { await handleDisconnectCodex(ctx, msg); @@ -418,7 +567,7 @@ export async function handleDisconnect( ctx.buffersRef, ctx.refreshDerived, msg, - `Error: Unknown provider "${provider}"\n\nAvailable providers: codex, claude, zai\nUsage: /disconnect `, + `Error: Unknown provider "${provider}"\n\nAvailable providers: codex, claude, zai, minimax\nUsage: /disconnect `, false, ); } diff --git a/src/providers/byok-providers.ts b/src/providers/byok-providers.ts index 1b04c1a..fc0c44e 100644 --- a/src/providers/byok-providers.ts +++ b/src/providers/byok-providers.ts @@ -45,6 +45,13 @@ export const BYOK_PROVIDERS = [ providerType: "zai", providerName: "lc-zai", }, + { + id: "minimax", + displayName: "MiniMax API", + description: "Connect a MiniMax key or coding plan", + providerType: "minimax", + providerName: "lc-minimax", + }, { id: "gemini", displayName: "Gemini API", diff --git a/src/providers/minimax-provider.ts b/src/providers/minimax-provider.ts new file mode 100644 index 0000000..18deaa8 --- /dev/null +++ b/src/providers/minimax-provider.ts @@ -0,0 +1,150 @@ +/** + * Direct API calls to Letta for managing MiniMax provider + */ + +import { LETTA_CLOUD_API_URL } from "../auth/oauth"; +import { settingsManager } from "../settings-manager"; + +// Provider name constant for MiniMax coding plan +export const MINIMAX_PROVIDER_NAME = "minimax-coding-plan"; + +interface ProviderResponse { + id: string; + name: string; + provider_type: string; + api_key?: string; + base_url?: string; +} + +/** + * Get the Letta API base URL and auth token + */ +async function getLettaConfig(): Promise<{ baseUrl: string; apiKey: string }> { + const settings = await settingsManager.getSettingsWithSecureTokens(); + const baseUrl = + process.env.LETTA_BASE_URL || + settings.env?.LETTA_BASE_URL || + LETTA_CLOUD_API_URL; + const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY || ""; + return { baseUrl, apiKey }; +} + +/** + * Make a request to the Letta providers API + */ +async function providersRequest( + method: "GET" | "POST" | "PATCH" | "DELETE", + path: string, + body?: Record, +): Promise { + const { baseUrl, apiKey } = await getLettaConfig(); + const url = `${baseUrl}${path}`; + + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "X-Letta-Source": "letta-code", + }, + ...(body && { body: JSON.stringify(body) }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Provider API error (${response.status}): ${errorText}`); + } + + // Handle empty responses (e.g., DELETE) + const text = await response.text(); + if (!text) { + return {} as T; + } + return JSON.parse(text) as T; +} + +/** + * List all providers + */ +async function listProviders(): Promise { + try { + const response = await providersRequest( + "GET", + "/v1/providers", + ); + return response; + } catch { + return []; + } +} + +/** + * Get the minimax-coding-plan provider if it exists + */ +export async function getMinimaxProvider(): Promise { + const providers = await listProviders(); + return providers.find((p) => p.name === MINIMAX_PROVIDER_NAME) || null; +} + +/** + * Create the MiniMax coding plan provider with the given API key + */ +export async function createMinimaxProvider( + apiKey: string, +): Promise { + return providersRequest("POST", "/v1/providers", { + name: MINIMAX_PROVIDER_NAME, + provider_type: "minimax", + api_key: apiKey, + }); +} + +/** + * Update an existing MiniMax provider with a new API key + */ +export async function updateMinimaxProvider( + providerId: string, + apiKey: string, +): Promise { + return providersRequest( + "PATCH", + `/v1/providers/${providerId}`, + { + api_key: apiKey, + }, + ); +} + +/** + * Create or update the MiniMax coding plan provider + * If provider exists, updates it with the new API key + * If not, creates a new provider + */ +export async function createOrUpdateMinimaxProvider( + apiKey: string, +): Promise { + const existing = await getMinimaxProvider(); + + if (existing) { + return updateMinimaxProvider(existing.id, apiKey); + } + + return createMinimaxProvider(apiKey); +} + +/** + * Delete the MiniMax provider by ID + */ +async function deleteMinimaxProvider(providerId: string): Promise { + await providersRequest("DELETE", `/v1/providers/${providerId}`); +} + +/** + * Remove the MiniMax provider (called on /disconnect minimax) + */ +export async function removeMinimaxProvider(): Promise { + const existing = await getMinimaxProvider(); + if (existing) { + await deleteMinimaxProvider(existing.id); + } +}