codex, claude: a model with a window above 272K is compacted at 272K, as OpenAI's and Anthropic's own are (X: Chen — through magpie on DeepSeek flash, Codex and Claude Code both "wait" before every answer, and more the longer the task) — magpie told both agents a 1M model's whole window, so neither compacted until near 1M and every turn sent the whole conversation again: the live ledger had turns of ~550K tokens waiting 70–90s for a first token, while magpie's own time before the vendor stays under 0.2s. OpenAI runs its own 1M-capable models at 272K in Codex (context_window 272000, max_context_window 872000) and Anthropic its at 200K unless a [1m] one is picked; magpie now does the same by default. Codex's catalog gives such a model context_window 272000 and its whole window as max_context_window; Claude Code gets CLAUDE_CODE_AUTO_COMPACT_WINDOW=272000 while it runs on magpie (it compacts at the smaller of that and the model's window, a [1m] model's too), kept apart from a value the user set and taken out with magpie. Settings → Long conversations ("Compact at 272K" / "Full window", en and zh) turns it off (settings.FullContext), and both agents' lists are written again at once. In a sandbox with the owner's real accounts: Codex's /models listed group/test and group/auto-glm-5-3-flash at 272000 with max 1000000 (stepfun's 32K and 16K as they were, gpt-6-* still 272000/872000), Claude Code on deepseek/deepseek-flash[1m] answered and its /context said 16.3k / 272k, and Full window turned both back to 1M (/context 16.3k / 1m). TestClaudeCompactWindow and the extended TestCodexCatalogToolSearch fail on the old code. go vet (darwin, GOOS=windows), builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.

This commit is contained in:
yetone committed 2026-10-04 18:27:09 +08:00
1 parent 17363b7e30
commit e950eb080a
11 files changed
+215 -4

No files matched your search

+51
View File
@@ -16,6 +16,7 @@ import (
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/settings"
)
// Claude Code reads its endpoint from the `env` block of settings.json.
@@ -136,6 +137,13 @@ var claudeAliases = []string{"default", "best", "opus", "sonnet", "haiku", "fabl
// is 1M whatever it says.
const claudeContextEnv = "CLAUDE_CODE_MAX_CONTEXT_TOKENS"
// claudeCompactEnv is where Claude Code compacts a conversation, the smaller
// of it and the model's window, a [1m] one's too. magpie sets it to
// settings.WorkingWindow, so a 1M model isn't run to 1M with every turn
// sending all of it (X: Chen, turns of ~550K tokens waiting 70–90s for a
// first token); settings.FullContext leaves it out.
const claudeCompactEnv = "CLAUDE_CODE_AUTO_COMPACT_WINDOW"
// claudeCapsEnv tells Claude Code what a model it doesn't know can do, as
// "<model>=effort,xhigh_effort;<model>=…" (a trailing * a prefix, [1m]
// taken off the model first, the model lowercased, the pattern not). Run on
@@ -327,6 +335,37 @@ func claudeIn(at place) *Agent {
}
return nil
}
// the auto-compact window magpie last wrote, kept apart from the
// user's own in the same way
compactKey := at.key("claude.compact_window")
compactOurs := func() bool {
cur := env(claudeCompactEnv)
return cur != "" && cur == stashLoad()[compactKey]
}
dropCompact := func() error {
defer forget(compactKey)
if compactOurs() {
return edit.DelJSON(path, "env."+claudeCompactEnv)
}
return nil
}
// writeCompact has Claude Code compact at the working window
// (settings.WorkingWindow) on magpie, unless settings.FullContext
writeCompact := func() error {
if env(claudeCompactEnv) != "" && !compactOurs() {
forget(compactKey)
return nil
}
if settings.Load().FullContext {
return dropCompact()
}
w := strconv.Itoa(settings.WorkingWindow)
stash(map[string]string{compactKey: w})
if env(claudeCompactEnv) == w {
return nil
}
return edit.SetJSON(path, edit.KV{Path: "env." + claudeCompactEnv, Value: w})
}
// Claude Code's /model lists what modelPicker says (2.1.287): while it
// runs on magpie, that is every one of magpie's models, so the user
// picks among them in Claude Code itself. One the user wrote is theirs.
@@ -421,6 +460,9 @@ func claudeIn(at place) *Agent {
if err := dropWindow(); err != nil {
return "", err
}
if err := dropCompact(); err != nil {
return "", err
}
if err := dropCaps(); err != nil {
return "", err
}
@@ -464,6 +506,9 @@ func claudeIn(at place) *Agent {
if err := dropWindow(); err != nil {
return err
}
if err := dropCompact(); err != nil {
return err
}
if err := dropCaps(); err != nil {
return err
}
@@ -572,6 +617,9 @@ func claudeIn(at place) *Agent {
m, _ := tierAt(tiers[t])
models = append(models, m)
}
if err := writeCompact(); err != nil {
return err
}
if err := writePicker(); err != nil {
return err
}
@@ -879,6 +927,9 @@ func claudeIn(at place) *Agent {
for _, t := range claudeTiers {
models = append(models, env(tierEnv(t)))
}
if err := writeCompact(); err != nil {
return err
}
if err := writePicker(); err != nil {
return err
}
+71
View File
@@ -0,0 +1,71 @@
package agent
import (
"path/filepath"
"testing"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/settings"
)
// TestClaudeCompactWindow: on magpie Claude Code compacts at the working
// window (CLAUDE_CODE_AUTO_COMPACT_WINDOW), a [1m] model too, so a 1M model's
// conversation isn't run to 1M with every turn sending all of it (X: Chen);
// settings.FullContext takes it away, a value the user set is theirs, and
// Claude Code as installed has none.
func TestClaudeCompactWindow(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
if err := provider.Save(provider.Provider{ID: "v", Name: "V", Chat: "https://example.test/v1", Key: "k",
Models: []string{"big"}}); err != nil {
t.Fatal(err)
}
if err := catalog.SaveLive("v", "https://example.test/v1", []catalog.Model{{ID: "big", Context: 1000000}}); err != nil {
t.Fatal(err)
}
path := filepath.Join(home, ".claude", "settings.json")
writeFile(t, path, `{}`)
a := claude(home)
compact := func() string { v, _ := edit.GetJSON(path, "env."+claudeCompactEnv); return v }
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
must(a.Field("model").Set("v/big[1m]"))
if compact() != "272000" {
t.Fatalf("on magpie: %q", compact())
}
must(settings.Save(settings.Settings{FullContext: true}))
must(a.Sync())
if compact() != "" {
t.Fatalf("full context: %q", compact())
}
must(settings.Save(settings.Settings{}))
must(a.Sync())
if compact() != "272000" {
t.Fatalf("back to the working window: %q", compact())
}
must(a.Field("model").Set(""))
if compact() != "" {
t.Fatalf("Claude Code as installed: %q", compact())
}
// the user's own is left as it is
must(edit.SetJSON(path, edit.KV{Path: "env." + claudeCompactEnv, Value: "500000"}))
must(a.Field("model").Set("v/big[1m]"))
must(a.Sync())
if compact() != "500000" {
t.Fatalf("the user's own: %q", compact())
}
must(a.Field("model").Set(""))
if compact() != "500000" {
t.Fatalf("reset took the user's own: %q", compact())
}
}
+11 -2
View File
@@ -83,7 +83,11 @@ func Entries(ms []catalog.Model, after int) []any {
Tools []string `json:"experimental_supported_tools"`
Modalities []string `json:"input_modalities"`
Context *int `json:"context_window,omitempty"`
Tiers []tier `json:"service_tiers"`
// the model's whole window when Context is the working one
// (settings.Working): Codex's model_context_window may raise it
// that far, as it does OpenAI's own models'
MaxContext *int `json:"max_context_window,omitempty"`
Tiers []tier `json:"service_tiers"`
// Without the search, Codex puts every MCP tool's schema (a
// ChatGPT sign-in's apps' among them) in every request, 190K
// tokens before the first word (#258); with it, they are named in
@@ -108,6 +112,7 @@ func Entries(ms []catalog.Model, after int) []any {
}
own := CacheEntries()
v1 := V1()
work := settings.Load()
var entries []any
for i, m := range ms {
if raw, ok := own[strings.TrimPrefix(m.ID, "codex/")]; ok && strings.HasPrefix(m.ID, "codex/") {
@@ -146,7 +151,11 @@ func Entries(ms []catalog.Model, after int) []any {
e.Modalities = append(e.Modalities, "image")
}
if c := m.Context; c > 0 {
e.Context = &c
w := work.Working(c)
e.Context = &w
if w < c {
e.MaxContext = &c
}
}
e.Truncation.Mode, e.Truncation.Limit = "tokens", 10000
for _, ef := range m.Efforts {
+16 -2
View File
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/settings"
)
// Codex's own models, routed through magpie, keep what Codex knows of them
@@ -69,7 +70,8 @@ func TestCodexCatalogImages(t *testing.T) {
// A model magpie describes has Codex search its MCP tools rather than send
// every one in each request (#258), with its prompt, context window, and
// neither code mode nor Responses Lite.
// neither code mode nor Responses Lite. A window above the working window
// is told as that, the whole one as its max, unless settings.FullContext.
func TestCodexCatalogToolSearch(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("USERPROFILE", os.Getenv("HOME"))
@@ -83,9 +85,21 @@ func TestCodexCatalogToolSearch(t *testing.T) {
t.Fatalf("%v", got)
}
e := got.Models[0]
if e["supports_search_tool"] != true || e["base_instructions"] != Prompt || e["context_window"] != float64(996147) {
if e["supports_search_tool"] != true || e["base_instructions"] != Prompt || e["context_window"] != float64(settings.WorkingWindow) || e["max_context_window"] != float64(996147) {
t.Errorf("entry: %v", e)
}
if err := settings.Save(settings.Settings{FullContext: true}); err != nil {
t.Fatal(err)
}
var full struct {
Models []map[string]any `json:"models"`
}
json.Unmarshal(Catalog([]catalog.Model{
{ID: "group/auto-gemini-3-8-flash", Name: "auto", Context: 996147},
}), &full)
if e := full.Models[0]; e["context_window"] != float64(996147) || e["max_context_window"] != nil {
t.Errorf("full context: %v", e)
}
for _, off := range []string{"tool_mode", "use_responses_lite", "multi_agent_version", "model_messages"} {
if v, ok := e[off]; ok {
t.Errorf("%s = %v", off, v)
+16
View File
@@ -832,6 +832,7 @@ func Handler(w Windows, gw *gateway.Server) http.Handler {
// how agents' lists name models, set on its own for the agents to be told
in.PlainNames, in.PlainOwnNames = cur.PlainNames, cur.PlainOwnNames
in.CodexAgentsV1 = cur.CodexAgentsV1
in.FullContext = cur.FullContext // set on its own (full-context below)
in.CodexTitles = cur.CodexTitles // set on its own (codex-titles below)
in.ChinaMirror = cur.ChinaMirror // the Plugins page's, set on its own
// which Codex accounts spend a reset by themselves, set on the Usage card
@@ -971,6 +972,21 @@ func Handler(w Windows, gw *gateway.Server) http.Handler {
}
writeJSON(rw, settingsState())
})
// whether Codex and Claude Code are told a model's whole context window
// or the working one (settings.FullContext): their lists are written
// again
mux.HandleFunc("POST /api/settings/full-context", func(rw http.ResponseWriter, r *http.Request) {
var in struct{ On bool }
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
fail(rw, err)
return
}
if err := provider.SetFullContext(in.On); err != nil {
fail(rw, err)
return
}
writeJSON(rw, settingsState())
})
// where Codex's requests for a thread's title go (#705): "" as Codex
// sends them, "off", or a model's id
mux.HandleFunc("POST /api/settings/codex-titles", func(rw http.ResponseWriter, r *http.Request) {
+4
View File
@@ -14927,6 +14927,10 @@ function renderTrayUsage(s, keep) {
// magpie's other models (#141)
$("#codexAgentsV1Segs").replaceChildren(segs([["off", t("Off")], ["on", t("On")]], s.codexAgentsV1 ? "on" : "off", (v) =>
writingPrefs(api("settings/codex-agents-v1", { on: v === "on" })).then((ns) => { prefs = ns; renderSettings(); }).catch((e) => { status(t(e.message), "err"); renderSettings(); })));
// a 1M model's whole window, or compacting at the working one (X: Chen,
// 70–90s to a first token at 550K)
$("#fullContextSegs").replaceChildren(segs([["off", t("Compact at 272K")], ["on", t("Full window")]], s.fullContext ? "on" : "off", (v) =>
writingPrefs(api("settings/full-context", { on: v === "on" })).then((ns) => { prefs = ns; renderSettings(); }).catch((e) => { status(t(e.message), "err"); renderSettings(); })));
renderCodexTitles(s);
const rate = s.fx?.rate;
const currencySub = $("#currencySub");
+4
View File
@@ -398,6 +398,10 @@ const I18N = {
"Agents’ lists put each model’s provider after its name, or not after names you set": "写给 agent 的模型列表在模型名后带上供应商,或自定义的名称不带",
"Not on names I set": "自定义名称不带供应商",
"Codex subagents on any model": "Codex 子代理可用任意模型",
"Long conversations": "长对话",
"Codex and Claude Code compact a conversation at 272K tokens on a model with a larger window, as they do on OpenAI’s and Anthropic’s own; the model’s full window lets it grow further, with every turn slower and costlier": "模型窗口超过 272K 时,Codex 和 Claude Code 在 272K tokens 处压缩对话,和它们在 OpenAI、Anthropic 自家模型上一样;用模型的完整窗口则对话可以继续变长,但每一轮都更慢、更贵",
"Compact at 272K": "272K 时压缩",
"Full window": "完整窗口",
"Codex thread titles": "Codex 会话标题",
"Codex’s own (ChatGPT)": "Codex 自带(ChatGPT)",
"as Codex sends them": "按 Codex 原样发送",
+4
View File
@@ -256,6 +256,10 @@
<div class="who"><div class="name" data-t>Codex subagents on any model</div><div class="sub" data-t>Codex’s OpenAI models hand subagents their tasks as text (multi-agent V1), so a GPT lead can spawn them on magpie’s other models. New threads only; Codex’s own multi_agent_v2 setting still wins</div></div>
<div id="codexAgentsV1Segs"></div>
</div>
<div class="row pref">
<div class="who"><div class="name" data-t>Long conversations</div><div class="sub" data-t>Codex and Claude Code compact a conversation at 272K tokens on a model with a larger window, as they do on OpenAI’s and Anthropic’s own; the model’s full window lets it grow further, with every turn slower and costlier</div></div>
<div id="fullContextSegs"></div>
</div>
<div class="row pref" id="codexTitlesRow">
<div class="who"><div class="name" data-t>Codex thread titles</div><div class="sub" id="codexTitlesSub"></div></div>
<div class="val" id="codexTitlesPick"></div>
+1
View File
@@ -139,6 +139,7 @@ func TestSettingsSaveKeepsWhatItDoesNotSend(t *testing.T) {
UpdateSkip: "0.1.500",
UpdateMirror: "https://mirror.example/",
Window: []int{900, 700},
FullContext: true,
}
if err := settings.Save(was); err != nil {
t.Fatal(err)
+15
View File
@@ -692,6 +692,21 @@ func SetCodexAgentsV1(on bool) error {
return nil
}
// SetFullContext turns settings.FullContext on or off: Codex and Claude
// Code are told a model's whole window, or the working one, again.
func SetFullContext(on bool) error {
s := settings.Load()
if s.FullContext == on {
return nil
}
s.FullContext = on
if err := settings.Save(s); err != nil {
return err
}
catalog.Touched()
return nil
}
// SetPlainNames has the agents' model lists name models by their names
// alone (see Labels), or with their providers' again, and the agents told.
func SetPlainNames(on bool) error {
+22
View File
@@ -215,6 +215,14 @@ type Settings struct {
// as Codex sends them, "off" answered by magpie with no title and sent
// nowhere, or a model's id (provider/model, group/<id>) that writes it.
CodexTitles string `json:"codexTitles,omitempty"`
// FullContext has Codex and Claude Code told a model's whole context
// window. Off, a window above WorkingWindow is told as WorkingWindow,
// so they compact a long conversation there instead of sending ever
// more of it on every turn (X: Chen, a 1M DeepSeek model through
// Codex and Claude Code took 70–90s to its first token at 550K):
// what OpenAI does with its own models, 272K though they can take
// more, and Anthropic with its, 200K unless a [1m] one is picked.
FullContext bool `json:"fullContext,omitempty"`
// ChinaMirror is the Plugins page's 「国内镜像」 switch: the plugin list,
// npm (the plugins' packages and what npm says of them) and Bun's
// downloads are asked of mirrors in China first, and of their official
@@ -498,6 +506,20 @@ func CarryPerModel(in, cur *Settings) {
}
}
// WorkingWindow is the context window Codex and Claude Code are told for a
// model with a larger one, unless FullContext: Codex's own for OpenAI's
// models that can take more.
const WorkingWindow = 272000
// Working is the context window an agent is told for a model with one of
// n tokens (see FullContext).
func (s Settings) Working(n int) int {
if !s.FullContext && n > WorkingWindow {
return WorkingWindow
}
return n
}
// KeepOwn puts back cur's settings that are this computer's own, which a
// sync or a restored backup never brings from another: the window's size,
// the proxy, the Dock, and what the menu bar or tray shows beside magpie's