Skip to content

Commit 26ff347

Browse files
author
1bcMax
committed
v0.12.155: sync BlockRun API surface (Grok 4.20 + Imagine image/video + CogView-4)
BlockRun server added models and one new endpoint between 2026-04-01 and 2026-04-17. ClawRouter now exposes: - xAI Grok 4.20 chat family: reasoning, non-reasoning, multi-agent ($2/$6 per 1M, 2M context, 16K max output). Hidden from the /model picker to mirror BlockRun's hidden:true flag; routable by explicit model ID. - Image gen: xai/grok-imagine-image ($0.02), xai/grok-imagine-image-pro ($0.07), zai/cogview-4 ($0.015, $0.02 at 1440²). Uses the existing /v1/images/generations proxy + IMAGE_PRICING table. - Video gen: new POST /v1/videos/generations handler + /videos/:file static route. Mirrors the audio pattern — buffer request, x402 payFetch, download MP4 into ~/.openclaw/blockrun/videos/, rewrite URL, log tier=VIDEO. Registered with OpenClaw via buildVideoGenerationProvider(). - Alias: qwen/qwen3-coder-480b-a35b-instruct -> free/qwen3-coder-480b. End-to-end verified against prod: grok-4.20 200 OK, qwen alias resolves, grok-imagine-image + cogview-4 + grok-imagine-video all saved locally. Anthropic thinking:{} param passthrough confirmed — proxy never strips it.
1 parent c7f7501 commit 26ff347

11 files changed

Lines changed: 843 additions & 7 deletions

File tree

