From aa08863d1246bbd179cdb8b7c4326abf2b640293 Mon Sep 17 00:00:00 2001 From: yetone Date: Sun, 27 Sep 2026 20:10:08 +0800 Subject: [PATCH] =?UTF-8?q?grok:=20Codex=20works=20on=20a=20Grok=20subscri?= =?UTF-8?q?ption=20=E2=80=94=20the=20tools=20Grok's=20backend=20turns=20a?= =?UTF-8?q?=20request=20away=20for=20(Codex's=20custom=20apply=5Fpatch,=20?= =?UTF-8?q?its=20namespace=20of=20agent=20tools)=20are=20left=20out,=20and?= =?UTF-8?q?=20so=20are=20web=5Fsearch's=20external=5Fweb=5Faccess=20and=20?= =?UTF-8?q?a=20null=20content=20on=20reasoning=20handed=20back,=20which=20?= =?UTF-8?q?the=20backend=20couldn't=20decode=20the=20encrypted=20reasoning?= =?UTF-8?q?=20beside.=20Tested=20live:=20the=20Codex=20CLI=20through=20mag?= =?UTF-8?q?pie=20counts=20a=20file's=20lines,=20writes=20the=20number=20an?= =?UTF-8?q?d=20reads=20it=20back=20over=20several=20rounds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/provider/grok.go | 70 ++++++++++++++++++++++++++++++++++ internal/provider/grok_test.go | 34 +++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/internal/provider/grok.go b/internal/provider/grok.go index 50338838..5f6f6135 100644 --- a/internal/provider/grok.go +++ b/internal/provider/grok.go @@ -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) { diff --git a/internal/provider/grok_test.go b/internal/provider/grok_test.go index 8c37e7a5..7d526c55 100644 --- a/internal/provider/grok_test.go +++ b/internal/provider/grok_test.go @@ -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") + } +}