@ckcloudai.com/clawrouter 0.0.5 → 0.0.7
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/index.js +1487 -603
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -12,6 +12,430 @@ var PROXY_PORT = (() => {
|
|
|
12
12
|
})();
|
|
13
13
|
var PLUGIN_NAME = "ckcloud";
|
|
14
14
|
|
|
15
|
+
// src/balance.ts
|
|
16
|
+
import { createPublicClient, http, erc20Abi } from "viem";
|
|
17
|
+
import { base } from "viem/chains";
|
|
18
|
+
|
|
19
|
+
// src/errors.ts
|
|
20
|
+
var RpcError = class extends Error {
|
|
21
|
+
code = "RPC_ERROR";
|
|
22
|
+
originalError;
|
|
23
|
+
constructor(message, originalError) {
|
|
24
|
+
super(`RPC error: ${message}. Check network connectivity.`);
|
|
25
|
+
this.name = "RpcError";
|
|
26
|
+
this.originalError = originalError;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/plugin-logger.ts
|
|
31
|
+
var pluginLogger = console;
|
|
32
|
+
function setPluginLogger(logger2) {
|
|
33
|
+
pluginLogger = logger2 ?? console;
|
|
34
|
+
}
|
|
35
|
+
var logger = {
|
|
36
|
+
debug: (message) => pluginLogger.debug?.(message),
|
|
37
|
+
info: (message) => pluginLogger.info(message),
|
|
38
|
+
warn: (message) => pluginLogger.warn(message),
|
|
39
|
+
error: (message) => pluginLogger.error(message)
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/balance.ts
|
|
43
|
+
var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
44
|
+
var CACHE_TTL_MS = 3e4;
|
|
45
|
+
var REMOTE_BALANCE_TIMEOUT_MS = 5e3;
|
|
46
|
+
var REMOTE_BALANCE_URL = "https://t.ckcloudai.com/v1/balance?Pubkey=";
|
|
47
|
+
var BALANCE_THRESHOLDS = {
|
|
48
|
+
/** Low balance warning threshold: $1.00 */
|
|
49
|
+
LOW_BALANCE_MICROS: 1000000n,
|
|
50
|
+
/** Effectively zero threshold: $0.0001 (covers dust/rounding) */
|
|
51
|
+
ZERO_THRESHOLD: 100n
|
|
52
|
+
};
|
|
53
|
+
var BalanceMonitor = class {
|
|
54
|
+
client;
|
|
55
|
+
walletAddress;
|
|
56
|
+
/** Cached balance (null = not yet fetched) */
|
|
57
|
+
cachedBalance = null;
|
|
58
|
+
/** Timestamp when cache was last updated */
|
|
59
|
+
cachedAt = 0;
|
|
60
|
+
constructor(walletAddress) {
|
|
61
|
+
this.walletAddress = walletAddress;
|
|
62
|
+
this.client = createPublicClient({
|
|
63
|
+
chain: base,
|
|
64
|
+
transport: http(void 0, {
|
|
65
|
+
timeout: 1e4
|
|
66
|
+
// 10 second timeout to prevent hanging on slow RPC
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check current USDC balance.
|
|
72
|
+
* Uses cache if valid, otherwise fetches from RPC.
|
|
73
|
+
*/
|
|
74
|
+
async checkBalance() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS) {
|
|
77
|
+
return this.buildInfo(this.cachedBalance);
|
|
78
|
+
}
|
|
79
|
+
const remoteBalance = await this.fetchRemoteBalance();
|
|
80
|
+
if (remoteBalance !== null) {
|
|
81
|
+
if (remoteBalance > 0n) {
|
|
82
|
+
this.cachedBalance = remoteBalance;
|
|
83
|
+
this.cachedAt = now;
|
|
84
|
+
}
|
|
85
|
+
return this.buildInfo(remoteBalance);
|
|
86
|
+
}
|
|
87
|
+
const balance = await this.fetchBalance();
|
|
88
|
+
if (balance > 0n) {
|
|
89
|
+
this.cachedBalance = balance;
|
|
90
|
+
this.cachedAt = now;
|
|
91
|
+
}
|
|
92
|
+
return this.buildInfo(balance);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Best-effort remote balance lookup.
|
|
96
|
+
* Returns balance in USDC micros or null if unavailable/invalid.
|
|
97
|
+
*/
|
|
98
|
+
async fetchRemoteBalance() {
|
|
99
|
+
const url = `${REMOTE_BALANCE_URL}${encodeURIComponent(this.walletAddress)}`;
|
|
100
|
+
try {
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timeoutId = setTimeout(() => controller.abort(), REMOTE_BALANCE_TIMEOUT_MS);
|
|
103
|
+
const resp = await fetch(url, { signal: controller.signal });
|
|
104
|
+
clearTimeout(timeoutId);
|
|
105
|
+
if (!resp.ok) {
|
|
106
|
+
logger.warn(
|
|
107
|
+
`[ckcloud] Remote balance lookup failed (${resp.status}) for ${this.walletAddress}`
|
|
108
|
+
);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const text = await resp.text();
|
|
112
|
+
const parsed = this.parseRemoteBalance(text);
|
|
113
|
+
if (parsed === null) {
|
|
114
|
+
logger.warn(`[ckcloud] Remote balance lookup returned unparseable data`);
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
logger.warn(
|
|
119
|
+
`[ckcloud] Remote balance lookup error for ${this.walletAddress}: ${err instanceof Error ? err.message : String(err)}`
|
|
120
|
+
);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
parseRemoteBalance(body) {
|
|
125
|
+
const trimmed = body.trim();
|
|
126
|
+
if (!trimmed) return null;
|
|
127
|
+
try {
|
|
128
|
+
const data = JSON.parse(trimmed);
|
|
129
|
+
const extracted = this.extractBalanceValue(data);
|
|
130
|
+
return this.coerceMicros(extracted);
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
return this.coerceMicros(trimmed);
|
|
134
|
+
}
|
|
135
|
+
extractBalanceValue(data) {
|
|
136
|
+
if (data && typeof data === "object") {
|
|
137
|
+
const obj = data;
|
|
138
|
+
return obj.balance ?? obj.balance_usd ?? obj.balanceUSD ?? (obj.data && typeof obj.data === "object" ? obj.data.balance ?? obj.data.balance_usd ?? obj.data.balanceUSD : void 0) ?? (obj.result && typeof obj.result === "object" ? obj.result.balance ?? obj.result.balance_usd ?? obj.result.balanceUSD : void 0);
|
|
139
|
+
}
|
|
140
|
+
return data;
|
|
141
|
+
}
|
|
142
|
+
coerceMicros(value) {
|
|
143
|
+
if (value === null || value === void 0) return null;
|
|
144
|
+
if (typeof value === "number") {
|
|
145
|
+
if (!Number.isFinite(value)) return null;
|
|
146
|
+
if (Number.isInteger(value)) return value >= 0 ? BigInt(value) : null;
|
|
147
|
+
return BigInt(Math.round(value * 1e6));
|
|
148
|
+
}
|
|
149
|
+
if (typeof value === "string") {
|
|
150
|
+
const match = value.trim().match(/-?\d+(\.\d+)?/);
|
|
151
|
+
if (!match) return null;
|
|
152
|
+
const s = match[0];
|
|
153
|
+
if (s.includes(".")) {
|
|
154
|
+
const num = Number.parseFloat(s);
|
|
155
|
+
if (!Number.isFinite(num)) return null;
|
|
156
|
+
return BigInt(Math.round(num * 1e6));
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return BigInt(s);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if balance is sufficient for an estimated cost.
|
|
168
|
+
*
|
|
169
|
+
* @param estimatedCostMicros - Estimated cost in USDC smallest unit (6 decimals)
|
|
170
|
+
*/
|
|
171
|
+
async checkSufficient(estimatedCostMicros) {
|
|
172
|
+
const info = await this.checkBalance();
|
|
173
|
+
if (info.balance >= estimatedCostMicros) {
|
|
174
|
+
return { sufficient: true, info };
|
|
175
|
+
}
|
|
176
|
+
const shortfall = estimatedCostMicros - info.balance;
|
|
177
|
+
return {
|
|
178
|
+
sufficient: false,
|
|
179
|
+
info,
|
|
180
|
+
shortfall: this.formatUSDC(shortfall)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Optimistically deduct estimated cost from cached balance.
|
|
185
|
+
* Call this after a successful payment to keep cache accurate.
|
|
186
|
+
*
|
|
187
|
+
* @param amountMicros - Amount to deduct in USDC smallest unit
|
|
188
|
+
*/
|
|
189
|
+
deductEstimated(amountMicros) {
|
|
190
|
+
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
191
|
+
this.cachedBalance -= amountMicros;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Invalidate cache, forcing next checkBalance() to fetch from RPC.
|
|
196
|
+
* Call this after a payment failure to get accurate balance.
|
|
197
|
+
*/
|
|
198
|
+
invalidate() {
|
|
199
|
+
this.cachedBalance = null;
|
|
200
|
+
this.cachedAt = 0;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Force refresh balance from RPC (ignores cache).
|
|
204
|
+
*/
|
|
205
|
+
async refresh() {
|
|
206
|
+
this.invalidate();
|
|
207
|
+
return this.checkBalance();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Format USDC amount (in micros) as "$X.XX".
|
|
211
|
+
*/
|
|
212
|
+
formatUSDC(amountMicros) {
|
|
213
|
+
const dollars = Number(amountMicros) / 1e6;
|
|
214
|
+
return `$${dollars.toFixed(2)}`;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get the wallet address being monitored.
|
|
218
|
+
*/
|
|
219
|
+
getWalletAddress() {
|
|
220
|
+
return this.walletAddress;
|
|
221
|
+
}
|
|
222
|
+
/** Fetch balance from RPC */
|
|
223
|
+
async fetchBalance() {
|
|
224
|
+
try {
|
|
225
|
+
const balance = await this.client.readContract({
|
|
226
|
+
address: USDC_BASE,
|
|
227
|
+
abi: erc20Abi,
|
|
228
|
+
functionName: "balanceOf",
|
|
229
|
+
args: [this.walletAddress]
|
|
230
|
+
});
|
|
231
|
+
return balance;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new RpcError(error instanceof Error ? error.message : "Unknown error", error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Build BalanceInfo from raw balance */
|
|
237
|
+
buildInfo(balance) {
|
|
238
|
+
return {
|
|
239
|
+
balance,
|
|
240
|
+
balanceUSD: this.formatUSDC(balance),
|
|
241
|
+
isLow: balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS,
|
|
242
|
+
isEmpty: balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD,
|
|
243
|
+
walletAddress: this.walletAddress
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// src/solana-balance.ts
|
|
249
|
+
import { address as solAddress, createSolanaRpc } from "@solana/kit";
|
|
250
|
+
var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
251
|
+
var SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
|
|
252
|
+
var BALANCE_TIMEOUT_MS = 1e4;
|
|
253
|
+
var CACHE_TTL_MS2 = 3e4;
|
|
254
|
+
var REMOTE_BALANCE_TIMEOUT_MS2 = 5e3;
|
|
255
|
+
var REMOTE_BALANCE_URL2 = "https://t.ckcloudai.com/v1/balance?Pubkey=";
|
|
256
|
+
var SolanaBalanceMonitor = class {
|
|
257
|
+
rpc;
|
|
258
|
+
walletAddress;
|
|
259
|
+
cachedBalance = null;
|
|
260
|
+
cachedAt = 0;
|
|
261
|
+
constructor(walletAddress, rpcUrl) {
|
|
262
|
+
this.walletAddress = walletAddress;
|
|
263
|
+
const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
|
|
264
|
+
this.rpc = createSolanaRpc(url);
|
|
265
|
+
}
|
|
266
|
+
async checkBalance() {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS2) {
|
|
269
|
+
return this.buildInfo(this.cachedBalance);
|
|
270
|
+
}
|
|
271
|
+
const remoteBalance = await this.fetchRemoteBalance();
|
|
272
|
+
if (remoteBalance !== null) {
|
|
273
|
+
if (remoteBalance > 0n) {
|
|
274
|
+
this.cachedBalance = remoteBalance;
|
|
275
|
+
this.cachedAt = now;
|
|
276
|
+
}
|
|
277
|
+
return this.buildInfo(remoteBalance);
|
|
278
|
+
}
|
|
279
|
+
const balance = await this.fetchBalance();
|
|
280
|
+
if (balance > 0n) {
|
|
281
|
+
this.cachedBalance = balance;
|
|
282
|
+
this.cachedAt = now;
|
|
283
|
+
}
|
|
284
|
+
return this.buildInfo(balance);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Best-effort remote balance lookup.
|
|
288
|
+
* Returns balance in USDC micros or null if unavailable/invalid.
|
|
289
|
+
*/
|
|
290
|
+
async fetchRemoteBalance() {
|
|
291
|
+
const url = `${REMOTE_BALANCE_URL2}${encodeURIComponent(this.walletAddress)}`;
|
|
292
|
+
try {
|
|
293
|
+
const controller = new AbortController();
|
|
294
|
+
const timeoutId = setTimeout(() => controller.abort(), REMOTE_BALANCE_TIMEOUT_MS2);
|
|
295
|
+
const resp = await fetch(url, { signal: controller.signal });
|
|
296
|
+
clearTimeout(timeoutId);
|
|
297
|
+
if (!resp.ok) {
|
|
298
|
+
logger.warn(
|
|
299
|
+
`[ckcloud] Remote balance lookup failed (${resp.status}) for ${this.walletAddress}`
|
|
300
|
+
);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const text = await resp.text();
|
|
304
|
+
const parsed = this.parseRemoteBalance(text);
|
|
305
|
+
if (parsed === null) {
|
|
306
|
+
logger.warn(`[ckcloud] Remote balance lookup returned unparseable data`);
|
|
307
|
+
}
|
|
308
|
+
return parsed;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
logger.warn(
|
|
311
|
+
`[ckcloud] Remote balance lookup error for ${this.walletAddress}: ${err instanceof Error ? err.message : String(err)}`
|
|
312
|
+
);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
parseRemoteBalance(body) {
|
|
317
|
+
const trimmed = body.trim();
|
|
318
|
+
if (!trimmed) return null;
|
|
319
|
+
try {
|
|
320
|
+
const data = JSON.parse(trimmed);
|
|
321
|
+
const extracted = this.extractBalanceValue(data);
|
|
322
|
+
return this.coerceMicros(extracted);
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
return this.coerceMicros(trimmed);
|
|
326
|
+
}
|
|
327
|
+
extractBalanceValue(data) {
|
|
328
|
+
if (data && typeof data === "object") {
|
|
329
|
+
const obj = data;
|
|
330
|
+
return obj.balance ?? obj.balance_usd ?? obj.balanceUSD ?? (obj.data && typeof obj.data === "object" ? obj.data.balance ?? obj.data.balance_usd ?? obj.data.balanceUSD : void 0) ?? (obj.result && typeof obj.result === "object" ? obj.result.balance ?? obj.result.balance_usd ?? obj.result.balanceUSD : void 0);
|
|
331
|
+
}
|
|
332
|
+
return data;
|
|
333
|
+
}
|
|
334
|
+
coerceMicros(value) {
|
|
335
|
+
if (value === null || value === void 0) return null;
|
|
336
|
+
if (typeof value === "number") {
|
|
337
|
+
if (!Number.isFinite(value)) return null;
|
|
338
|
+
if (Number.isInteger(value)) return value >= 0 ? BigInt(value) : null;
|
|
339
|
+
return BigInt(Math.round(value * 1e6));
|
|
340
|
+
}
|
|
341
|
+
if (typeof value === "string") {
|
|
342
|
+
const match = value.trim().match(/-?\d+(\.\d+)?/);
|
|
343
|
+
if (!match) return null;
|
|
344
|
+
const s = match[0];
|
|
345
|
+
if (s.includes(".")) {
|
|
346
|
+
const num = Number.parseFloat(s);
|
|
347
|
+
if (!Number.isFinite(num)) return null;
|
|
348
|
+
return BigInt(Math.round(num * 1e6));
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
return BigInt(s);
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
deductEstimated(amountMicros) {
|
|
359
|
+
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
360
|
+
this.cachedBalance -= amountMicros;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
invalidate() {
|
|
364
|
+
this.cachedBalance = null;
|
|
365
|
+
this.cachedAt = 0;
|
|
366
|
+
}
|
|
367
|
+
async refresh() {
|
|
368
|
+
this.invalidate();
|
|
369
|
+
return this.checkBalance();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Check if balance is sufficient for an estimated cost.
|
|
373
|
+
*/
|
|
374
|
+
async checkSufficient(estimatedCostMicros) {
|
|
375
|
+
const info = await this.checkBalance();
|
|
376
|
+
if (info.balance >= estimatedCostMicros) {
|
|
377
|
+
return { sufficient: true, info };
|
|
378
|
+
}
|
|
379
|
+
const shortfall = estimatedCostMicros - info.balance;
|
|
380
|
+
return {
|
|
381
|
+
sufficient: false,
|
|
382
|
+
info,
|
|
383
|
+
shortfall: this.formatUSDC(shortfall)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Format USDC amount (in micros) as "$X.XX".
|
|
388
|
+
*/
|
|
389
|
+
formatUSDC(amountMicros) {
|
|
390
|
+
const dollars = Number(amountMicros) / 1e6;
|
|
391
|
+
return `$${dollars.toFixed(2)}`;
|
|
392
|
+
}
|
|
393
|
+
getWalletAddress() {
|
|
394
|
+
return this.walletAddress;
|
|
395
|
+
}
|
|
396
|
+
async fetchBalance() {
|
|
397
|
+
const owner = solAddress(this.walletAddress);
|
|
398
|
+
const mint = solAddress(SOLANA_USDC_MINT);
|
|
399
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
400
|
+
const result = await this.fetchBalanceOnce(owner, mint);
|
|
401
|
+
if (result > 0n || attempt === 1) return result;
|
|
402
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
403
|
+
}
|
|
404
|
+
return 0n;
|
|
405
|
+
}
|
|
406
|
+
async fetchBalanceOnce(owner, mint) {
|
|
407
|
+
const controller = new AbortController();
|
|
408
|
+
const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
|
|
409
|
+
try {
|
|
410
|
+
const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
|
|
411
|
+
if (response.value.length === 0) return 0n;
|
|
412
|
+
let total = 0n;
|
|
413
|
+
for (const account of response.value) {
|
|
414
|
+
const parsed = account.account.data;
|
|
415
|
+
total += BigInt(parsed.parsed.info.tokenAmount.amount);
|
|
416
|
+
}
|
|
417
|
+
return total;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`,
|
|
421
|
+
{ cause: err }
|
|
422
|
+
);
|
|
423
|
+
} finally {
|
|
424
|
+
clearTimeout(timer);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
buildInfo(balance) {
|
|
428
|
+
const dollars = Number(balance) / 1e6;
|
|
429
|
+
return {
|
|
430
|
+
balance,
|
|
431
|
+
balanceUSD: `$${dollars.toFixed(2)}`,
|
|
432
|
+
isLow: balance < 1000000n,
|
|
433
|
+
isEmpty: balance < 100n,
|
|
434
|
+
walletAddress: this.walletAddress
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
15
439
|
// src/index.ts
|
|
16
440
|
import { join as join6 } from "path";
|
|
17
441
|
import { homedir as homedir5 } from "os";
|
|
@@ -128,7 +552,7 @@ async function loadBaseModels() {
|
|
|
128
552
|
}
|
|
129
553
|
var BASE_MODELS = [...DEFAULT_BASE_MODELS];
|
|
130
554
|
function resolveModelAlias(model) {
|
|
131
|
-
const normalized = model.trim()
|
|
555
|
+
const normalized = model.trim();
|
|
132
556
|
const resolved = MODEL_ALIASES[normalized];
|
|
133
557
|
if (resolved) return resolved;
|
|
134
558
|
if (normalized.startsWith("ckcloud/")) {
|
|
@@ -163,68 +587,7 @@ function toOpenClawModel(m) {
|
|
|
163
587
|
maxTokens: m.maxOutput
|
|
164
588
|
};
|
|
165
589
|
}
|
|
166
|
-
var MODEL_ALIASES = {
|
|
167
|
-
// Claude - use newest versions (4.6)
|
|
168
|
-
claude: "anthropic/claude-sonnet-4.6",
|
|
169
|
-
sonnet: "anthropic/claude-sonnet-4.6",
|
|
170
|
-
"sonnet-4": "anthropic/claude-sonnet-4.6",
|
|
171
|
-
"sonnet-4.6": "anthropic/claude-sonnet-4.6",
|
|
172
|
-
"sonnet-4-6": "anthropic/claude-sonnet-4.6",
|
|
173
|
-
opus: "anthropic/claude-opus-4.6",
|
|
174
|
-
"opus-4": "anthropic/claude-opus-4.6",
|
|
175
|
-
"opus-4.6": "anthropic/claude-opus-4.6",
|
|
176
|
-
"opus-4-6": "anthropic/claude-opus-4.6",
|
|
177
|
-
haiku: "anthropic/claude-haiku-4.5",
|
|
178
|
-
// Claude - provider/shortname patterns (common in agent frameworks)
|
|
179
|
-
"anthropic/sonnet": "anthropic/claude-sonnet-4.6",
|
|
180
|
-
"anthropic/opus": "anthropic/claude-opus-4.6",
|
|
181
|
-
"anthropic/haiku": "anthropic/claude-haiku-4.5",
|
|
182
|
-
"anthropic/claude": "anthropic/claude-sonnet-4.6",
|
|
183
|
-
// Backward compatibility - map all variants to 4.6
|
|
184
|
-
"anthropic/claude-sonnet-4": "anthropic/claude-sonnet-4.6",
|
|
185
|
-
"anthropic/claude-sonnet-4-6": "anthropic/claude-sonnet-4.6",
|
|
186
|
-
"anthropic/claude-opus-4": "anthropic/claude-opus-4.6",
|
|
187
|
-
"anthropic/claude-opus-4-6": "anthropic/claude-opus-4.6",
|
|
188
|
-
"anthropic/claude-opus-4.5": "anthropic/claude-opus-4.6",
|
|
189
|
-
"anthropic/claude-haiku-4": "anthropic/claude-haiku-4.5",
|
|
190
|
-
"anthropic/claude-haiku-4-5": "anthropic/claude-haiku-4.5",
|
|
191
|
-
// OpenAI
|
|
192
|
-
gpt: "openai/gpt-4o",
|
|
193
|
-
gpt4: "openai/gpt-4o",
|
|
194
|
-
gpt5: "openai/gpt-5.4",
|
|
195
|
-
"gpt-5.4": "openai/gpt-5.4",
|
|
196
|
-
"gpt-5.4-pro": "openai/gpt-5.4-pro",
|
|
197
|
-
codex: "openai/gpt-5.2-codex",
|
|
198
|
-
mini: "openai/gpt-4o-mini",
|
|
199
|
-
o1: "openai/o1",
|
|
200
|
-
o3: "openai/o3",
|
|
201
|
-
// DeepSeek
|
|
202
|
-
deepseek: "deepseek/deepseek-chat",
|
|
203
|
-
reasoner: "deepseek/deepseek-reasoner",
|
|
204
|
-
// Kimi / Moonshot
|
|
205
|
-
kimi: "moonshot/kimi-k2.5",
|
|
206
|
-
moonshot: "moonshot/kimi-k2.5",
|
|
207
|
-
"kimi-k2.5": "moonshot/kimi-k2.5",
|
|
208
|
-
// Google
|
|
209
|
-
gemini: "google/gemini-2.5-pro",
|
|
210
|
-
flash: "google/gemini-2.5-flash",
|
|
211
|
-
"gemini-3.1-pro-preview": "google/gemini-3.1-pro",
|
|
212
|
-
"google/gemini-3.1-pro-preview": "google/gemini-3.1-pro",
|
|
213
|
-
// xAI
|
|
214
|
-
grok: "xai/grok-3",
|
|
215
|
-
"grok-fast": "xai/grok-4-fast-reasoning",
|
|
216
|
-
"grok-code": "xai/grok-code-fast-1",
|
|
217
|
-
// NVIDIA
|
|
218
|
-
nvidia: "nvidia/gpt-oss-120b",
|
|
219
|
-
"gpt-120b": "nvidia/gpt-oss-120b",
|
|
220
|
-
// MiniMax
|
|
221
|
-
minimax: "minimax/minimax-m2.5",
|
|
222
|
-
// Routing profile aliases (common variations)
|
|
223
|
-
"auto-router": "auto",
|
|
224
|
-
router: "auto"
|
|
225
|
-
// Note: auto, free, eco, premium are virtual routing profiles registered in BASE_MODELS
|
|
226
|
-
// They don't need aliases since they're already top-level model IDs
|
|
227
|
-
};
|
|
590
|
+
var MODEL_ALIASES = {};
|
|
228
591
|
function buildAliasModels(baseModels) {
|
|
229
592
|
return Object.entries(MODEL_ALIASES).map(([alias, targetId]) => {
|
|
230
593
|
const target = baseModels.find((m) => m.id === targetId);
|
|
@@ -338,305 +701,57 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
338
701
|
try {
|
|
339
702
|
const payload2 = await client.createPaymentPayload(cached.paymentRequired);
|
|
340
703
|
const headers = httpClient.encodePaymentSignatureHeader(payload2);
|
|
341
|
-
const preAuthRequest = request.clone();
|
|
342
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
343
|
-
preAuthRequest.headers.set(key, value);
|
|
344
|
-
}
|
|
345
|
-
const response2 = await baseFetch(preAuthRequest);
|
|
346
|
-
if (response2.status !== 402) {
|
|
347
|
-
return response2;
|
|
348
|
-
}
|
|
349
|
-
cache.delete(urlPath);
|
|
350
|
-
} catch {
|
|
351
|
-
cache.delete(urlPath);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
const clonedRequest = request.clone();
|
|
355
|
-
const response = await baseFetch(request);
|
|
356
|
-
if (response.status !== 402) {
|
|
357
|
-
return response;
|
|
358
|
-
}
|
|
359
|
-
let paymentRequired;
|
|
360
|
-
try {
|
|
361
|
-
const getHeader = (name) => response.headers.get(name);
|
|
362
|
-
let body;
|
|
363
|
-
try {
|
|
364
|
-
const responseText = await Promise.race([
|
|
365
|
-
response.text(),
|
|
366
|
-
new Promise(
|
|
367
|
-
(_, reject) => setTimeout(() => reject(new Error("Body read timeout")), 3e4)
|
|
368
|
-
)
|
|
369
|
-
]);
|
|
370
|
-
if (responseText) body = JSON.parse(responseText);
|
|
371
|
-
} catch {
|
|
372
|
-
}
|
|
373
|
-
paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, body);
|
|
374
|
-
cache.set(urlPath, { paymentRequired, cachedAt: Date.now() });
|
|
375
|
-
} catch (error) {
|
|
376
|
-
throw new Error(
|
|
377
|
-
`Failed to parse payment requirements: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
378
|
-
{ cause: error }
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
const payload = await client.createPaymentPayload(paymentRequired);
|
|
382
|
-
const paymentHeaders = httpClient.encodePaymentSignatureHeader(payload);
|
|
383
|
-
for (const [key, value] of Object.entries(paymentHeaders)) {
|
|
384
|
-
clonedRequest.headers.set(key, value);
|
|
385
|
-
}
|
|
386
|
-
return baseFetch(clonedRequest);
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// src/proxy.ts
|
|
391
|
-
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
392
|
-
|
|
393
|
-
// src/balance.ts
|
|
394
|
-
import { createPublicClient, http, erc20Abi } from "viem";
|
|
395
|
-
import { base } from "viem/chains";
|
|
396
|
-
|
|
397
|
-
// src/errors.ts
|
|
398
|
-
var RpcError = class extends Error {
|
|
399
|
-
code = "RPC_ERROR";
|
|
400
|
-
originalError;
|
|
401
|
-
constructor(message, originalError) {
|
|
402
|
-
super(`RPC error: ${message}. Check network connectivity.`);
|
|
403
|
-
this.name = "RpcError";
|
|
404
|
-
this.originalError = originalError;
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
// src/balance.ts
|
|
409
|
-
var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
410
|
-
var CACHE_TTL_MS = 3e4;
|
|
411
|
-
var BALANCE_THRESHOLDS = {
|
|
412
|
-
/** Low balance warning threshold: $1.00 */
|
|
413
|
-
LOW_BALANCE_MICROS: 1000000n,
|
|
414
|
-
/** Effectively zero threshold: $0.0001 (covers dust/rounding) */
|
|
415
|
-
ZERO_THRESHOLD: 100n
|
|
416
|
-
};
|
|
417
|
-
var BalanceMonitor = class {
|
|
418
|
-
client;
|
|
419
|
-
walletAddress;
|
|
420
|
-
/** Cached balance (null = not yet fetched) */
|
|
421
|
-
cachedBalance = null;
|
|
422
|
-
/** Timestamp when cache was last updated */
|
|
423
|
-
cachedAt = 0;
|
|
424
|
-
constructor(walletAddress) {
|
|
425
|
-
this.walletAddress = walletAddress;
|
|
426
|
-
this.client = createPublicClient({
|
|
427
|
-
chain: base,
|
|
428
|
-
transport: http(void 0, {
|
|
429
|
-
timeout: 1e4
|
|
430
|
-
// 10 second timeout to prevent hanging on slow RPC
|
|
431
|
-
})
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Check current USDC balance.
|
|
436
|
-
* Uses cache if valid, otherwise fetches from RPC.
|
|
437
|
-
*/
|
|
438
|
-
async checkBalance() {
|
|
439
|
-
const now = Date.now();
|
|
440
|
-
if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS) {
|
|
441
|
-
return this.buildInfo(this.cachedBalance);
|
|
442
|
-
}
|
|
443
|
-
const balance = await this.fetchBalance();
|
|
444
|
-
if (balance > 0n) {
|
|
445
|
-
this.cachedBalance = balance;
|
|
446
|
-
this.cachedAt = now;
|
|
447
|
-
}
|
|
448
|
-
return this.buildInfo(balance);
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Check if balance is sufficient for an estimated cost.
|
|
452
|
-
*
|
|
453
|
-
* @param estimatedCostMicros - Estimated cost in USDC smallest unit (6 decimals)
|
|
454
|
-
*/
|
|
455
|
-
async checkSufficient(estimatedCostMicros) {
|
|
456
|
-
const info = await this.checkBalance();
|
|
457
|
-
if (info.balance >= estimatedCostMicros) {
|
|
458
|
-
return { sufficient: true, info };
|
|
459
|
-
}
|
|
460
|
-
const shortfall = estimatedCostMicros - info.balance;
|
|
461
|
-
return {
|
|
462
|
-
sufficient: false,
|
|
463
|
-
info,
|
|
464
|
-
shortfall: this.formatUSDC(shortfall)
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Optimistically deduct estimated cost from cached balance.
|
|
469
|
-
* Call this after a successful payment to keep cache accurate.
|
|
470
|
-
*
|
|
471
|
-
* @param amountMicros - Amount to deduct in USDC smallest unit
|
|
472
|
-
*/
|
|
473
|
-
deductEstimated(amountMicros) {
|
|
474
|
-
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
475
|
-
this.cachedBalance -= amountMicros;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Invalidate cache, forcing next checkBalance() to fetch from RPC.
|
|
480
|
-
* Call this after a payment failure to get accurate balance.
|
|
481
|
-
*/
|
|
482
|
-
invalidate() {
|
|
483
|
-
this.cachedBalance = null;
|
|
484
|
-
this.cachedAt = 0;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Force refresh balance from RPC (ignores cache).
|
|
488
|
-
*/
|
|
489
|
-
async refresh() {
|
|
490
|
-
this.invalidate();
|
|
491
|
-
return this.checkBalance();
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Format USDC amount (in micros) as "$X.XX".
|
|
495
|
-
*/
|
|
496
|
-
formatUSDC(amountMicros) {
|
|
497
|
-
const dollars = Number(amountMicros) / 1e6;
|
|
498
|
-
return `$${dollars.toFixed(2)}`;
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Get the wallet address being monitored.
|
|
502
|
-
*/
|
|
503
|
-
getWalletAddress() {
|
|
504
|
-
return this.walletAddress;
|
|
505
|
-
}
|
|
506
|
-
/** Fetch balance from RPC */
|
|
507
|
-
async fetchBalance() {
|
|
508
|
-
try {
|
|
509
|
-
const balance = await this.client.readContract({
|
|
510
|
-
address: USDC_BASE,
|
|
511
|
-
abi: erc20Abi,
|
|
512
|
-
functionName: "balanceOf",
|
|
513
|
-
args: [this.walletAddress]
|
|
514
|
-
});
|
|
515
|
-
return balance;
|
|
516
|
-
} catch (error) {
|
|
517
|
-
throw new RpcError(error instanceof Error ? error.message : "Unknown error", error);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
/** Build BalanceInfo from raw balance */
|
|
521
|
-
buildInfo(balance) {
|
|
522
|
-
return {
|
|
523
|
-
balance,
|
|
524
|
-
balanceUSD: this.formatUSDC(balance),
|
|
525
|
-
isLow: balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS,
|
|
526
|
-
isEmpty: balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD,
|
|
527
|
-
walletAddress: this.walletAddress
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
// src/solana-balance.ts
|
|
533
|
-
import { address as solAddress, createSolanaRpc } from "@solana/kit";
|
|
534
|
-
var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
535
|
-
var SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
|
|
536
|
-
var BALANCE_TIMEOUT_MS = 1e4;
|
|
537
|
-
var CACHE_TTL_MS2 = 3e4;
|
|
538
|
-
var SolanaBalanceMonitor = class {
|
|
539
|
-
rpc;
|
|
540
|
-
walletAddress;
|
|
541
|
-
cachedBalance = null;
|
|
542
|
-
cachedAt = 0;
|
|
543
|
-
constructor(walletAddress, rpcUrl) {
|
|
544
|
-
this.walletAddress = walletAddress;
|
|
545
|
-
const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
|
|
546
|
-
this.rpc = createSolanaRpc(url);
|
|
547
|
-
}
|
|
548
|
-
async checkBalance() {
|
|
549
|
-
const now = Date.now();
|
|
550
|
-
if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS2) {
|
|
551
|
-
return this.buildInfo(this.cachedBalance);
|
|
552
|
-
}
|
|
553
|
-
const balance = await this.fetchBalance();
|
|
554
|
-
if (balance > 0n) {
|
|
555
|
-
this.cachedBalance = balance;
|
|
556
|
-
this.cachedAt = now;
|
|
557
|
-
}
|
|
558
|
-
return this.buildInfo(balance);
|
|
559
|
-
}
|
|
560
|
-
deductEstimated(amountMicros) {
|
|
561
|
-
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
562
|
-
this.cachedBalance -= amountMicros;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
invalidate() {
|
|
566
|
-
this.cachedBalance = null;
|
|
567
|
-
this.cachedAt = 0;
|
|
568
|
-
}
|
|
569
|
-
async refresh() {
|
|
570
|
-
this.invalidate();
|
|
571
|
-
return this.checkBalance();
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Check if balance is sufficient for an estimated cost.
|
|
575
|
-
*/
|
|
576
|
-
async checkSufficient(estimatedCostMicros) {
|
|
577
|
-
const info = await this.checkBalance();
|
|
578
|
-
if (info.balance >= estimatedCostMicros) {
|
|
579
|
-
return { sufficient: true, info };
|
|
704
|
+
const preAuthRequest = request.clone();
|
|
705
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
706
|
+
preAuthRequest.headers.set(key, value);
|
|
707
|
+
}
|
|
708
|
+
const response2 = await baseFetch(preAuthRequest);
|
|
709
|
+
if (response2.status !== 402) {
|
|
710
|
+
return response2;
|
|
711
|
+
}
|
|
712
|
+
cache.delete(urlPath);
|
|
713
|
+
} catch {
|
|
714
|
+
cache.delete(urlPath);
|
|
715
|
+
}
|
|
580
716
|
}
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
shortfall: this.formatUSDC(shortfall)
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Format USDC amount (in micros) as "$X.XX".
|
|
590
|
-
*/
|
|
591
|
-
formatUSDC(amountMicros) {
|
|
592
|
-
const dollars = Number(amountMicros) / 1e6;
|
|
593
|
-
return `$${dollars.toFixed(2)}`;
|
|
594
|
-
}
|
|
595
|
-
getWalletAddress() {
|
|
596
|
-
return this.walletAddress;
|
|
597
|
-
}
|
|
598
|
-
async fetchBalance() {
|
|
599
|
-
const owner = solAddress(this.walletAddress);
|
|
600
|
-
const mint = solAddress(SOLANA_USDC_MINT);
|
|
601
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
602
|
-
const result = await this.fetchBalanceOnce(owner, mint);
|
|
603
|
-
if (result > 0n || attempt === 1) return result;
|
|
604
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
717
|
+
const clonedRequest = request.clone();
|
|
718
|
+
const response = await baseFetch(request);
|
|
719
|
+
if (response.status !== 402) {
|
|
720
|
+
return response;
|
|
605
721
|
}
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
async fetchBalanceOnce(owner, mint) {
|
|
609
|
-
const controller = new AbortController();
|
|
610
|
-
const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
|
|
722
|
+
let paymentRequired;
|
|
611
723
|
try {
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
724
|
+
const getHeader = (name) => response.headers.get(name);
|
|
725
|
+
let body;
|
|
726
|
+
try {
|
|
727
|
+
const responseText = await Promise.race([
|
|
728
|
+
response.text(),
|
|
729
|
+
new Promise(
|
|
730
|
+
(_, reject) => setTimeout(() => reject(new Error("Body read timeout")), 3e4)
|
|
731
|
+
)
|
|
732
|
+
]);
|
|
733
|
+
if (responseText) body = JSON.parse(responseText);
|
|
734
|
+
} catch {
|
|
618
735
|
}
|
|
619
|
-
|
|
620
|
-
|
|
736
|
+
paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, body);
|
|
737
|
+
cache.set(urlPath, { paymentRequired, cachedAt: Date.now() });
|
|
738
|
+
} catch (error) {
|
|
621
739
|
throw new Error(
|
|
622
|
-
`Failed to
|
|
623
|
-
{ cause:
|
|
740
|
+
`Failed to parse payment requirements: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
741
|
+
{ cause: error }
|
|
624
742
|
);
|
|
625
|
-
} finally {
|
|
626
|
-
clearTimeout(timer);
|
|
627
743
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
};
|
|
744
|
+
const payload = await client.createPaymentPayload(paymentRequired);
|
|
745
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(payload);
|
|
746
|
+
for (const [key, value] of Object.entries(paymentHeaders)) {
|
|
747
|
+
clonedRequest.headers.set(key, value);
|
|
748
|
+
}
|
|
749
|
+
return baseFetch(clonedRequest);
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/proxy.ts
|
|
754
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
640
755
|
|
|
641
756
|
// src/stats.ts
|
|
642
757
|
import { readdir, unlink } from "fs/promises";
|
|
@@ -791,86 +906,819 @@ function formatStatsAscii(stats) {
|
|
|
791
906
|
} else {
|
|
792
907
|
lines.push(savingsLine.padEnd(61) + "\u2551");
|
|
793
908
|
}
|
|
794
|
-
lines.push(`\u2551 Avg Latency: ${stats.avgLatencyMs.toFixed(0)}ms`.padEnd(61) + "\u2551");
|
|
795
|
-
lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
796
|
-
lines.push("\u2551 Routing by Tier: \u2551");
|
|
797
|
-
const knownTiers = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING", "DIRECT"];
|
|
798
|
-
const allTiers = Object.keys(stats.byTier);
|
|
799
|
-
const otherTiers = allTiers.filter((t) => !knownTiers.includes(t));
|
|
800
|
-
const tierOrder = [...knownTiers.filter((t) => stats.byTier[t]), ...otherTiers];
|
|
801
|
-
for (const tier of tierOrder) {
|
|
802
|
-
const data = stats.byTier[tier];
|
|
803
|
-
if (data) {
|
|
804
|
-
const bar = "\u2588".repeat(Math.min(20, Math.round(data.percentage / 5)));
|
|
805
|
-
const displayTier = tier === "UNKNOWN" ? "OTHER" : tier;
|
|
806
|
-
const line = `\u2551 ${displayTier.padEnd(10)} ${bar.padEnd(20)} ${data.percentage.toFixed(1).padStart(5)}% (${data.count})`;
|
|
807
|
-
lines.push(line.padEnd(61) + "\u2551");
|
|
909
|
+
lines.push(`\u2551 Avg Latency: ${stats.avgLatencyMs.toFixed(0)}ms`.padEnd(61) + "\u2551");
|
|
910
|
+
lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
911
|
+
lines.push("\u2551 Routing by Tier: \u2551");
|
|
912
|
+
const knownTiers = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING", "DIRECT"];
|
|
913
|
+
const allTiers = Object.keys(stats.byTier);
|
|
914
|
+
const otherTiers = allTiers.filter((t) => !knownTiers.includes(t));
|
|
915
|
+
const tierOrder = [...knownTiers.filter((t) => stats.byTier[t]), ...otherTiers];
|
|
916
|
+
for (const tier of tierOrder) {
|
|
917
|
+
const data = stats.byTier[tier];
|
|
918
|
+
if (data) {
|
|
919
|
+
const bar = "\u2588".repeat(Math.min(20, Math.round(data.percentage / 5)));
|
|
920
|
+
const displayTier = tier === "UNKNOWN" ? "OTHER" : tier;
|
|
921
|
+
const line = `\u2551 ${displayTier.padEnd(10)} ${bar.padEnd(20)} ${data.percentage.toFixed(1).padStart(5)}% (${data.count})`;
|
|
922
|
+
lines.push(line.padEnd(61) + "\u2551");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
926
|
+
lines.push("\u2551 Top Models: \u2551");
|
|
927
|
+
const sortedModels = Object.entries(stats.byModel).sort((a, b) => b[1].count - a[1].count).slice(0, 5);
|
|
928
|
+
for (const [model, data] of sortedModels) {
|
|
929
|
+
const shortModel = model.length > 25 ? model.slice(0, 22) + "..." : model;
|
|
930
|
+
const line = `\u2551 ${shortModel.padEnd(25)} ${data.count.toString().padStart(5)} reqs $${data.cost.toFixed(4)}`;
|
|
931
|
+
lines.push(line.padEnd(61) + "\u2551");
|
|
932
|
+
}
|
|
933
|
+
if (stats.dailyBreakdown.length > 0) {
|
|
934
|
+
lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
935
|
+
lines.push("\u2551 Daily Breakdown: \u2551");
|
|
936
|
+
lines.push("\u2551 Date Requests Cost Saved \u2551");
|
|
937
|
+
for (const day of stats.dailyBreakdown.slice(-7)) {
|
|
938
|
+
const saved = day.totalBaselineCost - day.totalCost;
|
|
939
|
+
const line = `\u2551 ${day.date} ${day.totalRequests.toString().padStart(6)} $${day.totalCost.toFixed(4).padStart(8)} $${saved.toFixed(4)}`;
|
|
940
|
+
lines.push(line.padEnd(61) + "\u2551");
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
944
|
+
return lines.join("\n");
|
|
945
|
+
}
|
|
946
|
+
async function clearStats() {
|
|
947
|
+
try {
|
|
948
|
+
const files = await readdir(LOG_DIR);
|
|
949
|
+
const logFiles = files.filter((f) => f.startsWith("usage-") && f.endsWith(".jsonl"));
|
|
950
|
+
await Promise.all(logFiles.map((f) => unlink(join2(LOG_DIR, f))));
|
|
951
|
+
return { deletedFiles: logFiles.length };
|
|
952
|
+
} catch {
|
|
953
|
+
return { deletedFiles: 0 };
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/logger.ts
|
|
958
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
959
|
+
import { join as join3 } from "path";
|
|
960
|
+
import { homedir as homedir2 } from "os";
|
|
961
|
+
var LOG_DIR2 = join3(homedir2(), ".openclaw", "ckcloud", "logs");
|
|
962
|
+
var dirReady = false;
|
|
963
|
+
async function ensureDir() {
|
|
964
|
+
if (dirReady) return;
|
|
965
|
+
await mkdir(LOG_DIR2, { recursive: true });
|
|
966
|
+
dirReady = true;
|
|
967
|
+
}
|
|
968
|
+
async function logUsage(entry) {
|
|
969
|
+
try {
|
|
970
|
+
await ensureDir();
|
|
971
|
+
const date = entry.timestamp.slice(0, 10);
|
|
972
|
+
const file = join3(LOG_DIR2, `usage-${date}.jsonl`);
|
|
973
|
+
await appendFile(file, JSON.stringify(entry) + "\n");
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/compression/types.ts
|
|
979
|
+
var DEFAULT_COMPRESSION_CONFIG = {
|
|
980
|
+
enabled: true,
|
|
981
|
+
preserveRaw: true,
|
|
982
|
+
layers: {
|
|
983
|
+
deduplication: true,
|
|
984
|
+
// Safe: removes duplicate messages
|
|
985
|
+
whitespace: true,
|
|
986
|
+
// Safe: normalizes whitespace
|
|
987
|
+
dictionary: false,
|
|
988
|
+
// DISABLED: requires model to understand codebook
|
|
989
|
+
paths: false,
|
|
990
|
+
// DISABLED: requires model to understand path codes
|
|
991
|
+
jsonCompact: true,
|
|
992
|
+
// Safe: just removes JSON whitespace
|
|
993
|
+
observation: false,
|
|
994
|
+
// DISABLED: may lose important context
|
|
995
|
+
dynamicCodebook: false
|
|
996
|
+
// DISABLED: requires model to understand codes
|
|
997
|
+
},
|
|
998
|
+
dictionary: {
|
|
999
|
+
maxEntries: 50,
|
|
1000
|
+
minPhraseLength: 15,
|
|
1001
|
+
includeCodebookHeader: false
|
|
1002
|
+
// No codebook header needed
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// src/compression/layers/deduplication.ts
|
|
1007
|
+
import crypto from "crypto";
|
|
1008
|
+
function hashMessage(message) {
|
|
1009
|
+
let contentStr = "";
|
|
1010
|
+
if (typeof message.content === "string") {
|
|
1011
|
+
contentStr = message.content;
|
|
1012
|
+
} else if (Array.isArray(message.content)) {
|
|
1013
|
+
contentStr = JSON.stringify(message.content);
|
|
1014
|
+
}
|
|
1015
|
+
const parts = [message.role, contentStr, message.tool_call_id || "", message.name || ""];
|
|
1016
|
+
if (message.tool_calls) {
|
|
1017
|
+
parts.push(
|
|
1018
|
+
JSON.stringify(
|
|
1019
|
+
message.tool_calls.map((tc) => ({
|
|
1020
|
+
name: tc.function.name,
|
|
1021
|
+
args: tc.function.arguments
|
|
1022
|
+
}))
|
|
1023
|
+
)
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
const content = parts.join("|");
|
|
1027
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
1028
|
+
}
|
|
1029
|
+
function deduplicateMessages(messages) {
|
|
1030
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1031
|
+
const result = [];
|
|
1032
|
+
let duplicatesRemoved = 0;
|
|
1033
|
+
const referencedToolCallIds = /* @__PURE__ */ new Set();
|
|
1034
|
+
for (const message of messages) {
|
|
1035
|
+
if (message.role === "tool" && message.tool_call_id) {
|
|
1036
|
+
referencedToolCallIds.add(message.tool_call_id);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
for (const message of messages) {
|
|
1040
|
+
if (message.role === "system") {
|
|
1041
|
+
result.push(message);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (message.role === "user") {
|
|
1045
|
+
result.push(message);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (message.role === "tool") {
|
|
1049
|
+
result.push(message);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (message.role === "assistant" && message.tool_calls) {
|
|
1053
|
+
const hasReferencedToolCall = message.tool_calls.some(
|
|
1054
|
+
(tc) => referencedToolCallIds.has(tc.id)
|
|
1055
|
+
);
|
|
1056
|
+
if (hasReferencedToolCall) {
|
|
1057
|
+
result.push(message);
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const hash = hashMessage(message);
|
|
1062
|
+
if (!seen.has(hash)) {
|
|
1063
|
+
seen.add(hash);
|
|
1064
|
+
result.push(message);
|
|
1065
|
+
} else {
|
|
1066
|
+
duplicatesRemoved++;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
messages: result,
|
|
1071
|
+
duplicatesRemoved,
|
|
1072
|
+
originalCount: messages.length
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/compression/layers/whitespace.ts
|
|
1077
|
+
function normalizeWhitespace(content) {
|
|
1078
|
+
if (!content || typeof content !== "string") return content;
|
|
1079
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]+$/gm, "").replace(/([^\n]) {2,}/g, "$1 ").replace(/^[ ]{8,}/gm, (match) => " ".repeat(Math.ceil(match.length / 4))).replace(/\t/g, " ").trim();
|
|
1080
|
+
}
|
|
1081
|
+
function normalizeMessagesWhitespace(messages) {
|
|
1082
|
+
let charsSaved = 0;
|
|
1083
|
+
const result = messages.map((message) => {
|
|
1084
|
+
if (!message.content || typeof message.content !== "string") return message;
|
|
1085
|
+
const originalLength = message.content.length;
|
|
1086
|
+
const normalizedContent = normalizeWhitespace(message.content);
|
|
1087
|
+
charsSaved += originalLength - normalizedContent.length;
|
|
1088
|
+
return {
|
|
1089
|
+
...message,
|
|
1090
|
+
content: normalizedContent
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
return {
|
|
1094
|
+
messages: result,
|
|
1095
|
+
charsSaved
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/compression/codebook.ts
|
|
1100
|
+
var STATIC_CODEBOOK = {
|
|
1101
|
+
// High-impact: OpenClaw/Agent system prompt patterns (very common)
|
|
1102
|
+
$OC01: "unbrowse_",
|
|
1103
|
+
// Common prefix in tool names
|
|
1104
|
+
$OC02: "<location>",
|
|
1105
|
+
$OC03: "</location>",
|
|
1106
|
+
$OC04: "<name>",
|
|
1107
|
+
$OC05: "</name>",
|
|
1108
|
+
$OC06: "<description>",
|
|
1109
|
+
$OC07: "</description>",
|
|
1110
|
+
$OC08: "(may need login)",
|
|
1111
|
+
$OC09: "API skill for OpenClaw",
|
|
1112
|
+
$OC10: "endpoints",
|
|
1113
|
+
// Skill/tool markers
|
|
1114
|
+
$SK01: "<available_skills>",
|
|
1115
|
+
$SK02: "</available_skills>",
|
|
1116
|
+
$SK03: "<skill>",
|
|
1117
|
+
$SK04: "</skill>",
|
|
1118
|
+
// Schema patterns (very common in tool definitions)
|
|
1119
|
+
$T01: 'type: "function"',
|
|
1120
|
+
$T02: '"type": "function"',
|
|
1121
|
+
$T03: '"type": "string"',
|
|
1122
|
+
$T04: '"type": "object"',
|
|
1123
|
+
$T05: '"type": "array"',
|
|
1124
|
+
$T06: '"type": "boolean"',
|
|
1125
|
+
$T07: '"type": "number"',
|
|
1126
|
+
// Common descriptions
|
|
1127
|
+
$D01: "description:",
|
|
1128
|
+
$D02: '"description":',
|
|
1129
|
+
// Common instructions
|
|
1130
|
+
$I01: "You are a personal assistant",
|
|
1131
|
+
$I02: "Tool names are case-sensitive",
|
|
1132
|
+
$I03: "Call tools exactly as listed",
|
|
1133
|
+
$I04: "Use when",
|
|
1134
|
+
$I05: "without asking",
|
|
1135
|
+
// Safety phrases
|
|
1136
|
+
$S01: "Do not manipulate or persuade",
|
|
1137
|
+
$S02: "Prioritize safety and human oversight",
|
|
1138
|
+
$S03: "unless explicitly requested",
|
|
1139
|
+
// JSON patterns
|
|
1140
|
+
$J01: '"required": ["',
|
|
1141
|
+
$J02: '"properties": {',
|
|
1142
|
+
$J03: '"additionalProperties": false',
|
|
1143
|
+
// Heartbeat patterns
|
|
1144
|
+
$H01: "HEARTBEAT_OK",
|
|
1145
|
+
$H02: "Read HEARTBEAT.md if it exists",
|
|
1146
|
+
// Role markers
|
|
1147
|
+
$R01: '"role": "system"',
|
|
1148
|
+
$R02: '"role": "user"',
|
|
1149
|
+
$R03: '"role": "assistant"',
|
|
1150
|
+
$R04: '"role": "tool"',
|
|
1151
|
+
// Common endings/phrases
|
|
1152
|
+
$E01: "would you like to",
|
|
1153
|
+
$E02: "Let me know if you",
|
|
1154
|
+
$E03: "internal APIs",
|
|
1155
|
+
$E04: "session cookies",
|
|
1156
|
+
// ckcloud model aliases (common in prompts)
|
|
1157
|
+
$M01: "ckcloud/",
|
|
1158
|
+
$M02: "openai/",
|
|
1159
|
+
$M03: "anthropic/",
|
|
1160
|
+
$M04: "google/",
|
|
1161
|
+
$M05: "xai/"
|
|
1162
|
+
};
|
|
1163
|
+
function getInverseCodebook() {
|
|
1164
|
+
const inverse = {};
|
|
1165
|
+
for (const [code, phrase] of Object.entries(STATIC_CODEBOOK)) {
|
|
1166
|
+
inverse[phrase] = code;
|
|
1167
|
+
}
|
|
1168
|
+
return inverse;
|
|
1169
|
+
}
|
|
1170
|
+
function generateCodebookHeader(usedCodes, pathMap = {}) {
|
|
1171
|
+
if (usedCodes.size === 0 && Object.keys(pathMap).length === 0) {
|
|
1172
|
+
return "";
|
|
1173
|
+
}
|
|
1174
|
+
const parts = [];
|
|
1175
|
+
if (usedCodes.size > 0) {
|
|
1176
|
+
const codeEntries = Array.from(usedCodes).map((code) => `${code}=${STATIC_CODEBOOK[code]}`).join(", ");
|
|
1177
|
+
parts.push(`[Dict: ${codeEntries}]`);
|
|
1178
|
+
}
|
|
1179
|
+
if (Object.keys(pathMap).length > 0) {
|
|
1180
|
+
const pathEntries = Object.entries(pathMap).map(([code, path]) => `${code}=${path}`).join(", ");
|
|
1181
|
+
parts.push(`[Paths: ${pathEntries}]`);
|
|
1182
|
+
}
|
|
1183
|
+
return parts.join("\n");
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/compression/layers/dictionary.ts
|
|
1187
|
+
function encodeContent(content, inverseCodebook) {
|
|
1188
|
+
if (!content || typeof content !== "string") {
|
|
1189
|
+
return { encoded: content, substitutions: 0, codes: /* @__PURE__ */ new Set(), charsSaved: 0 };
|
|
1190
|
+
}
|
|
1191
|
+
let encoded = content;
|
|
1192
|
+
let substitutions = 0;
|
|
1193
|
+
let charsSaved = 0;
|
|
1194
|
+
const codes = /* @__PURE__ */ new Set();
|
|
1195
|
+
const phrases = Object.keys(inverseCodebook).sort((a, b) => b.length - a.length);
|
|
1196
|
+
for (const phrase of phrases) {
|
|
1197
|
+
const code = inverseCodebook[phrase];
|
|
1198
|
+
const regex = new RegExp(escapeRegex(phrase), "g");
|
|
1199
|
+
const matches = encoded.match(regex);
|
|
1200
|
+
if (matches && matches.length > 0) {
|
|
1201
|
+
encoded = encoded.replace(regex, code);
|
|
1202
|
+
substitutions += matches.length;
|
|
1203
|
+
charsSaved += matches.length * (phrase.length - code.length);
|
|
1204
|
+
codes.add(code);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return { encoded, substitutions, codes, charsSaved };
|
|
1208
|
+
}
|
|
1209
|
+
function escapeRegex(str) {
|
|
1210
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1211
|
+
}
|
|
1212
|
+
function encodeMessages(messages) {
|
|
1213
|
+
const inverseCodebook = getInverseCodebook();
|
|
1214
|
+
let totalSubstitutions = 0;
|
|
1215
|
+
let totalCharsSaved = 0;
|
|
1216
|
+
const allUsedCodes = /* @__PURE__ */ new Set();
|
|
1217
|
+
const result = messages.map((message) => {
|
|
1218
|
+
if (!message.content || typeof message.content !== "string") return message;
|
|
1219
|
+
const { encoded, substitutions, codes, charsSaved } = encodeContent(
|
|
1220
|
+
message.content,
|
|
1221
|
+
inverseCodebook
|
|
1222
|
+
);
|
|
1223
|
+
totalSubstitutions += substitutions;
|
|
1224
|
+
totalCharsSaved += charsSaved;
|
|
1225
|
+
codes.forEach((code) => allUsedCodes.add(code));
|
|
1226
|
+
return {
|
|
1227
|
+
...message,
|
|
1228
|
+
content: encoded
|
|
1229
|
+
};
|
|
1230
|
+
});
|
|
1231
|
+
return {
|
|
1232
|
+
messages: result,
|
|
1233
|
+
substitutionCount: totalSubstitutions,
|
|
1234
|
+
usedCodes: allUsedCodes,
|
|
1235
|
+
charsSaved: totalCharsSaved
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/compression/layers/paths.ts
|
|
1240
|
+
var PATH_REGEX = /(?:\/[\w.-]+){3,}/g;
|
|
1241
|
+
function extractPaths(messages) {
|
|
1242
|
+
const paths = [];
|
|
1243
|
+
for (const message of messages) {
|
|
1244
|
+
if (!message.content || typeof message.content !== "string") continue;
|
|
1245
|
+
const matches = message.content.match(PATH_REGEX);
|
|
1246
|
+
if (matches) {
|
|
1247
|
+
paths.push(...matches);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return paths;
|
|
1251
|
+
}
|
|
1252
|
+
function findFrequentPrefixes(paths) {
|
|
1253
|
+
const prefixCounts = /* @__PURE__ */ new Map();
|
|
1254
|
+
for (const path of paths) {
|
|
1255
|
+
const parts = path.split("/").filter(Boolean);
|
|
1256
|
+
for (let i = 2; i < parts.length; i++) {
|
|
1257
|
+
const prefix = "/" + parts.slice(0, i).join("/") + "/";
|
|
1258
|
+
prefixCounts.set(prefix, (prefixCounts.get(prefix) || 0) + 1);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return Array.from(prefixCounts.entries()).filter(([, count]) => count >= 3).sort((a, b) => b[0].length - a[0].length).slice(0, 5).map(([prefix]) => prefix);
|
|
1262
|
+
}
|
|
1263
|
+
function shortenPaths(messages) {
|
|
1264
|
+
const allPaths = extractPaths(messages);
|
|
1265
|
+
if (allPaths.length < 5) {
|
|
1266
|
+
return {
|
|
1267
|
+
messages,
|
|
1268
|
+
pathMap: {},
|
|
1269
|
+
charsSaved: 0
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
const prefixes = findFrequentPrefixes(allPaths);
|
|
1273
|
+
if (prefixes.length === 0) {
|
|
1274
|
+
return {
|
|
1275
|
+
messages,
|
|
1276
|
+
pathMap: {},
|
|
1277
|
+
charsSaved: 0
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const pathMap = {};
|
|
1281
|
+
prefixes.forEach((prefix, i) => {
|
|
1282
|
+
pathMap[`$P${i + 1}`] = prefix;
|
|
1283
|
+
});
|
|
1284
|
+
let charsSaved = 0;
|
|
1285
|
+
const result = messages.map((message) => {
|
|
1286
|
+
if (!message.content || typeof message.content !== "string") return message;
|
|
1287
|
+
let content = message.content;
|
|
1288
|
+
const originalLength = content.length;
|
|
1289
|
+
for (const [code, prefix] of Object.entries(pathMap)) {
|
|
1290
|
+
content = content.split(prefix).join(code + "/");
|
|
1291
|
+
}
|
|
1292
|
+
charsSaved += originalLength - content.length;
|
|
1293
|
+
return {
|
|
1294
|
+
...message,
|
|
1295
|
+
content
|
|
1296
|
+
};
|
|
1297
|
+
});
|
|
1298
|
+
return {
|
|
1299
|
+
messages: result,
|
|
1300
|
+
pathMap,
|
|
1301
|
+
charsSaved
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/compression/layers/json-compact.ts
|
|
1306
|
+
function compactJson(jsonString) {
|
|
1307
|
+
try {
|
|
1308
|
+
const parsed = JSON.parse(jsonString);
|
|
1309
|
+
return JSON.stringify(parsed);
|
|
1310
|
+
} catch {
|
|
1311
|
+
return jsonString;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function looksLikeJson(str) {
|
|
1315
|
+
const trimmed = str.trim();
|
|
1316
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
1317
|
+
}
|
|
1318
|
+
function compactToolCalls(toolCalls) {
|
|
1319
|
+
return toolCalls.map((tc) => ({
|
|
1320
|
+
...tc,
|
|
1321
|
+
function: {
|
|
1322
|
+
...tc.function,
|
|
1323
|
+
arguments: compactJson(tc.function.arguments)
|
|
1324
|
+
}
|
|
1325
|
+
}));
|
|
1326
|
+
}
|
|
1327
|
+
function compactMessagesJson(messages) {
|
|
1328
|
+
let charsSaved = 0;
|
|
1329
|
+
const result = messages.map((message) => {
|
|
1330
|
+
const newMessage = { ...message };
|
|
1331
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
1332
|
+
const originalLength = JSON.stringify(message.tool_calls).length;
|
|
1333
|
+
newMessage.tool_calls = compactToolCalls(message.tool_calls);
|
|
1334
|
+
const newLength = JSON.stringify(newMessage.tool_calls).length;
|
|
1335
|
+
charsSaved += originalLength - newLength;
|
|
1336
|
+
}
|
|
1337
|
+
if (message.role === "tool" && message.content && typeof message.content === "string" && looksLikeJson(message.content)) {
|
|
1338
|
+
const originalLength = message.content.length;
|
|
1339
|
+
const compacted = compactJson(message.content);
|
|
1340
|
+
charsSaved += originalLength - compacted.length;
|
|
1341
|
+
newMessage.content = compacted;
|
|
1342
|
+
}
|
|
1343
|
+
return newMessage;
|
|
1344
|
+
});
|
|
1345
|
+
return {
|
|
1346
|
+
messages: result,
|
|
1347
|
+
charsSaved
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/compression/layers/observation.ts
|
|
1352
|
+
var TOOL_RESULT_THRESHOLD = 500;
|
|
1353
|
+
var COMPRESSED_RESULT_MAX = 300;
|
|
1354
|
+
function compressToolResult(content) {
|
|
1355
|
+
if (!content || content.length <= TOOL_RESULT_THRESHOLD) {
|
|
1356
|
+
return content;
|
|
1357
|
+
}
|
|
1358
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1359
|
+
const errorLines = lines.filter(
|
|
1360
|
+
(l) => /error|exception|failed|denied|refused|timeout|invalid/i.test(l) && l.length < 200
|
|
1361
|
+
);
|
|
1362
|
+
const statusLines = lines.filter(
|
|
1363
|
+
(l) => /success|complete|created|updated|found|result|status|total|count/i.test(l) && l.length < 150
|
|
1364
|
+
);
|
|
1365
|
+
const jsonMatches = [];
|
|
1366
|
+
const jsonPattern = /"(id|name|status|error|message|count|total|url|path)":\s*"?([^",}\n]+)"?/gi;
|
|
1367
|
+
let match;
|
|
1368
|
+
while ((match = jsonPattern.exec(content)) !== null) {
|
|
1369
|
+
jsonMatches.push(`${match[1]}: ${match[2].slice(0, 50)}`);
|
|
1370
|
+
}
|
|
1371
|
+
const firstLine = lines[0]?.slice(0, 100);
|
|
1372
|
+
const lastLine = lines.length > 1 ? lines[lines.length - 1]?.slice(0, 100) : "";
|
|
1373
|
+
const parts = [];
|
|
1374
|
+
if (errorLines.length > 0) {
|
|
1375
|
+
parts.push("[ERR] " + errorLines.slice(0, 3).join(" | "));
|
|
1376
|
+
}
|
|
1377
|
+
if (statusLines.length > 0) {
|
|
1378
|
+
parts.push(statusLines.slice(0, 3).join(" | "));
|
|
1379
|
+
}
|
|
1380
|
+
if (jsonMatches.length > 0) {
|
|
1381
|
+
parts.push(jsonMatches.slice(0, 5).join(", "));
|
|
1382
|
+
}
|
|
1383
|
+
if (parts.length === 0) {
|
|
1384
|
+
parts.push(firstLine || "");
|
|
1385
|
+
if (lines.length > 2) {
|
|
1386
|
+
parts.push(`[...${lines.length - 2} lines...]`);
|
|
1387
|
+
}
|
|
1388
|
+
if (lastLine && lastLine !== firstLine) {
|
|
1389
|
+
parts.push(lastLine);
|
|
808
1390
|
}
|
|
809
1391
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
for (const [model, data] of sortedModels) {
|
|
814
|
-
const shortModel = model.length > 25 ? model.slice(0, 22) + "..." : model;
|
|
815
|
-
const line = `\u2551 ${shortModel.padEnd(25)} ${data.count.toString().padStart(5)} reqs $${data.cost.toFixed(4)}`;
|
|
816
|
-
lines.push(line.padEnd(61) + "\u2551");
|
|
1392
|
+
let result = parts.join("\n");
|
|
1393
|
+
if (result.length > COMPRESSED_RESULT_MAX) {
|
|
1394
|
+
result = result.slice(0, COMPRESSED_RESULT_MAX - 20) + "\n[...truncated]";
|
|
817
1395
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1396
|
+
return result;
|
|
1397
|
+
}
|
|
1398
|
+
function deduplicateLargeBlocks(messages) {
|
|
1399
|
+
const blockHashes = /* @__PURE__ */ new Map();
|
|
1400
|
+
let charsSaved = 0;
|
|
1401
|
+
const result = messages.map((msg, idx) => {
|
|
1402
|
+
if (!msg.content || typeof msg.content !== "string" || msg.content.length < 500) {
|
|
1403
|
+
return msg;
|
|
1404
|
+
}
|
|
1405
|
+
const blockKey = msg.content.slice(0, 200);
|
|
1406
|
+
if (blockHashes.has(blockKey)) {
|
|
1407
|
+
const firstIdx = blockHashes.get(blockKey);
|
|
1408
|
+
const original = msg.content;
|
|
1409
|
+
const compressed = `[See message #${firstIdx + 1} - same content]`;
|
|
1410
|
+
charsSaved += original.length - compressed.length;
|
|
1411
|
+
return { ...msg, content: compressed };
|
|
1412
|
+
}
|
|
1413
|
+
blockHashes.set(blockKey, idx);
|
|
1414
|
+
return msg;
|
|
1415
|
+
});
|
|
1416
|
+
return { messages: result, charsSaved };
|
|
1417
|
+
}
|
|
1418
|
+
function compressObservations(messages) {
|
|
1419
|
+
let charsSaved = 0;
|
|
1420
|
+
let observationsCompressed = 0;
|
|
1421
|
+
let result = messages.map((msg) => {
|
|
1422
|
+
if (msg.role !== "tool" || !msg.content || typeof msg.content !== "string") {
|
|
1423
|
+
return msg;
|
|
1424
|
+
}
|
|
1425
|
+
const original = msg.content;
|
|
1426
|
+
if (original.length <= TOOL_RESULT_THRESHOLD) {
|
|
1427
|
+
return msg;
|
|
1428
|
+
}
|
|
1429
|
+
const compressed = compressToolResult(original);
|
|
1430
|
+
const saved = original.length - compressed.length;
|
|
1431
|
+
if (saved > 50) {
|
|
1432
|
+
charsSaved += saved;
|
|
1433
|
+
observationsCompressed++;
|
|
1434
|
+
return { ...msg, content: compressed };
|
|
1435
|
+
}
|
|
1436
|
+
return msg;
|
|
1437
|
+
});
|
|
1438
|
+
const dedupResult = deduplicateLargeBlocks(result);
|
|
1439
|
+
result = dedupResult.messages;
|
|
1440
|
+
charsSaved += dedupResult.charsSaved;
|
|
1441
|
+
return {
|
|
1442
|
+
messages: result,
|
|
1443
|
+
charsSaved,
|
|
1444
|
+
observationsCompressed
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/compression/layers/dynamic-codebook.ts
|
|
1449
|
+
var MIN_PHRASE_LENGTH = 20;
|
|
1450
|
+
var MAX_PHRASE_LENGTH = 200;
|
|
1451
|
+
var MIN_FREQUENCY = 3;
|
|
1452
|
+
var MAX_ENTRIES = 100;
|
|
1453
|
+
var CODE_PREFIX = "$D";
|
|
1454
|
+
function findRepeatedPhrases(allContent) {
|
|
1455
|
+
const phrases = /* @__PURE__ */ new Map();
|
|
1456
|
+
const segments = allContent.split(/(?<=[.!?\n])\s+/);
|
|
1457
|
+
for (const segment of segments) {
|
|
1458
|
+
const trimmed = segment.trim();
|
|
1459
|
+
if (trimmed.length >= MIN_PHRASE_LENGTH && trimmed.length <= MAX_PHRASE_LENGTH) {
|
|
1460
|
+
phrases.set(trimmed, (phrases.get(trimmed) || 0) + 1);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const lines = allContent.split("\n");
|
|
1464
|
+
for (const line of lines) {
|
|
1465
|
+
const trimmed = line.trim();
|
|
1466
|
+
if (trimmed.length >= MIN_PHRASE_LENGTH && trimmed.length <= MAX_PHRASE_LENGTH) {
|
|
1467
|
+
phrases.set(trimmed, (phrases.get(trimmed) || 0) + 1);
|
|
826
1468
|
}
|
|
827
1469
|
}
|
|
828
|
-
|
|
829
|
-
return lines.join("\n");
|
|
1470
|
+
return phrases;
|
|
830
1471
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
|
|
1472
|
+
function buildDynamicCodebook(messages) {
|
|
1473
|
+
let allContent = "";
|
|
1474
|
+
for (const msg of messages) {
|
|
1475
|
+
if (msg.content && typeof msg.content === "string") {
|
|
1476
|
+
allContent += msg.content + "\n";
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const phrases = findRepeatedPhrases(allContent);
|
|
1480
|
+
const candidates = [];
|
|
1481
|
+
for (const [phrase, count] of phrases.entries()) {
|
|
1482
|
+
if (count >= MIN_FREQUENCY) {
|
|
1483
|
+
const codeLength = 4;
|
|
1484
|
+
const savings = (phrase.length - codeLength) * count;
|
|
1485
|
+
if (savings > 50) {
|
|
1486
|
+
candidates.push({ phrase, count, savings });
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
839
1489
|
}
|
|
1490
|
+
candidates.sort((a, b) => b.savings - a.savings);
|
|
1491
|
+
const topCandidates = candidates.slice(0, MAX_ENTRIES);
|
|
1492
|
+
const codebook = {};
|
|
1493
|
+
topCandidates.forEach((c, i) => {
|
|
1494
|
+
const code = `${CODE_PREFIX}${String(i + 1).padStart(2, "0")}`;
|
|
1495
|
+
codebook[code] = c.phrase;
|
|
1496
|
+
});
|
|
1497
|
+
return codebook;
|
|
840
1498
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
import { join as join3 } from "path";
|
|
845
|
-
import { homedir as homedir2 } from "os";
|
|
846
|
-
var LOG_DIR2 = join3(homedir2(), ".openclaw", "ckcloud", "logs");
|
|
847
|
-
var dirReady = false;
|
|
848
|
-
async function ensureDir() {
|
|
849
|
-
if (dirReady) return;
|
|
850
|
-
await mkdir(LOG_DIR2, { recursive: true });
|
|
851
|
-
dirReady = true;
|
|
1499
|
+
function escapeRegex2(str) {
|
|
1500
|
+
if (!str || typeof str !== "string") return "";
|
|
1501
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
852
1502
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1503
|
+
function applyDynamicCodebook(messages) {
|
|
1504
|
+
const codebook = buildDynamicCodebook(messages);
|
|
1505
|
+
if (Object.keys(codebook).length === 0) {
|
|
1506
|
+
return {
|
|
1507
|
+
messages,
|
|
1508
|
+
charsSaved: 0,
|
|
1509
|
+
dynamicCodes: {},
|
|
1510
|
+
substitutions: 0
|
|
1511
|
+
};
|
|
860
1512
|
}
|
|
1513
|
+
const phraseToCode = {};
|
|
1514
|
+
for (const [code, phrase] of Object.entries(codebook)) {
|
|
1515
|
+
phraseToCode[phrase] = code;
|
|
1516
|
+
}
|
|
1517
|
+
const sortedPhrases = Object.keys(phraseToCode).sort((a, b) => b.length - a.length);
|
|
1518
|
+
let charsSaved = 0;
|
|
1519
|
+
let substitutions = 0;
|
|
1520
|
+
const result = messages.map((msg) => {
|
|
1521
|
+
if (!msg.content || typeof msg.content !== "string") return msg;
|
|
1522
|
+
let content = msg.content;
|
|
1523
|
+
for (const phrase of sortedPhrases) {
|
|
1524
|
+
const code = phraseToCode[phrase];
|
|
1525
|
+
const regex = new RegExp(escapeRegex2(phrase), "g");
|
|
1526
|
+
const matches = content.match(regex);
|
|
1527
|
+
if (matches) {
|
|
1528
|
+
content = content.replace(regex, code);
|
|
1529
|
+
charsSaved += (phrase.length - code.length) * matches.length;
|
|
1530
|
+
substitutions += matches.length;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return { ...msg, content };
|
|
1534
|
+
});
|
|
1535
|
+
return {
|
|
1536
|
+
messages: result,
|
|
1537
|
+
charsSaved,
|
|
1538
|
+
dynamicCodes: codebook,
|
|
1539
|
+
substitutions
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function generateDynamicCodebookHeader(codebook) {
|
|
1543
|
+
if (Object.keys(codebook).length === 0) return "";
|
|
1544
|
+
const entries = Object.entries(codebook).slice(0, 20).map(([code, phrase]) => {
|
|
1545
|
+
const displayPhrase = phrase.length > 40 ? phrase.slice(0, 37) + "..." : phrase;
|
|
1546
|
+
return `${code}=${displayPhrase}`;
|
|
1547
|
+
}).join(", ");
|
|
1548
|
+
return `[DynDict: ${entries}]`;
|
|
861
1549
|
}
|
|
862
1550
|
|
|
863
|
-
// src/
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1551
|
+
// src/compression/index.ts
|
|
1552
|
+
function calculateTotalChars(messages) {
|
|
1553
|
+
return messages.reduce((total, msg) => {
|
|
1554
|
+
let chars = 0;
|
|
1555
|
+
if (typeof msg.content === "string") {
|
|
1556
|
+
chars = msg.content.length;
|
|
1557
|
+
} else if (Array.isArray(msg.content)) {
|
|
1558
|
+
chars = JSON.stringify(msg.content).length;
|
|
1559
|
+
}
|
|
1560
|
+
if (msg.tool_calls) {
|
|
1561
|
+
chars += JSON.stringify(msg.tool_calls).length;
|
|
1562
|
+
}
|
|
1563
|
+
return total + chars;
|
|
1564
|
+
}, 0);
|
|
1565
|
+
}
|
|
1566
|
+
function cloneMessages(messages) {
|
|
1567
|
+
return JSON.parse(JSON.stringify(messages));
|
|
1568
|
+
}
|
|
1569
|
+
function prependCodebookHeader(messages, usedCodes, pathMap) {
|
|
1570
|
+
const header = generateCodebookHeader(usedCodes, pathMap);
|
|
1571
|
+
if (!header) return messages;
|
|
1572
|
+
const userIndex = messages.findIndex((m) => m.role === "user");
|
|
1573
|
+
if (userIndex === -1) {
|
|
1574
|
+
return [{ role: "system", content: header }, ...messages];
|
|
1575
|
+
}
|
|
1576
|
+
return messages.map((msg, i) => {
|
|
1577
|
+
if (i === userIndex) {
|
|
1578
|
+
if (typeof msg.content === "string") {
|
|
1579
|
+
return {
|
|
1580
|
+
...msg,
|
|
1581
|
+
content: `${header}
|
|
1582
|
+
|
|
1583
|
+
${msg.content}`
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return msg;
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
async function compressContext(messages, config = {}) {
|
|
1591
|
+
const fullConfig = {
|
|
1592
|
+
...DEFAULT_COMPRESSION_CONFIG,
|
|
1593
|
+
...config,
|
|
1594
|
+
layers: {
|
|
1595
|
+
...DEFAULT_COMPRESSION_CONFIG.layers,
|
|
1596
|
+
...config.layers
|
|
1597
|
+
},
|
|
1598
|
+
dictionary: {
|
|
1599
|
+
...DEFAULT_COMPRESSION_CONFIG.dictionary,
|
|
1600
|
+
...config.dictionary
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
if (!fullConfig.enabled) {
|
|
1604
|
+
const originalChars2 = calculateTotalChars(messages);
|
|
1605
|
+
return {
|
|
1606
|
+
messages,
|
|
1607
|
+
originalMessages: messages,
|
|
1608
|
+
originalChars: originalChars2,
|
|
1609
|
+
compressedChars: originalChars2,
|
|
1610
|
+
compressionRatio: 1,
|
|
1611
|
+
stats: {
|
|
1612
|
+
duplicatesRemoved: 0,
|
|
1613
|
+
whitespaceSavedChars: 0,
|
|
1614
|
+
dictionarySubstitutions: 0,
|
|
1615
|
+
pathsShortened: 0,
|
|
1616
|
+
jsonCompactedChars: 0,
|
|
1617
|
+
observationsCompressed: 0,
|
|
1618
|
+
observationCharsSaved: 0,
|
|
1619
|
+
dynamicSubstitutions: 0,
|
|
1620
|
+
dynamicCharsSaved: 0
|
|
1621
|
+
},
|
|
1622
|
+
codebook: {},
|
|
1623
|
+
pathMap: {},
|
|
1624
|
+
dynamicCodes: {}
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
const originalMessages = fullConfig.preserveRaw ? cloneMessages(messages) : messages;
|
|
1628
|
+
const originalChars = calculateTotalChars(messages);
|
|
1629
|
+
const stats = {
|
|
1630
|
+
duplicatesRemoved: 0,
|
|
1631
|
+
whitespaceSavedChars: 0,
|
|
1632
|
+
dictionarySubstitutions: 0,
|
|
1633
|
+
pathsShortened: 0,
|
|
1634
|
+
jsonCompactedChars: 0,
|
|
1635
|
+
observationsCompressed: 0,
|
|
1636
|
+
observationCharsSaved: 0,
|
|
1637
|
+
dynamicSubstitutions: 0,
|
|
1638
|
+
dynamicCharsSaved: 0
|
|
1639
|
+
};
|
|
1640
|
+
let result = cloneMessages(messages);
|
|
1641
|
+
let usedCodes = /* @__PURE__ */ new Set();
|
|
1642
|
+
let pathMap = {};
|
|
1643
|
+
let dynamicCodes = {};
|
|
1644
|
+
if (fullConfig.layers.deduplication) {
|
|
1645
|
+
const dedupResult = deduplicateMessages(result);
|
|
1646
|
+
result = dedupResult.messages;
|
|
1647
|
+
stats.duplicatesRemoved = dedupResult.duplicatesRemoved;
|
|
1648
|
+
}
|
|
1649
|
+
if (fullConfig.layers.whitespace) {
|
|
1650
|
+
const wsResult = normalizeMessagesWhitespace(result);
|
|
1651
|
+
result = wsResult.messages;
|
|
1652
|
+
stats.whitespaceSavedChars = wsResult.charsSaved;
|
|
1653
|
+
}
|
|
1654
|
+
if (fullConfig.layers.dictionary) {
|
|
1655
|
+
const dictResult = encodeMessages(result);
|
|
1656
|
+
result = dictResult.messages;
|
|
1657
|
+
stats.dictionarySubstitutions = dictResult.substitutionCount;
|
|
1658
|
+
usedCodes = dictResult.usedCodes;
|
|
1659
|
+
}
|
|
1660
|
+
if (fullConfig.layers.paths) {
|
|
1661
|
+
const pathResult = shortenPaths(result);
|
|
1662
|
+
result = pathResult.messages;
|
|
1663
|
+
pathMap = pathResult.pathMap;
|
|
1664
|
+
stats.pathsShortened = Object.keys(pathMap).length;
|
|
1665
|
+
}
|
|
1666
|
+
if (fullConfig.layers.jsonCompact) {
|
|
1667
|
+
const jsonResult = compactMessagesJson(result);
|
|
1668
|
+
result = jsonResult.messages;
|
|
1669
|
+
stats.jsonCompactedChars = jsonResult.charsSaved;
|
|
1670
|
+
}
|
|
1671
|
+
if (fullConfig.layers.observation) {
|
|
1672
|
+
const obsResult = compressObservations(result);
|
|
1673
|
+
result = obsResult.messages;
|
|
1674
|
+
stats.observationsCompressed = obsResult.observationsCompressed;
|
|
1675
|
+
stats.observationCharsSaved = obsResult.charsSaved;
|
|
1676
|
+
}
|
|
1677
|
+
if (fullConfig.layers.dynamicCodebook) {
|
|
1678
|
+
const dynResult = applyDynamicCodebook(result);
|
|
1679
|
+
result = dynResult.messages;
|
|
1680
|
+
stats.dynamicSubstitutions = dynResult.substitutions;
|
|
1681
|
+
stats.dynamicCharsSaved = dynResult.charsSaved;
|
|
1682
|
+
dynamicCodes = dynResult.dynamicCodes;
|
|
1683
|
+
}
|
|
1684
|
+
if (fullConfig.dictionary.includeCodebookHeader && (usedCodes.size > 0 || Object.keys(pathMap).length > 0 || Object.keys(dynamicCodes).length > 0)) {
|
|
1685
|
+
result = prependCodebookHeader(result, usedCodes, pathMap);
|
|
1686
|
+
if (Object.keys(dynamicCodes).length > 0) {
|
|
1687
|
+
const dynHeader = generateDynamicCodebookHeader(dynamicCodes);
|
|
1688
|
+
if (dynHeader) {
|
|
1689
|
+
const systemIndex = result.findIndex((m) => m.role === "system");
|
|
1690
|
+
if (systemIndex >= 0 && typeof result[systemIndex].content === "string") {
|
|
1691
|
+
result[systemIndex] = {
|
|
1692
|
+
...result[systemIndex],
|
|
1693
|
+
content: `${dynHeader}
|
|
1694
|
+
${result[systemIndex].content}`
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const compressedChars = calculateTotalChars(result);
|
|
1701
|
+
const compressionRatio = compressedChars / originalChars;
|
|
1702
|
+
const usedCodebook = {};
|
|
1703
|
+
usedCodes.forEach((code) => {
|
|
1704
|
+
usedCodebook[code] = STATIC_CODEBOOK[code];
|
|
1705
|
+
});
|
|
1706
|
+
return {
|
|
1707
|
+
messages: result,
|
|
1708
|
+
originalMessages,
|
|
1709
|
+
originalChars,
|
|
1710
|
+
compressedChars,
|
|
1711
|
+
compressionRatio,
|
|
1712
|
+
stats,
|
|
1713
|
+
codebook: usedCodebook,
|
|
1714
|
+
pathMap,
|
|
1715
|
+
dynamicCodes
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function shouldCompress(messages) {
|
|
1719
|
+
const chars = calculateTotalChars(messages);
|
|
1720
|
+
return chars > 5e3;
|
|
867
1721
|
}
|
|
868
|
-
var logger = {
|
|
869
|
-
debug: (message) => pluginLogger.debug?.(message),
|
|
870
|
-
info: (message) => pluginLogger.info(message),
|
|
871
|
-
warn: (message) => pluginLogger.warn(message),
|
|
872
|
-
error: (message) => pluginLogger.error(message)
|
|
873
|
-
};
|
|
874
1722
|
|
|
875
1723
|
// src/session.ts
|
|
876
1724
|
import { createHash } from "crypto";
|
|
@@ -4029,8 +4877,6 @@ var IMAGE_DIR = join5(homedir4(), ".openclaw", "ckcloud", "images");
|
|
|
4029
4877
|
var AUTO_MODEL = "ckcloud/auto";
|
|
4030
4878
|
var BALANCE_CHECK_BUFFER = 1.5;
|
|
4031
4879
|
var ROUTING_PROFILES = /* @__PURE__ */ new Set([
|
|
4032
|
-
"ckcloud/free",
|
|
4033
|
-
"free",
|
|
4034
4880
|
"ckcloud/eco",
|
|
4035
4881
|
"eco",
|
|
4036
4882
|
"ckcloud/auto",
|
|
@@ -4038,7 +4884,6 @@ var ROUTING_PROFILES = /* @__PURE__ */ new Set([
|
|
|
4038
4884
|
"ckcloud/premium",
|
|
4039
4885
|
"premium"
|
|
4040
4886
|
]);
|
|
4041
|
-
var FREE_MODEL = "kimi-k2";
|
|
4042
4887
|
var MAX_MESSAGES = 200;
|
|
4043
4888
|
var CONTEXT_LIMIT_KB = 5120;
|
|
4044
4889
|
var HEARTBEAT_INTERVAL_MS = 2e3;
|
|
@@ -4605,7 +5450,7 @@ function transformPaymentError(errorBody) {
|
|
|
4605
5450
|
wallet,
|
|
4606
5451
|
current_balance_usd: currentUSD,
|
|
4607
5452
|
required_usd: requiredUSD,
|
|
4608
|
-
help: `Fund wallet ${shortWallet} with USDC on Base
|
|
5453
|
+
help: `Fund wallet ${shortWallet} with USDC on Base`
|
|
4609
5454
|
}
|
|
4610
5455
|
});
|
|
4611
5456
|
}
|
|
@@ -4627,7 +5472,7 @@ function transformPaymentError(errorBody) {
|
|
|
4627
5472
|
error: {
|
|
4628
5473
|
message: "Solana payment simulation failed. Retrying with a different model.",
|
|
4629
5474
|
type: "transaction_simulation_failed",
|
|
4630
|
-
help: "This is usually temporary. If it persists, check your Solana USDC balance
|
|
5475
|
+
help: "This is usually temporary. If it persists, check your Solana USDC balance."
|
|
4631
5476
|
}
|
|
4632
5477
|
});
|
|
4633
5478
|
}
|
|
@@ -4640,7 +5485,7 @@ function transformPaymentError(errorBody) {
|
|
|
4640
5485
|
error: {
|
|
4641
5486
|
message: gasError ? "Payment failed: network congestion or gas issue. Try again." : "Payment settlement failed. Try again in a moment.",
|
|
4642
5487
|
type: "settlement_failed",
|
|
4643
|
-
help: "This is usually temporary. If it persists, try
|
|
5488
|
+
help: "This is usually temporary. If it persists, try again later."
|
|
4644
5489
|
}
|
|
4645
5490
|
});
|
|
4646
5491
|
}
|
|
@@ -4654,6 +5499,7 @@ async function startProxy(options) {
|
|
|
4654
5499
|
setPluginLogger(options.logger ?? console);
|
|
4655
5500
|
await initBaseModels();
|
|
4656
5501
|
const paymentChain = options.paymentChain ?? await resolvePaymentChain();
|
|
5502
|
+
logger.info(`paymentChain: ${paymentChain}`);
|
|
4657
5503
|
const apiBase = options.apiBase ?? (paymentChain === "solana" && solanaPrivateKeyBytes ? CKCLOUD_SOLANA_API : CKCLOUD_API);
|
|
4658
5504
|
if (paymentChain === "solana" && !solanaPrivateKeyBytes) {
|
|
4659
5505
|
logger.warn(`[ckcloud] \u26A0 Payment chain is Solana but no mnemonic found \u2014 falling back to Base (EVM).`);
|
|
@@ -5152,7 +5998,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5152
5998
|
const estimatedTokens = Math.ceil(fullText.length / 4);
|
|
5153
5999
|
const normalizedModel2 = typeof parsed.model === "string" ? parsed.model.trim().toLowerCase() : "";
|
|
5154
6000
|
const profileName = normalizedModel2.replace("ckcloud/", "");
|
|
5155
|
-
const debugProfile = ["
|
|
6001
|
+
const debugProfile = ["eco", "auto", "premium"].includes(profileName) ? profileName : "auto";
|
|
5156
6002
|
const scoring = classifyByRules(
|
|
5157
6003
|
debugPrompt,
|
|
5158
6004
|
systemPrompt,
|
|
@@ -5463,121 +6309,44 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5463
6309
|
modelId = resolvedModel;
|
|
5464
6310
|
}
|
|
5465
6311
|
if (isRoutingProfile) {
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
latencyMs: 0
|
|
5481
|
-
});
|
|
5482
|
-
} else {
|
|
5483
|
-
effectiveSessionId = getSessionId(req.headers) ?? deriveSessionId(parsedMessages);
|
|
5484
|
-
const existingSession = effectiveSessionId ? sessionStore.getSession(effectiveSessionId) : void 0;
|
|
5485
|
-
const rawPrompt = lastUserMsg?.content;
|
|
5486
|
-
const prompt = typeof rawPrompt === "string" ? rawPrompt : Array.isArray(rawPrompt) ? rawPrompt.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
|
|
5487
|
-
const systemMsg = parsedMessages.find((m) => m.role === "system");
|
|
5488
|
-
const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
|
|
5489
|
-
const tools = parsed.tools;
|
|
5490
|
-
hasTools = Array.isArray(tools) && tools.length > 0;
|
|
5491
|
-
if (hasTools && tools) {
|
|
5492
|
-
logger.info(`[ckcloud] Tools detected (${tools.length}), forcing agentic tiers`);
|
|
5493
|
-
}
|
|
5494
|
-
hasVision = parsedMessages.some((m) => {
|
|
5495
|
-
if (Array.isArray(m.content)) {
|
|
5496
|
-
return m.content.some((p) => p.type === "image_url");
|
|
5497
|
-
}
|
|
5498
|
-
return false;
|
|
5499
|
-
});
|
|
5500
|
-
if (hasVision) {
|
|
5501
|
-
logger.info(`[ckcloud] Vision content detected, filtering to vision-capable models`);
|
|
6312
|
+
effectiveSessionId = getSessionId(req.headers) ?? deriveSessionId(parsedMessages);
|
|
6313
|
+
const existingSession = effectiveSessionId ? sessionStore.getSession(effectiveSessionId) : void 0;
|
|
6314
|
+
const rawPrompt = lastUserMsg?.content;
|
|
6315
|
+
const prompt = typeof rawPrompt === "string" ? rawPrompt : Array.isArray(rawPrompt) ? rawPrompt.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
|
|
6316
|
+
const systemMsg = parsedMessages.find((m) => m.role === "system");
|
|
6317
|
+
const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
|
|
6318
|
+
const tools = parsed.tools;
|
|
6319
|
+
hasTools = Array.isArray(tools) && tools.length > 0;
|
|
6320
|
+
if (hasTools && tools) {
|
|
6321
|
+
logger.info(`[ckcloud] Tools detected (${tools.length}), forcing agentic tiers`);
|
|
6322
|
+
}
|
|
6323
|
+
hasVision = parsedMessages.some((m) => {
|
|
6324
|
+
if (Array.isArray(m.content)) {
|
|
6325
|
+
return m.content.some((p) => p.type === "image_url");
|
|
5502
6326
|
}
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
effectiveSessionId,
|
|
5527
|
-
routingDecision.model,
|
|
5528
|
-
routingDecision.tier
|
|
5529
|
-
);
|
|
5530
|
-
}
|
|
5531
|
-
} else {
|
|
5532
|
-
logger.info(
|
|
5533
|
-
`[ckcloud] Session ${effectiveSessionId?.slice(0, 8)}... keeping pinned model: ${existingSession.model} (${existingSession.tier} >= ${routingDecision.tier})`
|
|
5534
|
-
);
|
|
5535
|
-
parsed.model = existingSession.model;
|
|
5536
|
-
modelId = existingSession.model;
|
|
5537
|
-
bodyModified = true;
|
|
5538
|
-
sessionStore.touchSession(effectiveSessionId);
|
|
5539
|
-
routingDecision = {
|
|
5540
|
-
...routingDecision,
|
|
5541
|
-
model: existingSession.model,
|
|
5542
|
-
tier: existingSession.tier
|
|
5543
|
-
};
|
|
5544
|
-
}
|
|
5545
|
-
const lastAssistantMsg = [...parsedMessages].reverse().find((m) => m.role === "assistant");
|
|
5546
|
-
const assistantToolCalls = lastAssistantMsg?.tool_calls;
|
|
5547
|
-
const toolCallNames = Array.isArray(assistantToolCalls) ? assistantToolCalls.map((tc) => tc.function?.name).filter((n) => Boolean(n)) : void 0;
|
|
5548
|
-
const contentHash = hashRequestContent(prompt, toolCallNames);
|
|
5549
|
-
const shouldEscalate = sessionStore.recordRequestHash(effectiveSessionId, contentHash);
|
|
5550
|
-
if (shouldEscalate) {
|
|
5551
|
-
const activeTierConfigs = (() => {
|
|
5552
|
-
if (routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers) {
|
|
5553
|
-
return routerOpts.config.agenticTiers;
|
|
5554
|
-
}
|
|
5555
|
-
if (routingProfile === "eco" && routerOpts.config.ecoTiers) {
|
|
5556
|
-
return routerOpts.config.ecoTiers;
|
|
5557
|
-
}
|
|
5558
|
-
if (routingProfile === "premium" && routerOpts.config.premiumTiers) {
|
|
5559
|
-
return routerOpts.config.premiumTiers;
|
|
5560
|
-
}
|
|
5561
|
-
return routerOpts.config.tiers;
|
|
5562
|
-
})();
|
|
5563
|
-
const escalation = sessionStore.escalateSession(
|
|
5564
|
-
effectiveSessionId,
|
|
5565
|
-
activeTierConfigs
|
|
5566
|
-
);
|
|
5567
|
-
if (escalation) {
|
|
5568
|
-
logger.info(
|
|
5569
|
-
`[ckcloud] \u26A1 3-strike escalation: ${existingSession.model} \u2192 ${escalation.model} (${existingSession.tier} \u2192 ${escalation.tier})`
|
|
5570
|
-
);
|
|
5571
|
-
parsed.model = escalation.model;
|
|
5572
|
-
modelId = escalation.model;
|
|
5573
|
-
routingDecision = {
|
|
5574
|
-
...routingDecision,
|
|
5575
|
-
model: escalation.model,
|
|
5576
|
-
tier: escalation.tier
|
|
5577
|
-
};
|
|
5578
|
-
}
|
|
5579
|
-
}
|
|
5580
|
-
} else {
|
|
6327
|
+
return false;
|
|
6328
|
+
});
|
|
6329
|
+
if (hasVision) {
|
|
6330
|
+
logger.info(`[ckcloud] Vision content detected, filtering to vision-capable models`);
|
|
6331
|
+
}
|
|
6332
|
+
routingDecision = route(prompt, systemPrompt, maxTokens, {
|
|
6333
|
+
...routerOpts,
|
|
6334
|
+
routingProfile: routingProfile ?? void 0,
|
|
6335
|
+
hasTools
|
|
6336
|
+
});
|
|
6337
|
+
if (existingSession) {
|
|
6338
|
+
const tierRank = {
|
|
6339
|
+
SIMPLE: 0,
|
|
6340
|
+
MEDIUM: 1,
|
|
6341
|
+
COMPLEX: 2,
|
|
6342
|
+
REASONING: 3
|
|
6343
|
+
};
|
|
6344
|
+
const existingRank = tierRank[existingSession.tier] ?? 0;
|
|
6345
|
+
const newRank = tierRank[routingDecision.tier] ?? 0;
|
|
6346
|
+
if (newRank > existingRank) {
|
|
6347
|
+
logger.info(
|
|
6348
|
+
`[ckcloud] Session ${effectiveSessionId?.slice(0, 8)}... upgrading: ${existingSession.tier} \u2192 ${routingDecision.tier} (${routingDecision.model})`
|
|
6349
|
+
);
|
|
5581
6350
|
parsed.model = routingDecision.model;
|
|
5582
6351
|
modelId = routingDecision.model;
|
|
5583
6352
|
bodyModified = true;
|
|
@@ -5587,13 +6356,72 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5587
6356
|
routingDecision.model,
|
|
5588
6357
|
routingDecision.tier
|
|
5589
6358
|
);
|
|
6359
|
+
}
|
|
6360
|
+
} else {
|
|
6361
|
+
logger.info(
|
|
6362
|
+
`[ckcloud] Session ${effectiveSessionId?.slice(0, 8)}... keeping pinned model: ${existingSession.model} (${existingSession.tier} >= ${routingDecision.tier})`
|
|
6363
|
+
);
|
|
6364
|
+
parsed.model = existingSession.model;
|
|
6365
|
+
modelId = existingSession.model;
|
|
6366
|
+
bodyModified = true;
|
|
6367
|
+
sessionStore.touchSession(effectiveSessionId);
|
|
6368
|
+
routingDecision = {
|
|
6369
|
+
...routingDecision,
|
|
6370
|
+
model: existingSession.model,
|
|
6371
|
+
tier: existingSession.tier
|
|
6372
|
+
};
|
|
6373
|
+
}
|
|
6374
|
+
const lastAssistantMsg = [...parsedMessages].reverse().find((m) => m.role === "assistant");
|
|
6375
|
+
const assistantToolCalls = lastAssistantMsg?.tool_calls;
|
|
6376
|
+
const toolCallNames = Array.isArray(assistantToolCalls) ? assistantToolCalls.map((tc) => tc.function?.name).filter((n) => Boolean(n)) : void 0;
|
|
6377
|
+
const contentHash = hashRequestContent(prompt, toolCallNames);
|
|
6378
|
+
const shouldEscalate = sessionStore.recordRequestHash(effectiveSessionId, contentHash);
|
|
6379
|
+
if (shouldEscalate) {
|
|
6380
|
+
const activeTierConfigs = (() => {
|
|
6381
|
+
if (routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers) {
|
|
6382
|
+
return routerOpts.config.agenticTiers;
|
|
6383
|
+
}
|
|
6384
|
+
if (routingProfile === "eco" && routerOpts.config.ecoTiers) {
|
|
6385
|
+
return routerOpts.config.ecoTiers;
|
|
6386
|
+
}
|
|
6387
|
+
if (routingProfile === "premium" && routerOpts.config.premiumTiers) {
|
|
6388
|
+
return routerOpts.config.premiumTiers;
|
|
6389
|
+
}
|
|
6390
|
+
return routerOpts.config.tiers;
|
|
6391
|
+
})();
|
|
6392
|
+
const escalation = sessionStore.escalateSession(
|
|
6393
|
+
effectiveSessionId,
|
|
6394
|
+
activeTierConfigs
|
|
6395
|
+
);
|
|
6396
|
+
if (escalation) {
|
|
5590
6397
|
logger.info(
|
|
5591
|
-
`[ckcloud]
|
|
6398
|
+
`[ckcloud] \u26A1 3-strike escalation: ${existingSession.model} \u2192 ${escalation.model} (${existingSession.tier} \u2192 ${escalation.tier})`
|
|
5592
6399
|
);
|
|
6400
|
+
parsed.model = escalation.model;
|
|
6401
|
+
modelId = escalation.model;
|
|
6402
|
+
routingDecision = {
|
|
6403
|
+
...routingDecision,
|
|
6404
|
+
model: escalation.model,
|
|
6405
|
+
tier: escalation.tier
|
|
6406
|
+
};
|
|
5593
6407
|
}
|
|
5594
6408
|
}
|
|
5595
|
-
|
|
6409
|
+
} else {
|
|
6410
|
+
parsed.model = routingDecision.model;
|
|
6411
|
+
modelId = routingDecision.model;
|
|
6412
|
+
bodyModified = true;
|
|
6413
|
+
if (effectiveSessionId) {
|
|
6414
|
+
sessionStore.setSession(
|
|
6415
|
+
effectiveSessionId,
|
|
6416
|
+
routingDecision.model,
|
|
6417
|
+
routingDecision.tier
|
|
6418
|
+
);
|
|
6419
|
+
logger.info(
|
|
6420
|
+
`[ckcloud] Session ${effectiveSessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
|
|
6421
|
+
);
|
|
6422
|
+
}
|
|
5596
6423
|
}
|
|
6424
|
+
options.onRouted?.(routingDecision);
|
|
5597
6425
|
}
|
|
5598
6426
|
if (bodyModified) {
|
|
5599
6427
|
body = Buffer.from(JSON.stringify(parsed));
|
|
@@ -5685,32 +6513,55 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5685
6513
|
}
|
|
5686
6514
|
deduplicator.markInflight(dedupKey);
|
|
5687
6515
|
let estimatedCostMicros;
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
6516
|
+
const sendBalanceError = (status, payload) => {
|
|
6517
|
+
if (isStreaming) {
|
|
6518
|
+
res.writeHead(status, {
|
|
6519
|
+
"Content-Type": "text/event-stream",
|
|
6520
|
+
"Cache-Control": "no-cache",
|
|
6521
|
+
Connection: "keep-alive"
|
|
6522
|
+
});
|
|
6523
|
+
res.write(`data: ${JSON.stringify(payload)}
|
|
6524
|
+
|
|
6525
|
+
`);
|
|
6526
|
+
res.write("data: [DONE]\n\n");
|
|
6527
|
+
res.end();
|
|
6528
|
+
return;
|
|
6529
|
+
}
|
|
6530
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6531
|
+
res.end(JSON.stringify(payload));
|
|
6532
|
+
};
|
|
6533
|
+
if (modelId && !options.skipBalanceCheck) {
|
|
5691
6534
|
const estimated = estimateAmount(modelId, body.length, maxTokens);
|
|
5692
6535
|
if (estimated) {
|
|
5693
6536
|
estimatedCostMicros = BigInt(estimated);
|
|
5694
6537
|
const bufferedCostMicros = estimatedCostMicros * BigInt(Math.ceil(BALANCE_CHECK_BUFFER * 100)) / 100n;
|
|
5695
6538
|
const sufficiency = await balanceMonitor.checkSufficient(bufferedCostMicros);
|
|
5696
6539
|
if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
|
|
5697
|
-
const
|
|
6540
|
+
const balanceMicros = sufficiency.info.balance;
|
|
6541
|
+
const currentUSD = (Number(balanceMicros) / 1e6).toFixed(6);
|
|
6542
|
+
const requiredUSD = (Number(bufferedCostMicros) / 1e6).toFixed(6);
|
|
6543
|
+
const wallet = sufficiency.info.walletAddress;
|
|
6544
|
+
const shortWallet = wallet.length > 12 ? `${wallet.slice(0, 6)}...${wallet.slice(-4)}` : wallet;
|
|
5698
6545
|
logger.info(
|
|
5699
|
-
`[ckcloud] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (${sufficiency.info.balanceUSD}),
|
|
6546
|
+
`[ckcloud] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (${sufficiency.info.balanceUSD}), request blocked (model: ${modelId})`
|
|
5700
6547
|
);
|
|
5701
|
-
|
|
5702
|
-
const parsed = JSON.parse(body.toString());
|
|
5703
|
-
parsed.model = FREE_MODEL;
|
|
5704
|
-
body = Buffer.from(JSON.stringify(parsed));
|
|
5705
|
-
balanceFallbackNotice = sufficiency.info.isEmpty ? `> **\u26A0\uFE0F Wallet empty** \u2014 using free model. Fund your wallet to use ${originalModel}.
|
|
5706
|
-
|
|
5707
|
-
` : `> **\u26A0\uFE0F Insufficient balance** (${sufficiency.info.balanceUSD}) \u2014 using free model instead of ${originalModel}.
|
|
5708
|
-
|
|
5709
|
-
`;
|
|
5710
|
-
options.onLowBalance?.({
|
|
6548
|
+
options.onInsufficientFunds?.({
|
|
5711
6549
|
balanceUSD: sufficiency.info.balanceUSD,
|
|
6550
|
+
requiredUSD: `$${requiredUSD}`,
|
|
5712
6551
|
walletAddress: sufficiency.info.walletAddress
|
|
5713
6552
|
});
|
|
6553
|
+
const payload = {
|
|
6554
|
+
error: {
|
|
6555
|
+
message: sufficiency.info.isEmpty ? "No USDC balance. Fund your wallet to continue." : `Insufficient USDC balance. Current: $${currentUSD}, Required: ~$${requiredUSD}`,
|
|
6556
|
+
type: sufficiency.info.isEmpty ? "empty_wallet" : "insufficient_funds",
|
|
6557
|
+
wallet,
|
|
6558
|
+
current_balance_usd: currentUSD,
|
|
6559
|
+
required_usd: requiredUSD,
|
|
6560
|
+
help: `Fund wallet ${shortWallet} with USDC`
|
|
6561
|
+
}
|
|
6562
|
+
};
|
|
6563
|
+
sendBalanceError(402, payload);
|
|
6564
|
+
return;
|
|
5714
6565
|
} else if (sufficiency.info.isLow) {
|
|
5715
6566
|
options.onLowBalance?.({
|
|
5716
6567
|
balanceUSD: sufficiency.info.balanceUSD,
|
|
@@ -5829,9 +6680,6 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5829
6680
|
} else {
|
|
5830
6681
|
modelsToTry = modelId ? [modelId] : [];
|
|
5831
6682
|
}
|
|
5832
|
-
if (!modelsToTry.includes(FREE_MODEL)) {
|
|
5833
|
-
modelsToTry.push(FREE_MODEL);
|
|
5834
|
-
}
|
|
5835
6683
|
let upstream;
|
|
5836
6684
|
let lastError;
|
|
5837
6685
|
let actualModelUsed = modelId;
|
|
@@ -5882,13 +6730,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5882
6730
|
const isPaymentErr = /payment.*verification.*failed|payment.*settlement.*failed|insufficient.*funds|transaction_simulation_failed/i.test(
|
|
5883
6731
|
result.errorBody || ""
|
|
5884
6732
|
);
|
|
5885
|
-
if (isPaymentErr
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
logger.info(`[ckcloud] Payment error \u2014 skipping to free model: ${FREE_MODEL}`);
|
|
5889
|
-
i = freeIdx - 1;
|
|
5890
|
-
continue;
|
|
5891
|
-
}
|
|
6733
|
+
if (isPaymentErr) {
|
|
6734
|
+
logger.info(`[ckcloud] Payment error \u2014 stopping retries`);
|
|
6735
|
+
break;
|
|
5892
6736
|
}
|
|
5893
6737
|
logger.info(
|
|
5894
6738
|
`[ckcloud] Provider error from ${tryModel}, trying fallback: ${result.errorBody?.slice(0, 100)}`
|
|
@@ -6018,25 +6862,6 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6018
6862
|
`;
|
|
6019
6863
|
safeWrite(res, roleData);
|
|
6020
6864
|
responseChunks.push(Buffer.from(roleData));
|
|
6021
|
-
if (balanceFallbackNotice) {
|
|
6022
|
-
const noticeChunk = {
|
|
6023
|
-
...baseChunk,
|
|
6024
|
-
choices: [
|
|
6025
|
-
{
|
|
6026
|
-
index,
|
|
6027
|
-
delta: { content: balanceFallbackNotice },
|
|
6028
|
-
logprobs: null,
|
|
6029
|
-
finish_reason: null
|
|
6030
|
-
}
|
|
6031
|
-
]
|
|
6032
|
-
};
|
|
6033
|
-
const noticeData = `data: ${JSON.stringify(noticeChunk)}
|
|
6034
|
-
|
|
6035
|
-
`;
|
|
6036
|
-
safeWrite(res, noticeData);
|
|
6037
|
-
responseChunks.push(Buffer.from(noticeData));
|
|
6038
|
-
balanceFallbackNotice = void 0;
|
|
6039
|
-
}
|
|
6040
6865
|
if (content) {
|
|
6041
6866
|
const contentChunk = {
|
|
6042
6867
|
...baseChunk,
|
|
@@ -6129,17 +6954,6 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6129
6954
|
}
|
|
6130
6955
|
}
|
|
6131
6956
|
let responseBody = Buffer.concat(bodyParts);
|
|
6132
|
-
if (balanceFallbackNotice && responseBody.length > 0) {
|
|
6133
|
-
try {
|
|
6134
|
-
const parsed = JSON.parse(responseBody.toString());
|
|
6135
|
-
if (parsed.choices?.[0]?.message?.content !== void 0) {
|
|
6136
|
-
parsed.choices[0].message.content = balanceFallbackNotice + parsed.choices[0].message.content;
|
|
6137
|
-
responseBody = Buffer.from(JSON.stringify(parsed));
|
|
6138
|
-
}
|
|
6139
|
-
} catch {
|
|
6140
|
-
}
|
|
6141
|
-
balanceFallbackNotice = void 0;
|
|
6142
|
-
}
|
|
6143
6957
|
responseHeaders["content-length"] = String(responseBody.length);
|
|
6144
6958
|
res.writeHead(upstream.status, responseHeaders);
|
|
6145
6959
|
safeWrite(res, responseBody);
|
|
@@ -6277,6 +7091,13 @@ var plugin = {
|
|
|
6277
7091
|
`Failed to refresh models list: ${err instanceof Error ? err.message : String(err)}`
|
|
6278
7092
|
);
|
|
6279
7093
|
});
|
|
7094
|
+
createWalletInfoCommand().then((walletCommand) => {
|
|
7095
|
+
api.registerCommand(walletCommand);
|
|
7096
|
+
}).catch((err) => {
|
|
7097
|
+
api.logger.warn(
|
|
7098
|
+
`Failed to register /wallet command: ${err instanceof Error ? err.message : String(err)}`
|
|
7099
|
+
);
|
|
7100
|
+
});
|
|
6280
7101
|
createStatsCommand().then((statsCommand) => {
|
|
6281
7102
|
api.registerCommand(statsCommand);
|
|
6282
7103
|
}).catch((err) => {
|
|
@@ -6327,6 +7148,8 @@ var plugin = {
|
|
|
6327
7148
|
const healthy = await waitForProxyHealth(port, 5e3);
|
|
6328
7149
|
if (!healthy) {
|
|
6329
7150
|
api.logger.warn(`Proxy health check timed out, commands may not work immediately`);
|
|
7151
|
+
} else {
|
|
7152
|
+
api.logger.info(`Proxy health check ok`);
|
|
6330
7153
|
}
|
|
6331
7154
|
}).catch((err) => {
|
|
6332
7155
|
api.logger.error(
|
|
@@ -6462,12 +7285,12 @@ function injectModelsConfig(logger2) {
|
|
|
6462
7285
|
}
|
|
6463
7286
|
const model = defaults.model;
|
|
6464
7287
|
if (!model.primary) {
|
|
6465
|
-
model.primary = "ckcloud/
|
|
7288
|
+
model.primary = "ckcloud/auto";
|
|
6466
7289
|
logger2.info("Set default model");
|
|
6467
7290
|
needsWrite = true;
|
|
6468
7291
|
}
|
|
6469
7292
|
const TOP_MODELS = [
|
|
6470
|
-
"
|
|
7293
|
+
"auto",
|
|
6471
7294
|
"eco",
|
|
6472
7295
|
"premium"
|
|
6473
7296
|
];
|
|
@@ -6605,6 +7428,67 @@ async function createStatsCommand() {
|
|
|
6605
7428
|
}
|
|
6606
7429
|
};
|
|
6607
7430
|
}
|
|
7431
|
+
async function createWalletInfoCommand() {
|
|
7432
|
+
return {
|
|
7433
|
+
name: "wallet",
|
|
7434
|
+
description: "Show ckcloud wallet info (address, chain, balance)",
|
|
7435
|
+
acceptsArgs: false,
|
|
7436
|
+
requireAuth: false,
|
|
7437
|
+
handler: async (_ctx) => {
|
|
7438
|
+
try {
|
|
7439
|
+
const wallet = await resolveOrGenerateWalletKey();
|
|
7440
|
+
const chain = await resolvePaymentChain();
|
|
7441
|
+
let solanaAddress;
|
|
7442
|
+
if (wallet.solanaPrivateKeyBytes) {
|
|
7443
|
+
solanaAddress = await getSolanaAddress(wallet.solanaPrivateKeyBytes).catch(() => void 0);
|
|
7444
|
+
}
|
|
7445
|
+
const evmAddress = wallet.address;
|
|
7446
|
+
const solAddress2 = solanaAddress || "(not set up)";
|
|
7447
|
+
let balanceInfo;
|
|
7448
|
+
if (activeProxyHandle) {
|
|
7449
|
+
balanceInfo = await activeProxyHandle.balanceMonitor.checkBalance().catch(() => void 0);
|
|
7450
|
+
}
|
|
7451
|
+
if (!balanceInfo) {
|
|
7452
|
+
if (chain === "solana") {
|
|
7453
|
+
if (solanaAddress) {
|
|
7454
|
+
const monitor = new SolanaBalanceMonitor(solanaAddress);
|
|
7455
|
+
balanceInfo = await monitor.checkBalance().catch(() => void 0);
|
|
7456
|
+
}
|
|
7457
|
+
} else {
|
|
7458
|
+
const monitor = new BalanceMonitor(wallet.address);
|
|
7459
|
+
balanceInfo = await monitor.checkBalance().catch(() => void 0);
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
let balanceLine = "Balance: (checking...)";
|
|
7463
|
+
if (chain === "solana" && !solanaAddress) {
|
|
7464
|
+
balanceLine = "Balance: (unavailable \u2014 Solana wallet not set up)";
|
|
7465
|
+
} else if (balanceInfo) {
|
|
7466
|
+
if (balanceInfo.isEmpty) {
|
|
7467
|
+
balanceLine = "Balance: $0.00";
|
|
7468
|
+
} else if (balanceInfo.isLow) {
|
|
7469
|
+
balanceLine = `Balance: ${balanceInfo.balanceUSD} (low)`;
|
|
7470
|
+
} else {
|
|
7471
|
+
balanceLine = `Balance: ${balanceInfo.balanceUSD}`;
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7474
|
+
const lines = [
|
|
7475
|
+
"Wallet info",
|
|
7476
|
+
`Chain: ${chain}`,
|
|
7477
|
+
`EVM Address: ${evmAddress}`,
|
|
7478
|
+
`Solana Address: ${solAddress2}`,
|
|
7479
|
+
`Source: ${wallet.source}`,
|
|
7480
|
+
balanceLine
|
|
7481
|
+
];
|
|
7482
|
+
return { text: lines.join("\n") };
|
|
7483
|
+
} catch (err) {
|
|
7484
|
+
return {
|
|
7485
|
+
text: `Failed to load wallet info: ${err instanceof Error ? err.message : String(err)}`,
|
|
7486
|
+
isError: true
|
|
7487
|
+
};
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
};
|
|
7491
|
+
}
|
|
6608
7492
|
async function startProxyInBackground(api) {
|
|
6609
7493
|
const wallet = await resolveOrGenerateWalletKey();
|
|
6610
7494
|
if (wallet.source === "generated") {
|
|
@@ -6655,7 +7539,7 @@ async function startProxyInBackground(api) {
|
|
|
6655
7539
|
proxy.balanceMonitor.checkBalance().then((balance) => {
|
|
6656
7540
|
if (balance.isEmpty) {
|
|
6657
7541
|
api.logger.info(`Wallet: ${displayAddress} | Balance: $0.00`);
|
|
6658
|
-
api.logger.info(`Using
|
|
7542
|
+
api.logger.info(`Using AUTO model. Fund wallet for premium models.`);
|
|
6659
7543
|
} else if (balance.isLow) {
|
|
6660
7544
|
api.logger.info(`Wallet: ${displayAddress} | Balance: ${balance.balanceUSD} (low)`);
|
|
6661
7545
|
} else {
|