Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/agent-profile-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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`

Expand Down
14 changes: 14 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
},
}
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions examples/agents/minimax-builder.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion schema/v1/devspace.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@
"pi",
"cursor",
"copilot",
"grok"
"grok",
"minimax"
]
},
"enabled": {
Expand Down
7 changes: 7 additions & 0 deletions src/local-agent-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import assert from "node:assert/strict";
import { delimiter } from "node:path";
import {
claudeCommandEnvironment,
createLocalAgentDrivers,
extractOpenCodeFinalResponse,
extractPiFinalResponse,
extractPiProviderError,
resolveAcpModelConfigUpdate,
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",
Expand Down
2 changes: 2 additions & 0 deletions src/local-agent-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
];
}

Expand Down
1 change: 1 addition & 0 deletions src/local-agent-availability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
2 changes: 2 additions & 0 deletions src/local-agent-availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

Expand Down
1 change: 1 addition & 0 deletions src/local-agent-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}

Expand Down
188 changes: 188 additions & 0 deletions src/local-agent-minimax.test.ts
Original file line number Diff line number Diff line change
@@ -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<AgentSessionEventListener>();

async prompt(): Promise<void> {
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<void> {}
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<MiniMaxModelRegistry["find"]>;
}
Loading