From 1e2db1502d0a2d160c9b0b5dfd139625987ccee8 Mon Sep 17 00:00:00 2001 From: VickyXAI <115643921+VickyXAI@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:57:42 -0500 Subject: [PATCH 1/4] fix(video): tighten Seedance capability tables to token360's own schema (0.38.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sibling of blockrun#353 and blockrun-sol#133. Yesterday's 0.38.1 tightened the guards toward the gateway's tables; live probing (2026-08-07) shows those tables were themselves hand-typed drift. token360 publishes the answer — GET /v1/models/{id} → parameter_schema — and nobody had ever read it. Per-model truth, now enforced here: 2.0 = 480p/720p/1080p/4K in BOTH modes (the t2v/i2v split is gone; 540p/1K hard-reject upstream even image-conditioned), 1.5-pro = 480p/720p/1080p, 2.0-fast and 2.5 = 480p/720p. 360p/540p/1K leave the resolution enum and 9:21 leaves the ratio enum — no model lists them. Off-schema values that merely pass upstream validation (2.0-fast 1080p, 1.5-pro 540p) stay blocked: "passes validation" is not "renders", and 1.5-pro historically echoed 2K/4K, billed the tier, rendered 720p. All probes cost $0: token360 validates duration → resolution → ratio → other, so an invalid LATER parameter guarantees rejection after the one under test is judged, and a failed submit never creates a task or bills. RESOLUTION_TOKEN_FACTOR drops the unreachable tiers (the estimator throws on a missing key, so stale is loud, not a silent 720p-baseline reserve). The verify:prices matrix keeps 2.5@1080p as a fleet-consistency probe — the gateway still quotes $3.55 for a render token360 refuses until #353 deploys — and drops 2.0@360p, which this client can no longer express. 302 tests pass. Live gate: 0 under-reserving. --- CHANGELOG.md | 52 ++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/verify-prices.ts | 15 ++++---- src/tools/video.ts | 77 ++++++++++++++++++--------------------- test/video-models.test.ts | 31 +++++++++++----- 6 files changed, 119 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfbf3f..0c24784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,58 @@ All notable changes to BlockRun MCP will be documented in this file. +## 0.38.2 + +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): 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 — values that never produced output are no longer +accepted. 302 tests pass; live gate: 0 under-reserving. + ## 0.38.1 Review pass over 0.38.0. The model addition held up; the guards and the tests diff --git a/package-lock.json b/package-lock.json index 3d07844..f7060a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@blockrun/mcp", - "version": "0.38.1", + "version": "0.38.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@blockrun/mcp", - "version": "0.38.1", + "version": "0.38.2", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", diff --git a/package.json b/package.json index a933a7a..b8c8e40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blockrun/mcp", - "version": "0.38.1", + "version": "0.38.2", "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", diff --git a/scripts/verify-prices.ts b/scripts/verify-prices.ts index f9229ee..78b3dd1 100644 --- a/scripts/verify-prices.ts +++ b/scripts/verify-prices.ts @@ -162,14 +162,15 @@ 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"], ] as Array<[string, number | undefined, string | undefined]>).map(([model, seconds, resolution]) => ({ label: `video ${model.split("/")[1]}${seconds ? ` ${seconds}s` : ""}${resolution ? ` ${resolution}` : ""}`, path: "videos/generations", diff --git a/src/tools/video.ts b/src/tools/video.ts index d3240d7..1411662 100644 --- a/src/tools/video.ts +++ b/src/tools/video.ts @@ -59,13 +59,12 @@ const VIDEO_BASE_PRICE_PER_SECOND: Record = { // 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 = { - "360p": 0.3, "480p": 0.5, - "540p": 0.7, "720p": 1, "1080p": 2.25, - "1K": 2.25, "4K": 9, }; @@ -113,37 +112,35 @@ const VIDEO_DURATION_RANGE: Record parameter_schema), verified +// live 2026-08-07. The gateway's checkUnsupportedVideoInput now derives from +// the same source (blockrun PR #353), so the two sides cannot drift apart. // -// 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; i2v: Set; 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. +const SEEDANCE_RESOLUTIONS: Record; 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 — use bytedance/seedance-2.0 for 1080p or 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" }, }; /** @@ -212,8 +209,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("Seedance only: output aspect ratio, e.g. '9:16' for vertical/mobile, '16:9' for landscape. Defaults to the model's own default. (9:21 removed 2026-08-07 — no Seedance model offers it.) Ignored by xAI/Sora."), 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."), @@ -278,15 +275,11 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC // 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; diff --git a/test/video-models.test.ts b/test/video-models.test.ts index f6f7685..f63f575 100644 --- a/test/video-models.test.ts +++ b/test/video-models.test.ts @@ -211,15 +211,26 @@ test("4K is refused on every model but seedance-2.0 — and 2.0 keeps it", async assert.equal(estimate, "$10.22", `4K must reserve the 9x factor: ${text}`); }); -test("seedance-2.0 mirrors the gateway's t2v/i2v resolution split", async () => { - // Text-to-video on 2.0 rejects 360p/540p/1K upstream; image-conditioned accepts - // them. Getting this wrong is only a wasted round trip, which is precisely what - // these guards exist to remove. - assert.match( - await errorText({ prompt: "a cube", model: "bytedance/seedance-2.0", resolution: "360p" }), - /does not render 360p/, - ); - await reservedFor({ prompt: "a cube", model: "bytedance/seedance-2.0", resolution: "360p", image_url: "https://example.com/a.png" }); +test("off-schema resolutions are refused per model, in BOTH modes", async () => { + // 2026-08-07 probes killed the t2v/i2v split: token360's schema is per-model, + // and 540p/1K hard-reject upstream even image-conditioned. 360p passes + // upstream validation image-conditioned but is off-schema — and off-schema + // acceptance is the "bill the tier, render 720p" trap — so it is refused + // here too, with or without an image. + for (const args of [{}, { image_url: "https://example.com/a.png" }]) { + assert.match( + await errorText({ prompt: "a cube", model: "bytedance/seedance-2.0", resolution: "360p", ...args }), + /does not render 360p/, + ); + } + // 2.0-fast tops out at 720p (schema) — 1080p passes upstream validation but + // is exactly the trap shape, so it must be refused with the pointer to 2.0. + const fast = await errorText({ prompt: "a cube", model: "bytedance/seedance-2.0-fast", resolution: "1080p" }); + assert.match(fast, /does not render 1080p/); + assert.match(fast, /seedance-2\.0/); + // ...while the schema-listed sets still clear the gate. + await reservedFor({ prompt: "a cube", model: "bytedance/seedance-1.5-pro", resolution: "1080p" }); + await reservedFor({ prompt: "a cube", model: "bytedance/seedance-2.0", resolution: "480p" }); }); test("resolution is ignored, not rejected, on the per-second models", async () => { @@ -245,7 +256,7 @@ test("every resolution the schema accepts has a token factor — no silent 1x", // enum and the factor table cannot drift apart in the first place. const { config } = makeHarness(); const base = estimateVideoCost("bytedance/seedance-2.0", 5, "720p"); - for (const r of ["360p", "480p", "540p", "720p", "1080p", "1K", "4K"]) { + for (const r of ["480p", "720p", "1080p", "4K"]) { assert.equal(config.inputSchema.resolution.parse(r), r, `${r} must be in the enum`); if (r === "720p") continue; assert.notEqual(estimateVideoCost("bytedance/seedance-2.0", 5, r), base, `${r} prices at the 720p factor`); From 33c0476006afc333ac0461ac772190aa9818ef06 Mon Sep 17 00:00:00 2001 From: VickyXAI <115643921+VickyXAI@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:21:06 -0500 Subject: [PATCH 2/4] fix(review): the acceptance side was silently revertible, and one assertion matched itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five-reviewer pass over this PR (testing, maintainability, api-contract, Claude adversarial, Codex). What survived verification: THE TESTS PINNED ONLY HALF THE TABLE — mutation-proven. Emptying 2.0-fast's resolution set left the suite green: every rejection was tested, almost no acceptance was, so an over-restrictive edit could brick a paid capability with no red test. And the first fix draft iterated the exported table in the test — mutating the table mutated the expectation, same two escapes. The acceptance sweep now hardcodes the expected pairs (the test IS the spec) and asserts deep-equality with the shipped table, both directions. A FOURTH VACUOUS ASSERTION, mutation-proven: `/seedance-2\.0/` intended to pin the advice pointer, but the message opens with "bytedance/seedance-2.0- fast", which contains the pattern — deleting the pointer entirely stayed green. Anchored on the note's phrasing now, and the note itself stops steering 1080p seekers to the $2.56 model when 1.5-pro renders it at $0.80. THE "BOTH MODES" CLAIM was carried by 360p — a value real callers can no longer send (it left the zod enum), tested on the one model whose set holds all four enum values. Re-adding an image-conditioned wide set in enum-valid values (the production-reachable bill-high/render-low trap) passed. Now pinned with 1080p/4K image-conditioned rejections per model; the 360p leg stays as labeled defense-in-depth. ASPECT_RATIO'S DESCRIPTION LIED (Codex): "Ignored by xAI/Sora" — the handler forwards it for every model and the gateway maps it to portrait/landscape on the per-second models. Description now says what happens. 0.38.2 -> 0.39.0 (api-contract): the enum removals are a deliberate breaking schema change and were shipping as a patch. CHANGELOG's justification also overclaimed — seedance-2.0 image-conditioned 360p DID produce billed output before this release; reworded to the real rationale (off-schema shapes can downscale while billing the requested tier). Comment sweep: "per generation mode" (the mode split this PR deleted), "cannot drift apart" (nothing mechanical enforces it — softened, and the probe matrix now covers each model's ceiling and floor tier so divergence shows up in the free 402 sweep: three new rows, including the only above-720p tier that had no live pin, 1.5-pro@1080p, charge $0.797311 vs reserve $0.798311), and a "2K" that never existed in this client. 303 tests, 6/6 mutations caught, live gate 0 under-reserving (16 video probes). --- CHANGELOG.md | 19 ++++++++--- package-lock.json | 4 +-- package.json | 2 +- scripts/verify-prices.ts | 8 +++++ src/tools/video.ts | 18 +++++++---- test/video-models.test.ts | 68 ++++++++++++++++++++++++++++----------- 6 files changed, 86 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c24784..9556f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to BlockRun MCP will be documented in this file. -## 0.38.2 +## 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 @@ -49,10 +49,19 @@ gateways, so client and server can no longer disagree. the defect blockrun PR #353 fixes — so that probe reports the moment the fleet is consistent. -Breaking (deliberately): 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 — values that never produced output are no longer -accepted. 302 tests pass; live gate: 0 under-reserving. +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. That includes +Sora/Grok callers for whom those values were previously accepted no-ops. +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 ignore it: the handler forwards +it for every model and the gateway uses it to pick portrait vs landscape +on the per-second models. + +302 tests pass; live gate: 0 under-reserving. ## 0.38.1 diff --git a/package-lock.json b/package-lock.json index f7060a8..87531a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@blockrun/mcp", - "version": "0.38.2", + "version": "0.39.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@blockrun/mcp", - "version": "0.38.2", + "version": "0.39.0", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", diff --git a/package.json b/package.json index b8c8e40..ef756b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blockrun/mcp", - "version": "0.38.2", + "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", diff --git a/scripts/verify-prices.ts b/scripts/verify-prices.ts index 78b3dd1..6b1c772 100644 --- a/scripts/verify-prices.ts +++ b/scripts/verify-prices.ts @@ -171,6 +171,14 @@ const PROBES: Probe[] = [ // 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"], + // 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"], ] as Array<[string, number | undefined, string | undefined]>).map(([model, seconds, resolution]) => ({ label: `video ${model.split("/")[1]}${seconds ? ` ${seconds}s` : ""}${resolution ? ` ${resolution}` : ""}`, path: "videos/generations", diff --git a/src/tools/video.ts b/src/tools/video.ts index 1411662..0c4b4b2 100644 --- a/src/tools/video.ts +++ b/src/tools/video.ts @@ -114,8 +114,12 @@ const VIDEO_DURATION_RANGE: Record parameter_schema), verified -// live 2026-08-07. The gateway's checkUnsupportedVideoInput now derives from -// the same source (blockrun PR #353), so the two sides cannot drift apart. +// live 2026-08-07. The gateway's checkUnsupportedVideoInput derives from the +// same probe run (blockrun PR #353, machine-written snapshot); THIS table is a +// hand-copy of it — re-check here 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. // // 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 @@ -124,14 +128,14 @@ const VIDEO_DURATION_RANGE: Record; note: string }> = { +export const SEEDANCE_RESOLUTIONS: Record; 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 — use bytedance/seedance-2.0 for 1080p or 4K", + 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": { resolutions: new Set(["480p", "720p", "1080p", "4K"]), @@ -210,7 +214,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC 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(["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("Seedance only: output aspect ratio, e.g. '9:16' for vertical/mobile, '16:9' for landscape. Defaults to the model's own default. (9:21 removed 2026-08-07 — no Seedance model offers it.) Ignored by xAI/Sora."), + 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/Grok use it only to pick portrait vs landscape (9:16 / 3:4 -> portrait). 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."), @@ -269,7 +273,7 @@ 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, @@ -352,7 +356,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). diff --git a/test/video-models.test.ts b/test/video-models.test.ts index f63f575..861afe7 100644 --- a/test/video-models.test.ts +++ b/test/video-models.test.ts @@ -42,7 +42,7 @@ mock.module("../src/utils/wallet.js", { }, }); -const { registerVideoTool, estimateVideoCost } = await import("../src/tools/video.js"); +const { registerVideoTool, estimateVideoCost, SEEDANCE_RESOLUTIONS } = await import("../src/tools/video.js"); function makeHarness() { let handler: ((args: Record) => Promise) | undefined; @@ -211,26 +211,58 @@ test("4K is refused on every model but seedance-2.0 — and 2.0 keeps it", async assert.equal(estimate, "$10.22", `4K must reserve the 9x factor: ${text}`); }); -test("off-schema resolutions are refused per model, in BOTH modes", async () => { - // 2026-08-07 probes killed the t2v/i2v split: token360's schema is per-model, - // and 540p/1K hard-reject upstream even image-conditioned. 360p passes - // upstream validation image-conditioned but is off-schema — and off-schema - // acceptance is the "bill the tier, render 720p" trap — so it is refused - // here too, with or without an image. +test("off-schema resolutions are refused per model, in BOTH modes — with reachable inputs", async () => { + // The load-bearing cases use ENUM-VALID values (1080p, 4K): a revert that + // re-adds an image-conditioned wider set expressed in enum values is the + // production-reachable "bill the tier, render 720p" trap, and mutation + // testing showed the old 360p-only version missed it entirely. for (const args of [{}, { image_url: "https://example.com/a.png" }]) { - assert.match( - await errorText({ prompt: "a cube", model: "bytedance/seedance-2.0", resolution: "360p", ...args }), - /does not render 360p/, - ); + assert.match(await errorText({ prompt: "a cube", model: "bytedance/seedance-2.0-fast", resolution: "1080p", ...args }), /does not render 1080p/); + assert.match(await errorText({ prompt: "a cube", model: "bytedance/seedance-2.5", resolution: "1080p", ...args }), /does not render 1080p/); + assert.match(await errorText({ prompt: "a cube", model: "bytedance/seedance-1.5-pro", resolution: "4K", ...args }), /does not render 4K/); } - // 2.0-fast tops out at 720p (schema) — 1080p passes upstream validation but - // is exactly the trap shape, so it must be refused with the pointer to 2.0. + // The 2.0-fast note must point somewhere REAL. Anchored on the note's own + // phrasing — a bare /seedance-2\.0/ was self-matching ("seedance-2.0-fast" + // contains it), the fourth vacuous assertion this file has grown. const fast = await errorText({ prompt: "a cube", model: "bytedance/seedance-2.0-fast", resolution: "1080p" }); - assert.match(fast, /does not render 1080p/); - assert.match(fast, /seedance-2\.0/); - // ...while the schema-listed sets still clear the gate. - await reservedFor({ prompt: "a cube", model: "bytedance/seedance-1.5-pro", resolution: "1080p" }); - await reservedFor({ prompt: "a cube", model: "bytedance/seedance-2.0", resolution: "480p" }); + assert.match(fast, /for 1080p use bytedance\/seedance-1\.5-pro/); + // Defense-in-depth only: 360p left the zod enum in this release, so real MCP + // callers get the enum error and never reach this guard message — the direct + // handler call here pins the backstop for unvalidated invocation paths. + assert.match( + await errorText({ prompt: "a cube", model: "bytedance/seedance-2.0", resolution: "360p" }), + /does not render 360p/, + ); +}); + +test("every schema-listed (model, resolution) pair is ACCEPTED — the whole table, positively", async () => { + // Mutation testing proved the acceptance side was silently revertible: + // emptying 2.0-fast's set left the suite green. The expected pairs are + // HARDCODED here — iterating the exported table would test the mutation + // against itself (a first draft of this test did exactly that and the same + // two mutations still passed). This literal map IS the spec: token360's + // published parameter schema, verified live 2026-08-07. + const EXPECTED: Record = { + "bytedance/seedance-1.5-pro": ["480p", "720p", "1080p"], + "bytedance/seedance-2.0-fast": ["480p", "720p"], + "bytedance/seedance-2.0": ["480p", "720p", "1080p", "4K"], + "bytedance/seedance-2.5": ["480p", "720p"], + }; + // The shipped table must equal the spec exactly — both directions. + assert.deepEqual( + Object.fromEntries(Object.entries(SEEDANCE_RESOLUTIONS).map(([m, s]) => [m, [...s.resolutions].sort()])), + Object.fromEntries(Object.entries(EXPECTED).map(([m, r]) => [m, [...r].sort()])), + ); + for (const [model, resolutions] of Object.entries(EXPECTED)) { + for (const resolution of resolutions) { + const { text } = await reservedFor({ prompt: "a cube", model, resolution }); + assert.doesNotMatch(text, /does not render/, `${model} must accept ${resolution}`); + // Every accepted resolution must also be expressible by real callers and + // priced by a real factor (estimateVideoCost throws on a missing one). + assert.equal(makeHarness().config.inputSchema.resolution.parse(resolution), resolution); + estimateVideoCost(model, 5, resolution); + } + } }); test("resolution is ignored, not rejected, on the per-second models", async () => { From 2474630519132c55aae4f71c8cced467b5dc0abe Mon Sep 17 00:00:00 2001 From: VickyXAI <115643921+VickyXAI@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:45:04 -0500 Subject: [PATCH 3/4] =?UTF-8?q?fix(review):=20round=203=20=E2=80=94=20the?= =?UTF-8?q?=20round-2=20fixes=20hold;=20their=20prose=20didn't?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh-context regression hunt over 61e6993 (round 2's fix commit) plus a manual pass. The code survived: four new mutations against the acceptance table, the image-conditioned guard, the pointer note, and 2.5's set all went red; the probe rows estimate cleanly; the semver bump is consistent. What did not survive was the WORDS around the code — including one error round 2 introduced while fixing another: - The aspect_ratio describe (and CHANGELOG) said "Sora/Grok use it to pick portrait vs landscape". Grok doesn't: submitXAIVideo builds its body from model/prompt/image_url/duration only — the gateway never forwards aspect_ratio to xAI. Round 2 replaced "both ignore it" (false for Sora) with "both use it" (false for Grok). Third try states each model's truth. - The CHANGELOG's "previously accepted no-ops" was false for (Sora, 9:21): azureSoraSize treats 9:21 as a portrait trigger, so the removed value changed output orientation. 9:16 is the exact replacement, now said. - "302 tests pass" was stale by round 2's own commit (303). - The probe-matrix comment claimed floor+ceiling coverage per model while seedance-2.0@480p — after round 2, the only schema pair with no live pin — had no row. Added: charge $0.56874, reserve $0.56974, the fee gap. - video.ts's table comment described unmerged blockrun PR #353 in the present tense. Until it deploys, production runs the old wide tables and this client is the only gate — the comment now says so. 303 tests, live gate 0 under-reserving (17 video probes). --- CHANGELOG.md | 24 +++++++++++++----------- scripts/verify-prices.ts | 1 + src/tools/video.ts | 10 ++++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9556f8f..358cd43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,17 +51,19 @@ gateways, so client and server can no longer disagree. 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. That includes -Sora/Grok callers for whom those values were previously accepted no-ops. -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 ignore it: the handler forwards -it for every model and the gateway uses it to pick portrait vs landscape -on the per-second models. - -302 tests pass; live gate: 0 under-reserving. +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. ## 0.38.1 diff --git a/scripts/verify-prices.ts b/scripts/verify-prices.ts index 6b1c772..b1c451a 100644 --- a/scripts/verify-prices.ts +++ b/scripts/verify-prices.ts @@ -179,6 +179,7 @@ const PROBES: Probe[] = [ ["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", diff --git a/src/tools/video.ts b/src/tools/video.ts index 0c4b4b2..4c0f961 100644 --- a/src/tools/video.ts +++ b/src/tools/video.ts @@ -114,9 +114,11 @@ const VIDEO_DURATION_RANGE: Record parameter_schema), verified -// live 2026-08-07. The gateway's checkUnsupportedVideoInput derives from the -// same probe run (blockrun PR #353, machine-written snapshot); THIS table is a -// hand-copy of it — re-check here whenever the gateway resyncs its snapshot. +// 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. @@ -214,7 +216,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC 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(["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/Grok use it only to pick portrait vs landscape (9:16 / 3:4 -> portrait). Defaults to the model's own default. (9:21 removed 2026-08-07 — no Seedance model offers it; use 9:16 for vertical.)"), + 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."), From 84a6aa1189ee464fc18ec3fcb5b2edd16bd92508 Mon Sep 17 00:00:00 2001 From: VickyXAI <115643921+VickyXAI@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:51:27 -0500 Subject: [PATCH 4/4] docs: record what #97 ships inside 0.39.0, and its dual-gateway view of the 2.5@1080p probe --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358cd43..821a6c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,18 @@ 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