@blockrun/franklin 3.25.3 → 3.26.0

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.
@@ -161,6 +161,7 @@ export declare class ModelClient {
161
161
  stopReason: string;
162
162
  }>;
163
163
  private signPayment;
164
+ private recordSettledPayment;
164
165
  private signBasePayment;
165
166
  private signSolanaPayment;
166
167
  private extractPaymentReq;
package/dist/agent/llm.js CHANGED
@@ -591,17 +591,18 @@ export class ModelClient {
591
591
  if (response.status === 402) {
592
592
  if (this.debug)
593
593
  console.error('[franklin] Payment required — signing...');
594
- const paymentHeader = await this.signPayment(response, request.model);
595
- if (!paymentHeader) {
594
+ const signedPayment = await this.signPayment(response, request.model);
595
+ if (!signedPayment) {
596
596
  yield { kind: 'error', payload: { message: 'Payment signing failed' } };
597
597
  return;
598
598
  }
599
599
  response = await withAbortableTimeout(() => fetchWithUnwrappedCause(endpoint, {
600
600
  method: 'POST',
601
- headers: { ...headers, ...paymentHeader },
601
+ headers: { ...headers, ...signedPayment.headers },
602
602
  body,
603
603
  signal: requestController.signal,
604
604
  }), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
605
+ this.recordSettledPayment(signedPayment, response);
605
606
  }
606
607
  if (!response.ok) {
607
608
  const errorBody = await response.text().catch(() => 'unknown error');
@@ -653,17 +654,18 @@ export class ModelClient {
653
654
  signal: requestController.signal,
654
655
  }), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
655
656
  if (response.status === 402) {
656
- const paymentHeader = await this.signPayment(response, request.model);
657
- if (!paymentHeader) {
657
+ const signedPayment = await this.signPayment(response, request.model);
658
+ if (!signedPayment) {
658
659
  yield { kind: 'error', payload: { message: 'Payment signing failed' } };
659
660
  return;
660
661
  }
661
662
  response = await withAbortableTimeout(() => fetchWithUnwrappedCause(endpoint, {
662
663
  method: 'POST',
663
- headers: { ...headers, ...paymentHeader },
664
+ headers: { ...headers, ...signedPayment.headers },
664
665
  body: retryBody,
665
666
  signal: requestController.signal,
666
667
  }), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
668
+ this.recordSettledPayment(signedPayment, response);
667
669
  }
668
670
  if (!response.ok) {
669
671
  const retryBodyText = await response.text().catch(() => 'unknown error');
@@ -1101,6 +1103,15 @@ export class ModelClient {
1101
1103
  return null;
1102
1104
  }
1103
1105
  }
1106
+ recordSettledPayment(payment, response) {
1107
+ // A post-signature 402 means the gateway rejected the payment rather than
1108
+ // settling it. Keep both the session cost and cost_log anchored to calls
1109
+ // the gateway accepted after the paid retry returned.
1110
+ if (response.status === 402)
1111
+ return;
1112
+ this.lastPaidUsd += payment.amountUsd;
1113
+ appendSettlementRow(payment.endpoint, payment.amountUsd, payment.meta);
1114
+ }
1104
1115
  async signBasePayment(response, model) {
1105
1116
  // Refresh wallet cache after TTL to pick up balance/key changes
1106
1117
  if (!this.cachedBaseWallet || (Date.now() - this.walletCacheTime > ModelClient.WALLET_CACHE_TTL)) {
@@ -1116,25 +1127,24 @@ export class ModelClient {
1116
1127
  throw new Error('No payment requirements in 402 response');
1117
1128
  const paymentRequired = parsePaymentRequired(paymentHeader);
1118
1129
  const details = extractPaymentDetails(paymentRequired);
1119
- this.lastPaidUsd = paymentAmountToUsd(details.amount);
1120
- // Mirror the SDK's appendCostLog write so cost_log.jsonl becomes a
1121
- // true wallet-truth ledger covering both SDK helper traffic AND the
1122
- // agent's main LLM stream (which uses this signer, not the SDK).
1123
- // Match SDK schema (model/wallet/network/client_kind) so every row
1124
- // is independently queryable.
1125
- appendSettlementRow('/v1/messages', this.lastPaidUsd, {
1126
- model,
1127
- wallet: wallet.address,
1128
- network: details.network || 'base-mainnet',
1129
- client_kind: 'AgentClient',
1130
- });
1130
+ const amountUsd = paymentAmountToUsd(details.amount);
1131
1131
  const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
1132
1132
  resourceUrl: details.resource?.url || this.apiUrl,
1133
1133
  resourceDescription: details.resource?.description || 'BlockRun AI API call',
1134
1134
  maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
1135
1135
  extra: details.extra,
1136
1136
  });
1137
- return { 'PAYMENT-SIGNATURE': payload };
1137
+ return {
1138
+ headers: { 'PAYMENT-SIGNATURE': payload },
1139
+ endpoint: '/v1/messages',
1140
+ amountUsd,
1141
+ meta: {
1142
+ model,
1143
+ wallet: wallet.address,
1144
+ network: details.network || 'base-mainnet',
1145
+ client_kind: 'AgentClient',
1146
+ },
1147
+ };
1138
1148
  }
1139
1149
  async signSolanaPayment(response, model) {
1140
1150
  if (!this.cachedSolanaWallet || (Date.now() - this.walletCacheTime > ModelClient.WALLET_CACHE_TTL)) {
@@ -1149,13 +1159,7 @@ export class ModelClient {
1149
1159
  throw new Error('No payment requirements in 402 response');
1150
1160
  const paymentRequired = parsePaymentRequired(paymentHeader);
1151
1161
  const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
1152
- this.lastPaidUsd = paymentAmountToUsd(details.amount);
1153
- appendSettlementRow('/v1/messages', this.lastPaidUsd, {
1154
- model,
1155
- wallet: wallet.address,
1156
- network: details.network || 'solana-mainnet',
1157
- client_kind: 'AgentClient',
1158
- });
1162
+ const amountUsd = paymentAmountToUsd(details.amount);
1159
1163
  const secretBytes = await solanaKeyToBytes(wallet.privateKey);
1160
1164
  const feePayer = details.extra?.feePayer || details.recipient;
1161
1165
  const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
@@ -1164,7 +1168,17 @@ export class ModelClient {
1164
1168
  maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
1165
1169
  extra: details.extra,
1166
1170
  });
1167
- return { 'PAYMENT-SIGNATURE': payload };
1171
+ return {
1172
+ headers: { 'PAYMENT-SIGNATURE': payload },
1173
+ endpoint: '/v1/messages',
1174
+ amountUsd,
1175
+ meta: {
1176
+ model,
1177
+ wallet: wallet.address,
1178
+ network: details.network || 'solana-mainnet',
1179
+ client_kind: 'AgentClient',
1180
+ },
1181
+ };
1168
1182
  }
1169
1183
  async extractPaymentReq(response) {
1170
1184
  let header = response.headers.get('payment-required');
package/dist/pricing.js CHANGED
@@ -64,17 +64,19 @@ export const MODEL_PRICING = {
64
64
  'xai/grok-4-fast-reasoning': { input: 0.2, output: 0.5 },
65
65
  'xai/grok-4-1-fast': { input: 0.2, output: 0.5 },
66
66
  'xai/grok-4-1-fast-reasoning': { input: 0.2, output: 0.5 },
67
- 'xai/grok-4-0709': { input: 0.2, output: 1.5 },
67
+ 'xai/grok-4-0709': { input: 3.0, output: 15.0 }, // gateway lists $3/$15 (was mispriced here at $0.2/$1.5)
68
68
  'xai/grok-3-mini': { input: 0.3, output: 0.5 },
69
69
  'xai/grok-2-vision': { input: 2.0, output: 10.0 },
70
70
  'xai/grok-3': { input: 3.0, output: 15.0 },
71
+ 'xai/grok-4.3': { input: 1.5, output: 4.0 }, // public flagship 2026-06-04, OpenRouter resale
72
+ 'xai/grok-build-0.1': { input: 1.5, output: 3.0 }, // agentic coding, OpenRouter resale
71
73
  // DeepSeek (gateway re-aliased these to V4 Flash on 2026-05-03; price
72
74
  // dropped from $0.28/$0.42 to $0.20/$0.40, context bumped 128K→1M).
73
75
  'deepseek/deepseek-chat': { input: 0.20, output: 0.40 },
74
76
  'deepseek/deepseek-reasoner': { input: 0.20, output: 0.40 },
75
77
  // V4 Pro (1.6T MoE / 49B active, 1M ctx, 65K out). 75% launch promo
76
78
  // through 2026-05-31 — list is $2.00/$4.00, promo is $0.50/$1.00.
77
- 'deepseek/deepseek-v4-pro': { input: 0.50, output: 1.00 },
79
+ 'deepseek/deepseek-v4-pro': { input: 0.435, output: 0.87 }, // 75% promo became permanent list after 2026-05-31
78
80
  // Minimax
79
81
  'minimax/minimax-m3': { input: 0.3, output: 1.2 },
80
82
  'minimax/minimax-m2.7': { input: 0.3, output: 1.2 },
@@ -87,7 +89,7 @@ export const MODEL_PRICING = {
87
89
  'nvidia/kimi-k2.5': { input: 0.55, output: 2.5 },
88
90
  // PROMOTION (active ~2026-04): flat $0.001/call for all GLM models
89
91
  'zai/glm-5': { input: 0, output: 0, perCall: 0.001 },
90
- 'zai/glm-5.1': { input: 0, output: 0, perCall: 0.001 },
92
+ 'zai/glm-5.1': { input: 1.40, output: 4.40 }, // launch promo ended 2026-06-05 — per-token now
91
93
  'zai/glm-5-turbo': { input: 0, output: 0, perCall: 0.001 },
92
94
  'zai/glm-5.1-turbo': { input: 0, output: 0, perCall: 0.001 }, // client alias for zai/glm-5-turbo
93
95
  };
@@ -767,12 +767,13 @@ async function handleBasePayment(response, url, method, headers, body, privateKe
767
767
  const paymentRequired = parsePaymentRequired(paymentHeader);
768
768
  const details = extractPaymentDetails(paymentRequired);
769
769
  const paidUsd = paymentAmountToUsd(details.amount);
770
- appendSettlementRow(extractEndpointPath(url), paidUsd, {
770
+ const endpoint = extractEndpointPath(url);
771
+ const settlementMeta = {
771
772
  model,
772
773
  wallet: fromAddress,
773
774
  network: details.network || 'base-mainnet',
774
775
  client_kind: 'ProxyClient',
775
- });
776
+ };
776
777
  const paymentPayload = await createPaymentPayload(privateKey, fromAddress, details.recipient, details.amount, details.network || 'eip155:8453', {
777
778
  resourceUrl: details.resource?.url || url,
778
779
  resourceDescription: details.resource?.description || 'BlockRun AI API call',
@@ -787,6 +788,9 @@ async function handleBasePayment(response, url, method, headers, body, privateKe
787
788
  },
788
789
  body: body || undefined,
789
790
  }, timeoutMs, `Paid proxy request for ${model}`);
791
+ if (paid.status === 402)
792
+ return { response: paid, paidUsd: 0 };
793
+ appendSettlementRow(endpoint, paidUsd, settlementMeta);
790
794
  return { response: paid, paidUsd };
791
795
  }
792
796
  // ======================================================================
@@ -800,12 +804,13 @@ async function handleSolanaPayment(response, url, method, headers, body, private
800
804
  const paymentRequired = parsePaymentRequired(paymentHeader);
801
805
  const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
802
806
  const paidUsd = paymentAmountToUsd(details.amount);
803
- appendSettlementRow(extractEndpointPath(url), paidUsd, {
807
+ const endpoint = extractEndpointPath(url);
808
+ const settlementMeta = {
804
809
  model,
805
810
  wallet: fromAddress,
806
811
  network: details.network || 'solana-mainnet',
807
812
  client_kind: 'ProxyClient',
808
- });
813
+ };
809
814
  const secretKey = await solanaKeyToBytes(privateKey);
810
815
  const feePayer = details.extra?.feePayer || details.recipient;
811
816
  const paymentPayload = await createSolanaPaymentPayload(secretKey, fromAddress, details.recipient, details.amount, feePayer, {
@@ -822,6 +827,9 @@ async function handleSolanaPayment(response, url, method, headers, body, private
822
827
  },
823
828
  body: body || undefined,
824
829
  }, timeoutMs, `Paid proxy request for ${model}`);
830
+ if (paid.status === 402)
831
+ return { response: paid, paidUsd: 0 };
832
+ appendSettlementRow(endpoint, paidUsd, settlementMeta);
825
833
  return { response: paid, paidUsd };
826
834
  }
827
835
  /**
@@ -36,7 +36,7 @@ function loadLearnedWeights() {
36
36
  }
37
37
  // ─── Tier Model Configs ───
38
38
  // Auto-routing strategy (post-DeepSeek-V4-Pro launch promo, 2026-05-03):
39
- // V4 Pro at $0.50/$1.00 with 1M context is the new sweet spot for SIMPLE +
39
+ // V4 Pro at $0.435/$0.87 with 1M context is the new sweet spot for SIMPLE +
40
40
  // MEDIUM agent work — Sonnet-quality reasoning at ~1/6 the price. Reserve
41
41
  // Opus only for genuinely complex multi-file/multi-decision tasks where
42
42
  // the model's wider context handling and tighter tool-use discipline still
@@ -78,7 +78,7 @@ const AUTO_TIERS = {
78
78
  * if none of them have vision, escalates to COMPLEX (Opus is always vision).
79
79
  *
80
80
  * Note: only applied when the caller signals needsVision=true. Without that
81
- * hint the classic per-tier defaults still rule — V4 Pro's $0.50/$1.00 promo
81
+ * hint the classic per-tier defaults still rule — V4 Pro's $0.435/$0.87 price
82
82
  * is the right SIMPLE/MEDIUM pick for text-only turns and we don't want to
83
83
  * blanket-upgrade everyone to a vision model.
84
84
  */
package/dist/ui/app.js CHANGED
@@ -61,6 +61,21 @@ function encodePasteBlock(content) {
61
61
  function encodeImageBlock(absolutePath) {
62
62
  return `${IMG_BLOCK_START}${Buffer.from(absolutePath, 'utf8').toString('base64')}${IMG_BLOCK_END}`;
63
63
  }
64
+ /**
65
+ * Probe the clipboard for an image and return the input-block to splice in at
66
+ * the cursor — an encoded `[IMG:…]` block on success, an inline
67
+ * `[Image rejected: …]` notice if the image was found but unusable, or null
68
+ * when there's no image. Shared by PromptTextInput's Ctrl+V path and VimInput
69
+ * (which renders instead of PromptTextInput in vim mode).
70
+ */
71
+ async function readClipboardImageInjection() {
72
+ const img = await tryReadClipboardImage();
73
+ if (img && 'path' in img)
74
+ return encodeImageBlock(img.path);
75
+ if (img && 'error' in img)
76
+ return `[Image rejected: ${img.error}] `;
77
+ return null;
78
+ }
64
79
  function decodeBlockPayload(token, startMarker, endMarker) {
65
80
  if (!token.startsWith(startMarker) || !token.endsWith(endMarker))
66
81
  return token;
@@ -126,6 +141,15 @@ function decodePromptValue(value) {
126
141
  }
127
142
  return decoded + value.slice(cursor);
128
143
  }
144
+ function promptValueForDisplay(value) {
145
+ let rendered = '';
146
+ let cursor = 0;
147
+ for (const block of findPasteBlocks(value)) {
148
+ rendered += value.slice(cursor, block.start) + pasteSummary(block);
149
+ cursor = block.end;
150
+ }
151
+ return rendered + value.slice(cursor);
152
+ }
129
153
  /**
130
154
  * Read the system clipboard, and if it currently holds an image, save it to
131
155
  * a temp file and return the absolute path. Otherwise return null.
@@ -378,6 +402,15 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
378
402
  onChange(nextValue);
379
403
  setCursorOffset(cursorOffsetRef.current);
380
404
  }, [onChange]);
405
+ const insertClipboardImageAt = useCallback((insertAt) => {
406
+ readClipboardImageInjection().then((injected) => {
407
+ if (!injected)
408
+ return; // no image on clipboard — nothing to do
409
+ const cur = valueRef.current;
410
+ const at = Math.min(insertAt, cur.length);
411
+ updateValue(cur.slice(0, at) + injected + cur.slice(at), at + injected.length);
412
+ }).catch(() => { });
413
+ }, [updateValue]);
381
414
  useInput((input, key) => {
382
415
  if (!focus)
383
416
  return;
@@ -393,7 +426,7 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
393
426
  pasteBufferRef.current = '';
394
427
  }
395
428
  if (key.return && !isPasting) {
396
- onSubmit(decodePromptValue(currentValue));
429
+ onSubmit(currentValue);
397
430
  return;
398
431
  }
399
432
  if (key.home || (key.ctrl && input === 'a')) {
@@ -434,6 +467,13 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
434
467
  }
435
468
  return;
436
469
  }
470
+ // Some Linux terminals do not emit a bracketed-paste event for image-only
471
+ // clipboard contents. Ctrl+V gives users a raw-key fallback that probes the
472
+ // same clipboard image path without relying on terminal paste behavior.
473
+ if (key.ctrl && input === 'v') {
474
+ insertClipboardImageAt(currentCursorOffset);
475
+ return;
476
+ }
437
477
  if (key.upArrow || key.downArrow || key.tab || key.ctrl || key.meta)
438
478
  return;
439
479
  let text = normalizeInputNewlines(stripPasteMarkers(input));
@@ -515,7 +555,7 @@ function PromptTextInput({ value, onChange, onSubmit, placeholder = '', focus =
515
555
  return _jsx(Text, { children: rendered });
516
556
  }
517
557
  function formatUserPromptForDisplay(value) {
518
- return `❯ ${decodePromptValue(value)}`;
558
+ return `❯ ${promptValueForDisplay(value)}`;
519
559
  }
520
560
  function disableTerminalAutoWrap() {
521
561
  if (!process.stdout.isTTY)
@@ -601,7 +641,7 @@ function InputBox({ input, setInput, onSubmit, model, balance, chain, walletTail
601
641
  const leadingGlyph = (awaitingApproval || awaitingAnswer)
602
642
  ? _jsx(Text, { color: "yellow", bold: true, children: "\u26A0 " })
603
643
  : (showSpinner ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null);
604
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: borderColor, borderDimColor: !borderColor, paddingX: 1, width: boxWidth, children: [leadingGlyph, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(PromptTextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", (() => {
644
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: borderColor, borderDimColor: !borderColor, paddingX: 1, width: boxWidth, children: [leadingGlyph, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange, onClipboardImage: readClipboardImageInjection })) : (_jsx(PromptTextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", (() => {
605
645
  // Color the balance by funding state. Real session 2026-05-04
606
646
  // had a user staring at "$0.08 USDC" in dim text wondering
607
647
  // whether it meant "out of money" or "wrong chain". Make
@@ -1042,17 +1082,20 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
1042
1082
  turnTierRef.current = undefined;
1043
1083
  turnSavingsRef.current = undefined;
1044
1084
  turnCtxPctRef.current = undefined;
1045
- onSubmit(lastPrompt);
1085
+ onSubmit(decodePromptValue(lastPrompt).trim());
1046
1086
  return;
1047
1087
  default:
1048
- // All other slash commands pass through to the agent loop's command registry
1088
+ // All other slash commands pass through to the agent loop's command registry.
1089
+ // Decode here too: a slash command can carry an encoded paste/image block
1090
+ // as an argument, and the registry expects real text / file paths,
1091
+ // not the encoded block sentinels.
1049
1092
  setStreamText('');
1050
1093
  setThinking(false);
1051
1094
  setThinkingText('');
1052
1095
  setTools(new Map());
1053
1096
  setWaiting(true);
1054
1097
  setReady(false);
1055
- onSubmit(trimmed);
1098
+ onSubmit(decodePromptValue(trimmed).trim());
1056
1099
  return;
1057
1100
  }
1058
1101
  }
@@ -1091,7 +1134,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
1091
1134
  turnTierRef.current = undefined;
1092
1135
  turnSavingsRef.current = undefined;
1093
1136
  turnCtxPctRef.current = undefined;
1094
- onSubmit(trimmed);
1137
+ onSubmit(decodePromptValue(trimmed).trim());
1095
1138
  }, [ready, currentModel, totalCost, onSubmit, onModelChange, requestExit, lastPrompt, inputHistory, showStatus]);
1096
1139
  // Mouse support — OFF by default because Node stdin is shared: mouse escape
1097
1140
  // sequences leak into Ink's input handler as typed text. Opt in with
@@ -50,8 +50,11 @@ export const MODEL_SHORTCUTS = {
50
50
  flash: 'google/gemini-2.5-flash',
51
51
  'gemini-3': 'google/gemini-3.1-pro',
52
52
  'gemini-3.1': 'google/gemini-3.1-pro',
53
- // xAI
54
- grok: 'xai/grok-3',
53
+ // xAI — grok-4.3 is the public flagship since 2026-06-04 (grok-3 and the
54
+ // fast families are hidden on the gateway; explicit IDs still resolve).
55
+ grok: 'xai/grok-4.3',
56
+ 'grok-4.3': 'xai/grok-4.3',
57
+ 'grok-build': 'xai/grok-build-0.1',
55
58
  'grok-3': 'xai/grok-3',
56
59
  'grok-4': 'xai/grok-4-0709',
57
60
  'grok-fast': 'xai/grok-4-1-fast-reasoning',
@@ -95,6 +98,7 @@ export const MODEL_SHORTCUTS = {
95
98
  'm3': 'minimax/minimax-m3',
96
99
  'm2.7': 'minimax/minimax-m2.7',
97
100
  glm: 'zai/glm-5.1',
101
+ 'glm-5': 'zai/glm-5',
98
102
  'glm-turbo': 'zai/glm-5-turbo',
99
103
  'glm5': 'zai/glm-5.1',
100
104
  kimi: 'moonshot/kimi-k2.6',
@@ -123,9 +127,12 @@ export function resolveModel(input) {
123
127
  */
124
128
  export const PICKER_CATEGORIES = [
125
129
  {
126
- category: '🔥 Promo (flat $0.001/call)',
130
+ // glm-5.1's launch promo ended 2026-06-05 (per-token $1.40/$4.40 now;
131
+ // shortcut `glm` still resolves to it). glm-5 / glm-5-turbo are permanent
132
+ // flat-rate SKUs (gateway billingMode: "flat"), not a promo.
133
+ category: '🔥 Flat rate ($0.001/call)',
127
134
  models: [
128
- { id: 'zai/glm-5.1', shortcut: 'glm', label: 'GLM-5.1', price: '$0.001/call', highlight: true },
135
+ { id: 'zai/glm-5', shortcut: 'glm-5', label: 'GLM-5', price: '$0.001/call', highlight: true },
129
136
  { id: 'zai/glm-5-turbo', shortcut: 'glm-turbo', label: 'GLM-5 Turbo', price: '$0.001/call', highlight: true },
130
137
  ],
131
138
  },
@@ -136,7 +143,7 @@ export const PICKER_CATEGORIES = [
136
143
  // Premium are kept as shortcut aliases (`eco`, `premium`) and resolve
137
144
  // through the router for back-compat with older configs/sessions, but
138
145
  // they're hidden from new users — Auto already covers the cheap end
139
- // (V4 Pro at $0.50/$1.00 for SIMPLE/MEDIUM) and the quality end (Opus
146
+ // (V4 Pro at $0.435/$0.87 for SIMPLE/MEDIUM) and the quality end (Opus
140
147
  // for COMPLEX), so a separate Eco/Premium picker entry just adds
141
148
  // choice paralysis without distinct value.
142
149
  { id: 'blockrun/auto', shortcut: 'auto', label: 'Auto', price: 'routed' },
@@ -156,7 +163,7 @@ export const PICKER_CATEGORIES = [
156
163
  { id: 'openai/gpt-5.5', shortcut: 'gpt', label: 'GPT-5.5', price: '$5/$30', highlight: true },
157
164
  { id: 'google/gemini-3.1-pro', shortcut: 'gemini-3', label: 'Gemini 3.1 Pro', price: '$2/$12' },
158
165
  { id: 'google/gemini-2.5-pro', shortcut: 'gemini', label: 'Gemini 2.5 Pro', price: '$1.25/$10' },
159
- { id: 'xai/grok-4-0709', shortcut: 'grok-4', label: 'Grok 4', price: '$0.2/$1.5' },
166
+ { id: 'xai/grok-4.3', shortcut: 'grok', label: 'Grok 4.3', price: '$1.5/$4' },
160
167
  ],
161
168
  },
162
169
  {
@@ -164,9 +171,10 @@ export const PICKER_CATEGORIES = [
164
171
  models: [
165
172
  { id: 'openai/o3', shortcut: 'o3', label: 'O3', price: '$2/$8' },
166
173
  { id: 'openai/gpt-5.3-codex', shortcut: 'codex', label: 'GPT-5.3 Codex', price: '$1.75/$14' },
167
- // V4 Pro on launch promo (75% off through 2026-05-31). 1M context,
168
- // 1.6T MoE → punches up to GPT-5.5/Opus on hard tasks at <1/10 the price.
169
- { id: 'deepseek/deepseek-v4-pro', shortcut: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', price: '$0.5/$1 (promo)', highlight: true },
174
+ // V4 Pro: the 75% launch promo became DeepSeek's permanent list price
175
+ // after 2026-05-31. 1M context, 1.6T MoE → punches up to GPT-5.5/Opus
176
+ // on hard tasks at <1/10 the price.
177
+ { id: 'deepseek/deepseek-v4-pro', shortcut: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', price: '$0.435/$0.87', highlight: true },
170
178
  { id: 'deepseek/deepseek-reasoner', shortcut: 'r1', label: 'DeepSeek V4 Flash R.', price: '$0.2/$0.4' },
171
179
  { id: 'xai/grok-4-1-fast-reasoning', shortcut: 'grok-fast', label: 'Grok 4.1 Fast R.', price: '$0.2/$0.5' },
172
180
  ],
@@ -14,6 +14,11 @@ interface VimInputProps {
14
14
  focus?: boolean;
15
15
  showMode?: boolean;
16
16
  onModeChange?: (mode: VimMode) => void;
17
+ /** Probe the clipboard for an image and return the input-block to splice in
18
+ * (or null if there's no image). Wired to the same path as PromptTextInput's
19
+ * Ctrl+V fallback so vim-mode users on terminals that don't emit a
20
+ * bracketed-paste event for images can still paste. */
21
+ onClipboardImage?: () => Promise<string | null>;
17
22
  }
18
- export default function VimInput({ value, onChange, onSubmit, placeholder, focus, showMode, onModeChange, }: VimInputProps): import("react/jsx-runtime").JSX.Element;
23
+ export default function VimInput({ value, onChange, onSubmit, placeholder, focus, showMode, onModeChange, onClipboardImage, }: VimInputProps): import("react/jsx-runtime").JSX.Element;
19
24
  export {};
@@ -56,13 +56,18 @@ function endWord(text, pos) {
56
56
  i++;
57
57
  return Math.min(i, text.length - 1);
58
58
  }
59
- export default function VimInput({ value, onChange, onSubmit, placeholder = '', focus = true, showMode = true, onModeChange, }) {
59
+ export default function VimInput({ value, onChange, onSubmit, placeholder = '', focus = true, showMode = true, onModeChange, onClipboardImage, }) {
60
60
  const [mode, setMode] = useState('insert');
61
61
  const [cursor, setCursor] = useState(value.length);
62
62
  const [cmdBuf, setCmdBuf] = useState(''); // accumulated command buffer (for counts + operators)
63
63
  const [yankBuf, setYankBuf] = useState(''); // internal clipboard
64
64
  const [undoStack, setUndoStack] = useState([]); // simple undo
65
65
  const lastValueRef = useRef(value);
66
+ // Mirror the latest value prop every render so the async Ctrl+V clipboard
67
+ // insert (which resolves after the keypress) never splices into a stale
68
+ // string when the parent swaps `value` mid-probe — e.g. a submit clears the
69
+ // input, or another paste path writes first.
70
+ lastValueRef.current = value;
66
71
  // Keep cursor in bounds when value changes externally
67
72
  const clampedCursor = Math.min(cursor, mode === 'normal' ? Math.max(0, value.length - 1) : value.length);
68
73
  const switchMode = useCallback((newMode) => {
@@ -155,6 +160,24 @@ export default function VimInput({ value, onChange, onSubmit, placeholder = '',
155
160
  updateValue(value.slice(0, clampedCursor), clampedCursor);
156
161
  return;
157
162
  }
163
+ // Ctrl+V: clipboard-image fallback for terminals that don't emit a
164
+ // bracketed-paste event for image-only clipboards. Probe is async, so the
165
+ // handler returns now and updateValue happens when it resolves; capture
166
+ // the cursor offset so the block lands where the user pasted.
167
+ if (key.ctrl && input === 'v') {
168
+ if (onClipboardImage) {
169
+ const at = clampedCursor;
170
+ saveUndo();
171
+ onClipboardImage().then((injected) => {
172
+ if (!injected)
173
+ return;
174
+ const cur = lastValueRef.current;
175
+ const pos = Math.min(at, cur.length);
176
+ updateValue(cur.slice(0, pos) + injected + cur.slice(pos), pos + injected.length);
177
+ }).catch(() => { });
178
+ }
179
+ return;
180
+ }
158
181
  // Skip control chars and tab
159
182
  if (key.ctrl || key.meta || key.tab)
160
183
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.25.3",
3
+ "version": "3.26.0",
4
4
  "description": "Franklin Agent — 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": {