From 9a48cd65a1f05739d24fd73b5e1258cdf160df81 Mon Sep 17 00:00:00 2001 From: Gokhan Efe Date: Thu, 27 Aug 2026 22:58:51 +0200 Subject: [PATCH] request-logger: add Grok support Add Grok as a catalogue agent in the Copilot shape: one xAI provider, GROK_CLI_CHAT_PROXY_BASE_URL, a /v1 suffix, and the OpenAI Responses renderer. A grok.com login and an XAI_API_KEY both talk to cli-chat-proxy.grok.com; GROK_MODELS_BASE_URL is a different override and is warned about, not set. Tests cover host, command, Windows env syntax, and the custom-target template. Driven against a live grok -p turn through the proxy. --- request-logger/README.md | 9 ++++- request-logger/agents.test.ts | 74 +++++++++++++++++++++++++++++++++++ request-logger/agents.ts | 27 +++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/request-logger/README.md b/request-logger/README.md index 803748c..13e8899 100644 --- a/request-logger/README.md +++ b/request-logger/README.md @@ -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. | @@ -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. @@ -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 diff --git a/request-logger/agents.test.ts b/request-logger/agents.test.ts index eeec70a..981c43b 100644 --- a/request-logger/agents.test.ts +++ b/request-logger/agents.test.ts @@ -79,6 +79,7 @@ describe("listAgents", () => { "claude-code", "codex", "copilot", + "grok", "opencode", "pi", "omp", @@ -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); @@ -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([]); }); @@ -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" @@ -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"); }); @@ -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"); }); @@ -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" @@ -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"); @@ -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([]); }); @@ -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"); }); @@ -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", @@ -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); } }); }); diff --git a/request-logger/agents.ts b/request-logger/agents.ts index 344587c..a1bd4b7 100644 --- a/request-logger/agents.ts +++ b/request-logger/agents.ts @@ -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",