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
9 changes: 8 additions & 1 deletion request-logger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ from this table. It is here so you can see what is supported before you start.
| Claude Code | Yes | One command. Works with a subscription login, an Anthropic API key, or Google Vertex AI. |
| Codex | Yes | One flag. A subscription or an API key works. |
| GitHub Copilot | Yes | Your normal subscription login. |
| Grok | Yes | One command. A grok.com login or an API key works. |
| OpenCode | Yes | One command, or a config file. |
| Pi | Yes | A config file. Pi has no base URL variable. |
| OMP | Yes | A YAML config file. Point it at any backend. |
Expand Down Expand Up @@ -283,6 +284,8 @@ Different agents fan out differently, and that is worth watching:

- **OpenCode** never counts tokens. Instead it makes a second call with a small
model to title the thread, so one turn writes exactly two captures.
- **Grok** titles the session with a second model call too, so one turn can
write two captures — the same shape as OpenCode.
- **Pi** never counts tokens at all, so every file is a real turn.
- **Gemini** on the free Google login makes several extra calls that carry no
prompt. Those are not logged either.
Expand Down Expand Up @@ -341,7 +344,11 @@ Be fair to the tool when you judge a failure.
by driving them against a local listener. They were not each run through a
full course of the lesson. Claude Code on Google Vertex AI is in this
group — verified against Anthropic's own Vertex documentation, not yet
driven against a real Vertex project.
driven against a real Vertex project. Grok is in this group too — verified
against xAI's published CLI docs (`GROK_CLI_CHAT_PROXY_BASE_URL`, the
default `https://cli-chat-proxy.grok.com/v1` host, and the OpenAI Responses
wire format) and by driving a live `grok -p` turn through this proxy. It
has not been run through a full course of the lesson.
- **Junie is the least-verified entry in the catalogue.** Junie CLI is
closed source, so unlike every other agent here its entry was not checked
against real source, only against JetBrains' published Junie CLI docs
Expand Down
74 changes: 74 additions & 0 deletions request-logger/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ describe("listAgents", () => {
"claude-code",
"codex",
"copilot",
"grok",
"opencode",
"pi",
"omp",
Expand Down Expand Up @@ -117,6 +118,17 @@ describe("listAgents", () => {
expect(opencode?.needsProvider).toBe(true);
});

it("marks Grok as supported", () => {
const grok = listAgents().find((agent) => agent.id === "grok");
expect(grok?.supported).toBe(true);
});

it("says Grok needs no provider question, because it has only one", () => {
const grok = listAgents().find((agent) => agent.id === "grok");
expect(grok?.needsProvider).toBe(false);
expect(grok?.alwaysCustom).toBe(false);
});

it("marks OMP as supported", () => {
const omp = listAgents().find((agent) => agent.id === "omp");
expect(omp?.supported).toBe(true);
Expand Down Expand Up @@ -201,6 +213,11 @@ describe("agentProviders", () => {
expect(agentProviders("copilot").map((p) => p.id)).toEqual(["github"]);
});

it("returns Grok's one xAI provider, unlike listProviders", () => {
expect(listProviders("grok")).toEqual([]);
expect(agentProviders("grok").map((p) => p.id)).toEqual(["xai"]);
});

it("returns nothing for a refused agent", () => {
expect(agentProviders("cursor")).toEqual([]);
});
Expand Down Expand Up @@ -259,6 +276,10 @@ describe("resolveChoice — upstream hosts", () => {
expect(target("copilot").upstreamHost).toBe("api.githubcopilot.com");
});

it("sends Grok to the CLI chat proxy host", () => {
expect(target("grok").upstreamHost).toBe("cli-chat-proxy.grok.com");
});

it("sends OpenCode on Anthropic to Anthropic", () => {
expect(target("opencode", "anthropic").upstreamHost).toBe(
"api.anthropic.com"
Expand Down Expand Up @@ -331,6 +352,10 @@ describe("resolveChoice — renderers", () => {
expect(target("copilot").renderer).toBe("openai");
});

it("reads Grok with the OpenAI renderer, since its default is the Responses API", () => {
expect(target("grok").renderer).toBe("openai");
});

it("reads OpenCode on Anthropic with the Anthropic renderer", () => {
expect(target("opencode", "anthropic").renderer).toBe("anthropic");
});
Expand Down Expand Up @@ -376,6 +401,10 @@ describe("resolveChoice — base URLs", () => {
expect(target("copilot").baseUrl).toBe("http://localhost:8787");
});

it("gives Grok the /v1 suffix, because it appends /responses to the default", () => {
expect(target("grok").baseUrl).toBe("http://localhost:8787/v1");
});

it("gives Pi on Anthropic no suffix, because Pi's SDK adds /v1/messages", () => {
expect(target("pi", "anthropic").baseUrl).toBe("http://localhost:8787");
});
Expand Down Expand Up @@ -477,6 +506,19 @@ describe("resolveChoice — commands", () => {
);
});

it("uses Grok's CLI chat-proxy variable, not the custom-models one", () => {
expect(target("grok").command).toBe(
"GROK_CLI_CHAT_PROXY_BASE_URL=http://localhost:8787/v1 grok"
);
});

it("never puts GROK_MODELS_BASE_URL in the Grok command", () => {
// GROK_MODELS_BASE_URL switches the CLI onto API-key auth against a
// custom /v1/models endpoint. Setting it here would drop the grok.com
// login and talk to the wrong host.
expect(target("grok").command).not.toContain("GROK_MODELS_BASE_URL");
});

it("carries the suffix through into the OpenCode command", () => {
expect(target("opencode", "anthropic").command).toBe(
"ANTHROPIC_BASE_URL=http://localhost:8787/v1 opencode"
Expand Down Expand Up @@ -559,6 +601,12 @@ describe("resolveChoice — commands on win32", () => {
);
});

it("uses PowerShell $env: syntax for Grok too", () => {
expect(winTarget("grok").command).toBe(
"$env:GROK_CLI_CHAT_PROXY_BASE_URL = 'http://localhost:8787/v1'; grok"
);
});

it("leaves a command with no env vars unchanged, since there is nothing to rewrite", () => {
// Pi has no base URL variable at all.
expect(winTarget("pi", "anthropic").command).toBe("pi");
Expand Down Expand Up @@ -597,6 +645,10 @@ describe("resolveChoice — setup files", () => {
expect(target("claude-code", "anthropic").setup).toEqual([]);
});

it("gives Grok no config file to write — it's all env vars", () => {
expect(target("grok").setup).toEqual([]);
});

it("gives Claude Code on Vertex AI no config file either — it's all env vars", () => {
expect(target("claude-code", "vertex").setup).toEqual([]);
});
Expand Down Expand Up @@ -671,6 +723,14 @@ describe("resolveChoice — notes and warnings", () => {
expect(target("copilot").warnings.join(" ")).toContain("WebSocket");
});

it("tells a Grok student that a grok.com login still works", () => {
expect(target("grok").notes.join(" ")).toContain("grok.com login");
});

it("warns a Grok student that GROK_MODELS_BASE_URL is a different override", () => {
expect(target("grok").warnings.join(" ")).toContain("GROK_MODELS_BASE_URL");
});

it("tells a Pi student on a ChatGPT subscription to use SSE", () => {
expect(target("pi", "codex").notes.join(" ")).toContain("SSE");
});
Expand Down Expand Up @@ -1384,6 +1444,19 @@ describe("resolveChoice — custom base URL, per-agent command template", () =>
expect(result.command).toContain("COPILOT_API_URL=http://localhost:8787");
});

it("reuses Grok's own template regardless of the wire format chosen, since it has only one", () => {
const result = customTarget({
agent: "grok",
provider: CUSTOM_ID,
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
});
expect(result.command).toContain(
"GROK_CLI_CHAT_PROXY_BASE_URL=http://localhost:8787/v1"
);
expect(result.upstreamBaseUrl).toBe("http://localhost:11434");
});

it("never borrows Codex's ChatGPT-subscription template for a custom target", () => {
const result = customTarget({
agent: "codex",
Expand Down Expand Up @@ -1414,6 +1487,7 @@ describe("customTargetNeedsModel", () => {
expect(customTargetNeedsModel("codex", renderer)).toBe(false);
expect(customTargetNeedsModel("claude-code", renderer)).toBe(false);
expect(customTargetNeedsModel("omp", renderer)).toBe(false);
expect(customTargetNeedsModel("grok", renderer)).toBe(false);
}
});
});
Expand Down
27 changes: 27 additions & 0 deletions request-logger/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,33 @@ const AGENTS: AgentEntry[] = [
},
],
},
{
id: "grok",
label: "Grok",
providers: [
{
id: "xai",
label: "xAI",
upstreamHost: "cli-chat-proxy.grok.com",
renderer: "openai",
suffix: "/v1",
env: [["GROK_CLI_CHAT_PROXY_BASE_URL", "{baseUrl}"]],
bin: "grok",
notes: [
"This works with a grok.com login and with an XAI_API_KEY. Your " +
"login stays active. Only the model traffic moves.",
"Grok also makes a second model call to title the session, so one " +
"turn can write two captures. That is what it really sends.",
],
warnings: [
"GROK_MODELS_BASE_URL is a different override. If it is already " +
"set, Grok talks to that host instead of the CLI chat proxy this " +
"command points at, and your logs folder stays empty. Unset it " +
"for this run.",
],
},
],
},
{
id: "cursor",
label: "Cursor CLI",
Expand Down