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/7] 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/7] 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/7] 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/7] 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 From 6b7cf4a3c680b74de92ab62b3f67801081cbb28c Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:31:29 +0000 Subject: [PATCH 5/7] docs: align MiniMax pricing table --- docs/providers.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 1a80c702..771ca53d 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -178,15 +178,12 @@ 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 | +Pay-as-you-go prices are in USD per million tokens. + +| Model | Input | Output | Cache read | Cache write | +|-------|-------|--------|------------|-------------| +| `MiniMax-M3` | $0.60 | $2.40 | $0.12 | n/a | +| `MiniMax-M2.7` | $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. From 6ea1a4c282062e8164821e468e133c9012f62888 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:24:22 +0800 Subject: [PATCH 6/7] providers: keep MiniMax-M2.7 thinking enabled --- docs/providers.md | 2 +- src/agent/__tests__/thinking-config.test.ts | 10 ++++++++-- src/agent/thinking-config.ts | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 771ca53d..0afcffca 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -140,7 +140,7 @@ The `minimax` preset supports both current models. The default Anthropic runtime | `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. +Phantom sends adaptive thinking for MiniMax-M3 by default. MiniMax-M3 also supports disabled thinking at the API level, while MiniMax-M2.7 always keeps thinking enabled. ```yaml # phantom.yaml, default Anthropic runtime diff --git a/src/agent/__tests__/thinking-config.test.ts b/src/agent/__tests__/thinking-config.test.ts index 0f166b39..d6cefbbf 100644 --- a/src/agent/__tests__/thinking-config.test.ts +++ b/src/agent/__tests__/thinking-config.test.ts @@ -26,9 +26,15 @@ describe("getThinkingConfig", () => { expect(getThinkingConfig("claude-mythos-preview")).toEqual({ type: "adaptive" }); }); - test("MiniMax models use the adaptive request shape", () => { + test("MiniMax-M3 uses the adaptive request shape", () => { expect(getThinkingConfig("MiniMax-M3")).toEqual({ type: "adaptive" }); - expect(getThinkingConfig("MiniMax-M2.7")).toEqual({ type: "adaptive" }); + }); + + test("MiniMax-M2.7 keeps thinking enabled", () => { + expect(getThinkingConfig("MiniMax-M2.7")).toEqual({ + type: "enabled", + budgetTokens: 8192, + }); }); test("Haiku 4.5 returns enabled + budgetTokens (Haiku rejects adaptive with 400)", () => { diff --git a/src/agent/thinking-config.ts b/src/agent/thinking-config.ts index 6d96d558..718031e1 100644 --- a/src/agent/thinking-config.ts +++ b/src/agent/thinking-config.ts @@ -20,6 +20,7 @@ import type { ThinkingConfig } from "@anthropic-ai/claude-agent-sdk"; const ADAPTIVE_PREFIXES: readonly string[] = [ + "MiniMax-M3", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", @@ -27,6 +28,7 @@ const ADAPTIVE_PREFIXES: readonly string[] = [ ]; const MANUAL_ONLY_PREFIXES: readonly string[] = [ + "MiniMax-M2.7", "claude-haiku-4", "claude-haiku-3", "claude-sonnet-3", From b7c5e454998c3d56d7fa84f5e4ae5aed38af4321 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:33:22 +0800 Subject: [PATCH 7/7] feat(providers): default MiniMax to M3.1 Flash --- README.md | 2 +- config/phantom.yaml | 8 +- docs/providers.md | 30 +++-- src/agent/__tests__/minimax-http.test.ts | 109 ++++++++++++++++++ src/agent/__tests__/thinking-config.test.ts | 3 +- src/agent/agent-sdk.ts | 16 +++ src/config/__tests__/minimax-defaults.test.ts | 73 ++++++++++++ src/config/providers.ts | 19 ++- 8 files changed, 235 insertions(+), 25 deletions(-) create mode 100644 src/agent/__tests__/minimax-http.test.ts create mode 100644 src/config/__tests__/minimax-defaults.test.ts diff --git a/README.md b/README.md index 15056925..870ed97b 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Phantom is not locked to any single AI backend. It ships with support for eight - **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 +- **MiniMax** - MiniMax-M3.1-Flash (default) and MiniMax-M3 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 diff --git a/config/phantom.yaml b/config/phantom.yaml index b66412b0..ffe5ebe1 100644 --- a/config/phantom.yaml +++ b/config/phantom.yaml @@ -36,14 +36,12 @@ timeout_minutes: 240 # sonnet: glm-5.1 # haiku: glm-4.5-air # -# Example: MiniMax with the two current text models +# Example: MiniMax defaults to MiniMax-M3.1-Flash for every tier. +# Set model: MiniMax-M3 to select the other current text model. +# Explicit model IDs and saved model_mappings remain supported. # 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. diff --git a/docs/providers.md b/docs/providers.md index 0afcffca..86a867e3 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -43,7 +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 | +| `minimax` | `https://api.minimax.io/anthropic` | `MINIMAX_API_KEY` | MiniMax-M3.1-Flash (default) and MiniMax-M3 | | `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 | @@ -133,25 +133,20 @@ Both the main agent and every evolution judge route through Z.AI. The `claude-so ### MiniMax -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. +The `minimax` preset defaults to `MiniMax-M3.1-Flash` for the main agent and tier aliases in both runtimes. Set `model: MiniMax-M3` to select the other current model. Explicit model IDs and saved `model_mappings`, including legacy IDs, are preserved. The default Anthropic runtime uses the Anthropic-compatible endpoint, while Murph uses the OpenAI-compatible endpoint. -| Model | Context window | Input modalities | Thinking | -|------|----------------|------------------|----------| +| Model | Context window | API input modalities | Thinking | +|------|----------------|----------------------|----------| +| `MiniMax-M3.1-Flash` | 1,000,000 tokens | text, image, video | adaptive only | | `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 for MiniMax-M3 by default. MiniMax-M3 also supports disabled thinking at the API level, while MiniMax-M2.7 always keeps thinking enabled. +Phantom explicitly requests adaptive thinking for both current models. Flash rejects disabled thinking and supports low, medium, high, xhigh, and max effort (API default: max); Phantom exposes low, medium, high, and max. With the default runtime, Flash at max uses the API default to avoid the pinned Agent SDK downgrading custom models to high. An explicit `CLAUDE_CODE_EFFORT_LEVEL` override is preserved. M3 ignores effort; its API default is disabled on Anthropic and adaptive on OpenAI. API modalities describe the model capabilities, not additional Phantom attachment support. ```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 @@ -173,17 +168,18 @@ For the OpenAI-compatible route, select Murph and use the concrete model ID: ```yaml # phantom.yaml, Murph runtime agent_runtime: murph -model: MiniMax-M3 +model: MiniMax-M3.1-Flash provider: type: minimax ``` -Pay-as-you-go prices are in USD per million tokens. +Standard pay-as-you-go prices are in USD per million tokens. M3 priority pricing is 1.5 times the standard rate. Cache-write pricing is not specified here. -| Model | Input | Output | Cache read | Cache write | -|-------|-------|--------|------------|-------------| -| `MiniMax-M3` | $0.60 | $2.40 | $0.12 | n/a | -| `MiniMax-M2.7` | $0.30 | $1.20 | $0.06 | $0.375 | +| Model | Input length | Input | Output | Cache read | +|-------|--------------|-------|--------|------------| +| `MiniMax-M3.1-Flash` | all | $0.15 | $0.60 | $0.015 | +| `MiniMax-M3` | at most 512k | $0.30 | $1.20 | $0.06 | +| `MiniMax-M3` | above 512k | $0.60 | $2.40 | $0.12 | 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. diff --git a/src/agent/__tests__/minimax-http.test.ts b/src/agent/__tests__/minimax-http.test.ts new file mode 100644 index 00000000..22733993 --- /dev/null +++ b/src/agent/__tests__/minimax-http.test.ts @@ -0,0 +1,109 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { buildAgentRuntimeEnv, resolveAgentRuntimeModel } from "../../config/providers.ts"; +import { PhantomConfigSchema } from "../../config/schemas.ts"; +import { __setAgentSdkQueryForTests, configureAgentSdkRuntime, query } from "../agent-sdk.ts"; +import { getThinkingConfig } from "../thinking-config.ts"; + +type CapturedRequest = { + model: string; + thinking?: { type: string }; + output_config?: { effort?: string }; + service_tier?: string; +}; + +// A local HTTP fixture exercises the pinned CLI's serialization, including its +// custom-model effort handling, which a query() mock cannot detect. +for (const effort of ["max", "low", "medium", "high"] as const) { + test(`MiniMax Flash serializes ${effort} effort without downgrading max`, async () => { + const directory = mkdtempSync(join(import.meta.dir, ".minimax-http-")); + const captures: CapturedRequest[] = []; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request): Promise { + if (new URL(request.url).pathname !== "/anthropic/v1/messages") { + return new Response("Unknown fixture endpoint", { status: 404 }); + } + const body = (await request.json()) as CapturedRequest; + captures.push(body); + const events = [ + { + type: "message_start", + message: { + id: "msg_fixture", + type: "message", + role: "assistant", + model: body.model, + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 0 }, + }, + }, + { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Fixture response" } }, + { type: "content_block_stop", index: 0 }, + { + type: "message_delta", + delta: { stop_reason: "end_turn", stop_sequence: null }, + usage: { output_tokens: 2 }, + }, + { type: "message_stop" }, + ]; + return new Response( + events.map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(""), + { + headers: { "Content-Type": "text/event-stream" }, + }, + ); + }, + }); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 20_000); + try { + await configureAgentSdkRuntime({ agentRuntime: "anthropic", env: {} }); + __setAgentSdkQueryForTests(null); + const config = PhantomConfigSchema.parse({ name: "fixture", effort, provider: { type: "minimax" } }); + const model = resolveAgentRuntimeModel(config, config.model); + const stream = query({ + prompt: "Return a fixture response.", + options: { + model, + effort, + thinking: getThinkingConfig(model), + cwd: directory, + tools: [], + settingSources: [], + persistSession: false, + maxTurns: 1, + abortController: controller, + env: { + ...buildAgentRuntimeEnv(config, model), + PATH: process.env.PATH, + HOME: directory, + ANTHROPIC_BASE_URL: `http://127.0.0.1:${server.port}/anthropic`, + ANTHROPIC_API_KEY: "fixture-only", + ANTHROPIC_AUTH_TOKEN: "fixture-only", + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", + DISABLE_AUTOUPDATER: "1", + DISABLE_TELEMETRY: "1", + }, + }, + }); + for await (const message of stream) { + if (message.type === "result") expect(message.is_error).toBe(false); + } + expect(captures).toHaveLength(1); + expect(captures[0]?.model).toBe("MiniMax-M3.1-Flash"); + expect(captures[0]?.thinking).toEqual({ type: "adaptive" }); + expect(captures[0]?.output_config?.effort).toBe(effort === "max" ? undefined : effort); + expect(captures[0]?.service_tier).toBeUndefined(); + } finally { + clearTimeout(timeout); + server.stop(true); + rmSync(directory, { recursive: true, force: true }); + } + }, 30_000); +} diff --git a/src/agent/__tests__/thinking-config.test.ts b/src/agent/__tests__/thinking-config.test.ts index d6cefbbf..0168e056 100644 --- a/src/agent/__tests__/thinking-config.test.ts +++ b/src/agent/__tests__/thinking-config.test.ts @@ -26,7 +26,8 @@ describe("getThinkingConfig", () => { expect(getThinkingConfig("claude-mythos-preview")).toEqual({ type: "adaptive" }); }); - test("MiniMax-M3 uses the adaptive request shape", () => { + test("current MiniMax models use the adaptive request shape", () => { + expect(getThinkingConfig("MiniMax-M3.1-Flash")).toEqual({ type: "adaptive" }); expect(getThinkingConfig("MiniMax-M3")).toEqual({ type: "adaptive" }); }); diff --git a/src/agent/agent-sdk.ts b/src/agent/agent-sdk.ts index 60b1dd33..256b7f20 100644 --- a/src/agent/agent-sdk.ts +++ b/src/agent/agent-sdk.ts @@ -73,6 +73,22 @@ export function tool( } export function query(params: AgentSdkQueryParams): Query { + const options = params.options; + // The pinned Anthropic CLI clamps max effort to high for custom model IDs. + // Omit the field for Flash's API default (max), without changing other efforts + // or an operator's explicit environment override. Murph owns its own transport. + if ( + activeQuery === anthropicQuery && + options?.model === "MiniMax-M3.1-Flash" && + options.effort === "max" && + !options.env?.CLAUDE_CODE_EFFORT_LEVEL && + !process.env.CLAUDE_CODE_EFFORT_LEVEL + ) { + return activeQuery({ + ...params, + options: { ...options, env: { ...options.env, CLAUDE_CODE_EFFORT_LEVEL: "auto" } }, + }); + } return activeQuery(params); } diff --git a/src/config/__tests__/minimax-defaults.test.ts b/src/config/__tests__/minimax-defaults.test.ts new file mode 100644 index 00000000..06ebc9af --- /dev/null +++ b/src/config/__tests__/minimax-defaults.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test"; +import { getThinkingConfig } from "../../agent/thinking-config.ts"; +import { JUDGE_MODEL_HAIKU, JUDGE_MODEL_OPUS, JUDGE_MODEL_SONNET } from "../../evolution/judge-models.ts"; +import { buildAgentRuntimeEnv, resolveAgentRuntimeModel } from "../providers.ts"; +import { PhantomConfigSchema } from "../schemas.ts"; + +for (const agent_runtime of ["anthropic", "murph"] as const) { + describe(`MiniMax defaults: ${agent_runtime}`, () => { + test("resolves the default agent and every judge tier to adaptive Flash", () => { + const config = PhantomConfigSchema.parse({ name: "test", agent_runtime, provider: { type: "minimax" } }); + for (const requested of [ + config.model, + "opus", + "sonnet", + "haiku", + JUDGE_MODEL_OPUS, + JUDGE_MODEL_SONNET, + JUDGE_MODEL_HAIKU, + ]) { + const model = resolveAgentRuntimeModel(config, requested); + expect(model).toBe("MiniMax-M3.1-Flash"); + expect(getThinkingConfig(model)).toEqual({ type: "adaptive" }); + const env = buildAgentRuntimeEnv(config, model); + if (agent_runtime === "murph") { + expect(env.MURPH_MODEL).toBe(model); + expect(env.OPENAI_BASE_URL).toBe("https://api.minimax.io/v1"); + } else { + expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe(model); + expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe(model); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe(model); + } + } + }); + + test("preserves explicit current, legacy, and custom model IDs", () => { + for (const model of ["MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "saved-custom-model"]) { + const config = PhantomConfigSchema.parse({ name: "test", agent_runtime, model, provider: { type: "minimax" } }); + expect(resolveAgentRuntimeModel(config, model)).toBe(model); + expect(resolveAgentRuntimeModel(config, JUDGE_MODEL_HAIKU)).toBe(model); + } + }); + + test("preserves partial saved mappings before the concrete model fallback", () => { + const config = PhantomConfigSchema.parse({ + name: "test", + agent_runtime, + model: "MiniMax-M3", + provider: { type: "minimax", model_mappings: { haiku: "MiniMax-M2.7", sonnet: "custom-model" } }, + }); + expect(resolveAgentRuntimeModel(config, JUDGE_MODEL_HAIKU)).toBe("MiniMax-M2.7"); + expect(resolveAgentRuntimeModel(config, "sonnet")).toBe("custom-model"); + expect(resolveAgentRuntimeModel(config, "opus")).toBe("MiniMax-M3"); + expect(resolveAgentRuntimeModel(config, "MiniMax-M3.1-Flash")).toBe("MiniMax-M3.1-Flash"); + }); + + test("fills only missing saved mappings with Flash", () => { + const config = PhantomConfigSchema.parse({ + name: "test", + agent_runtime, + provider: { type: "minimax", model_mappings: { haiku: "MiniMax-M2.7" } }, + }); + expect(resolveAgentRuntimeModel(config, "haiku")).toBe("MiniMax-M2.7"); + expect(resolveAgentRuntimeModel(config, "sonnet")).toBe("MiniMax-M3.1-Flash"); + }); + + test("leaves other provider models unchanged", () => { + for (const type of ["anthropic", "zai", "openrouter", "custom"] as const) { + const config = PhantomConfigSchema.parse({ name: "test", agent_runtime, provider: { type } }); + expect(resolveAgentRuntimeModel(config, config.model)).toBe(config.model); + } + }); + }); +} diff --git a/src/config/providers.ts b/src/config/providers.ts index 2d34fdf5..8b718ede 100644 --- a/src/config/providers.ts +++ b/src/config/providers.ts @@ -200,6 +200,16 @@ export function selectedProviderSecretEnvKey(config: PhantomConfig): string | un } export function resolveAgentRuntimeModel(config: PhantomConfig, requestedModel: string, tier?: ModelTier): string { + // Resolve before choosing thinking options, including the Haiku reflection path. + // Explicit model IDs and saved mappings remain authoritative. + if (config.provider.type === "minimax") { + const mappedTier = tier ?? tierForRequestedModel(requestedModel); + if (!mappedTier) return requestedModel; + return ( + config.provider.model_mappings?.[mappedTier] ?? + (!tierForRequestedModel(config.model) ? config.model : "MiniMax-M3.1-Flash") + ); + } if (config.agent_runtime !== "murph") { return requestedModel; } @@ -360,7 +370,14 @@ export function buildProviderEnv(config: PhantomConfig): Record // Why: the bundled cli.js reads these three vars to resolve the opus/sonnet/haiku // aliases to concrete model IDs on the chosen provider. A Z.AI user who sets // `model: opus` in phantom.yaml gets GLM-5.1 on the wire if opus is mapped here. - const mappings = provider.model_mappings; + const mappings = + provider.type === "minimax" + ? { + opus: resolveAgentRuntimeModel(config, "opus"), + sonnet: resolveAgentRuntimeModel(config, "sonnet"), + haiku: resolveAgentRuntimeModel(config, "haiku"), + } + : provider.model_mappings; if (mappings?.opus) { env.ANTHROPIC_DEFAULT_OPUS_MODEL = mappings.opus; }