Files
yetone c2947dd6e7 zed: an OpenAI model's request reaches Zed's cloud in the Responses types it reads, so Codex on a Zed GPT model is answered (nico on Discord: "failed to parse OpenAI Responses API request: unknown variant developer, expected one of user, assistant, system, tool", and with it no model switch mid-thread and no auto-review) — Zed's cloud parses the request into its own types (zed-industries/zed crates/open_ai/src/responses.rs: Role user/assistant/system/tool, items message/function_call/function_call_output/custom_tool_call(_output)/reasoning/compaction/compaction_trigger, tools function/custom, include reasoning.encrypted_content only) and refuses the whole request over anything else. The Zed plugin (@magpie-community/opencode-zed-auth) gets Codex's Responses request relayed as Codex sent it, developer messages and all — its permissions message on every request, the one a model switch adds, the auto-review's — so every Codex turn on a Zed OpenAI model failed. The plugin's account now fits the body as grokBody does Grok's (ZedBody): a developer message goes as a system one where it stands (Zed's own client sends system messages too), the shorthand {role, content} gets its type and a string content becomes a list, reasoning's null content goes, an agent_message is the user's turn, an item Zed has no type for (a web_search_call an OpenAI model ran) is left out, namespaced tools go flat and their calls come back under the namespace (as Grok's), other tool types go, include keeps reasoning.encrypted_content alone, a function tool_choice is required, and a reasoning summary Zed doesn't know goes. The built-in Zed account already folded developer messages in; its OpenAI request goes through the same fitting, so a web search offered with no search API no longer leaves a web_search tool and web_search_call.action.sources in it. Codex's prompts and instructions are unchanged. TestZedPluginGetsNoDeveloperRole (a Codex-shaped request through the fake plugin to a fake upstream that parses as Zed's cloud: fails with the exact 400 without the change) and TestZedBuiltInResponsesParse fail without it. No request was made to Zed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 13:23:41 +08:00

162 lines
6.4 KiB
Go

