@blockrun/franklin 3.15.50 → 3.15.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/llm.js +58 -5
- package/dist/tools/videogen.js +25 -2
- package/package.json +1 -1
package/dist/agent/llm.js
CHANGED
|
@@ -431,11 +431,64 @@ export class ModelClient {
|
|
|
431
431
|
if (!response.ok) {
|
|
432
432
|
const errorBody = await response.text().catch(() => 'unknown error');
|
|
433
433
|
const message = extractApiErrorMessage(errorBody);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
434
|
+
// Runtime tool_choice retry. The static allowlist at line ~35
|
|
435
|
+
// catches the case where the request goes directly to a model
|
|
436
|
+
// whose name contains `deepseek-reasoner` / `openai/o1` /
|
|
437
|
+
// `openai/o3`. But the gateway sometimes ALIASES a different
|
|
438
|
+
// model name to a reasoner backend — verified 2026-05-04 in a
|
|
439
|
+
// live session: a request for `deepseek/deepseek-v4-pro`
|
|
440
|
+
// returned `400 Invalid request: 400 deepseek-reasoner does not
|
|
441
|
+
// support this tool_choice`, because the gateway routed v4-pro
|
|
442
|
+
// to a deepseek-reasoner upstream. The static allowlist can't
|
|
443
|
+
// know that. Catch the error, drop tool_choice, re-fire once.
|
|
444
|
+
// No payment re-sign needed — original 402 already settled, and
|
|
445
|
+
// the gateway treats this as the same logical request.
|
|
446
|
+
const lc = message.toLowerCase();
|
|
447
|
+
const looksLikeToolChoiceReject = response.status === 400 &&
|
|
448
|
+
lc.includes('tool_choice') &&
|
|
449
|
+
(lc.includes('not support') || lc.includes('unsupported') || lc.includes('does not support'));
|
|
450
|
+
if (looksLikeToolChoiceReject && requestPayload['tool_choice'] !== undefined) {
|
|
451
|
+
delete requestPayload['tool_choice'];
|
|
452
|
+
const retryBody = JSON.stringify(requestPayload);
|
|
453
|
+
if (this.debug) {
|
|
454
|
+
console.error(`[franklin] tool_choice rejected by upstream; retrying without it (model=${request.model})`);
|
|
455
|
+
}
|
|
456
|
+
response = await withAbortableTimeout(() => fetch(endpoint, {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers,
|
|
459
|
+
body: retryBody,
|
|
460
|
+
signal: requestController.signal,
|
|
461
|
+
}), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
|
|
462
|
+
if (response.status === 402) {
|
|
463
|
+
const paymentHeader = await this.signPayment(response);
|
|
464
|
+
if (!paymentHeader) {
|
|
465
|
+
yield { kind: 'error', payload: { message: 'Payment signing failed' } };
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
response = await withAbortableTimeout(() => fetch(endpoint, {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
headers: { ...headers, ...paymentHeader },
|
|
471
|
+
body: retryBody,
|
|
472
|
+
signal: requestController.signal,
|
|
473
|
+
}), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
|
|
474
|
+
}
|
|
475
|
+
if (!response.ok) {
|
|
476
|
+
const retryBodyText = await response.text().catch(() => 'unknown error');
|
|
477
|
+
yield {
|
|
478
|
+
kind: 'error',
|
|
479
|
+
payload: { status: response.status, message: extractApiErrorMessage(retryBodyText) },
|
|
480
|
+
};
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// Successful retry — fall through to SSE parsing below.
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
yield {
|
|
487
|
+
kind: 'error',
|
|
488
|
+
payload: { status: response.status, message },
|
|
489
|
+
};
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
439
492
|
}
|
|
440
493
|
// Parse SSE stream
|
|
441
494
|
yield* this.parseSSEStream(response, requestController, streamTimeoutMs, request.model);
|
package/dist/tools/videogen.js
CHANGED
|
@@ -23,6 +23,7 @@ import path from 'node:path';
|
|
|
23
23
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
24
24
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
25
25
|
import { logger } from '../logger.js';
|
|
26
|
+
import { resolveReferenceImage } from './imagegen.js';
|
|
26
27
|
import { ModelClient } from '../agent/llm.js';
|
|
27
28
|
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
|
|
28
29
|
import { recordUsage } from '../stats/tracker.js';
|
|
@@ -46,6 +47,28 @@ function buildExecute(deps) {
|
|
|
46
47
|
const { output_path, model, image_url, duration_seconds, contentId } = rawInput;
|
|
47
48
|
if (!rawInput.prompt)
|
|
48
49
|
return { output: 'Error: prompt is required', isError: true };
|
|
50
|
+
// Resolve image_url before sending. The gateway requires a URL (http(s)
|
|
51
|
+
// or data: URI), but agents naturally pass a local file path —
|
|
52
|
+
// verified 2026-05-04 in a live session: agent passed
|
|
53
|
+
// `/Users/.../keyframe.png` and the gateway returned
|
|
54
|
+
// `400 Invalid request body: invalid_format url path: image_url`.
|
|
55
|
+
// ImageGen already had `resolveReferenceImage` for the same problem;
|
|
56
|
+
// sharing the helper keeps the contract consistent across both tools
|
|
57
|
+
// (local path → base64 data URI; http(s) URL → fetched + inlined;
|
|
58
|
+
// data: URI → passes through). On any resolution failure, surface
|
|
59
|
+
// the message instead of letting the gateway 400 bubble back.
|
|
60
|
+
let resolvedImageUrl;
|
|
61
|
+
if (image_url) {
|
|
62
|
+
try {
|
|
63
|
+
resolvedImageUrl = await resolveReferenceImage(image_url, ctx.workingDir);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return {
|
|
67
|
+
output: `Could not resolve image_url ${JSON.stringify(image_url)}: ${err.message}`,
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
49
72
|
// One-shot refinement opt-out: leading `///` tells Franklin "don't
|
|
50
73
|
// refine this prompt." Strip the prefix and pass skipRefine through.
|
|
51
74
|
let prompt = rawInput.prompt;
|
|
@@ -131,7 +154,7 @@ function buildExecute(deps) {
|
|
|
131
154
|
const body = JSON.stringify({
|
|
132
155
|
model: videoModel,
|
|
133
156
|
prompt: chosenPrompt,
|
|
134
|
-
...(
|
|
157
|
+
...(resolvedImageUrl ? { image_url: resolvedImageUrl } : {}),
|
|
135
158
|
...(duration_seconds ? { duration_seconds } : {}),
|
|
136
159
|
});
|
|
137
160
|
const headers = {
|
|
@@ -440,7 +463,7 @@ export function createVideoGenCapability(deps = {}) {
|
|
|
440
463
|
prompt: { type: 'string', description: 'Text description of the video to generate' },
|
|
441
464
|
output_path: { type: 'string', description: 'Where to save the MP4. Default: generated-<timestamp>.mp4 in working directory' },
|
|
442
465
|
model: { type: 'string', description: 'Video model. Default: xai/grok-imagine-video' },
|
|
443
|
-
image_url: { type: 'string', description: 'Optional seed image
|
|
466
|
+
image_url: { type: 'string', description: 'Optional seed image (image-to-video). Accepts http(s) URL, data: URI, or local file path — local paths get inlined as base64 data URIs automatically.' },
|
|
444
467
|
duration_seconds: { type: 'number', description: 'Duration billed for. Default depends on model (8s for grok-imagine-video).' },
|
|
445
468
|
contentId: { type: 'string', description: 'Optional Content id to attach and budget against.' },
|
|
446
469
|
},
|
package/package.json
CHANGED