codex: magpie's models join Codex's own rather than replacing them — signed in (ChatGPT or an API key), Codex keeps its built-in OpenAI provider and sign-in and is pointed at magpie with openai_base_url alone, so its official models still go to OpenAI on the user's own account, its threads (listed per provider) stay together, and the Codex app keeps its model picker; magpie's gateway answers at /backend-api/codex, passing Codex's own models through as they came, serving magpie's (provider/model) itself, and handing out OpenAI's model list with magpie's after it for /model. Compacting a magpie model's conversation is done by that model with Codex's own prompt; Codex's compressed request bodies are read, and usage is counted for both. Not signed in, magpie stays a provider of Codex's as before; a magpie set up that way moves over the next time a magpie model is picked

This commit is contained in:
yetone committed 2026-09-25 02:42:24 +08:00
1 parent 0281df4d87
commit c3931c91df
10 files changed
+952 -140

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/godbus/dbus/v5 v5.2.2
github.com/klauspost/compress v1.18.3
github.com/tidwall/gjson v1.19.0
github.com/tidwall/jsonc v0.3.3
github.com/wailsapp/wails/v3 v3.0.0-beta.24
+2
View File
@@ -42,6 +42,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+96 -136
View File
@@ -1,7 +1,6 @@
package agent
import (
_ "embed"
"encoding/json"
"os"
"path/filepath"
@@ -9,32 +8,36 @@ import (
"strings"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/codexcat"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
"github.com/yetone/magpie/internal/provider"
)
// Codex talks the OpenAI Responses API to whichever provider config.toml
// names. Routing it through magpie means a [model_providers.magpie] table,
// `model_provider = "magpie"`, and a model catalog file so the whole magpie
// catalog shows up inside Codex's own /model picker.
// codexPrompt is Codex's generic system prompt (Apache-2.0, openai/codex,
// core/gpt-5.2-codex_prompt.md). Third-party models need one because the
// bundled catalog only carries prompts for OpenAI models.
//
//go:embed codex_prompt.md
var codexPrompt string
// Codex talks the OpenAI Responses API. Signed in (ChatGPT or an API key),
// it is routed through magpie with `openai_base_url` alone: Codex keeps its
// built-in OpenAI provider and sign-in, so its own models, its threads
// (listed per provider) and the Codex app's model picker stay as they are,
// and magpie's gateway passes its own models through to OpenAI while it
// answers the rest — the model list included, so magpie's models join
// OpenAI's in /model. Not signed in, the built-in provider can't run, and
// magpie is a provider of its own: a [model_providers.magpie] table,
// `model_provider = "magpie"`, and a model catalog file for /model.
func codex(home string) *Agent {
dir := filepath.Join(home, ".codex")
path := filepath.Join(dir, "config.toml")
catalogPath := filepath.Join(dir, "magpie-models.json")
get := func(k string) string { v, _ := edit.GetTOMLTop(path, k); return v }
routed := func() bool { return get("model_provider") == magpieID }
asProvider := func() bool { return get("model_provider") == magpieID }
viaBase := func() bool { return isCodexGateway(get("openai_base_url")) }
routed := func() bool { return asProvider() || viaBase() }
models := func() []catalog.Model {
if routed() {
switch {
case asProvider():
return magpieModels()
case viaBase():
return append(catalog.Codex(), magpieModels()...)
}
return catalog.Codex()
}
@@ -43,13 +46,36 @@ func codex(home string) *Agent {
ms := models()
model, effort := get("model"), get("model_reasoning_effort")
if e := catalog.Efforts(ms, model); len(e) > 0 && !contains(e, effort) {
return edit.SetTOMLTop(path, edit.KV{Path: "model_reasoning_effort", Value: defaultEffort(e)})
return edit.SetTOMLTop(path, edit.KV{Path: "model_reasoning_effort", Value: codexcat.DefaultEffort(e)})
}
return nil
}
// dropProvider takes magpie out as a provider of Codex's.
dropProvider := func() error {
if !asProvider() {
return nil
}
if err := edit.DelTOMLTop(path, "model_provider", "model_catalog_json"); err != nil {
return err
}
if err := edit.DelTOMLTable(path, "model_providers."+magpieID); err != nil {
return err
}
os.Remove(catalogPath)
return nil
}
dropBase := func() error {
if !viaBase() {
return nil
}
return edit.DelTOMLTop(path, "openai_base_url")
}
set := func(v string) error {
if v == "" {
// Codex as installed: OpenAI, its own catalog, its default model
if err := dropBase(); err != nil {
return err
}
if err := edit.DelTOMLTop(path, "model", "model_provider", "model_catalog_json"); err != nil {
return err
}
@@ -57,13 +83,33 @@ func codex(home string) *Agent {
return err
}
os.Remove(catalogPath)
forget("codex.model", "codex.effort", "codex.provider")
forget("codex.model", "codex.effort", "codex.provider", "codex.catalog")
return nil
}
if isMagpie(v) {
if !routed() {
stash(map[string]string{"codex.model": get("model"), "codex.effort": get("model_reasoning_effort"),
"codex.provider": get("model_provider")})
"codex.provider": get("model_provider"), "codex.catalog": get("model_catalog_json")})
}
if codexSignedIn(dir) {
if err := dropProvider(); err != nil {
return err
}
// the base URL is the built-in provider's, and a catalog
// file would stand in for the list magpie hands out
if err := edit.DelTOMLTop(path, "model_provider", "model_catalog_json"); err != nil {
return err
}
if err := edit.SetTOMLTop(path,
edit.KV{Path: "openai_base_url", Value: codexGatewayURL()},
edit.KV{Path: "model", Value: v},
); err != nil {
return err
}
return settle()
}
if err := dropBase(); err != nil {
return err
}
if err := edit.SetTOMLTable(path, "model_providers."+magpieID,
edit.KV{Path: "name", Value: "magpie"},
@@ -73,7 +119,7 @@ func codex(home string) *Agent {
); err != nil {
return err
}
if err := edit.WriteAtomic(catalogPath, codexCatalog(magpieModels())); err != nil {
if err := edit.WriteAtomic(catalogPath, codexcat.Catalog(magpieModels())); err != nil {
return err
}
if err := edit.SetTOMLTop(path,
@@ -86,18 +132,20 @@ func codex(home string) *Agent {
return settle()
}
if routed() {
if err := edit.DelTOMLTop(path, "model_provider", "model_catalog_json"); err != nil {
if err := dropBase(); err != nil {
return err
}
if err := edit.DelTOMLTable(path, "model_providers."+magpieID); err != nil {
if err := dropProvider(); err != nil {
return err
}
os.Remove(catalogPath)
unstash("codex.model")
var back []edit.KV
if p := unstash("codex.provider"); p != "" {
if p := unstash("codex.provider"); p != "" && p != magpieID {
back = append(back, edit.KV{Path: "model_provider", Value: p})
}
if c := unstash("codex.catalog"); c != "" && c != catalogPath {
back = append(back, edit.KV{Path: "model_catalog_json", Value: c})
}
if e := unstash("codex.effort"); e != "" {
back = append(back, edit.KV{Path: "model_reasoning_effort", Value: e})
}
@@ -167,120 +215,6 @@ func contains(xs []string, x string) bool {
return false
}
// defaultEffort picks the middle of the road: "medium" or "high" when
// offered, else whatever the list starts with.
func defaultEffort(e []string) string {
for _, want := range []string{"medium", "high"} {
if contains(e, want) {
return want
}
}
return e[0]
}
// codexCatalog renders models in the shape of Codex's models.json so
// `model_catalog_json` can point at it. Only fields Codex requires or that
// change behaviour are set; the rest take Codex's defaults.
func codexCatalog(ms []catalog.Model) []byte {
type level struct {
Effort string `json:"effort"`
Description string `json:"description"`
}
type model struct {
Slug string `json:"slug"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Instructions string `json:"base_instructions"`
DefaultEffort *string `json:"default_reasoning_level"`
Efforts []level `json:"supported_reasoning_levels"`
Shell string `json:"shell_type"`
Visibility string `json:"visibility"`
InAPI bool `json:"supported_in_api"`
Priority int `json:"priority"`
Verbosity bool `json:"support_verbosity"`
DefVerbosity *string `json:"default_verbosity"`
ApplyPatch string `json:"apply_patch_tool_type"`
Truncation struct {
Mode string `json:"mode"`
Limit int `json:"limit"`
} `json:"truncation_policy"`
Tools []string `json:"experimental_supported_tools"`
Modalities []string `json:"input_modalities"`
}
own := codexCacheEntries()
var entries []any
for i, m := range ms {
if raw, ok := own[strings.TrimPrefix(m.ID, "codex/")]; ok && strings.HasPrefix(m.ID, "codex/") {
entries = append(entries, codexOwnEntry(raw, m.ID, m.Name, i+1))
continue
}
e := model{
Slug: m.ID, DisplayName: m.Name, Description: m.Name + " via magpie",
Instructions: codexPrompt, Efforts: []level{},
Shell: "unified_exec", Visibility: "list", InAPI: true, Priority: i + 1,
ApplyPatch: "freeform", Tools: []string{}, Modalities: []string{"text"},
}
e.Truncation.Mode, e.Truncation.Limit = "tokens", 10000
for _, ef := range m.Efforts {
e.Efforts = append(e.Efforts, level{Effort: ef})
}
if len(m.Efforts) > 0 {
d := defaultEffort(m.Efforts)
e.DefaultEffort = &d
}
entries = append(entries, e)
}
out := struct {
Models []any `json:"models"`
}{Models: entries}
if out.Models == nil {
out.Models = []any{}
}
b, _ := json.MarshalIndent(out, "", " ")
return b
}
// codexCacheEntries is Codex's own models, as models_cache.json describes
// them for the ChatGPT account it last asked with, by slug.
func codexCacheEntries() map[string]map[string]any {
home, _ := os.UserHomeDir()
b, err := os.ReadFile(filepath.Join(home, ".codex", "models_cache.json"))
if err != nil {
return nil
}
var cache struct {
Models []map[string]any `json:"models"`
}
if json.Unmarshal(b, &cache) != nil {
return nil
}
out := map[string]map[string]any{}
for _, m := range cache.Models {
if slug, _ := m["slug"].(string); slug != "" {
out[slug] = m
}
}
return out
}
// codexOwnEntry is one of Codex's own models, reached through magpie with
// the ChatGPT sign-in: its entry as Codex has it (images, context window,
// tools, instructions), under magpie's id. The start-up notice and the
// upgrade prompt are left out; they name slugs the catalog does not have.
func codexOwnEntry(raw map[string]any, id, name string, priority int) map[string]any {
e := make(map[string]any, len(raw))
for k, v := range raw {
e[k] = v
}
delete(e, "availability_nux")
delete(e, "upgrade")
e["slug"], e["display_name"], e["priority"], e["visibility"] = id, name, priority, "list"
if s, _ := e["base_instructions"].(string); s == "" {
e["base_instructions"] = codexPrompt
}
return e
}
// ownCodex is Codex's own models, narrowed to the ones ticked on its ChatGPT
// subscription in magpie when any are.
func ownCodex() []catalog.Model {
@@ -300,3 +234,29 @@ func ownCodex() []catalog.Model {
}
return out
}
// codexGatewayURL is where Codex's built-in OpenAI provider is pointed to
// reach magpie.
func codexGatewayURL() string { return gateway.URL() + gateway.CodexPath }
// isCodexGateway reports whether an openai_base_url is magpie's, on
// whichever port it listened on then.
func isCodexGateway(u string) bool {
return strings.HasPrefix(u, "http://127.0.0.1:") && strings.HasSuffix(strings.TrimSuffix(u, "/"), gateway.CodexPath)
}
// codexSignedIn reports whether Codex has a sign-in of its own, a ChatGPT
// account or an API key, which its built-in OpenAI provider needs.
func codexSignedIn(dir string) bool {
var a struct {
Key string `json:"OPENAI_API_KEY"`
Tokens struct {
Access string `json:"access_token"`
} `json:"tokens"`
}
b, err := os.ReadFile(filepath.Join(dir, "auth.json"))
if err != nil || json.Unmarshal(b, &a) != nil {
return false
}
return a.Key != "" || a.Tokens.Access != ""
}
+96
View File
@@ -0,0 +1,96 @@
package agent
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/yetone/magpie/internal/provider"
)
func codexHome(t *testing.T, auth, config string) (home string, read func() string) {
t.Helper()
home = t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
t.Setenv("CODEX_HOME", "")
dir := filepath.Join(home, ".codex")
os.MkdirAll(dir, 0o755)
if auth != "" {
os.WriteFile(filepath.Join(dir, "auth.json"), []byte(auth), 0o600)
}
os.WriteFile(filepath.Join(dir, "config.toml"), []byte(config), 0o644)
if err := provider.Save(provider.Provider{ID: "fake", Name: "Fake", Key: "k", Chat: "http://127.0.0.1:1/v1", Models: []string{"m1"}}); err != nil {
t.Fatal(err)
}
return home, func() string {
b, _ := os.ReadFile(filepath.Join(dir, "config.toml"))
return string(b)
}
}
// Signed in, a magpie model points Codex's built-in OpenAI provider at
// magpie and leaves the provider as it is; one of Codex's own models takes
// the base URL away again and brings back what was there.
func TestCodexSignedInRoutesByBaseURL(t *testing.T) {
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`,
"model = \"gpt-5.5\"\nmodel_reasoning_effort = \"xhigh\"\n\n[projects.\"/x\"]\ntrust_level = \"trusted\"\n")
cx := codex(home)
if err := cx.Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
cfg := read()
if !strings.Contains(cfg, `openai_base_url = "http://127.0.0.1:`) || !strings.Contains(cfg, `/backend-api/codex"`) ||
!strings.Contains(cfg, `model = "fake/m1"`) || strings.Contains(cfg, "model_provider") ||
strings.Contains(cfg, "model_catalog_json") || strings.Contains(cfg, "[model_providers") {
t.Fatalf("magpie model:\n%s", cfg)
}
if _, err := os.Stat(filepath.Join(home, ".codex", "magpie-models.json")); err == nil {
t.Error("catalog file written")
}
if err := cx.Fields[0].Set("gpt-5.4"); err != nil {
t.Fatal(err)
}
cfg = read()
if strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, `model = "gpt-5.4"`) ||
!strings.Contains(cfg, `model_reasoning_effort = "xhigh"`) || !strings.Contains(cfg, `trust_level = "trusted"`) {
t.Fatalf("own model:\n%s", cfg)
}
}
// A magpie set up as a provider of Codex's, from before, moves to the base
// URL the next time a magpie model is picked.
func TestCodexSignedInLeavesProviderTable(t *testing.T) {
home, read := codexHome(t, `{"OPENAI_API_KEY":"sk-x"}`,
"model = \"fake/m1\"\nmodel_provider = \"magpie\"\nmodel_catalog_json = \"/x/magpie-models.json\"\n\n[model_providers.magpie]\nname = \"magpie\"\nbase_url = \"http://127.0.0.1:3425/v1\"\nwire_api = \"responses\"\n")
if err := codex(home).Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
cfg := read()
if !strings.Contains(cfg, "openai_base_url") || strings.Contains(cfg, "model_provider") || strings.Contains(cfg, "model_catalog_json") {
t.Fatalf("\n%s", cfg)
}
}
// Not signed in, Codex's OpenAI provider can't run, so magpie is a provider
// of its own.
func TestCodexSignedOutUsesProvider(t *testing.T) {
home, read := codexHome(t, "", "")
cx := codex(home)
if err := cx.Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
cfg := read()
if strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, `model_provider = "magpie"`) ||
!strings.Contains(cfg, "[model_providers.magpie]") || !strings.Contains(cfg, "model_catalog_json") {
t.Fatalf("\n%s", cfg)
}
if err := cx.Fields[0].Set(""); err != nil {
t.Fatal(err)
}
if cfg = read(); strings.Contains(cfg, "magpie") || strings.Contains(cfg, "model") {
t.Fatalf("reset:\n%s", cfg)
}
}
File renamed without changes.
+141
View File
@@ -0,0 +1,141 @@
// Package codexcat renders models the way Codex describes them: the entries
// of its models.json, for `model_catalog_json` and for the model list magpie
// hands Codex in place of the ChatGPT backend's.
package codexcat
import (
_ "embed"
"encoding/json"
"os"
"path/filepath"
"slices"
"strings"
"github.com/yetone/magpie/internal/catalog"
)
// Prompt is Codex's generic system prompt (Apache-2.0, openai/codex,
// core/gpt-5.2-codex_prompt.md). Third-party models need one because the
// bundled catalog only carries prompts for OpenAI models.
//
//go:embed codex_prompt.md
var Prompt string
// DefaultEffort picks the middle of the road: "medium" or "high" when
// offered, else whatever the list starts with.
func DefaultEffort(e []string) string {
for _, want := range []string{"medium", "high"} {
if slices.Contains(e, want) {
return want
}
}
return e[0]
}
// Catalog renders models as a whole models.json.
func Catalog(ms []catalog.Model) []byte {
out := struct {
Models []any `json:"models"`
}{Models: Entries(ms, 0)}
if out.Models == nil {
out.Models = []any{}
}
b, _ := json.MarshalIndent(out, "", " ")
return b
}
// Entries renders models as models.json entries, ranked after the first
// `after`. Only fields Codex requires or that change behaviour are set; the
// rest take Codex's defaults.
func Entries(ms []catalog.Model, after int) []any {
type level struct {
Effort string `json:"effort"`
Description string `json:"description"`
}
type model struct {
Slug string `json:"slug"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Instructions string `json:"base_instructions"`
DefaultEffort *string `json:"default_reasoning_level"`
Efforts []level `json:"supported_reasoning_levels"`
Shell string `json:"shell_type"`
Visibility string `json:"visibility"`
InAPI bool `json:"supported_in_api"`
Priority int `json:"priority"`
Verbosity bool `json:"support_verbosity"`
DefVerbosity *string `json:"default_verbosity"`
ApplyPatch string `json:"apply_patch_tool_type"`
Truncation struct {
Mode string `json:"mode"`
Limit int `json:"limit"`
} `json:"truncation_policy"`
Tools []string `json:"experimental_supported_tools"`
Modalities []string `json:"input_modalities"`
}
own := CacheEntries()
var entries []any
for i, m := range ms {
if raw, ok := own[strings.TrimPrefix(m.ID, "codex/")]; ok && strings.HasPrefix(m.ID, "codex/") {
entries = append(entries, ownEntry(raw, m.ID, m.Name, after+i+1))
continue
}
e := model{
Slug: m.ID, DisplayName: m.Name, Description: m.Name + " via magpie",
Instructions: Prompt, Efforts: []level{},
Shell: "unified_exec", Visibility: "list", InAPI: true, Priority: after + i + 1,
ApplyPatch: "freeform", Tools: []string{}, Modalities: []string{"text"},
}
e.Truncation.Mode, e.Truncation.Limit = "tokens", 10000
for _, ef := range m.Efforts {
e.Efforts = append(e.Efforts, level{Effort: ef})
}
if len(m.Efforts) > 0 {
d := DefaultEffort(m.Efforts)
e.DefaultEffort = &d
}
entries = append(entries, e)
}
return entries
}
// CacheEntries is Codex's own models, as models_cache.json describes them
// for the ChatGPT account it last asked with, by slug.
func CacheEntries() map[string]map[string]any {
home, _ := os.UserHomeDir()
b, err := os.ReadFile(filepath.Join(home, ".codex", "models_cache.json"))
if err != nil {
return nil
}
var cache struct {
Models []map[string]any `json:"models"`
}
if json.Unmarshal(b, &cache) != nil {
return nil
}
out := map[string]map[string]any{}
for _, m := range cache.Models {
if slug, _ := m["slug"].(string); slug != "" {
out[slug] = m
}
}
return out
}
// ownEntry is one of Codex's own models, reached through magpie with the
// ChatGPT sign-in: its entry as Codex has it (images, context window, tools,
// instructions), under magpie's id. The start-up notice and the upgrade
// prompt are left out; they name slugs the catalog does not have.
func ownEntry(raw map[string]any, id, name string, priority int) map[string]any {
e := make(map[string]any, len(raw))
for k, v := range raw {
e[k] = v
}
delete(e, "availability_nux")
delete(e, "upgrade")
e["slug"], e["display_name"], e["priority"], e["visibility"] = id, name, priority, "list"
if s, _ := e["base_instructions"].(string); s == "" {
e["base_instructions"] = Prompt
}
return e
}
@@ -1,4 +1,4 @@
package agent
package codexcat
import (
"encoding/json"
@@ -23,21 +23,21 @@ func TestCodexCatalogKeepsOwnEntries(t *testing.T) {
var got struct {
Models []map[string]any `json:"models"`
}
if err := json.Unmarshal(codexCatalog([]catalog.Model{
if err := json.Unmarshal(Catalog([]catalog.Model{
{ID: "deepseek/deepseek-chat", Name: "deepseek-chat · DeepSeek"},
{ID: "codex/gpt-5.5", Name: "GPT-5.5 · Codex", Efforts: []string{"low", "high"}},
}), &got); err != nil || len(got.Models) != 2 {
t.Fatalf("%v %v", err, got)
}
third, own := got.Models[0], got.Models[1]
if third["slug"] != "deepseek/deepseek-chat" || third["base_instructions"] != codexPrompt || len(third["input_modalities"].([]any)) != 1 {
if third["slug"] != "deepseek/deepseek-chat" || third["base_instructions"] != Prompt || len(third["input_modalities"].([]any)) != 1 {
t.Errorf("third-party entry: %v", third)
}
if own["slug"] != "codex/gpt-5.5" || own["display_name"] != "GPT-5.5 · Codex" || own["priority"] != float64(2) ||
own["context_window"] != float64(272000) || len(own["input_modalities"].([]any)) != 2 {
t.Errorf("own entry: %v", own)
}
if own["base_instructions"] != codexPrompt {
if own["base_instructions"] != Prompt {
t.Error("own entry without base_instructions")
}
if _, ok := own["upgrade"]; ok {
+410
View File
@@ -0,0 +1,410 @@
package gateway
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/klauspost/compress/zstd"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/codexcat"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/usage"
)
// CodexPath is where Codex's built-in OpenAI provider reaches magpie, as its
// `openai_base_url`. Codex keeps its own sign-in and sends what it would send
// the ChatGPT backend: a request for one of its own models goes on there as
// it came, one for a magpie model (a catalog id, provider/model) is served
// like any other, and the model list is OpenAI's with magpie's added. Ending
// in /backend-api/codex keeps Codex treating it as that backend.
const CodexPath = "/backend-api/codex"
// codexAPIBase is where a Codex signed in with an API key sends its requests;
// its model list still comes from the ChatGPT backend. A var so tests can
// point it elsewhere.
var codexAPIBase = "https://api.openai.com/v1"
// magpieCompaction marks a compaction item magpie made: its summary, which
// only magpie reads back.
const magpieCompaction = "magpie1:"
// codexCompactPrompt and codexSummaryPrefix are Codex's own (Apache-2.0,
// openai/codex, prompts/templates/compact).
const codexCompactPrompt = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
Include:
- Current progress and key decisions made
- Important context, constraints, or user preferences
- What remains to be done (clear next steps)
- Any critical data, examples, or references needed to continue
Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`
const codexSummaryPrefix = `Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis:`
func (s *Server) codexBackend(w http.ResponseWriter, r *http.Request) {
// Responses over a WebSocket: 426 sends Codex to plain HTTP at once
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
http.Error(w, "magpie speaks HTTP", http.StatusUpgradeRequired)
return
}
rest := strings.TrimPrefix(r.URL.Path, CodexPath)
body, err := codexBody(r)
if err != nil {
writeError(w, provider.Responses, 400, err.Error())
return
}
switch {
case r.Method == http.MethodGet && rest == "/models":
s.codexModels(w, r)
return
case r.Method == http.MethodPost && rest == "/responses":
if model := modelOf(body); isCatalogID(model) {
body, compact := codexInput(body)
if compact {
s.codexCompact(w, r, body)
return
}
s.serve(w, r, provider.Responses, body)
return
}
body, _ = codexInput(body)
}
s.codexUpstream(w, r, rest, body)
}
// codexBody reads a request's body as it was before Codex compressed it
// (zstd, for the ChatGPT backend), so it can be read and passed on plain.
func codexBody(r *http.Request) ([]byte, error) {
var rd io.Reader = r.Body
switch enc := strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Encoding"))); enc {
case "", "identity":
case "zstd":
d, err := zstd.NewReader(r.Body)
if err != nil {
return nil, err
}
defer d.Close()
rd = d
case "gzip":
g, err := gzip.NewReader(r.Body)
if err != nil {
return nil, err
}
rd = g
default:
return nil, fmt.Errorf("magpie can't read a %s body", enc)
}
r.Header.Del("Content-Encoding")
return io.ReadAll(io.LimitReader(rd, 64<<20))
}
// isCatalogID reports whether a model is one of magpie's, by its id.
// Codex's own models are bare slugs; magpie's are provider/model.
func isCatalogID(model string) bool {
if !strings.Contains(model, "/") {
return false
}
for _, e := range provider.Catalog() {
if e.ID == model {
return true
}
}
return false
}
// codexUpstream relays a request as it came, the sign-in included, to where
// Codex would have sent it.
func (s *Server) codexUpstream(w http.ResponseWriter, r *http.Request, rest string, body []byte) {
start := time.Now()
base := provider.CodexBase
if apiKey(r.Header) && rest != "/models" {
base = codexAPIBase
}
u := base + rest
if r.URL.RawQuery != "" {
u += "?" + r.URL.RawQuery
}
req, err := http.NewRequestWithContext(r.Context(), r.Method, u, bytes.NewReader(body))
if err != nil {
writeError(w, provider.Responses, 502, err.Error())
return
}
copyHeaders(req.Header, r.Header)
// left to the transport, the reply comes back plain for the usage in it
req.Header.Del("Accept-Encoding")
res, err := s.client.Do(req)
if err != nil {
writeError(w, provider.Responses, 502, "OpenAI: "+err.Error())
return
}
defer res.Body.Close()
for k, vs := range res.Header {
if !hopHeader(k) {
w.Header()[k] = vs
}
}
w.WriteHeader(res.StatusCode)
var sniff *usageSniffer
if rest == "/responses" {
ct := res.Header.Get("Content-Type")
if ct == "" && streamOf(body) { // the ChatGPT backend streams without saying so
ct = "text/event-stream"
}
sniff = newSniffer(provider.Responses, ct)
}
f, _ := w.(http.Flusher)
buf := make([]byte, 32<<10)
for {
n, err := res.Body.Read(buf)
if n > 0 {
if sniff != nil {
sniff.write(buf[:n])
}
if _, werr := w.Write(buf[:n]); werr != nil {
break
}
if f != nil {
f.Flush()
}
}
if err != nil {
break
}
}
if sniff == nil {
return
}
call := Call{Time: start, From: provider.Responses, To: provider.Responses, Model: modelOf(body),
Provider: "openai", Agent: usage.AgentOf(r.Header.Get("User-Agent")), Status: res.StatusCode,
Millis: time.Since(start).Milliseconds()}
var uu Usage
uu.add(sniff.usage())
call.Usage = uu
if res.StatusCode >= 400 {
call.Error = res.Status
}
s.record(call)
usage.Append(usage.Record{Time: start, Agent: call.Agent, Provider: call.Provider, Model: call.Model,
Input: uu.Input, Output: uu.Output, CacheRead: uu.CacheRead, CacheWrite: uu.CacheWrite,
Reasoning: uu.Reasoning, Millis: call.Millis, Status: call.Status})
}
// apiKey reports whether Codex signed in with an API key rather than a
// ChatGPT account.
func apiKey(h http.Header) bool {
return strings.HasPrefix(strings.TrimPrefix(h.Get("Authorization"), "Bearer "), "sk-")
}
func hopHeader(k string) bool {
switch http.CanonicalHeaderKey(k) {
case "Connection", "Keep-Alive", "Proxy-Connection", "Transfer-Encoding", "Upgrade", "Te", "Trailer", "Content-Length":
return true
}
return false
}
func copyHeaders(dst, src http.Header) {
for k, vs := range src {
if !hopHeader(k) && http.CanonicalHeaderKey(k) != "Host" {
dst[k] = vs
}
}
}
// codexModels is the ChatGPT backend's model list for this sign-in, with
// magpie's models after it. Should the backend not answer, Codex's last
// list of its own stands in.
func (s *Server) codexModels(w http.ResponseWriter, r *http.Request) {
var own []any
etag := ""
u := provider.CodexBase + "/models"
if r.URL.RawQuery != "" {
u += "?" + r.URL.RawQuery
}
if req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, u, nil); err == nil {
copyHeaders(req.Header, r.Header)
req.Header.Del("Accept-Encoding")
if res, err := s.client.Do(req); err == nil {
var list struct {
Models []any `json:"models"`
}
b, _ := io.ReadAll(io.LimitReader(res.Body, 16<<20))
res.Body.Close()
if res.StatusCode < 300 && json.Unmarshal(b, &list) == nil {
own, etag = list.Models, res.Header.Get("ETag")
} else if res.StatusCode == 401 || res.StatusCode == 403 {
// a sign-in to renew is Codex's to see
w.Header().Set("Content-Type", res.Header.Get("Content-Type"))
w.WriteHeader(res.StatusCode)
w.Write(b)
return
}
}
}
if own == nil {
for _, e := range codexcat.CacheEntries() {
own = append(own, e)
}
}
var ms []catalog.Model
for _, e := range provider.Catalog() {
// a ChatGPT account in magpie offers the models listed already
if e.Provider.Account != nil && e.Provider.Account.Agent == "codex" {
continue
}
ms = append(ms, catalog.Model{ID: e.ID, Name: e.Name + " · " + e.Provider.Name, Efforts: e.Efforts})
}
if etag != "" {
w.Header().Set("ETag", etag)
}
writeJSON(w, 200, map[string]any{"models": append(own, codexcat.Entries(ms, len(own)+100)...)})
}
// codexInput readies Codex's input for whoever serves it: a summary magpie
// made becomes the message it stands for, and the backend's own controls
// go. compact reports a request to compact the conversation, whose trigger
// becomes the request to summarise it.
func codexInput(body []byte) (_ []byte, compact bool) {
var q map[string]json.RawMessage
if json.Unmarshal(body, &q) != nil {
return body, false
}
var items []map[string]any
if json.Unmarshal(q["input"], &items) != nil {
return body, false
}
changed := false
out := make([]map[string]any, 0, len(items))
for _, it := range items {
switch it["type"] {
case "compaction", "compaction_summary":
enc, _ := it["encrypted_content"].(string)
sum, ok := strings.CutPrefix(enc, magpieCompaction)
if !ok {
out = append(out, it) // OpenAI's own, for OpenAI
continue
}
changed = true
if b, err := base64.StdEncoding.DecodeString(sum); err == nil {
out = append(out, userMessage(codexSummaryPrefix+"\n"+string(b)))
}
case "compaction_trigger":
changed, compact = true, true
out = append(out, userMessage(codexCompactPrompt))
default:
out = append(out, it)
}
}
if !changed {
return body, false
}
b, _ := json.Marshal(out)
q["input"] = b
if compact {
// the summary is text; a tool call would be no summary
delete(q, "tools")
delete(q, "tool_choice")
delete(q, "parallel_tool_calls")
q["stream"] = json.RawMessage("false")
}
nb, err := json.Marshal(q)
if err != nil {
return body, false
}
return nb, compact
}
func userMessage(text string) map[string]any {
return map[string]any{"type": "message", "role": "user",
"content": []map[string]any{{"type": "input_text", "text": text}}}
}
// codexCompact compacts a conversation held by a magpie model: the model
// summarises it, and the summary goes back to Codex as the compaction item
// the backend would have made, marked as magpie's so magpie reads it back
// in later requests.
func (s *Server) codexCompact(w http.ResponseWriter, r *http.Request, body []byte) {
rec := &recorder{header: http.Header{}, status: 200}
s.serve(rec, r, provider.Responses, body)
if rec.status >= 400 {
for k, vs := range rec.header {
w.Header()[k] = vs
}
w.WriteHeader(rec.status)
w.Write(rec.body.Bytes())
return
}
var res struct {
ID string `json:"id"`
Output []struct {
Type string `json:"type"`
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
} `json:"output"`
Usage json.RawMessage `json:"usage"`
}
if err := json.Unmarshal(rec.body.Bytes(), &res); err != nil {
writeError(w, provider.Responses, 502, "compaction: "+err.Error())
return
}
var sum strings.Builder
for _, o := range res.Output {
if o.Type != "message" {
continue
}
for _, c := range o.Content {
sum.WriteString(c.Text)
}
}
if strings.TrimSpace(sum.String()) == "" {
writeError(w, provider.Responses, 502, "compaction: the model wrote no summary")
return
}
id := res.ID
if id == "" {
id = fmt.Sprintf("resp_magpie_%d", time.Now().UnixNano())
}
item := map[string]any{"type": "compaction", "id": "cmp_" + strings.TrimPrefix(id, "resp_"),
"encrypted_content": magpieCompaction + base64.StdEncoding.EncodeToString([]byte(sum.String()))}
done := map[string]any{"id": id, "object": "response", "status": "completed", "output": []any{item}}
if len(res.Usage) > 0 {
done["usage"] = res.Usage
}
h := w.Header()
h.Set("Content-Type", "text/event-stream")
h.Set("Cache-Control", "no-cache")
w.WriteHeader(200)
for _, ev := range []map[string]any{
{"type": "response.created", "response": map[string]any{"id": id, "object": "response", "status": "in_progress", "output": []any{}}},
{"type": "response.output_item.added", "output_index": 0, "item": item},
{"type": "response.output_item.done", "output_index": 0, "item": item},
{"type": "response.completed", "response": done},
} {
b, _ := json.Marshal(ev)
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", ev["type"], b)
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
// recorder keeps a reply for a second look.
type recorder struct {
header http.Header
status int
body bytes.Buffer
}
func (r *recorder) Header() http.Header { return r.header }
func (r *recorder) WriteHeader(status int) { r.status = status }
func (r *recorder) Write(b []byte) (int, error) { return r.body.Write(b) }
+201
View File
@@ -0,0 +1,201 @@
package gateway
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/klauspost/compress/zstd"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/usage"
)
// chatgpt stands in for the ChatGPT backend behind CodexBase.
func chatgpt(t *testing.T, h http.HandlerFunc) *httptest.Server {
t.Helper()
t.Setenv("HOME", t.TempDir()) // no sign-ins but the test's
up := httptest.NewServer(h)
t.Cleanup(up.Close)
was := provider.CodexBase
provider.CodexBase = up.URL + "/backend-api/codex"
t.Cleanup(func() { provider.CodexBase = was })
return up
}
func codexPost(t *testing.T, body string) (int, string) {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest("POST", CodexPath+"/responses", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer chatgpt-token")
req.Header.Set("chatgpt-account-id", "acct-1")
New().Handler().ServeHTTP(rec, req)
return rec.Code, rec.Body.String()
}
// One of Codex's own models goes on to the ChatGPT backend as it came, the
// sign-in with it; a summary magpie made earlier goes as the text it holds.
func TestCodexOwnModelPassesThrough(t *testing.T) {
setup(t, provider.Chat, &fake{t: t})
var got []byte
var head http.Header
var path string
chatgpt(t, func(w http.ResponseWriter, r *http.Request) {
got, _ = io.ReadAll(r.Body)
head, path = r.Header, r.URL.Path
w.Header()["Content-Type"] = nil // as the ChatGPT backend sends it
io.WriteString(w, sse(
`data: {"type":"response.created","response":{"id":"r1"}}`,
`data: {"type":"response.completed","response":{"id":"r1","usage":{"input_tokens":9,"output_tokens":2}}}`))
})
sum := magpieCompaction + base64.StdEncoding.EncodeToString([]byte("did X, next Y"))
code, body := codexPost(t, `{"model":"gpt-5.5","stream":true,"input":[
{"type":"compaction","encrypted_content":"`+sum+`"},
{"type":"compaction","encrypted_content":"openai-own"},
{"type":"message","role":"user","content":[{"type":"input_text","text":"go on"}]}]}`)
if code != 200 || !strings.Contains(body, `"input_tokens":9`) {
t.Fatalf("%d %s", code, body)
}
if u := usage.Load(time.Time{}); len(u) != 1 || u[0].Input != 9 || u[0].Output != 2 || u[0].Provider != "openai" {
t.Errorf("usage %+v", u)
}
if path != "/backend-api/codex/responses" || head.Get("Authorization") != "Bearer chatgpt-token" || head.Get("chatgpt-account-id") != "acct-1" {
t.Errorf("upstream %s %v", path, head)
}
var q struct {
Input []map[string]any `json:"input"`
}
json.Unmarshal(got, &q)
if len(q.Input) != 3 || q.Input[0]["type"] != "message" || !strings.Contains(string(got), "did X, next Y") ||
q.Input[1]["encrypted_content"] != "openai-own" {
t.Errorf("input: %s", got)
}
}
// A magpie model is served by magpie, whatever sign-in Codex sent.
func TestCodexMagpieModelServed(t *testing.T) {
f := &fake{t: t, reply: sse(
`data: {"id":"c1","model":"m1","choices":[{"delta":{"content":"hi"}}]}`,
`data: {"id":"c1","choices":[{"delta":{},"finish_reason":"stop"}]}`,
`data: [DONE]`)}
setup(t, provider.Chat, f)
chatgpt(t, func(w http.ResponseWriter, r *http.Request) { t.Error("sent to ChatGPT") })
code, body := codexPost(t, `{"model":"fake/m1","stream":true,"input":"hello"}`)
if code != 200 || !strings.Contains(body, `"delta":"hi"`) {
t.Fatalf("%d %s", code, body)
}
if !strings.Contains(string(f.got), `"model":"m1"`) {
t.Errorf("upstream got %s", f.got)
}
}
// Codex compresses what it sends the ChatGPT backend; magpie reads it all
// the same.
func TestCodexReadsCompressedBody(t *testing.T) {
f := &fake{t: t, reply: sse(
`data: {"id":"c1","model":"m1","choices":[{"delta":{"content":"hi"}}]}`,
`data: {"id":"c1","choices":[{"delta":{},"finish_reason":"stop"}]}`,
`data: [DONE]`)}
setup(t, provider.Chat, f)
chatgpt(t, func(w http.ResponseWriter, r *http.Request) { t.Error("sent to ChatGPT") })
enc, _ := zstd.NewWriter(nil)
z := enc.EncodeAll([]byte(`{"model":"fake/m1","stream":true,"input":"hello"}`), nil)
rec := httptest.NewRecorder()
req := httptest.NewRequest("POST", CodexPath+"/responses", bytes.NewReader(z))
req.Header.Set("Content-Encoding", "zstd")
New().Handler().ServeHTTP(rec, req)
if rec.Code != 200 || !strings.Contains(rec.Body.String(), `"delta":"hi"`) {
t.Fatalf("%d %s", rec.Code, rec.Body)
}
}
// Compacting a magpie model's conversation: the model summarises, and Codex
// gets the compaction item it asked for, holding the summary.
func TestCodexCompactsMagpieModel(t *testing.T) {
f := &fake{t: t, reply: sse(
`data: {"id":"c1","model":"m1","choices":[{"delta":{"content":"SUMM"}}]}`,
`data: {"id":"c1","choices":[{"delta":{"content":"ARY"},"finish_reason":"stop"}]}`,
`data: [DONE]`)}
setup(t, provider.Chat, f)
code, body := codexPost(t, `{"model":"fake/m1","stream":true,
"tools":[{"type":"function","name":"shell","parameters":{"type":"object"}}],
"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"fix the bug"}]},
{"type":"compaction_trigger"}]}`)
if code != 200 {
t.Fatalf("%d %s", code, body)
}
if strings.Contains(string(f.got), `"tools"`) || !strings.Contains(string(f.got), "CONTEXT CHECKPOINT COMPACTION") {
t.Errorf("upstream got %s", f.got)
}
var item map[string]any
completed := false
for _, e := range events(body) {
switch e["type"] {
case "response.output_item.done":
item = e["item"].(map[string]any)
case "response.completed":
completed = true
}
}
enc, _ := item["encrypted_content"].(string)
b, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(enc, magpieCompaction))
if item["type"] != "compaction" || !strings.HasPrefix(enc, magpieCompaction) || string(b) != "SUMMARY" || !completed {
t.Errorf("events: %s", body)
}
}
// The model list is the backend's for this sign-in, then magpie's.
func TestCodexModelList(t *testing.T) {
setup(t, provider.Chat, &fake{t: t})
var auth string
chatgpt(t, func(w http.ResponseWriter, r *http.Request) {
auth = r.Header.Get("Authorization")
w.Header().Set("ETag", `"v1"`)
io.WriteString(w, `{"models":[{"slug":"gpt-5.5","priority":1}]}`)
})
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", CodexPath+"/models?client_version=0.155.1", nil)
req.Header.Set("Authorization", "Bearer chatgpt-token")
New().Handler().ServeHTTP(rec, req)
var list struct {
Models []map[string]any `json:"models"`
}
json.Unmarshal(rec.Body.Bytes(), &list)
var slugs []string
var fake map[string]any
for _, m := range list.Models {
slug, _ := m["slug"].(string)
slugs = append(slugs, slug)
if slug == "fake/m1" {
fake = m
}
}
// Other sign-ins found on the machine (Cursor's, say) may follow.
if rec.Code != 200 || len(slugs) < 2 || slugs[0] != "gpt-5.5" || fake == nil || fake["base_instructions"] == "" ||
rec.Header().Get("ETag") != `"v1"` || auth != "Bearer chatgpt-token" {
t.Errorf("%d %v %q %q", rec.Code, slugs, rec.Header().Get("ETag"), auth)
}
for _, s := range slugs {
if strings.HasPrefix(s, "codex/") {
t.Errorf("Codex's own models twice: %v", slugs)
}
}
}
// Responses over a WebSocket are turned away so Codex uses HTTP at once.
func TestCodexWebSocketUpgradeRequired(t *testing.T) {
chatgpt(t, func(w http.ResponseWriter, r *http.Request) { t.Error("sent to ChatGPT") })
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", CodexPath+"/responses", nil)
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
New().Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUpgradeRequired {
t.Errorf("%d", rec.Code)
}
}
+1
View File
@@ -162,6 +162,7 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("POST /messages", s.handle(provider.Anthropic))
mux.HandleFunc("POST /v1/messages/count_tokens", s.countTokens)
mux.HandleFunc("POST /_magpie/claude-mcp/{token}", s.subscription.mcpCall)
mux.HandleFunc(CodexPath+"/", s.codexBackend)
mux.HandleFunc("GET /v1beta/models", s.geminiModels)
mux.HandleFunc("POST /v1beta/models/{call...}", s.gemini)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {