@blockrun/franklin 3.15.53 → 3.15.55
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/loop.js
CHANGED
|
@@ -288,6 +288,19 @@ function getBackoffDelay(attempt, maxDelayMs = 32_000) {
|
|
|
288
288
|
const jitter = base * 0.25 * (Math.random() * 2 - 1); // ±25%
|
|
289
289
|
return Math.max(500, Math.round(base + jitter));
|
|
290
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Format the user-facing "switching model" line. Includes the resolved
|
|
293
|
+
* concrete model in parentheses when the user-facing alias (e.g.
|
|
294
|
+
* `blockrun/auto`) differs from what was actually being called (e.g.
|
|
295
|
+
* `anthropic/claude-sonnet-4.6`). Verified 2026-05-04 in a live session:
|
|
296
|
+
* a payment fail surfaced as `*blockrun/auto failed — switching to
|
|
297
|
+
* nvidia/qwen3-coder-480b*` with no hint of which concrete model
|
|
298
|
+
* actually failed, and no hint of why. The reason label closes that gap.
|
|
299
|
+
*/
|
|
300
|
+
function formatModelSwitch(alias, resolved, reason, newModel) {
|
|
301
|
+
const oldDisplay = alias === resolved ? alias : `${alias} (${resolved})`;
|
|
302
|
+
return `${oldDisplay} ${reason} — switching to ${newModel}`;
|
|
303
|
+
}
|
|
291
304
|
/**
|
|
292
305
|
* Identify models known to hallucinate tool calls (invented names, literal
|
|
293
306
|
* `[TOOLCALL]` / `<tool_call>` text in answers) — they need the explicit
|
|
@@ -1013,8 +1026,9 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1013
1026
|
const oldModel = config.model;
|
|
1014
1027
|
config.model = nextModel;
|
|
1015
1028
|
config.onModelChange?.(nextModel, 'system');
|
|
1016
|
-
|
|
1017
|
-
|
|
1029
|
+
const switchLine = formatModelSwitch(oldModel, resolvedModel, 'returned empty', nextModel);
|
|
1030
|
+
logger.warn(`[franklin] ${switchLine}`);
|
|
1031
|
+
onEvent({ kind: 'text_delta', text: `\n*${switchLine}*\n` });
|
|
1018
1032
|
continue;
|
|
1019
1033
|
}
|
|
1020
1034
|
// No fallback available OR already tried 2 models — give up, tell the user.
|
|
@@ -1177,7 +1191,11 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1177
1191
|
const oldModel = config.model;
|
|
1178
1192
|
config.model = nextFree;
|
|
1179
1193
|
config.onModelChange?.(nextFree, 'system');
|
|
1180
|
-
|
|
1194
|
+
const reason = `failed [${classified.label}]`;
|
|
1195
|
+
onEvent({
|
|
1196
|
+
kind: 'text_delta',
|
|
1197
|
+
text: `\n*${formatModelSwitch(oldModel, resolvedModel, reason, nextFree)}*\n`,
|
|
1198
|
+
});
|
|
1181
1199
|
continue; // Retry with next model
|
|
1182
1200
|
}
|
|
1183
1201
|
}
|
|
@@ -1202,7 +1220,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1202
1220
|
recoveryAttempts = 0;
|
|
1203
1221
|
onEvent({
|
|
1204
1222
|
kind: 'text_delta',
|
|
1205
|
-
text: `\n*${oldModel
|
|
1223
|
+
text: `\n*${formatModelSwitch(oldModel, resolvedModel, 'rate-limited', nextFree)}*\n`,
|
|
1206
1224
|
});
|
|
1207
1225
|
continue;
|
|
1208
1226
|
}
|
|
@@ -7,7 +7,26 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ProviderError } from '../standard-models.js';
|
|
9
9
|
export declare const TICKER_TO_ID: Record<string, string>;
|
|
10
|
+
/** For tests + cache invalidation. */
|
|
11
|
+
export declare function clearIdResolutionCache(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a ticker to its CoinGecko id. Synchronous — checks the static
|
|
14
|
+
* map and the dynamic cache. Falls through to lowercase as a final guess.
|
|
15
|
+
*
|
|
16
|
+
* Use this from `transformData` (which is sync). Use `resolveProviderIdAsync`
|
|
17
|
+
* from `fetchData` to populate the cache before the sync read happens.
|
|
18
|
+
*/
|
|
10
19
|
export declare function resolveProviderId(ticker: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Like `resolveProviderId`, but on a static-map miss, hits CoinGecko's
|
|
22
|
+
* `/search?query=` to find the canonical id. Caches the result for 7 days
|
|
23
|
+
* so `resolveProviderId` (sync) can read it back during `transformData`.
|
|
24
|
+
*
|
|
25
|
+
* Why not always async: `transformData` is part of the Fetcher contract
|
|
26
|
+
* and is intentionally sync. `fetchData` is async, runs first, and is the
|
|
27
|
+
* right place to do network resolution. The two share state via the cache.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveProviderIdAsync(ticker: string): Promise<string>;
|
|
11
30
|
export declare function cached<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T>;
|
|
12
31
|
/** For tests: wipe every cached entry. */
|
|
13
32
|
export declare function clearCache(): void;
|
|
@@ -11,7 +11,15 @@ const BASE = 'https://api.coingecko.com/api/v3';
|
|
|
11
11
|
const UA = `franklin/${VERSION} (trading)`;
|
|
12
12
|
const TIMEOUT_MS = 10_000;
|
|
13
13
|
// Ticker → CoinGecko slug. Not exhaustive; unknown tickers fall through to
|
|
14
|
-
//
|
|
14
|
+
// the dynamic /search resolver below, which caches results.
|
|
15
|
+
//
|
|
16
|
+
// Verified 2026-05-04 in a live session: user asked Franklin for TON price,
|
|
17
|
+
// TradingMarket returned "No CoinGecko data for TON" because TON wasn't in
|
|
18
|
+
// this map and the lowercase fallback ("ton") doesn't match CoinGecko's
|
|
19
|
+
// actual id ("the-open-network"). Same hole exists for any token whose
|
|
20
|
+
// symbol differs from its id slug. Expanded the static map to cover the
|
|
21
|
+
// top ~30 currently-missing tokens, and added a /search-based resolver
|
|
22
|
+
// for everything else.
|
|
15
23
|
export const TICKER_TO_ID = {
|
|
16
24
|
BTC: 'bitcoin', ETH: 'ethereum', SOL: 'solana', BNB: 'binancecoin', XRP: 'ripple',
|
|
17
25
|
ADA: 'cardano', DOGE: 'dogecoin', AVAX: 'avalanche-2', DOT: 'polkadot', MATIC: 'matic-network',
|
|
@@ -20,12 +28,87 @@ export const TICKER_TO_ID = {
|
|
|
20
28
|
FIL: 'filecoin', AAVE: 'aave', MKR: 'maker', SNX: 'synthetix-network-token',
|
|
21
29
|
COMP: 'compound-governance-token', INJ: 'injective-protocol', TIA: 'celestia',
|
|
22
30
|
PEPE: 'pepe', WIF: 'dogwifcoin', RENDER: 'render-token',
|
|
31
|
+
// ── Added 2026-05-04 after live "No CoinGecko data for TON" report ──
|
|
32
|
+
TON: 'the-open-network', HYPE: 'hyperliquid', TRX: 'tron', TAO: 'bittensor',
|
|
33
|
+
WLD: 'worldcoin-wld', ENA: 'ethena', BERA: 'berachain-bera', JUP: 'jupiter-exchange-solana',
|
|
34
|
+
FET: 'fetch-ai', ONDO: 'ondo-finance', RNDR: 'render-token',
|
|
35
|
+
USDT: 'tether', USDC: 'usd-coin', DAI: 'dai', BCH: 'bitcoin-cash', ETC: 'ethereum-classic',
|
|
36
|
+
XLM: 'stellar', XMR: 'monero', IMX: 'immutable-x', GRT: 'the-graph', SAND: 'the-sandbox',
|
|
37
|
+
MANA: 'decentraland', AXS: 'axie-infinity', KAS: 'kaspa', ICP: 'internet-computer',
|
|
38
|
+
HBAR: 'hedera-hashgraph', VET: 'vechain', ALGO: 'algorand', FTM: 'fantom',
|
|
39
|
+
EGLD: 'elrond-erd-2', CRV: 'curve-dao-token', LDO: 'lido-dao', SHIB: 'shiba-inu',
|
|
40
|
+
BONK: 'bonk', POPCAT: 'popcat', FLOKI: 'floki', PNUT: 'peanut-the-squirrel',
|
|
23
41
|
};
|
|
42
|
+
const ID_RESOLUTION_CACHE = new Map();
|
|
43
|
+
const ID_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
44
|
+
function normalizeTicker(ticker) {
|
|
45
|
+
return ticker.toUpperCase().replace(/-USD$/, '').replace(/USDT?$/, '');
|
|
46
|
+
}
|
|
47
|
+
/** For tests + cache invalidation. */
|
|
48
|
+
export function clearIdResolutionCache() {
|
|
49
|
+
ID_RESOLUTION_CACHE.clear();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a ticker to its CoinGecko id. Synchronous — checks the static
|
|
53
|
+
* map and the dynamic cache. Falls through to lowercase as a final guess.
|
|
54
|
+
*
|
|
55
|
+
* Use this from `transformData` (which is sync). Use `resolveProviderIdAsync`
|
|
56
|
+
* from `fetchData` to populate the cache before the sync read happens.
|
|
57
|
+
*/
|
|
24
58
|
export function resolveProviderId(ticker) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
59
|
+
const normalized = normalizeTicker(ticker);
|
|
60
|
+
if (TICKER_TO_ID[normalized])
|
|
61
|
+
return TICKER_TO_ID[normalized];
|
|
62
|
+
if (TICKER_TO_ID[ticker.toUpperCase()])
|
|
63
|
+
return TICKER_TO_ID[ticker.toUpperCase()];
|
|
64
|
+
const cached = ID_RESOLUTION_CACHE.get(normalized);
|
|
65
|
+
if (cached && cached.expiresAt > Date.now())
|
|
66
|
+
return cached.id;
|
|
67
|
+
return normalized.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Like `resolveProviderId`, but on a static-map miss, hits CoinGecko's
|
|
71
|
+
* `/search?query=` to find the canonical id. Caches the result for 7 days
|
|
72
|
+
* so `resolveProviderId` (sync) can read it back during `transformData`.
|
|
73
|
+
*
|
|
74
|
+
* Why not always async: `transformData` is part of the Fetcher contract
|
|
75
|
+
* and is intentionally sync. `fetchData` is async, runs first, and is the
|
|
76
|
+
* right place to do network resolution. The two share state via the cache.
|
|
77
|
+
*/
|
|
78
|
+
export async function resolveProviderIdAsync(ticker) {
|
|
79
|
+
const normalized = normalizeTicker(ticker);
|
|
80
|
+
// Static map and dynamic cache — fast path.
|
|
81
|
+
if (TICKER_TO_ID[normalized])
|
|
82
|
+
return TICKER_TO_ID[normalized];
|
|
83
|
+
if (TICKER_TO_ID[ticker.toUpperCase()])
|
|
84
|
+
return TICKER_TO_ID[ticker.toUpperCase()];
|
|
85
|
+
const cached = ID_RESOLUTION_CACHE.get(normalized);
|
|
86
|
+
if (cached && cached.expiresAt > Date.now())
|
|
87
|
+
return cached.id;
|
|
88
|
+
// Network: ask CoinGecko's search endpoint.
|
|
89
|
+
try {
|
|
90
|
+
const result = await coingeckoGet(`/search?query=${encodeURIComponent(normalized)}`);
|
|
91
|
+
if (result && typeof result === 'object' && !('kind' in result) && 'coins' in result) {
|
|
92
|
+
const coins = result.coins;
|
|
93
|
+
if (Array.isArray(coins) && coins.length > 0) {
|
|
94
|
+
// Prefer an exact symbol match; fall back to the highest-ranked
|
|
95
|
+
// coin (lowest market_cap_rank value, ignoring null/undefined).
|
|
96
|
+
const exact = coins.find(c => c.symbol?.toUpperCase() === normalized && typeof c.id === 'string');
|
|
97
|
+
const fallback = [...coins]
|
|
98
|
+
.filter((c) => typeof c.id === 'string')
|
|
99
|
+
.sort((a, b) => (a.market_cap_rank ?? Infinity) - (b.market_cap_rank ?? Infinity))[0];
|
|
100
|
+
const resolved = exact?.id ?? fallback?.id;
|
|
101
|
+
if (resolved) {
|
|
102
|
+
ID_RESOLUTION_CACHE.set(normalized, { id: resolved, expiresAt: Date.now() + ID_TTL_MS });
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// /search itself failed — fall through to the lowercase guess.
|
|
110
|
+
}
|
|
111
|
+
return normalized.toLowerCase();
|
|
29
112
|
}
|
|
30
113
|
const cache = new Map();
|
|
31
114
|
export async function cached(key, ttlMs, fn) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cached, coingeckoGet,
|
|
1
|
+
import { cached, coingeckoGet, resolveProviderIdAsync, TTL } from './client.js';
|
|
2
2
|
export const coingeckoOHLCVFetcher = {
|
|
3
3
|
providerName: 'coingecko',
|
|
4
4
|
transformQuery(input) {
|
|
@@ -9,7 +9,7 @@ export const coingeckoOHLCVFetcher = {
|
|
|
9
9
|
return { ticker, days };
|
|
10
10
|
},
|
|
11
11
|
async fetchData(query) {
|
|
12
|
-
const id =
|
|
12
|
+
const id = await resolveProviderIdAsync(query.ticker);
|
|
13
13
|
return cached(`ohlcv:${id}:${query.days}`, TTL.ohlcv, async () => {
|
|
14
14
|
return coingeckoGet(`/coins/${id}/market_chart?vs_currency=usd&days=${query.days}&interval=daily`);
|
|
15
15
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Coerces: the nested `{ [id]: { usd: ..., usd_24h_change: ... } }`
|
|
7
7
|
* response into the standard PriceData shape.
|
|
8
8
|
*/
|
|
9
|
-
import { cached, coingeckoGet, resolveProviderId, TTL } from './client.js';
|
|
9
|
+
import { cached, coingeckoGet, resolveProviderId, resolveProviderIdAsync, TTL } from './client.js';
|
|
10
10
|
export const coingeckoPriceFetcher = {
|
|
11
11
|
providerName: 'coingecko',
|
|
12
12
|
transformQuery(input) {
|
|
@@ -17,7 +17,10 @@ export const coingeckoPriceFetcher = {
|
|
|
17
17
|
return { ticker };
|
|
18
18
|
},
|
|
19
19
|
async fetchData(query) {
|
|
20
|
-
|
|
20
|
+
// resolveProviderIdAsync warms the dynamic id cache via /search when the
|
|
21
|
+
// ticker isn't in the static map (e.g. TON → the-open-network).
|
|
22
|
+
// transformData below reads back from the same cache synchronously.
|
|
23
|
+
const id = await resolveProviderIdAsync(query.ticker);
|
|
21
24
|
return cached(`price:${id}`, TTL.price, async () => {
|
|
22
25
|
return coingeckoGet(`/simple/price?ids=${id}` +
|
|
23
26
|
`&vs_currencies=usd&include_24hr_change=true` +
|
package/package.json
CHANGED