@blockrun/franklin 3.9.2 → 3.9.4
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 +68 -14
- package/dist/agent/loop.js +4 -2
- package/dist/agent/tokens.js +3 -0
- package/dist/commands/start.js +6 -12
- package/dist/pricing.js +3 -1
- package/dist/ui/model-picker.js +7 -8
- 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
|
@@ -170,6 +170,23 @@ export function classifyToolCallFailure(toolName, rawInput, signal, model) {
|
|
|
170
170
|
`Preview: ${preview}${rawInput.length > 120 ? '…' : ''} — ` +
|
|
171
171
|
`this is usually a model output bug; try \`/model <other>\` or retry.]`;
|
|
172
172
|
}
|
|
173
|
+
export function isRoleplayedJsonToolCallText(text) {
|
|
174
|
+
const trimmed = text.trim();
|
|
175
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
|
|
176
|
+
return false;
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(trimmed);
|
|
179
|
+
return (parsed !== null &&
|
|
180
|
+
typeof parsed === 'object' &&
|
|
181
|
+
!Array.isArray(parsed) &&
|
|
182
|
+
parsed.type === 'function' &&
|
|
183
|
+
typeof parsed.name === 'string' &&
|
|
184
|
+
('parameters' in parsed || 'arguments' in parsed));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
173
190
|
function applyAnthropicPromptCaching(payload, request) {
|
|
174
191
|
const out = { ...payload };
|
|
175
192
|
const cacheMarker = { type: 'ephemeral' };
|
|
@@ -402,6 +419,7 @@ export class ModelClient {
|
|
|
402
419
|
let currentToolId = '';
|
|
403
420
|
let currentToolName = '';
|
|
404
421
|
let currentToolInput = '';
|
|
422
|
+
const textEmission = { mode: 'undecided' };
|
|
405
423
|
// Split inline <think>…</think> emitted by reasoning models (nemotron,
|
|
406
424
|
// deepseek-r1, qwq, etc.) that use the text field instead of the native
|
|
407
425
|
// thinking block. Thinking emitted this way is display-only — we don't
|
|
@@ -413,6 +431,24 @@ export class ModelClient {
|
|
|
413
431
|
// system-prompt guard in loop.ts is responsible for preventing this.
|
|
414
432
|
// Debug-only because the user already sees the literal text in the UI.
|
|
415
433
|
let toolCallRoleplayWarned = false;
|
|
434
|
+
const appendText = (text) => {
|
|
435
|
+
if (!text)
|
|
436
|
+
return;
|
|
437
|
+
currentText += text;
|
|
438
|
+
if (textEmission.mode === 'undecided') {
|
|
439
|
+
const trimmed = currentText.trimStart();
|
|
440
|
+
if (!trimmed)
|
|
441
|
+
return;
|
|
442
|
+
textEmission.mode = trimmed.startsWith('{') ? 'hold' : 'stream';
|
|
443
|
+
if (textEmission.mode === 'stream') {
|
|
444
|
+
onStreamDelta?.({ type: 'text', text: currentText });
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (textEmission.mode === 'stream') {
|
|
449
|
+
onStreamDelta?.({ type: 'text', text });
|
|
450
|
+
}
|
|
451
|
+
};
|
|
416
452
|
for await (const chunk of this.streamCompletion(request, signal)) {
|
|
417
453
|
switch (chunk.kind) {
|
|
418
454
|
case 'content_block_start': {
|
|
@@ -429,6 +465,7 @@ export class ModelClient {
|
|
|
429
465
|
}
|
|
430
466
|
else if (cblock?.type === 'text') {
|
|
431
467
|
currentText = '';
|
|
468
|
+
textEmission.mode = 'undecided';
|
|
432
469
|
textStripper = new ThinkTagStripper();
|
|
433
470
|
}
|
|
434
471
|
break;
|
|
@@ -458,9 +495,7 @@ export class ModelClient {
|
|
|
458
495
|
}
|
|
459
496
|
for (const seg of textStripper.push(raw)) {
|
|
460
497
|
if (seg.type === 'text') {
|
|
461
|
-
|
|
462
|
-
if (seg.text)
|
|
463
|
-
onStreamDelta?.({ type: 'text', text: seg.text });
|
|
498
|
+
appendText(seg.text);
|
|
464
499
|
}
|
|
465
500
|
else if (seg.text) {
|
|
466
501
|
onStreamDelta?.({ type: 'thinking', text: seg.text });
|
|
@@ -537,20 +572,30 @@ export class ModelClient {
|
|
|
537
572
|
// Flush any partial tag held in the stripper
|
|
538
573
|
for (const seg of textStripper.flush()) {
|
|
539
574
|
if (seg.type === 'text') {
|
|
540
|
-
|
|
541
|
-
if (seg.text)
|
|
542
|
-
onStreamDelta?.({ type: 'text', text: seg.text });
|
|
575
|
+
appendText(seg.text);
|
|
543
576
|
}
|
|
544
577
|
else if (seg.text) {
|
|
545
578
|
onStreamDelta?.({ type: 'thinking', text: seg.text });
|
|
546
579
|
}
|
|
547
580
|
}
|
|
548
581
|
if (currentText) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
582
|
+
if (textEmission.mode === 'hold' && isRoleplayedJsonToolCallText(currentText)) {
|
|
583
|
+
if (this.debug) {
|
|
584
|
+
console.error(`[franklin] Model ${request.model} emitted a raw JSON function-call object as text. ` +
|
|
585
|
+
'Treating it as non-productive output so recovery can try another model.');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
if (textEmission.mode !== 'stream') {
|
|
590
|
+
onStreamDelta?.({ type: 'text', text: currentText });
|
|
591
|
+
}
|
|
592
|
+
collected.push({
|
|
593
|
+
type: 'text',
|
|
594
|
+
text: currentText,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
553
597
|
currentText = '';
|
|
598
|
+
textEmission.mode = 'undecided';
|
|
554
599
|
}
|
|
555
600
|
}
|
|
556
601
|
break;
|
|
@@ -588,16 +633,25 @@ export class ModelClient {
|
|
|
588
633
|
// Flush any remaining text (stream ended without content_block_stop)
|
|
589
634
|
for (const seg of textStripper.flush()) {
|
|
590
635
|
if (seg.type === 'text') {
|
|
591
|
-
|
|
592
|
-
if (seg.text)
|
|
593
|
-
onStreamDelta?.({ type: 'text', text: seg.text });
|
|
636
|
+
appendText(seg.text);
|
|
594
637
|
}
|
|
595
638
|
else if (seg.text) {
|
|
596
639
|
onStreamDelta?.({ type: 'thinking', text: seg.text });
|
|
597
640
|
}
|
|
598
641
|
}
|
|
599
642
|
if (currentText) {
|
|
600
|
-
|
|
643
|
+
if (textEmission.mode === 'hold' && isRoleplayedJsonToolCallText(currentText)) {
|
|
644
|
+
if (this.debug) {
|
|
645
|
+
console.error(`[franklin] Model ${request.model} emitted a raw JSON function-call object as text. ` +
|
|
646
|
+
'Treating it as non-productive output so recovery can try another model.');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
if (textEmission.mode !== 'stream') {
|
|
651
|
+
onStreamDelta?.({ type: 'text', text: currentText });
|
|
652
|
+
}
|
|
653
|
+
collected.push({ type: 'text', text: currentText });
|
|
654
|
+
}
|
|
601
655
|
}
|
|
602
656
|
return { content: collected, usage, stopReason };
|
|
603
657
|
}
|
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
|
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/ui/model-picker.js
CHANGED
|
@@ -118,26 +118,26 @@ export const PICKER_CATEGORIES = [
|
|
|
118
118
|
],
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
|
+
// Picker trim (v3.9.3): hide superseded / awkward-middle / niche-premium
|
|
122
|
+
// entries to bring choice paralysis down. Their shortcuts (`opus-4.6`,
|
|
123
|
+
// `gpt-5.4`, `gpt-5.4-pro`, `grok`, `o1`, `o4`, `nano`) all stay live in
|
|
124
|
+
// MODEL_SHORTCUTS, so muscle memory keeps working — they just aren't
|
|
125
|
+
// listed in the visible picker. Same pattern v3.9.0 used to retire dead
|
|
126
|
+
// free-tier entries and v3.9.2 used to retire Kimi K2.5.
|
|
121
127
|
category: '✨ Premium frontier',
|
|
122
128
|
models: [
|
|
123
129
|
{ id: 'anthropic/claude-opus-4.7', shortcut: 'opus', label: 'Claude Opus 4.7', price: '$5/$25', highlight: true },
|
|
124
130
|
{ id: 'anthropic/claude-sonnet-4.6', shortcut: 'sonnet', label: 'Claude Sonnet 4.6', price: '$3/$15' },
|
|
125
|
-
{ id: 'anthropic/claude-opus-4.6', shortcut: 'opus-4.6', label: 'Claude Opus 4.6', price: '$5/$25' },
|
|
126
131
|
{ id: 'openai/gpt-5.5', shortcut: 'gpt', label: 'GPT-5.5', price: '$5/$30', highlight: true },
|
|
127
|
-
{ id: 'openai/gpt-5.4', shortcut: 'gpt-5.4', label: 'GPT-5.4', price: '$2.5/$15' },
|
|
128
|
-
{ id: 'openai/gpt-5.4-pro', shortcut: 'gpt-5.4-pro', label: 'GPT-5.4 Pro', price: '$30/$180' },
|
|
129
|
-
{ id: 'google/gemini-2.5-pro', shortcut: 'gemini', label: 'Gemini 2.5 Pro', price: '$1.25/$10' },
|
|
130
132
|
{ id: 'google/gemini-3.1-pro', shortcut: 'gemini-3', label: 'Gemini 3.1 Pro', price: '$2/$12' },
|
|
133
|
+
{ id: 'google/gemini-2.5-pro', shortcut: 'gemini', label: 'Gemini 2.5 Pro', price: '$1.25/$10' },
|
|
131
134
|
{ id: 'xai/grok-4-0709', shortcut: 'grok-4', label: 'Grok 4', price: '$0.2/$1.5' },
|
|
132
|
-
{ id: 'xai/grok-3', shortcut: 'grok', label: 'Grok 3', price: '$3/$15' },
|
|
133
135
|
],
|
|
134
136
|
},
|
|
135
137
|
{
|
|
136
138
|
category: '🔬 Reasoning',
|
|
137
139
|
models: [
|
|
138
140
|
{ id: 'openai/o3', shortcut: 'o3', label: 'O3', price: '$2/$8' },
|
|
139
|
-
{ id: 'openai/o4-mini', shortcut: 'o4', label: 'O4 Mini', price: '$1.1/$4.4' },
|
|
140
|
-
{ id: 'openai/o1', shortcut: 'o1', label: 'O1', price: '$15/$60' },
|
|
141
141
|
{ id: 'openai/gpt-5.3-codex', shortcut: 'codex', label: 'GPT-5.3 Codex', price: '$1.75/$14' },
|
|
142
142
|
{ id: 'deepseek/deepseek-reasoner', shortcut: 'r1', label: 'DeepSeek R1', price: '$0.28/$0.42' },
|
|
143
143
|
{ id: 'xai/grok-4-1-fast-reasoning', shortcut: 'grok-fast', label: 'Grok 4.1 Fast R.', price: '$0.2/$0.5' },
|
|
@@ -148,7 +148,6 @@ export const PICKER_CATEGORIES = [
|
|
|
148
148
|
models: [
|
|
149
149
|
{ id: 'anthropic/claude-haiku-4.5-20251001', shortcut: 'haiku', label: 'Claude Haiku 4.5', price: '$1/$5' },
|
|
150
150
|
{ id: 'openai/gpt-5-mini', shortcut: 'mini', label: 'GPT-5 Mini', price: '$0.25/$2' },
|
|
151
|
-
{ id: 'openai/gpt-5-nano', shortcut: 'nano', label: 'GPT-5 Nano', price: '$0.05/$0.4' },
|
|
152
151
|
{ id: 'google/gemini-2.5-flash', shortcut: 'flash', label: 'Gemini 2.5 Flash', price: '$0.3/$2.5' },
|
|
153
152
|
{ id: 'deepseek/deepseek-chat', shortcut: 'deepseek', label: 'DeepSeek V3', price: '$0.28/$0.42' },
|
|
154
153
|
{ id: 'moonshot/kimi-k2.6', shortcut: 'kimi', label: 'Kimi K2.6', price: '$0.95/$4' },
|
package/package.json
CHANGED