Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/image-aliases.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>` alias literal out of the proxy source. */
function aliasTargets(name: string): string[] {
const start = source.indexOf(`const ${name}: Record<string, string> = {`);
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);
});
});
32 changes: 25 additions & 7 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1454,7 +1454,12 @@ const IMAGE_PRICING: Record<string, { default: number; sizes?: Record<string, nu
default: 0.02,
sizes: { "1024x1024": 0.02, "1536x1024": 0.04, "1024x1536": 0.04 },
},
"black-forest/flux-1.1-pro": { default: 0.04 },
// Prices mirrored from the gateway catalog (pricePerImage / per-size).
"openai/gpt-image-2": {
default: 0.06,
sizes: { "1024x1024": 0.06, "1536x1024": 0.12, "1024x1536": 0.12 },
},
"bytedance/seedream-5-pro": { default: 0.045 },
"google/nano-banana": { default: 0.05 },
"google/nano-banana-pro": {
default: 0.1,
Expand Down Expand Up @@ -2132,7 +2137,9 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> {
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 {
Expand Down Expand Up @@ -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<string, string> = {
"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();
Expand Down
Loading