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
3 changes: 3 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ requires the complete access-token triple.
|---|---|---|
| `OPENAI_API_KEY` | `--agent codex`, `--agent pi --model openai/...` | Codex SDK token or Pi OpenAI-provider token. Unset is fine if `AI_GATEWAY_API_KEY` is set. |
| `OPENAI_BASE_URL` | `--agent codex` | Default (when `AI_GATEWAY_API_KEY` is set): `https://ai-gateway.vercel.sh/v1`. |
| `MINIMAX_API_KEY` | `--agent pi --model minimax/...` | Auth for the built-in MiniMax provider preset. |
| `MINIMAX_REGION` | `--agent pi --model minimax/...` | Endpoint preset: `global` (default) or `cn`. See [models.md](models.md). |
| `MINIMAX_BASE_URL` | `--agent pi --model minimax/...` | Override the region preset with a custom base URL. |
| `PI_CODING_AGENT_DIR` | `--agent pi` | Optional Pi config/auth directory. Defaults to `~/.pi/agent`; local non-sandbox runs can reuse `auth.json` there. |
| `DEEPSEC_AGENT_DEBUG` | both backends | Set to `1` to enable verbose agent logging. |
| `DEEPSEC_DATA_ROOT` | core | Override the data directory location. Equivalent to `dataDir` in config. |
Expand Down
30 changes: 30 additions & 0 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,36 @@ Later `process`, `revalidate`, and Sandbox commands resolve the persisted
route. Per-command `--ai-provider`, `--ai-base-url`, `--ai-api-key-env`, and
repeatable `--ai-header name=value` remain available as Pi runtime overrides.

### MiniMax (`minimax/…`) presets

MiniMax ships as a built-in Pi provider preset, so you can select its
models by id without spelling out the base URL. The provider registers
`minimax/MiniMax-M3.1-Flash` first and `minimax/MiniMax-M3` second; both
have a 1M-token context window:

```bash
MINIMAX_API_KEY=...
pnpm deepsec process --project-id my-app --agent pi --model minimax/MiniMax-M3.1-Flash
```

Two regional endpoint presets are built in. The default is the global
endpoint (`https://api.minimax.io/v1`); set `MINIMAX_REGION=cn` to use
the CN endpoint (`https://api.minimaxi.com/v1`) instead:

```bash
MINIMAX_API_KEY=...
MINIMAX_REGION=cn
pnpm deepsec process --project-id my-app --agent pi --model minimax/MiniMax-M3
```

`MINIMAX_BASE_URL` (or `--ai-base-url`) overrides the region preset for a
custom endpoint. Auth comes from `MINIMAX_API_KEY`, or point
`--ai-api-key-env` at a different variable.

Flash uses adaptive reasoning with Pi's `xhigh` level mapped to `max`;
M3 uses its adaptive default and ignores reasoning-effort fields. In sandbox
mode, add the endpoint host to `allowedHosts` so worker egress can reach it.

### `claude-sonnet-4-6` for `triage`

Triage buckets findings into P0/P1/P2/skip without re-reading the code.
Expand Down
140 changes: 140 additions & 0 deletions packages/processor/src/__tests__/pi-sdk-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
configureProviderOverrides,
createPiReadOnlyToolDefinitions,
registerMiniMaxProvider,
resolvePiModelWithDynamicGateway,
} from "../agents/pi-sdk.js";

Expand Down Expand Up @@ -71,6 +72,10 @@ describe("Pi model resolution", () => {
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
"MINIMAX_REGION",
"SAVED_MINIMAX_KEY",
"ACME_MODEL_KEY",
] as const;
const originalEnv = Object.fromEntries(KEYS.map((key) => [key, process.env[key]]));
Expand Down Expand Up @@ -212,4 +217,139 @@ describe("Pi model resolution", () => {
headers: { "x-api-key": "secret" },
});
});

it("registers the Flash-first MiniMax presets and resolves them offline", async () => {
for (const key of KEYS) delete process.env[key];
let called = false;
globalThis.fetch = async () => {
called = true;
return new Response(JSON.stringify({ data: [] }), { status: 200 });
};

const registry = await freshRegistry();
registerMiniMaxProvider(registry, {});

const flash = await resolvePiModelWithDynamicGateway(
registry,
"minimax/MiniMax-M3.1-Flash",
{},
);
expect(flash.provider).toBe("minimax");
expect(flash.id).toBe("MiniMax-M3.1-Flash");
expect(flash.api).toBe("openai-completions");
expect(flash.reasoning).toBe(true);
expect(flash.thinkingLevelMap).toEqual({ xhigh: "max" });
expect(flash.input).toEqual(["text", "image"]);
expect(flash.contextWindow).toBe(1_000_000);
expect(flash.maxTokens).toBe(524_288);
expect(flash.cost).toMatchObject({ input: 0.15, output: 0.6, cacheRead: 0.015 });

const m3 = await resolvePiModelWithDynamicGateway(registry, "minimax/MiniMax-M3", {});
expect(m3.provider).toBe("minimax");
expect(m3.id).toBe("MiniMax-M3");
expect(m3.contextWindow).toBe(1_000_000);
expect(m3.thinkingLevelMap).toBeUndefined();
expect(m3.cost).toMatchObject({ input: 0.3, output: 1.2, cacheRead: 0.06 });

expect(called).toBe(false);
});

