grok: Codex works on a Grok subscription — the tools Grok's backend turns a request away for (Codex's custom apply_patch, its namespace of agent tools) are left out, and so are web_search's external_web_access and a null content on reasoning handed back, which the backend couldn't decode the encrypted reasoning beside. Tested live: the Codex CLI through magpie counts a file's lines, writes the number and reads it back over several rounds

This commit is contained in:
yetone committed 2026-09-27 20:10:08 +08:00
1 parent 1d0c9ccf27
commit aa08863d12
2 files changed
+104

No files matched your search

+70
View File
@@ -8,6 +8,7 @@ package provider
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
@@ -106,6 +107,7 @@ func grokAccount() (Provider, bool) {
acct.Home = "" // the CLI's own, wherever it is
}
grokSigned(acct, home)
acct.body = grokBody
acct.models = func() []catalog.Model { return []catalog.Model{{ID: "grok-4.7", Name: "Grok 4.7"}} }
acct.fetch = func(ctx context.Context) ([]catalog.Model, error) {
ms, err := grokModels(ctx, acct.sign)
@@ -138,6 +140,74 @@ func grokSigned(acct *Account, home string) {
}
}
// grokTools are the tool types Grok's backend takes; it turns the whole
// request away over another, as over Codex's freeform apply_patch (custom)
// or its sub-agent tools, grouped in a namespace.
var grokTools = map[string]bool{"function": true, "web_search": true, "x_search": true, "image_generation": true,
"collections_search": true, "file_search": true, "code_execution": true, "code_interpreter": true,
"mcp": true, "shell": true, "tool_search": true}
// grokBody leaves out the tools Grok's backend doesn't take, as a backend
// magpie translates for goes without them: Codex, without apply_patch,
// edits files through its shell. Codex's web_search says whether it may
// reach the live web, which Grok's doesn't take either; it searches live.
// And Codex hands reasoning back with "content": null, which the backend
// can't read the encrypted reasoning beside ("Could not decode the
// compaction blob"), so a null content goes.
func grokBody(body []byte) []byte {
if !bytes.Contains(body, []byte(`"tools"`)) && !bytes.Contains(body, []byte(`"reasoning"`)) {
return body
}
dec := json.NewDecoder(bytes.NewReader(body))
dec.UseNumber()
var m map[string]any
if dec.Decode(&m) != nil || m == nil {
return body
}
dirty := false
if tools, ok := m["tools"].([]any); ok {
kept := tools[:0:0]
for _, t := range tools {
if tm, ok := t.(map[string]any); ok {
ty, _ := tm["type"].(string)
if !grokTools[ty] {
dirty = true
continue
}
if _, ok := tm["external_web_access"]; ok {
delete(tm, "external_web_access")
dirty = true
}
}
kept = append(kept, t)
}
m["tools"] = kept
if tc, ok := m["tool_choice"].(map[string]any); ok {
if ty, _ := tc["type"].(string); !grokTools[ty] {
delete(m, "tool_choice")
dirty = true
}
}
}
input, _ := m["input"].([]any)
for _, it := range input {
if im, ok := it.(map[string]any); ok && im["type"] == "reasoning" {
if c, ok := im["content"]; ok && c == nil {
delete(im, "content")
dirty = true
}
}
}
if !dirty {
return body
}
b, err := json.Marshal(m)
if err != nil {
return body
}
return b
}
// grokHeaders say a request comes from the Grok CLI, which the backend
// wants to know the version of: without it the CLI is "outdated".
func grokHeaders(req *http.Request, token string) {
+34
View File
@@ -76,3 +76,37 @@ func TestGrokTokenReadsTheCLIsSignIn(t *testing.T) {
t.Fatal("no sign-in, yet a token")
}
}
// Codex's freeform apply_patch is left out: Grok's backend turns away a
// request with a tool type it doesn't know.
func TestGrokBodyLeavesOutCustomTools(t *testing.T) {
in := []byte(`{"model":"grok-4.7","tools":[{"type":"function","name":"shell"},{"type":"custom","name":"apply_patch","format":{"type":"grammar"}},{"type":"namespace","name":"multi_agent_v1","tools":[]},{"type":"web_search","external_web_access":false}],"tool_choice":{"type":"custom","name":"apply_patch"},"max_output_tokens":100}`)
var got struct {
Tools []map[string]any `json:"tools"`
Choice any `json:"tool_choice"`
Max int `json:"max_output_tokens"`
}
if err := json.Unmarshal(grokBody(in), &got); err != nil {
t.Fatal(err)
}
if len(got.Tools) != 2 || got.Tools[0]["type"] != "function" || len(got.Tools[1]) != 1 || got.Tools[1]["type"] != "web_search" || got.Choice != nil || got.Max != 100 {
t.Fatalf("%+v", got)
}
same := []byte(`{"tools":[{"type":"function","name":"x"}],"tool_choice":"auto"}`)
if string(grokBody(same)) != string(same) {
t.Fatal("a body Grok takes was changed")
}
}
// Codex's reasoning, handed back with a null content, is sent without it.
func TestGrokBodyDropsNullReasoningContent(t *testing.T) {
in := []byte(`{"input":[{"type":"reasoning","id":"rs_1","summary":[],"content":null,"encrypted_content":"a+b/c="},{"type":"message","role":"user","content":"hi"}]}`)
want := `{"input":[{"encrypted_content":"a+b/c=","id":"rs_1","summary":[],"type":"reasoning"},{"content":"hi","role":"user","type":"message"}]}`
if got := string(grokBody(in)); got != want {
t.Fatalf("got %s\nwant %s", got, want)
}
same := []byte(`{"input":[{"type":"reasoning","content":[],"encrypted_content":"x"}]}`)
if string(grokBody(same)) != string(same) {
t.Fatal("reasoning with content was changed")
}
}