Files
yetone 1aaac93b52 provider, gui: Compact at — where Codex and Claude Code compact a long conversation, set per provider or per model in its editor and to any size in Settings → Long conversations (#876, hisiling)
Until now the only choice was 272K for every model or Full window for every model. The 272K default stays as it is (e950eb08: OpenAI runs its own 1M models at 272K in Codex, and Chen saw 70–90s to a first token at 550K).

What changes:

- A provider's editor has Compact at beside Context window and Max output, in the same form: "500k" for all of its models, "deepseek-v4-flash=272k" for one, and empty to follow Settings. A size at or above a model's window means the whole window. The values are kept in settings.ModelCompacts.
- Settings → Long conversations takes a size of its own beside Compact at / Full window (settings.CompactAt).
- `magpie model compact` takes a size such as 500k.

Order: the model's own value, then its provider's "*", then Settings. Full window turns off only the Settings value. A group uses the smallest value among its members.

Where it applies:

- Codex's model_catalog is told the threshold as context_window, with the whole window as max_context_window.
- Claude Code's CLAUDE_CODE_AUTO_COMPACT_WINDOW follows the main model. A Claude model now takes its provider's value when one is set.
- The Codex list tag changes when the Settings size or Full window changes.

Tests:

- New: TestModelCompactsSetFromTheEditor, TestSetCompactAt, TestCodexCatalogCompactAt and TestClaudeCompactAt.
- TestSettingsSaveKeepsWhatItDoesNotSend now covers the two new fields.
- compact-at.test.cjs (WebKit, en/zh) checks the editor field (shown, refused, chips, saved) and the Settings size (posted, hidden under Full window, no scroll).
- go vet ./..., go test -tags nogui of the root package, provider, codexcat, agent, gui, gateway, settings and catalog under a temp HOME, and GOOS=linux/windows builds all pass.
2026-10-05 15:20:05 +08:00

113 lines
4.2 KiB
Go

package provider
import (
"maps"
"testing"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/settings"
)
// A provider's editor sets where Codex and Claude Code compact on its
// models (#876, hisiling): "*" for all of them and model=size for one,
// the model's own first; those it leaves out come off, another
// provider's stay, a model it doesn't serve is refused, and a save that
// changes nothing tells the agents nothing. A group takes the smallest
// its models have.
func TestModelCompactsSetFromTheEditor(t *testing.T) {
limitsHome(t)
for _, id := range []string{"deepseek", "other"} {
if err := Save(Provider{ID: id, Name: id, Chat: "https://relay.example/v1", Key: "k", Models: []string{"deepseek-v4-flash", "deepseek-v4-pro"}}); err != nil {
t.Fatal(err)
}
}
if err := SetModelCompacts("other", map[string]int{"deepseek-v4-pro": 300000}); err != nil {
t.Fatal(err)
}
told := 0
catalog.Changed = func() { told++ }
t.Cleanup(func() { catalog.Changed = nil })
if err := SetModelCompacts("deepseek", map[string]int{"*": 500000, "deepseek-v4-flash": 272000}); err != nil {
t.Fatal(err)
}
if told != 1 {
t.Errorf("told the agents %d times; want once", told)
}
want := map[string]int{"deepseek/*": 500000, "deepseek/deepseek-v4-flash": 272000, "other/deepseek-v4-pro": 300000}
if got := settings.Load().ModelCompacts; !maps.Equal(got, want) {
t.Errorf("compacts %v; want %v", got, want)
}
if got := CompactsOf("deepseek"); !maps.Equal(got, map[string]int{"*": 500000, "deepseek-v4-flash": 272000}) {
t.Errorf("CompactsOf: %v", got)
}
for id, n := range map[string]int{
"deepseek/deepseek-v4-flash": 272000,
"magpie/deepseek/deepseek-v4-pro": 500000,
"deepseek/deepseek-v4-pro[1m]": 500000,
"other/deepseek-v4-flash": 0,
"other/deepseek-v4-pro": 300000,
"nobody/deepseek-v4-pro": 0,
} {
if got := CompactSet(id); got != n {
t.Errorf("CompactSet(%s) = %d; want %d", id, got, n)
}
}
group := func(string) (Group, []Member, bool) {
dp, _ := Find("deepseek")
ot, _ := Find("other")
return Group{}, []Member{
{Path: []string{"deepseek/deepseek-v4-pro"}, Provider: *dp, Model: "deepseek-v4-pro"},
{Path: []string{"other/deepseek-v4-pro"}, Provider: *ot, Model: "wire-name"},
{Path: []string{"other/deepseek-v4-flash"}, Provider: *ot, Model: "deepseek-v4-flash"},
}, true
}
if got := compactSet(settings.Load(), GroupPrefix+"mix", group); got != 300000 {
t.Errorf("a group: %d; want its smallest, 300000", got)
}
told = 0
if err := SetModelCompacts("deepseek", map[string]int{"*": 500000, "deepseek-v4-flash": 272000}); err != nil || told != 0 {
t.Errorf("the same again: %v, told %d", err, told)
}
if err := SetModelCompacts("deepseek", map[string]int{"typo": 5}); err == nil {
t.Error("a model deepseek doesn't serve was accepted")
}
if got := settings.Load().ModelCompacts; !maps.Equal(got, want) {
t.Errorf("a refused save changed them: %v", got)
}
if err := SetModelCompacts("deepseek", map[string]int{}); err != nil {
t.Fatal(err)
}
if got := settings.Load().ModelCompacts; !maps.Equal(got, map[string]int{"other/deepseek-v4-pro": 300000}) {
t.Errorf("emptied: %v", got)
}
}
// The threshold for every model can be any size, not 272K alone (#876):
// typing one turns Full window off, 272K or nothing is the default again.
func TestSetCompactAt(t *testing.T) {
limitsHome(t)
if err := settings.Save(settings.Settings{FullContext: true}); err != nil {
t.Fatal(err)
}
if err := SetCompactAt(500000); err != nil {
t.Fatal(err)
}
if s := settings.Load(); s.FullContext || s.CompactAt != 500000 || s.Compact() != 500000 || s.Working(1000000) != 500000 || s.Working(200000) != 200000 {
t.Errorf("at 500K: %+v", s)
}
if err := SetCompactAt(settings.WorkingWindow); err != nil {
t.Fatal(err)
}
if s := settings.Load(); s.CompactAt != 0 || s.Compact() != settings.WorkingWindow {
t.Errorf("at 272K: %+v", s)
}
if err := SetCompactAt(-1); err == nil {
t.Error("a negative size was accepted")
}
if s := (settings.Settings{CompactAt: 500000, FullContext: true}); s.Compact() != 0 || s.Working(1000000) != 1000000 {
t.Errorf("full window: %d", s.Compact())
}
}