it("selects the CN MiniMax endpoint preset via MINIMAX_REGION", async () => {
for (const key of KEYS) delete process.env[key];
process.env.MINIMAX_REGION = "cn";

const registry = await freshRegistry();
registerMiniMaxProvider(registry, {});

expect(registry.getRegisteredProviderConfig("minimax")?.baseUrl).toBe(
"https://api.minimaxi.com/v1",
);
});

it("defaults MiniMax to the global endpoint preset", async () => {
for (const key of KEYS) delete process.env[key];

const registry = await freshRegistry();
registerMiniMaxProvider(registry, {});

expect(registry.getRegisteredProviderConfig("minimax")?.baseUrl).toBe(
"https://api.minimax.io/v1",
);
});

it("lets --ai-base-url override the MiniMax region preset", async () => {
for (const key of KEYS) delete process.env[key];

const registry = await freshRegistry();
registerMiniMaxProvider(registry, { aiBaseUrl: "https://example.test/v1" });

expect(registry.getRegisteredProviderConfig("minimax")?.baseUrl).toBe(
"https://example.test/v1",
);
});

it("keeps a saved custom MiniMax model route and key override intact", async () => {
for (const key of KEYS) delete process.env[key];
process.env.SAVED_MINIMAX_KEY = "saved-key";

const registry = await freshRegistry();
registerMiniMaxProvider(registry, {
model: "minimax/custom-saved",
aiProvider: "minimax",
aiBaseUrl: "https://saved.example/v1",
aiApiKeyEnv: "SAVED_MINIMAX_KEY",
aiHeaders: { "x-saved": "yes" },
});

expect(registry.getRegisteredProviderConfig("minimax")).toMatchObject({
baseUrl: "https://saved.example/v1",
apiKey: "saved-key",
headers: { "x-saved": "yes" },
});
});

it("does not leak another provider's custom route or credential into MiniMax", async () => {
for (const key of KEYS) delete process.env[key];
process.env.ACME_MODEL_KEY = "acme-key";
process.env.MINIMAX_API_KEY = "minimax-key";

const registry = await freshRegistry();
registerMiniMaxProvider(registry, {
aiProvider: "acme",
aiBaseUrl: "https://models.acme.test/v1",
aiApiKeyEnv: "ACME_MODEL_KEY",
aiHeaders: { "x-acme": "yes" },
});

expect(registry.getRegisteredProviderConfig("minimax")).toMatchObject({
baseUrl: "https://api.minimax.io/v1",
apiKey: "minimax-key",
});
expect(registry.getRegisteredProviderConfig("minimax")?.headers).toBeUndefined();
});

it("keeps request discovery offline for MiniMax presets", async () => {
for (const key of KEYS) delete process.env[key];
process.env.MINIMAX_API_KEY = "fixture-key";
let called = false;
globalThis.fetch = async () => {
called = true;
throw new Error("network boundary must not be reached");
};

const registry = await freshRegistry();
registerMiniMaxProvider(registry, {});
const model = await resolvePiModelWithDynamicGateway(
registry,
"minimax/MiniMax-M3.1-Flash",
{},
);

expect(model.provider).toBe("minimax");
expect(registry.getRegisteredProviderConfig("minimax")).toMatchObject({
baseUrl: "https://api.minimax.io/v1",
apiKey: "fixture-key",
});
expect(called).toBe(false);
});
});
81 changes: 81 additions & 0 deletions packages/processor/src/agents/pi-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,83 @@ type PiModel = ReturnType<ModelRegistry["getAll"]>[number];
type PiProviderConfig = Parameters<ModelRegistry["registerProvider"]>[1];
type PiProviderModelConfig = NonNullable<PiProviderConfig["models"]>[number];

// MiniMax exposes an OpenAI-compatible endpoint on two regional hosts that
// share the same model catalog. Registering the provider locally keeps model
// resolution offline; the selected route is still used by Pi at request time.
const MINIMAX_PROVIDER = "minimax";
const MINIMAX_API = "openai-completions" as const;
const MINIMAX_REGION_BASE_URLS = {
global: "https://api.minimax.io/v1",
cn: "https://api.minimaxi.com/v1",
} as const;
type MiniMaxRegion = keyof typeof MINIMAX_REGION_BASE_URLS;
const MINIMAX_DEFAULT_REGION: MiniMaxRegion = "global";
// Output cap applied on the deepsec side; matches the generic pass-through
// default. deepsec's investigation JSON fits well under this.
const MINIMAX_FLASH_MAX_TOKENS = 524_288;
const MINIMAX_DEEPSEC_MAX_TOKENS = 32_000;

// Costs are USD per million tokens, matching pi's provider model contract.
// Pi only exposes text/image input labels; the MiniMax API's video capability
// remains outside this text-agent integration.
const MINIMAX_MODELS: PiProviderModelConfig[] = [
{
id: "MiniMax-M3.1-Flash",
name: "MiniMax-M3.1-Flash",
api: MINIMAX_API,
reasoning: true,
thinkingLevelMap: { xhigh: "max" },
input: ["text", "image"],
cost: { input: 0.15, output: 0.6, cacheRead: 0.015, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: MINIMAX_FLASH_MAX_TOKENS,
},
{
id: "MiniMax-M3",
name: "MiniMax-M3",
api: MINIMAX_API,
reasoning: true,
input: ["text", "image"],
cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: MINIMAX_DEEPSEC_MAX_TOKENS,
},
];

function isMiniMaxRoute(cfg: PiAgentConfig): boolean {
return (cfg.aiProvider ?? modelProviderFromName(cfg.model)) === MINIMAX_PROVIDER;
}

function resolveMiniMaxBaseUrl(cfg: PiAgentConfig): string {
if (cfg.aiBaseUrl && isMiniMaxRoute(cfg)) return cfg.aiBaseUrl;
if (process.env.MINIMAX_BASE_URL) return process.env.MINIMAX_BASE_URL;
const region = process.env.MINIMAX_REGION as MiniMaxRegion | undefined;
if (region && region in MINIMAX_REGION_BASE_URLS) return MINIMAX_REGION_BASE_URLS[region];
return MINIMAX_REGION_BASE_URLS[MINIMAX_DEFAULT_REGION];
}

/**
* Register the built-in MiniMax provider preset (regional base URL + model
* catalog). Auth is layered separately in configureRuntimeAuth, or via the
* generic --ai-api-key-env override; a missing key just leaves the provider
* unauthenticated. Registering an unused provider is cheap.
*/
export function registerMiniMaxProvider(registry: ModelRegistry, cfg: PiAgentConfig): void {
const override: PiProviderConfig = {
baseUrl: resolveMiniMaxBaseUrl(cfg),
api: MINIMAX_API,
models: MINIMAX_MODELS,
};
const key = isMiniMaxRoute(cfg) && cfg.aiApiKeyEnv
? process.env[cfg.aiApiKeyEnv]
: process.env.MINIMAX_API_KEY;
if (key) override.apiKey = key;
if (isMiniMaxRoute(cfg) && cfg.aiHeaders && Object.keys(cfg.aiHeaders).length > 0) {
override.headers = cfg.aiHeaders;
}
registry.registerProvider(MINIMAX_PROVIDER, override);
}

interface PiSessionSetup {
session: AgentSession;
modelLabel: string;
Expand Down Expand Up @@ -342,6 +419,9 @@ async function configureRuntimeAuth(runtime: ModelRuntime, cfg: PiAgentConfig):
const openaiKey = process.env.OPENAI_API_KEY;
if (openaiKey) await runtime.setRuntimeApiKey("openai", openaiKey, offline);

const minimaxKey = process.env.MINIMAX_API_KEY;
if (minimaxKey) await runtime.setRuntimeApiKey(MINIMAX_PROVIDER, minimaxKey, offline);

const customProvider = cfg.aiProvider ?? modelProviderFromName(cfg.model);
if (customProvider && cfg.aiApiKeyEnv) {
const key = process.env[cfg.aiApiKeyEnv];
Expand Down Expand Up @@ -542,6 +622,7 @@ async function createPiSession(projectRoot: string, cfg: PiAgentConfig): Promise
// their tests) on the same ModelRegistry API as before.
const modelRegistry = new ModelRegistry(runtime);
configureProviderOverrides(modelRegistry, cfg);
registerMiniMaxProvider(modelRegistry, cfg);

const modelName = cfg.model ?? DEFAULT_MODEL;
const model = await resolvePiModelWithDynamicGateway(modelRegistry, modelName, cfg);
Expand Down