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
75 changes: 75 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,81 @@

All notable changes to BlockRun MCP will be documented in this file.

## 0.39.0

The Seedance capability tables, re-derived from token360's OWN published
parameter schema (GET /v1/models/{id} → parameter_schema) — a source nobody
had ever read; every table in every repo predated knowing it existed. Sibling
of blockrun PR #353 and blockrun-sol PR #133, which land the same truth in the
gateways, so client and server can no longer disagree.

- **`fix(video)` — per-model resolution sets tightened to the schema.** 0.38.1
(yesterday) tightened toward the gateway's tables; live probing shows those
tables were themselves wrong, in both directions:

| model | 0.38.1 allowed | schema truth |
|---|---|---|
| 1.5-pro | 360p-1K | **480p / 720p / 1080p** |
| 2.0-fast | 360p-1K | **480p / 720p** |
| 2.0 | t2v narrow · i2v 360p-1K+4K | **480p / 720p / 1080p / 4K, both modes** |
| 2.5 | 360p-720p | **480p / 720p** |

The t2v/i2v split is gone — the schema is per-model, everything it lists
passes in both modes, and 540p/1K hard-reject upstream even image-conditioned
(the pay-then-lose path the 0.38.1 guard half-closed). A few off-schema
values do pass upstream validation (2.0-fast 1080p, 1.5-pro 540p) and stay
blocked anyway: "passes validation" is not "renders" — 1.5-pro historically
echoed 2K/4K, billed the requested tier, and rendered 720p.

- **`fix(video)` — `360p`/`540p`/`1K` leave the resolution enum, `9:21` leaves
the aspect-ratio enum.** No Seedance model lists any of them, and 9:21 is
rejected upstream for the whole family. Probed at $0: token360 validates
duration → resolution → ratio → other params, so a deliberately invalid
LATER parameter (ratio "9:99"; output_format "avi" when testing ratios)
guarantees rejection after the parameter under test is judged, and a failed
submit never creates a task or bills. RESOLUTION_TOKEN_FACTOR drops the same
tiers — the estimator throws on anything missing, so a stale entry is a loud
failure, not a silent 720p-baseline reserve.

- **`fix(video)` — the resolution guard's advice now names the real ceiling.**
2.0-fast is told 720p is its ceiling (0.38.1 wrongly offered it 1080p/1K —
the exact bill-high-render-low trap shape).

- **`test(video)` — the t2v/i2v split test became the no-split test**, and the
factor-coverage test iterates the trimmed enum. verify:prices drops the
2.0@360p tracking probe (inexpressible from this client now) and keeps
2.5@1080p: the gateway still quotes $3.55 for a render token360 refuses —
the defect blockrun PR #353 fixes — so that probe reports the moment the
fleet is consistent.

Breaking (deliberately — hence a minor bump, not a patch): a stale caller
sending `resolution:"360p"` or `aspect_ratio:"9:21"` now gets a zod enum
error instead of a paid round trip or an upstream 400. For Sora/Grok
callers, removed `resolution` values were accepted no-ops; `9:21` was NOT a
no-op on Sora — it selected portrait orientation — and `9:16` is the exact
replacement. One removed value did produce billed output before this
release — seedance-2.0 image-conditioned 360p passed every layer — but it
is exactly the off-schema shape that can silently downscale while billing
the requested tier, which is why the schema is the line. The
`aspect_ratio` description also stops claiming Sora/Grok both ignore it:
the handler forwards it for every model, SORA uses it to pick portrait vs
landscape, and Grok is the one that truly ignores it (the gateway never
forwards it to xAI).

303 tests pass; live gate: 0 under-reserving.

