From 221a965e1d4f2000c648fa1b5666f26b38323d61 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:42:50 +0800 Subject: [PATCH 1/4] providers: add MiniMax preset --- config/phantom.yaml | 11 ++- docs/providers.md | 58 ++++++++++++++ src/config/__tests__/loader-metadata.test.ts | 33 ++++++++ src/config/__tests__/loader.test.ts | 22 ++++++ src/config/__tests__/providers.test.ts | 81 +++++++++++++++++++- src/config/providers.ts | 14 ++++ 6 files changed, 215 insertions(+), 4 deletions(-) diff --git a/config/phantom.yaml b/config/phantom.yaml index 6def3965..b66412b0 100644 --- a/config/phantom.yaml +++ b/config/phantom.yaml @@ -24,7 +24,7 @@ timeout_minutes: 240 # the chosen provider; authentication happens at the env var named below. # # provider: -# type: anthropic # anthropic | zai | openrouter | vllm | ollama | litellm | custom +# type: anthropic # anthropic | zai | minimax | openrouter | vllm | ollama | litellm | custom # # api_key_env: ANTHROPIC_API_KEY # # Example: GLM-5.1 via Z.AI's Anthropic-compatible API (15x cheaper than Opus) @@ -36,6 +36,15 @@ timeout_minutes: 240 # sonnet: glm-5.1 # haiku: glm-4.5-air # +# Example: MiniMax with the two current text models +# provider: +# type: minimax +# api_key_env: MINIMAX_API_KEY +# model_mappings: +# opus: MiniMax-M3 +# sonnet: MiniMax-M3 +# haiku: MiniMax-M2.7 +# # Example: Murph runtime with OpenAI. Requires @murph/anthropic-sdk-shim to be # installed or linked into this Phantom project. # agent_runtime: murph diff --git a/docs/providers.md b/docs/providers.md index bed71244..44b797d0 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -43,6 +43,7 @@ phantom start --agent-runtime murph | `anthropic` (default) | `https://api.anthropic.com` | `ANTHROPIC_API_KEY` | Claude Opus, Sonnet, Haiku | | `openai` | `https://api.openai.com` | `OPENAI_API_KEY` | Murph runtime only | | `zai` | `https://api.z.ai/api/anthropic` | `ZAI_API_KEY` | GLM-5.1 and GLM-4.5-Air, roughly 15x cheaper than Opus | +| `minimax` | `https://api.minimax.io/anthropic` | `MINIMAX_API_KEY` | MiniMax-M3 and MiniMax-M2.7 | | `openrouter` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | 100+ models through a single key | | `vllm` | `http://localhost:8000` | none | Self-hosted OpenAI-compatible inference | | `ollama` | `http://localhost:11434` | none | Local GGUF models, zero API cost | @@ -130,6 +131,63 @@ ZAI_API_KEY= Both the main agent and every evolution judge route through Z.AI. The `claude-sonnet-4-6` model name is translated to `glm-5.1` on the wire by the `model_mappings` block. +### MiniMax + +The `minimax` preset supports both current text models. The default Anthropic runtime uses the Anthropic-compatible endpoint, while Murph uses the OpenAI-compatible endpoint. + +| Model | Context window | Input modalities | Thinking | +|------|----------------|------------------|----------| +| `MiniMax-M3` | 1,000,000 tokens | text, image, video | adaptive or disabled | +| `MiniMax-M2.7` | 204,800 tokens | text | always on | + +```yaml +# phantom.yaml, default Anthropic runtime +model: claude-sonnet-4-6 +provider: + type: minimax + api_key_env: MINIMAX_API_KEY + model_mappings: + opus: MiniMax-M3 + sonnet: MiniMax-M3 + haiku: MiniMax-M2.7 +``` + +```bash +# .env +MINIMAX_API_KEY= +``` + +Choose the endpoint for the runtime protocol and region: + +| Runtime protocol | Global default | China override | +|------------------|----------------|----------------| +| Anthropic | `https://api.minimax.io/anthropic` | `https://api.minimaxi.com/anthropic` | +| Murph OpenAI-compatible | `https://api.minimax.io/v1` | `https://api.minimaxi.com/v1` | + +The Anthropic client appends `/v1/messages`, so its base URL must end at `/anthropic`; the resulting global request URL is `https://api.minimax.io/anthropic/v1/messages`. To use the China endpoint, set the matching value from the table as `provider.base_url`. + +For the OpenAI-compatible route, select Murph and use the concrete model ID: + +```yaml +# phantom.yaml, Murph runtime +agent_runtime: murph +model: MiniMax-M3 +provider: + type: minimax +``` + +Pay-as-you-go prices are in USD per million tokens. MiniMax-M3 pricing depends on both the service tier and input length, so each tier is listed separately. + +| Model / service tier | Input length | Input | Output | Cache read | Cache write | +|----------------------|--------------|-------|--------|------------|-------------| +| `MiniMax-M3` standard | up to 512k | $0.30 | $1.20 | $0.06 | n/a | +| `MiniMax-M3` standard | over 512k | $0.60 | $2.40 | $0.12 | n/a | +| `MiniMax-M3` priority | up to 512k | $0.45 | $1.80 | $0.09 | n/a | +| `MiniMax-M3` priority | over 512k | $0.90 | $3.60 | $0.18 | n/a | +| `MiniMax-M2.7` | all requests | $0.30 | $1.20 | $0.06 | $0.375 | + +See the [MiniMax Anthropic API documentation](https://platform.minimax.io/docs/api-reference/text-anthropic-api) and [current pricing](https://platform.minimax.io/docs/guides/pricing-paygo) for API details. + ### Ollama (local, free) Run any GGUF model on your own GPU. No API key needed. diff --git a/src/config/__tests__/loader-metadata.test.ts b/src/config/__tests__/loader-metadata.test.ts index 63e9a54a..649efa8b 100644 --- a/src/config/__tests__/loader-metadata.test.ts +++ b/src/config/__tests__/loader-metadata.test.ts @@ -24,6 +24,7 @@ describe("loadConfig secret_source", () => { "ANTHROPIC_AUTH_TOKEN", "OPENAI_API_KEY", "ZAI_API_KEY", + "MINIMAX_API_KEY", "OPENROUTER_API_KEY", "LITELLM_KEY", ] as const; @@ -32,6 +33,7 @@ describe("loadConfig secret_source", () => { ANTHROPIC_AUTH_TOKEN: undefined, OPENAI_API_KEY: undefined, ZAI_API_KEY: undefined, + MINIMAX_API_KEY: undefined, OPENROUTER_API_KEY: undefined, LITELLM_KEY: undefined, }; @@ -149,6 +151,37 @@ provider: expect(process.env.OPENAI_API_KEY).toBeUndefined(); }); + test("secret_source: metadata with Murph MiniMax populates only MINIMAX_API_KEY", async () => { + const stubBody = "minimax-provider-token"; + globalThis.fetch = mock((url: string | Request) => { + expect(String(url)).toBe("http://gateway.test/v1/secrets/provider_token"); + return Promise.resolve( + new Response(stubBody, { + status: 200, + headers: { "X-Phantom-Rotation-Id": "1" }, + }), + ); + }) as unknown as typeof fetch; + + const path = writeYaml( + "metadata-murph-minimax.yaml", + ` +name: metadata-minimax +agent_runtime: murph +secret_source: metadata +secret_source_url: http://gateway.test +provider: + type: minimax +`, + ); + await loadConfig(path); + expect(process.env.MINIMAX_API_KEY).toBe(stubBody); + expect(process.env.ANTHROPIC_API_KEY).toBeUndefined(); + expect(process.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); + expect(process.env.OPENAI_API_KEY).toBeUndefined(); + expect(process.env.ZAI_API_KEY).toBeUndefined(); + }); + test("secret_source: metadata resolves whole-string ${secret:NAME} references in nested config", async () => { // Use peers.test_peer.token, an existing schema field that accepts an // arbitrary string and is nested. We assert the resolved value via a diff --git a/src/config/__tests__/loader.test.ts b/src/config/__tests__/loader.test.ts index 524e53a3..898f2515 100644 --- a/src/config/__tests__/loader.test.ts +++ b/src/config/__tests__/loader.test.ts @@ -460,6 +460,28 @@ provider: } }); + test("loads a MiniMax provider block", async () => { + const path = writeYaml( + "minimax-provider.yaml", + ` +name: test +provider: + type: minimax + api_key_env: MINIMAX_API_KEY + model_mappings: + sonnet: MiniMax-M3 +`, + ); + try { + const config = await loadConfig(path); + expect(config.provider.type).toBe("minimax"); + expect(config.provider.api_key_env).toBe("MINIMAX_API_KEY"); + expect(config.provider.model_mappings?.sonnet).toBe("MiniMax-M3"); + } finally { + cleanup(); + } + }); + test("loads provider.type: openai when Murph runtime is selected", async () => { const path = writeYaml( "openai-provider-murph.yaml", diff --git a/src/config/__tests__/providers.test.ts b/src/config/__tests__/providers.test.ts index d720c4b4..41bc4695 100644 --- a/src/config/__tests__/providers.test.ts +++ b/src/config/__tests__/providers.test.ts @@ -29,6 +29,7 @@ const WATCHED = [ "ANTHROPIC_BASE_URL", "OPENAI_API_KEY", "ZAI_API_KEY", + "MINIMAX_API_KEY", "OPENROUTER_API_KEY", "LITELLM_KEY", "MURPH_PROVIDER", @@ -69,7 +70,17 @@ describe("ProviderSchema", () => { }); test("accepts each valid provider type", () => { - for (const type of ["anthropic", "openai", "zai", "openrouter", "vllm", "ollama", "litellm", "custom"] as const) { + for (const type of [ + "anthropic", + "openai", + "zai", + "minimax", + "openrouter", + "vllm", + "ollama", + "litellm", + "custom", + ] as const) { const parsed = ProviderSchema.parse({ type }); expect(parsed.type).toBe(type); } @@ -173,6 +184,31 @@ describe("buildProviderEnv: zai preset", () => { }); }); +describe("buildProviderEnv: MiniMax preset", () => { + test("sets the global Anthropic endpoint, credentials, model mappings, and disables betas", () => { + process.env.MINIMAX_API_KEY = "minimax-secret"; + const config = makeConfig({ + type: "minimax", + model_mappings: { sonnet: "MiniMax-M3", haiku: "MiniMax-M2.7" }, + }); + const env = buildProviderEnv(config); + + expect(env.ANTHROPIC_BASE_URL).toBe("https://api.minimax.io/anthropic"); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe("minimax-secret"); + expect(env.ANTHROPIC_API_KEY).toBe("minimax-secret"); + expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("MiniMax-M3"); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe("MiniMax-M2.7"); + expect(env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS).toBe("1"); + }); + + test("accepts the China Anthropic endpoint override", () => { + const config = makeConfig({ type: "minimax", base_url: "https://api.minimaxi.com/anthropic" }); + const env = buildProviderEnv(config); + + expect(env.ANTHROPIC_BASE_URL).toBe("https://api.minimaxi.com/anthropic"); + }); +}); + describe("buildProviderEnv: ollama preset", () => { test("sets local base_url, no auth token, disables betas", () => { const config = makeConfig({ type: "ollama" }); @@ -331,6 +367,35 @@ describe("buildAgentRuntimeEnv: Murph runtime", () => { expect(env.OPENROUTER_API_KEY).toBe("openrouter-secret"); }); + test("emits the MiniMax OpenAI-compatible route env", () => { + process.env.MINIMAX_API_KEY = "minimax-secret"; + const config = makeConfig( + { type: "minimax", model_mappings: { sonnet: "MiniMax-M3" } }, + { agent_runtime: "murph", model: "sonnet" }, + ); + const model = resolveAgentRuntimeModel(config, config.model); + const env = buildAgentRuntimeEnv(config, model); + + expect(model).toBe("MiniMax-M3"); + expect(env.MURPH_PROVIDER).toBe("openai-compat"); + expect(env.MURPH_PROVIDER_CONFIG).toBe("custom"); + expect(env.MURPH_MODEL).toBe("MiniMax-M3"); + expect(env.OPENAI_BASE_URL).toBe("https://api.minimax.io/v1"); + expect(env.OPENAI_API_KEY).toBe("minimax-secret"); + expect(env.MINIMAX_API_KEY).toBe(""); + }); + + test("accepts the MiniMax China OpenAI endpoint override", () => { + const config = makeConfig( + { type: "minimax", base_url: "https://api.minimaxi.com/v1" }, + { agent_runtime: "murph", model: "MiniMax-M2.7" }, + ); + const env = buildAgentRuntimeEnv(config, config.model); + + expect(env.MURPH_PROVIDER).toBe("openai-compat"); + expect(env.OPENAI_BASE_URL).toBe("https://api.minimaxi.com/v1"); + }); + test("emits no credential key for Ollama and vLLM by default", () => { for (const type of ["ollama", "vllm"] as const) { const config = makeConfig({ type }, { agent_runtime: "murph", model: "local-model" }); @@ -378,7 +443,17 @@ describe("buildAgentRuntimeEnv: Murph runtime", () => { describe("PROVIDER_PRESETS", () => { test("contains every provider type declared in the schema", () => { - for (const type of ["anthropic", "openai", "zai", "openrouter", "vllm", "ollama", "litellm", "custom"] as const) { + for (const type of [ + "anthropic", + "openai", + "zai", + "minimax", + "openrouter", + "vllm", + "ollama", + "litellm", + "custom", + ] as const) { expect(PROVIDER_PRESETS[type]).toBeDefined(); } }); @@ -389,7 +464,7 @@ describe("PROVIDER_PRESETS", () => { }); test("every non-anthropic Anthropic-compatible preset disables betas by default", () => { - for (const type of ["zai", "openrouter", "vllm", "ollama", "litellm", "custom"] as const) { + for (const type of ["zai", "minimax", "openrouter", "vllm", "ollama", "litellm", "custom"] as const) { expect(PROVIDER_PRESETS[type].disable_betas).toBe(true); } }); diff --git a/src/config/providers.ts b/src/config/providers.ts index 66d72c71..2d34fdf5 100644 --- a/src/config/providers.ts +++ b/src/config/providers.ts @@ -17,6 +17,7 @@ export const PROVIDER_TYPES = [ "anthropic", "openai", "zai", + "minimax", "openrouter", "vllm", "ollama", @@ -86,6 +87,11 @@ export const PROVIDER_PRESETS: Readonly> = api_key_env: "ZAI_API_KEY", disable_betas: true, }, + minimax: { + base_url: "https://api.minimax.io/anthropic", + api_key_env: "MINIMAX_API_KEY", + disable_betas: true, + }, openrouter: { base_url: "https://openrouter.ai/api/v1", api_key_env: "OPENROUTER_API_KEY", @@ -136,6 +142,7 @@ const MURPH_ROUTE_ENV_KEYS = [ const MURPH_CREDENTIAL_ENV_KEYS = [ "OPENAI_API_KEY", "ZAI_API_KEY", + "MINIMAX_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "DASHSCOPE_API_KEY", @@ -247,6 +254,13 @@ export function buildMurphProviderEnv( addSelectedCredential(env, sourceEnvKey, "ZAI_API_KEY"); break; } + case "minimax": { + env.MURPH_PROVIDER = "openai-compat"; + env.MURPH_PROVIDER_CONFIG = "custom"; + env.OPENAI_BASE_URL = provider.base_url ?? "https://api.minimax.io/v1"; + addSelectedCredential(env, sourceEnvKey, "OPENAI_API_KEY"); + break; + } case "openrouter": { env.MURPH_PROVIDER = "openai-compat"; env.MURPH_PROVIDER_CONFIG = "openrouter"; From 300bd67703eb52745a8417412accb483e0f65014 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:26:32 +0800 Subject: [PATCH 2/4] providers: fix MiniMax metadata credentials --- src/config/__tests__/loader-metadata.test.ts | 31 ++++++++++++++++++++ src/config/loader.ts | 15 ++++------ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/config/__tests__/loader-metadata.test.ts b/src/config/__tests__/loader-metadata.test.ts index 649efa8b..783a6851 100644 --- a/src/config/__tests__/loader-metadata.test.ts +++ b/src/config/__tests__/loader-metadata.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { loadConfig } from "../loader.ts"; +import { buildProviderEnv } from "../providers.ts"; // Distinct TEST_DIR from loader.test.ts so the two suites cannot race on the // same filesystem path when bun runs them in parallel. @@ -182,6 +183,36 @@ provider: expect(process.env.ZAI_API_KEY).toBeUndefined(); }); + test("secret_source: metadata with default MiniMax reaches the runtime environment", async () => { + const stubBody = "minimax-provider-token"; + globalThis.fetch = mock((url: string | Request) => { + expect(String(url)).toBe("http://gateway.test/v1/secrets/provider_token"); + return Promise.resolve( + new Response(stubBody, { + status: 200, + headers: { "X-Phantom-Rotation-Id": "1" }, + }), + ); + }) as unknown as typeof fetch; + + const path = writeYaml( + "metadata-default-minimax.yaml", + ` +name: metadata-minimax +secret_source: metadata +secret_source_url: http://gateway.test +provider: + type: minimax +`, + ); + const config = await loadConfig(path); + const env = buildProviderEnv(config); + + expect(process.env.MINIMAX_API_KEY).toBe(stubBody); + expect(env.ANTHROPIC_API_KEY).toBe(stubBody); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe(stubBody); + }); + test("secret_source: metadata resolves whole-string ${secret:NAME} references in nested config", async () => { // Use peers.test_peer.token, an existing schema field that accepts an // arbitrary string and is nested. We assert the resolved value via a diff --git a/src/config/loader.ts b/src/config/loader.ts index d0ba5f0e..a33f9005 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -172,10 +172,8 @@ export async function loadConfig(path?: string): Promise { const fetcher = new MetadataSecretFetcher(baseUrl); // Resolve the provider token first so process.env is populated before any - // downstream code that reads it. Anthropic runtime keeps the existing - // ANTHROPIC_API_KEY plus ANTHROPIC_AUTH_TOKEN behavior. Murph runtime - // populates only the selected provider key so ambient credentials do not - // steer the native route. + // downstream code that reads it. Provider-specific presets receive their + // selected key, while the default generic credential behavior remains intact. const providerToken = await fetcher.get(config.provider.secret_name); populateMetadataProviderEnv(config, providerToken); @@ -191,15 +189,14 @@ function validateRuntimeProviderCombination(config: PhantomConfig): void { } function populateMetadataProviderEnv(config: PhantomConfig, providerToken: string): void { - if (config.agent_runtime !== "murph") { - process.env.ANTHROPIC_API_KEY = providerToken; - process.env.ANTHROPIC_AUTH_TOKEN = providerToken; - return; - } const key = selectedProviderSecretEnvKey(config); if (key) { process.env[key] = providerToken; } + if (config.agent_runtime !== "murph" && (!key || key === "ANTHROPIC_API_KEY")) { + process.env.ANTHROPIC_API_KEY = providerToken; + process.env.ANTHROPIC_AUTH_TOKEN = providerToken; + } } /** From 0671863f2cb784588e56ed94be4c19866261310e Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:40:06 +0800 Subject: [PATCH 3/4] docs: clarify MiniMax thinking defaults --- docs/providers.md | 4 +++- src/agent/__tests__/thinking-config.test.ts | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/providers.md b/docs/providers.md index 44b797d0..1a80c702 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -133,13 +133,15 @@ Both the main agent and every evolution judge route through Z.AI. The `claude-so ### MiniMax -The `minimax` preset supports both current text models. The default Anthropic runtime uses the Anthropic-compatible endpoint, while Murph uses the OpenAI-compatible endpoint. +The `minimax` preset supports both current models. The default Anthropic runtime uses the Anthropic-compatible endpoint, while Murph uses the OpenAI-compatible endpoint. The listed context window is the combined input and output budget. | Model | Context window | Input modalities | Thinking | |------|----------------|------------------|----------| | `MiniMax-M3` | 1,000,000 tokens | text, image, video | adaptive or disabled | | `MiniMax-M2.7` | 204,800 tokens | text | always on | +Phantom sends adaptive thinking by default for both runtime protocols. MiniMax-M3 also supports disabled thinking at the API level, while MiniMax-M2.7 always keeps thinking enabled. + ```yaml # phantom.yaml, default Anthropic runtime model: claude-sonnet-4-6 diff --git a/src/agent/__tests__/thinking-config.test.ts b/src/agent/__tests__/thinking-config.test.ts index ccd06291..0f166b39 100644 --- a/src/agent/__tests__/thinking-config.test.ts +++ b/src/agent/__tests__/thinking-config.test.ts @@ -26,6 +26,11 @@ describe("getThinkingConfig", () => { expect(getThinkingConfig("claude-mythos-preview")).toEqual({ type: "adaptive" }); }); + test("MiniMax models use the adaptive request shape", () => { + expect(getThinkingConfig("MiniMax-M3")).toEqual({ type: "adaptive" }); + expect(getThinkingConfig("MiniMax-M2.7")).toEqual({ type: "adaptive" }); + }); + test("Haiku 4.5 returns enabled + budgetTokens (Haiku rejects adaptive with 400)", () => { const config = getThinkingConfig(JUDGE_MODEL_HAIKU); expect(config.type).toBe("enabled"); From 1103354d1da94d739828beadf86278e3a788bda0 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:24:38 +0800 Subject: [PATCH 4/4] docs: add MiniMax to provider overview --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3eacb356..15056925 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,11 @@ This is what happens when you give an AI its own computer. ## Bring Your Own Model -Phantom is not locked to any single AI backend. It ships with support for seven providers out of the box, configured through a single YAML block: +Phantom is not locked to any single AI backend. It ships with support for eight providers out of the box, configured through a single YAML block: - **Anthropic** (default) - Claude Opus, Sonnet, Haiku - **Z.AI** - GLM-5.1 and GLM-4.5-Air via [Z.AI's Anthropic-compatible API](https://docs.z.ai/guides/llm/glm-5). Roughly 15x cheaper than Claude Opus for comparable coding quality. +- **MiniMax** - MiniMax-M3 and MiniMax-M2.7 through Anthropic- and OpenAI-compatible endpoints - **OpenRouter** - 100+ models through one key - **Ollama** - Any GGUF model on your own GPU, zero API cost - **vLLM** - Self-hosted inference with OpenAI-compatible endpoints