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
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@
# OPENROUTER_MODEL=anthropic/claude-sonnet-4-20250514

# MINIMAX_API_KEY=...
# MINIMAX_MODEL=MiniMax-M2.7
# MINIMAX_MODEL=MiniMax-M3 # MiniMax-M2.7 is also supported
# MINIMAX_BASE_URL=https://api.minimax.io/anthropic # Global Anthropic-compatible API
# MINIMAX_BASE_URL=https://api.minimaxi.com/anthropic # China Anthropic-compatible API
# For OpenAI-compatible access, use OPENAI_API_KEY with:
# OPENAI_MODEL=MiniMax-M3 # MiniMax-M2.7 is also supported
# OPENAI_BASE_URL=https://api.minimax.io/v1 # Global API
# OPENAI_BASE_URL=https://api.minimaxi.com/v1 # China API

# MAX_TOKENS=4096 # Cap LLM completion tokens for compression / summarise calls

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,13 @@ agentmemory auto-detects from your environment. By default, no LLM calls are mad
| **Local (Ollama / LM Studio / vLLM / llama.cpp)** | `OPENAI_API_KEY=local` + `OPENAI_BASE_URL=http://localhost:11434/v1` (Ollama) or `http://localhost:1234/v1` (LM Studio) + `OPENAI_MODEL=<your model>` | Anything OpenAI-API-compatible. Zero cost, runs on your hardware. See [Local models](#local-models-ollama-lm-studio-vllm) below. |
| Claude subscription fallback | `AGENTMEMORY_ALLOW_AGENT_SDK=true` | Opt-in only. Spawns `@anthropic-ai/claude-agent-sdk` sessions — used to cause unbounded Stop-hook recursion so it is no longer the default. |

MiniMax defaults to `MiniMax-M3`; set the model variable to `MiniMax-M2.7` to use another supported model. Choose the matching protocol configuration and regional base URL:

| Protocol | Configuration | Global base URL | China base URL |
|----------|---------------|-----------------|----------------|
| Anthropic-compatible | `MINIMAX_API_KEY`, `MINIMAX_MODEL`, `MINIMAX_BASE_URL` | `https://api.minimax.io/anthropic` | `https://api.minimaxi.com/anthropic` |
| OpenAI-compatible | `OPENAI_API_KEY`, `OPENAI_MODEL`, `OPENAI_BASE_URL` | `https://api.minimax.io/v1` | `https://api.minimaxi.com/v1` |

### Local models (Ollama / LM Studio / vLLM)

agentmemory talks to any OpenAI-API-compatible server, so anything that exposes `/v1/chat/completions` works without code changes. No paid keys, no cloud, no rate limits — runs entirely on your hardware.
Expand Down
2 changes: 1 addition & 1 deletion src/cli/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const PROVIDERS: { value: string; label: string; envKey: string | null }[] = [
{ value: "openai", label: "OpenAI — gpt", envKey: "OPENAI_API_KEY" },
{ value: "gemini", label: "Google — gemini", envKey: "GEMINI_API_KEY" },
{ value: "openrouter", label: "OpenRouter — multi-model", envKey: "OPENROUTER_API_KEY" },
{ value: "minimax", label: "MiniMax — minimax-m1", envKey: "MINIMAX_API_KEY" },
{ value: "minimax", label: "MiniMax — MiniMax-M3", envKey: "MINIMAX_API_KEY" },
{ value: "skip", label: "Skip — BM25-only mode (no LLM key)", envKey: null },
];

Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function detectProvider(env: Record<string, string>): ProviderConfig {
if (hasRealValue(env["MINIMAX_API_KEY"])) {
return {
provider: "minimax",
model: env["MINIMAX_MODEL"] || "MiniMax-M2.7",
model: env["MINIMAX_MODEL"] || "MiniMax-M3",
maxTokens,
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function defaultModelFor(providerType: ProviderConfig["provider"]): string {
getEnvVar("OPENROUTER_MODEL") || "anthropic/claude-sonnet-4-20250514"
);
case "minimax":
return getEnvVar("MINIMAX_MODEL") || "MiniMax-M2.7";
return getEnvVar("MINIMAX_MODEL") || "MiniMax-M3";
case "agent-sdk":
return "claude-sonnet-4-20250514";
case "noop":
Expand Down
13 changes: 9 additions & 4 deletions src/providers/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { fetchWithTimeout } from './_fetch.js'
*
* Required env vars (loaded from ~/.agentmemory/.env or process.env):
* MINIMAX_API_KEY — your MiniMax API key
* MINIMAX_MODEL — model name (default: MiniMax-M2.7)
* MAX_TOKENS — max output tokens (default: 800; MiniMax-M2.7 needs ≤800)
* MINIMAX_MODEL — model name (default: MiniMax-M3)
* MAX_TOKENS — max output tokens (default: 4096)
*
* Optional:
* MINIMAX_BASE_URL — base URL without path (default: https://api.minimax.io/anthropic)
* MINIMAX_BASE_URL — Anthropic-compatible base URL (default: https://api.minimax.io/anthropic)
*/
export class MinimaxProvider implements MemoryProvider {
name = 'minimax'
Expand All @@ -31,6 +31,11 @@ export class MinimaxProvider implements MemoryProvider {
getEnvVar('MINIMAX_BASE_URL') || 'https://api.minimax.io/anthropic'
}

private messagesUrl(): string {
const baseUrl = this.baseUrl.replace(/\/+$/, '').replace(/\/v1$/, '')
return `${baseUrl}/v1/messages`
}

async compress(systemPrompt: string, userPrompt: string): Promise<string> {
return this.call(systemPrompt, userPrompt)
}
Expand All @@ -40,7 +45,7 @@ export class MinimaxProvider implements MemoryProvider {
}

private async call(systemPrompt: string, userPrompt: string): Promise<string> {
const url = `${this.baseUrl}/v1/messages`
const url = this.messagesUrl()
const response = await fetchWithTimeout(url, {
method: 'POST',
headers: {
Expand Down
2 changes: 1 addition & 1 deletion test/fallback-model-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe("Fallback provider model resolution (#778)", () => {
const openai = captured.find((c) => c.provider === "openai");
const minimax = captured.find((c) => c.provider === "minimax");
expect(openai?.model).toBe("gpt-5");
expect(minimax?.model).toBe("MiniMax-M2.7");
expect(minimax?.model).toBe("MiniMax-M3");
// Neither inherits the Anthropic model name.
expect(openai?.model).not.toBe("claude-sonnet-4-20250514");
expect(minimax?.model).not.toBe("claude-sonnet-4-20250514");
Expand Down
76 changes: 61 additions & 15 deletions test/minimax-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,76 @@
import { describe, expect, it, afterEach } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MinimaxProvider } from "../src/providers/minimax.js";

describe("MinimaxProvider — base URL resolution (#285)", () => {
const originalEnv = process.env["MINIMAX_BASE_URL"];
let originalBaseUrl: string | undefined;

beforeEach(() => {
originalBaseUrl = process.env["MINIMAX_BASE_URL"];
});

afterEach(() => {
if (originalEnv === undefined) {
vi.restoreAllMocks();
if (originalBaseUrl === undefined) {
delete process.env["MINIMAX_BASE_URL"];
} else {
process.env["MINIMAX_BASE_URL"] = originalEnv;
process.env["MINIMAX_BASE_URL"] = originalBaseUrl;
}
});

it("defaults to https://api.minimax.io/anthropic (not the legacy minimaxi.com host)", () => {
delete process.env["MINIMAX_BASE_URL"];
const provider = new MinimaxProvider("test-key", "MiniMax-M2.7", 800);
expect((provider as unknown as { baseUrl: string }).baseUrl).toBe(
"https://api.minimax.io/anthropic",
function mockSuccessfulResponse() {
return vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify({ content: [{ type: "text", text: "ok" }] }),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
});
}

it("sends default requests to the global Anthropic-compatible endpoint", async () => {
delete process.env["MINIMAX_BASE_URL"];
const fetchSpy = mockSuccessfulResponse();
const provider = new MinimaxProvider("test-key", "MiniMax-M3", 800);

await provider.compress("system", "user");

it("honors MINIMAX_BASE_URL via getEnvVar (merged ~/.agentmemory/.env + process.env)", () => {
process.env["MINIMAX_BASE_URL"] = "https://custom.example.com/anthropic";
const provider = new MinimaxProvider("test-key", "MiniMax-M2.7", 800);
expect((provider as unknown as { baseUrl: string }).baseUrl).toBe(
"https://custom.example.com/anthropic",
expect(fetchSpy).toHaveBeenCalledWith(
"https://api.minimax.io/anthropic/v1/messages",
expect.objectContaining({ method: "POST" }),
);
const request = fetchSpy.mock.calls[0]?.[1];
expect(JSON.parse(String(request?.body))).toMatchObject({
model: "MiniMax-M3",
});
});

it.each([
[
"China regional",
"https://api.minimaxi.com/anthropic/",
"https://api.minimaxi.com/anthropic/v1/messages",
],
[
"versioned custom",
"https://custom.example.com/anthropic/v1",
"https://custom.example.com/anthropic/v1/messages",
],
])(
"normalizes %s base URLs before sending requests",
async (_name, baseUrl, expectedUrl) => {
process.env["MINIMAX_BASE_URL"] = baseUrl;
const fetchSpy = mockSuccessfulResponse();
const provider = new MinimaxProvider("test-key", "MiniMax-M2.7", 800);

await provider.compress("system", "user");

expect(fetchSpy).toHaveBeenCalledWith(
expectedUrl,
expect.objectContaining({ method: "POST" }),
);
const request = fetchSpy.mock.calls[0]?.[1];
expect(JSON.parse(String(request?.body))).toMatchObject({
model: "MiniMax-M2.7",
});
},
);
});