Also shipping in 0.39.0 (landed on main as #97, recorded here because
releases are cut from this file): the price sweep now probes BOTH gateways —
Base charges base + $0.001, Solana base + $0.000, and a Solana price DEARER
than Base fails the gate since an agent can switch chains mid-session;
`blockrun_defi`/`blockrun_modal` gained the Base-only chain guard six other
tools already had; eleven skills' flat prices were re-read off the live 402;
and dFlow left the catalog (all three routes 404 at the gateway). The
dual-gateway sweep also gives the 2.5@1080p fleet-consistency probe its
other half for free: Solana already 400s it (blockrun-sol#130's cap) while
Base still quotes it — one line of output now shows the split until
blockrun#353 deploys.

## 0.38.1

Review pass over 0.38.0. The model addition held up; the guards and the tests
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blockrun/mcp",
"version": "0.38.1",
"version": "0.39.0",
"mcpName": "io.github.BlockRunAI/blockrun-mcp",
"description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, and crypto data. Paid via x402 micropayments.",
"type": "module",
Expand Down
24 changes: 17 additions & 7 deletions scripts/verify-prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,24 @@ const PROBES: Probe[] = [
["bytedance/seedance-2.0", undefined, "4K"],
["bytedance/seedance-2.5", undefined, undefined],
["bytedance/seedance-2.5", 30, undefined],
// Combinations the client-side guards REFUSE, probed anyway because a guard
// is a claim about the gateway and this is the only thing that checks it.
// 2.5@1080p is the interesting one: the gateway still QUOTES it ($3.55) even
// though token360 rejects it at submit (probed 2026-08-07), so this line
// tracks a known gateway defect. The day it stops quoting, the gateway has
// been fixed and this probe will report `no 402` like 2.0@360p does.
// A combination the client-side guard REFUSES, probed anyway because a
// guard is a claim about the gateway and this is the only thing checking
// it: the gateway still QUOTES 2.5@1080p ($3.55) even though token360
// rejects it at submit (probed 2026-08-07) — a known gateway defect, fixed
// by blockrun PR #353. Once that deploys, this probe reports `no 402` and
// the guard is vindicated. (2.0@360p left the matrix: 360p is out of the
// schema for every model and out of this client's enum, so the estimator
// now throws on it — it can no longer even be expressed from here.)
["bytedance/seedance-2.5", undefined, "1080p"],
["bytedance/seedance-2.0", undefined, "360p"],
// Each model probed at (or near) its ceiling and floor tier, so a gateway
// whose capability surface diverges from SEEDANCE_RESOLUTIONS — in either
// direction — shows up here as `no 402` or a price mismatch. 1.5-pro@1080p
// was the only above-720p schema tier with no live pin anywhere until the
// 0.39.0 review probed it: charge $0.797311 vs reserve $0.798311.
["bytedance/seedance-1.5-pro", undefined, "1080p"],
["bytedance/seedance-2.0-fast", undefined, "480p"],
["bytedance/seedance-2.5", undefined, "480p"],
["bytedance/seedance-2.0", undefined, "480p"],
] as Array<[string, number | undefined, string | undefined]>).map(([model, seconds, resolution]) => ({
label: `video ${model.split("/")[1]}${seconds ? ` ${seconds}s` : ""}${resolution ? ` ${resolution}` : ""}`,
path: "videos/generations",
Expand Down
87 changes: 43 additions & 44 deletions src/tools/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,12 @@ const VIDEO_BASE_PRICE_PER_SECOND: Record<string, number> = {
// is 0.3, not 0.25), and the live 402 agrees with the table, not the geometry.
// So a new tier must be read off a real quote (`npm run verify:prices`), never
// derived. Sora/Grok bill per second and ignore this entirely.
// 360p/540p/1K trimmed 2026-08-07: token360's schema lists them for no model,
// so they left the enum too; estimateVideoCost throws on anything missing here.
const RESOLUTION_TOKEN_FACTOR: Record<string, number> = {
"360p": 0.3,
"480p": 0.5,
"540p": 0.7,
"720p": 1,
"1080p": 2.25,
"1K": 2.25,
"4K": 9,
};

Expand Down Expand Up @@ -113,37 +112,41 @@ const VIDEO_DURATION_RANGE: Record<string, { min: number; max: number; allowed?:
"azure/sora-2": { min: 4, max: 12, allowed: [4, 8, 12] },
};

// Which resolutions each Seedance SKU actually accepts. Mirrors the gateway's
// checkUnsupportedVideoInput (blockrun/src/lib/video-input.ts), which is the
// authority — it rejects PRE-402, so a mismatch here costs a wasted round trip
// rather than money. The asymmetry that makes it worth gating anyway: where the
// gateway DOES quote, token360 bills the tier it was asked for even if it
// renders lower, so an over-tall request pays 2.25x (1080p) or 9x (4K) for 720p.
// Which resolutions each Seedance SKU actually accepts — token360's OWN
// published parameter schema (GET /v1/models/{id} -> parameter_schema), verified
// live 2026-08-07. The gateway's checkUnsupportedVideoInput will derive from
// the same source once blockrun PR #353 lands (machine-written snapshot);
// until it deploys, production still runs the OLD wide tables and this client
// is the only gate. THIS table is a hand-copy of that probe run — re-check it
// whenever the gateway resyncs its snapshot.
// The verify:prices matrix probes each model at its ceiling tier, so a gateway
// that starts quoting (or stops quoting) a tier this table disagrees with
// shows up in the free 402 sweep.
//
// seedance-2.0 splits on generation mode — token360 400s 360p/540p/1K in
// text-to-video but accepts them when a first frame is supplied (live-probed
// upstream 2026-06-26). 4K is real 3840x2160 on 2.0 and on nothing else.
const RES_LOW = ["360p", "480p", "540p", "720p", "1080p", "1K"];
const SEEDANCE_RESOLUTIONS: Record<string, { t2v: Set<string>; i2v: Set<string>; note: string }> = {
"bytedance/seedance-1.5-pro": { t2v: new Set(RES_LOW), i2v: new Set(RES_LOW), note: "1080p is the ceiling — only bytedance/seedance-2.0 renders true 4K" },
"bytedance/seedance-2.0-fast": { t2v: new Set(RES_LOW), i2v: new Set(RES_LOW), note: "1080p is the ceiling — only bytedance/seedance-2.0 renders true 4K" },
// No t2v/i2v split: the schema is per-model, and probing shows everything it
// lists passes in both modes, while 540p/1K hard-reject upstream even
// image-conditioned. We allow exactly the schema, nothing more — a few
// off-schema values PASS submit validation (2.0-fast 1080p, 1.5-pro 540p) but
// "passes validation" is not "renders": 1.5-pro historically echoed 2K/4K,
// billed the requested tier, and rendered 720p. Off-schema acceptance can
// silently downscale while billing the higher tier, so it stays out.
export const SEEDANCE_RESOLUTIONS: Record<string, { resolutions: Set<string>; note: string }> = {
"bytedance/seedance-1.5-pro": {
resolutions: new Set(["480p", "720p", "1080p"]),
note: "1080p is the ceiling — only bytedance/seedance-2.0 renders true 4K",
},
"bytedance/seedance-2.0-fast": {
resolutions: new Set(["480p", "720p"]),
note: "720p is the ceiling on 2.0-fast — for 1080p use bytedance/seedance-1.5-pro (cheapest) or bytedance/seedance-2.0 (also 4K)",
},
"bytedance/seedance-2.0": {
t2v: new Set(["480p", "720p", "1080p", "4K"]),
i2v: new Set([...RES_LOW, "4K"]),
note: "text-to-video on 2.0 accepts 480p / 720p / 1080p / 4K only; the lower tiers need an image_url first frame",
resolutions: new Set(["480p", "720p", "1080p", "4K"]),
note: "the only model that renders true 4K (3840x2160)",
},
"bytedance/seedance-2.5": {
resolutions: new Set(["480p", "720p"]),
note: "2.5 caps at 720p — it trades resolution for length (up to 30s). Use bytedance/seedance-2.0 for 1080p or 4K",
},
// 2.5 trades resolution for length, and this is PROVEN, not inferred: a paid
// probe on 2026-08-07 sent 1080p and 1K at both t2v and i2v, and token360
// rejected all four — "the parameter resolution specified in the request is
// not valid for model dreamina-seedance-2-5". The probe cost $0.00 because
// nothing settles on a failed submit.
//
// Worth knowing why this guard earns its keep: the GATEWAY does not enforce
// this. checkUnsupportedVideoInput's `is20` regex does not match "2.5", so 2.5
// falls to a permissive fallback and the gateway happily quotes $3.551218 for
// 1080p — a 402 signed for a request upstream will refuse. Without this check
// the caller signs a payment and gets a 500 the gateway labels "temporary".
"bytedance/seedance-2.5": { t2v: new Set(["360p", "480p", "540p", "720p"]), i2v: new Set(["360p", "480p", "540p", "720p"]), note: "2.5 caps at 720p — it trades resolution for length (up to 30s). Use bytedance/seedance-2.0 for 1080p or 4K" },
};

/**
Expand Down Expand Up @@ -212,8 +215,8 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
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 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 4-15s · seedance-2.5 4-30s · sora-2 exactly 4, 8 or 12 · grok-imagine-video 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(["360p", "480p", "540p", "720p", "1080p", "1K", "4K"]).optional().describe("Seedance only: output resolution. Defaults to 720p. Higher resolutions cost more (token-priced upstream, ~2.25x at 1080p and ~9x at 4K) — the final price comes from the 402 challenge, so the up-front estimate understates 1080p/4K. 4K is bytedance/seedance-2.0 only; seedance-2.5 caps at 720p. Ignored by xAI/Sora."),
aspect_ratio: z.enum(["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional().describe("Seedance only: output aspect ratio, e.g. '9:16' for vertical/mobile, '16:9' for landscape. Defaults to the model's own default. Ignored by xAI/Sora."),
resolution: z.enum(["480p", "720p", "1080p", "4K"]).optional().describe("Seedance only: output resolution. Defaults to 720p. Higher resolutions cost more (token-priced upstream, ~2.25x at 1080p and ~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 and 2.5 480p/720p only. Ignored by xAI/Sora (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 only (NOT 2.5): 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-fast", "bytedance/seedance-2.0", "bytedance/seedance-2.5"]).optional().default("xai/grok-imagine-video").describe("Video model to use"),
agent_id: z.string().optional().describe("Agent identifier for budget tracking and enforcement."),
Expand Down Expand Up @@ -272,21 +275,17 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
}
}

// Resolution ceilings, per model and per generation mode. Only Seedance
// Resolution ceilings, per model. Only Seedance
// is checked: Sora and Grok bill per second and ignore the parameter,
// which is what the schema promises — so for them it is DROPPED from the
// body below rather than rejected. (Forwarding it earned a gateway 400,
// which is the opposite of "ignored".)
const seedanceRes = SEEDANCE_RESOLUTIONS[selectedModel];
if (resolution && seedanceRes) {
const imageConditioned = Boolean(image_url || real_face_asset_id || last_frame_url);
const allowed = imageConditioned ? seedanceRes.i2v : seedanceRes.t2v;
if (!allowed.has(resolution)) {
return {
content: [{ type: "text", text: formatError(`${selectedModel} does not render ${resolution}${imageConditioned ? " for image-conditioned video" : ""}. ${seedanceRes.note}. Supported here: ${[...allowed].join(", ")}.`) }],
isError: true,
};
}
if (resolution && seedanceRes && !seedanceRes.resolutions.has(resolution)) {
return {
content: [{ type: "text", text: formatError(`${selectedModel} does not render ${resolution}. ${seedanceRes.note}. Supported: ${[...seedanceRes.resolutions].join(", ")}.`) }],
isError: true,
};
}

const billedSeconds = duration_seconds ?? VIDEO_DEFAULT_DURATION[selectedModel] ?? 8;
Expand Down Expand Up @@ -359,7 +358,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
const settledUsd = amountToUsd(details.amount);

// The 402 carries the REAL price; Seedance/Sora are token-priced, so a
// 1080p/2K/4K render can far exceed the per-second estimate reserved at
// 1080p/4K render can far exceed the per-second estimate reserved at
// the gate. Re-reserve against the cap BEFORE paying so a single high-res
// call can't settle past the budget (and concurrent jobs hold the true
// amount, not the low estimate, for the whole polling window).
Expand Down
Loading
Loading