providers: Command Code's free models are marked free, and providers switched off fold below the ones on (01huadalang on Discord: Space Bunny Alpha 在 Command Code 里是 FREE,magpie 只给 Ling 3.1 Flash 标了免费;已关闭的供应商还占着位置) — magpie marked a model free when the plan said so (m.free) or its id or name said "free"; Command Code's /provider/v1/models says nothing of it, so only ids like inclusionai/ling-3.1-flash:free got 免费. Its CLI (1.73.2) badges eleven models free in its own table (badge:"free"), Space Bunny Alpha and Pixel Canary among them; those are now marked Free on the Go table, on Go's fetched list and on a keyed plan's, so the chips say 免费 with "doesn't use the plan's credits". Neither the CLI nor the API says a discount (no "-99%" anywhere), so none is shown. On the Providers page the providers switched off now sit below the list under a "Turned off (N)" fold (已关闭的供应商), folded at first and remembered per viewer; a click on it moves nothing, and a provider switched off moves under it. TestCommandCodeFreeModels and providers-off-fold.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves a load flake, passes alone); providers-off-fold, click-scroll, old-webkit, provider-search, provider-remove, add-sheet, nokey-editor, plain-names, model-test-one pass.

This commit is contained in:
yetone committed 2026-10-01 17:13:34 +08:00
1 parent 10b940233c
commit 4af01cd742
8 files changed
+276 -6

No files matched your search

