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: 4 additions & 4 deletions commands/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Parse `$ARGUMENTS` to identify what the user wants. If no arguments are given, s
2. Display all settings clearly:

**General**
- Model: (e.g. `opus`, `sonnet`, `haiku`, `glm` or "default")
- API token: (first 5 chars + "..." or "not configured"; used when `model` is `glm`)
- Model: (e.g. `opus`, `sonnet`, `haiku`, `glm`, `minimax` or "default")
- API token: (first 5 chars + "..." or "not configured"; used when `model` is `glm` or a `minimax` preset)
- Fallback model: (e.g. `glm`, `sonnet`, or "not configured")
- Fallback API token: (first 5 chars + "..." or "not configured")
- Timezone: (e.g. `America/New_York` or "UTC")
Expand Down Expand Up @@ -285,8 +285,8 @@ Location: `.claude/claudeclaw/settings.json`

| Key | Type | Description |
|----------------------------|------------|------------------------------------------------|
| `model` | string | Claude model (`opus`, `sonnet`, `haiku`, `glm`, or full ID). Empty = default |
| `api` | string | API token used when model is `glm` (mapped to `ANTHROPIC_AUTH_TOKEN`) |
| `model` | string | Claude model (`opus`, `sonnet`, `haiku`, `glm`, `minimax`/`minimax-m3`/`minimax-m2.7`, or full ID). Append `-cn` to a MiniMax alias for the China endpoint. Empty = default |
| `api` | string | API token used when model is `glm` or a `minimax` preset (mapped to `ANTHROPIC_AUTH_TOKEN`) |
| `fallback.model` | string | Backup model used automatically if primary run returns rate-limit text (recommend `glm` for provider diversity) |
| `fallback.api` | string | API token used with `fallback.model` (optional) |
| `timezone` | string | IANA timezone name (e.g. `America/New_York`) |
Expand Down
4 changes: 2 additions & 2 deletions commands/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ Defaults: `WEB_HOST=127.0.0.1`, `WEB_PORT=4632` unless changed via settings or `
}
}
```
- `model` — Claude model to use (`opus`, `sonnet`, `haiku`, `glm`, or full model ID). Empty string uses default. Ignored when `agentic.enabled` is true.
- `api` — API token used when `model` is `glm` (passed as `ANTHROPIC_AUTH_TOKEN` for that provider path).
- `model` — Claude model to use (`opus`, `sonnet`, `haiku`, `glm`, `minimax`/`minimax-m3`/`minimax-m2.7`, or full model ID). Append `-cn` to a MiniMax alias to use the China endpoint. Empty string uses default. Ignored when `agentic.enabled` is true.
- `api` — API token used when `model` is `glm` or a `minimax` preset (passed as `ANTHROPIC_AUTH_TOKEN` for that provider path).
- `fallback.model` — backup model used automatically if the primary run returns a rate-limit message. Prefer `glm` for provider diversity.
- `fallback.api` — optional API token to use with `fallback.model`.
- `agentic.enabled` — when true, automatically routes tasks to appropriate models based on task type
Expand Down
71 changes: 71 additions & 0 deletions src/__tests__/provider-preset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect } from "bun:test";
import { resolveModelArg, resolveMinimaxModel, buildChildEnv } from "../runner";

describe("resolveMinimaxModel", () => {
it("maps the bare minimax alias to the latest model on the global endpoint", () => {
expect(resolveMinimaxModel("minimax")).toEqual({
baseUrl: "https://api.minimax.io/anthropic",
modelId: "MiniMax-M3",
});
});

it("maps explicit MiniMax model aliases", () => {
expect(resolveMinimaxModel("minimax-m3")?.modelId).toBe("MiniMax-M3");
expect(resolveMinimaxModel("minimax-m2.7")?.modelId).toBe("MiniMax-M2.7");
});

it("routes to the China endpoint with the -cn suffix", () => {
expect(resolveMinimaxModel("minimax-m3-cn")).toEqual({
baseUrl: "https://api.minimaxi.com/anthropic",
modelId: "MiniMax-M3",
});
expect(resolveMinimaxModel("minimax-cn")?.baseUrl).toBe(
"https://api.minimaxi.com/anthropic",
);
});

it("is case-insensitive and trims surrounding whitespace", () => {
expect(resolveMinimaxModel(" MiniMax-M3 ")?.modelId).toBe("MiniMax-M3");
});

it("returns null for non-preset and unknown MiniMax models", () => {
expect(resolveMinimaxModel("opus")).toBeNull();
expect(resolveMinimaxModel("minimax-m9")).toBeNull();
});
});

describe("resolveModelArg", () => {
it("passes MiniMax presets through as canonical model IDs", () => {
expect(resolveModelArg("minimax-m2.7")).toBe("MiniMax-M2.7");
expect(resolveModelArg("minimax-m3-cn")).toBe("MiniMax-M3");
});

it("suppresses the flag for empty models", () => {
expect(resolveModelArg(" ")).toBeNull();
});

it("passes standard models through unchanged", () => {
expect(resolveModelArg("opus")).toBe("opus");
});
});

