Files
magpie/internal/gateway/image_tool.go
T
yetone 963d8d9d88 gateway: a provider that turns Codex's image tool away with a bare 400 is asked again without it, and isn't offered it again once that works, so a plain chat no longer breaks on it; a ChatGPT sign-in, openai.com and a Codex backend that takes it still get it (#949, cjyrainbow)
Since #870, magpie's provider in Codex's config carries x-openai-actor-authorization, so Codex offers its image_gen namespace tool on every request. A vendor's Responses API that can't take it, a relay's included, answered "bad response status code 400" to "只回复 OK". After a bare 400 (one that names no field or message magpie already knows to drop), a provider other than a ChatGPT sign-in or openai.com is asked once more without the tool. This works both passed through (the namespace) and translated (image_gen__imagegen flat). If the retry answers, the provider is marked unfit for tool:image_gen. If it doesn't, the tool goes back and the vendor's 400 stands, and the cache-key guess runs on the request as it was. The header stays as #870 wrote it.

image_tool_test.go is new. Its fixture is the image_gen tool Codex 0.160.0 sent to a capture server with magpie's provider table and catalog. TestImageToolLeftOutWhereRefused (translated, passed through) fails with the reporter's 400 when gateway.go is reverted, and so does TestImageToolKeptOverAnotherRefusal. TestImageToolKeptForChatGPT fails when the ChatGPT exemption is dropped. docs/subsystems/gateway-routing.md has the rule. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-06 03:39:04 +08:00

87 lines
2.5 KiB
Go

package gateway
import (
"bytes"
"encoding/json"
"strings"
"github.com/yetone/magpie/internal/provider"
)
// Codex offers its image tool, the namespace image_gen with its function
// imagegen, to a provider it reads as the OpenAI actor, as magpie's is
// (#870). A Codex backend (a ChatGPT sign-in, sub2api) takes it, and a
// call to it is drawn through magpie (drawing_route.go). A vendor's
// Responses API that knows no namespaces turns the whole request away with
// a bare 400, so a plain chat broke (#949). Such a vendor is asked again
// without the tool, and once that works it isn't sent it again.
// imageNamespace is the namespace Codex's image tool is offered in.
const imageNamespace = "image_gen"
// imageToolRefused is how unfit remembers a provider refusing it.
const imageToolRefused = "tool:" + imageNamespace
// mayDropImageTool reports whether p is one that may not take Codex's
// image tool: anything but a ChatGPT sign-in and OpenAI's own API.
func mayDropImageTool(p provider.Provider) bool {
return (p.Account == nil || p.Account.Agent != "codex") && !strings.HasSuffix(p.Host(), "openai.com")
}
// withoutImageTool is a Responses body without Codex's image tool among its
// tools, and whether it had it.
func withoutImageTool(body []byte) ([]byte, bool) {
if !bytes.Contains(body, []byte(`"`+imageNamespace+`"`)) {
return body, false
}
dec := json.NewDecoder(bytes.NewReader(body))
dec.UseNumber()
var m map[string]any
if dec.Decode(&m) != nil {
return body, false
}
tools, _ := m["tools"].([]any)
kept := make([]any, 0, len(tools))
for _, t := range tools {
if tm, _ := t.(map[string]any); tm != nil && tm["type"] == "namespace" && tm["name"] == imageNamespace {
continue
}
kept = append(kept, t)
}
if len(kept) == len(tools) {
return body, false
}
if len(kept) == 0 {
delete(m, "tools")
delete(m, "tool_choice")
} else {
m["tools"] = kept
}
out, err := marshalPlain(m)
if err != nil {
return body, false
}
return out, true
}
// withoutImageToolReq is a translated request without Codex's image tool,
// which it offers flat (image_gen__imagegen), and whether it had it.
func withoutImageToolReq(req *Request) (*Request, bool) {
var kept []Tool
for _, t := range req.Tools {
if ns, ok := req.Namespaced[t.Name]; ok && ns.Namespace == imageNamespace {
continue
}
kept = append(kept, t)
}
if len(kept) == len(req.Tools) {
return req, false
}
r := *req
r.Tools = kept
if len(kept) == 0 && r.ToolChoice != "" && r.ToolChoice != "none" {
r.ToolChoice = ""
}
return &r, true
}