@blockrun/franklin 3.15.87 → 3.15.89
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/context.js +45 -6
- package/dist/agent/evaluator.js +1 -1
- package/dist/agent/llm.d.ts +16 -0
- package/dist/agent/llm.js +62 -8
- package/dist/agent/loop.js +32 -20
- package/dist/agent/media-router.js +3 -3
- package/dist/agent/optimize.js +42 -7
- package/dist/agent/turn-analyzer.js +7 -7
- package/dist/commands/content.d.ts +3 -3
- package/dist/commands/content.js +3 -3
- package/dist/commands/panel.js +16 -2
- package/dist/commands/start.js +15 -2
- package/dist/learnings/extractor.js +1 -1
- package/dist/proxy/server.js +77 -13
- package/dist/router/categories.js +4 -6
- package/dist/router/index.js +10 -8
- package/dist/social/a11y.d.ts +1 -1
- package/dist/social/a11y.js +5 -4
- package/dist/social/browser.js +63 -4
- package/dist/stats/cost-log.d.ts +52 -17
- package/dist/stats/cost-log.js +67 -17
- package/dist/tools/prediction.debug.js +1 -1
- package/dist/tools/prediction.js +1 -1
- package/dist/tools/searchx.js +3 -3
- package/dist/tools/wallet.js +1 -1
- package/dist/ui/app.js +1 -1
- package/package.json +1 -1
package/dist/commands/start.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { getOrCreateWallet, getOrCreateSolanaWallet } from '@blockrun/llm';
|
|
3
|
-
import { loadChain, API_URLS } from '../config.js';
|
|
5
|
+
import { BLOCKRUN_DIR, loadChain, API_URLS } from '../config.js';
|
|
4
6
|
import { retryFetchBalance } from './balance-retry.js';
|
|
5
7
|
import { flushStats, loadStats } from '../stats/tracker.js';
|
|
6
8
|
import { OPUS_PRICING, MODEL_PRICING } from '../pricing.js';
|
|
@@ -647,7 +649,18 @@ async function startPanelBackground(startPort) {
|
|
|
647
649
|
});
|
|
648
650
|
server.listen(port, '127.0.0.1', () => {
|
|
649
651
|
server.unref?.();
|
|
650
|
-
|
|
652
|
+
const url = `http://localhost:${port}`;
|
|
653
|
+
// Persist the bound URL so the agent context (assembled per-turn)
|
|
654
|
+
// can point users at /#wallet for funding without baking in the
|
|
655
|
+
// 3100 default — the panel auto-increments past EADDRINUSE.
|
|
656
|
+
// Best-effort write: a stale file from a crashed run is harmless,
|
|
657
|
+
// since the user just sees a dead link.
|
|
658
|
+
try {
|
|
659
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
660
|
+
fs.writeFileSync(path.join(BLOCKRUN_DIR, 'panel-url'), url, 'utf8');
|
|
661
|
+
}
|
|
662
|
+
catch { /* best-effort */ }
|
|
663
|
+
resolve(url);
|
|
651
664
|
});
|
|
652
665
|
};
|
|
653
666
|
tryListen(startPort, 0);
|
|
@@ -21,7 +21,7 @@ const VALID_CATEGORIES = new Set([
|
|
|
21
21
|
const EXTRACTION_PROMPT = `You are analyzing a conversation between a user and an AI coding agent. Extract user preferences, behavioral patterns, and project knowledge that would help personalize future interactions.
|
|
22
22
|
|
|
23
23
|
Analyze for:
|
|
24
|
-
1. Language — what language does the user write in? (English,
|
|
24
|
+
1. Language — what language does the user write in? (English, another language, mixed?)
|
|
25
25
|
2. Model preferences — did they switch models or express a preference?
|
|
26
26
|
3. Coding style — did they correct the agent's code style? (naming, formatting, conventions)
|
|
27
27
|
4. Communication — are they terse or verbose? Do they want explanations or just code?
|
package/dist/proxy/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
3
3
|
import { recordUsage } from '../stats/tracker.js';
|
|
4
|
+
import { appendSettlementRow } from '../stats/cost-log.js';
|
|
4
5
|
import { appendAudit } from '../stats/audit.js';
|
|
5
6
|
import { buildFallbackChain, DEFAULT_FALLBACK_CONFIG, ROUTING_PROFILES, } from './fallback.js';
|
|
6
7
|
import { routeRequest, parseRoutingProfile, } from '../router/index.js';
|
|
@@ -430,6 +431,11 @@ export function createProxy(options) {
|
|
|
430
431
|
};
|
|
431
432
|
let response;
|
|
432
433
|
let finalModel = requestModel;
|
|
434
|
+
// Real x402 charge for the call that ultimately succeeded. 0 when
|
|
435
|
+
// no payment was needed (free model / cached). Fed into recordUsage
|
|
436
|
+
// and appendAudit below so franklin-stats.json reflects what the
|
|
437
|
+
// wallet actually paid, not a token-catalog estimate.
|
|
438
|
+
let paidUsd = 0;
|
|
433
439
|
const requestTimeoutMs = effectiveRequestTimeoutMs;
|
|
434
440
|
// Use fallback chain if enabled
|
|
435
441
|
if (fallbackEnabled && body && requestPath.includes('messages')) {
|
|
@@ -457,6 +463,7 @@ export function createProxy(options) {
|
|
|
457
463
|
// Use the body with the correct fallback model for payment
|
|
458
464
|
body = result.bodyUsed;
|
|
459
465
|
usedFallback = result.fallbackUsed;
|
|
466
|
+
paidUsd = result.paidUsd;
|
|
460
467
|
// Skip the success log when the request originated from a test
|
|
461
468
|
// fixture, even if the fallback ended on a real model. Verified
|
|
462
469
|
// on a real machine: 5 spurious "↺ Fallback successful: using
|
|
@@ -473,7 +480,7 @@ export function createProxy(options) {
|
|
|
473
480
|
}
|
|
474
481
|
}
|
|
475
482
|
else {
|
|
476
|
-
|
|
483
|
+
const attempt = await fetchModelAttempt(targetUrl, requestInit, body, requestModel, {
|
|
477
484
|
method: req.method || 'POST',
|
|
478
485
|
headers,
|
|
479
486
|
chain,
|
|
@@ -481,6 +488,8 @@ export function createProxy(options) {
|
|
|
481
488
|
solanaWallet,
|
|
482
489
|
timeoutMs: requestTimeoutMs,
|
|
483
490
|
});
|
|
491
|
+
response = attempt.response;
|
|
492
|
+
paidUsd = attempt.paidUsd;
|
|
484
493
|
}
|
|
485
494
|
const responseHeaders = {};
|
|
486
495
|
response.headers.forEach((v, k) => {
|
|
@@ -569,7 +578,13 @@ export function createProxy(options) {
|
|
|
569
578
|
if (outputTokens > 0) {
|
|
570
579
|
trackOutputTokens(finalModel, outputTokens);
|
|
571
580
|
const latencyMs = Date.now() - requestStartTime;
|
|
572
|
-
|
|
581
|
+
// Real x402 charge wins over the token-catalog estimate.
|
|
582
|
+
// estimateCost only fills in for the no-payment path
|
|
583
|
+
// (free models / cached) so stats stay non-null there.
|
|
584
|
+
const cost = paidUsd > 0
|
|
585
|
+
? paidUsd
|
|
586
|
+
: estimateCost(finalModel, inputTokens, outputTokens);
|
|
587
|
+
const costSource = paidUsd > 0 ? 'charged' : 'estimated';
|
|
573
588
|
recordUsage(finalModel, inputTokens, outputTokens, cost, latencyMs, usedFallback);
|
|
574
589
|
appendAudit({
|
|
575
590
|
ts: Date.now(),
|
|
@@ -582,7 +597,7 @@ export function createProxy(options) {
|
|
|
582
597
|
source: 'proxy',
|
|
583
598
|
});
|
|
584
599
|
if (options.debug)
|
|
585
|
-
logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
|
|
600
|
+
logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} (${costSource}) fallback=${usedFallback}`);
|
|
586
601
|
}
|
|
587
602
|
}
|
|
588
603
|
res.end();
|
|
@@ -609,7 +624,10 @@ export function createProxy(options) {
|
|
|
609
624
|
trackOutputTokens(finalModel, outputTokens);
|
|
610
625
|
const inputTokens = parsed.usage?.input_tokens || 0;
|
|
611
626
|
const latencyMs = Date.now() - requestStartTime;
|
|
612
|
-
const cost =
|
|
627
|
+
const cost = paidUsd > 0
|
|
628
|
+
? paidUsd
|
|
629
|
+
: estimateCost(finalModel, inputTokens, outputTokens);
|
|
630
|
+
const costSource = paidUsd > 0 ? 'charged' : 'estimated';
|
|
613
631
|
recordUsage(finalModel, inputTokens, outputTokens, cost, latencyMs, usedFallback);
|
|
614
632
|
appendAudit({
|
|
615
633
|
ts: Date.now(),
|
|
@@ -622,7 +640,7 @@ export function createProxy(options) {
|
|
|
622
640
|
source: 'proxy',
|
|
623
641
|
});
|
|
624
642
|
if (options.debug)
|
|
625
|
-
logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
|
|
643
|
+
logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} (${costSource}) fallback=${usedFallback}`);
|
|
626
644
|
}
|
|
627
645
|
}
|
|
628
646
|
catch {
|
|
@@ -645,16 +663,17 @@ export function createProxy(options) {
|
|
|
645
663
|
return server;
|
|
646
664
|
}
|
|
647
665
|
async function fetchModelAttempt(url, init, body, model, payment) {
|
|
648
|
-
|
|
666
|
+
const response = await fetchWithTimeout(url, { ...init, body: body || undefined }, payment.timeoutMs, `Proxy request for ${model}`);
|
|
667
|
+
// Non-402 path: free model or cached response — no payment, paidUsd = 0.
|
|
649
668
|
if (response.status !== 402)
|
|
650
|
-
return response;
|
|
669
|
+
return { response, paidUsd: 0 };
|
|
651
670
|
if (payment.chain === 'solana' && payment.solanaWallet) {
|
|
652
671
|
return handleSolanaPayment(response, url, payment.method, payment.headers, body, payment.solanaWallet.privateKey, payment.solanaWallet.address, payment.timeoutMs, model);
|
|
653
672
|
}
|
|
654
673
|
if (payment.baseWallet) {
|
|
655
674
|
return handleBasePayment(response, url, payment.method, payment.headers, body, payment.baseWallet.privateKey, payment.baseWallet.address, payment.timeoutMs, model);
|
|
656
675
|
}
|
|
657
|
-
return response;
|
|
676
|
+
return { response, paidUsd: 0 };
|
|
658
677
|
}
|
|
659
678
|
/**
|
|
660
679
|
* Try each fallback model as a full x402 attempt:
|
|
@@ -670,7 +689,7 @@ async function fetchWithPaymentFallback(url, init, originalBody, config, payment
|
|
|
670
689
|
const body = replaceModelInBody(originalBody, model);
|
|
671
690
|
try {
|
|
672
691
|
attempts++;
|
|
673
|
-
const response = await fetchModelAttempt(url, init, body, model, payment);
|
|
692
|
+
const { response, paidUsd } = await fetchModelAttempt(url, init, body, model, payment);
|
|
674
693
|
if (!config.retryOn.includes(response.status)) {
|
|
675
694
|
return {
|
|
676
695
|
response,
|
|
@@ -679,6 +698,7 @@ async function fetchWithPaymentFallback(url, init, originalBody, config, payment
|
|
|
679
698
|
fallbackUsed: i > 0,
|
|
680
699
|
attemptsCount: attempts,
|
|
681
700
|
failedModels,
|
|
701
|
+
paidUsd,
|
|
682
702
|
};
|
|
683
703
|
}
|
|
684
704
|
try {
|
|
@@ -719,17 +739,24 @@ function sleep(ms) {
|
|
|
719
739
|
async function handleBasePayment(response, url, method, headers, body, privateKey, fromAddress, timeoutMs = getProxyRequestTimeoutMs(), model = 'unknown') {
|
|
720
740
|
const paymentHeader = await extractPaymentHeader(response);
|
|
721
741
|
if (!paymentHeader) {
|
|
722
|
-
throw new Error('402 Payment Required — wallet may need funding.
|
|
742
|
+
throw new Error('402 Payment Required — wallet may need funding. Open http://localhost:3100/#wallet to deposit USDC (or run: franklin balance)');
|
|
723
743
|
}
|
|
724
744
|
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
725
745
|
const details = extractPaymentDetails(paymentRequired);
|
|
746
|
+
const paidUsd = paymentAmountToUsd(details.amount);
|
|
747
|
+
appendSettlementRow(extractEndpointPath(url), paidUsd, {
|
|
748
|
+
model,
|
|
749
|
+
wallet: fromAddress,
|
|
750
|
+
network: details.network || 'base-mainnet',
|
|
751
|
+
client_kind: 'ProxyClient',
|
|
752
|
+
});
|
|
726
753
|
const paymentPayload = await createPaymentPayload(privateKey, fromAddress, details.recipient, details.amount, details.network || 'eip155:8453', {
|
|
727
754
|
resourceUrl: details.resource?.url || url,
|
|
728
755
|
resourceDescription: details.resource?.description || 'BlockRun AI API call',
|
|
729
756
|
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
730
757
|
extra: details.extra,
|
|
731
758
|
});
|
|
732
|
-
|
|
759
|
+
const paid = await fetchWithTimeout(url, {
|
|
733
760
|
method,
|
|
734
761
|
headers: {
|
|
735
762
|
...headers,
|
|
@@ -737,6 +764,7 @@ async function handleBasePayment(response, url, method, headers, body, privateKe
|
|
|
737
764
|
},
|
|
738
765
|
body: body || undefined,
|
|
739
766
|
}, timeoutMs, `Paid proxy request for ${model}`);
|
|
767
|
+
return { response: paid, paidUsd };
|
|
740
768
|
}
|
|
741
769
|
// ======================================================================
|
|
742
770
|
// Solana payment handler
|
|
@@ -744,10 +772,17 @@ async function handleBasePayment(response, url, method, headers, body, privateKe
|
|
|
744
772
|
async function handleSolanaPayment(response, url, method, headers, body, privateKey, fromAddress, timeoutMs = getProxyRequestTimeoutMs(), model = 'unknown') {
|
|
745
773
|
const paymentHeader = await extractPaymentHeader(response);
|
|
746
774
|
if (!paymentHeader) {
|
|
747
|
-
throw new Error('402 Payment Required — wallet may need funding.
|
|
775
|
+
throw new Error('402 Payment Required — wallet may need funding. Open http://localhost:3100/#wallet to deposit USDC (or run: franklin balance)');
|
|
748
776
|
}
|
|
749
777
|
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
750
778
|
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
779
|
+
const paidUsd = paymentAmountToUsd(details.amount);
|
|
780
|
+
appendSettlementRow(extractEndpointPath(url), paidUsd, {
|
|
781
|
+
model,
|
|
782
|
+
wallet: fromAddress,
|
|
783
|
+
network: details.network || 'solana-mainnet',
|
|
784
|
+
client_kind: 'ProxyClient',
|
|
785
|
+
});
|
|
751
786
|
const secretKey = await solanaKeyToBytes(privateKey);
|
|
752
787
|
const feePayer = details.extra?.feePayer || details.recipient;
|
|
753
788
|
const paymentPayload = await createSolanaPaymentPayload(secretKey, fromAddress, details.recipient, details.amount, feePayer, {
|
|
@@ -756,7 +791,7 @@ async function handleSolanaPayment(response, url, method, headers, body, private
|
|
|
756
791
|
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
757
792
|
extra: details.extra,
|
|
758
793
|
});
|
|
759
|
-
|
|
794
|
+
const paid = await fetchWithTimeout(url, {
|
|
760
795
|
method,
|
|
761
796
|
headers: {
|
|
762
797
|
...headers,
|
|
@@ -764,6 +799,35 @@ async function handleSolanaPayment(response, url, method, headers, body, private
|
|
|
764
799
|
},
|
|
765
800
|
body: body || undefined,
|
|
766
801
|
}, timeoutMs, `Paid proxy request for ${model}`);
|
|
802
|
+
return { response: paid, paidUsd };
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Extract just the path portion of a URL — `https://api.blockrun.ai/v1/messages`
|
|
806
|
+
* → `/v1/messages`. Used as the `endpoint` field in `cost_log.jsonl` so
|
|
807
|
+
* proxy entries match the SDK's path-only convention. Falls back to the
|
|
808
|
+
* raw input if URL parsing throws (defensive — better to log a weird
|
|
809
|
+
* string than skip the row).
|
|
810
|
+
*/
|
|
811
|
+
function extractEndpointPath(url) {
|
|
812
|
+
try {
|
|
813
|
+
return new URL(url).pathname || url;
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
return url;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Convert an x402 `details.amount` field (USDC in micro-units, 6 decimals)
|
|
821
|
+
* to a USD float. Mirrors the SDK's `appendCostLog` math so the proxy and
|
|
822
|
+
* `cost_log.jsonl` agree to the cent.
|
|
823
|
+
*/
|
|
824
|
+
function paymentAmountToUsd(amount) {
|
|
825
|
+
if (amount === undefined || amount === null)
|
|
826
|
+
return 0;
|
|
827
|
+
const n = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
828
|
+
if (!Number.isFinite(n))
|
|
829
|
+
return 0;
|
|
830
|
+
return n / 1e6;
|
|
767
831
|
}
|
|
768
832
|
export function classifyRequest(body) {
|
|
769
833
|
try {
|
|
@@ -4,44 +4,42 @@
|
|
|
4
4
|
* using keyword matching from router weights or built-in defaults.
|
|
5
5
|
*/
|
|
6
6
|
// Built-in category keywords (used when no learned weights available)
|
|
7
|
+
// Keyword fast-path uses English only by policy (English-only-source rule).
|
|
8
|
+
// Non-English user queries route through the LLM-level classifier above this
|
|
9
|
+
// fast-path, which is multilingual and handles intent correctly without
|
|
10
|
+
// needing a per-language keyword list here.
|
|
7
11
|
const DEFAULT_CATEGORY_KEYWORDS = {
|
|
8
12
|
coding: [
|
|
9
13
|
'function', 'class', 'import', 'def', 'SELECT', 'async', 'await',
|
|
10
14
|
'const', 'let', 'var', 'return', '```', 'bug', 'error', 'fix',
|
|
11
15
|
'refactor', 'implement', 'test', 'npm', 'pip', 'git', 'deploy',
|
|
12
16
|
'API', 'endpoint', 'database', 'query', 'migration', 'lint',
|
|
13
|
-
'函数', '类', '导入', '修复', '调试', '部署',
|
|
14
17
|
],
|
|
15
18
|
trading: [
|
|
16
19
|
'BTC', 'ETH', 'SOL', 'bitcoin', 'ethereum', 'solana', 'crypto',
|
|
17
20
|
'price', 'market', 'signal', 'trade', 'buy', 'sell', 'RSI',
|
|
18
21
|
'MACD', 'volume', 'bullish', 'bearish', 'support', 'resistance',
|
|
19
22
|
'portfolio', 'risk', 'leverage', 'DeFi', 'token', 'swap',
|
|
20
|
-
'比特币', '以太坊', '价格', '市场', '交易', '信号',
|
|
21
23
|
],
|
|
22
24
|
reasoning: [
|
|
23
25
|
'prove', 'theorem', 'derive', 'step by step', 'chain of thought',
|
|
24
26
|
'formally', 'mathematical', 'proof', 'logically', 'analyze',
|
|
25
27
|
'compare', 'evaluate', 'trade-off', 'pros and cons', 'why',
|
|
26
28
|
'explain why', 'reasoning', 'logic', 'deduce', 'infer',
|
|
27
|
-
'证明', '定理', '推导', '分析', '比较',
|
|
28
29
|
],
|
|
29
30
|
creative: [
|
|
30
31
|
'write a story', 'poem', 'creative', 'brainstorm', 'imagine',
|
|
31
32
|
'generate an image', 'design', 'logo', 'illustration', 'art',
|
|
32
33
|
'narrative', 'fiction', 'song', 'lyrics', 'slogan', 'tagline',
|
|
33
|
-
'写一个故事', '诗', '创意', '设计', '头脑风暴',
|
|
34
34
|
],
|
|
35
35
|
research: [
|
|
36
36
|
'search', 'find', 'look up', 'what is', 'who is', 'when was',
|
|
37
37
|
'summarize', 'report', 'overview', 'comparison', 'review',
|
|
38
38
|
'article', 'paper', 'study', 'data', 'statistics', 'trend',
|
|
39
|
-
'搜索', '查找', '什么是', '总结', '报告',
|
|
40
39
|
],
|
|
41
40
|
chat: [
|
|
42
41
|
'hello', 'hi', 'thanks', 'thank you', 'how are you', 'help',
|
|
43
42
|
'translate', 'yes', 'no', 'ok', 'sure', 'good',
|
|
44
|
-
'你好', '谢谢', '帮我', '翻译',
|
|
45
43
|
],
|
|
46
44
|
};
|
|
47
45
|
/**
|
package/dist/router/index.js
CHANGED
|
@@ -70,13 +70,18 @@ const AUTO_TIERS = {
|
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
// ─── Keywords for Classification ───
|
|
73
|
+
//
|
|
74
|
+
// Keyword fast-path uses English only by policy (English-only-source rule).
|
|
75
|
+
// Non-English user queries route through the LLM-level classifier above this
|
|
76
|
+
// fast-path, which is multilingual and handles intent correctly without
|
|
77
|
+
// needing per-language keyword lists here.
|
|
73
78
|
const CODE_KEYWORDS = [
|
|
74
79
|
'function', 'class', 'import', 'def', 'SELECT', 'async', 'await',
|
|
75
|
-
'const', 'let', 'var', 'return', '```',
|
|
80
|
+
'const', 'let', 'var', 'return', '```',
|
|
76
81
|
];
|
|
77
82
|
const REASONING_KEYWORDS = [
|
|
78
83
|
'prove', 'theorem', 'derive', 'step by step', 'chain of thought',
|
|
79
|
-
'formally', 'mathematical', 'proof', 'logically',
|
|
84
|
+
'formally', 'mathematical', 'proof', 'logically',
|
|
80
85
|
];
|
|
81
86
|
const SIMPLE_KEYWORDS = [
|
|
82
87
|
// True simple intents: greeting, definition lookup, translation. Factual
|
|
@@ -84,7 +89,7 @@ const SIMPLE_KEYWORDS = [
|
|
|
84
89
|
// because they look easy but require external recall — sending them to
|
|
85
90
|
// SIMPLE-tier models reliably produces hallucinated subscriber counts,
|
|
86
91
|
// birth years, etc. that the post-hoc grounding check then has to flag.
|
|
87
|
-
'define', 'translate', 'hello', 'yes or no',
|
|
92
|
+
'define', 'translate', 'hello', 'yes or no',
|
|
88
93
|
];
|
|
89
94
|
// Research / fact-retrieval intent: questions whose correct answer depends
|
|
90
95
|
// on data the model can't reliably recall from weights — current statistics,
|
|
@@ -98,19 +103,16 @@ const RESEARCH_KEYWORDS = [
|
|
|
98
103
|
'best', 'top ', 'most popular', 'compare', 'vs ', ' vs.',
|
|
99
104
|
'latest', 'current', 'recent', 'today', 'now',
|
|
100
105
|
'subscribers', 'members', 'followers', 'market cap', 'price of',
|
|
101
|
-
'最好的', '最新', '最近', '现在', '当前', '排名', '对比',
|
|
102
106
|
];
|
|
103
107
|
const TECHNICAL_KEYWORDS = [
|
|
104
108
|
'algorithm', 'optimize', 'architecture', 'distributed', 'kubernetes',
|
|
105
|
-
'microservice', 'database', 'infrastructure',
|
|
109
|
+
'microservice', 'database', 'infrastructure',
|
|
106
110
|
];
|
|
107
111
|
const AGENTIC_KEYWORDS = [
|
|
108
112
|
'read file', 'edit', 'modify', 'update', 'create file', 'execute',
|
|
109
113
|
'deploy', 'install', 'npm', 'pip', 'fix', 'debug', 'verify',
|
|
110
114
|
'commit', 'push', 'pull', 'merge', 'rename', 'replace', 'delete',
|
|
111
115
|
'remove', 'add', 'change', 'move', 'refactor', 'migrate',
|
|
112
|
-
'编辑', '修改', '部署', '安装', '修复', '调试',
|
|
113
|
-
'更新', '替换', '删除', '添加', '提交', '改',
|
|
114
116
|
];
|
|
115
117
|
// URL patterns that signal agentic/coding tasks
|
|
116
118
|
const AGENTIC_URL_PATTERNS = [
|
|
@@ -223,7 +225,7 @@ function classifyRequest(prompt, tokenCount) {
|
|
|
223
225
|
// Imperative verbs (build, create, implement, etc.)
|
|
224
226
|
const imperativeMatches = countMatches(prompt, [
|
|
225
227
|
'build', 'create', 'implement', 'design', 'develop', 'write', 'make',
|
|
226
|
-
'generate', 'construct',
|
|
228
|
+
'generate', 'construct',
|
|
227
229
|
]);
|
|
228
230
|
if (imperativeMatches >= 1) {
|
|
229
231
|
score += 0.15;
|
package/dist/social/a11y.d.ts
CHANGED
|
@@ -51,4 +51,4 @@ export declare function extractArticleBlocks(tree: string): Array<{
|
|
|
51
51
|
* This doubles as the "this is a tweet" signal in social-bot — the only link
|
|
52
52
|
* inside an article block with this label shape is the permalink to the tweet.
|
|
53
53
|
*/
|
|
54
|
-
export declare const X_TIME_LINK_PATTERN = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}
|
|
54
|
+
export declare const X_TIME_LINK_PATTERN = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}\\p{Script=Han}\\d{1,2}\\p{Script=Han}\\d{1,2}\\p{Script=Han}";
|
package/dist/social/a11y.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @returns Array of ref ids like ["0-0", "1-3"] in document order
|
|
24
24
|
*/
|
|
25
25
|
export function findRefs(tree, role, label = '.*') {
|
|
26
|
-
const re = new RegExp(`\\[(\\d+-\\d+)\\]\\s+${escapeRegex(role)}:\\s
|
|
26
|
+
const re = new RegExp(`\\[(\\d+-\\d+)\\]\\s+${escapeRegex(role)}:\\s*(?:${label})`, 'gu');
|
|
27
27
|
const out = [];
|
|
28
28
|
let m;
|
|
29
29
|
while ((m = re.exec(tree)) !== null) {
|
|
@@ -36,7 +36,7 @@ export function findRefs(tree, role, label = '.*') {
|
|
|
36
36
|
* (ref) and the visible text (label) in one pass.
|
|
37
37
|
*/
|
|
38
38
|
export function findRefsWithLabels(tree, role, label = '.*') {
|
|
39
|
-
const re = new RegExp(`\\[(\\d+-\\d+)\\]\\s+${escapeRegex(role)}:\\s*(${label})`, '
|
|
39
|
+
const re = new RegExp(`\\[(\\d+-\\d+)\\]\\s+${escapeRegex(role)}:\\s*(${label})`, 'gu');
|
|
40
40
|
const out = [];
|
|
41
41
|
let m;
|
|
42
42
|
while ((m = re.exec(tree)) !== null) {
|
|
@@ -86,8 +86,9 @@ export function extractArticleBlocks(tree) {
|
|
|
86
86
|
// Matches all known X time-link formats:
|
|
87
87
|
// "Mar 16", "Apr 12, 2026", "5h", "5m", "2d", "30s", "just now", "now"
|
|
88
88
|
// "31 seconds ago", "35 minutes ago", "4 hours ago" (full-word format)
|
|
89
|
-
// "Yesterday", "Apr 12", "12:30 AM"
|
|
90
|
-
|
|
89
|
+
// "Yesterday", "Apr 12", "12:30 AM"
|
|
90
|
+
// Locale-rendered numeric dates separated by Han-script date markers
|
|
91
|
+
export const X_TIME_LINK_PATTERN = '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}\\p{Script=Han}\\d{1,2}\\p{Script=Han}\\d{1,2}\\p{Script=Han}';
|
|
91
92
|
function escapeRegex(s) {
|
|
92
93
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
94
|
}
|
package/dist/social/browser.js
CHANGED
|
@@ -78,6 +78,54 @@ export function serializeAxTree(root) {
|
|
|
78
78
|
walk(root, 0);
|
|
79
79
|
return { tree: lines.join('\n'), refs };
|
|
80
80
|
}
|
|
81
|
+
function cdpStringValue(v) {
|
|
82
|
+
if (v === undefined || v === null)
|
|
83
|
+
return '';
|
|
84
|
+
if (typeof v === 'string')
|
|
85
|
+
return v;
|
|
86
|
+
return String(v);
|
|
87
|
+
}
|
|
88
|
+
function cdpNodesToAxTree(nodes) {
|
|
89
|
+
if (!nodes || nodes.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
const byId = new Map();
|
|
92
|
+
const childSet = new Set();
|
|
93
|
+
for (const n of nodes) {
|
|
94
|
+
byId.set(n.nodeId, n);
|
|
95
|
+
if (n.childIds)
|
|
96
|
+
for (const cid of n.childIds)
|
|
97
|
+
childSet.add(cid);
|
|
98
|
+
}
|
|
99
|
+
// The root has no parent (or no entry pointing at it as a child).
|
|
100
|
+
const root = nodes.find((n) => !n.parentId && !childSet.has(n.nodeId)) ??
|
|
101
|
+
nodes.find((n) => !n.parentId) ??
|
|
102
|
+
nodes[0];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
function build(node) {
|
|
105
|
+
if (seen.has(node.nodeId))
|
|
106
|
+
return null;
|
|
107
|
+
seen.add(node.nodeId);
|
|
108
|
+
const ax = {
|
|
109
|
+
role: cdpStringValue(node.role?.value),
|
|
110
|
+
name: cdpStringValue(node.name?.value),
|
|
111
|
+
value: cdpStringValue(node.value?.value),
|
|
112
|
+
description: cdpStringValue(node.description?.value),
|
|
113
|
+
children: [],
|
|
114
|
+
};
|
|
115
|
+
if (node.childIds) {
|
|
116
|
+
for (const cid of node.childIds) {
|
|
117
|
+
const child = byId.get(cid);
|
|
118
|
+
if (!child)
|
|
119
|
+
continue;
|
|
120
|
+
const built = build(child);
|
|
121
|
+
if (built)
|
|
122
|
+
ax.children.push(built);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return ax;
|
|
126
|
+
}
|
|
127
|
+
return build(root);
|
|
128
|
+
}
|
|
81
129
|
/**
|
|
82
130
|
* Franklin's social browser driver. Lazy-imports playwright-core so the
|
|
83
131
|
* rest of the CLI stays fast to start.
|
|
@@ -144,10 +192,21 @@ export class SocialBrowser {
|
|
|
144
192
|
*/
|
|
145
193
|
async snapshot() {
|
|
146
194
|
this.requirePage();
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
195
|
+
// page.accessibility was removed from playwright-core (gone by 1.59).
|
|
196
|
+
// Calling it threw `Cannot read properties of undefined (reading 'snapshot')`
|
|
197
|
+
// in production (failures.jsonl entries 1776662596215 / 1776662608060).
|
|
198
|
+
// The supported replacement is the CDP Accessibility domain, which still
|
|
199
|
+
// ships with Chromium-based browsers.
|
|
200
|
+
const cdp = await this.page.context().newCDPSession(this.page);
|
|
201
|
+
let axRoot;
|
|
202
|
+
try {
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
const result = (await cdp.send('Accessibility.getFullAXTree'));
|
|
205
|
+
axRoot = cdpNodesToAxTree(result?.nodes);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await cdp.detach().catch(() => { });
|
|
209
|
+
}
|
|
151
210
|
if (!axRoot)
|
|
152
211
|
return '';
|
|
153
212
|
const { tree, refs } = serializeAxTree(axRoot);
|
package/dist/stats/cost-log.d.ts
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reader for `~/.blockrun/cost_log.jsonl` — the
|
|
3
|
-
* settled x402 payment.
|
|
2
|
+
* Reader (and limited writer) for `~/.blockrun/cost_log.jsonl` — the
|
|
3
|
+
* append-only ledger of every settled x402 payment.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* History: this file was originally SDK-only territory. `@blockrun/llm`'s
|
|
6
|
+
* internal `appendCostLog` writes one line per micropayment when callers
|
|
7
|
+
* use SDK helper methods (modal sandbox, prediction market, exa, etc.).
|
|
8
|
+
* But Franklin's main LLM stream — both the in-process agent loop
|
|
9
|
+
* (`src/agent/llm.ts`) and the proxy server (`src/proxy/server.ts`) —
|
|
10
|
+
* have **their own** x402 signers that bypass the SDK entirely. Verified
|
|
11
|
+
* 2026-05-09 on a real machine: a single paid agent turn dropped the
|
|
12
|
+
* wallet by $0.001 and updated `franklin-stats.json` correctly, but
|
|
13
|
+
* cost_log.jsonl gained zero entries. So cost_log was never the
|
|
14
|
+
* "wallet truth" it advertised — it was an SDK-subset.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* alongside the recorded total.
|
|
16
|
+
* Fix (2026-05-09): expose `appendSettlementRow` so the agent and proxy
|
|
17
|
+
* signers can write the same shape the SDK does. The format contract
|
|
18
|
+
* (snake_case `cost_usd`, `ts` in unix seconds with subsecond precision,
|
|
19
|
+
* one JSON object per line) is preserved exactly so both writers
|
|
20
|
+
* interleave cleanly. Order in the file follows wall-clock arrival.
|
|
19
21
|
*
|
|
20
|
-
* Responsibility: read-only. We never
|
|
21
|
-
* the SDK
|
|
22
|
+
* Responsibility: read + append-only write. We never trim or rotate
|
|
23
|
+
* cost_log.jsonl — that contract still belongs to the SDK / hygiene.
|
|
22
24
|
*/
|
|
23
25
|
export interface SettlementRow {
|
|
24
26
|
/** Endpoint path that was paid for, e.g. `/v1/chat/completions`. */
|
|
@@ -61,6 +63,39 @@ interface ReadOptions {
|
|
|
61
63
|
* is only created on the first paid call.
|
|
62
64
|
*/
|
|
63
65
|
export declare function loadSdkSettlements(opts?: ReadOptions): SettlementRow[];
|
|
66
|
+
/**
|
|
67
|
+
* Optional metadata fields the SDK writes alongside `endpoint` / `cost_usd`.
|
|
68
|
+
* Adding these to agent + proxy entries keeps cost_log.jsonl uniformly
|
|
69
|
+
* queryable (group by model, filter by wallet, etc.). Verified 2026-05-10
|
|
70
|
+
* against a real cost_log: the SDK writes
|
|
71
|
+
* {endpoint, cost_usd, model, wallet, network, client_kind}
|
|
72
|
+
* Without these on agent rows you can't tell which model burned a $0.001
|
|
73
|
+
* — the row is just `/v1/messages: 0.001`. With them, every line is a
|
|
74
|
+
* complete forensic record.
|
|
75
|
+
*/
|
|
76
|
+
export interface SettlementMeta {
|
|
77
|
+
model?: string;
|
|
78
|
+
wallet?: string;
|
|
79
|
+
network?: string;
|
|
80
|
+
client_kind?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Append one settlement row to ~/.blockrun/cost_log.jsonl in the same
|
|
84
|
+
* shape `@blockrun/llm`'s internal `appendCostLog` writes. Best-effort:
|
|
85
|
+
* silently swallows fs errors so a logging failure never breaks the
|
|
86
|
+
* paid call that just succeeded. Costs <= 0 are treated as no-op (no
|
|
87
|
+
* point logging $0 — the file's purpose is "what was actually paid").
|
|
88
|
+
*
|
|
89
|
+
* Honors FRANKLIN_NO_AUDIT=1 the same way `appendAudit` and `recordUsage`
|
|
90
|
+
* do, so test runs (test/e2e.mjs sets this) don't pollute the user's
|
|
91
|
+
* real cost_log. Verified 2026-05-10 on a real machine: two
|
|
92
|
+
* `/v1/messages: $0.000001` rows leaked into the user's cost_log from
|
|
93
|
+
* a paid e2e run because this gate was missing — paid e2e was hitting
|
|
94
|
+
* the real gateway with a real wallet, but the test framework expected
|
|
95
|
+
* NO writes to land. Restoring the gate keeps cost_log a clean ledger
|
|
96
|
+
* of REAL traffic.
|
|
97
|
+
*/
|
|
98
|
+
export declare function appendSettlementRow(endpoint: string, costUsd: number, meta?: SettlementMeta): void;
|
|
64
99
|
/** Aggregate the SDK ledger into a single summary object. */
|
|
65
100
|
export declare function summarizeSdkSettlements(opts?: ReadOptions): SettlementSummary;
|
|
66
101
|
export {};
|