describe("buildChildEnv", () => {
it("routes MiniMax models to the selected Anthropic-compatible endpoint", () => {
expect(buildChildEnv({}, "minimax-m3", "token").ANTHROPIC_BASE_URL).toBe(
"https://api.minimax.io/anthropic",
);
expect(buildChildEnv({}, "minimax-m2.7-cn", "token").ANTHROPIC_BASE_URL).toBe(
"https://api.minimaxi.com/anthropic",
);
});

it("passes the api token through as ANTHROPIC_AUTH_TOKEN", () => {
expect(buildChildEnv({}, "minimax", "example-token").ANTHROPIC_AUTH_TOKEN).toBe(
"example-token",
);
});

it("leaves the base URL unset for default models", () => {
expect(buildChildEnv({}, "opus", "").ANTHROPIC_BASE_URL).toBeUndefined();
});
});
72 changes: 63 additions & 9 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,56 @@ function isNotFoundError(error: unknown): boolean {
return /enoent|no such file or directory/i.test(message);
}

function buildChildEnv(baseEnv: Record<string, string>, model: string, api: string): Record<string, string> {
/** Anthropic-compatible base URLs for the MiniMax provider preset. */
const MINIMAX_ANTHROPIC_BASE_URLS = {
global: "https://api.minimax.io/anthropic",
cn: "https://api.minimaxi.com/anthropic",
} as const;

/** Canonical MiniMax model IDs, keyed by the accepted `model` setting aliases. */
const MINIMAX_MODEL_IDS: Record<string, string> = {
minimax: "MiniMax-M3",
"minimax-m3": "MiniMax-M3",
"minimax-m2.7": "MiniMax-M2.7",
};

/**
* Resolve the MiniMax provider preset for a configured `model` string.
*
* Users select a MiniMax model with `minimax`, `minimax-m3`, or `minimax-m2.7`
* (the bare `minimax` alias maps to the latest MiniMax-M3). Append `-cn` to any
* of those to route to the China endpoint instead of the global one. Returns
* null when the model is not a MiniMax preset.
*/
export function resolveMinimaxModel(model: string): { baseUrl: string; modelId: string } | null {
let normalized = model.trim().toLowerCase();
if (!normalized.startsWith("minimax")) return null;

let baseUrl = MINIMAX_ANTHROPIC_BASE_URLS.global;
if (normalized.endsWith("-cn")) {
baseUrl = MINIMAX_ANTHROPIC_BASE_URLS.cn;
normalized = normalized.slice(0, -"-cn".length);
}

const modelId = MINIMAX_MODEL_IDS[normalized];
return modelId ? { baseUrl, modelId } : null;
}

/**
* Resolve the value passed to the Claude CLI `--model` flag, or null when no
* flag should be added. GLM uses its provider default (no flag); MiniMax presets
* are rewritten to their canonical model IDs; anything else is passed through.
*/
export function resolveModelArg(model: string): string | null {
const trimmed = model.trim();
if (!trimmed) return null;
if (trimmed.toLowerCase() === "glm") return null;
const minimax = resolveMinimaxModel(trimmed);
if (minimax) return minimax.modelId;
return trimmed;
}

export function buildChildEnv(baseEnv: Record<string, string>, model: string, api: string): Record<string, string> {
const childEnv: Record<string, string> = { ...baseEnv };
const normalizedModel = model.trim().toLowerCase();

Expand All @@ -334,6 +383,11 @@ function buildChildEnv(baseEnv: Record<string, string>, model: string, api: stri
childEnv.API_TIMEOUT_MS = "3000000";
}

const minimax = resolveMinimaxModel(model);
if (minimax) {
childEnv.ANTHROPIC_BASE_URL = minimax.baseUrl;
}

return childEnv;
}

Expand Down Expand Up @@ -414,8 +468,8 @@ async function runClaudeOnce(
cwd?: string
): Promise<{ rawStdout: string; stderr: string; exitCode: number }> {
const args = [...baseArgs];
const normalizedModel = model.trim().toLowerCase();
if (model.trim() && normalizedModel !== "glm") args.push("--model", model.trim());
const modelArg = resolveModelArg(model);
if (modelArg) args.push("--model", modelArg);

const proc = Bun.spawn(args, {
stdout: "pipe",
Expand Down Expand Up @@ -483,8 +537,8 @@ async function runClaudeStream(
onToolEvent?: (line: string) => void
): Promise<{ rawStdout: string; stderr: string; exitCode: number; sessionId?: string }> {
const args = [...baseArgs];
const normalizedModel = model.trim().toLowerCase();
if (model.trim() && normalizedModel !== "glm") args.push("--model", model.trim());
const modelArg = resolveModelArg(model);
if (modelArg) args.push("--model", modelArg);

const proc = Bun.spawn(args, {
stdout: "pipe",
Expand Down Expand Up @@ -635,8 +689,8 @@ async function runClaudeStreaming(
onToolEvent?: (line: string) => void
): Promise<{ result: string; stderr: string; exitCode: number; sessionId?: string; isRateLimit: boolean }> {
const args = [...baseArgs];
const normalizedModel = model.trim().toLowerCase();
if (model.trim() && normalizedModel !== "glm") args.push("--model", model.trim());
const modelArg = resolveModelArg(model);
if (modelArg) args.push("--model", modelArg);

const proc = Bun.spawn(args, {
stdout: "pipe",
Expand Down Expand Up @@ -1502,8 +1556,8 @@ async function streamClaude(
args.push("--append-system-prompt", appendParts.join("\n\n"));
}

const normalizedModel = model.trim().toLowerCase();
if (model.trim() && normalizedModel !== "glm") args.push("--model", model.trim());
const modelArg = resolveModelArg(model);
if (modelArg) args.push("--model", modelArg);

const childEnv = buildChildEnv(cleanSpawnEnv(), model, api);

Expand Down
Loading