gateway, gui: Claude Desktop can list a model of 1M or more once, as its 1M entry (#1272)
Claude Desktop reads magpie's gateway models and, for every row whose
max_input_tokens is 1,000,000 or more and whose id has no [1m] suffix,
adds a second "1M context window" entry; an id that already ends in [1m]
gets one entry (BIt/PIt/JLt in Desktop's index chunk). So every 1M model
showed twice. A new switch under Agents → Claude Desktop, "Only each
model's largest window" (settings desktopLongest, off by default), lists
such a model by its [1m] id. Models under 1M keep their real window;
fast/non-fast, providers and groups stay separate rows. The base id still
resolves (aliased, Resolve and SwitchedOff trim [1m]), so sessions saved
on it keep working. Desktop needs a restart to re-read the list.
Verified:
- TestClaudeDesktopLongestOnly fails without the change ("on: 6 entries
of 4 rows") and passes with it; the [1m] and old plain ids reach
upstream as the same model.
- Desktop's own discovery and menu code, extracted from a copy of
app.asar, run over magpie's output: off 4 rows → 6 entries, on 4 rows
→ 4 entries.
- desktop-longest.test.cjs (Chromium and WebKit, en/zh, 1100/640px)
times out on origin/main's app.js and passes 8/8; gui-ja, gui-de,
gui-zh-tw, i18n-keys, desktop-model-menu, desktop-tiers,
agent-models-redraw and click-scroll pass 44/44.
- go test (sandbox HOME, nogui) gateway, gui, settings, agent pass;
go vet for darwin, linux and windows.
- Full GUI suite: 3802/3809. agent-expand-steady, panel-arrange,
redact-rule-row, remote-magpie and signin-again pass alone;
context-window fails alone with origin/main's assets too.
Not verified: the real Desktop menu after a restart (its live config was
not touched).
This commit is contained in:
1 parent
6030fe88ca
commit
a2a7fb1123
9 files changed
+304
-1
No files matched your search
@@ -152,7 +152,8 @@ func DesktopID(e provider.Entry) string { return claudeLooking(e) }
|
||||
// V4.1 Flash", not with its provider's beside it). Desktop shows the name,
|
||||
// not the id, and its description after it, so none is given; it folds rows
|
||||
// of one name into one entry, so two models of one name keep their
|
||||
// provider's after it (desktopNames).
|
||||
// provider's after it (desktopNames). A model of 1M or more is listed by
|
||||
// its "[1m]" id under settings.DesktopLongest (desktop1M).
|
||||
func desktopModels(entries []provider.Entry) []map[string]any {
|
||||
names := desktopNames(entries)
|
||||
// the tier each model stands in for: the user's pick (a model picked
|
||||
@@ -165,11 +166,15 @@ func desktopModels(entries []provider.Entry) []map[string]any {
|
||||
tierOf[id] = t
|
||||
}
|
||||
}
|
||||
longest := settings.Load().DesktopLongest
|
||||
data := make([]map[string]any, 0, len(entries))
|
||||
for i, e := range entries {
|
||||
m := modelObject(e)
|
||||
m["display_name"] = names[i]
|
||||
m["id"] = claudeLooking(e)
|
||||
if longest && e.Context >= desktop1M {
|
||||
m["id"] = m["id"].(string) + "[1m]"
|
||||
}
|
||||
if t := tierOf[e.ID]; t != "" {
|
||||
m["anthropic_family_tier"], m["is_family_default"] = t, true
|
||||
} else if t := claudeTier(e.ID); t != "" && picked[t] == "" {
|
||||
@@ -180,6 +185,15 @@ func desktopModels(entries []provider.Entry) []map[string]any {
|
||||
return data
|
||||
}
|
||||
|
||||
// desktop1M is the max_input_tokens from which Claude Desktop adds a 1M
|
||||
// entry beside a gateway model whose id has no "[1m]" (the discovery in its
|
||||
// app.asar, 2.7032: supports_1m when given, else max_input_tokens >= 1e6;
|
||||
// one whose id has "[1m]" gets none). Under settings.DesktopLongest such a
|
||||
// model is listed by that 1M id alone (#1272), which magpie serves as the
|
||||
// plain one (aliased, provider.Resolve take "[1m]" off), so a session
|
||||
// saved on the plain id still runs.
|
||||
const desktop1M = 1_000_000
|
||||
|
||||
// Claude Desktop's Code tab runs Claude Code with ANTHROPIC_DEFAULT_<TIER>_MODEL
|
||||
// set to "" for every tier, unless the gateway's /v1/models tags a model
|
||||
// with anthropic_family_tier (shortnameIdentityOverrides in its app.asar,
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
)
|
||||
|
||||
// Claude Desktop shows a gateway model of 1M tokens or more twice, its
|
||||
// plain entry and a "1M context window" one it adds itself (#1272: 21
|
||||
// models made 34 entries). With DesktopLongest on, such a model is listed
|
||||
// by its "[1m]" id alone, so Desktop's menu has one entry per model; one
|
||||
// under 1M (872K) keeps its own id and window, and a request on the plain id
|
||||
// a session was saved with is still served.
|
||||
func TestClaudeDesktopLongestOnly(t *testing.T) {
|
||||
f := &fake{reply: sse(
|
||||
`event: message_start`+"\n"+`data: {"type":"message_start","message":{"id":"m","type":"message","role":"assistant","model":"x","content":[],"usage":{"input_tokens":1,"output_tokens":0}}}`,
|
||||
`event: message_stop`+"\n"+`data: {"type":"message_stop"}`)}
|
||||
up := setup(t, provider.Anthropic, f)
|
||||
p := provider.Provider{ID: "vend", Name: "Vend", Key: "k", Anthropic: up.URL, Models: []string{"deepseek-v4-pro", "exo-free", "claude-opus-4-8"}}
|
||||
if err := provider.Save(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for model, n := range map[string]int{"deepseek-v4-pro": 1_048_576, "exo-free": 872_000, "claude-opus-4-8": 1_000_000} {
|
||||
now, err := provider.Find(p.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := provider.SetContext(*now, model, n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
type row struct {
|
||||
ID string `json:"id"`
|
||||
MaxInput int `json:"max_input_tokens"`
|
||||
}
|
||||
list := func() map[string]row {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest("GET", "/v1/models?limit=1000", nil)
|
||||
req.Header.Set("x-api-key", TokenFor("claude-desktop"))
|
||||
rec := httptest.NewRecorder()
|
||||
New().Handler().ServeHTTP(rec, req)
|
||||
var out struct{ Data []row }
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := map[string]row{}
|
||||
for _, d := range out.Data {
|
||||
got[DesktopCatalogID(strings.TrimSuffix(d.ID, "[1m]"))] = d
|
||||
}
|
||||
return got
|
||||
}
|
||||
// the entries Desktop's menu makes of the list: one per row, and one
|
||||
// more for a row of 1M or more whose id has no "[1m]"
|
||||
entries := func(rows map[string]row) int {
|
||||
n := 0
|
||||
for _, r := range rows {
|
||||
n++
|
||||
if r.MaxInput >= 1_000_000 && !strings.HasSuffix(r.ID, "[1m]") {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
off := list()
|
||||
if entries(off) != len(off)+2 {
|
||||
t.Fatalf("off: %d entries of %d rows, want two 1M extras: %v", entries(off), len(off), off)
|
||||
}
|
||||
for id, r := range off {
|
||||
if strings.HasSuffix(r.ID, "[1m]") {
|
||||
t.Fatalf("off: %s listed as %s", id, r.ID)
|
||||
}
|
||||
}
|
||||
|
||||
s := settings.Load()
|
||||
s.DesktopLongest = true
|
||||
if err := settings.Save(s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
on := list()
|
||||
if entries(on) != len(on) || len(on) != len(off) {
|
||||
t.Fatalf("on: %d entries of %d rows (off had %d rows): %v", entries(on), len(on), len(off), on)
|
||||
}
|
||||
for id, want := range map[string]string{
|
||||
"vend/deepseek-v4-pro": off["vend/deepseek-v4-pro"].ID + "[1m]",
|
||||
"vend/claude-opus-4-8": off["vend/claude-opus-4-8"].ID + "[1m]",
|
||||
"vend/exo-free": off["vend/exo-free"].ID,
|
||||
"fake/m1": off["fake/m1"].ID,
|
||||
} {
|
||||
if r := on[id]; r.ID != want {
|
||||
t.Errorf("on: %s listed as %q, want %q", id, r.ID, want)
|
||||
}
|
||||
}
|
||||
if r := on["vend/exo-free"]; r.MaxInput != 872_000 {
|
||||
t.Errorf("exo-free's window told as %d, want its own 872000", r.MaxInput)
|
||||
}
|
||||
|
||||
// the 1M id and the plain id an older session holds both reach the model
|
||||
for _, id := range []string{on["vend/deepseek-v4-pro"].ID, off["vend/deepseek-v4-pro"].ID} {
|
||||
f.calls = 0
|
||||
req := httptest.NewRequest("POST", "/v1/messages", strings.NewReader(`{"model":"`+id+`","max_tokens":10,"stream":true,"tools":[{"name":"t","input_schema":{"type":"object"}}],"messages":[{"role":"user","content":"hi"}]}`))
|
||||
req.Header.Set("x-api-key", TokenFor("claude-desktop"))
|
||||
rec := httptest.NewRecorder()
|
||||
New().Handler().ServeHTTP(rec, req)
|
||||
var sent struct{ Model string }
|
||||
if rec.Code != 200 || f.calls != 1 || json.Unmarshal(f.got, &sent) != nil || sent.Model != "deepseek-v4-pro" {
|
||||
t.Fatalf("%s: %d %s, %d calls, sent %s", id, rec.Code, rec.Body, f.calls, f.got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1017,6 +1017,8 @@ func Handler(w Windows, gw *gateway.Server) http.Handler {
|
||||
in.CodexAgentsV1 = cur.CodexAgentsV1
|
||||
in.FullContext = cur.FullContext // set on its own (full-context below)
|
||||
in.CompactAt = cur.CompactAt // and so is the threshold
|
||||
// Claude Desktop's list, set in its row on the Agents page
|
||||
in.DesktopLongest = cur.DesktopLongest
|
||||
|
||||
in.CodexTitles = cur.CodexTitles // set on its own (codex-titles below)
|
||||
// and so is the model Codex's auto-review runs on (codex-auto-review)
|
||||
@@ -1168,6 +1170,22 @@ func Handler(w Windows, gw *gateway.Server) http.Handler {
|
||||
}
|
||||
writeJSON(rw, settingsState())
|
||||
})
|
||||
// whether Claude Desktop lists a model of 1M or more once, by its 1M id
|
||||
// (settings.DesktopLongest, #1272): it reads the list as it starts
|
||||
mux.HandleFunc("POST /api/settings/desktop-longest", 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
|
||||
}
|
||||
s := settings.Load()
|
||||
s.DesktopLongest = in.On
|
||||
if err := settings.Save(s); err != nil {
|
||||
fail(rw, err)
|
||||
return
|
||||
}
|
||||
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
|
||||
|
||||
@@ -3198,6 +3198,9 @@ body.window .row + .row.agent.conn > .who { min-height: 41px; } /* the line abov
|
||||
.ag-vl.ag-menu-cap { flex-wrap: nowrap; align-items: flex-start; line-height: 1.6; }
|
||||
.ag-menu-cap > .ag-dot { margin-top: calc(0.8em - 3px); }
|
||||
.ag-menu-cap > span:last-child { flex: 1; min-width: 0; }
|
||||
/* Claude Desktop's "largest window only" switch (#1272): its words beside it, the hint after them */
|
||||
.ag-longest { cursor: pointer; }
|
||||
.ag-longest-say { color: var(--fg-2); }
|
||||
/* which copy is left on the old list: a sentence that wraps as one, under the warning's words */
|
||||
.ag-vl.ag-stale-copy { display: block; padding-left: 14px; font-size: 12px; line-height: 1.6; color: var(--fg-2); }
|
||||
.ag-hint { font-size: 12px; color: var(--muted); }
|
||||
|
||||
@@ -1236,6 +1236,40 @@ async function addProviderFromAgents() {
|
||||
$("#addProvider")?.click();
|
||||
}
|
||||
|
||||
// desktopLongestLine: Claude Desktop lists a model of 1M or more twice, the
|
||||
// plain one and a "1M context window" one of its own; switched on, magpie
|
||||
// lists it by its 1M id alone, one entry each (#1272). Desktop reads the
|
||||
// list as it starts.
|
||||
function desktopLongestLine() {
|
||||
const on = !!state.settings?.desktopLongest;
|
||||
const l = el("label", "ag-vl ag-longest");
|
||||
const b = el("button", "lib-switch use-credits" + (on ? " on" : ""));
|
||||
b.type = "button";
|
||||
b.setAttribute("role", "switch");
|
||||
b.setAttribute("aria-checked", String(on));
|
||||
b.append(el("i"));
|
||||
const say = el("span", "ag-longest-say", t("Only each model's largest window"));
|
||||
b.setAttribute("aria-label", say.textContent);
|
||||
const hint = el("span", "ag-hint", t("A model of 1M or more is listed once, as its 1M entry. Quit and reopen Claude Desktop after a change."));
|
||||
l.append(b, say, hint);
|
||||
b.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (b.disabled) return;
|
||||
b.disabled = true;
|
||||
try {
|
||||
prefs = await writingPrefs(api("settings/desktop-longest", { on: !on }));
|
||||
state.settings = prefs;
|
||||
status(t("Saved — quit and reopen Claude Desktop to see it"), "ok");
|
||||
renderAgents();
|
||||
} catch (err) {
|
||||
b.disabled = false;
|
||||
status(t(err.message), "err");
|
||||
}
|
||||
};
|
||||
return l;
|
||||
}
|
||||
|
||||
// connectPanel: a connected agent's row, opened
|
||||
function connectPanel(a, { fields, fieldBtn }) {
|
||||
const box = el("div", "ag-exp");
|
||||
@@ -1352,6 +1386,7 @@ function connectPanel(a, { fields, fieldBtn }) {
|
||||
w.append(el("span", "ag-dot"), el("span", "", t("The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.", { max: CODEX_APP_MENU, n: a.models.shown - CODEX_APP_MENU })));
|
||||
v.append(w);
|
||||
}
|
||||
if (a.id === "claude-desktop") v.append(desktopLongestLine());
|
||||
}
|
||||
// what a new session starts on: optional, the agent's own last pick
|
||||
// unset; Codex's is the very value its /model picks, so one choice
|
||||
|
||||
@@ -413,6 +413,9 @@ const I18N = {
|
||||
"claude-* models need a Claude subscription in magpie": "claude-* 模型要先在 magpie 里添加 Claude 订阅",
|
||||
"Pick": "挑选",
|
||||
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Codex App 的模型菜单只显示前 {max} 个:后面 {n} 个模型在那里选不到(Codex CLI 的 /model 能列出全部)。在「挑选」里关掉不用的,或在「排序」里把要用的拖到前面。",
|
||||
"Only each model's largest window": "只显示每个模型的最大上下文",
|
||||
"A model of 1M or more is listed once, as its 1M entry. Quit and reopen Claude Desktop after a change.": "上下文达到 1M 或以上的模型只列一项,即它的 1M 版本。改动后需退出并重新打开 Claude Desktop。",
|
||||
"Saved — quit and reopen Claude Desktop to see it": "已保存——退出并重新打开 Claude Desktop 后生效",
|
||||
"Model list": "模型列表",
|
||||
"New sessions": "新会话默认",
|
||||
"The same choice as {agent}'s {cmd}: change it here or there": "与 {agent} 的 {cmd} 同步,两边均可修改",
|
||||
@@ -4162,6 +4165,9 @@ const I18N = {
|
||||
"claude-* models need a Claude subscription in magpie": "claude-* 模型要先在 magpie 裡新增 Claude 訂閱",
|
||||
"Pick": "挑選",
|
||||
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Codex App 的模型選單只顯示前 {max} 個:後面 {n} 個模型在那裡選不到(Codex CLI 的 /model 能列出全部)。在「挑選」裡關掉不用的,或在「排序」裡把要用的拖到前面。",
|
||||
"Only each model's largest window": "只顯示每個模型的最大上下文",
|
||||
"A model of 1M or more is listed once, as its 1M entry. Quit and reopen Claude Desktop after a change.": "上下文達到 1M 或以上的模型只列一項,即它的 1M 版本。改動後需結束並重新開啟 Claude Desktop。",
|
||||
"Saved — quit and reopen Claude Desktop to see it": "已儲存——結束並重新開啟 Claude Desktop 後生效",
|
||||
"Model list": "模型列表",
|
||||
"New sessions": "新會話預設",
|
||||
"The same choice as {agent}'s {cmd}: change it here or there": "與 {agent} 的 {cmd} 同步,兩邊均可修改",
|
||||
@@ -7876,6 +7882,9 @@ const I18N = {
|
||||
"claude-* models need a Claude subscription in magpie": "claude-* モデルには magpie の Claude サブスクリプションが必要です",
|
||||
"Pick": "選択",
|
||||
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Codex アプリのモデルメニューには先頭の {max} 個しか表示されず、残りの {n} 個のモデルはそこで選べません(Codex CLI の /model にはすべて表示されます)。「選択」で使わないモデルをオフにするか、「並び順」で使うモデルを先頭へドラッグしてください。",
|
||||
"Only each model's largest window": "各モデルの最大コンテキストのみ表示",
|
||||
"A model of 1M or more is listed once, as its 1M entry. Quit and reopen Claude Desktop after a change.": "1M 以上のコンテキストを持つモデルは 1M 版の 1 項目だけ表示されます。変更後は Claude Desktop を終了して開き直してください。",
|
||||
"Saved — quit and reopen Claude Desktop to see it": "保存しました。Claude Desktop を終了して開き直すと反映されます",
|
||||
"Model list": "モデル一覧",
|
||||
"New sessions": "新しいセッション",
|
||||
"The same choice as {agent}'s {cmd}: change it here or there": "{agent} の {cmd} と同じ選択です。ここでもそちらでも変更できます",
|
||||
@@ -11608,6 +11617,9 @@ const I18N = {
|
||||
"claude-* models need a Claude subscription in magpie": "claude-*-Modelle benötigen ein Claude-Abo in magpie",
|
||||
"Pick": "Wählen",
|
||||
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Das Modellmenü der Codex-App zeigt nur die ersten {max}: Die letzten {n} Modelle lassen sich dort nicht wählen (/model in der Codex-CLI listet alle). Schalte unter „Wählen“ ungenutzte aus oder zieh unter „Reihenfolge“ die genutzten nach vorn.",
|
||||
"Only each model's largest window": "Nur das größte Kontextfenster jedes Modells",
|
||||
"A model of 1M or more is listed once, as its 1M entry. Quit and reopen Claude Desktop after a change.": "Ein Modell mit 1M oder mehr erscheint nur einmal, als 1M-Eintrag. Beende Claude Desktop nach einer Änderung und öffne es neu.",
|
||||
"Saved — quit and reopen Claude Desktop to see it": "Gespeichert – beende Claude Desktop und öffne es neu, um es zu sehen",
|
||||
"Model list": "Modellliste",
|
||||
"New sessions": "Neue Sitzungen",
|
||||
"The same choice as {agent}'s {cmd}: change it here or there": "Dieselbe Auswahl wie {cmd} von {agent}: hier oder dort ändern",
|
||||
|
||||
@@ -153,6 +153,7 @@ func TestSettingsSaveKeepsWhatItDoesNotSend(t *testing.T) {
|
||||
Window: []int{900, 700},
|
||||
WindowMaximised: true,
|
||||
FullContext: true,
|
||||
DesktopLongest: true,
|
||||
GatewayMode: "off",
|
||||
}
|
||||
if err := settings.Save(was); err != nil {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// Claude Desktop lists a model of 1M or more twice, its plain entry and a
|
||||
// "1M context window" one it adds (#1272). Claude Desktop's opened row has a
|
||||
// switch under its model list that lists such a model once, by its 1M entry:
|
||||
// off as before, it posts the new say at once, shows it, says Desktop has to
|
||||
// be reopened, and no click scrolls the page. In English and Chinese, wide
|
||||
// and narrow. No backend: faked here.
|
||||
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");
|
||||
|
||||
function fixture(lang) {
|
||||
const settings = { lang, theme: "light" };
|
||||
const posts = [];
|
||||
const state = () => ({
|
||||
agents: [{
|
||||
id: "claude-desktop", name: "Claude Desktop", path: "/test/claude_desktop_config.json", icon: "claude-color", wired: true,
|
||||
fields: [{ key: "provider", label: "provider", value: "magpie", options: [{ value: "magpie", label: "magpie", icon: "magpie" }] }],
|
||||
models: { shown: 2, listed: 2, by: [{ name: "OpenCode Go", icon: "opencode", n: 2 }], names: ["DeepSeek V4 Pro", "Exo Free"] },
|
||||
}],
|
||||
profiles: [], settings,
|
||||
});
|
||||
async function serve(route) {
|
||||
const req = route.request(), url = new URL(req.url());
|
||||
const json = (data) => route.fulfill({ json: data });
|
||||
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 json(state());
|
||||
if (url.pathname === "/api/settings/desktop-longest") {
|
||||
const body = req.postDataJSON();
|
||||
posts.push(body);
|
||||
settings.desktopLongest = body.on;
|
||||
return json({ ...settings });
|
||||
}
|
||||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||||
if (url.pathname === "/api/groups") return json({ groups: [] });
|
||||
if (url.pathname === "/api/agents/cli") return json({ agents: {}, pending: false });
|
||||
if (url.pathname === "/api/providers") return json({ providers: [], gateway: { running: true } });
|
||||
if (url.pathname === "/api/plugins") return json({ plugins: [] });
|
||||
if (url.pathname.startsWith("/api/")) return 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 });
|
||||
}
|
||||
return { serve, posts };
|
||||
}
|
||||
|
||||
const W = {
|
||||
en: { say: "Only each model's largest window", hint: "Quit and reopen Claude Desktop", details: "Details" },
|
||||
zh: { say: "只显示每个模型的最大上下文", hint: "退出并重新打开 Claude Desktop", details: "详情" },
|
||||
};
|
||||
const cd = '.row.agent[data-id="claude-desktop"]';
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of ["en", "zh"]) {
|
||||
for (const width of [1100, 640]) {
|
||||
test(`${engine} ${lang} ${width}px: Claude Desktop's row switches its list to each model's largest window`, async (t) => {
|
||||
const w = W[lang];
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
const page = await browser.newPage({ viewport: { width, height: 700 } });
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
const fx = fixture(lang);
|
||||
await page.route("**/*", fx.serve);
|
||||
t.after(() => browser.close());
|
||||
await page.goto("http://magpie.test/");
|
||||
await page.locator(cd).waitFor();
|
||||
await page.locator(`${cd} .ag-link`, { hasText: w.details }).click();
|
||||
const line = page.locator(`${cd} .ag-exp .ag-longest`);
|
||||
await line.waitFor();
|
||||
const sw = line.locator(".lib-switch");
|
||||
assert.equal(await line.locator(".ag-longest-say").textContent(), w.say);
|
||||
assert.ok((await line.locator(".ag-hint").textContent()).includes(w.hint));
|
||||
assert.equal(await sw.getAttribute("aria-checked"), "false", "on before it was switched");
|
||||
const box = await line.boundingBox();
|
||||
assert.ok(box.width <= width && box.height > 0, JSON.stringify(box));
|
||||
|
||||
const y = await page.evaluate(() => scrollY);
|
||||
await sw.click();
|
||||
await page.waitForFunction((sel) => document.querySelector(sel)?.getAttribute("aria-checked") === "true", `${cd} .ag-longest .lib-switch`);
|
||||
assert.deepEqual(fx.posts, [{ on: true }]);
|
||||
// and off again, by its words
|
||||
await page.locator(`${cd} .ag-longest .ag-longest-say`).click();
|
||||
await page.waitForFunction((sel) => document.querySelector(sel)?.getAttribute("aria-checked") === "false", `${cd} .ag-longest .lib-switch`);
|
||||
assert.deepEqual(fx.posts, [{ on: true }, { on: false }]);
|
||||
|
||||
assert.equal(await page.evaluate(() => scrollY), y, "a click scrolled the page");
|
||||
assert.equal(await page.locator("select").count(), 0, "a native select");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,6 +312,13 @@ type Settings struct {
|
||||
// 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"`
|
||||
// DesktopLongest has Claude Desktop list a model of 1M tokens or more
|
||||
// once, as its 1M entry (its id with Claude Code's "[1m]"), instead of
|
||||
// twice (#1272): Desktop adds a "1M context window" entry beside any
|
||||
// gateway model whose max_input_tokens is 1M or more and whose id has no
|
||||
// "[1m]", and adds none to one whose id has it. Off, as before, it shows
|
||||
// both, the plain one compacting at Claude Code's 200K.
|
||||
DesktopLongest bool `json:"desktopLongest,omitempty"`
|
||||
// CompactAt is the window told for a longer one when FullContext is
|
||||
// off, in tokens: 0 is WorkingWindow (#876: 272K was the only one).
|
||||
// A provider's or a model's own (ModelCompacts) comes before it.
|
||||
|
||||
Reference in new issue
Block a user