The Codex app resumes a thread on the provider it was started on, kept in
Codex's state database (app-server thread_processor.rs:
merge_persisted_resume_metadata), whatever config.toml's model_provider is
now, and loads no config for it when config.toml has no table of that id
(core config: "Model provider `{id}` not found"). CC Switch puts every
third-party thread on "custom" and rewrites config.toml whole when it
switches, so a switch to its official provider leaves those threads with no
table, and the app can't open them. magpie never removes [model_providers.custom],
but nothing put it back either.
While magpie is wired into Codex (takeTables: set, Join and Sync), each
provider a thread in state_*.sqlite names that config.toml has none of
(table, inline or dotted) gets a table pointed at magpie's gateway, as
CC Switch's own tables already are. Codex's built-in providers (openai,
ollama, lmstudio, amazon-bedrock[-runtime], ollama-chat) and magpie's own
are left out, and a provider the user has stays as it is. The table stays
when Codex steps off magpie, as magpie's own does, for the threads that
name it. Not wired, nothing is written. sessions.CodexThreadProviders reads
the distinct model_provider of the threads table, read-only.
Run:
- Real Codex 0.161 in a sandbox HOME: `codex exec` recorded a thread under
a [model_providers.custom] pointed at a fake Responses server, the table
was removed, and the app-server's thread/resume answered "failed to load
configuration: Model provider `custom` not found" (the reporter's error).
With this build's `magpie codex fakerelay/m1` (sandbox HOME, gateway on
127.0.0.1:3546), [model_providers.custom] was written to the gateway, the
same thread/resume succeeded, and a turn on it completed through magpie to
the provider (its request reached the fake). The thread kept "custom".
- TestCodexWritesTablesThreadsName (signed in and not) fails without the
call ("custom not written through magpie"), and its inline case fails
without the parsed check ("edited TOML is invalid: key inl should be a
table"); TestCodexThreadTablesOnlyWired.
- gofmt -l, go vet, GOOS=linux/windows go vet -tags nogui on agent and
sessions, GOOS=linux go build -tags nogui, GOOS=windows go build, and
go test -tags nogui ./... under a temp HOME. One full run failed
TestSetAnswersWhileDevinAndProvidersHang (internal/gui, set took 1.6s
against its 1.5s wall-clock bound; Claude only, Codex not wired, so this
code isn't on its path); it passes alone 5/5 (0.19-0.42s) and in a rerun
of gui, agent and sessions together.
105 lines
3.8 KiB
Go
105 lines
3.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"database/sql"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/yetone/magpie/internal/edit"
|
|
)
|
|
|
|
// codexThreads makes Codex's state database with a thread on each of the
|
|
// providers, as Codex 0.161's state_5.sqlite keeps them (the columns that
|
|
// matter).
|
|
func codexThreads(t *testing.T, dir string, providers ...string) {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", filepath.Join(dir, "state_5.sqlite"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
if _, err := db.Exec(`CREATE TABLE threads (id TEXT PRIMARY KEY, rollout_path TEXT NOT NULL, model_provider TEXT NOT NULL, model TEXT)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, p := range providers {
|
|
if _, err := db.Exec(`INSERT INTO threads VALUES (?, 'x', ?, 'gpt-5.4')`, string(rune('a'+i)), p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// #1372: a thread started through CC Switch keeps model_provider "custom",
|
|
// and the Codex app resumes it on that provider. CC Switch rewrites
|
|
// config.toml whole when it switches, so its table can be gone, and the
|
|
// app then loads no config for the thread: "Model provider `custom` not
|
|
// found". While magpie is wired, each provider a thread names that has no
|
|
// table gets one pointed at magpie, and keeps it when Codex steps off
|
|
// magpie; Codex's built-in ones, magpie's own and a table the user has
|
|
// stay as they are, one spelled inline too.
|
|
func TestCodexWritesTablesThreadsName(t *testing.T) {
|
|
for _, auth := range []string{`{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, ""} {
|
|
mine := "[model_providers.mine]\nname = \"mine\"\nbase_url = \"https://mine.example/v1\"\n"
|
|
inline := "model_providers.inl = { name = \"inl\", base_url = \"https://inl.example/v1\" }\n"
|
|
home, read := codexHome(t, auth, "model = \"gpt-5.4\"\n"+inline+"\n"+mine)
|
|
dir := filepath.Join(home, ".codex")
|
|
codexThreads(t, dir, "custom", "openai", "magpie", "mine", "ollama", "cc-switch-2", "custom", "inl")
|
|
path := filepath.Join(dir, "config.toml")
|
|
table := func(id string) map[string]string {
|
|
tb, err := edit.GetTOMLTable(path, "model_providers."+id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return tb
|
|
}
|
|
cx := codex(home)
|
|
if err := cx.Fields[0].Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v1, key := here(home).v1(), here(home).gwKey()
|
|
for _, id := range []string{"custom", "cc-switch-2"} {
|
|
if c := table(id); c["base_url"] != v1 || c["wire_api"] != "responses" || c["experimental_bearer_token"] != key {
|
|
t.Fatalf("auth %q: %s not written through magpie:\n%s", auth, id, read())
|
|
}
|
|
}
|
|
for _, id := range []string{"openai", "ollama"} {
|
|
if table(id) != nil {
|
|
t.Fatalf("auth %q: a table for built-in %s:\n%s", auth, id, read())
|
|
}
|
|
}
|
|
// one spelled inline is there too, and isn't written twice
|
|
if !strings.Contains(read(), inline) || strings.Contains(read(), "[model_providers.inl]") {
|
|
t.Fatalf("auth %q: inline provider:\n%s", auth, read())
|
|
}
|
|
if !strings.Contains(read(), mine) || strings.Count(read(), "[model_providers.custom]") != 1 {
|
|
t.Fatalf("auth %q: user's table changed or custom twice:\n%s", auth, read())
|
|
}
|
|
if d := cx.Check(); d != "" {
|
|
t.Fatalf("auth %q: check: %s", auth, d)
|
|
}
|
|
// Codex back on its own model, and reset: the threads still open
|
|
for _, v := range []string{"gpt-5.4", ""} {
|
|
if err := cx.Fields[0].Set(v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if table("custom")["base_url"] != v1 {
|
|
t.Fatalf("auth %q: %q took the table:\n%s", auth, v, read())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Not wired, magpie writes nothing for the threads: Codex isn't magpie's
|
|
// to change then.
|
|
func TestCodexThreadTablesOnlyWired(t *testing.T) {
|
|
home, read := codexHome(t, "", "model = \"gpt-5.4\"\n")
|
|
codexThreads(t, filepath.Join(home, ".codex"), "custom")
|
|
cx := codex(home)
|
|
if err := cx.Sync(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(read(), "custom") {
|
|
t.Fatalf("\n%s", read())
|
|
}
|
|
}
|