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
-
+
@@ -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 });