Skip to content
Closed
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
11 changes: 10 additions & 1 deletion config/phantom.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
58 changes: 58 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -130,6 +131,63 @@ ZAI_API_KEY=<your-zai-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=<your-minimax-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.
Expand Down
33 changes: 33 additions & 0 deletions src/config/__tests__/loader-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/config/__tests__/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
63 changes: 60 additions & 3 deletions src/config/__tests__/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -173,6 +184,24 @@ describe("buildProviderEnv: zai preset", () => {
});
});

describe("buildProviderEnv: MiniMax preset", () => {
test("sets the SDK base URL, 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");
});
});

describe("buildProviderEnv: ollama preset", () => {
test("sets local base_url, no auth token, disables betas", () => {
const config = makeConfig({ type: "ollama" });
Expand Down Expand Up @@ -316,6 +345,24 @@ describe("buildAgentRuntimeEnv: Murph runtime", () => {
expect(env.ANTHROPIC_API_KEY).toBe("");
});

test("emits MiniMax 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("minimax");
expect(env.MURPH_MODEL).toBe("MiniMax-M3");
expect(env.OPENAI_BASE_URL).toBe("https://api.minimax.io/v1");
expect(env.MINIMAX_API_KEY).toBe("minimax-secret");
expect(env.OPENAI_API_KEY).toBe("");
});

test("emits OpenRouter route env", () => {
process.env.OPENROUTER_API_KEY = "openrouter-secret";
const config = makeConfig(
Expand Down Expand Up @@ -378,7 +425,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();
}
});
Expand All @@ -389,7 +446,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);
}
});
Expand Down
14 changes: 14 additions & 0 deletions src/config/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const PROVIDER_TYPES = [
"anthropic",
"openai",
"zai",
"minimax",
"openrouter",
"vllm",
"ollama",
Expand Down Expand Up @@ -86,6 +87,11 @@ export const PROVIDER_PRESETS: Readonly<Record<ProviderType, ProviderPreset>> =
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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 = "minimax";
env.OPENAI_BASE_URL = provider.base_url ?? "https://api.minimax.io/v1";
addSelectedCredential(env, sourceEnvKey, "MINIMAX_API_KEY");
break;
}
case "openrouter": {
env.MURPH_PROVIDER = "openai-compat";
env.MURPH_PROVIDER_CONFIG = "openrouter";
Expand Down