@blockrun/franklin 3.9.3 → 3.9.5
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.d.ts +1 -0
- package/dist/agent/llm.js +86 -14
- package/dist/agent/loop.js +4 -2
- package/dist/agent/nemotron-prose-stripper.d.ts +23 -0
- package/dist/agent/nemotron-prose-stripper.js +77 -0
- package/dist/agent/tokens.js +3 -0
- package/dist/commands/start.js +6 -12
- package/dist/pricing.js +3 -1
- package/dist/tools/imagegen.js +8 -2
- package/package.json +1 -1
package/dist/agent/llm.d.ts
CHANGED
|
@@ -90,6 +90,7 @@ export declare function modelHasExtendedThinking(model: string): boolean;
|
|
|
90
90
|
* direct unit testing — the happy path hits it only on stream error.
|
|
91
91
|
*/
|
|
92
92
|
export declare function classifyToolCallFailure(toolName: string, rawInput: string, signal: AbortSignal | undefined, model: string): string;
|
|
93
|
+
export declare function isRoleplayedJsonToolCallText(text: string): boolean;
|
|
93
94
|
export declare class ModelClient {
|
|
94
95
|
private apiUrl;
|
|
95
96
|
private chain;
|
package/dist/agent/llm.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, creat
|
|
|
7
7
|
import { USER_AGENT } from '../config.js';
|
|
8
8
|
import { routeRequest, parseRoutingProfile } from '../router/index.js';
|
|
9
9
|
import { ThinkTagStripper } from './think-tag-stripper.js';
|
|
10
|
+
import { isNemotronProseModel, stripNemotronProse } from './nemotron-prose-stripper.js';
|
|
10
11
|
function parseTimeoutEnv(name) {
|
|
11
12
|
const raw = process.env[name];
|
|
12
13
|
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
@@ -170,6 +171,23 @@ export function classifyToolCallFailure(toolName, rawInput, signal, model) {
|
|
|
170
171
|
`Preview: ${preview}${rawInput.length > 120 ? '…' : ''} — ` +
|
|
171
172
|
`this is usually a model output bug; try \`/model <other>\` or retry.]`;
|
|
172
173
|
}
|
|
174
|
+
export function isRoleplayedJsonToolCallText(text) {
|
|
175
|
+
const trimmed = text.trim();
|
|
176
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
|
|
177
|
+
return false;
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(trimmed);
|
|
180
|
+
return (parsed !== null &&
|
|
181
|
+
typeof parsed === 'object' &&
|
|
182
|
+
!Array.isArray(parsed) &&
|
|
183
|
+
parsed.type === 'function' &&
|
|
184
|
+
typeof parsed.name === 'string' &&
|
|
185
|
+
('parameters' in parsed || 'arguments' in parsed));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
173
191
|
function applyAnthropicPromptCaching(payload, request) {
|
|
174
192
|
const out = { ...payload };
|
|
175
193
|
const cacheMarker = { type: 'ephemeral' };
|
|
@@ -402,6 +420,8 @@ export class ModelClient {
|
|
|
402
420
|
let currentToolId = '';
|
|
403
421
|
let currentToolName = '';
|
|
404
422
|
let currentToolInput = '';
|
|
423
|
+
const textEmission = { mode: 'undecided' };
|
|
424
|
+
const isNemotronProse = isNemotronProseModel(request.model);
|
|
405
425
|
// Split inline <think>…</think> emitted by reasoning models (nemotron,
|
|
406
426
|
// deepseek-r1, qwq, etc.) that use the text field instead of the native
|
|
407
427
|
// thinking block. Thinking emitted this way is display-only — we don't
|
|
@@ -413,6 +433,26 @@ export class ModelClient {
|
|
|
413
433
|
// system-prompt guard in loop.ts is responsible for preventing this.
|
|
414
434
|
// Debug-only because the user already sees the literal text in the UI.
|
|
415
435
|
let toolCallRoleplayWarned = false;
|
|
436
|
+
const appendText = (text) => {
|
|
437
|
+
if (!text)
|
|
438
|
+
return;
|
|
439
|
+
currentText += text;
|
|
440
|
+
if (textEmission.mode === 'undecided') {
|
|
441
|
+
const trimmed = currentText.trimStart();
|
|
442
|
+
if (!trimmed)
|
|
443
|
+
return;
|
|
444
|
+
// Nemotron Omni leaks reasoning prose into the text channel without
|
|
445
|
+
// <think> tags. Hold the buffer for end-of-stream stripping.
|
|
446
|
+
textEmission.mode = isNemotronProse || trimmed.startsWith('{') ? 'hold' : 'stream';
|
|
447
|
+
if (textEmission.mode === 'stream') {
|
|
448
|
+
onStreamDelta?.({ type: 'text', text: currentText });
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (textEmission.mode === 'stream') {
|
|
453
|
+
onStreamDelta?.({ type: 'text', text });
|
|
454
|
+
}
|
|
455
|
+
};
|
|
416
456
|
for await (const chunk of this.streamCompletion(request, signal)) {
|
|
417
457
|
switch (chunk.kind) {
|
|
418
458
|
case 'content_block_start': {
|
|
@@ -429,6 +469,7 @@ export class ModelClient {
|
|
|
429
469
|
}
|
|
430
470
|
else if (cblock?.type === 'text') {
|
|
431
471
|
currentText = '';
|
|
472
|
+
textEmission.mode = 'undecided';
|
|
432
473
|
textStripper = new ThinkTagStripper();
|
|
433
474
|
}
|
|
434
475
|
break;
|
|
@@ -458,9 +499,7 @@ export class ModelClient {
|
|
|
458
499
|
}
|
|
459
500
|
for (const seg of textStripper.push(raw)) {
|
|
460
501
|
if (seg.type === 'text') {
|
|
461
|
-
|
|
462
|
-
if (seg.text)
|
|
463
|
-
onStreamDelta?.({ type: 'text', text: seg.text });
|
|
502
|
+
appendText(seg.text);
|
|
464
503
|
}
|
|
465
504
|
else if (seg.text) {
|
|
466
505
|
onStreamDelta?.({ type: 'thinking', text: seg.text });
|
|
@@ -537,20 +576,37 @@ export class ModelClient {
|
|
|
537
576
|
// Flush any partial tag held in the stripper
|
|
538
577
|
for (const seg of textStripper.flush()) {
|
|
539
578
|
if (seg.type === 'text') {
|
|
540
|
-
|
|
541
|
-
if (seg.text)
|
|
542
|
-
onStreamDelta?.({ type: 'text', text: seg.text });
|
|
579
|
+
appendText(seg.text);
|
|
543
580
|
}
|
|
544
581
|
else if (seg.text) {
|
|
545
582
|
onStreamDelta?.({ type: 'thinking', text: seg.text });
|
|
546
583
|
}
|
|
547
584
|
}
|
|
548
585
|
if (currentText) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
586
|
+
if (textEmission.mode === 'hold' && isRoleplayedJsonToolCallText(currentText)) {
|
|
587
|
+
if (this.debug) {
|
|
588
|
+
console.error(`[franklin] Model ${request.model} emitted a raw JSON function-call object as text. ` +
|
|
589
|
+
'Treating it as non-productive output so recovery can try another model.');
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else if (textEmission.mode === 'hold' && isNemotronProse) {
|
|
593
|
+
const { thinking, answer } = stripNemotronProse(currentText);
|
|
594
|
+
if (thinking)
|
|
595
|
+
onStreamDelta?.({ type: 'thinking', text: thinking });
|
|
596
|
+
onStreamDelta?.({ type: 'text', text: answer });
|
|
597
|
+
collected.push({ type: 'text', text: answer });
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
if (textEmission.mode !== 'stream') {
|
|
601
|
+
onStreamDelta?.({ type: 'text', text: currentText });
|
|
602
|
+
}
|
|
603
|
+
collected.push({
|
|
604
|
+
type: 'text',
|
|
605
|
+
text: currentText,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
553
608
|
currentText = '';
|
|
609
|
+
textEmission.mode = 'undecided';
|
|
554
610
|
}
|
|
555
611
|
}
|
|
556
612
|
break;
|
|
@@ -588,16 +644,32 @@ export class ModelClient {
|
|
|
588
644
|
// Flush any remaining text (stream ended without content_block_stop)
|
|
589
645
|
for (const seg of textStripper.flush()) {
|
|
590
646
|
if (seg.type === 'text') {
|
|
591
|
-
|
|
592
|
-
if (seg.text)
|
|
593
|
-
onStreamDelta?.({ type: 'text', text: seg.text });
|
|
647
|
+
appendText(seg.text);
|
|
594
648
|
}
|
|
595
649
|
else if (seg.text) {
|
|
596
650
|
onStreamDelta?.({ type: 'thinking', text: seg.text });
|
|
597
651
|
}
|
|
598
652
|
}
|
|
599
653
|
if (currentText) {
|
|
600
|
-
|
|
654
|
+
if (textEmission.mode === 'hold' && isRoleplayedJsonToolCallText(currentText)) {
|
|
655
|
+
if (this.debug) {
|
|
656
|
+
console.error(`[franklin] Model ${request.model} emitted a raw JSON function-call object as text. ` +
|
|
657
|
+
'Treating it as non-productive output so recovery can try another model.');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else if (textEmission.mode === 'hold' && isNemotronProse) {
|
|
661
|
+
const { thinking, answer } = stripNemotronProse(currentText);
|
|
662
|
+
if (thinking)
|
|
663
|
+
onStreamDelta?.({ type: 'thinking', text: thinking });
|
|
664
|
+
onStreamDelta?.({ type: 'text', text: answer });
|
|
665
|
+
collected.push({ type: 'text', text: answer });
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
if (textEmission.mode !== 'stream') {
|
|
669
|
+
onStreamDelta?.({ type: 'text', text: currentText });
|
|
670
|
+
}
|
|
671
|
+
collected.push({ type: 'text', text: currentText });
|
|
672
|
+
}
|
|
601
673
|
}
|
|
602
674
|
return { content: collected, usage, stopReason };
|
|
603
675
|
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -822,8 +822,10 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
822
822
|
'\n\n# Available tools\n' +
|
|
823
823
|
`You have exactly these tools: ${names}.\n` +
|
|
824
824
|
'Do not invent other tool names. Do not emit literal "[TOOLCALL]", ' +
|
|
825
|
-
'"<tool_call>",
|
|
826
|
-
'
|
|
825
|
+
'"<tool_call>", raw JSON function-call objects like {"type":"function","name":"Tool","parameters":{}}, ' +
|
|
826
|
+
'or similar tokens in your text — call tools via the proper API only. ' +
|
|
827
|
+
'If the user asks you to echo a token, marker, or string, echo it as plain text; ' +
|
|
828
|
+
'do not call Wallet or any other tool unless the user explicitly asks for that tool-backed information.';
|
|
827
829
|
}
|
|
828
830
|
// Safety net: handled in llm.ts resolveVirtualModel()
|
|
829
831
|
// Sanitize: remove orphaned tool results that could confuse the API
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip leaked reasoning prose from Nemotron-family models.
|
|
3
|
+
*
|
|
4
|
+
* NVIDIA's Nemotron Omni reasoning model emits its chain of thought as plain
|
|
5
|
+
* text — without `<think>` tags or a separate reasoning_content channel — so
|
|
6
|
+
* the think-tag stripper can't catch it. The reasoning prose is then concatenated
|
|
7
|
+
* directly with the answer (often without even a separator), e.g.:
|
|
8
|
+
*
|
|
9
|
+
* "The user asks: ... According to instructions, we must obey. Just output
|
|
10
|
+
* the tokenOMNI_E2E_OK"
|
|
11
|
+
*
|
|
12
|
+
* This module detects the reasoning preamble (heuristic: leading sentence
|
|
13
|
+
* matches a known meta-reasoning opener) and strips everything up to and
|
|
14
|
+
* including the last "answer-introducer" phrase ("just output the token",
|
|
15
|
+
* "the answer is:", "output:", etc.). The stripped portion is returned as
|
|
16
|
+
* `thinking` so it can be routed to the thinking display channel; the
|
|
17
|
+
* remainder is the user-facing `answer`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isNemotronProseModel(model: string): boolean;
|
|
20
|
+
export declare function stripNemotronProse(text: string): {
|
|
21
|
+
thinking: string;
|
|
22
|
+
answer: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip leaked reasoning prose from Nemotron-family models.
|
|
3
|
+
*
|
|
4
|
+
* NVIDIA's Nemotron Omni reasoning model emits its chain of thought as plain
|
|
5
|
+
* text — without `<think>` tags or a separate reasoning_content channel — so
|
|
6
|
+
* the think-tag stripper can't catch it. The reasoning prose is then concatenated
|
|
7
|
+
* directly with the answer (often without even a separator), e.g.:
|
|
8
|
+
*
|
|
9
|
+
* "The user asks: ... According to instructions, we must obey. Just output
|
|
10
|
+
* the tokenOMNI_E2E_OK"
|
|
11
|
+
*
|
|
12
|
+
* This module detects the reasoning preamble (heuristic: leading sentence
|
|
13
|
+
* matches a known meta-reasoning opener) and strips everything up to and
|
|
14
|
+
* including the last "answer-introducer" phrase ("just output the token",
|
|
15
|
+
* "the answer is:", "output:", etc.). The stripped portion is returned as
|
|
16
|
+
* `thinking` so it can be routed to the thinking display channel; the
|
|
17
|
+
* remainder is the user-facing `answer`.
|
|
18
|
+
*/
|
|
19
|
+
const REASONING_OPENERS = [
|
|
20
|
+
/^the user (asks|wants|says|requested|is asking|wants me|wrote|just|said)/i,
|
|
21
|
+
/^looking at (this|the)/i,
|
|
22
|
+
/^based on (the|this)/i,
|
|
23
|
+
/^according to/i,
|
|
24
|
+
/^we (must|should|need)/i,
|
|
25
|
+
/^i (need|should|must|will|'ll|am going to|have to)\s/i,
|
|
26
|
+
/^let me/i,
|
|
27
|
+
/^there'?s? no need/i,
|
|
28
|
+
/^okay,?\s+(the user|so|let|i)/i,
|
|
29
|
+
/^alright,?\s+(the user|so|let|i)/i,
|
|
30
|
+
/^so,?\s+the user/i,
|
|
31
|
+
/^the question (is|asks)/i,
|
|
32
|
+
/^the prompt (is|says|asks)/i,
|
|
33
|
+
];
|
|
34
|
+
const ANSWER_INTRODUCERS = [
|
|
35
|
+
/\bjust\s+(?:output|respond|say|reply|return|emit|write|give|print)\s+(?:the|a|with|out|to|exactly|back|only)?\s*(?:token|word|answer|response|string|text|output|message)?\s*:?\s*/gi,
|
|
36
|
+
/\b(?:the|my)\s+(?:answer|response|token|output|reply)\s+is\s*:?\s*/gi,
|
|
37
|
+
/\bhere'?s?\s+(?:the|my)?\s*(?:response|answer|output|token|reply):?\s*/gi,
|
|
38
|
+
/(?:^|[\s.])(?:output|response|answer|reply|token)\s*:\s*/gi,
|
|
39
|
+
/\bi(?:'ll| will| shall)\s+(?:output|respond|say|reply|return|emit|write|give|print)\s+(?:the|a|with|out|to|exactly|back|only)?\s*(?:token|word|answer|response|string|text|output|message)?\s*:?\s*/gi,
|
|
40
|
+
];
|
|
41
|
+
export function isNemotronProseModel(model) {
|
|
42
|
+
return /^nvidia\/nemotron-3-nano-omni/i.test(model);
|
|
43
|
+
}
|
|
44
|
+
export function stripNemotronProse(text) {
|
|
45
|
+
if (!text)
|
|
46
|
+
return { thinking: '', answer: '' };
|
|
47
|
+
const leadingWhitespaceMatch = text.match(/^\s*/);
|
|
48
|
+
const leadingWhitespace = leadingWhitespaceMatch ? leadingWhitespaceMatch[0] : '';
|
|
49
|
+
const trimmed = text.slice(leadingWhitespace.length);
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
return { thinking: '', answer: text };
|
|
52
|
+
// Reject early: if no reasoning opener at the start, this isn't leaked prose.
|
|
53
|
+
if (!REASONING_OPENERS.some((p) => p.test(trimmed))) {
|
|
54
|
+
return { thinking: '', answer: text };
|
|
55
|
+
}
|
|
56
|
+
let lastEnd = -1;
|
|
57
|
+
for (const re of ANSWER_INTRODUCERS) {
|
|
58
|
+
const matches = [...trimmed.matchAll(re)];
|
|
59
|
+
for (const m of matches) {
|
|
60
|
+
const end = (m.index ?? 0) + m[0].length;
|
|
61
|
+
if (end > lastEnd)
|
|
62
|
+
lastEnd = end;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (lastEnd === -1) {
|
|
66
|
+
// Reasoning detected but no transition phrase found. Conservative: leave
|
|
67
|
+
// the text intact rather than swallow what might be a legitimate answer.
|
|
68
|
+
return { thinking: '', answer: text };
|
|
69
|
+
}
|
|
70
|
+
const thinking = leadingWhitespace + trimmed.slice(0, lastEnd);
|
|
71
|
+
const answer = trimmed.slice(lastEnd).replace(/^[\s.,:;\-—]+/, '');
|
|
72
|
+
// Don't return an empty answer — fall back to the original text so the user
|
|
73
|
+
// gets *something* even if our heuristic over-stripped.
|
|
74
|
+
if (!answer)
|
|
75
|
+
return { thinking: '', answer: text };
|
|
76
|
+
return { thinking, answer };
|
|
77
|
+
}
|
package/dist/agent/tokens.js
CHANGED
|
@@ -197,6 +197,9 @@ const MODEL_CONTEXT_WINDOWS = {
|
|
|
197
197
|
'moonshot/kimi-k2.6': 256_000,
|
|
198
198
|
'moonshot/kimi-k2.5': 128_000,
|
|
199
199
|
'minimax/minimax-m2.7': 128_000,
|
|
200
|
+
// NVIDIA-hosted free tier (2026-04-29 V4 Flash + Omni launch)
|
|
201
|
+
'nvidia/deepseek-v4-flash': 1_000_000,
|
|
202
|
+
'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning': 256_000,
|
|
200
203
|
};
|
|
201
204
|
/**
|
|
202
205
|
* Get the context window size for a model, with a conservative default.
|
package/dist/commands/start.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getOrCreateWallet, getOrCreateSolanaWallet } from '@blockrun/llm';
|
|
|
3
3
|
import { loadChain, API_URLS } from '../config.js';
|
|
4
4
|
import { retryFetchBalance } from './balance-retry.js';
|
|
5
5
|
import { flushStats, loadStats } from '../stats/tracker.js';
|
|
6
|
-
import { OPUS_PRICING } from '../pricing.js';
|
|
6
|
+
import { OPUS_PRICING, MODEL_PRICING } from '../pricing.js';
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
8
8
|
import { printBanner } from '../banner.js';
|
|
9
9
|
import { assembleInstructions } from '../agent/context.js';
|
|
@@ -126,17 +126,11 @@ export async function startCommand(options) {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
// Warn when a paid model is active so users know they'll be charged.
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
'nvidia/mistral-small-4-119b',
|
|
135
|
-
'nvidia/llama-4-maverick',
|
|
136
|
-
'nvidia/deepseek-v3.2',
|
|
137
|
-
'blockrun/free',
|
|
138
|
-
]);
|
|
139
|
-
if (!FREE_MODELS.has(model)) {
|
|
129
|
+
// Derive "free" from MODEL_PRICING so adding a new free entry there is enough —
|
|
130
|
+
// no second hardcoded list to keep in sync.
|
|
131
|
+
const pricing = MODEL_PRICING[model];
|
|
132
|
+
const isFree = pricing != null && pricing.input === 0 && pricing.output === 0 && (pricing.perCall ?? 0) === 0;
|
|
133
|
+
if (!isFree) {
|
|
140
134
|
console.log(chalk.yellow(` Model: ${model} (paid — charges from your wallet per call)`));
|
|
141
135
|
console.log(chalk.dim(` Switch to free with: /model free\n`));
|
|
142
136
|
}
|
package/dist/pricing.js
CHANGED
|
@@ -8,7 +8,9 @@ export const MODEL_PRICING = {
|
|
|
8
8
|
'blockrun/eco': { input: 0.2, output: 1.0 },
|
|
9
9
|
'blockrun/premium': { input: 3.0, output: 15.0 },
|
|
10
10
|
'blockrun/free': { input: 0, output: 0 },
|
|
11
|
-
// FREE — BlockRun gateway free tier (refreshed 2026-04)
|
|
11
|
+
// FREE — BlockRun gateway free tier (refreshed 2026-04-29 with V4 Flash + Omni launch)
|
|
12
|
+
'nvidia/deepseek-v4-flash': { input: 0, output: 0 },
|
|
13
|
+
'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning': { input: 0, output: 0 },
|
|
12
14
|
'nvidia/glm-4.7': { input: 0, output: 0 },
|
|
13
15
|
'nvidia/qwen3-next-80b-a3b-thinking': { input: 0, output: 0 },
|
|
14
16
|
'nvidia/qwen3-coder-480b': { input: 0, output: 0 },
|
package/dist/tools/imagegen.js
CHANGED
|
@@ -118,7 +118,7 @@ function buildExecute(deps) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
let imageModel = model || (referenceImage ? 'openai/gpt-image-2' : 'openai/gpt-image-1');
|
|
121
|
-
|
|
121
|
+
let imageSize = size || '1024x1024';
|
|
122
122
|
let chosenPrompt = prompt;
|
|
123
123
|
// Skip the proposal flow when a reference image is set: the media router
|
|
124
124
|
// doesn't know which models support image-to-image, so its suggestions
|
|
@@ -171,6 +171,12 @@ function buildExecute(deps) {
|
|
|
171
171
|
// Router / AskUser failed — fall back to default model silently.
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
// gpt-image-2 reliably serves 1024x1024 only — other sizes time out at
|
|
175
|
+
// the gateway. Force the supported size regardless of caller / router
|
|
176
|
+
// input so we never burn USDC on a request that's going to abort.
|
|
177
|
+
if (imageModel === 'openai/gpt-image-2' && imageSize !== '1024x1024') {
|
|
178
|
+
imageSize = '1024x1024';
|
|
179
|
+
}
|
|
174
180
|
if (contentId && deps.library) {
|
|
175
181
|
const decision = checkImageBudget(deps.library, contentId, imageModel, imageSize);
|
|
176
182
|
if (!decision.ok) {
|
|
@@ -427,7 +433,7 @@ export function createImageGenCapability(deps = {}) {
|
|
|
427
433
|
properties: {
|
|
428
434
|
prompt: { type: 'string', description: 'Text description of the image to generate' },
|
|
429
435
|
output_path: { type: 'string', description: 'Where to save the image. Default: generated-<timestamp>.png in working directory' },
|
|
430
|
-
size: { type: 'string', description: 'Image size: 1024x1024, 1792x1024, or 1024x1792. Default: 1024x1024' },
|
|
436
|
+
size: { type: 'string', description: 'Image size: 1024x1024, 1792x1024, or 1024x1792. Default: 1024x1024. Note: openai/gpt-image-2 is forced to 1024x1024 (other sizes time out at the gateway).' },
|
|
431
437
|
model: { type: 'string', description: 'Image model to use. Default: openai/gpt-image-1' },
|
|
432
438
|
image_url: { type: 'string', description: 'Optional reference image (image-to-image / style transfer). Accepts an http(s) URL, a data URI, or a local file path. Only works with edit-capable models.' },
|
|
433
439
|
contentId: { type: 'string', description: 'Optional Content id to attach this generation to. Pre-flight budget check + auto-record on success.' },
|
package/package.json
CHANGED