@blockrun/franklin 3.6.9 → 3.6.11

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.
@@ -627,7 +627,7 @@ export async function handleSlashCommand(input, ctx) {
627
627
  const newModel = resolveModel(input.slice(7).trim());
628
628
  ctx.config.model = newModel;
629
629
  ctx.config.baseModel = newModel; // Update recovery target so loop doesn't reset
630
- ctx.config.onModelChange?.(newModel);
630
+ ctx.config.onModelChange?.(newModel, 'user');
631
631
  ctx.onEvent({ kind: 'text_delta', text: `Model → **${newModel}**\n` });
632
632
  }
633
633
  emitDone(ctx);
package/dist/agent/llm.js CHANGED
@@ -105,14 +105,15 @@ export class ModelClient {
105
105
  catch {
106
106
  // Router not available (e.g. old build) — use hardcoded fallback table
107
107
  }
108
- // Static fallback if router is unavailable
108
+ // Static fallback if router is unavailable. Default to FREE model so
109
+ // users aren't silently charged when their intended model can't resolve.
109
110
  const FALLBACKS = {
110
- 'blockrun/auto': 'zai/glm-5.1',
111
+ 'blockrun/auto': 'nvidia/nemotron-ultra-253b',
111
112
  'blockrun/eco': 'nvidia/nemotron-ultra-253b',
112
113
  'blockrun/premium': 'anthropic/claude-sonnet-4.6',
113
114
  'blockrun/free': 'nvidia/nemotron-ultra-253b',
114
115
  };
115
- return FALLBACKS[model] || 'zai/glm-5.1';
116
+ return FALLBACKS[model] || 'nvidia/nemotron-ultra-253b';
116
117
  }
