gui: a model with no list price is given its price one box at a time (PAMI on Discord) — in Names & levels, a model with no list price (a custom provider's, or one added by hand) couldn't be priced: its Input typed first, the editor said "has no list price: give its input and output prices" and emptied the box again, and the same with Output first, so both were never there at once. A part typed now stays, the row marked unsaved, until the other is given; a Save before then is held, the empty box focused and the same message beside its buttons. model-price-edit.test.cjs (WebKit, en and zh) fails without the change. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
This commit is contained in:
1 parent
509e8444d1
commit
4c001cef85
2 files changed
+43
-5
No files matched your search
@@ -6185,6 +6185,18 @@ function cancelEdit(keep) {
|
||||
editing = null; draft = null; importing = null; importingApps = null; renderProviders();
|
||||
}
|
||||
|
||||
// priceTypedError: a model with no list price given only part of its
|
||||
// price in Names & levels (its input and output both needed) holds the
|
||||
// Save, its empty box focused, rather than the part typed being dropped.
|
||||
function priceTypedError(ed) {
|
||||
const id = Object.keys(draft?.priceTyped || {}).find((x) => !draft.chosen?.length || draft.chosen.includes(x));
|
||||
if (id === undefined) return false;
|
||||
const row = [...ed.querySelectorAll(".mname")].find((r) => r.querySelector("code")?.textContent === id);
|
||||
[...(row?.querySelectorAll(".mprice input") || [])].find((i) => i.value.trim() === "" && !i.placeholder)?.focus({ preventScroll: true });
|
||||
editorError(t("{id} has no list price: give its input and output prices", { id }), "warn");
|
||||
return true;
|
||||
}
|
||||
|
||||
// modelPrefsOfDraft: the names, levels and images staged in the editor's
|
||||
// Names & levels, for its Save, or nothing when none changed.
|
||||
function modelPrefsOfDraft() {
|
||||
@@ -7070,6 +7082,7 @@ function drawEditor(p, presetID) {
|
||||
if (maxConcurrency === undefined) return concurrencyError(ed);
|
||||
const priceRate = priceRateOfDraft();
|
||||
if (priceRate === undefined) return priceRateError(ed);
|
||||
if (priceTypedError(ed)) return;
|
||||
saveBtn.classList.add("busy"); providerAction("save", { id: p.id, models: chosenIds(), unlisted: draft.unlisted, fallback: draft.fallback, contexts: cx.map, outputs: ox.map, compacts: cpx.map, proxy, accountProxies: own.map, maxConcurrency, priceRate, modelPrefs: modelPrefsOfDraft(), pinUpstream: !!draft.pinUpstream, ...routingOfDraft(p) }, t("{name} saved", { name: p.name })); };
|
||||
bar.append(cancel, saveBtn);
|
||||
ed.append(bar);
|
||||
@@ -7410,6 +7423,7 @@ function drawEditor(p, presetID) {
|
||||
if (body.maxConcurrency === undefined) return concurrencyError(ed);
|
||||
body.priceRate = priceRateOfDraft();
|
||||
if (body.priceRate === undefined) return priceRateError(ed);
|
||||
if (p && priceTypedError(ed)) return;
|
||||
if (draft.balanceToken) body.balanceToken = draft.balanceToken;
|
||||
else if (draft.clearBalanceToken) body.clearBalanceToken = true;
|
||||
if (team) {
|
||||
@@ -8436,7 +8450,8 @@ function renderModels(p) {
|
||||
priceBox.append(el("span", "", t("Price, $ / 1M tokens")));
|
||||
const cells = parts.map(([k, l]) => {
|
||||
const box = el("label", "mpart");
|
||||
const i = input(priceNow() ? shown(priceNow()[k]) : "", m.list ? shown(m.list[k]) : "", "number");
|
||||
const typed = draft.priceTyped?.[id];
|
||||
const i = input(typed ? typed[parts.findIndex(([x]) => x === k)] : priceNow() ? shown(priceNow()[k]) : "", m.list ? shown(m.list[k]) : "", "number");
|
||||
i.inputMode = "decimal";
|
||||
i.min = "0";
|
||||
i.step = "any";
|
||||
@@ -8453,6 +8468,7 @@ function renderModels(p) {
|
||||
const v = cells.map((c) => c.value.trim());
|
||||
const x = pref();
|
||||
delete x.price; delete x.ownPrice;
|
||||
if (draft.priceTyped) delete draft.priceTyped[id];
|
||||
if (v.every((s) => s === "")) {
|
||||
if (m.price) x.ownPrice = true;
|
||||
drawReset();
|
||||
@@ -8461,6 +8477,16 @@ function renderModels(p) {
|
||||
const price = {};
|
||||
for (const [i, [k]] of parts.entries()) {
|
||||
const n = v[i] === "" ? m.list?.[k] ?? (k.startsWith("cache") ? 0 : NaN) : Number(v[i]);
|
||||
// a model with no list price is given its input and output one
|
||||
// box at a time: what is typed stays, waiting for the other, and
|
||||
// the Save asks for it (PAMI on Discord: each box typed was
|
||||
// emptied again, so a price could never be set)
|
||||
if (Number.isNaN(n) && v[i] === "" && v.every((s) => s === "" || (Number.isFinite(Number(s)) && Number(s) >= 0))) {
|
||||
(draft.priceTyped = draft.priceTyped || {})[id] = v;
|
||||
status(t("{id} has no list price: give its input and output prices", { id: m.id }), "warn");
|
||||
drawReset();
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
status(Number.isNaN(n) && v[i] === "" ? t("{id} has no list price: give its input and output prices", { id: m.id }) : t("A price is a number of dollars, 0 or more"), "err");
|
||||
showPrice();
|
||||
@@ -8531,6 +8557,7 @@ function renderModels(p) {
|
||||
reset.title = t("Its own name, every reasoning level it has, whether it sees images, the API it is asked on, the model it is the same as, and its list price");
|
||||
reset.onclick = () => {
|
||||
prefs[id] = {};
|
||||
if (draft.priceTyped) delete draft.priceTyped[id];
|
||||
if (m.default) prefs[id].name = "";
|
||||
if (m.kept?.length) prefs[id].efforts = [];
|
||||
if (m.imageSet) prefs[id].ownImages = true;
|
||||
@@ -8548,10 +8575,10 @@ function renderModels(p) {
|
||||
const drawReset = () => {
|
||||
const x = prefs[id];
|
||||
if (x && !Object.keys(x).length) delete prefs[id];
|
||||
unsaved.hidden = !prefs[id];
|
||||
unsaved.hidden = !prefs[id] && !draft.priceTyped?.[id];
|
||||
// staged back to its own already, there is nothing to restore
|
||||
const images = prefs[id]?.ownImages ? false : prefs[id]?.images !== undefined ? prefs[id].images !== !!m.ownImages : !!m.imageSet;
|
||||
const custom = nameNow() !== "" || (prefs[id]?.efforts ? prefs[id].efforts.length > 0 : !!m.kept?.length) || images || apiNow() !== "" || sameNow() !== "" || priceNow() !== null;
|
||||
const custom = nameNow() !== "" || (prefs[id]?.efforts ? prefs[id].efforts.length > 0 : !!m.kept?.length) || images || apiNow() !== "" || sameNow() !== "" || priceNow() !== null || !!draft.priceTyped?.[id];
|
||||
reset.hidden = !custom;
|
||||
};
|
||||
row.append(unsaved, reset);
|
||||
|
||||
@@ -81,11 +81,21 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
assert.deepEqual(await boxes("model-1").evaluateAll((is) => is.map((i) => i.value)), ["1.25", "8", "0.125", "0"]);
|
||||
await row("model-2").getByRole("button", { name: L.reset }).click();
|
||||
assert.deepEqual(await boxes("model-2").evaluateAll((is) => is.map((i) => i.value)), ["", "", "", ""]);
|
||||
// no list price: input and output must be given
|
||||
// no list price: input and output must be given, one box at a time —
|
||||
// the part typed stays (PAMI on Discord: it was emptied, so no price
|
||||
// could be set), and a Save before the other part asks for it
|
||||
await row("model-3").getByRole("spinbutton", { name: L.input, exact: true }).fill("3");
|
||||
await row("model-3").getByRole("spinbutton", { name: L.input, exact: true }).press("Enter");
|
||||
await page.locator("#status", { hasText: L.nolist }).waitFor();
|
||||
assert.equal(await row("model-3").getByText(L.unsaved, { exact: true }).isVisible(), false);
|
||||
assert.deepEqual(await boxes("model-3").evaluateAll((is) => is.map((i) => i.value)), ["3", "", "", ""]);
|
||||
assert(await row("model-3").getByText(L.unsaved, { exact: true }).isVisible());
|
||||
await page.getByRole("button", { name: L.save, exact: true }).click();
|
||||
await page.locator(".editor-error", { hasText: L.nolist }).waitFor();
|
||||
assert.equal(await page.evaluate(() => document.activeElement?.getAttribute("aria-label")), L.output);
|
||||
assert.deepEqual(posts, [], "a price half given isn't sent");
|
||||
await row("model-3").getByRole("spinbutton", { name: L.output, exact: true }).fill("15");
|
||||
await row("model-3").getByRole("spinbutton", { name: L.output, exact: true }).press("Enter");
|
||||
assert.deepEqual(await boxes("model-3").evaluateAll((is) => is.map((i) => i.value)), ["3", "15", "0", "0"]);
|
||||
assert.equal(await page.evaluate(() => scrollY), y, "nothing scrolled the page");
|
||||
assert.deepEqual(posts, [], "nothing is sent before the Save");
|
||||
if (process.env.ARTIFACT_DIR) await page.locator(".mnames").screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-model-price-rows.png`) });
|
||||
@@ -96,6 +106,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
assert.deepEqual(posts[0].body.modelPrefs, {
|
||||
"model-1": { price: { input: 1.25, output: 8, cache_read: 0.125, cache_write: 0 } },
|
||||
"model-2": { ownPrice: true },
|
||||
"model-3": { price: { input: 3, output: 15, cache_read: 0, cache_write: 0 } },
|
||||
});
|
||||
const missing = await page.evaluate(() => ["Price, $ / 1M tokens", "{id} has no list price: give its input and output prices", "A price is a number of dollars, 0 or more"]
|
||||
.filter((k) => !I18N.zh[k] || !I18N.ja[k] || !I18N.de[k]));
|
||||
|
||||
Reference in new issue
Block a user