codex: effort left at Default stays Default and says the level Codex takes then ("default (medium)"), and isn't written over with medium when a model is picked or the catalog synced (lgtm on Discord)

lgtm set Codex · WSL Ubuntu's 推理强度 to 默认 and it came back as 中.
Two things did that, on every platform (the WSL row is the same codexIn
code as the local one):

- The effort field's Get answered an unset model_reasoning_effort on a
  magpie model with codexcat.DefaultEffort (medium), so right after the
  pick the slider and the row read 中, not 默认.
- settle() counted an unset effort as one the model doesn't take and
  wrote DefaultEffort into config.toml: on every Sync for a magpie
  model, on Join, and on a model pick, Codex's own models included. So
  Default was pinned as medium, and on Codex's own models whose default
  is low (gpt-6.1-sol, gpt-6-astra, gpt-5.6-sol in 0.162's models.json)
  magpie overrode Codex's own default.

With model_reasoning_effort unset, Codex takes the catalog entry's
default_reasoning_level (core/src/client.rs build_reasoning). Checked
for real with codex-cli 0.162.0 in a sandbox CODEX_HOME: a
model_catalog_json entry with default_reasoning_level "low" and no
model_reasoning_effort sent {"reasoning":{"effort":"low"}} to a stand-in
Responses endpoint.

Now settle replaces only a set effort the model doesn't take, Get
returns the unset value, and the effort control's options start with
Default carrying Takes: the level Codex then takes, from the entry it
reads (codexcat.TakesEffort: Codex's own models_cache entry for its
models and a ChatGPT account's codex/ ones, else the one Entries writes).
The GUI shows it as "default (medium)" / 默认(中) / 預設(中) /
デフォルト(中) / Standard (mittel). A stopped WSL distro's options get the
same Default (codexEfforts).

Tests: TestCodexDefaultEffortStaysDefault fails on the old settle
("Default written over: model_reasoning_effort = \"high\"", "Default
shows as \"high\"") and passes now. TestCodexCustomProviderModelHasEfforts
now expects Default first with Takes "high" and an unset value read as
"". codex-default-effort.test.cjs (window at 560 and 360px, tray panel;
en, zh, ja, de; Chromium and WebKit) fails on the old effortName and
passes now.

Run: gofmt -l (clean); go vet -tags nogui on darwin, GOOS=linux and
GOOS=windows for ./internal/agent ./internal/codexcat ./internal/gui
./internal/tui and the main package; go test -tags nogui of those and
./internal/catalog under a scratch HOME (all ok); the GUI effort, gui-ja,
gui-de, gui-zh-tw and i18n-keys tests (all pass), and the whole GUI suite
in both engines: 4711 pass, 8 fail, all in library-servers-bulk
(chromium, ".lib-everyon moved from 290 to 288") and purpose-filter
(webkit), which fail the same way run alone on origin/main c1c88779.
Not run on a real WSL distro.
This commit is contained in:
yetone committed 2026-10-10 06:27:40 +08:00
1 parent c1c887798b
commit 5aca7d0924
8 files changed
+262 -30

No files matched your search

+4
View File
@@ -40,6 +40,10 @@ type Option struct {
// Context is the tokens the model takes, when known; the picker marks
// the large ones
Context int `json:"context,omitempty"`
// Takes, on an effort field's default (Value ""), is the level the
// agent takes for the model with none set (Codex: the catalog entry's
// default_reasoning_level), shown beside "default"
Takes string `json:"takes,omitempty"`
// Direct names who the agent asks for this model itself, on its own
// sign-in or key, with magpie not in the way ("Anthropic"): its config
// then names no magpie endpoint, which is right, not a failed setup
+27 -21
View File
@@ -80,6 +80,21 @@ func codex(home string) *Agent { return codexIn(here(home)) }
// codexIn is Codex as it lives at a place: this machine's home, or a WSL
// distro's (see wsl.go).
// codexEfforts are the effort control's choices for a model among ms:
// Default first, with the level Codex takes for it when none is set, as
// the catalog entry it reads says it, then the model's levels.
func codexEfforts(ms []catalog.Model, model string) []Option {
e := catalog.Efforts(ms, model)
if len(e) == 0 {
return static("low", "medium", "high", "xhigh")
}
d := Option{Value: "", Note: "Codex's default for the model", Takes: codexcat.TakesEffort(ms, model)}
if d.Takes != "" {
d.Note += ": " + d.Takes
}
return append([]Option{d}, static(e...)...)
}
func codexIn(at place) *Agent {
dir := filepath.Join(at.home, ".codex")
path := filepath.Join(dir, "config.toml")
@@ -142,7 +157,10 @@ func codexIn(at place) *Agent {
return catalog.Codex()
}
var dropSubEffort func() error
// keep the effort valid for the model; a fresh model gets its default.
// keep the effort valid for the model: one set that the model doesn't
// take becomes its default. One left unset stays unset — Codex then
// takes the model's own default, which is what Default on the effort
// control asked for (lgtm: Default turned into medium on its own).
// Routed, the Codex app offers magpie's models' efforts too (#310).
settle := func() error {
if routed() {
@@ -152,7 +170,7 @@ func codexIn(at place) *Agent {
}
ms := models()
model, effort := get("model"), get("model_reasoning_effort")
if e := catalog.Efforts(ms, model); len(e) > 0 && !contains(e, effort) {
if e := catalog.Efforts(ms, model); len(e) > 0 && effort != "" && !contains(e, effort) {
if err := edit.SetTOMLTop(path, edit.KV{Path: "model_reasoning_effort", Value: codexcat.DefaultEffort(e)}); err != nil {
return err
}
@@ -954,31 +972,19 @@ func codexIn(at place) *Agent {
},
{
Key: "effort", Label: "effort",
// unset, Codex takes the model's default, as the catalog
// magpie wrote says it — shown as such rather than as none
Get: func() string {
if e := get("model_reasoning_effort"); e != "" {
return e
}
if m := get("model"); isMagpie(m) {
if e := catalog.Efforts(models(), m); len(e) > 0 {
return codexcat.DefaultEffort(e)
}
}
return ""
},
// unset reads as Default, which it is; the level Codex then
// takes is on the Default option (Takes), not the value, so
// picking Default doesn't read back as medium (lgtm)
Get: func() string { return get("model_reasoning_effort") },
Set: func(v string) error {
if v == "" {
return edit.DelTOMLTop(path, "model_reasoning_effort")
}
return edit.SetTOMLTop(path, edit.KV{Path: "model_reasoning_effort", Value: v})
},
Options: func(cur map[string]string) []Option {
if e := catalog.Efforts(models(), cur["model"]); len(e) > 0 {
return static(e...)
}
return static("low", "medium", "high", "xhigh")
},
// Default first, with the level Codex takes for the model
// when none is set, as the catalog entry it reads says it
Options: func(cur map[string]string) []Option { return codexEfforts(models(), cur["model"]) },
},
{
// Codex lists only the first few models in the spawn_agent
+67 -4
View File
@@ -68,18 +68,23 @@ func TestCodexCustomProviderModelHasEfforts(t *testing.T) {
}
effort := cx.Fields[1]
var opts []string
for _, o := range effort.Options(map[string]string{"model": "volc/glm-5.3-flash"}) {
ef := effort.Options(map[string]string{"model": "volc/glm-5.3-flash"})
for _, o := range ef {
opts = append(opts, o.Value)
}
if strings.Join(opts, ",") != "low,high,max" {
if strings.Join(opts, ",") != ",low,high,max" {
t.Errorf("effort options: %v", opts)
}
// Default says what Codex takes then: the catalog's default
if ef[0].Takes != "high" {
t.Errorf("default takes %q", ef[0].Takes)
}
// unset, the control shows what Codex takes: the catalog's default
// unset, the control shows Default, which is what was picked
if err := effort.Set(""); err != nil {
t.Fatal(err)
}
if v := effort.Get(); v != "high" {
if v := effort.Get(); v != "" {
t.Errorf("unset effort shows %q", v)
}
@@ -95,3 +100,61 @@ func TestCodexCustomProviderModelHasEfforts(t *testing.T) {
t.Errorf("sync left the effort:\n%s", cfg)
}
}
// Default on Codex's effort control stays Default (lgtm on Discord: picked
// on Codex · WSL Ubuntu, it turned into medium): an effort left unset is
// not written in when a model is picked or the catalog synced, and the
// control reads it as Default, with the level Codex then takes beside it —
// for Codex's own model, its own default (gpt-6.1-sol's low), not medium.
func TestCodexDefaultEffortStaysDefault(t *testing.T) {
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, "")
os.WriteFile(filepath.Join(home, ".codex", "models_cache.json"), []byte(`{"models":[
{"slug":"gpt-6.1-sol","display_name":"GPT-6.1 Sol","priority":1,"default_reasoning_level":"low",
"supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"}]}]}`), 0o644)
os.MkdirAll(filepath.Dir(catalog.CachePath()), 0o755)
os.WriteFile(catalog.CachePath(), []byte(`{
"zai":{"models":{"glm-5.3-flash":{"id":"glm-5.3-flash","reasoning_options":[{"type":"effort","values":["low","high","max"]}]}}}}`), 0o644)
catalog.Reset()
t.Cleanup(catalog.Reset)
if err := provider.Save(provider.Provider{ID: "volc", Name: "Volc", Key: "k", Chat: "http://127.0.0.1:1/api/v3",
Models: []string{"glm-5.3-flash"}}); err != nil {
t.Fatal(err)
}
cx := codex(home)
model, effort := cx.Fields[0], cx.Fields[1]
unset := func(when string) {
t.Helper()
if cfg := read(); strings.Contains(cfg, "model_reasoning_effort") {
t.Errorf("%s: Default written over:\n%s", when, cfg)
}
if v := effort.Get(); v != "" {
t.Errorf("%s: Default shows as %q", when, v)
}
}
if err := model.Set("volc/glm-5.3-flash"); err != nil {
t.Fatal(err)
}
unset("magpie's model picked")
if err := cx.Sync(); err != nil {
t.Fatal(err)
}
unset("catalog synced")
if err := model.Set("gpt-6.1-sol"); err != nil {
t.Fatal(err)
}
unset("Codex's own model picked")
if d := effort.Options(map[string]string{"model": "gpt-6.1-sol"})[0]; d.Value != "" || d.Takes != "low" {
t.Errorf("Codex's own model's default: %+v", d)
}
// a level set that the model doesn't take is still settled
if err := effort.Set("max"); err != nil {
t.Fatal(err)
}
if err := model.Set("gpt-6.1-sol"); err != nil {
t.Fatal(err)
}
if v := effort.Get(); v == "max" || v == "" {
t.Errorf("max left on a model without it: %q", v)
}
}
+1 -4
View File
@@ -255,10 +255,7 @@ var wslKinds = []wslKind{
}
case "effort":
return func(cur map[string]string) []Option {
if e := catalog.Efforts(append(catalog.Codex(), magpieModels("codex")...), cur["model"]); len(e) > 0 {
return static(e...)
}
return static("low", "medium", "high", "xhigh")
return codexEfforts(append(catalog.Codex(), magpieModels("codex")...), cur["model"])
}
}
return nil
+19
View File
@@ -38,6 +38,25 @@ func DefaultEffort(e []string) string {
return e[0]
}
// TakesEffort is the effort Codex takes for the model id when none is set:
// the default_reasoning_level of the entry it reads for it. Codex's own
// model and a ChatGPT account's (codex/) keep their entry from Codex's
// models_cache.json, so it is that entry's (gpt-6.1-sol: low); one of
// magpie's other models takes the one Entries writes. "" when the model
// has no levels, or Codex's entry names none.
func TakesEffort(ms []catalog.Model, id string) string {
e := catalog.Efforts(ms, id)
if len(e) == 0 {
return ""
}
slug, chatgpt := strings.CutPrefix(id, "codex/")
if raw, ok := CacheEntries()[slug]; ok && (chatgpt || slug == id) {
d, _ := raw["default_reasoning_level"].(string)
return d
}
return DefaultEffort(e)
}
// Catalog renders models as a whole models.json.
func Catalog(ms []catalog.Model) []byte {
out := struct {
+4 -1
View File
@@ -3744,7 +3744,10 @@ function openPicker(agent, field, anchor, ev, only) {
}
function effortName(option) {
if (!option?.value) return t("default");
// the agent's default, with the level it takes then when the agent says
// (Codex: the model's default_reasoning_level), so Default doesn't read
// as a level of its own that turned into medium (lgtm)
if (!option?.value) return option?.takes ? t("default ({level})", { level: t(option.takes) }) : t("default");
return t(option.label || option.value);
}
+4
View File
@@ -872,6 +872,7 @@ const I18N = {
"agent default": "Agent 默认值",
"every magpie model as a provider in T3 Code, on Claude Code": "magpie 的全部模型作为 T3 Code 里的一个供应商(经 Claude Code 运行)",
"default": "默认",
"default ({level})": "默认({level})",
"off": "关闭",
"none": "不思考",
"minimal": "最低",
@@ -4754,6 +4755,7 @@ const I18N = {
"agent default": "Agent 預設值",
"every magpie model as a provider in T3 Code, on Claude Code": "magpie 的全部模型作為 T3 Code 裡的一個供應商(經 Claude Code 執行)",
"default": "預設",
"default ({level})": "預設({level})",
"off": "關閉",
"none": "不思考",
"minimal": "最低",
@@ -8606,6 +8608,7 @@ const I18N = {
"agent default": "エージェントのデフォルト",
"every magpie model as a provider in T3 Code, on Claude Code": "magpie のすべてのモデルを T3 Code のプロバイダとして(Claude Code 上で)",
"default": "デフォルト",
"default ({level})": "デフォルト({level})",
"off": "オフ",
"none": "なし",
"minimal": "最小",
@@ -12477,6 +12480,7 @@ const I18N = {
"agent default": "Standard des Agenten",
"every magpie model as a provider in T3 Code, on Claude Code": "alle magpie-Modelle als Anbieter in T3 Code, über Claude Code",
"default": "Standard",
"default ({level})": "Standard ({level})",
"off": "aus",
"none": "keiner",
"minimal": "minimal",
@@ -0,0 +1,136 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// Codex's effort left at Default reads as Default, with the level Codex
// then takes for the model beside it (lgtm on Discord: Default turned into
// 中 on Codex · WSL Ubuntu). Its row and slider say "default (medium)", a
// level picked shows the level, and Default picked again posts "" and
// reads as Default again, not as medium.
// English, Chinese, Japanese and German, Chromium and WebKit, the window
// and the tray panel.
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
const { test } = require("node:test");
const { chromium, webkit } = require("playwright");
const assets = path.resolve(__dirname, "../assets");
const levels = ["low", "medium", "high", "xhigh"];
const codex = (effort) => ({
id: "codex", name: "Codex", icon: "codex-color", path: "/h/.codex/config.toml", wired: true,
fields: [
{ key: "model", label: "model", value: "volc/glm", options: [{ value: "volc/glm", label: "GLM", ref: "volc/glm" }] },
{ key: "effort", label: "effort", value: effort, options: [{ value: "", note: "Codex's default for the model: medium", takes: "medium" }, ...levels.map((value) => ({ value }))] },
],
});
const filler = Array.from({ length: 6 }, (_, i) => ({ id: "pi" + i, name: "Pi " + i, path: "/p", fields: [{ key: "model", label: "model", value: "", options: [] }] }));
function serve(lang, sets) {
let effort = "";
const st = () => ({ agents: [codex(effort), ...filler], profiles: [], settings: { lang, theme: "light" } });
return async (route) => {
const req = route.request(), url = new URL(req.url());
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` });
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (url.pathname === "/api/state") return route.fulfill({ json: st() });
if (url.pathname === "/api/set") {
const b = req.postDataJSON();
sets.push(b);
if (b.field === "effort") effort = b.value;
return route.fulfill({ json: st() });
}
if (url.pathname === "/api/plugins") return route.fulfill({ json: { plugins: [] } });
if (url.pathname === "/api/usage/quotas") return route.fulfill({ json: [] });
if (url.pathname === "/api/groups") return route.fulfill({ json: { groups: [] } });
if (url.pathname === "/api/providers") return route.fulfill({ json: { providers: [], gateway: { running: true } } });
if (url.pathname.startsWith("/api/")) return route.fulfill({ json: {} });
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
await route.fulfill({ body: await fs.readFile(file), contentType });
};
}
const words = {
en: { def: "default (medium)", high: "high" },
zh: { def: "默认(中)", high: "高" },
ja: { def: "デフォルト(中)", high: "高" },
de: { def: "Standard (mittel)", high: "hoch" },
};
const row = `.row.agent[data-id="codex"]`;
const noSideScroll = (page) => page.evaluate(() => document.scrollingElement.scrollWidth <= innerWidth);
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of ["en", "zh", "ja", "de"]) {
const w = words[lang];
test(`${engine} ${lang}: Codex's Default effort reads as Default`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
const errors = [];
t.after(async () => {
if (errors.length) console.log(errors);
await browser.close();
});
const open = async (mode, sets, width) => {
const page = await (await browser.newContext({ viewport: { width, height: 640 }, reducedMotion: "reduce" })).newPage();
page.setDefaultTimeout(5000);
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang, sets));
await page.goto("http://magpie.test/" + (mode ? "?mode=" + mode : ""));
await page.locator(row).waitFor();
return page;
};
const slide = (page, i) => page.locator("#effortRange").evaluate((r, i) => {
r.value = String(i);
r.dispatchEvent(new Event("input", { bubbles: true }));
r.dispatchEvent(new Event("change", { bubbles: true }));
}, i);
for (const width of [560, 360]) {
await t.test(`the window at ${width}px`, async () => {
const sets = [];
const page = await open("", sets, width);
let field = page.locator(`${row} .field[data-key="effort"]`).first();
if (!(await field.isVisible())) {
await page.locator(`${row} .ag-link`).click();
field = page.locator(`${row} .ag-exp .field[data-key="effort"]`);
}
await field.waitFor();
assert.equal(await field.locator(".v").textContent(), w.def);
const box = await field.boundingBox();
assert(box.x >= 0 && box.x + box.width <= width, `the effort runs off the window: ${JSON.stringify(box)}`);
await field.click();
await page.locator("#effortControl:not([hidden])").waitFor();
assert.equal(await page.locator("#effortValue").textContent(), w.def);
assert.equal(await page.locator("#effortMin").textContent(), w.def);
// Default, then the model's four levels: no stop of its own added
assert.equal(await page.locator("#effortTicks i").count(), 5);
await slide(page, 3);
assert.equal(await page.locator("#effortValue").textContent(), w.high);
await page.waitForFunction(() => document.querySelector('.row.agent[data-id="codex"] .field[data-key="effort"] .v')?.textContent !== document.querySelector("#effortMin")?.textContent);
await slide(page, 0);
assert.equal(await page.locator("#effortValue").textContent(), w.def);
for (let i = 0; i < 100 && sets.length < 2; i++) await page.waitForTimeout(50);
await page.waitForFunction((d) => [...document.querySelectorAll('.row.agent[data-id="codex"] .field[data-key="effort"] .v')].some((v) => v.textContent === d), w.def);
assert.deepEqual(sets, [{ agent: "codex", field: "effort", value: "high" }, { agent: "codex", field: "effort", value: "" }]);
assert(await noSideScroll(page), "the window scrolls sideways");
});
}
await t.test("the panel", async () => {
const sets = [];
const page = await open("panel", sets, 440);
assert.equal(await page.locator(`${row} .ag-sum .eff`).getAttribute("data-l"), "0");
await page.locator(`${row} .ag-sum`).click();
await page.waitForTimeout(700);
const box = page.locator(`${row} .ag-open .effort-control`);
assert.equal(await box.locator(".effort-ticks i").count(), 5);
assert.equal(await box.locator(".effort-head b").textContent(), w.def);
assert.equal(await box.locator(".effort-ends span").first().textContent(), w.def);
await box.locator(".eslide").press("ArrowRight");
await page.waitForTimeout(300);
assert.deepEqual(sets, [{ agent: "codex", field: "effort", value: "low" }]);
assert(await noSideScroll(page), "the panel scrolls sideways");
});
assert.deepEqual(errors, []);
});
}
}