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:
1 parent
17363b7e30
commit
e950eb080a
11 files changed
+215
-4
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 原样发送",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user