Files
magpie/internal/gateway/codex_model_order_test.go
T
yetone 273be22cac agents: Codex's /model lists its models in the order dragged on the Agents page (M3chD09, #855) — Codex lists the models by the priority magpie gives them: a ChatGPT account's own first, as the backend has them, then magpie's in the catalog's order, with no way to change it. Codex's model list (Pick on its row) now has a third view, Order: the models shown as Codex lists them, numbered, each dragged elsewhere (its grip shows on hover) or moved with Alt+↑/↓; it is saved at once (settings orderedModels, by agent and entry id), and the account's own models and magpie's go in one order. A model not in the order (one added later) goes after those that are, as it stood; the hidden stay hidden and out of the order view, hiding keeps the order, and a provider renamed renames its ids in the order too. Default order puts magpie's back. Codex gets it at once: signed in to ChatGPT, the gateway's /models gives the backend's and magpie's models priorities 1..n in that order and the list's ETag carries the order so Codex asks again; not signed in, the catalog file magpie writes for Codex (ListedFor → InOrder) is rewritten in it. Only Codex's list can be ordered; base_instructions are untouched. zh, ja, de strings. TestCodexModelListInUsersOrder (gateway: default order, an order across the account's and magpie's, the ETag changing, CatalogFor, opencode's untouched, none restoring it) and TestAgentModelOrderAPI (gui: orderable, an order read back, a new model last, hidden kept, hiding keeping it, reset, opencode refused) fail on the old code (no SetModelOrder/ModelOrder); codex-model-order.test.cjs (WebKit, en and zh: Order lists the own first and leaves the hidden out, Alt+↓ and a drag post the order, Default order posts none, nothing scrolls; a list that can't be ordered has no Order) fails on the old code (no Order). agent-models, agent-models-all-hidden, agent-models-loading, agent-model-groups, click-scroll, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 11:52:34 +08:00

105 lines
3.6 KiB
Go

package gateway
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"slices"
"testing"
"github.com/yetone/magpie/internal/provider"
)
// Codex's /model lists the models in the order dragged on the Agents page
// (M3chD09, #855): the account's own and magpie's in one order, a model
// it doesn't name after them as it stood, and the ETag changes with the
// order so Codex asks for the list again. Without one, the backend's come
// first, as before.
func TestCodexModelListInUsersOrder(t *testing.T) {
codexSignedIn(t)
if err := provider.Save(provider.Provider{ID: "codex", Models: []string{"gpt-5.5", "gpt-5-codex"}}); err != nil {
t.Fatal(err)
}
if err := provider.Save(provider.Provider{ID: "relay", Name: "Relay", Key: "k", Anthropic: "http://127.0.0.1:1",
Models: []string{"claude-opus-5.5", "claude-sonnet-4-5"}}); err != nil {
t.Fatal(err)
}
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("ETag", `"v1"`)
io.WriteString(w, `{"models":[{"slug":"gpt-5.5","priority":1},{"slug":"gpt-5-codex","priority":2}]}`)
}))
defer up.Close()
was := provider.CodexBase
provider.CodexBase = up.URL + "/backend-api/codex"
defer func() { provider.CodexBase = was }()
// the slugs as Codex lists them, by priority, and the list's ETag
list := func() ([]string, string) {
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", CodexPath+"/models", nil)
req.Header.Set("Authorization", "Bearer chatgpt-token")
New().Handler().ServeHTTP(rec, req)
var got struct {
Models []map[string]any `json:"models"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
slices.SortStableFunc(got.Models, func(a, b map[string]any) int {
return int(a["priority"].(float64) - b["priority"].(float64))
})
var out []string
for _, m := range got.Models {
out = append(out, m["slug"].(string))
}
return out, rec.Header().Get("ETag")
}
before, tag := list()
if want := []string{"gpt-5.5", "gpt-5-codex", "relay/claude-opus-5.5", "relay/claude-sonnet-4-5"}; !slices.Equal(before, want) {
t.Fatalf("default order %v, want %v", before, want)
}
var own string
for _, e := range provider.Catalog() {
if provider.CodexOwn(e) && e.Model == "gpt-5-codex" {
own = e.ID
}
}
if own == "" {
t.Fatal("no catalog entry for the account's gpt-5-codex")
}
// magpie's sonnet first, then the account's gpt-5-codex; the rest after
if err := provider.SetModelOrder("codex", []string{"relay/claude-sonnet-4-5", own}); err != nil {
t.Fatal(err)
}
after, tag2 := list()
if want := []string{"relay/claude-sonnet-4-5", "gpt-5-codex", "gpt-5.5", "relay/claude-opus-5.5"}; !slices.Equal(after, want) {
t.Errorf("ordered %v, want %v", after, want)
}
if tag2 == tag {
t.Errorf("the ETag %q didn't change with the order", tag)
}
// the catalog magpie writes for Codex follows it too
shown, _ := provider.CatalogFor("codex")
var ids []string
for _, e := range shown {
ids = append(ids, e.ID)
}
if len(ids) < 2 || ids[0] != "relay/claude-sonnet-4-5" || ids[1] != own {
t.Errorf("CatalogFor(codex) %v", ids)
}
// another agent's list keeps magpie's own order
other, _ := provider.CatalogFor("opencode")
for i, e := range other {
if e.ID == "relay/claude-sonnet-4-5" && i == 0 {
t.Errorf("opencode's list took Codex's order: %v", other)
}
}
// none puts the backend's first again
if err := provider.SetModelOrder("codex", nil); err != nil {
t.Fatal(err)
}
if back, _ := list(); !slices.Equal(back, before) {
t.Errorf("after the order was taken away %v, want %v", back, before)
}
}