117
118
  async *streamCompletion(request, signal) {
118
119
  // Resolve virtual models before any API call
@@ -298,7 +298,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
298
298
  const baseModel = config.baseModel ?? config.model;
299
299
  if (config.model !== baseModel && !paymentFailedModels.has(baseModel)) {
300
300
  config.model = baseModel;
301
- config.onModelChange?.(baseModel);
301
+ config.onModelChange?.(baseModel, 'system');
302
302
  }
303
303
  turnFailedModels = new Set(); // Fresh slate for transient failures this turn
304
304
  const abort = new AbortController();
@@ -589,7 +589,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
589
589
  if (nextFree) {
590
590
  const oldModel = config.model;
591
591
  config.model = nextFree;
592
- config.onModelChange?.(nextFree);
592
+ config.onModelChange?.(nextFree, 'system');
593
593
  onEvent({ kind: 'text_delta', text: `\n*${oldModel} failed — switching to ${nextFree}*\n` });
594
594
  continue; // Retry with next model
595
595
  }
@@ -140,8 +140,10 @@ export interface AgentConfig {
140
140
  permissionPromptFn?: (toolName: string, description: string) => Promise<'yes' | 'no' | 'always'>;
141
141
  /** Routes AskUser questions through ink UI input to avoid raw-mode stdin conflict */
142
142
  onAskUser?: (question: string, options?: string[]) => Promise<string>;
143
- /** Notify UI when agent switches model (e.g. payment fallback) */
144
- onModelChange?: (model: string) => void;
143
+ /** Notify UI when agent switches model. `reason` is 'user' for explicit /model
144
+ * commands and 'system' for payment fallbacks or recovery. User-initiated
145
+ * changes must also update `baseModel`. */
146
+ onModelChange?: (model: string, reason?: 'user' | 'system') => void;
145
147
  /** The user's intended model — updated by /model command, used for turn recovery */
146
148
  baseModel?: string;
147
149
  }
@@ -17,7 +17,8 @@ export async function startCommand(options) {
17
17
  const chain = loadChain();
18
18
  const apiUrl = API_URLS[chain];
19
19
  const config = loadConfig();
20
- // Resolve model — default to GLM-5.1 promo if nothing specified
20
+ // Resolve model — default to FREE (nemotron) so users don't get surprise charges.
21
+ // Paid models (glm-5.1, sonnet, opus, etc.) must be opted in with --model or /model.
21
22
  let model;
22
23
  const configModel = config['default-model'];
23
24
  if (options.model) {
@@ -27,9 +28,19 @@ export async function startCommand(options) {
27
28
  model = configModel;
28
29
  }
29
30
  else {
30
- // Default: GLM-5.1 promo if still active, otherwise Gemini Flash (cheap & reliable)
31
- const promoExpiry = new Date('2026-04-15');
32
- model = Date.now() < promoExpiry.getTime() ? 'zai/glm-5.1' : 'google/gemini-2.5-flash';
31
+ // Default: free NVIDIA model zero wallet charges until user explicitly switches
32
+ model = 'nvidia/nemotron-ultra-253b';
33
+ }
34
+ // Warn when a paid model is active so users know they'll be charged
35
+ const FREE_MODELS = new Set([
36
+ 'nvidia/nemotron-ultra-253b',
37
+ 'nvidia/qwen3-coder-480b',
38
+ 'nvidia/devstral-2-123b',
39
+ 'blockrun/free',
40
+ ]);
41
+ if (!FREE_MODELS.has(model)) {
42
+ console.log(chalk.yellow(` Model: ${model} (paid — charges from your wallet per call)`));
43
+ console.log(chalk.dim(` Switch to free with: /model free\n`));
33
44
  }
34
45
  // Auto-create wallet if needed (no interruption — free models work without funding)
35
46
  let walletAddress = '';
@@ -184,8 +195,13 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
184
195
  walletAddress: walletInfo?.address,
185
196
  walletBalance: walletInfo?.balance,
186
197
  chain: walletInfo?.chain,
187
- onModelChange: (newModel) => {
198
+ onModelChange: (newModel, reason) => {
188
199
  agentConfig.model = newModel;
200
+ // User-initiated switch must also update baseModel so the agent loop
201
+ // doesn't revert to the previous model on the next turn.
202
+ if (reason === 'user') {
203
+ agentConfig.baseModel = newModel;
204
+ }
189
205
  },
190
206
  });
191
207
  // Wire permission prompts through Ink UI to avoid stdin/readline conflict.
@@ -329,12 +345,14 @@ async function handleSlashCommand(cmd, config, ui) {
329
345
  const newModel = parts[1];
330
346
  if (newModel) {
331
347
  config.model = resolveModel(newModel);
348
+ config.baseModel = config.model;
332
349
  console.error(chalk.green(` Model → ${config.model}`));
333
350
  return null;
334
351
  }
335
352
  const picked = await pickModel(config.model);
336
353
  if (picked) {
337
354
  config.model = picked;
355
+ config.baseModel = picked;
338
356
  console.error(chalk.green(` Model → ${config.model}`));
339
357
  }
340
358
  return null;
@@ -343,6 +361,7 @@ async function handleSlashCommand(cmd, config, ui) {
343
361
  const picked = await pickModel(config.model);
344
362
  if (picked) {
345
363
  config.model = picked;
364
+ config.baseModel = picked;
346
365
  console.error(chalk.green(` Model → ${config.model}`));
347
366
  }
348
367
  return null;
package/dist/ui/app.d.ts CHANGED
@@ -22,5 +22,5 @@ export declare function launchInkUI(opts: {
22
22
  walletBalance?: string;
23
23
  chain?: string;
24
24
  showPicker?: boolean;
25
- onModelChange?: (model: string) => void;
25
+ onModelChange?: (model: string, reason?: 'user' | 'system') => void;
26
26
  }): InkUIHandle;
package/dist/ui/app.js CHANGED
@@ -219,7 +219,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
219
219
  else if (key.return) {
220
220
  const selected = PICKER_MODELS_FLAT[pickerIdx];
221
221
  setCurrentModel(selected.id);
222
- onModelChange(selected.id);
222
+ onModelChange(selected.id, 'user');
223
223
  showStatus(`Model → ${selected.label}`, 'success', 3000);
224
224
  setMode('input');
225
225
  setReady(true);
@@ -290,7 +290,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
290
290
  if (parts[1]) {
291
291
  const resolved = resolveModel(parts[1]);
292
292
  setCurrentModel(resolved);
293
- onModelChange(resolved);
293
+ onModelChange(resolved, 'user');
294
294
  showStatus(`Model → ${resolved}`, 'success', 3000);
295
295
  }
296
296
  else {
@@ -685,7 +685,7 @@ export function launchInkUI(opts) {
685
685
  // Agent loop hasn't called waitForInput yet — queue the input
686
686
  pendingInput = value;
687
687
  }
688
- }, onModelChange: (model) => { opts.onModelChange?.(model); }, onAbort: () => { abortCallback?.(); }, onExit: () => {
688
+ }, onModelChange: (model, reason) => { opts.onModelChange?.(model, reason); }, onAbort: () => { abortCallback?.(); }, onExit: () => {
689
689
  exiting = true;
690
690
  if (resolveInput) {
691
691
  resolveInput(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.6.9",
3
+ "version": "3.6.11",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {