From 65e2c7a79c7e42e12e19d3f7921525d08efa432b Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Thu, 3 Sep 2026 03:00:30 +0800 Subject: [PATCH] feat: add MiniMax subagent provider --- docs/agent-profile-schema.md | 3 + docs/configuration.md | 14 ++ examples/agents/minimax-builder.md | 10 ++ schema/v1/devspace.schema.json | 3 +- src/local-agent-adapters.test.ts | 7 + src/local-agent-adapters.ts | 2 + src/local-agent-availability.test.ts | 1 + src/local-agent-availability.ts | 2 + src/local-agent-errors.ts | 1 + src/local-agent-minimax.test.ts | 188 +++++++++++++++++++++++++++ src/local-agent-minimax.ts | 156 ++++++++++++++++++++++ src/local-agent-pi.ts | 101 ++++++++++---- src/local-agent-profiles.ts | 5 +- 13 files changed, 465 insertions(+), 28 deletions(-) create mode 100644 examples/agents/minimax-builder.md create mode 100644 src/local-agent-minimax.test.ts create mode 100644 src/local-agent-minimax.ts diff --git a/docs/agent-profile-schema.md b/docs/agent-profile-schema.md index 5ede01ad1..eaedb79bf 100644 --- a/docs/agent-profile-schema.md +++ b/docs/agent-profile-schema.md @@ -74,6 +74,7 @@ provider: pi provider: cursor provider: copilot provider: grok +provider: minimax ``` Unsupported or custom providers are rejected. DevSpace maps providers to their @@ -86,6 +87,7 @@ native integration: - `cursor`: ACP - `copilot`: ACP - `grok`: Grok Build ACP (`grok agent stdio`) +- `minimax`: embedded MiniMax API runtime, with MiniMax-M3 as the default model Codex is resolved from the user's environment rather than bundled with DevSpace. Run `codex login` normally before using it; set `CODEX_COMMAND` when @@ -122,6 +124,7 @@ DevSpace passes this through to providers that expose a matching control: - `opencode`: model variant. - `cursor` and `copilot`: ACP thought-level config when supported. - `grok`: `--reasoning-effort` on startup and xAI's ACP model metadata for resumed sessions. +- `minimax`: `off` disables MiniMax-M3 thinking; other levels enable adaptive thinking. MiniMax-M2.7 always uses thinking. ### `disabled` diff --git a/docs/configuration.md b/docs/configuration.md index 6a6f607bd..12889f292 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -121,6 +121,12 @@ Subagent providers are explicit. Omitted providers are disabled: "enabled": true, "model": "sonnet", }, + { + "id": "minimax", + "enabled": true, + "model": "MiniMax-M3", + "effort": "high", + }, ], }, } @@ -135,6 +141,14 @@ are `CODEX_COMMAND`, `CODEX_HOME`, `CLAUDE_COMMAND`, `CURSOR_COMMAND`, `COPILOT_COMMAND`, `GROK_COMMAND`, and `GROK_AGENT_PROFILE`. DevSpace does not persist provider credentials. +MiniMax supports `MiniMax-M3` and `MiniMax-M2.7`. The unqualified model ids use +the global `https://api.minimax.io/anthropic` endpoint and +`MINIMAX_API_KEY`. Use a `minimax-cn/` prefix, such as +`minimax-cn/MiniMax-M3`, with `MINIMAX_CN_API_KEY` to select the China +`https://api.minimaxi.com/anthropic` endpoint. The corresponding +OpenAI-compatible endpoints are `https://api.minimax.io/v1` and +`https://api.minimaxi.com/v1`. + ## Native artifact download Set `artifacts.enabled` to `true` when a host needs to save a native attached or diff --git a/examples/agents/minimax-builder.md b/examples/agents/minimax-builder.md new file mode 100644 index 000000000..31b3ed099 --- /dev/null +++ b/examples/agents/minimax-builder.md @@ -0,0 +1,10 @@ +--- +name: minimax-builder +description: Implement a focused change with MiniMax. +provider: minimax +model: MiniMax-M3 +effort: high +--- + +Implement the requested change in the current workspace. Inspect the existing +patterns first, keep the patch focused, and report the verification you ran. diff --git a/schema/v1/devspace.schema.json b/schema/v1/devspace.schema.json index e7c18466e..d554b7dc2 100644 --- a/schema/v1/devspace.schema.json +++ b/schema/v1/devspace.schema.json @@ -179,7 +179,8 @@ "pi", "cursor", "copilot", - "grok" + "grok", + "minimax" ] }, "enabled": { diff --git a/src/local-agent-adapters.test.ts b/src/local-agent-adapters.test.ts index 395072a1d..c4df55215 100644 --- a/src/local-agent-adapters.test.ts +++ b/src/local-agent-adapters.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { delimiter } from "node:path"; import { claudeCommandEnvironment, + createLocalAgentDrivers, extractOpenCodeFinalResponse, extractPiFinalResponse, extractPiProviderError, @@ -9,6 +10,12 @@ import { resolveAcpEffortConfigUpdate, } from "./local-agent-adapters.js"; import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js"; + +assert.deepEqual( + createLocalAgentDrivers().map((driver) => driver.provider), + ["codex", "claude", "opencode", "pi", "cursor", "copilot", "grok", "minimax"], +); + assert.deepEqual( resolveAcpModelConfigUpdate({ sessionId: "session_model_1", diff --git a/src/local-agent-adapters.ts b/src/local-agent-adapters.ts index 03a5cc40c..e2fe7129e 100644 --- a/src/local-agent-adapters.ts +++ b/src/local-agent-adapters.ts @@ -10,6 +10,7 @@ import { type ClaudeQueryFactory, } from "./local-agent-claude.js"; import { CodexLocalAgentDriver } from "./local-agent-codex.js"; +import { MiniMaxLocalAgentDriver } from "./local-agent-minimax.js"; import { OpencodeLocalAgentDriver, extractOpenCodeFinalResponse, @@ -43,6 +44,7 @@ export function createLocalAgentDrivers( new AcpLocalAgentDriver("cursor", options.env), new AcpLocalAgentDriver("copilot", options.env), new AcpLocalAgentDriver("grok", options.env), + new MiniMaxLocalAgentDriver(options.piSessionFactory), ]; } diff --git a/src/local-agent-availability.test.ts b/src/local-agent-availability.test.ts index 7e0ebc1ce..bc5e7b807 100644 --- a/src/local-agent-availability.test.ts +++ b/src/local-agent-availability.test.ts @@ -10,3 +10,4 @@ assert.deepEqual(snapshot.find((provider) => provider.name === "codex"), { available: false, reason: "/definitely/missing/devspace-codex executable not found", }); +assert.equal(snapshot.find((provider) => provider.name === "minimax")?.available, true); diff --git a/src/local-agent-availability.ts b/src/local-agent-availability.ts index 3a67b98f7..358415054 100644 --- a/src/local-agent-availability.ts +++ b/src/local-agent-availability.ts @@ -37,6 +37,8 @@ function checkLocalAgentProviderAvailability( return commandAvailability(provider, env.COPILOT_COMMAND ?? "copilot", env); case "grok": return commandAvailability(provider, env.GROK_COMMAND ?? "grok", env); + case "minimax": + return packageAvailability(provider, "@earendil-works/pi-coding-agent"); } } diff --git a/src/local-agent-errors.ts b/src/local-agent-errors.ts index 0df50b867..603c27577 100644 --- a/src/local-agent-errors.ts +++ b/src/local-agent-errors.ts @@ -451,6 +451,7 @@ function displayProvider(provider: LocalAgentProvider): string { case "cursor": return "Cursor"; case "copilot": return "Copilot"; case "grok": return "Grok"; + case "minimax": return "MiniMax"; } } diff --git a/src/local-agent-minimax.test.ts b/src/local-agent-minimax.test.ts new file mode 100644 index 000000000..c15babffa --- /dev/null +++ b/src/local-agent-minimax.test.ts @@ -0,0 +1,188 @@ +import assert from "node:assert/strict"; +import { + AuthStorage, + ModelRegistry, + type AgentSessionEvent, + type AgentSessionEventListener, + type ProviderConfig, +} from "@earendil-works/pi-coding-agent"; +import { + MINIMAX_DEFAULT_MODEL, + MINIMAX_MODEL_CONFIGS, + MINIMAX_REGIONAL_ENDPOINTS, + MiniMaxLocalAgentDriver, + registerMiniMaxProviders, + resolveMiniMaxModel, + type MiniMaxModelRegistry, +} from "./local-agent-minimax.js"; +import type { PiSessionLike } from "./local-agent-pi.js"; + +assert.deepEqual(MINIMAX_REGIONAL_ENDPOINTS, [ + { + region: "global_en", + providerId: "minimax", + apiKey: "$MINIMAX_API_KEY", + openaiBaseUrl: "https://api.minimax.io/v1", + anthropicBaseUrl: "https://api.minimax.io/anthropic", + docsRoot: "https://platform.minimax.io/docs", + }, + { + region: "cn_zh", + providerId: "minimax-cn", + apiKey: "$MINIMAX_CN_API_KEY", + openaiBaseUrl: "https://api.minimaxi.com/v1", + anthropicBaseUrl: "https://api.minimaxi.com/anthropic", + docsRoot: "https://platform.minimaxi.com/docs", + }, +]); +assert.deepEqual(MINIMAX_MODEL_CONFIGS, [ + { + modelId: "MiniMax-M3", + contextWindow: 1_000_000, + pricingUsdPerMillionTokens: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: null, + }, + inputModalities: ["text", "image", "video"], + thinking: ["adaptive", "disabled"], + }, + { + modelId: "MiniMax-M2.7", + contextWindow: 204_800, + pricingUsdPerMillionTokens: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + inputModalities: ["text"], + thinking: ["always_on"], + }, +]); + +const builtInModels = new Map([ + ["minimax/MiniMax-M3", model("minimax", "MiniMax-M3", 128_000)], + ["minimax/MiniMax-M2.7", model("minimax", "MiniMax-M2.7", 131_072)], + ["minimax-cn/MiniMax-M3", model("minimax-cn", "MiniMax-M3", 128_000)], + ["minimax-cn/MiniMax-M2.7", model("minimax-cn", "MiniMax-M2.7", 131_072)], +]); +const registrations: Array<{ provider: string; config: ProviderConfig }> = []; +const registry = { + find(provider: string, modelId: string) { + return builtInModels.get(`${provider}/${modelId}`); + }, + hasConfiguredAuth() { + return false; + }, + registerProvider(provider: string, config: ProviderConfig) { + registrations.push({ provider, config }); + }, +} as MiniMaxModelRegistry; + +registerMiniMaxProviders(registry); +assert.deepEqual(registrations.map(({ provider }) => provider), ["minimax", "minimax-cn"]); +const globalConfig = registrations[0]?.config; +assert.equal(globalConfig?.baseUrl, "https://api.minimax.io/anthropic"); +const m3 = globalConfig?.models?.find((candidate) => candidate.id === "MiniMax-M3"); +assert.deepEqual(m3?.cost, { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, +}); +assert.deepEqual(m3?.input, ["text", "image"]); +assert.equal(m3?.contextWindow, 1_000_000); +assert.equal(m3?.maxTokens, 128_000); +assert.equal((m3?.compat as { forceAdaptiveThinking?: boolean })?.forceAdaptiveThinking, true); +const m27 = globalConfig?.models?.find((candidate) => candidate.id === "MiniMax-M2.7"); +assert.deepEqual(m27?.thinkingLevelMap, { off: null }); + +const realRegistry = ModelRegistry.inMemory(AuthStorage.inMemory({ + "minimax-cn": { type: "api_key", key: "test-key" }, +})); +registerMiniMaxProviders(realRegistry); +assert.equal(resolveMiniMaxModel(realRegistry, MINIMAX_DEFAULT_MODEL)?.provider, "minimax-cn"); +assert.deepEqual(realRegistry.find("minimax", "MiniMax-M3")?.cost, { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, +}); + +const regionalRegistry = { + find(provider: string, modelId: string) { + return builtInModels.get(`${provider}/${modelId}`); + }, + hasConfiguredAuth(candidate: { provider: string }) { + return candidate.provider === "minimax-cn"; + }, + registerProvider() {}, +} as MiniMaxModelRegistry; +assert.equal( + resolveMiniMaxModel(regionalRegistry, "MiniMax-M3")?.provider, + "minimax-cn", +); +assert.equal( + resolveMiniMaxModel(regionalRegistry, "minimax/MiniMax-M2.7")?.provider, + "minimax", +); +assert.equal(resolveMiniMaxModel(regionalRegistry, "other/MiniMax-M3"), undefined); + +class FakeSession implements PiSessionLike { + readonly sessionId = "minimax_session_1"; + readonly messages: any[] = []; + readonly modelRegistry = regionalRegistry as PiSessionLike["modelRegistry"]; + private readonly listeners = new Set(); + + async prompt(): Promise { + this.messages.push({ + role: "assistant", + content: [{ type: "text", text: "MiniMax response" }], + }); + for (const listener of this.listeners) listener({ type: "agent_end" } as AgentSessionEvent); + } + + subscribe(listener: AgentSessionEventListener): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + async setModel(): Promise {} + setActiveToolsByName(): void {} + setThinkingLevel(): void {} + dispose(): void {} +} + +const factoryContexts: Array<{ provider: string; model?: string }> = []; +const driver = new MiniMaxLocalAgentDriver(async (context) => { + factoryContexts.push({ provider: context.provider, model: context.model }); + return new FakeSession(); +}); +const runtimeResult = await driver.createRuntime({ + agentId: "agt_minimax", + provider: "minimax", + workspaceRoot: "/tmp/project", +}); +assert.equal(runtimeResult.isOk(), true); +if (runtimeResult.isErr()) throw runtimeResult.error; +assert.deepEqual(factoryContexts, [{ provider: "minimax", model: MINIMAX_DEFAULT_MODEL }]); +const runResult = await runtimeResult.value.run({ + prompt: "inspect", + workspaceRoot: "/tmp/project", +}); +assert.equal(runResult.isOk(), true); +if (runResult.isErr()) throw runResult.error; +assert.equal(runResult.value.provider, "minimax"); +assert.equal(runResult.value.finalResponse, "MiniMax response"); +await runtimeResult.value.close(); + +function model(provider: string, id: string, maxTokens: number) { + return { + provider, + id, + maxTokens, + compat: {}, + } as ReturnType; +} diff --git a/src/local-agent-minimax.ts b/src/local-agent-minimax.ts new file mode 100644 index 000000000..1caba6af2 --- /dev/null +++ b/src/local-agent-minimax.ts @@ -0,0 +1,156 @@ +import type { + ModelRegistry, + ProviderConfig, + ProviderModelConfig, +} from "@earendil-works/pi-coding-agent"; +import { + PiLocalAgentDriver, + type PiSessionFactory, +} from "./local-agent-pi.js"; + +export const MINIMAX_DEFAULT_MODEL = "MiniMax-M3"; + +export const MINIMAX_REGIONAL_ENDPOINTS = [ + { + region: "global_en", + providerId: "minimax", + apiKey: "$MINIMAX_API_KEY", + openaiBaseUrl: "https://api.minimax.io/v1", + anthropicBaseUrl: "https://api.minimax.io/anthropic", + docsRoot: "https://platform.minimax.io/docs", + }, + { + region: "cn_zh", + providerId: "minimax-cn", + apiKey: "$MINIMAX_CN_API_KEY", + openaiBaseUrl: "https://api.minimaxi.com/v1", + anthropicBaseUrl: "https://api.minimaxi.com/anthropic", + docsRoot: "https://platform.minimaxi.com/docs", + }, +] as const; + +export const MINIMAX_MODEL_CONFIGS = [ + { + modelId: "MiniMax-M3", + contextWindow: 1_000_000, + pricingUsdPerMillionTokens: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: null, + }, + inputModalities: ["text", "image", "video"], + thinking: ["adaptive", "disabled"], + }, + { + modelId: "MiniMax-M2.7", + contextWindow: 204_800, + pricingUsdPerMillionTokens: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + inputModalities: ["text"], + thinking: ["always_on"], + }, +] as const; + +export interface MiniMaxModelRegistry { + find: ModelRegistry["find"]; + hasConfiguredAuth: ModelRegistry["hasConfiguredAuth"]; + registerProvider: ModelRegistry["registerProvider"]; +} + +export class MiniMaxLocalAgentDriver extends PiLocalAgentDriver { + constructor(factory?: PiSessionFactory) { + super(factory, { + provider: "minimax", + defaultModel: MINIMAX_DEFAULT_MODEL, + configureModelRegistry: registerMiniMaxProviders, + resolveModel: resolveMiniMaxModel, + }); + } +} + +export function registerMiniMaxProviders(registry: MiniMaxModelRegistry): void { + for (const endpoint of MINIMAX_REGIONAL_ENDPOINTS) { + const config: ProviderConfig = { + name: endpoint.region === "global_en" ? "MiniMax" : "MiniMax CN", + baseUrl: endpoint.anthropicBaseUrl, + apiKey: endpoint.apiKey, + api: "anthropic-messages", + models: MINIMAX_MODEL_CONFIGS.map((model) => ( + miniMaxProviderModel(registry, endpoint.providerId, endpoint.anthropicBaseUrl, model) + )), + }; + registry.registerProvider(endpoint.providerId, config); + } +} + +export function resolveMiniMaxModel( + registry: MiniMaxModelRegistry, + reference: string, +): ReturnType { + const separator = reference.indexOf("/"); + if (separator !== -1) { + const provider = reference.slice(0, separator); + const modelId = reference.slice(separator + 1); + if (!isMiniMaxProviderId(provider)) return undefined; + return registry.find(provider, modelId); + } + + const candidates = MINIMAX_REGIONAL_ENDPOINTS + .map((endpoint) => registry.find(endpoint.providerId, reference)) + .filter((model): model is NonNullable => model !== undefined); + return candidates.find((model) => registry.hasConfiguredAuth(model)) ?? candidates[0]; +} + +function miniMaxProviderModel( + registry: MiniMaxModelRegistry, + providerId: string, + baseUrl: string, + model: typeof MINIMAX_MODEL_CONFIGS[number], +): ProviderModelConfig { + const builtIn = registry.find(providerId, model.modelId); + if (!builtIn) { + throw new Error(`Embedded MiniMax model is unavailable: ${providerId}/${model.modelId}.`); + } + const thinking: readonly string[] = model.thinking; + const inputModalities: readonly ("text" | "image" | "video")[] = model.inputModalities; + const compat = thinking.includes("adaptive") + ? { ...builtIn.compat, forceAdaptiveThinking: true } + : builtIn.compat; + const thinkingLevelMap = thinking.includes("always_on") + ? { ...builtIn.thinkingLevelMap, off: null } + : builtIn.thinkingLevelMap; + + return { + id: model.modelId, + name: model.modelId, + api: "anthropic-messages", + baseUrl, + reasoning: true, + ...(thinkingLevelMap ? { thinkingLevelMap } : {}), + input: inputModalities.filter(isSupportedModelInput), + cost: { + input: model.pricingUsdPerMillionTokens.input, + output: model.pricingUsdPerMillionTokens.output, + cacheRead: model.pricingUsdPerMillionTokens.cacheRead, + cacheWrite: model.pricingUsdPerMillionTokens.cacheWrite ?? 0, + }, + contextWindow: model.contextWindow, + maxTokens: builtIn.maxTokens, + ...(compat ? { compat } : {}), + }; +} + +function isMiniMaxProviderId(value: string): boolean { + return MINIMAX_REGIONAL_ENDPOINTS.some((endpoint) => endpoint.providerId === value); +} + +function isSupportedModelInput( + value: typeof MINIMAX_MODEL_CONFIGS[number]["inputModalities"][number], +): value is "text" | "image" { + return value === "text" || value === "image"; +} diff --git a/src/local-agent-pi.ts b/src/local-agent-pi.ts index 8880983b1..885111ea9 100644 --- a/src/local-agent-pi.ts +++ b/src/local-agent-pi.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { AgentSession } from "@earendil-works/pi-coding-agent"; +import type { AgentSession, ModelRegistry } from "@earendil-works/pi-coding-agent"; import { AgentProviderExecutionError, AgentProviderProtocolError, @@ -14,6 +14,7 @@ import type { LocalAgentRuntime, LocalAgentRuntimeContext, } from "./local-agent-runtime.js"; +import type { LocalAgentProvider } from "./local-agent-profiles.js"; import { createPiSandboxExtension, createPiSandboxModeRef, @@ -45,8 +46,22 @@ export type PiSessionFactory = ( input: LocalAgentRunInput, ) => Promise; +export type PiModelRegistryConfigurer = (registry: ModelRegistry) => void; + +export type PiModelResolver = ( + registry: ModelRegistry, + reference: string, +) => unknown; + +export interface PiLocalAgentDriverOptions { + provider?: LocalAgentProvider; + defaultModel?: string; + configureModelRegistry?: PiModelRegistryConfigurer; + resolveModel?: PiModelResolver; +} + export class PiSessionRuntime implements LocalAgentRuntime { - readonly provider = "pi" as const; + readonly provider: LocalAgentProvider; private readonly unsubscribe: () => void; private alive = true; private closed = false; @@ -55,7 +70,10 @@ export class PiSessionRuntime implements LocalAgentRuntime { constructor( private readonly session: PiSessionLike, + provider: LocalAgentProvider = "pi", + private readonly resolveModel: PiModelResolver = resolvePiModel, ) { + this.provider = provider; this.unsubscribe = session.subscribe((event) => { if (!this.collectingEvents) return; if (this.events.length >= MAX_PI_EVENTS) this.events.shift(); @@ -74,7 +92,7 @@ export class PiSessionRuntime implements LocalAgentRuntime { provider: this.provider, operation: "run", retryable: true, - message: "Pi runtime is not running.", + message: `${this.provider} runtime is not running.`, }); } await callbacks?.onSessionId?.(this.session.sessionId); @@ -98,7 +116,7 @@ export class PiSessionRuntime implements LocalAgentRuntime { operation: "run", retryable: false, cause: new Error(providerError), - message: "Pi agent turn failed.", + message: `${this.provider} agent turn failed.`, }); } throw new AgentProviderProtocolError({ @@ -106,7 +124,7 @@ export class PiSessionRuntime implements LocalAgentRuntime { provider: this.provider, operation: "run", retryable: false, - message: "Pi did not return a final assistant response.", + message: `${this.provider} did not return a final assistant response.`, }); } return { @@ -143,14 +161,14 @@ export class PiSessionRuntime implements LocalAgentRuntime { await updatePiSandboxSession(this.session, input.workspaceRoot, input.writeMode ?? "allowed"); this.session.setActiveToolsByName([...piToolsForWriteMode(input.writeMode)]); if (input.model) { - const model = resolvePiModel(this.session.modelRegistry, input.model); + const model = this.resolveModel(this.session.modelRegistry, input.model); if (!model) { throw new AgentProviderProtocolError({ code: "PROVIDER_PROTOCOL_ERROR", - provider: "pi", + provider: this.provider, operation: "configure_model", retryable: false, - message: `Pi model not found: ${input.model}.`, + message: `Model not found for ${this.provider}: ${input.model}.`, }); } await this.session.setModel(model as never); @@ -162,13 +180,29 @@ export class PiSessionRuntime implements LocalAgentRuntime { } export class PiLocalAgentDriver implements LocalAgentDriver { - readonly provider = "pi" as const; + readonly provider: LocalAgentProvider; readonly idleTimeoutMs = 3 * 60_000; + private readonly factory: PiSessionFactory; + private readonly defaultModel?: string; + private readonly resolveModel: PiModelResolver; - constructor(private readonly factory: PiSessionFactory = defaultPiSessionFactory) {} + constructor( + factory?: PiSessionFactory, + options: PiLocalAgentDriverOptions = {}, + ) { + this.provider = options.provider ?? "pi"; + this.defaultModel = options.defaultModel; + this.resolveModel = options.resolveModel ?? resolvePiModel; + this.factory = factory ?? ((context, input) => defaultPiSessionFactory( + context, + input, + options.configureModelRegistry, + this.resolveModel, + )); + } runtimeKey(context: LocalAgentRuntimeContext): string { - return `pi:${context.agentId}`; + return `${this.provider}:${context.agentId}`; } async createRuntime(context: LocalAgentRuntimeContext) { @@ -177,16 +211,21 @@ export class PiLocalAgentDriver implements LocalAgentDriver { agentId: context.agentId, operation: "create_runtime", run: async (): Promise => { + const effectiveContext: LocalAgentRuntimeContext = { + ...context, + provider: this.provider, + model: context.model ?? this.defaultModel, + }; const input: LocalAgentRunInput = { prompt: "", - workspaceRoot: context.workspaceRoot, - providerSessionId: context.providerSessionId, - writeMode: context.writeMode, - model: context.model, - effort: context.effort, + workspaceRoot: effectiveContext.workspaceRoot, + providerSessionId: effectiveContext.providerSessionId, + writeMode: effectiveContext.writeMode, + model: effectiveContext.model, + effort: effectiveContext.effort, }; - const session = await this.factory(context, input); - return new PiSessionRuntime(session); + const session = await this.factory(effectiveContext, input); + return new PiSessionRuntime(session, this.provider, this.resolveModel); }, }); } @@ -195,6 +234,8 @@ export class PiLocalAgentDriver implements LocalAgentDriver { async function defaultPiSessionFactory( context: LocalAgentRuntimeContext, input: LocalAgentRunInput, + configureModelRegistry?: PiModelRegistryConfigurer, + resolveModel: PiModelResolver = resolvePiModel, ): Promise { const { AuthStorage, @@ -209,16 +250,22 @@ async function defaultPiSessionFactory( const agentDir = getAgentDir(); const authStorage = AuthStorage.create(join(agentDir, "auth.json")); const modelRegistry = ModelRegistry.create(authStorage, join(agentDir, "models.json")); - const sessionManager = await resolveSessionManager(SessionManager, input.workspaceRoot, input.providerSessionId); - const model = input.model ? resolvePiModel(modelRegistry, input.model) : undefined; + configureModelRegistry?.(modelRegistry); + const sessionManager = await resolveSessionManager( + SessionManager, + input.workspaceRoot, + input.providerSessionId, + context.provider, + ); + const model = input.model ? resolveModel(modelRegistry, input.model) : undefined; if (input.model && !model) { throw new AgentProviderProtocolError({ code: "PROVIDER_PROTOCOL_ERROR", - provider: "pi", + provider: context.provider, agentId: context.agentId, operation: "configure_model", retryable: false, - message: `Pi model not found: ${input.model}.`, + message: `Model not found for ${context.provider}: ${input.model}.`, }); } const modeRef = createPiSandboxModeRef(input.writeMode ?? "allowed"); @@ -278,6 +325,7 @@ async function resolveSessionManager( SessionManager: PiSessionManagerApi, workspaceRoot: string, providerSessionId: string | undefined, + provider: LocalAgentProvider = "pi", ): Promise { if (!providerSessionId) return SessionManager.create(workspaceRoot); const sessions = await SessionManager.list(workspaceRoot); @@ -285,16 +333,19 @@ async function resolveSessionManager( if (!match) { throw new AgentProviderProtocolError({ code: "PROVIDER_PROTOCOL_ERROR", - provider: "pi", + provider, operation: "session", retryable: false, - message: `Pi session not found: ${providerSessionId}.`, + message: `${provider} session not found: ${providerSessionId}.`, }); } return SessionManager.open(match.path); } -function resolvePiModel(registry: { find(provider: string, modelId: string): unknown; getAll?: () => unknown[] }, reference: string): unknown { +export function resolvePiModel( + registry: { find(provider: string, modelId: string): unknown; getAll?: () => unknown[] }, + reference: string, +): unknown { const separator = reference.indexOf("/"); if (separator !== -1) { return registry.find(reference.slice(0, separator), reference.slice(separator + 1)); diff --git a/src/local-agent-profiles.ts b/src/local-agent-profiles.ts index ad99a225c..13876a7c4 100644 --- a/src/local-agent-profiles.ts +++ b/src/local-agent-profiles.ts @@ -4,7 +4,7 @@ import { basename, join, resolve } from "node:path"; import { parse as parseYaml } from "yaml"; import type { ServerConfig } from "./config.js"; -export type LocalAgentProvider = "codex" | "claude" | "opencode" | "pi" | "cursor" | "copilot" | "grok"; +export type LocalAgentProvider = "codex" | "claude" | "opencode" | "pi" | "cursor" | "copilot" | "grok" | "minimax"; export const LOCAL_AGENT_PROVIDERS: readonly LocalAgentProvider[] = [ "codex", @@ -14,6 +14,7 @@ export const LOCAL_AGENT_PROVIDERS: readonly LocalAgentProvider[] = [ "cursor", "copilot", "grok", + "minimax", ]; export interface LocalAgentProfile { @@ -161,7 +162,7 @@ function readProvider(frontmatter: Record, filePath: string): L } if (!PROVIDERS.has(provider as LocalAgentProvider)) { throw new Error( - `Subagent profile provider must be codex, claude, opencode, pi, cursor, copilot, or grok: ${filePath}`, + `Subagent profile provider must be codex, claude, opencode, pi, cursor, copilot, grok, or minimax: ${filePath}`, ); } return provider as LocalAgentProvider;