package gateway
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
// zedParses is what Zed's cloud asks of an OpenAI Responses request
// (zed-industries/zed, crates/open_ai/src/responses.rs) before it goes on,
// "" when it would take it.
func zedParses(q map[string]any) string {
items, _ := q["input"].([]any)
for _, it := range items {
im, _ := it.(map[string]any)
switch ty, _ := im["type"].(string); ty {
case "message":
switch im["role"] {
case "user", "assistant", "system", "tool":
default:
return "unknown variant `" + im["role"].(string) + "`, expected one of `user`, `assistant`, `system`, `tool`"
}
if _, ok := im["content"].([]any); !ok {
return "invalid type: expected a sequence"
}
case "reasoning":
if c, ok := im["content"]; ok && c == nil {
return "invalid type: null, expected a sequence"
}
case "function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "compaction", "compaction_trigger":
default:
return "unknown variant `" + ty + "`"
}
}
tools, _ := q["tools"].([]any)
for _, t := range tools {
if ty := t.(map[string]any)["type"]; ty != "function" && ty != "custom" {
return "unknown variant `" + ty.(string) + "`, expected `function` or `custom`"
}
}
inc, _ := q["include"].([]any)
for _, v := range inc {
if v != "reasoning.encrypted_content" {
return "unknown variant `" + v.(string) + "`"
}
}
return ""
}
// A Codex request on an OpenAI model of Zed's plugin reaches Zed as its
// cloud reads it: the developer messages Codex sends at the start and
// mid-conversation (a model switched) as system ones where they stand,
// reasoning with a null content without it, a web search an OpenAI model
// ran left out, and the namespaced tools flat (nico on Discord: "failed to
// parse OpenAI Responses API request: unknown variant `developer`").
func TestZedPluginGetsNoDeveloperRole(t *testing.T) {
var mu sync.Mutex
var sent []map[string]any
pid := besideFake(t, "zed", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var q map[string]any
json.Unmarshal(b, &q)
mu.Lock()
sent = append(sent, q)
mu.Unlock()
if why := zedParses(q); why != "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
io.WriteString(w, `{"error":{"message":"failed to parse OpenAI Responses API request: `+why+`"}}`)
return
}
done := `{"id":"r1","object":"response","status":"completed","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":5,"output_tokens":1}}`
if q["stream"] != true {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, done)
return
}
w.Header().Set("Content-Type", "text/event-stream")
io.WriteString(w, sse(
`event: response.created`+"\n"+`data: {"type":"response.created","response":{"id":"r1","object":"response","status":"in_progress","output":[]}}`,
`event: response.output_text.delta`+"\n"+`data: {"type":"response.output_text.delta","item_id":"m1","output_index":0,"delta":"ok"}`,
`event: response.completed`+"\n"+`data: {"type":"response.completed","response":`+done+`}`))
}))
body := `{"model":"` + pid + `/fake-resp","stream":true,"instructions":"You are Codex.",` + namespacedTools + `,
"include":["reasoning.encrypted_content"],"reasoning":{"effort":"medium","summary":"auto"},"input":[
{"type":"message","role":"developer","content":[{"type":"input_text","text":"<permissions instructions>sandboxed</permissions instructions>"}]},
{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]},
{"type":"reasoning","summary":[{"type":"summary_text","text":"thinking"}],"content":null,"encrypted_content":"gAAAAAx"},
{"type":"web_search_call","id":"ws_1","status":"completed","action":{"type":"search","query":"magpie"}},
{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hello"}]},
{"type":"message","role":"developer","content":[{"type":"input_text","text":"<model_switch>now another model</model_switch>"}]},
{"type":"message","role":"user","content":[{"type":"input_text","text":"again"}]}]}`
rec := httptest.NewRecorder()
New().Handler().ServeHTTP(rec, httptest.NewRequest("POST", "/v1/responses", strings.NewReader(body)))
if rec.Code != 200 {
t.Fatalf("%d %s", rec.Code, rec.Body)
}
mu.Lock()
defer mu.Unlock()
if len(sent) != 1 {
t.Fatalf("asked %d times", len(sent))
}
q := sent[0]
var roles []string
for _, it := range q["input"].([]any) {
im := it.(map[string]any)
if im["type"] == "message" {
roles = append(roles, im["role"].(string))
}
}
if got := strings.Join(roles, ","); got != "system,user,assistant,system,user" {
t.Fatalf("roles %s", got)
}
in := q["input"].([]any)
if len(in) != 6 {
t.Fatalf("input %v", in)
}
if s := in[4].(map[string]any)["content"].([]any)[0].(map[string]any)["text"]; s != "<model_switch>now another model</model_switch>" {
t.Fatalf("the model switch went as %v", in[4])
}
if r := in[2].(map[string]any); r["type"] != "reasoning" || r["encrypted_content"] != "gAAAAAx" {
t.Fatalf("reasoning went as %v", r)
}
if q["instructions"] != "You are Codex." {
t.Fatalf("instructions %v", q["instructions"])
}
var names []string
for _, tl := range q["tools"].([]any) {
tm := tl.(map[string]any)
names = append(names, tm["type"].(string)+":"+tm["name"].(string))
}
if got := strings.Join(names, ","); got != "function:exec_command,function:collaboration__spawn_agent" {
t.Fatalf("Zed was offered %s", got)
}
}
// The built-in Zed account's OpenAI request is fitted the same way: a web
// search offered (no search API set up) leaves no web_search tool, nor
// include's web_search_call.action.sources, for Zed's cloud to turn away.
func TestZedBuiltInResponsesParse(t *testing.T) {
req, err := parseResponses([]byte(`{"model":"m","stream":true,"include":["reasoning.encrypted_content"],"reasoning":{"effort":"high"},
"tools":[{"type":"function","name":"exec_command","parameters":{"type":"object"}},{"type":"web_search"}],"input":[
{"type":"message","role":"developer","content":[{"type":"input_text","text":"rules"}]},
{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}]}`))
if err != nil {
t.Fatal(err)
}
raw, _, err := zedRequest(req, "open_ai", "gpt-5.5")
if err != nil {
t.Fatal(err)
}
var q map[string]any
json.Unmarshal(raw, &q)
if why := zedParses(q); why != "" {
t.Fatalf("%s: %s", why, raw)
}
}