@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.
- package/dist/agent/llm.d.ts +1 -0
- package/dist/agent/llm.js +41 -27
- package/dist/pricing.js +5 -3
- package/dist/proxy/server.js +12 -4
- package/dist/router/index.js +2 -2
- package/dist/ui/app.js +50 -7
- package/dist/ui/model-picker.js +17 -9
- package/dist/ui/vim-input.d.ts +6 -1
- package/dist/ui/vim-input.js +24 -1
- package/package.json +1 -1
package/dist/agent/llm.d.ts
CHANGED
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
|
|
595
|
-
if (!
|
|
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, ...
|
|
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
|
|
657
|
-
if (!
|
|
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, ...
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
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:
|
|
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
|
};
|
package/dist/proxy/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/router/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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(
|
|
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 `❯ ${
|
|
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
|
package/dist/ui/model-picker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
168
|
-
// 1.6T MoE → punches up to GPT-5.5/Opus
|
|
169
|
-
|
|
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
|
],
|
package/dist/ui/vim-input.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/ui/vim-input.js
CHANGED
|
@@ -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