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:
2 files changed
+104
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user