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:
8 files changed
+276
-6
No files matched your search
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user