dist/cli.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73780,6 +73780,7 @@ var MODEL_ALIASES = {
7378073780
"nvidia/deepseek-v3.2": "free/deepseek-v3.2",
7378173781
"nvidia/mistral-large-3-675b": "free/mistral-large-3-675b",
7378273782
"nvidia/qwen3-coder-480b": "free/qwen3-coder-480b",
73783+
"qwen/qwen3-coder-480b-a35b-instruct": "free/qwen3-coder-480b",
7378373784
"nvidia/devstral-2-123b": "free/devstral-2-123b",
7378473785
"nvidia/glm-4.7": "free/glm-4.7",
7378573786
"nvidia/llama-4-maverick": "free/llama-4-maverick",
@@ -74379,6 +74380,39 @@ var BLOCKRUN_MODELS = [
7437974380
vision: true,
7438074381
toolCalling: true
7438174382
},
74383+
// xAI Grok 4.20 Family (hidden in picker; explicit-only — mirrors BlockRun hidden:true)
74384+
{
74385+
id: "xai/grok-4.20-reasoning",
74386+
name: "Grok 4.20 Reasoning",
74387+
version: "4.20",
74388+
inputPrice: 2,
74389+
outputPrice: 6,
74390+
contextWindow: 2e6,
74391+
maxOutput: 16384,
74392+
reasoning: true,
74393+
toolCalling: true
74394+
},
74395+
{
74396+
id: "xai/grok-4.20-non-reasoning",
74397+
name: "Grok 4.20",
74398+
version: "4.20",
74399+
inputPrice: 2,
74400+
outputPrice: 6,
74401+
contextWindow: 2e6,
74402+
maxOutput: 16384,
74403+
toolCalling: true
74404+
},
74405+
{
74406+
id: "xai/grok-4.20-multi-agent",
74407+
name: "Grok 4.20 Multi-Agent",
74408+
version: "4.20",
74409+
inputPrice: 2,
74410+
outputPrice: 6,
74411+
contextWindow: 2e6,
74412+
maxOutput: 16384,
74413+
reasoning: true,
74414+
toolCalling: true
74415+
},
7438274416
// MiniMax
7438374417
{
7438474418
id: "minimax/minimax-m2.7",
@@ -76535,6 +76569,7 @@ var BLOCKRUN_API = "https://blockrun.ai/api";
7653576569
var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
7653676570
var IMAGE_DIR = join8(homedir5(), ".openclaw", "blockrun", "images");
7653776571
var AUDIO_DIR = join8(homedir5(), ".openclaw", "blockrun", "audio");
76572+
var VIDEO_DIR = join8(homedir5(), ".openclaw", "blockrun", "videos");
7653876573
var AUTO_MODEL = "blockrun/auto";
7653976574
var ROUTING_PROFILES = /* @__PURE__ */ new Set([
7654076575
"blockrun/eco",
@@ -77242,8 +77277,30 @@ var IMAGE_PRICING = {
7724277277
"google/nano-banana-pro": {
7724377278
default: 0.1,
7724477279
sizes: { "1024x1024": 0.1, "2048x2048": 0.1, "4096x4096": 0.15 }
77280+
},
77281+
"xai/grok-imagine-image": { default: 0.02, sizes: { "1024x1024": 0.02 } },
77282+
"xai/grok-imagine-image-pro": { default: 0.07, sizes: { "1024x1024": 0.07 } },
77283+
"zai/cogview-4": {
77284+
default: 0.015,
77285+
sizes: {
77286+
"512x512": 0.015,
77287+
"768x768": 0.015,
77288+
"1024x1024": 0.015,
77289+
"768x1344": 0.015,
77290+
"1344x768": 0.015,
77291+
"1440x1440": 0.02
77292+
}
7724577293
}
7724677294
};
77295+
var VIDEO_PRICING = {
77296+
"xai/grok-imagine-video": { pricePerSecond: 0.05, defaultDurationSeconds: 8 }
77297+
};
77298+
function estimateVideoCost(model, durationSeconds) {
77299+
const p = VIDEO_PRICING[model];
77300+
if (!p) return 0.4 * 1.05;
77301+
const dur = durationSeconds ?? p.defaultDurationSeconds;
77302+
return p.pricePerSecond * dur * 1.05;
77303+
}
7724777304
function estimateImageCost(model, size5, n = 1) {
7724877305
const pricing = IMAGE_PRICING[model];
7724977306
if (!pricing) return 0.04 * n * 1.05;
@@ -77630,6 +77687,35 @@ async function startProxy(options) {
7763077687
}
7763177688
return;
7763277689
}
77690+
if (req.url?.startsWith("/videos/") && req.method === "GET") {
77691+
const filename = req.url.slice("/videos/".length).split("?")[0].replace(/[^a-zA-Z0-9._-]/g, "");
77692+
if (!filename) {
77693+
res.writeHead(400);
77694+
res.end("Bad request");
77695+
return;
77696+
}
77697+
const filePath = join8(VIDEO_DIR, filename);
77698+
try {
77699+
const s3 = await fsStat(filePath);
77700+
if (!s3.isFile()) throw new Error("not a file");
77701+
const ext = filename.split(".").pop()?.toLowerCase() ?? "mp4";
77702+
const mime = {
77703+
mp4: "video/mp4",
77704+
webm: "video/webm",
77705+
mov: "video/quicktime"
77706+
};
77707+
const data = await readFile(filePath);
77708+
res.writeHead(200, {
77709+
"Content-Type": mime[ext] ?? "video/mp4",
77710+
"Content-Length": data.length
77711+
});
77712+
res.end(data);
77713+
} catch {
77714+
res.writeHead(404, { "Content-Type": "application/json" });
77715+
res.end(JSON.stringify({ error: "Video not found" }));
77716+
}
77717+
return;
77718+
}
7763377719
if (req.url === "/v1/images/generations" && req.method === "POST") {
7763477720
const imgStartTime = Date.now();
7763577721
const chunks = [];
@@ -77917,6 +78003,88 @@ async function startProxy(options) {
7791778003
}
7791878004
return;
7791978005
}
78006+
if (req.url === "/v1/videos/generations" && req.method === "POST") {
78007+
const videoStartTime = Date.now();
78008+
const chunks = [];
78009+
for await (const chunk of req) {
78010+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
78011+
}
78012+
const reqBody = Buffer.concat(chunks);
78013+
let videoModel = "xai/grok-imagine-video";
78014+
let videoDuration;
78015+
try {
78016+
const parsed = JSON.parse(reqBody.toString());
78017+
videoModel = parsed.model || videoModel;
78018+
videoDuration = typeof parsed.duration_seconds === "number" ? parsed.duration_seconds : void 0;
78019+
} catch {
78020+
}
78021+
try {
78022+
const upstream = await payFetch(`${apiBase}/v1/videos/generations`, {
78023+
method: "POST",
78024+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
78025+
body: reqBody
78026+
});
78027+
const text = await upstream.text();
78028+
if (!upstream.ok) {
78029+
res.writeHead(upstream.status, { "Content-Type": "application/json" });
78030+
res.end(text);
78031+
return;
78032+
}
78033+
let result;
78034+
try {
78035+
result = JSON.parse(text);
78036+
} catch {
78037+
res.writeHead(200, { "Content-Type": "application/json" });
78038+
res.end(text);
78039+
return;
78040+
}
78041+
if (result.data?.length) {
78042+
await mkdir3(VIDEO_DIR, { recursive: true });
78043+
const port2 = server.address()?.port ?? 8402;
78044+
for (const clip of result.data) {
78045+
if (clip.url?.startsWith("https://") || clip.url?.startsWith("http://")) {
78046+
try {
78047+
const videoResp = await fetch(clip.url);
78048+
if (videoResp.ok) {
78049+
const contentType = videoResp.headers.get("content-type") ?? "video/mp4";
78050+
const ext = contentType.includes("webm") ? "webm" : contentType.includes("quicktime") ? "mov" : "mp4";
78051+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
78052+
const buf = Buffer.from(await videoResp.arrayBuffer());
78053+
await writeFile2(join8(VIDEO_DIR, filename), buf);
78054+
clip.url = `http://localhost:${port2}/videos/${filename}`;
78055+
console.log(`[ClawRouter] Video saved \u2192 ${clip.url}`);
78056+
}
78057+
} catch (downloadErr) {
78058+
console.warn(
78059+
`[ClawRouter] Failed to download video, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
78060+
);
78061+
}
78062+
}
78063+
}
78064+
}
78065+
const videoActualCost = paymentStore.getStore()?.amountUsd ?? estimateVideoCost(videoModel, videoDuration);
78066+
logUsage({
78067+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
78068+
model: videoModel,
78069+
tier: "VIDEO",
78070+
cost: videoActualCost,
78071+
baselineCost: videoActualCost,
78072+
savings: 0,
78073+
latencyMs: Date.now() - videoStartTime
78074+
}).catch(() => {
78075+
});
78076+
res.writeHead(200, { "Content-Type": "application/json" });
78077+
res.end(JSON.stringify(result));
78078+
} catch (err) {
78079+
const msg = err instanceof Error ? err.message : String(err);
78080+
console.error(`[ClawRouter] Video generation error: ${msg}`);
78081+
if (!res.headersSent) {
78082+
res.writeHead(502, { "Content-Type": "application/json" });
78083+
res.end(JSON.stringify({ error: "Video generation failed", details: msg }));
78084+
}
78085+
}
78086+
return;
78087+
}
7792078088
if (req.url?.match(/^\/v1\/(?:x|partner|pm|exa|modal)\//)) {
7792178089
try {
7792278090
await proxyPaidApiRequest(

dist/cli.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.d.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,73 @@ type MusicGenerationProviderPlugin = {
218218
}) => boolean;
219219
generateMusic: (req: MusicGenerationRequest) => Promise<MusicGenerationResult>;
220220
};
221+
type VideoGenerationResolution = "480P" | "720P" | "768P" | "1080P";
222+
type GeneratedVideoAsset = {
223+
buffer: Buffer;
224+
mimeType: string;
225+
fileName?: string;
226+
metadata?: Record<string, unknown>;
227+
};
228+
type VideoGenerationSourceAsset = {
229+
url?: string;
230+
buffer?: Buffer;
231+
mimeType?: string;
232+
fileName?: string;
233+
metadata?: Record<string, unknown>;
234+
};
235+
type VideoGenerationRequest = {
236+
provider: string;
237+
model: string;
238+
prompt: string;
239+
cfg: Record<string, unknown>;
240+
agentDir?: string;
241+
timeoutMs?: number;
242+
size?: string;
243+
aspectRatio?: string;
244+
resolution?: VideoGenerationResolution;
245+
durationSeconds?: number;
246+
audio?: boolean;
247+
watermark?: boolean;
248+
inputImages?: VideoGenerationSourceAsset[];
249+
inputVideos?: VideoGenerationSourceAsset[];
250+
};
251+
type VideoGenerationResult = {
252+
videos: GeneratedVideoAsset[];
253+
model?: string;
254+
metadata?: Record<string, unknown>;
255+
};
256+
type VideoGenerationModeCapabilities = {
257+
maxVideos?: number;
258+
maxInputImages?: number;
259+
maxInputVideos?: number;
260+
maxDurationSeconds?: number;
261+
supportedDurationSeconds?: readonly number[];
262+
supportsSize?: boolean;
263+
supportsAspectRatio?: boolean;
264+
supportsResolution?: boolean;
265+
supportsAudio?: boolean;
266+
supportsWatermark?: boolean;
267+
};
268+
type VideoGenerationTransformCapabilities = VideoGenerationModeCapabilities & {
269+
enabled: boolean;
270+
};
271+
type VideoGenerationProviderCapabilities = VideoGenerationModeCapabilities & {
272+
generate?: VideoGenerationModeCapabilities;
273+
imageToVideo?: VideoGenerationTransformCapabilities;
274+
videoToVideo?: VideoGenerationTransformCapabilities;
275+
};
276+
type VideoGenerationProviderPlugin = {
277+
id: string;
278+
aliases?: string[];
279+
label?: string;
280+
defaultModel?: string;
281+
models?: string[];
282+
capabilities: VideoGenerationProviderCapabilities;
283+
isConfigured?: (ctx: {
284+
cfg?: Record<string, unknown>;
285+
}) => boolean;
286+
generateVideo: (req: VideoGenerationRequest) => Promise<VideoGenerationResult>;
287+
};
221288
type WebSearchProviderToolDefinition = {
222289
description: string;
223290
parameters: unknown;
@@ -262,7 +329,7 @@ type OpenClawPluginApi = {
262329
registerProvider: (provider: ProviderPlugin) => void;
263330
registerImageGenerationProvider: (provider: ImageGenerationProviderPlugin) => void;
264331
registerMusicGenerationProvider: (provider: MusicGenerationProviderPlugin) => void;
265-
registerVideoGenerationProvider?: (provider: unknown) => void;
332+
registerVideoGenerationProvider?: (provider: VideoGenerationProviderPlugin) => void;
266333
registerWebSearchProvider?: (provider: WebSearchProviderPlugin) => void;
267334
registerTool: (tool: unknown, opts?: unknown) => void;
268335
registerHook: (events: string | string[], handler: unknown, opts?: unknown) => void;

0 commit comments

Comments
 (0)