From 2cc2d2c3b687c1871126bc4cd880048a3c6d63e2 Mon Sep 17 00:00:00 2001 From: VickyXAI <115643921+VickyXAI@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:25:53 -0500 Subject: [PATCH] fix(catalog): bring chat, free, image and video tables in line with the live 2026-10-20 catalogue Chat (src/utils/constants.ts) - CHAT_PRICE_PER_MTOKEN: add the 33 live paid chat ids that had no row (gpt-6.1-sol, gpt-5.6-*-pro, mistral-large-4, glm-5.3/-flash, qwen3.8-max/ -flash, gemini-3.6/3.8-flash, hy4-preview, mimo-v2.5, ...), so an explicit call reserves its own rate instead of the $5/$30 default. - Drop tencent/hy3 (hidden, redirected; row stale vs the account sheet). claude-opus-4 stays: hidden but served at $15/$75 on the account rail. - LONG_CONTEXT: add gpt-6.1-sol's >272K step ($4/$15). - MODEL_TIERS.free: re-probed both chains (unpaid, realistic prompt). north-mini-code and laguna-xs-2.1 now answer as nemotron-3-super-120b on both chains and leave the tier; nemotron-3-super-120b (self-serving on both) joins it. FREE_CHAT_MODELS keeps every $0 id: all fourteen still answer 200 unpaid and the gateway redirects retired ids to $0 targets. Media - image: add gpt-image-2.5-flare/-sunburst ($0.28 / $0.56) and grok-imagine-image-2.0 ($0.04); sunburst joins edit + mask sets; grok-imagine-image-pro $0.07 -> $0.05 (gateway cut it 2026-09-08). - video: add grok-imagine-video-1.5 ($0.08/$0.14/$0.25 per second at 480p/720p/1080p, 1-15s, no margin, same as grok-imagine-video). - verify:prices probes the new image and video rows. Tests pin the new rows against live unpaid 402 quotes; README / schema overhead doc / context-cost cards updated for the larger tool descriptions. --- README.md | 8 +- assets/context-cost-dark.svg | 6 +- assets/context-cost.svg | 6 +- docs/mcp-schema-overhead.md | 6 +- scripts/verify-prices.ts | 20 ++++ skills/image-prompting/SKILL.md | 4 +- src/tools/image.ts | 38 +++++-- src/tools/video.ts | 43 +++++--- src/utils/constants.ts | 134 ++++++++++++++++++++----- test/chat-wallet-after-payment.test.ts | 3 +- test/chat.test.ts | 19 ++-- test/image.test.ts | 23 ++++- test/long-context.test.ts | 13 +-- test/video-models.test.ts | 25 +++++ 14 files changed, 271 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 454914b..a342f68 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ claude mcp add blockrun -s user -- npx -y @blockrun/mcp@latest
- Context cost: 14.0K tokens, 7% of a 200K context window, charged every turn whether or not you call a tool. 5.4K with --profile trading, 61% less. + Context cost: 14.3K tokens, 7% of a 200K context window, charged every turn whether or not you call a tool. 5.4K with --profile trading, 62% less.
@@ -252,13 +252,13 @@ Package managers have shown install size for decades. Almost no MCP server shows | Profile | Tools | Context | |---------|-------|---------| -| `full` *(default)* | 19 | 14,018 | +| `full` *(default)* | 19 | 14,335 | | `trading` | 8 | 5,411 | -| `media` | 7 | 6,827 | +| `media` | 7 | 7,144 | | `research` | 5 | 2,727 | | `chat` | 3 | 2,082 | -Running `--profile trading` instead of the default costs **61% less context** for the same trading +Running `--profile trading` instead of the default costs **62% less context** for the same trading workflow. If you only ever ask about markets, that is the single cheapest change you can make. Measure it yourself — against us, or against any other stdio MCP server: diff --git a/assets/context-cost-dark.svg b/assets/context-cost-dark.svg index e326291..2ec30ff 100644 --- a/assets/context-cost-dark.svg +++ b/assets/context-cost-dark.svg @@ -1,10 +1,10 @@ - + CONTEXT COST - 14.0K tokens + 14.3K tokens 7% of a 200K context window · every turn, whether or not you call a tool - 5.4K with --profile trading — 61% less + 5.4K with --profile trading — 62% less measured, not estimated diff --git a/assets/context-cost.svg b/assets/context-cost.svg index 8020d37..8cbdc14 100644 --- a/assets/context-cost.svg +++ b/assets/context-cost.svg @@ -1,10 +1,10 @@ - + CONTEXT COST - 14.0K tokens + 14.3K tokens 7% of a 200K context window · every turn, whether or not you call a tool - 5.4K with --profile trading — 61% less + 5.4K with --profile trading — 62% less measured, not estimated diff --git a/docs/mcp-schema-overhead.md b/docs/mcp-schema-overhead.md index c98185c..2244ee2 100644 --- a/docs/mcp-schema-overhead.md +++ b/docs/mcp-schema-overhead.md @@ -16,13 +16,13 @@ Written 2026-09-01, verified against `@modelcontextprotocol/sdk` 1.29.0. Numbers | Profile | Tools | Context | |---------|-------|---------| -| `full` *(default)* | 19 | 14,018 | +| `full` *(default)* | 19 | 14,335 | | `trading` | 8 | 5,411 | -| `media` | 7 | 6,827 | +| `media` | 7 | 7,144 | | `research` | 5 | 2,727 | | `chat` | 3 | 2,082 | -Descriptions are ~54% of it, input schemas ~42%. `--profile trading` costs 61% less than the +Descriptions are ~54% of it, input schemas ~42%. `--profile trading` costs 62% less than the default for the same workflow. These figures move with every description edit, so they are not the source of truth — the README diff --git a/scripts/verify-prices.ts b/scripts/verify-prices.ts index beb28c8..f2b0778 100644 --- a/scripts/verify-prices.ts +++ b/scripts/verify-prices.ts @@ -187,6 +187,22 @@ const PROBES: Probe[] = [ // nano-banana-2 is 1024-only upstream (the gateway 400s any other size), so a // single probe covers its whole price surface. { label: "image nano-banana-2 1024", path: "images/generations", body: { model: "google/nano-banana-2", prompt: "a cube", size: "1024x1024" }, expected: estimateImageCost("google/nano-banana-2", "1024x1024") }, + // Added 2026-10-20 with their rows: the 2.5 pair at both size tiers (flat + // across quality, $0.28 / $0.56), Grok Imagine 2.0, and the Pro row that + // had sat at $0.07 for six weeks after the gateway cut it to $0.05. + ...([ + ["openai/gpt-image-2.5-flare", "1024x1024"], + ["openai/gpt-image-2.5-flare", "1536x1024"], + ["openai/gpt-image-2.5-sunburst", "1024x1024"], + ["openai/gpt-image-2.5-sunburst", "1024x1536"], + ["xai/grok-imagine-image-2.0", "1024x1024"], + ["xai/grok-imagine-image-pro", "1024x1024"], + ] as const).map(([model, size]) => ({ + label: `image ${model.slice(model.indexOf("/") + 1)} ${size}`, + path: "images/generations", + body: { model, prompt: "a cube", size }, + expected: estimateImageCost(model, size), + })), // Seedream's tier is keyed on min(w,h), not max: 2048x1024 must bill BASE // while 2048x2048 bills the large tier. Pin the boundary from both sides. ...(["1024x1024", "2048x1024", "2048x2048"].map((size) => ({ @@ -207,6 +223,10 @@ const PROBES: Probe[] = [ ["xai/grok-imagine-video", undefined, undefined], ["xai/grok-imagine-video", undefined, "720p"], ["xai/grok-imagine-video", 15, "720p"], + ["xai/grok-imagine-video-1.5", undefined, undefined], + ["xai/grok-imagine-video-1.5", undefined, "720p"], + ["xai/grok-imagine-video-1.5", undefined, "1080p"], + ["xai/grok-imagine-video-1.5", 15, "1080p"], ["azure/sora-2", undefined, undefined], ["bytedance/seedance-1.5-pro", undefined, undefined], ["bytedance/seedance-1.5-pro", 12, undefined], diff --git a/skills/image-prompting/SKILL.md b/skills/image-prompting/SKILL.md index a46cc63..e63e023 100644 --- a/skills/image-prompting/SKILL.md +++ b/skills/image-prompting/SKILL.md @@ -35,6 +35,8 @@ doubles the price ($0.064 → $0.127). Ask for `1536x1024` only when you need it | Artistic / stylized / fast | `google/nano-banana` | generate | `1024x1024` | $0.0535 | | Cheapest usable draft | `zai/cogview-4` | generate | `1024x1024` | $0.01675 | | Widescreen / banner on a budget | `bytedance/seedream-5-pro` | generate | `2048x1024` or `1280x720` | $0.04825 ($0.0955 when both sides >1024) | +| Premium render or high-precision edit | `openai/gpt-image-2.5-sunburst` (`-flare` generates only, same price) | generate or edit | `1024x1024` | $0.295 ($0.589 at `1536x1024` / `1024x1536`) | +| Stylized Grok render, one step above the base | `xai/grok-imagine-image-2.0` | generate | `1024x1024` (only size) | $0.043 | | Edit an existing image (localized change) | `openai/gpt-image-2` | edit | match source | $0.064 at 1024x1024, $0.127 above | | Composite from multiple refs | `openai/gpt-image-2` | edit (multi-ref) | match target | $0.064 at 1024x1024, $0.127 above | @@ -312,4 +314,4 @@ CONSTRAINTS: ## Reference -BlockRun image models: `openai/gpt-image-2`, `openai/gpt-image-1`, `google/nano-banana`, `google/nano-banana-2`, `google/nano-banana-pro`, `zai/cogview-4`, `xai/grok-imagine-image`, `xai/grok-imagine-image-pro`, `bytedance/seedream-5-pro` +BlockRun image models: `openai/gpt-image-2`, `openai/gpt-image-1`, `google/nano-banana`, `google/nano-banana-2`, `google/nano-banana-pro`, `openai/gpt-image-2.5-flare`, `openai/gpt-image-2.5-sunburst`, `zai/cogview-4`, `xai/grok-imagine-image`, `xai/grok-imagine-image-2.0`, `xai/grok-imagine-image-pro`, `bytedance/seedream-5-pro` diff --git a/src/tools/image.ts b/src/tools/image.ts index d121817..7f31d28 100644 --- a/src/tools/image.ts +++ b/src/tools/image.ts @@ -122,14 +122,26 @@ export async function resolveImageRef(ref: string): Promise { return { dataUri: `data:${mime};base64,${buffer.toString("base64")}`, localPath }; } -// Base (1024x1024) prices, mirroring the live /v1/images/models catalog. -// dall-e-3 and flux-schnell were delisted upstream — do not re-add them. +// Base (1024x1024) prices, mirroring the gateway registry's IMAGE_MODELS +// (blockrun src/lib/models.ts). GET /v1/models publishes these WITH the 5% +// margin (per_image = base x 1.05, e.g. gpt-image-2 $0.063), so divide by 1.05 +// when checking a row against it. dall-e-3 and flux-schnell were delisted +// upstream — do not re-add them. +// +// 2026-10-20: grok-imagine-image-pro cut $0.07 -> $0.05 (the gateway's own +// size row was corrected on 2026-09-08; this table never followed, so it +// over-reserved 40%). Added gpt-image-2.5-flare / -sunburst ($0.28, flat +// across `quality`, which this tool does not send) and grok-imagine-image-2.0 +// ($0.04 — the gateway pins xAI's `quality: "low"`, the tier that price covers). const GENERATE_MODEL_COST: Record = { "zai/cogview-4": 0.015, "xai/grok-imagine-image": 0.02, - "xai/grok-imagine-image-pro": 0.07, + "xai/grok-imagine-image-2.0": 0.04, + "xai/grok-imagine-image-pro": 0.05, "openai/gpt-image-1": 0.02, "openai/gpt-image-2": 0.06, + "openai/gpt-image-2.5-flare": 0.28, + "openai/gpt-image-2.5-sunburst": 0.28, "google/nano-banana": 0.05, "google/nano-banana-2": 0.09, "google/nano-banana-pro": 0.10, @@ -140,14 +152,18 @@ const GENERATE_MODEL_COST: Record = { const LARGE_SIZE_COST: Record = { "openai/gpt-image-1": 0.04, "openai/gpt-image-2": 0.12, + "openai/gpt-image-2.5-flare": 0.56, // 1536x1024 / 1024x1536 + "openai/gpt-image-2.5-sunburst": 0.56, "google/nano-banana-pro": 0.15, // 4096x4096 tier "bytedance/seedream-5-pro": 0.09, // min(w,h) > 1024 tier — see LARGE_SIZE_MIN_DIM_MODELS }; -// Mirrors the gateway's EDIT_SUPPORTED_MODELS (/v1/images/image2image). +// Mirrors the gateway's EDIT_SUPPORTED_MODELS (/v1/images/image2image, +// src/lib/image-editing.ts). Of the 2.5 pair only Sunburst edits. const EDIT_MODELS = new Set([ "openai/gpt-image-1", "openai/gpt-image-2", + "openai/gpt-image-2.5-sunburst", "google/nano-banana", "google/nano-banana-2", "google/nano-banana-pro", @@ -162,7 +178,7 @@ const MAX_EDIT_IMAGES_BY_PREFIX: Record = { }; // Mirrors the gateway's MASK_SUPPORTED_MODELS — only OpenAI image models inpaint. -const MASK_MODELS = new Set(["openai/gpt-image-1", "openai/gpt-image-2"]); +const MASK_MODELS = new Set(["openai/gpt-image-1", "openai/gpt-image-2", "openai/gpt-image-2.5-sunburst"]); const IMAGE_MODELS = [ "zai/cogview-4", @@ -171,7 +187,10 @@ const IMAGE_MODELS = [ "google/nano-banana-pro", "openai/gpt-image-1", "openai/gpt-image-2", + "openai/gpt-image-2.5-flare", + "openai/gpt-image-2.5-sunburst", "xai/grok-imagine-image", + "xai/grok-imagine-image-2.0", "xai/grok-imagine-image-pro", "bytedance/seedream-5-pro", ] as const; @@ -190,6 +209,8 @@ const IMAGE_MODELS = [ const LARGE_SIZE_THRESHOLD: Record = { "openai/gpt-image-1": 1024, "openai/gpt-image-2": 1024, + "openai/gpt-image-2.5-flare": 1024, + "openai/gpt-image-2.5-sunburst": 1024, "google/nano-banana-pro": 2048, // $0.107001 through 2048; $0.159500 at 4096 }; @@ -343,16 +364,19 @@ Actions: Generation models (1024x1024 base price; larger sizes cost more on gpt-image-*): - openai/gpt-image-2 ($0.06–0.12) — flagship, reasoning-driven, multilingual on-image text + character consistency (default) +- openai/gpt-image-2.5-sunburst ($0.28–0.56) — OpenAI's most capable image model, high-precision editing +- openai/gpt-image-2.5-flare ($0.28–0.56) — GPT Image 2.5 fast tier, same price as Sunburst (generate only) - openai/gpt-image-1 ($0.02–0.04) — GPT native image generation - google/nano-banana ($0.05) — Gemini-family image model - google/nano-banana-2 ($0.09) — Gemini 3.1 Flash, pro-level quality at Flash speed (1024x1024 only) - google/nano-banana-pro ($0.10; $0.15 at 4096px) — up to 4K, strongest photorealism - xai/grok-imagine-image ($0.02) — stylized, fast -- xai/grok-imagine-image-pro ($0.07) — higher quality Grok Imagine +- xai/grok-imagine-image-2.0 ($0.04) — Grok Imagine 2.0, between Grok Imagine and Pro on price +- xai/grok-imagine-image-pro ($0.05) — higher quality Grok Imagine - zai/cogview-4 ($0.015) — cheapest, photorealistic detailed scenes - bytedance/seedream-5-pro ($0.045; $0.09 when both dimensions exceed 1024) — Seedream 5.0 Pro; cheap widescreen at 1280x720 / 2048x1024, large formats up to 2848x1600 -Edit (img2img) models: openai/gpt-image-2 (default), openai/gpt-image-1, google/nano-banana, google/nano-banana-2, google/nano-banana-pro +Edit (img2img) models: openai/gpt-image-2 (default), openai/gpt-image-2.5-sunburst, openai/gpt-image-1, google/nano-banana, google/nano-banana-2, google/nano-banana-pro Multi-image edit: pass an array of 2–4 source images to "image" to fuse them in one render (openai/* up to 4, google/* up to 3) — e.g. a subject plus a sprite layout guide, or a reference plus a brand logo. Source images and masks accept a base64 data URI, an http(s) URL, or a local file path (auto-encoded). Inpaint mask (openai/gpt-image-* only) via "mask"; not combinable with multiple source images.`, annotations: TOOL_ANNOTATIONS.generative, diff --git a/src/tools/video.ts b/src/tools/video.ts index 97a3d76..eb3f278 100644 --- a/src/tools/video.ts +++ b/src/tools/video.ts @@ -108,10 +108,16 @@ const VIDEO_BASE_PRICE_PER_SECOND: Record = { // $0.7885. Forwarding the parameter without splitting the rate would have // under-reserved 720p by 1.33x — the parameter and the price had to move // together, which is why this is one change and not two. -const GROK_VIDEO_MODEL = "xai/grok-imagine-video"; -const GROK_PRICE_PER_SECOND: Record = { - "480p": 0.05, - "720p": 0.07, +// +// xai/grok-imagine-video-1.5 (added 2026-10-20) follows the same official- +// parity rule — xAI's per-second rate, no margin, plus the flat $0.001 +// per-call surcharge the transaction-fee reserve already covers — at +// $0.08 / $0.14 / $0.25 per second for 480p / 720p / 1080p (gateway registry +// pricePerSecondByResolution; GET /v1/models lists the 480p $0.08 default). +// Unlike 1.0 it has a real 1080p tier. +const GROK_PRICE_PER_SECOND: Record> = { + "xai/grok-imagine-video": { "480p": 0.05, "720p": 0.07 }, + "xai/grok-imagine-video-1.5": { "480p": 0.08, "720p": 0.14, "1080p": 0.25 }, }; // Token cost scales with output resolution; 21,690 tok/sec is the 720p figure. @@ -219,6 +225,7 @@ export function referenceCeilingSeconds(model: string): number | undefined { const VIDEO_DEFAULT_DURATION: Record = { "xai/grok-imagine-video": 8, + "xai/grok-imagine-video-1.5": 8, "bytedance/seedance-1.5-pro": 5, "bytedance/seedance-2.0-fast": 5, "bytedance/seedance-2.0": 5, @@ -240,6 +247,7 @@ const VIDEO_DEFAULT_DURATION: Record = { // trip into an immediate, specific message. Source: models.ts min/max/allowed. const VIDEO_DURATION_RANGE: Record = { "xai/grok-imagine-video": { min: 1, max: 15 }, + "xai/grok-imagine-video-1.5": { min: 1, max: 15 }, "bytedance/seedance-1.5-pro": { min: 4, max: 12 }, "bytedance/seedance-2.0-mini": { min: 4, max: 15 }, "bytedance/seedance-2.0-fast": { min: 4, max: 15 }, @@ -296,9 +304,13 @@ export const SEEDANCE_RESOLUTIONS: Record; no * immediate, specific message than a round trip that returns nothing useful. */ const GROK_RESOLUTIONS: Record; note: string }> = { - [GROK_VIDEO_MODEL]: { + "xai/grok-imagine-video": { resolutions: new Set(["480p", "720p"]), - note: "grok-imagine-video renders 480p (default, ~$0.05/sec) or 720p (~$0.07/sec) — for 1080p or 4K use a Seedance model", + note: "grok-imagine-video renders 480p (default, ~$0.05/sec) or 720p (~$0.07/sec) — for 1080p use grok-imagine-video-1.5 or a Seedance model; for 4K, seedance-2.0", + }, + "xai/grok-imagine-video-1.5": { + resolutions: new Set(["480p", "720p", "1080p"]), + note: "grok-imagine-video-1.5 renders 480p (default, ~$0.08/sec), 720p (~$0.14/sec) or 1080p (~$0.25/sec) — for 4K use bytedance/seedance-2.0", }, }; @@ -366,14 +378,16 @@ export function estimateVideoCost(model: string, durationSeconds?: number, resol return withTxFee((tokens * SEEDANCE_PRICE_PER_MTOKENS[model] / 1_000_000) * VIDEO_MARGIN); } - if (model === GROK_VIDEO_MODEL) { + if (Object.hasOwn(GROK_PRICE_PER_SECOND, model)) { // Defaults to 480p — the tier the gateway renders when no resolution is - // sent, confirmed by the default quote matching the explicit 480p one. + // sent, confirmed by the default quote matching the explicit 480p one + // (both Grok models share defaultResolution "480p" in the registry). const res = resolution ?? "480p"; - if (!Object.hasOwn(GROK_PRICE_PER_SECOND, res)) { - throw new Error(`grok-imagine-video does not render "${res}" — refusing to reserve at the 480p rate.`); + const rates = GROK_PRICE_PER_SECOND[model]; + if (!Object.hasOwn(rates, res)) { + throw new Error(`${model.slice(model.indexOf("/") + 1)} does not render "${res}" — refusing to reserve at the 480p rate.`); } - return withTxFee(GROK_PRICE_PER_SECOND[res] * seconds); // no margin, see above + return withTxFee(rates[res] * seconds); // no margin, see above } if (!Object.hasOwn(VIDEO_BASE_PRICE_PER_SECOND, model)) { @@ -433,6 +447,7 @@ Turns a text prompt (and optional seed image) into a short video clip (MP4, or M Models. Every rate below is what you are CHARGED (margin and transaction fee included), at the 720p baseline Seedance renders by default with synced audio: - azure/sora-2 (~$0.105/sec, 720p + synced audio, text- or image-to-video) — OpenAI Sora 2 via Azure AI Foundry. duration_seconds must be 4, 8, or 12 (4s default -> ~$0.42/clip). image_url takes a NON-HUMAN reference image (faces are rejected upstream by moderation — use Seedance + RealFace for real people); same price as text-to-video. No RealFace, no last_frame_url. Base only for now: the Solana gateway quotes it as Seedance 2.0 at $1.135 and the tool refuses that quote unsigned. - xai/grok-imagine-video ($0.05/sec at 480p default, $0.07/sec at 720p; 8s default -> $0.401/clip, 1-15s) — stylized, fast. 480p/720p only. +- xai/grok-imagine-video-1.5 ($0.08/sec at 480p default, $0.14/sec at 720p, $0.25/sec at 1080p; 8s default -> ~$0.64/clip, 1-15s) — xAI's flagship video model with native synced audio; text- or image-to-video. No 4K. - bytedance/seedance-1.5-pro (~$0.071/sec, 4-12s, 5s default -> ~$0.35/clip) — cheapest Seedance, token-priced upstream - bytedance/seedance-2.0-mini (~$0.080/sec, 4-15s, 5s default) — 2.0-generation quality at roughly half the 2.0-fast rate; 720p ceiling; supports RealFace, first/last-frame and the full reference set. The CHEAPEST model that takes reference video/audio — prefer it over 2.0 for reference work unless you need 4K. - bytedance/seedance-2.0-fast (~$0.165/sec, 4-15s, ~60-80s gen) — sweet-spot price/quality; supports RealFace, first/last-frame and the full reference set @@ -451,12 +466,12 @@ Returns a permanent blockrun-hosted video URL (the gateway mirrors the asset to prompt: z.string().describe("Text description of the video to generate. E.g. 'a red apple slowly spinning on a wooden table', 'a hummingbird hovering near a red flower, ultra slow motion'"), image_url: z.string().url().optional().describe("Optional seed image URL for image-to-video generation"), real_face_asset_id: z.string().regex(/^ta_[A-Za-z0-9]+$/, "token360 asset id like 'ta_xxxx'").optional().describe("BytePlus RealFace asset id (from blockrun_realface enroll/list) to generate video of a specific real person. Seedance 2.0 / 2.0-fast / 2.0-mini only (NOT 2.5). Mutually exclusive with image_url."), - duration_seconds: z.number().int().min(1).max(60).optional().describe("Duration to bill for. Defaults to the model's own default (8s xAI, 5s Seedance, 4s Sora). Per-model range: seedance-1.5-pro 4-12s · seedance-2.0 / 2.0-fast / 2.0-mini 4-15s · seedance-2.5 4-30s · sora-2 exactly 4, 8 or 12 · grok-imagine-video 1-15s."), + duration_seconds: z.number().int().min(1).max(60).optional().describe("Duration to bill for. Defaults to the model's own default (8s xAI, 5s Seedance, 4s Sora). Per-model range: seedance-1.5-pro 4-12s · seedance-2.0 / 2.0-fast / 2.0-mini 4-15s · seedance-2.5 4-30s · sora-2 exactly 4, 8 or 12 · grok-imagine-video / grok-imagine-video-1.5 1-15s."), generate_audio: z.boolean().optional().describe("Seedance only: whether to generate a synced audio track. Defaults ON for text-to-video and OFF for image/RealFace-conditioned. The auto-generated audio is occasionally rejected by upstream moderation ('output audio may contain sensitive information') even for benign prompts — pass false to skip audio and avoid that failure. Ignored by xAI/Sora."), - resolution: z.enum(["480p", "720p", "1080p", "4K"]).optional().describe("Output resolution. Seedance defaults to 720p and is token-priced (~2.25x at 1080p, ~9x at 4K); per-model sets from token360's published schema: seedance-2.0 480p/720p/1080p/4K · 1.5-pro 480p/720p/1080p · 2.0-fast, 2.0-mini and 2.5 480p/720p only. grok-imagine-video honours 480p (default, $0.05/sec) and 720p ($0.07/sec) and rejects anything higher. Ignored by Sora only (dropped from the request)."), + resolution: z.enum(["480p", "720p", "1080p", "4K"]).optional().describe("Output resolution. Seedance defaults to 720p and is token-priced (~2.25x at 1080p, ~9x at 4K); per-model sets from token360's published schema: seedance-2.0 480p/720p/1080p/4K · 1.5-pro 480p/720p/1080p · 2.0-fast, 2.0-mini and 2.5 480p/720p only. grok-imagine-video honours 480p (default, $0.05/sec) and 720p ($0.07/sec) and rejects anything higher; grok-imagine-video-1.5 honours 480p ($0.08/sec, default), 720p ($0.14/sec) and 1080p ($0.25/sec), not 4K. Ignored by Sora only (dropped from the request)."), aspect_ratio: z.enum(["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]).optional().describe("Output aspect ratio. Seedance honors the full set; Sora uses it only to pick portrait vs landscape (9:16 / 3:4 -> portrait); Grok ignores it (the gateway never forwards it to xAI). Defaults to the model's own default. (9:21 removed 2026-08-07 — no Seedance model offers it; use 9:16 for vertical.)"), last_frame_url: z.string().url().optional().describe("Seedance 1.5-pro / 2.0 / 2.0-fast / 2.0-mini / 2.5 (2.5 not on the Solana wallet rail): first-and-last-frame interpolation. A second image URL that seeds the FINAL frame so the model tweens from image_url (first frame) → last_frame_url (last frame). Requires image_url; mutually exclusive with real_face_asset_id."), - model: z.enum(["azure/sora-2", "xai/grok-imagine-video", "bytedance/seedance-1.5-pro", "bytedance/seedance-2.0-mini", "bytedance/seedance-2.0-fast", "bytedance/seedance-2.0", "bytedance/seedance-2.5"]).optional().default("xai/grok-imagine-video").describe("Video model to use"), + model: z.enum(["azure/sora-2", "xai/grok-imagine-video", "xai/grok-imagine-video-1.5", "bytedance/seedance-1.5-pro", "bytedance/seedance-2.0-mini", "bytedance/seedance-2.0-fast", "bytedance/seedance-2.0", "bytedance/seedance-2.5"]).optional().default("xai/grok-imagine-video").describe("Video model to use"), reference_image_urls: z.array(z.string().url().max(2048)).min(1).max(30).optional().describe("ACCOUNT RAIL ONLY (BLOCKRUN_API_KEY) — the Base and Solana gateways refuse reference media with a 400. Character/style reference images, cited as 'image 1', 'image 2' in the prompt: up to 9 on seedance-2.0 / 2.0-fast / 2.0-mini, 30 on 2.5. Mutually exclusive with image_url / last_frame_url / real_face_asset_id."), reference_videos: z.array(z.object({ url: z.string().url().max(2048) })).min(1).max(3).optional().describe("ACCOUNT RAIL ONLY. Motion reference clips (1-3) on the seedance-2.0 family and 2.5. Each clip is BILLED AT ITS MODEL'S CEILING (15.2s on the 2.0 family, 30.2s on 2.5) whatever its real length, so one clip makes a 5s 720p render cost about 4x (seedance-2.0-mini ~$0.40 -> ~$1.61), ~7x on 2.5, more at 480p."), reference_audios: z.array(z.object({ url: z.string().url().max(2048) })).min(1).max(3).optional().describe("ACCOUNT RAIL ONLY. Audio reference clips (1-3), same models. Requires a reference image or video alongside. Billed at 0.3x the video-clip ceiling (15.2s on the 2.0 family, 30.2s on 2.5)."), diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3143d7c..113f30a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -14,8 +14,10 @@ export const BASE_RPC_URLS = [ "https://1rpc.io/base", ]; -// Model catalogue, refreshed against the live GET /v1/models on 2026-08-12 -// (91 models on BOTH chains; free tier re-probed the same day — see free[]). +// Model catalogue, refreshed against the live GET /v1/models on 2026-10-20 +// (88 chat models on BOTH chains, identical $/M input/output; Base also lists +// azure/sora-2). Free tier re-probed the same day — see free[]. Earlier +// refreshes: 2026-08-12, 2026-09-08. // Prices below are $/M input / $/M output as the BASE gateway quotes them. // Known chain drift on 2026-08-12, confirmed against both gateways' source: // sol still quotes deepseek-chat/reasoner at $0.2/$0.4 (Base took DeepSeek's @@ -38,7 +40,8 @@ export const BASE_RPC_URLS = [ // presence in the catalogue is NECESSARY BUT NOT SUFFICIENT for health; // absence from it is NOT SUFFICIENT for death — probe before deleting. // -// OpenAI (31): gpt-6-astra ($10/$50, 1M — the GPT-6 flagship; listed by +// OpenAI (32): gpt-6.1-sol ($2/$10, 1M — refreshed GPT-6 Sol, same 272K step +// as gpt-6-sol: $4/$15), gpt-6-astra ($10/$50, 1M — the GPT-6 flagship; listed by // 2026-09-08 ABOVE the $5/$30 default, so it carries a CHAT_PRICE_PER_MTOKEN // row), gpt-6-sol ($2/$10, 1M — the cost-efficient GPT-6 tier), gpt-6-luna // ($0.1/$0.5, 1M — fast, low-cost GPT-6); all three GPT-6 bill input 2x and @@ -46,13 +49,13 @@ export const BASE_RPC_URLS = [ // reasoning; CUT from $5/$30 on 2026-08-21), gpt-5.6-terra // ($2/$12, 1M — the balanced default; CUT from $2.5/$15), gpt-5.6-luna // ($0.2/$1.2, 1M, no reasoning; CUT from $1/$6), plus the 2026-08 "pro -// reasoning mode" trio: gpt-5.6-sol-pro ($4/$20), gpt-5.6-terra-pro ($1/$6 — -// deliberately HALF the standard Terra rate, per the gateway registry, not a -// pricing bug), gpt-5.6-luna-pro ($0.1/$0.6). gpt-5.5 ($5/$30), gpt-5.5-pro +// reasoning mode" trio, now billed at the standard rates of their base +// models (2026-10-20 catalogue): gpt-5.6-sol-pro ($4/$20), gpt-5.6-terra-pro +// ($2/$12 — no longer the half rate it launched at), gpt-5.6-luna-pro ($0.2/$1.2). gpt-5.5 ($5/$30), gpt-5.5-pro // ($30/$180 — ties gpt-5.4-pro for priciest model served), chat-latest // (rolling ChatGPT alias, currently GPT-5.5 Instant, 128K, $5/$30), -// gpt-5.4 ($2.5/$15), gpt-5.4-pro ($30/$180), gpt-5.3 / gpt-5.2 / -// gpt-5.3-codex ($1.75/$14), gpt-5.2-pro ($21/$168), gpt-5.4-mini, +// gpt-5.4 ($2.5/$15), gpt-5.4-pro ($30/$180), gpt-5.2 / +// gpt-5.3-codex ($1.75/$14) (gpt-5.3 is hidden + available:false), gpt-5.2-pro ($21/$168), gpt-5.4-mini, // gpt-5-mini, gpt-5.4-nano, gpt-4.1{,-mini,-nano}, gpt-4o{,-mini}, // o1 ($15/$60), o3 ($2/$8), o3-mini, o4-mini // Anthropic (12): claude-fable-5.1 ($10/$50, 1M — listed by 2026-09-08 ABOVE @@ -65,18 +68,19 @@ export const BASE_RPC_URLS = [ // $3/$15; both gateways 2026-09-08. On the native path the price row IS the // ledger, so the stale row over-booked every sonnet-5 call 1.5x), // claude-opus-4.5, claude-sonnet-4.6, claude-sonnet-4.5, claude-haiku-4.5 -// Google (9): gemini-3.1-pro ($2/$12), gemini-3.6-flash ($1.5/$7.5, thinking — -// newest Flash), gemini-3.5-flash ($1.5/$9 — REPRICED from the $0.5/$3 this +// Google (10): gemini-3.1-pro ($2/$12), gemini-3.8-flash and gemini-3.6-flash +// ($0.75/$3.75 each — 3.6 was $1.5/$7.5 when first listed), gemini-3.5-flash ($1.5/$9 — REPRICED from the $0.5/$3 this // file recorded in July), gemini-3-flash-preview ($0.5/$3), // gemini-3.5-flash-lite ($0.3/$2.5, live-probed 2026-08-12), // gemini-3.1-flash-lite ($0.25/$1.5), gemini-2.5-pro, gemini-2.5-flash, // gemini-2.5-flash-lite — all 1M context -// DeepSeek (3): deepseek-v4-pro ($1.32/$3.96, 1M, reasoning + coding; +// DeepSeek (4): deepseek-v4-flash-vision-exp ($0.3/$1.2), deepseek-v4-pro ($1.32/$3.96, 1M, reasoning + coding; // repriced by the gateway and re-probed 2026-09-02), // deepseek-chat, deepseek-reasoner ($0.14/$0.28 on Base since DeepSeek's // 2026-08-07 cut; sol still quotes $0.2/$0.4 — see the drift note above) // Moonshot (1): kimi-k3 ($3/$15, 1M, vision + reasoning + coding) -// ZAI (4): glm-5.2 ($1.4/$4.4, 1M), glm-5.1 ($1.4/$4.4), glm-5 ($1/$3.2 — +// ZAI (6): glm-5.3 ($1.4/$4.4), glm-5.3-flash ($0.15/$0.5, vision), +// glm-5.2 ($1.4/$4.4, 1M), glm-5.1 ($1.4/$4.4), glm-5 ($1/$3.2 — // Z.AI RAISED the list rate; still the cheapest ZAI, but no longer a // cheap-tier pick), glm-5-turbo ($1.2/$4) // xAI (5): grok-4.7 ($2/$6, 500K, vision, always-on reasoning), grok-4.6 @@ -84,12 +88,20 @@ export const BASE_RPC_URLS = [ // native Live Search; grok-4.3 ($1.25/$2.5, 1M), // grok-build-0.1 ($1/$2, coding) // MiniMax (2): minimax-m3 ($0.3/$1.2, 1M), minimax-m2.7 ($0.3/$1.2, 200K) -// Qwen (3): qwen3.7-max ($1.475/$4.425, 1M), qwen3.7-plus ($0.32/$1.28, 1M), +// Qwen (5): qwen3.8-max ($2/$6), qwen3.8-flash ($0.15/$0.47, vision), +// qwen3.7-max ($1.475/$4.425, 1M), qwen3.7-plus ($0.32/$1.28, 1M), // qwen3.7-flash ($0.03/$0.13, 1M — the cheapest PAID model in the catalogue; // live-probed 2026-08-12) -// Tencent: hy3 RETIRED 2026-09-24 (hidden; the gateway redirects pins to -// qwen3.7-flash, already second in cheap[]). Price row kept for pinned callers. -// Xiaomi (1): mimo-v2.5-pro ($0.435/$0.87, 1M reasoning) +// Tencent (1): hy4-preview ($0.834/$2.501). hy3 RETIRED 2026-09-24 (hidden; +// the wallet gateways redirect pins to qwen3.7-flash). Its price row was +// dropped 2026-10-20: it had gone stale against the account sheet +// ($0.0825/$0.33), and a row at either rate would under-cover a huge prompt +// on the wallet rails, where the redirect target's own long-context steps +// reach $0.2/$0.8. Unlisted, a pinned hy3 reserves the $5/$30 default, +// which covers every rail. +// Xiaomi (2): mimo-v2.5 ($0.14/$0.28, vision), mimo-v2.5-pro ($0.435/$0.87) +// Mistral (1): mistral-large-4 ($0.68/$2.09, vision) +// Poolside: laguna-s-2.1 ($0.09/$0.18, paid); laguna-xs-2.1 is the $0 one // NVIDIA — genuinely $0. Serving healthy on the 2026-08-12 probe (both chains, // realistic ~1.4K-token prompt): nemotron-3-nano-omni-30b-a3b-reasoning // (vision, ~1s), step-3.7-flash, mistral-nemotron (24.9s on sol — slowest @@ -196,9 +208,27 @@ export const MODEL_TIERS = { // sol), muse-glimmer-30b and gemma-4-31b (400 "Unknown model" on Base; // gemma crawled 79.8s on sol) — routing has to hold on both chains. They are // still in FREE_CHAT_MODELS, so an explicit call to one reserves $0. + // + // 2026-10-20 SWEEP — same probe (~3,000 chars, max_tokens 32, no payment + // header, both gateways, response `model` read). All fourteen $0 ids still + // answer 200 unpaid on both chains, so none is dead in the money sense; but + // most now answer as nemotron-3-super-120b, the gateway's MODEL_REDIRECTS / + // fallback target. Two probes that day; self-serving: + // gpt-oss-20b itself on both, both probes (0.8-1.1s) + // nemotron-3-super-120b itself on both, both probes (1.0-1.2s) — hidden + // from GET /v1/models but it IS the backend every + // redirect lands on, so it joins the tier + // llama-3.2-11b-vision itself on both, both probes (1.1-6.6s) + // nemotron-3-nano-omni itself on Base in the first probe (3.3s), aliased + // on both in the second — tail rung only, kept + // because it is the other free vision model + // muse-glimmer-30b itself on sol only; nemotron-3.5-lightning sol only + // north-mini-code and laguna-xs-2.1 now answer as nemotron-3-super-120b on + // BOTH chains (and are available:false in the Base catalogue), so by this + // list's rule they leave the tier and stay in FREE_CHAT_MODELS. free: [ - "nvidia/gpt-oss-20b", "cohere/north-mini-code", "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - "poolside/laguna-xs-2.1", "nvidia/llama-3.2-11b-vision", + "nvidia/gpt-oss-20b", "nvidia/nemotron-3-super-120b", "nvidia/llama-3.2-11b-vision", + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", ], coding: ["anthropic/claude-opus-5", "openai/gpt-5.3-codex", "moonshot/kimi-k3", "xai/grok-build-0.1", "zai/glm-5.2", "qwen/qwen3.7-max", "anthropic/claude-sonnet-5"], glm: ["zai/glm-5", "zai/glm-5.2", "zai/glm-5.1", "zai/glm-5-turbo"], @@ -226,7 +256,7 @@ export type RoutingMode = keyof typeof MODEL_TIERS; * promise "still free when it returns". * * What that check cannot cover, and does not pretend to: a member the catalogue - * does not list at all. Six of these are in that state on both gateways today, + * does not list at all. Eight of these are in that state on both gateways today, * and delisting is not death — gpt-oss-120b is the gateway's own free fallback * and answers for itself while absent from GET /v1/models. The sweep names each * one as UNVERIFIED rather than counting it as checked; the only way to settle @@ -238,7 +268,14 @@ export type RoutingMode = keyof typeof MODEL_TIERS; */ export const FREE_CHAT_MODELS: ReadonlySet = new Set([ ...MODEL_TIERS.free, - // Still $0 on both gateways (200 without a payment header, 2026-09-13) but + // billing_mode:"free" in GET /v1/models (2026-10-20) but answering as + // nemotron-3-super-120b on both chains — out of the tier, still $0. + "cohere/north-mini-code", + "poolside/laguna-xs-2.1", + // Still $0 on both gateways (200 without a payment header, 2026-09-13 and + // again 2026-10-20 — every one now answers as nemotron-3-super-120b, or + // llama-3.2-11b-vision for the two nano-* ids on Base, via the gateway's + // MODEL_REDIRECTS, all of whose free targets are $0) but // answering as ANOTHER model — pulled from the routing tier, kept here so an // explicit call reserves $0. See the free[] sweep note. "nvidia/gpt-oss-120b", @@ -247,10 +284,10 @@ export const FREE_CHAT_MODELS: ReadonlySet = new Set([ "nvidia/mistral-nemotron", "nvidia/nemotron-nano-12b-v2-vl", "nvidia/nemotron-nano-9b-v2", - // Live billing_mode:"free" on 2026-09-08 but deliberately not routed. - "nvidia/nemotron-3.5-lightning", // 429 capacity on Base, no answer on sol (09-13) - "nvidia/muse-glimmer-30b", // Solana catalogue only (400 on Base) - "nvidia/gemma-4-31b", // Solana catalogue only (400 on Base; 79.8s crawl on sol) + // Live billing_mode:"free" but deliberately not routed (2026-10-20 probe). + "nvidia/nemotron-3.5-lightning", // itself on sol only; super-120b on Base + "nvidia/muse-glimmer-30b", // now listed on both; itself on sol only + "nvidia/gemma-4-31b", // delisted on both; redirected to super-120b (gateway MODEL_REDIRECTS) ]); /** @@ -258,7 +295,10 @@ export const FREE_CHAT_MODELS: ReadonlySet = new Set([ * catalog model priced ABOVE the DEFAULT_CHAT_PRICE an explicit `model` falls * back to. Read off the live GET /v1/models on 2026-08-13; gpt-6-astra and * claude-fable-5.1 added from the 2026-09-08 catalogue, after both had sat - * above the default with no row for weeks (see the block comment below). + * above the default with no row for weeks (see the block comment below). Since + * 2026-10-20 it also carries every other live, paid chat id in the catalogue, + * so the table is the whole priced catalogue plus the hidden-but-served + * claude-opus-4. * * This exists because the budget gate used to reserve chat against two hardcoded * constants — "$5/M input" and "4 chars per token" — and BOTH were wrong at the @@ -368,7 +408,45 @@ export const CHAT_PRICE_PER_MTOKEN: Record export const LONG_CONTEXT_PRICE_PER_MTOKEN: Record = { // OpenAI GPT-6 and GPT-5.4+: 2x input / 1.5x output above 272K. "openai/gpt-6-astra": OPENAI_272K(20, 75), + "openai/gpt-6.1-sol": OPENAI_272K(4, 15), "openai/gpt-6-sol": OPENAI_272K(4, 15), "openai/gpt-6-luna": OPENAI_272K(0.2, 0.75), "openai/gpt-5.6-sol": OPENAI_272K(8, 30), diff --git a/test/chat-wallet-after-payment.test.ts b/test/chat-wallet-after-payment.test.ts index 279805e..e39c121 100644 --- a/test/chat-wallet-after-payment.test.ts +++ b/test/chat-wallet-after-payment.test.ts @@ -25,6 +25,7 @@ import { test, mock } from "node:test"; import assert from "node:assert/strict"; import type { BudgetState } from "../src/types.js"; +import { MODEL_TIERS } from "../src/utils/constants.js"; type Fail = false | "unpaid-400" | "after-payment-524" | "after-payment-400" | "abort" | "midstream"; type Attempt = { settleUsd: number; fail: Fail; served?: string; finish?: string; text?: string }; @@ -203,7 +204,7 @@ for (const c of ["solana", "base"] as const) { test(`${c}: a free model that aborts is never "may have settled" — the free tier walks on`, async () => { chain = c; - const free = ["nvidia/gpt-oss-20b", "cohere/north-mini-code", "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"]; + const free = MODEL_TIERS.free; script = new Map([[free[0], { settleUsd: 0, fail: "abort" }], [free[1], { settleUsd: 0, fail: "after-payment-524" }]]); attempts = []; const { budget, call } = makeHarness(); diff --git a/test/chat.test.ts b/test/chat.test.ts index 9a11ee3..9779227 100644 --- a/test/chat.test.ts +++ b/test/chat.test.ts @@ -498,15 +498,20 @@ test("claude-sonnet-5 books at the gateway's $2/$10, not the old $3/$15", () => // has capacity), which is exactly why the tier keys on ids that echo their // own name: those are distinct backends. The pruned ids stay in // FREE_CHAT_MODELS so an explicit call still reserves $0 (D56). -test("free[] is the self-serving, both-chain set from the 2026-09-13 probe, fastest first", () => { +// 2026-10-20 re-probe (same shape): every $0 id still answers 200 unpaid on +// both chains, but north-mini-code and laguna-xs-2.1 now answer as +// nemotron-3-super-120b on BOTH — out of the tier, still in FREE_CHAT_MODELS. +test("free[] is the self-serving set from the 2026-10-20 probe, fastest first", () => { assert.deepEqual([...MODEL_TIERS.free], [ - "nvidia/gpt-oss-20b", // itself, 0.7s sol / 1.7s base - "cohere/north-mini-code", // itself, 1.0s / 3.0s - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", // itself (…-nim on sol), 2.3s / 3.6s - "poolside/laguna-xs-2.1", // itself on sol 0.7s; 429 capacity on base that day - "nvidia/llama-3.2-11b-vision", // itself on sol 9.8s; no answer in 90s on base — last + "nvidia/gpt-oss-20b", // itself on both, 0.8-1.1s + "nvidia/nemotron-3-super-120b", // itself on both, 1.0-1.2s (the redirect target) + "nvidia/llama-3.2-11b-vision", // itself on both, 1.1-6.6s + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", // itself on base once, aliased in the re-probe — last ]); - for (const aliased of ["nvidia/gpt-oss-120b", "nvidia/nemotron-3-ultra-550b", "nvidia/step-3.7-flash", "nvidia/mistral-nemotron", "nvidia/nemotron-nano-12b-v2-vl", "nvidia/nemotron-nano-9b-v2"]) { + for (const aliased of [ + "nvidia/gpt-oss-120b", "nvidia/nemotron-3-ultra-550b", "nvidia/step-3.7-flash", "nvidia/mistral-nemotron", + "nvidia/nemotron-nano-12b-v2-vl", "nvidia/nemotron-nano-9b-v2", "cohere/north-mini-code", "poolside/laguna-xs-2.1", + ]) { assert.ok(FREE_CHAT_MODELS.has(aliased), `${aliased} is still $0 — an explicit call must reserve nothing`); assert.ok(!(MODEL_TIERS.free as readonly string[]).includes(aliased), `${aliased} answers as another model on both chains — not a fallback rung`); } diff --git a/test/image.test.ts b/test/image.test.ts index da6b9e7..793b4a9 100644 --- a/test/image.test.ts +++ b/test/image.test.ts @@ -5,7 +5,7 @@ import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { readFileSync } from "node:fs"; -import { toImageDataUri, buildSolanaImageRequest, materializeImageUrl } from "../src/tools/image.js"; +import { toImageDataUri, buildSolanaImageRequest, materializeImageUrl, estimateCost } from "../src/tools/image.js"; const tmp = mkdtempSync(join(tmpdir(), "blockrun-img-")); @@ -127,3 +127,24 @@ test("materializeImageUrl maps image/jpeg data URIs to a .jpg file", async () => const out = await materializeImageUrl(`data:image/jpeg;base64,${PNG_BYTES.toString("base64")}`); assert.match(out, /\.jpg$/); }); + +// Live Base 402 quotes, unpaid probes 2026-10-20 (npm run verify:prices). The +// reserve keeps the repo-wide $0.002 fee above the gateway's current $0.001, +// so each sits one fee-gap above the charge — never below it. Pro was reserved +// at $0.07 (0.0755) until this refresh: 41% over the live charge. +const IMAGE_LIVE: Array<[string, string, number]> = [ + ["openai/gpt-image-2.5-flare", "1024x1024", 0.295001], + ["openai/gpt-image-2.5-flare", "1536x1024", 0.589001], + ["openai/gpt-image-2.5-sunburst", "1024x1024", 0.295001], + ["openai/gpt-image-2.5-sunburst", "1024x1536", 0.589001], + ["xai/grok-imagine-image-2.0", "1024x1024", 0.043], + ["xai/grok-imagine-image-pro", "1024x1024", 0.053501], +]; + +test("estimateCost covers the 2026-10-20 image rows without over-reserving", () => { + for (const [model, size, charged] of IMAGE_LIVE) { + const reserved = estimateCost(model, size); + assert.ok(reserved >= charged, `${model} ${size}: ${reserved} < ${charged}`); + assert.ok(reserved <= charged + 0.0011, `${model} ${size}: ${reserved} too far above ${charged}`); + } +}); diff --git a/test/long-context.test.ts b/test/long-context.test.ts index 35268ac..c28a5c3 100644 --- a/test/long-context.test.ts +++ b/test/long-context.test.ts @@ -82,14 +82,15 @@ test("a multi-step ladder prices at the highest step reached (qwen3.7-flash: 32K }); test("a step never lowers the reserve below the base it would otherwise use", () => { - // gpt-5.4 has no CHAT_PRICE_PER_MTOKEN row, so it reserves the $5/$30 default; - // its published >272K output rate ($22.50) is below that and must not win. + // Against the $5/$30 default base (what an id with no CHAT_PRICE_PER_MTOKEN + // row reserves), gpt-5.4's published >272K output rate ($22.50) is below the + // base and must not win. const rate = chatRateAt("openai/gpt-5.4", DEFAULT_CHAT_PRICE, 300_000); assert.deepEqual(rate, { input: DEFAULT_CHAT_PRICE.input, output: DEFAULT_CHAT_PRICE.output }); - assert.ok( - estimateChatCost(MAX_TOKENS, undefined, "gpt-5.4", undefined, charsFor(300_000)) >= - estimateChatCost(MAX_TOKENS, undefined, "someone/unknown", undefined, charsFor(300_000)), - ); + // Component-wise: a base above the step on one axis keeps that axis. + assert.deepEqual(chatRateAt("openai/gpt-5.4", { input: 6, output: 10 }, 300_000), { input: 6, output: 22.5 }); + // With its own $2.5/$15 row (2026-10-20), gpt-5.4 reserves the step at 300K. + assert.deepEqual(chatRateAt("openai/gpt-5.4", CHAT_PRICE_PER_MTOKEN["openai/gpt-5.4"], 300_000), { input: 5, output: 22.5 }); }); test("bare ids pick up the step too", () => { diff --git a/test/video-models.test.ts b/test/video-models.test.ts index 83da678..d5b405a 100644 --- a/test/video-models.test.ts +++ b/test/video-models.test.ts @@ -450,6 +450,31 @@ test("estimateVideoCost covers grok at both resolutions without over-reserving", } }); +// grok-imagine-video-1.5, live Base 402s 2026-10-20 (unpaid): per-second rate +// x duration + $0.001 surcharge + $0.001 fee, no margin — the estimator lands +// on the charge exactly. +const GROK15_LIVE: Array<[number | undefined, string | undefined, number]> = [ + [undefined, undefined, 0.642], + [undefined, "720p", 1.122], + [undefined, "1080p", 2.002], + [15, "1080p", 3.752], +]; + +test("estimateVideoCost covers grok-imagine-video-1.5 at every tier it renders", () => { + for (const [seconds, resolution, charged] of GROK15_LIVE) { + const reserved = estimateVideoCost("xai/grok-imagine-video-1.5", seconds, resolution); + assert.ok(reserved >= charged, `grok-1.5 ${seconds ?? "default"}s ${resolution ?? "default"}: ${reserved} < ${charged}`); + assert.ok(reserved <= charged + 0.003, `grok-1.5 ${seconds ?? "default"}s ${resolution ?? "default"}: ${reserved} too far above ${charged}`); + } + assert.throws(() => estimateVideoCost("xai/grok-imagine-video-1.5", 8, "4K"), /does not render/); +}); + +test("grok-imagine-video-1.5 takes 1080p but refuses 4K before paying", async () => { + assert.match(await errorText({ prompt: "a cube", model: "xai/grok-imagine-video-1.5", resolution: "4K" }), /does not render/); + const body = await bodySentFor({ prompt: "a cube", model: "xai/grok-imagine-video-1.5", resolution: "1080p" }); + assert.equal(body.resolution, "1080p"); +}); + test("grok refuses the resolutions the gateway rejects, before paying", async () => { for (const resolution of ["1080p", "4K"]) { const text = await errorText({ prompt: "a cube", model: "xai/grok-imagine-video", resolution });