From 60b78d5279b075bf69b9b98d89025eb136bd2381 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 27 Jul 2026 21:18:51 -0700 Subject: [PATCH] fix(images): two aliases and the default pointed at models the gateway does not serve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--model flux` resolved to black-forest/flux-1.1-pro, which is not in the gateway catalog at all. `--model dalle` resolved to openai/dall-e-3, which is available:false since it was delisted upstream. Both failed the call rather than degrading. Worse, dall-e-3 was also the DEFAULT: any image request that did not name a model went to a delisted one. That is every /imagegen call from the skill. flux is dropped, along with its pricing entry — the gateway never served it. The dall-e-3 NAMES stay as aliases so existing scripts keep working, but they now redirect to gpt-image-2. Default is nano-banana. Also adds aliases for four live models the proxy could already reach but had no shorthand for, including cogview — which the imagegen skill was already telling agents to use. src/image-aliases.test.ts pins the invariant: every alias target must be priced here, because a model this proxy cannot charge for is one it does not really support. Writing it caught the same bug in my own fix — I had aliased gpt-image-2 and seedream-5-pro before adding their prices. Prices mirrored from the gateway catalog. The test cannot catch "priced but retired upstream"; availability lives in the catalog, not here, and blockrun's own model tests cover that side. --- src/image-aliases.test.ts | 60 +++++++++++++++++++++++++++++++++++++++ src/proxy.ts | 32 ++++++++++++++++----- 2 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 src/image-aliases.test.ts diff --git a/src/image-aliases.test.ts b/src/image-aliases.test.ts new file mode 100644 index 0000000..4b1b0a8 --- /dev/null +++ b/src/image-aliases.test.ts @@ -0,0 +1,60 @@ +/** + * Every image alias must point at a model this proxy can actually price. + * + * Two of them did not, and both failed the call rather than degrading: + * + * --model flux -> black-forest/flux-1.1-pro, absent from the gateway catalog + * --model dalle -> openai/dall-e-3, delisted upstream (available:false) + * + * and the DEFAULT for an unqualified image request was the second one, so any + * request that did not name a model failed too. + * + * The invariant checkable offline is that an alias target is priced here: a + * model this repo cannot price is one it does not really support. That catches + * the flux case at the point someone adds it. + * + * It cannot catch "priced but retired upstream" — availability lives in the + * gateway catalog, not here. That one is caught by blockrun's own model tests, + * which is where the catalog is. + */ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const source = readFileSync("src/proxy.ts", "utf8"); + +/** Pull a `Record` alias literal out of the proxy source. */ +function aliasTargets(name: string): string[] { + const start = source.indexOf(`const ${name}: Record = {`); + expect(start, `${name} not found — has it been renamed?`).toBeGreaterThan(-1); + const body = source.slice(start, source.indexOf("};", start)); + return [...body.matchAll(/:\s*"([^"]+)"/g)].map((m) => m[1]); +} + +/** Model ids the pricing table knows how to charge for. */ +const priced = new Set( + [...source.matchAll(/^\s{2}"([a-z0-9-]+\/[a-z0-9.-]+)":\s*\{/gim)].map((m) => m[1]), +); + +describe("image model aliases", () => { + it("finds a pricing table to check against", () => { + expect(priced.size).toBeGreaterThan(3); + }); + + it.each([["IMAGE_MODEL_ALIASES"], ["IMG2IMG_ALIASES"]])("%s targets are all priced", (name) => { + const unpriced = aliasTargets(name).filter((t) => !priced.has(t)); + expect(unpriced, `unpriced alias target(s) — this proxy cannot charge for them`).toEqual([]); + }); + + it("does not route anything to the delisted dall-e-3", () => { + // Kept as an alias KEY so existing scripts keep working; it must never be + // a TARGET again. + expect(aliasTargets("IMAGE_MODEL_ALIASES")).not.toContain("openai/dall-e-3"); + }); + + it("defaults unqualified image requests to a model it can price", () => { + const fallback = source.match(/imgModel = parsed\.model \|\| "([^"]+)"/)?.[1]; + expect(fallback, "the default image model literal moved").toBeTruthy(); + expect(priced.has(fallback!), `default ${fallback} is not priced`).toBe(true); + }); +}); diff --git a/src/proxy.ts b/src/proxy.ts index b76e814..d336b9a 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1454,7 +1454,12 @@ const IMAGE_PRICING: Record { let imgCost = 0; try { const parsed = JSON.parse(reqBody.toString()); - imgModel = parsed.model || "openai/dall-e-3"; + // dall-e-3 is delisted upstream (available:false in the catalog), so + // it defaulted every unqualified image request into a failed call. + imgModel = parsed.model || "google/nano-banana"; const n = parsed.n || 1; imgCost = estimateImageCost(imgModel, parsed.size, n); } catch { @@ -3312,18 +3319,29 @@ async function proxyRequest( if (modelMatch) { const raw = modelMatch[1]; // Resolve shorthand aliases + // Every target must be an AVAILABLE model in the gateway catalog. + // Two were not: dall-e-3 has available:false since it was delisted + // upstream, and black-forest/flux-1.1-pro is not in the catalog at + // all — so `--model dalle` and `--model flux` both failed the call. + // Legacy names redirect to a live successor rather than 404. const IMAGE_MODEL_ALIASES: Record = { - "dall-e-3": "openai/dall-e-3", - dalle3: "openai/dall-e-3", - dalle: "openai/dall-e-3", + // Retired: dall-e-3 is delisted upstream. Kept as a redirect so + // existing scripts keep working instead of breaking outright. + "dall-e-3": "openai/gpt-image-2", + dalle3: "openai/gpt-image-2", + dalle: "openai/gpt-image-2", "gpt-image": "openai/gpt-image-1", "gpt-image-1": "openai/gpt-image-1", - flux: "black-forest/flux-1.1-pro", - "flux-pro": "black-forest/flux-1.1-pro", + "gpt-image-2": "openai/gpt-image-2", banana: "google/nano-banana", "nano-banana": "google/nano-banana", "banana-pro": "google/nano-banana-pro", "nano-banana-pro": "google/nano-banana-pro", + grok: "xai/grok-imagine-image", + "grok-imagine": "xai/grok-imagine-image", + "grok-imagine-pro": "xai/grok-imagine-image-pro", + seedream: "bytedance/seedream-5-pro", + cogview: "zai/cogview-4", }; imageModel = IMAGE_MODEL_ALIASES[raw] ?? raw; imagePrompt = imagePrompt.replace(/--model\s+\S+/, "").trim();