codex: a ChatGPT account out of allowance gets magpie as a provider of Codex's, and a routing group is named as one in the Codex app's list — signed in through openai_base_url, the Codex app won't send anything once the account has used its allowance up, a magpie model's request too, so magpie is then wired as it is when signed out (model_provider, as the account's own models can't run anyway); and the gateway's model list for the signed-in app named a group after its first member's provider ("z-ai/glm-5.3 · nvidia"), reading as that provider's own model, where it now says routing group, as magpie's other lists do

This commit is contained in:
yetone committed 2026-09-25 06:49:01 +08:00
1 parent 612e1d6e5c
commit bd5bf422ed
5 files changed
+84 -3

No files matched your search

+17 -2
View File
@@ -1,11 +1,13 @@
package agent
import (
"context"
"encoding/json"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/codexcat"
@@ -22,7 +24,9 @@ import (
// 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.
// `model_provider = "magpie"`, and a model catalog file for /model. So it
// is too for a ChatGPT account that has used its allowance up, which the
// Codex app won't send anything for, whoever serves the model.
func codex(home string) *Agent {
dir := filepath.Join(home, ".codex")
@@ -91,7 +95,10 @@ func codex(home string) *Agent {
stash(map[string]string{"codex.model": get("model"), "codex.effort": get("model_reasoning_effort"),
"codex.provider": get("model_provider"), "codex.catalog": get("model_catalog_json")})
}
if codexSignedIn(dir) {
// a ChatGPT account out of allowance keeps the Codex app from
// sending at all, a magpie model's request too; as a provider
// of Codex's own, magpie is past that
if codexSignedIn(dir) && !codexUsedUp() {
if err := dropProvider(); err != nil {
return err
}
@@ -246,6 +253,14 @@ func isCodexGateway(u string) bool {
return strings.HasPrefix(u, "http://127.0.0.1:") && strings.HasSuffix(strings.TrimSuffix(u, "/"), gateway.CodexPath)
}
// codexUsedUp reports whether the ChatGPT account Codex is signed in to
// has used up its allowance. A var so tests can say.
var codexUsedUp = func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return provider.CodexUsedUp(ctx)
}
// 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 {
+18
View File
@@ -16,6 +16,9 @@ func codexHome(t *testing.T, auth, config string) (home string, read func() stri
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
t.Setenv("CODEX_HOME", "")
usedUp := codexUsedUp
codexUsedUp = func() bool { return false }
t.Cleanup(func() { codexUsedUp = usedUp })
dir := filepath.Join(home, ".codex")
os.MkdirAll(dir, 0o755)
if auth != "" {
@@ -94,3 +97,18 @@ func TestCodexSignedOutUsesProvider(t *testing.T) {
t.Fatalf("reset:\n%s", cfg)
}
}
// A ChatGPT account that has used its allowance up keeps the Codex app from
// sending, so magpie is a provider of Codex's then, as when signed out.
func TestCodexUsedUpUsesProvider(t *testing.T) {
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, "")
codexUsedUp = func() bool { return true }
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 = "magpie"`) ||
!strings.Contains(cfg, "[model_providers.magpie]") || !strings.Contains(cfg, `model = "fake/m1"`) {
t.Fatalf("\n%s", cfg)
}
}
+6 -1
View File
@@ -260,7 +260,12 @@ func (s *Server) codexModels(w http.ResponseWriter, r *http.Request) {
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})
// a group answers for its first member but is not that provider's
by := e.Provider.Name
if e.Group != "" {
by = "routing group"
}
ms = append(ms, catalog.Model{ID: e.ID, Name: e.Name + " · " + by, Efforts: e.Efforts})
}
if etag != "" {
w.Header().Set("ETag", etag)
+26
View File
@@ -199,3 +199,29 @@ func TestCodexWebSocketUpgradeRequired(t *testing.T) {
t.Errorf("%d", rec.Code)
}
}
// A routing group in Codex's model list is named as one, not as its first
// member's provider, which would read as that provider's own model.
func TestCodexModelsNameGroups(t *testing.T) {
setup(t, provider.Chat, &fake{t: t})
chatgpt(t, func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, `{"models":[]}`) })
if err := provider.SaveGroup(provider.Group{Name: "G", Members: []string{"fake/m1"}}); err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
New().Handler().ServeHTTP(rec, httptest.NewRequest("GET", CodexPath+"/models", nil))
var list struct {
Models []struct {
Slug string `json:"slug"`
Name string `json:"display_name"`
} `json:"models"`
}
json.Unmarshal(rec.Body.Bytes(), &list)
names := map[string]string{}
for _, m := range list.Models {
names[m.Slug] = m.Name
}
if names["group/g"] != "G · routing group" || !strings.HasSuffix(names["fake/m1"], " · Fake") {
t.Fatalf("%v", names)
}
}
+17
View File
@@ -114,3 +114,20 @@ func loginQuota(ctx context.Context, l Login) SubscriptionQuota {
}
return q
}
// CodexUsedUp reports whether the ChatGPT account Codex is signed in to has
// used up its allowance for now; false when that isn't known.
func CodexUsedUp(ctx context.Context) bool {
for _, l := range Logins("codex") {
if !l.Active {
continue
}
q := loginQuota(ctx, l)
for _, w := range q.Windows {
if !w.Aside && w.Model == "" && w.Used >= 100 {
return true
}
}
}
return false
}