Skip to content
31 changes: 28 additions & 3 deletions docs/models.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Models"
description: "Choose Codex, Claude, or Pi for process and revalidate runs, and compare models under the same workload."
description: "Choose Codex, Claude, Pi, or Grok for process and revalidate runs, and compare models under the same workload."
---

deepsec talks to LLMs through interchangeable agent backends:
Expand All @@ -9,7 +9,8 @@ deepsec talks to LLMs through interchangeable agent backends:
|-----------------------------|-----------------------|------------------------------|
| `codex` (default) | `gpt-5.5` | `process`, `revalidate` |
| `claude` | `claude-opus-4-8` | `process`, `revalidate` |
| `pi` | `zai/glm-5.2` | `process`, `revalidate` |
| `pi` | `zai/glm-5.2` | `process`, `revalidate` |
| `grok` | `grok-4.6` | `process`, `revalidate` |
| `claude` (triage) | `claude-sonnet-4-6` | `triage` (Claude-only) |

Interactive one-shot setup recommends five benchmark-backed combinations:
Expand Down Expand Up @@ -40,7 +41,8 @@ npx deepsec init --yes --model-profile value --output jsonl
```

Direct OpenAI and Anthropic credentials automatically restrict profiles to a
compatible Codex or Claude harness; custom routes restrict them to Pi.
compatible Codex or Claude harness; custom routes restrict them to Pi. Grok
uses `XAI_API_KEY` or a prior `grok login` rather than a gateway route.

The built-in backends work with Vercel AI Gateway through the linked
workspace's OIDC credential. The model credential route is independent of the
Expand Down Expand Up @@ -76,6 +78,12 @@ pnpm deepsec process --project-id my-app --agent pi
# Pi with an AI SDK / AI Gateway style model id:
pnpm deepsec process --project-id my-app --agent pi --model zai/glm-5.2

# Grok Build CLI (local), default model:
pnpm deepsec process --project-id my-app --agent grok

# Grok Build CLI, specific model:
pnpm deepsec process --project-id my-app --agent grok --model grok-4.6

# Triage uses Claude; pass a cheaper model if you want:
pnpm deepsec triage --project-id my-app --model claude-haiku-4-5
```
Expand Down Expand Up @@ -105,6 +113,7 @@ The flag maps onto each backend's native dial:
|----------|---------------------------------------------|
| `codex` | model reasoning effort (`minimal`–`xhigh`) |
| `pi` | thinking level (`minimal`–`xhigh`) |
| `grok` | `--reasoning-effort` (`minimal`–`xhigh`) |
| `claude` | adaptive-thinking effort (`minimal` → `low`, `xhigh` → `max`) |

It applies to the main investigation/revalidation runs only.
Expand Down Expand Up @@ -171,6 +180,22 @@ 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.

### Grok Build for local CLI runs

Grok spawns the local [Grok Build](https://x.ai/cli) CLI (`grok`) headlessly
with the same deepsec prompt/schema as the other backends. Default model is
`grok-4.6`. Auth is `XAI_API_KEY` or a prior `grok login` (the binary must be
on `PATH`, or set `GROK_EXECUTABLE`):

```bash
export XAI_API_KEY=xai-...
# or: grok login
pnpm deepsec process --project-id my-app --agent grok
```

Sandbox mode does not support `--agent grok` yet. Use a local `process` or
`revalidate` run instead.

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

Triage buckets findings into P0/P1/P2/skip without re-reading the code.
Expand Down
2 changes: 2 additions & 0 deletions packages/deepsec/src/__tests__/agent-defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ describe("defaultModelForAgent", () => {
expect(defaultModelForAgent("codex")).toBe("gpt-5.5");
expect(defaultModelForAgent("pi")).toBe("zai/glm-5.2");
expect(defaultModelForAgent("claude-agent-sdk")).toBe("claude-opus-4-8");
expect(defaultModelForAgent("grok")).toBe("grok-4.6");
expect(defaultModelForAgent("grok-build")).toBe("grok-4.6");
});

it("uses the model persisted for the configured harness", () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/deepsec/src/__tests__/model-picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("DeepSecBench model picker", () => {
it("infers provider-prefixed OpenAI and Anthropic slugs before generic Pi slugs", () => {
expect(inferModelHarness("openai/gpt-5.6-sol")).toBe("codex");
expect(inferModelHarness("anthropic/claude-opus-5")).toBe("claude");
expect(inferModelHarness("xai/grok-4.5")).toBe("pi");
expect(inferModelHarness("xai/grok-4.5")).toBe("grok");
});
it("uses the highest-scoring combo for each recommendation and normalizes price", () => {
const choices = buildRecommendedModelChoices(results);
Expand Down
20 changes: 20 additions & 0 deletions packages/deepsec/src/__tests__/model-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@ import { describe, expect, it, vi } from "vitest";
import { applyResolvedModelRoute, resolveModelRoute } from "../auth/model-route.js";

describe("resolveModelRoute", () => {
it("resolves Grok Build to an XAI route independent of the stored gateway config", async () => {
const resolved = await resolveModelRoute(
{ mode: "gateway", provider: "vercel" },
{ agentType: "grok", env: { XAI_API_KEY: "xai-secret" } },
);
expect(resolved.route.provider).toBe("xai");
expect(resolved.environment.XAI_API_KEY).toBe("xai-secret");
expect(resolved.broker.host).toBe("api.x.ai");
});

it("allows Grok Build without XAI_API_KEY (OAuth / grok login)", async () => {
const resolved = await resolveModelRoute(
{ mode: "gateway", provider: "vercel" },
{ agentType: "grok", env: {} },
);
expect(resolved.route.provider).toBe("xai");
expect(resolved.credential).toBe("");
expect(resolved.environment.XAI_API_KEY).toBeUndefined();
});

it("refuses to resolve a local-subscription route to a brokered credential", async () => {
await expect(
resolveModelRoute({ mode: "local", provider: "local" }, { agentType: "codex", env: {} }),
Expand Down
3 changes: 3 additions & 0 deletions packages/deepsec/src/agent-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export function defaultModelForAgent(agentType: string): string {
return "gpt-5.5";
case "pi":
return "zai/glm-5.2";
case "grok":
case "grok-build":
return "grok-4.6";
default:
return "claude-opus-4-8";
}
Expand Down
7 changes: 6 additions & 1 deletion packages/deepsec/src/auth/ensure-connected-workspace.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { join } from "node:path";
import { getVercelOidcToken } from "@vercel/oidc";
import { updateEnvFile } from "../env-file.js";
import { assertSandboxCredential } from "../preflight.js";
import { assertAgentCredential, assertSandboxCredential } from "../preflight.js";
import {
applyResolvedModelRoute,
isGrokAgent,
type ModelRoute,
type ModelRouteVerifier,
type ResolvedModelRoute,
Expand Down Expand Up @@ -155,6 +156,9 @@ export async function ensureConnectedWorkspace(

const resolvedRoutes: ResolvedModelRoute[] = [];
for (const agentType of options.agentTypes) {
if (isGrokAgent(agentType)) {
assertAgentCredential(agentType);
}
const resolved = await (deps.resolveRoute ?? resolveModelRoute)(options.modelRoute, {
agentType,
env,
Expand All @@ -175,6 +179,7 @@ export async function ensureConnectedWorkspace(
);
if (!reuseModel) {
for (const resolved of resolvedRoutes) {
if (resolved.route.provider === "xai" && !resolved.credential) continue;
await (deps.verifyModelRoute ?? verifyModelRouteWithFetch)(resolved);
}
}
Expand Down
53 changes: 43 additions & 10 deletions packages/deepsec/src/auth/model-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { ModelRoute } from "./model-route.js";
export const DEEPSEC_BENCHMARK_URL =
"https://vercel.com/ai-gateway/leaderboards/deepsecbench/results.json";

export type ModelHarness = "codex" | "claude" | "pi";
export type ModelHarness = "codex" | "claude" | "pi" | "grok";

export interface BenchmarkResult {
rank: number;
Expand Down Expand Up @@ -108,7 +108,10 @@ function isBenchmarkResult(value: unknown): value is BenchmarkResult {
typeof result.modelId === "string" &&
typeof result.score === "number" &&
typeof result.cost === "number" &&
(result.harness === "codex" || result.harness === "claude" || result.harness === "pi")
(result.harness === "codex" ||
result.harness === "claude" ||
result.harness === "pi" ||
result.harness === "grok")
);
}

Expand Down Expand Up @@ -136,7 +139,11 @@ function strongest(results: BenchmarkResult[], modelId: string): BenchmarkResult
}

function configuredModel(result: BenchmarkResult): string {
return result.harness === "pi" ? result.modelId : result.model;
if (result.harness === "pi") return result.modelId;
if (result.harness === "grok") {
return result.modelId.startsWith("xai/") ? result.modelId.slice("xai/".length) : result.model;
}
return result.model;
}

function thinkingLevel(reasoning: string): string | undefined {
Expand All @@ -161,14 +168,21 @@ export function buildRecommendedModelChoices(results: BenchmarkResult[]): Recomm

function canonicalHarness(value: string | undefined): ModelHarness | undefined {
if (value === "claude-agent-sdk" || value === "claude") return "claude";
if (value === "grok-build" || value === "grok") return "grok";
if (value === "codex" || value === "pi") return value;
return undefined;
}

function compatibleHarness(route: ModelRoute, requested?: string): ModelHarness | undefined {
if (route.mode === "direct") return route.provider === "anthropic" ? "claude" : "codex";
const requestedHarness = canonicalHarness(requested);
if (requestedHarness === "grok") return "grok";
if (route.mode === "direct") {
if (route.provider === "anthropic") return "claude";
if (route.provider === "xai" || route.provider === "grok") return "grok";
return "codex";
}
if (route.mode === "custom") return "pi";
return canonicalHarness(requested);
return requestedHarness;
}

export function parseModelProfile(value: string | undefined): ModelProfile | undefined {
Expand Down Expand Up @@ -229,12 +243,14 @@ export async function resolveModelProfile(options: {
export function inferModelHarness(slug: string): ModelHarness {
if (/^(?:openai\/)?gpt-/i.test(slug)) return "codex";
if (/^(?:anthropic\/)?claude-/i.test(slug)) return "claude";
if (/^(?:xai\/)?grok-/i.test(slug)) return "grok";
if (slug.includes("/")) return "pi";
return "pi";
}

function modelForHarness(slug: string, harness: ModelHarness): string {
if (harness === "codex" && slug.startsWith("openai/")) return slug.slice("openai/".length);
if (harness === "grok" && slug.startsWith("xai/")) return slug.slice("xai/".length);
if (harness === "claude" && slug.startsWith("anthropic/")) {
return slug.slice("anthropic/".length);
}
Expand All @@ -244,6 +260,7 @@ function modelForHarness(slug: string, harness: ModelHarness): string {
function displayHarness(harness: ModelHarness): string {
if (harness === "claude") return "Claude";
if (harness === "codex") return "Codex";
if (harness === "grok") return "Grok Build";
return "Pi";
}

Expand All @@ -261,9 +278,23 @@ export async function promptForModelSelection(options: {
const benchmark = await fetchBenchmarkResults(options.fetchImpl);
const requiredHarness = compatibleHarness(options.route, options.agent);
const recommendations = buildRecommendedModelChoices(benchmark.results);
const choices = recommendations.filter(
(choice) => !requiredHarness || choice.agent === requiredHarness,
);
const choices = recommendations
.map((choice) => {
if (
requiredHarness === "grok" &&
choice.agent !== "grok" &&
/^(?:xai\/)?grok-/i.test(choice.modelId)
) {
return {
...choice,
agent: "grok" as const,
harness: "grok" as const,
configuredModel: modelForHarness(choice.modelId, "grok"),
};
}
return choice;
})
.filter((choice) => !requiredHarness || choice.agent === requiredHarness);
const prompt = createInterface({ input: process.stdin, output: process.stdout });
try {
if (choices.length < recommendations.length) {
Expand Down Expand Up @@ -300,10 +331,12 @@ export async function promptForModelSelection(options: {
let agent = requiredHarness ?? inferModelHarness(slug);
if (!requiredHarness) {
const harnessAnswer = (
await prompt.question(`Agent harness (codex, claude, pi) [${agent}]: `)
await prompt.question(`Agent harness (codex, claude, pi, grok) [${agent}]: `)
).trim();
const selectedHarness = canonicalHarness(harnessAnswer || agent);
if (!selectedHarness) throw new Error("Agent harness must be codex, claude, or pi");
if (!selectedHarness) {
throw new Error("Agent harness must be codex, claude, pi, or grok");
}
agent = selectedHarness;
}
const level = (await prompt.question("Thinking level [medium]: ")).trim() || "medium";
Expand Down
38 changes: 38 additions & 0 deletions packages/deepsec/src/auth/model-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const DEFAULTS: Record<string, { env: string; baseUrl: string }> = {
env: "OPENAI_API_KEY",
baseUrl: "https://api.openai.com/v1",
},
xai: {
env: "XAI_API_KEY",
baseUrl: "https://api.x.ai/v1",
},
};

function checkedUrl(value: string, label: string): URL {
Expand All @@ -62,10 +66,15 @@ function checkedUrl(value: string, label: string): URL {
return url;
}

export function isGrokAgent(agentType: string): boolean {
return agentType === "grok" || agentType === "grok-build";
}

export function modelRouteCompatibilityError(
route: ModelRoute,
agentType: string,
): string | undefined {
if (isGrokAgent(agentType)) return undefined;
if (route.mode === "custom" && agentType !== "pi") {
return `Custom model routes require --agent pi (received ${agentType})`;
}
Expand All @@ -78,6 +87,30 @@ export function modelRouteCompatibilityError(
return undefined;
}

function resolveGrokModelRoute(env: NodeJS.ProcessEnv): ResolvedModelRoute {
const credentialEnv = "XAI_API_KEY";
const credential = env[credentialEnv] ?? "";
return {
route: {
mode: "direct",
provider: "xai",
apiKeyEnv: credentialEnv,
baseUrl: "https://api.x.ai/v1",
},
credentialEnv,
credential,
environment: credential ? { XAI_API_KEY: credential } : {},
broker: {
host: "api.x.ai",
placeholderEnv: credentialEnv,
header: {
name: "authorization",
value: credential ? `Bearer ${credential}` : "",
},
},
};
}

function assertCompatible(route: ModelRoute, agentType: string): void {
const error = modelRouteCompatibilityError(route, agentType);
if (error) throw new Error(error);
Expand All @@ -96,6 +129,9 @@ export async function resolveModelRoute(
options: ResolveModelRouteOptions,
): Promise<ResolvedModelRoute> {
const env = options.env ?? process.env;
if (isGrokAgent(options.agentType)) {
return resolveGrokModelRoute(env);
}
assertCompatible(route, options.agentType);

if (route.mode === "local") {
Expand Down Expand Up @@ -251,6 +287,8 @@ export async function verifyModelRouteWithFetch(
route: ResolvedModelRoute,
fetchImpl: typeof fetch = fetch,
): Promise<void> {
if (route.route.provider === "xai" && !route.credential) return;

const endpoint = modelsEndpoint(route);
const headers: Record<string, string> = {
[route.broker.header.name]: route.broker.header.value,
Expand Down
Loading