agent: Codex on one of magpie's models beside its ChatGPT sign-in stays connected when Codex writes one of its own models back, and connecting keeps the model it was on through magpie where it can't join (#940, xybio)

Picked "via magpie" on a ChatGPT-signed Codex, a magpie model is set beside the sign-in: openai_base_url to the gateway, the magpie table, no model_provider. When Codex itself (its /model, or a Codex app still running on its old pick) then wrote one of its own models back (gpt-6.1-sol), Wired found nothing to say magpie had put it there, so the row read Not connected while its config still pointed at magpie's gateway. Set so, Codex keeps a codex.beside mark (gone when magpie becomes its provider, on a pick of its own through the picker, or on disconnect), and Agent.Beside makes Wired true on it; the model written back is still told as a change from outside (#750's drift stays). Connecting where Codex can't join (its ChatGPT allowance used up, or a model_provider of its own) went to the first of magpie's other models (Grok 4.7, "magpie doesn't serve the model it was on"), since Codex's field leaves out magpie's codex/* models: Agent.OwnVia now finds the model it is on as magpie serves it on the user's ChatGPT account (codex/gpt-6.1-sol), and Connect keeps it there.

TestCodexOwnPickAfterMagpieModelStaysConnected and TestCodexConnectKeepsItsOwnModelThroughMagpie are new; with the change taken out the first reads the reported config (model gpt-6.1-sol, openai_base_url, [model_providers.magpie], no model_provider) as not connected and the second connects to fake/m1 "first". Tested in temp HOMEs only. go vet, go build (GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/proc TestUserPath timed out waiting on a login shell under load once, and passes on its own).
This commit is contained in:
yetone committed 2026-10-06 02:30:31 +08:00
1 parent 4c170cbad6
commit ab47042ba8
4 files changed
+115 -4

No files matched your search

+11
View File
@@ -121,6 +121,17 @@ type Agent struct {
// Joined reports an agent Join connected: magpie is in its config
// though no field is on one of magpie's models.
Joined func() bool
// Beside reports an agent set on one of magpie's models beside its own
// (Codex by the base URL beside its ChatGPT sign-in), now on one of its
// own written in by the agent: still connected, as joined, though the
// change is told as drift (#940).
Beside func() bool
// OwnVia is the catalog id magpie serves one of the agent's own models
// by on a sign-in of the user's, which its model field lists as its
// own rather than as magpie's ("codex/gpt-5.5" for Codex's gpt-5.5),
// "" for none: Connect keeps the agent on that model through magpie
// where it can't Join (#940: Codex went to an unrelated model).
OwnVia func(model string) string
// Routed reports that the agent's config sends whatever model it
// names to magpie's gateway (Codex's openai_base_url or magpie as its
// provider), so a model's name the gateway takes as a routing group
+7 -1
View File
@@ -347,7 +347,7 @@ func (a *Agent) Wired() bool {
if a.Native != nil {
return a.Native.Read().Provider == "connected"
}
if a.Joined != nil && a.Joined() {
if a.Joined != nil && a.Joined() || a.Beside != nil && a.Beside() {
return true
}
vals := a.Values()
@@ -544,6 +544,12 @@ func (a *Agent) connect() (Connection, error) {
sub = o.Value
}
}
// the model it is on, of its own, as magpie serves it on a sign-in of
// the user's, which its field lists as its own (Codex's on a ChatGPT
// account it can't join, #940)
if same == "" && alike == "" && cur != "" && a.OwnVia != nil {
same = a.OwnVia(cur)
}
how := "first"
switch {
case same != "":
+15 -3
View File
@@ -113,13 +113,20 @@ func codexIn(at place) *Agent {
// joined: connected by Join, on a model of Codex's own (its last pick)
// with magpie's beside it, by the base URL
joined := func() bool { return viaBase() && stashLoad()[at.key("codex.joined")] == "1" }
// beside: set on one of magpie's models beside the ChatGPT sign-in, by
// the base URL, Codex's own models in its list too: one of them written
// in by Codex (its /model, or a Codex app still running on its old
// pick) leaves it connected, its config still on magpie's gateway
// (#940: it read as not connected), the change told as drift. One
// picked on the Agents page still takes magpie out (set).
beside := func() bool { return viaBase() && stashLoad()[at.key("codex.beside")] == "1" }
// magpie is in Codex's config: on one of magpie's models, or joined;
// or, routed, on the model of the group magpie set it to as Codex
// spells it (gpt-6.1-sol for group/auto-gpt-6-1-sol, #750), which the
// gateway takes as that group
wired := func() bool {
m := get("model")
return isMagpie(m) || joined() || routed() && setAsGroup(cmp.Or(at.id, "codex"), "model", m)
return isMagpie(m) || joined() || beside() || routed() && setAsGroup(cmp.Or(at.id, "codex"), "model", m)
}
models := func() []catalog.Model {
switch {
@@ -476,7 +483,7 @@ func codexIn(at place) *Agent {
// provider, catalog and effort); it answers the model Codex was on
// then, for Unwire to go back to
unroute := func() (string, error) {
forget(at.key("codex.out"), at.key("codex.joined"))
forget(at.key("codex.out"), at.key("codex.joined"), at.key("codex.beside"))
if err := giveTables(); err != nil {
return "", err
}
@@ -529,7 +536,7 @@ func codexIn(at place) *Agent {
return err
}
os.Remove(catalogPath)
forget(at.key("codex.model"), at.key("codex.effort"), at.key("codex.provider"), at.key("codex.catalog"), at.key("codex.out"), at.key("codex.joined"))
forget(at.key("codex.model"), at.key("codex.effort"), at.key("codex.provider"), at.key("codex.catalog"), at.key("codex.out"), at.key("codex.joined"), at.key("codex.beside"))
return nil
}
// magpie API: magpie stays Codex's provider whichever model is the
@@ -577,6 +584,7 @@ func codexIn(at place) *Agent {
); err != nil {
return err
}
stash(map[string]string{at.key("codex.beside"): "1"})
if err := takeTables(); err != nil {
return err
}
@@ -587,6 +595,8 @@ func codexIn(at place) *Agent {
} else {
forget(at.key("codex.out"))
}
// magpie is its provider now, not beside the sign-in
forget(at.key("codex.beside"))
if err := putProvider(); err != nil {
return err
}
@@ -707,7 +717,9 @@ func codexIn(at place) *Agent {
return true, settle()
},
Joined: joined,
Beside: beside,
Routed: routed,
OwnVia: codexOwnViaMagpie,
Sync: func() error {
// model_provider = "magpie" left with its table gone (taken by
// another tool, or a hand edit), which keeps Codex from loading
+82
View File
@@ -0,0 +1,82 @@
package agent
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/yetone/magpie/internal/edit"
)
// Codex signed in with ChatGPT, connected on one of magpie's models beside
// its sign-in, stays connected when Codex writes one of its own models in
// (its /model, or a Codex app still running on its old pick): its own
// models are in its list beside magpie's, as when it joined (#940: Codex
// read as not connected then, its config still on magpie's gateway).
// Disconnect leaves it on that model. Made magpie's provider (its
// allowance used up), it is not beside the sign-in.
func TestCodexOwnPickAfterMagpieModelStaysConnected(t *testing.T) {
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, "model = \"gpt-5.5\"\n")
cx := codex(home)
if err := cx.Apply("model", "fake/m1"); err != nil {
t.Fatal(err)
}
if !cx.Wired() {
t.Fatalf("not connected on fake/m1:\n%s", read())
}
if err := edit.SetTOMLTop(filepath.Join(home, ".codex", "config.toml"), edit.KV{Path: "model", Value: "gpt-6.1-sol"}); err != nil {
t.Fatal(err)
}
if !cx.Wired() {
t.Fatalf("Codex's own pick disconnected it:\n%s", read())
}
// told as a change from outside, the row still connected
if d := cx.Drift(); d == nil || d.Kind != "replaced" || d.Now != "gpt-6.1-sol" {
t.Fatalf("drift: %+v", d)
}
if err := cx.Disconnect(); err != nil {
t.Fatal(err)
}
if cfg := read(); cx.Wired() || strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, `model = "gpt-6.1-sol"`) {
t.Fatalf("disconnected:\n%s", cfg)
}
// out of its allowance, magpie is its provider: an own model written
// back is a change from outside, not a pick beside magpie's
codexUsedUp = func() bool { return true }
if err := cx.Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
path := filepath.Join(home, ".codex", "config.toml")
if err := edit.SetTOMLTop(path, edit.KV{Path: "model", Value: "gpt-6.1-sol"}); err != nil {
t.Fatal(err)
}
if err := edit.DelTOMLTop(path, "model_provider"); err != nil {
t.Fatal(err)
}
if cx.Wired() {
t.Fatalf("still beside the sign-in after magpie became its provider:\n%s", read())
}
}
// Connected while Codex can't join (its ChatGPT account used up, or a
// provider of its own in config.toml), Codex stays on the model it was on
// as magpie serves it on the ChatGPT account, not the first of magpie's
// other models (#940: it went to Grok 4.7, "magpie doesn't serve the model
// it was on").
func TestCodexConnectKeepsItsOwnModelThroughMagpie(t *testing.T) {
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, "model = \"gpt-b\"\n")
os.WriteFile(filepath.Join(home, ".codex", "models_cache.json"), []byte(`{"models":[
{"slug":"gpt-a","display_name":"A","priority":1},
{"slug":"gpt-b","display_name":"B","priority":2}]}`), 0o644)
codexUsedUp = func() bool { return true }
cx := codex(home)
c, err := cx.ConnectHow()
if err != nil {
t.Fatal(err)
}
if c.How != "same" || c.Value != "codex/gpt-b" || !strings.Contains(read(), `model = "codex/gpt-b"`) {
t.Fatalf("connected %+v:\n%s", c, read())
}
}