+3
View File
@@ -1151,6 +1151,9 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; }
#view-gateway .row-head, #view-usage .row-head { margin: 14px 0 6px; padding: 0 2px; }
#view-usage .row-head.quota-head { margin: 6px 0 6px; }
#view-gateway .row-head .note { font-size: 11px; color: var(--faint); }
/* providers switched off, folded below the ones on */
#view-providers .off-head { margin: 14px 0 6px; padding: 0 2px; flex: none; }
#view-providers .off-head .note { font-size: 11px; color: var(--faint); }
/* the Models head folds the list away: a chevron, the label, how many */
.fold { display: inline-flex; align-items: center; gap: 5px; margin-left: -4px; padding: 3px 6px 3px 4px; border-radius: 6px; color: var(--faint); transition: background .12s, color .12s; }
.fold:hover { background: var(--pill); color: var(--muted); }
+27 -4
View File
@@ -2243,9 +2243,23 @@ function renderProvidersLoading() {
row.append(el("span", "skeleton pv-sk-icon"), who, el("span", "skeleton pv-sk-key"));
list.append(row);
}
$("#offHead").hidden = $("#offProviders").hidden = true;
$("#excluded").replaceChildren();
}
// Providers switched off are kept apart, below the ones on, under a
// "Turned off (N)" fold that starts folded: among the rest they took the
// room of the ones in use (01huadalang on Discord). The fold is
// remembered, per viewer.
let offFolded = true;
try { offFolded = localStorage.getItem("magpie.offProvidersOpen") !== "1"; } catch {}
$("#foldOff").prepend(svg(CHEV_R, 11, 1.6));
$("#foldOff").onclick = () => {
offFolded = !offFolded;
try { localStorage.setItem("magpie.offProvidersOpen", offFolded ? "0" : "1"); } catch {}
renderProviders();
};
// One row per provider: logo, name, the agents pointed at it, key status.
// Everything else lives in the editor, a dialog over the page.
function renderProviders() {
@@ -2253,16 +2267,25 @@ function renderProviders() {
// scroll to the top; put it back so closing the editor leaves the reader
// where they were.
const view = $("#view-providers"), top = view.scrollTop;
keepIcons($("#providers"), $("#addSheet"), $("#excluded"));
keepIcons($("#providers"), $("#offProviders"), $("#addSheet"), $("#excluded"));
view.classList.remove("loading");
view.removeAttribute("aria-busy");
closeProtoMenu();
syncURL();
const list = $("#providers");
list.replaceChildren();
list.hidden = !providers.providers.length;
const onList = $("#providers"), offList = $("#offProviders");
onList.replaceChildren();
offList.replaceChildren();
const offs = providers.providers.filter((p) => p.off).length;
onList.hidden = offs === providers.providers.length;
$("#offHead").hidden = !offs;
offList.hidden = !offs || offFolded;
const fold = $("#foldOff");
fold.setAttribute("aria-expanded", String(!offFolded));
fold.title = t(offFolded ? "Show the providers turned off" : "Fold the providers turned off away");
$("#offCount").textContent = offs ? String(offs) : "";
let dialog = null; // the editor, if one is open
for (const p of providers.providers) {
const list = p.off ? offList : onList;
const open = editing === p.id;
const row = el("div", "row provider" + (open ? " selected" : "") + (p.off ? " off" : ""));
row.dataset.id = p.id;
+3
View File
@@ -286,6 +286,9 @@ const I18N = {
"no key": "无需密钥",
"needs a key": "需要密钥",
"Switched off": "已关闭",
"Turned off": "已关闭的供应商",
"Show the providers turned off": "展开已关闭的供应商",
"Fold the providers turned off away": "收起已关闭的供应商",
"Use {name}": "使用 {name}",
"On · switch it off and agents no longer get its models; its keys and settings are kept": "已开启 · 关闭后不再把它的模型写入 Agent,密钥和设置都会保留",
"Off · switch it on and agents get its models again": "已关闭 · 开启后它的模型会重新写入 Agent",
+2
View File
@@ -73,6 +73,8 @@
<main class="view" id="view-providers" hidden>
<div id="movable" hidden></div>
<div class="list" id="providers"></div>
<div class="row-head off-head" id="offHead" hidden><button class="fold" id="foldOff" aria-expanded="false" aria-controls="offProviders"><span class="label" data-t>Turned off</span><span class="note" id="offCount"></span></button></div>
<div class="list" id="offProviders" hidden></div>
<div id="excluded"></div>
<div class="after-list">
<button class="text" id="addProvider" data-t>+ Add provider</button>
+9 -1
View File
@@ -700,10 +700,18 @@ and can be asked again, and a call it didn't keep has nothing of it. No click
moves the page; no left-border accent. English and Chinese, Chromium and
WebKit, with `/api/plugins` faked.
`providers-off-fold.test.cjs` checks that providers switched off are kept
apart (01huadalang on Discord): the Providers list holds the ones on, and the
ones off sit below it under a "Turned off (N)" fold (已关闭的供应商), folded at
first. Opening it with the page scrolled leaves the head and the page where
they are, the fold is remembered across a reload, and a provider switched
off moves under it. English and Chinese, Chromium and WebKit, with
`/api/plugins` faked.
With Node.js and Playwright available:
```sh
node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/rollup-short.test.cjs internal/gui/tests/panel-routing.test.cjs internal/gui/tests/gateway-fold.test.cjs internal/gui/tests/routing-kind.test.cjs internal/gui/tests/balance-fix.test.cjs internal/gui/tests/cli-update.test.cjs internal/gui/tests/login-import.test.cjs internal/gui/tests/agy-launch.test.cjs internal/gui/tests/currency.test.cjs internal/gui/tests/usage-ledger.test.cjs internal/gui/tests/update-check.test.cjs internal/gui/tests/omarchy.test.cjs internal/gui/tests/brand.test.cjs internal/gui/tests/library-loading.test.cjs node --test internal/gui/tests/signin-callback.test.cjs internal/gui/tests/routing-steady.test.cjs internal/gui/tests/old-webkit.test.cjs internal/gui/tests/session-terminal.test.cjs internal/gui/tests/text-size.test.cjs internal/gui/tests/zcode-site.test.cjs internal/gui/tests/rail-tip.test.cjs internal/gui/tests/sync-update.test.cjs internal/gui/tests/plugin-signin.test.cjs internal/gui/tests/plugin-market.test.cjs internal/gui/tests/s3-sync.test.cjs internal/gui/tests/routing-wb-refused.test.cjs internal/gui/tests/routing-blocked.test.cjs internal/gui/tests/routing-shape.test.cjs internal/gui/tests/market-have.test.cjs internal/gui/tests/sessions-today.test.cjs internal/gui/tests/routing-side-calls.test.cjs internal/gui/tests/import-all-skills.test.cjs internal/gui/tests/routing-flood.test.cjs internal/gui/tests/routing-busy.test.cjs internal/gui/tests/own-skill.test.cjs internal/gui/tests/routing-manual.test.cjs internal/gui/tests/tray-usages.test.cjs internal/gui/tests/provider-remove.test.cjs internal/gui/tests/signin-link.test.cjs internal/gui/tests/tray-inuse.test.cjs internal/gui/tests/codex-auto-reset.test.cjs internal/gui/tests/routing-auto-reset.test.cjs internal/gui/tests/claude-usage-asked.test.cjs internal/gui/tests/claude-risk.test.cjs internal/gui/tests/library-toggle-status.test.cjs internal/gui/tests/library-all.test.cjs internal/gui/tests/routing-reroute-title.test.cjs internal/gui/tests/plain-names.test.cjs internal/gui/tests/panel-effort.test.cjs internal/gui/tests/agent-models-all-hidden.test.cjs internal/gui/tests/omp-roles.test.cjs internal/gui/tests/claude-effort.test.cjs internal/gui/tests/remote-magpie.test.cjs internal/gui/tests/qoder-cn-tile.test.cjs internal/gui/tests/provider-levels-scroll.test.cjs internal/gui/tests/signin-paste-codex.test.cjs internal/gui/tests/usage-alert.test.cjs internal/gui/tests/quota-families.test.cjs internal/gui/tests/provider-search.test.cjs internal/gui/tests/webdav-settings-refresh.test.cjs internal/gui/tests/lan-docker.test.cjs internal/gui/tests/plugin-key-hint.test.cjs internal/gui/tests/update-hide.test.cjs internal/gui/tests/tray-logos.test.cjs internal/gui/tests/routing-proxy.test.cjs internal/gui/tests/sse-body.test.cjs internal/gui/tests/remote-group-served.test.cjs internal/gui/tests/plugin-move-overflow.test.cjs internal/gui/tests/routing-time.test.cjs internal/gui/tests/unlisted-models.test.cjs internal/gui/tests/plugin-updates.test.cjs internal/gui/tests/request-archive.test.cjs internal/gui/tests/group-member-fast.test.cjs
node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/rollup-short.test.cjs internal/gui/tests/panel-routing.test.cjs internal/gui/tests/gateway-fold.test.cjs internal/gui/tests/routing-kind.test.cjs internal/gui/tests/balance-fix.test.cjs internal/gui/tests/cli-update.test.cjs internal/gui/tests/login-import.test.cjs internal/gui/tests/agy-launch.test.cjs internal/gui/tests/currency.test.cjs internal/gui/tests/usage-ledger.test.cjs internal/gui/tests/update-check.test.cjs internal/gui/tests/omarchy.test.cjs internal/gui/tests/brand.test.cjs internal/gui/tests/library-loading.test.cjs node --test internal/gui/tests/signin-callback.test.cjs internal/gui/tests/routing-steady.test.cjs internal/gui/tests/old-webkit.test.cjs internal/gui/tests/session-terminal.test.cjs internal/gui/tests/text-size.test.cjs internal/gui/tests/zcode-site.test.cjs internal/gui/tests/rail-tip.test.cjs internal/gui/tests/sync-update.test.cjs internal/gui/tests/plugin-signin.test.cjs internal/gui/tests/plugin-market.test.cjs internal/gui/tests/s3-sync.test.cjs internal/gui/tests/routing-wb-refused.test.cjs internal/gui/tests/routing-blocked.test.cjs internal/gui/tests/routing-shape.test.cjs internal/gui/tests/market-have.test.cjs internal/gui/tests/sessions-today.test.cjs internal/gui/tests/routing-side-calls.test.cjs internal/gui/tests/import-all-skills.test.cjs internal/gui/tests/routing-flood.test.cjs internal/gui/tests/routing-busy.test.cjs internal/gui/tests/own-skill.test.cjs internal/gui/tests/routing-manual.test.cjs internal/gui/tests/tray-usages.test.cjs internal/gui/tests/provider-remove.test.cjs internal/gui/tests/signin-link.test.cjs internal/gui/tests/tray-inuse.test.cjs internal/gui/tests/codex-auto-reset.test.cjs internal/gui/tests/routing-auto-reset.test.cjs internal/gui/tests/claude-usage-asked.test.cjs internal/gui/tests/claude-risk.test.cjs internal/gui/tests/library-toggle-status.test.cjs internal/gui/tests/library-all.test.cjs internal/gui/tests/routing-reroute-title.test.cjs internal/gui/tests/plain-names.test.cjs internal/gui/tests/panel-effort.test.cjs internal/gui/tests/agent-models-all-hidden.test.cjs internal/gui/tests/omp-roles.test.cjs internal/gui/tests/claude-effort.test.cjs internal/gui/tests/remote-magpie.test.cjs internal/gui/tests/qoder-cn-tile.test.cjs internal/gui/tests/provider-levels-scroll.test.cjs internal/gui/tests/signin-paste-codex.test.cjs internal/gui/tests/usage-alert.test.cjs internal/gui/tests/quota-families.test.cjs internal/gui/tests/provider-search.test.cjs internal/gui/tests/webdav-settings-refresh.test.cjs internal/gui/tests/lan-docker.test.cjs internal/gui/tests/plugin-key-hint.test.cjs internal/gui/tests/update-hide.test.cjs internal/gui/tests/tray-logos.test.cjs internal/gui/tests/routing-proxy.test.cjs internal/gui/tests/sse-body.test.cjs internal/gui/tests/remote-group-served.test.cjs internal/gui/tests/plugin-move-overflow.test.cjs internal/gui/tests/routing-time.test.cjs internal/gui/tests/unlisted-models.test.cjs internal/gui/tests/plugin-updates.test.cjs internal/gui/tests/request-archive.test.cjs internal/gui/tests/group-member-fast.test.cjs internal/gui/tests/providers-off-fold.test.cjs
```
If Playwright is installed outside the repository, set `NODE_PATH` to the
@@ -0,0 +1,120 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// Providers switched off are kept apart (01huadalang on Discord: 已关闭的
// 供应商还占着位置): the list holds the ones on, and the ones off sit below
// it under a "Turned off (N)" fold, folded at first. The fold opens and
// closes on a click that leaves the page where it is, and is remembered
// across a reload; a provider switched off moves under it. In English and
// Chinese, Chromium and WebKit.
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 provider = (i, off) => ({
id: "p" + i, name: "Provider " + i, icon: "generic", chat: "https://p" + i + ".example.com/v1", responses: "", anthropic: "", catalog: "",
host: "p" + i + ".example.com", models: [{ id: "model-" + i, name: "Model " + i, on: true }], agents: [], fallback: [], headers: {},
key: { set: true, masked: "sk-…" + i }, keyList: [], balanceToken: { takes: false, set: false }, proxy: "", off,
});
const OFF = new Set([1, 5, 9]);
function serve(lang) {
const list = Array.from({ length: 12 }, (_, i) => provider(i, OFF.has(i)));
const providers = () => ({ providers: list, presets: [], excluded: [], gateway: { running: true, window: true } });
const state = { agents: [], profiles: [], settings: { lang, theme: "light" } };
return async (route) => {
const url = new URL(route.request().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/providers") return json(providers());
if (url.pathname === "/api/plugins") return json({ plugins: [] });
if (url.pathname === "/api/groups") return json({ groups: [] });
if (url.pathname === "/api/provider/off" || url.pathname === "/api/provider/on") {
const { id } = route.request().postDataJSON();
for (const p of list) if (p.id === id) p.off = url.pathname.endsWith("/off");
return json(providers());
}
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 });
};
}
const words = { en: "Turned off", zh: "已关闭的供应商" };
const top = (page, sel) => page.locator(sel).evaluate((e) => e.getBoundingClientRect().top);
const ids = (page, sel) => page.locator(sel + " > .row.provider").evaluateAll((rs) => rs.map((r) => r.dataset.id));
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of ["en", "zh"]) {
test(`${engine} ${lang}: providers switched off fold away below the ones on`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
const page = await (await browser.newContext({ viewport: { width: 900, height: 560 }, reducedMotion: "reduce" })).newPage();
t.after(async () => {
if (process.env.ARTIFACT_DIR) {
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
await page.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-providers-off-fold.png`) });
}
await browser.close();
});
page.setDefaultTimeout(5000);
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang));
await page.goto("http://magpie.test/?view=providers");
await page.locator("#providers .row.provider").first().waitFor();
// the list holds the ones on; the ones off are under the fold, folded
assert.deepEqual(await ids(page, "#providers"), ["p0", "p2", "p3", "p4", "p6", "p7", "p8", "p10", "p11"]);
const fold = page.locator("#foldOff");
assert(await fold.isVisible(), "the fold shows");
assert.equal((await fold.locator(".label").textContent()).trim(), words[lang]);
assert.equal((await page.locator("#offCount").textContent()).trim(), "3");
assert.equal(await fold.getAttribute("aria-expanded"), "false", "folded at first");
assert(await page.locator("#offProviders").evaluate((l) => l.hidden), "folded, the ones off are hidden");
const listBottom = await page.locator("#providers").evaluate((e) => e.getBoundingClientRect().bottom);
assert(await top(page, "#offHead") >= listBottom, "the fold is below the list");
// opened with the page scrolled to it, the head stays where it is
const view = page.locator("#view-providers");
await page.mouse.move(450, 200);
await page.mouse.wheel(0, 400);
await page.waitForTimeout(400);
const scrolled = await view.evaluate((v) => v.scrollTop);
assert(scrolled > 20, "the page scrolled to the fold");
const was = await top(page, "#foldOff");
await fold.click();
await page.waitForTimeout(400);
assert.equal(await fold.getAttribute("aria-expanded"), "true");
assert.deepEqual(await ids(page, "#offProviders"), ["p1", "p5", "p9"]);
assert(await page.locator("#offProviders").isVisible());
assert(Math.abs(await top(page, "#foldOff") - was) <= 1, "opening moved the head");
assert(Math.abs(await view.evaluate((v) => v.scrollTop) - scrolled) <= 1, "opening scrolled the page");
// remembered across a reload
await page.reload();
await page.locator("#providers .row.provider").first().waitFor();
assert.equal(await page.locator("#foldOff").getAttribute("aria-expanded"), "true", "the fold is remembered");
assert.deepEqual(await ids(page, "#offProviders"), ["p1", "p5", "p9"]);
// a provider switched off moves under it
await page.locator('#providers .row.provider[data-id="p3"] .pswitch').click();
await page.waitForFunction(() => document.querySelector('#offProviders .row.provider[data-id="p3"]'));
assert.equal((await page.locator("#offCount").textContent()).trim(), "4");
assert.equal(await page.locator('#providers .row.provider[data-id="p3"]').count(), 0);
// and folds again
await page.mouse.move(450, 200);
await page.mouse.wheel(0, 200);
await page.waitForTimeout(400);
await page.locator("#foldOff").click();
await page.waitForTimeout(300);
assert.equal(await page.locator("#foldOff").getAttribute("aria-expanded"), "false");
assert(await page.locator("#offProviders").evaluate((l) => l.hidden));
assert.deepEqual(errors, []);
});
}
}
@@ -0,0 +1,81 @@
package provider
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
// Command Code's CLI marks Space Bunny Alpha FREE, as it does Ling 3.1
// Flash (01huadalang on Discord): magpie's chips showed 免费 on Ling only,
// whose id says :free, as Command Code's list says nothing of it. The
// models the CLI calls free are marked so, on Go's list and on a keyed
// plan's, before a fetch and after.
func TestCommandCodeFreeModels(t *testing.T) {
list, err := os.ReadFile(filepath.Join("testdata", "commandcode_models.json"))
if err != nil {
t.Fatal(err)
}
for _, plan := range []string{"individual-go-monthly", "individual-max-monthly"} {
t.Run(plan, func(t *testing.T) {
home := signIn(t)
writeFile(t, filepath.Join(home, ".commandcode", "auth.json"), map[string]any{"apiKey": "cc-key", "userName": "ccuser"})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/provider/v1/models":
_, _ = w.Write(list)
case "/alpha/billing/subscriptions":
_, _ = w.Write([]byte(`{"success":true,"data":{"planId":"` + plan + `","status":"active"}}`))
default:
w.WriteHeader(404)
}
}))
defer srv.Close()
oldAPI := cmdAPI
cmdAPI = srv.URL
defer func() { cmdAPI = oldAPI }()
cmdPlansSeen.Lock()
cmdPlansSeen.m = map[string]cmdSeen{}
cmdPlansSeen.Unlock()
p, found := find(All(), CommandCodePlanID)
if !found {
t.Fatal("no Command Code account")
}
CommandCodeGenerate(context.Background(), p) // the plan read
if plan == "individual-go-monthly" {
p, _ = find(All(), CommandCodePlanID)
free := map[string]bool{}
for _, m := range p.Available() {
free[m.ID] = m.Free
}
if !free["stealth/space-bunny-alpha"] || !free["inclusionai/ling-3.1-flash:free"] {
t.Errorf("before a fetch: Space Bunny Alpha free %v, Ling 3.1 Flash free %v", free["stealth/space-bunny-alpha"], free["inclusionai/ling-3.1-flash:free"])
}
}
if _, err := p.Fetch(context.Background()); err != nil {
t.Fatal(err)
}
p, _ = find(All(), CommandCodePlanID)
free, seen := map[string]bool{}, map[string]bool{}
for _, m := range p.Available() {
free[m.ID], seen[m.ID] = m.Free, true
}
for _, id := range []string{"stealth/space-bunny-alpha", "inclusionai/ling-3.1-flash:free", "poolside/laguna-s-2.1-free"} {
if !seen[id] {
t.Errorf("%s not listed", id)
} else if !free[id] {
t.Errorf("%s not marked free", id)
}
}
for _, id := range []string{"deepseek/deepseek-v4-flash", "thinkingmachines/inkling"} {
if free[id] {
t.Errorf("%s marked free", id)
}
}
})
}
}
+31 -1
View File
@@ -168,9 +168,34 @@ func cmdGoFetch(ctx context.Context) ([]catalog.Model, error) {
if len(out) == 0 {
return nil, errors.New("Command Code listed no models for the Go plan")
}
cmdMarkFree(out)
return out, catalog.SaveLive(CommandCodePlanID, base, out)
}
// cmdFree are the models Command Code's CLI marks FREE in its picker
// (1.73.2: badge:"free", "{name} is free and uses shared capacity"); its
// API's list doesn't say so, and Space Bunny Alpha's and Pixel Canary's
// ids name nothing free. Neither says a model is served at a discount.
var cmdFree = map[string]bool{
"stealth/space-bunny-alpha": true, "stealth/pixel-canary": true,
"poolside/laguna-s-2.1-free": true, "inclusionai/ling-3.0-flash-free": true,
"inclusionai/ling-3.0-flash-sante:free": true, "inclusionai/ling-3.1-flash:free": true,
"MiniMaxAI/MiniMax-M3-Free": true, "minimax/minimax-m3-free": true,
"minimax/minimax-m2.7-free": true, "meituan/LongCat-2.0:free": true, "tencent/Hy3": true,
}
// cmdMarkFree marks the models of ms the CLI calls free, in place.
func cmdMarkFree(ms []catalog.Model) []catalog.Model {
for i := range ms {
if cmdFree[ms[i].ID] {
ms[i].Free = true
}
}
return ms
}
func init() { cmdMarkFree(cmdGoModels) }
// cmdAuth is an account's key, as auth.json and the sign-in name it.
type cmdAuth struct {
APIKey string `json:"apiKey"`
@@ -303,7 +328,12 @@ func cmdProvider(who, plan string, a cmdAuth) Provider {
}
keyed := p
keyed.Account, keyed.Key = nil, a.APIKey
return keyed.Fetch(ctx)
ms, base, err := keyed.fetchOne(keyed.Via(ctx))
if err != nil {
return nil, err
}
cmdMarkFree(ms)
return catalog.Chat(ms), catalog.SaveLive(p.ID, base, ms)
}
acct.generate = func(ctx context.Context) (string, bool) {
return a.APIKey, cmdPlanNow(ctx, a, plan) == "Go"