@tokenbuddy/tokenbuddy 1.0.6 → 1.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/src/buyer-store.d.ts +28 -1
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +71 -16
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +17 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +201 -32
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +5 -0
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +279 -72
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
- package/dist/src/doctor-clawtip-wallet.js +54 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -0
- package/dist/src/doctor-diagnostics.d.ts +2 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +5 -0
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +48 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -0
- package/dist/src/init-clawtip-activation.js +395 -0
- package/dist/src/init-clawtip-activation.js.map +1 -0
- package/dist/src/init-payment-options.d.ts +23 -1
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +97 -22
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/terminal-image.d.ts +22 -0
- package/dist/src/terminal-image.d.ts.map +1 -0
- package/dist/src/terminal-image.js +135 -0
- package/dist/src/terminal-image.js.map +1 -0
- package/package.json +1 -1
- package/src/buyer-store.ts +140 -17
- package/src/cli.ts +251 -33
- package/src/daemon.ts +308 -53
- package/src/doctor-clawtip-wallet.ts +70 -0
- package/src/doctor-diagnostics.ts +11 -0
- package/src/init-clawtip-activation.ts +487 -0
- package/src/init-payment-options.ts +140 -22
- package/src/terminal-image.ts +187 -0
- package/tests/e2e.test.ts +79 -5
- package/tests/tokenbuddy.test.ts +745 -19
package/src/daemon.ts
CHANGED
|
@@ -52,6 +52,34 @@ interface UsageSummary {
|
|
|
52
52
|
billedMicros: number;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
interface SellerSettlementSummary {
|
|
56
|
+
requestId: string;
|
|
57
|
+
settledMicros: number;
|
|
58
|
+
settledUsdMicros?: number;
|
|
59
|
+
remainingCreditMicros: number;
|
|
60
|
+
reservedBalanceMicros?: number;
|
|
61
|
+
spentMicros?: number;
|
|
62
|
+
priceVersion?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SellerBalanceSnapshot {
|
|
66
|
+
creditMicros: number;
|
|
67
|
+
reservedMicros: number;
|
|
68
|
+
spentMicros: number;
|
|
69
|
+
availableMicros: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function numericHeaderField(value: unknown): number | undefined {
|
|
73
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
77
|
+
const parsed = Number(value);
|
|
78
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
interface ResponsesStreamState {
|
|
56
84
|
itemId: string;
|
|
57
85
|
text: string;
|
|
@@ -271,6 +299,83 @@ class ResponsesStreamNormalizer {
|
|
|
271
299
|
}
|
|
272
300
|
}
|
|
273
301
|
|
|
302
|
+
class SellerSettlementStreamExtractor {
|
|
303
|
+
private pending = "";
|
|
304
|
+
private settlement: SellerSettlementSummary | undefined;
|
|
305
|
+
|
|
306
|
+
public push(chunk: string): string {
|
|
307
|
+
this.pending += chunk;
|
|
308
|
+
const blocks = this.pending.split("\n\n");
|
|
309
|
+
this.pending = blocks.pop() || "";
|
|
310
|
+
return blocks
|
|
311
|
+
.map((block) => this.processBlock(block))
|
|
312
|
+
.filter((block) => block.length > 0)
|
|
313
|
+
.join("\n\n");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public finish(): { downstream: string; settlement: SellerSettlementSummary | undefined } {
|
|
317
|
+
const downstream = this.pending.trim() ? this.processBlock(this.pending) : "";
|
|
318
|
+
this.pending = "";
|
|
319
|
+
return { downstream, settlement: this.settlement };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public current(): SellerSettlementSummary | undefined {
|
|
323
|
+
return this.settlement;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private processBlock(block: string): string {
|
|
327
|
+
if (!block.trim()) {
|
|
328
|
+
return "";
|
|
329
|
+
}
|
|
330
|
+
const lines = block.split("\n");
|
|
331
|
+
const eventLine = lines.find((line) => line.startsWith("event:"));
|
|
332
|
+
const eventName = eventLine?.replace(/^event:\s?/, "").trim();
|
|
333
|
+
if (eventName !== "tokenbuddy.settlement") {
|
|
334
|
+
return block;
|
|
335
|
+
}
|
|
336
|
+
const dataLine = lines.find((line) => line.startsWith("data:"));
|
|
337
|
+
if (!dataLine) {
|
|
338
|
+
return "";
|
|
339
|
+
}
|
|
340
|
+
const parsed = parseSellerSettlementObject(dataLine.replace(/^data:\s?/, ""));
|
|
341
|
+
if (parsed) {
|
|
342
|
+
this.settlement = parsed;
|
|
343
|
+
}
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseSellerSettlementObject(raw: string): SellerSettlementSummary | undefined {
|
|
349
|
+
try {
|
|
350
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
351
|
+
const requestId = typeof parsed.requestId === "string"
|
|
352
|
+
? parsed.requestId
|
|
353
|
+
: typeof parsed.request_id === "string"
|
|
354
|
+
? parsed.request_id
|
|
355
|
+
: undefined;
|
|
356
|
+
const settledMicros = numericHeaderField(parsed.settledMicros ?? parsed.settled_micros);
|
|
357
|
+
const remainingCreditMicros = numericHeaderField(parsed.remainingCreditMicros ?? parsed.remaining_credit_micros);
|
|
358
|
+
if (!requestId || settledMicros === undefined || remainingCreditMicros === undefined) {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
requestId,
|
|
363
|
+
settledMicros,
|
|
364
|
+
settledUsdMicros: numericHeaderField(parsed.settledUsdMicros ?? parsed.settled_usd_micros),
|
|
365
|
+
remainingCreditMicros,
|
|
366
|
+
reservedBalanceMicros: numericHeaderField(parsed.reservedBalanceMicros ?? parsed.reserved_balance_micros),
|
|
367
|
+
spentMicros: numericHeaderField(parsed.spentMicros ?? parsed.spent_micros),
|
|
368
|
+
priceVersion: typeof parsed.priceVersion === "string"
|
|
369
|
+
? parsed.priceVersion
|
|
370
|
+
: typeof parsed.price_version === "string"
|
|
371
|
+
? parsed.price_version
|
|
372
|
+
: undefined
|
|
373
|
+
};
|
|
374
|
+
} catch {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
274
379
|
interface PurchaseCreateResponse {
|
|
275
380
|
purchaseId?: string;
|
|
276
381
|
purchase_id?: string;
|
|
@@ -588,6 +693,138 @@ export class TokenbuddyDaemon {
|
|
|
588
693
|
}
|
|
589
694
|
}
|
|
590
695
|
|
|
696
|
+
private parseSellerSettlementSummary(headers: Headers): SellerSettlementSummary | undefined {
|
|
697
|
+
const raw = headers.get("x-tokenbuddy-settlement");
|
|
698
|
+
if (!raw) {
|
|
699
|
+
return undefined;
|
|
700
|
+
}
|
|
701
|
+
return parseSellerSettlementObject(raw);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private recordReconciledInference(
|
|
705
|
+
route: SellerRoute,
|
|
706
|
+
endpoint: string,
|
|
707
|
+
requestId: string,
|
|
708
|
+
usage: UsageSummary,
|
|
709
|
+
settlement: SellerSettlementSummary | undefined,
|
|
710
|
+
prompt: string | undefined,
|
|
711
|
+
response?: string
|
|
712
|
+
): void {
|
|
713
|
+
if (settlement) {
|
|
714
|
+
this.tokenStore.reconcileTokenBalance({
|
|
715
|
+
sellerKey: route.seller.id,
|
|
716
|
+
balanceMicros: settlement.remainingCreditMicros,
|
|
717
|
+
reservedMicros: settlement.reservedBalanceMicros ?? 0,
|
|
718
|
+
spentMicros: settlement.spentMicros ?? 0,
|
|
719
|
+
balanceSource: "seller_settlement_summary"
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const settledMicros = settlement?.settledMicros;
|
|
724
|
+
this.tokenStore.recordInferenceLedger({
|
|
725
|
+
requestId: settlement?.requestId || requestId,
|
|
726
|
+
sellerKey: route.seller.id,
|
|
727
|
+
modelId: route.modelId,
|
|
728
|
+
endpoint,
|
|
729
|
+
status: settlement ? "settled" : "estimated",
|
|
730
|
+
promptTokens: usage.promptTokens,
|
|
731
|
+
completionTokens: usage.completionTokens,
|
|
732
|
+
billedMicros: settledMicros ?? usage.billedMicros,
|
|
733
|
+
estimatedMicros: usage.billedMicros,
|
|
734
|
+
settledMicros,
|
|
735
|
+
settledUsdMicros: settlement?.settledUsdMicros,
|
|
736
|
+
priceVersion: settlement?.priceVersion,
|
|
737
|
+
balanceSnapshotMicros: settlement?.remainingCreditMicros,
|
|
738
|
+
balanceSource: settlement ? "seller_authoritative" : "estimated",
|
|
739
|
+
prompt,
|
|
740
|
+
response
|
|
741
|
+
});
|
|
742
|
+
logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
|
|
743
|
+
requestId: settlement?.requestId || requestId,
|
|
744
|
+
sellerKey: route.seller.id,
|
|
745
|
+
model: route.modelId,
|
|
746
|
+
endpoint,
|
|
747
|
+
status: settlement ? "settled" : "estimated",
|
|
748
|
+
estimatedMicros: usage.billedMicros,
|
|
749
|
+
settledMicros,
|
|
750
|
+
balanceSource: settlement ? "seller_authoritative" : "estimated"
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private async refreshSellerBalance(route: SellerRoute, token: string, balanceSource: string): Promise<SellerBalanceSnapshot | undefined> {
|
|
755
|
+
const sellerKey = route.seller.id;
|
|
756
|
+
const sellerUrl = normalizeSellerUrl(route.seller);
|
|
757
|
+
const response = await fetch(`${sellerUrl}/v1/balance`, {
|
|
758
|
+
headers: { "Authorization": `Bearer ${token}` }
|
|
759
|
+
});
|
|
760
|
+
if (!response.ok) {
|
|
761
|
+
logger.warn("token.balance_refresh.failed", "seller balance refresh failed", {
|
|
762
|
+
sellerKey,
|
|
763
|
+
model: route.modelId,
|
|
764
|
+
status: response.status
|
|
765
|
+
});
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
const data = await response.json() as Record<string, unknown>;
|
|
769
|
+
const creditMicros = numericHeaderField(data.creditMicros ?? data.credit_micros) ?? 0;
|
|
770
|
+
const reservedMicros = numericHeaderField(data.reservedMicros ?? data.reserved_micros) ?? 0;
|
|
771
|
+
const spentMicros = numericHeaderField(data.spentMicros ?? data.spent_micros) ?? 0;
|
|
772
|
+
const snapshot = {
|
|
773
|
+
creditMicros,
|
|
774
|
+
reservedMicros,
|
|
775
|
+
spentMicros,
|
|
776
|
+
availableMicros: Math.max(0, creditMicros - reservedMicros)
|
|
777
|
+
};
|
|
778
|
+
this.tokenStore.reconcileTokenBalance({
|
|
779
|
+
sellerKey,
|
|
780
|
+
balanceMicros: snapshot.availableMicros,
|
|
781
|
+
reservedMicros,
|
|
782
|
+
spentMicros,
|
|
783
|
+
balanceSource
|
|
784
|
+
});
|
|
785
|
+
logger.info("token.balance_refresh.succeeded", "seller balance refreshed", {
|
|
786
|
+
sellerKey,
|
|
787
|
+
model: route.modelId,
|
|
788
|
+
availableMicros: snapshot.availableMicros,
|
|
789
|
+
reservedMicros,
|
|
790
|
+
spentMicros,
|
|
791
|
+
balanceSource
|
|
792
|
+
});
|
|
793
|
+
return snapshot;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private isInsufficientFundsResponse(status: number, bodyText: string): boolean {
|
|
797
|
+
if (status !== 402) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
const parsed = JSON.parse(bodyText) as { error?: { code?: string; message?: string } };
|
|
802
|
+
const code = parsed.error?.code || "";
|
|
803
|
+
const message = parsed.error?.message || "";
|
|
804
|
+
return code === "insufficient_funds" || /insufficient funds/i.test(message);
|
|
805
|
+
} catch {
|
|
806
|
+
return /insufficient funds/i.test(bodyText);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private async recoverFromInsufficientFunds(route: SellerRoute, token: string): Promise<string> {
|
|
811
|
+
const sellerKey = route.seller.id;
|
|
812
|
+
this.tokenStore.markTokenStale(sellerKey);
|
|
813
|
+
const snapshot = await this.refreshSellerBalance(route, token, "seller_402_refresh");
|
|
814
|
+
const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
|
|
815
|
+
if (!snapshot || snapshot.availableMicros <= rebuyMinBalanceMicros) {
|
|
816
|
+
logger.info("purchase.retry_after_402.started", "seller 402 triggered one-shot auto purchase retry", {
|
|
817
|
+
sellerKey,
|
|
818
|
+
model: route.modelId,
|
|
819
|
+
availableMicros: snapshot?.availableMicros ?? 0,
|
|
820
|
+
rebuyMinBalanceMicros
|
|
821
|
+
});
|
|
822
|
+
return await this.getOrPurchaseToken(route);
|
|
823
|
+
}
|
|
824
|
+
const cached = this.tokenStore.getToken(sellerKey);
|
|
825
|
+
return cached?.token || token;
|
|
826
|
+
}
|
|
827
|
+
|
|
591
828
|
private inferPromptForHash(body: unknown): string | undefined {
|
|
592
829
|
if (!body || typeof body !== "object") {
|
|
593
830
|
return undefined;
|
|
@@ -930,21 +1167,12 @@ export class TokenbuddyDaemon {
|
|
|
930
1167
|
endpoint,
|
|
931
1168
|
stream: Boolean((body as { stream?: unknown }).stream)
|
|
932
1169
|
});
|
|
933
|
-
const token = await this.getOrPurchaseToken(route);
|
|
934
1170
|
const sellerUrl = normalizeSellerUrl(route.seller);
|
|
935
1171
|
const upstreamBody = this.applyResolvedModelToBody(endpoint, {
|
|
936
1172
|
...(body as Record<string, unknown>),
|
|
937
1173
|
requestId
|
|
938
1174
|
}, modelId);
|
|
939
|
-
|
|
940
|
-
logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
|
|
941
|
-
requestId,
|
|
942
|
-
sellerKey,
|
|
943
|
-
model: modelId,
|
|
944
|
-
endpoint,
|
|
945
|
-
stream: Boolean((body as { stream?: unknown }).stream)
|
|
946
|
-
});
|
|
947
|
-
const upstreamResponse = await fetch(`${sellerUrl}${endpoint}`, {
|
|
1175
|
+
const sendSellerRequest = async (token: string) => fetch(`${sellerUrl}${endpoint}`, {
|
|
948
1176
|
method: "POST",
|
|
949
1177
|
headers: {
|
|
950
1178
|
"Content-Type": "application/json",
|
|
@@ -955,8 +1183,45 @@ export class TokenbuddyDaemon {
|
|
|
955
1183
|
body: JSON.stringify(upstreamBody)
|
|
956
1184
|
});
|
|
957
1185
|
|
|
1186
|
+
logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
|
|
1187
|
+
requestId,
|
|
1188
|
+
sellerKey,
|
|
1189
|
+
model: modelId,
|
|
1190
|
+
endpoint,
|
|
1191
|
+
stream: Boolean((body as { stream?: unknown }).stream)
|
|
1192
|
+
});
|
|
1193
|
+
let token = await this.getOrPurchaseToken(route);
|
|
1194
|
+
let upstreamResponse = await sendSellerRequest(token);
|
|
1195
|
+
|
|
958
1196
|
if (!upstreamResponse.ok) {
|
|
959
1197
|
const errorBody = await upstreamResponse.text();
|
|
1198
|
+
if (this.isInsufficientFundsResponse(upstreamResponse.status, errorBody)) {
|
|
1199
|
+
token = await this.recoverFromInsufficientFunds(route, token);
|
|
1200
|
+
upstreamResponse = await sendSellerRequest(token);
|
|
1201
|
+
if (upstreamResponse.ok) {
|
|
1202
|
+
logger.info("proxy.retry_after_402.succeeded", "seller request succeeded after one-shot auto purchase retry", {
|
|
1203
|
+
requestId,
|
|
1204
|
+
sellerKey,
|
|
1205
|
+
model: modelId,
|
|
1206
|
+
endpoint,
|
|
1207
|
+
durationMs: Date.now() - startedAt
|
|
1208
|
+
});
|
|
1209
|
+
} else {
|
|
1210
|
+
const retryErrorBody = await upstreamResponse.text();
|
|
1211
|
+
logger.warn("proxy.retry_after_402.failed", "seller request still failed after one-shot auto purchase retry", {
|
|
1212
|
+
requestId,
|
|
1213
|
+
sellerKey,
|
|
1214
|
+
model: modelId,
|
|
1215
|
+
endpoint,
|
|
1216
|
+
status: upstreamResponse.status,
|
|
1217
|
+
durationMs: Date.now() - startedAt
|
|
1218
|
+
});
|
|
1219
|
+
this.copyUpstreamHeaders(upstreamResponse, res);
|
|
1220
|
+
res.status(upstreamResponse.status);
|
|
1221
|
+
res.send(retryErrorBody);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
} else {
|
|
960
1225
|
logger.warn("proxy.upstream_fetch.failed", "proxy upstream fetch returned non-ok status", {
|
|
961
1226
|
requestId,
|
|
962
1227
|
sellerKey,
|
|
@@ -974,6 +1239,7 @@ export class TokenbuddyDaemon {
|
|
|
974
1239
|
res.status(upstreamResponse.status);
|
|
975
1240
|
res.send(errorBody);
|
|
976
1241
|
return;
|
|
1242
|
+
}
|
|
977
1243
|
}
|
|
978
1244
|
|
|
979
1245
|
this.copyUpstreamHeaders(upstreamResponse, res);
|
|
@@ -997,20 +1263,36 @@ export class TokenbuddyDaemon {
|
|
|
997
1263
|
let bytes = 0;
|
|
998
1264
|
const decoder = new TextDecoder();
|
|
999
1265
|
const responsesStreamNormalizer = new ResponsesStreamNormalizer();
|
|
1266
|
+
const settlementExtractor = new SellerSettlementStreamExtractor();
|
|
1000
1267
|
while (true) {
|
|
1001
1268
|
const { done, value } = await reader.read();
|
|
1002
1269
|
if (done) {
|
|
1003
1270
|
break;
|
|
1004
1271
|
}
|
|
1005
1272
|
bytes += value.byteLength;
|
|
1273
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1274
|
+
const sellerChunk = settlementExtractor.push(chunk);
|
|
1275
|
+
if (sellerChunk.length === 0) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1006
1278
|
if (endpoint === "/v1/responses") {
|
|
1007
|
-
const
|
|
1008
|
-
const normalized = responsesStreamNormalizer.push(chunk);
|
|
1279
|
+
const normalized = responsesStreamNormalizer.push(sellerChunk);
|
|
1009
1280
|
if (normalized.length > 0) {
|
|
1010
1281
|
res.write(`${normalized}\n\n`);
|
|
1011
1282
|
}
|
|
1012
1283
|
} else {
|
|
1013
|
-
res.write(
|
|
1284
|
+
res.write(sellerChunk);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const settlementTrailing = settlementExtractor.finish();
|
|
1288
|
+
if (settlementTrailing.downstream.length > 0) {
|
|
1289
|
+
if (endpoint === "/v1/responses") {
|
|
1290
|
+
const normalized = responsesStreamNormalizer.push(settlementTrailing.downstream);
|
|
1291
|
+
if (normalized.length > 0) {
|
|
1292
|
+
res.write(`${normalized}\n\n`);
|
|
1293
|
+
}
|
|
1294
|
+
} else {
|
|
1295
|
+
res.write(settlementTrailing.downstream);
|
|
1014
1296
|
}
|
|
1015
1297
|
}
|
|
1016
1298
|
if (endpoint === "/v1/responses") {
|
|
@@ -1020,56 +1302,29 @@ export class TokenbuddyDaemon {
|
|
|
1020
1302
|
}
|
|
1021
1303
|
}
|
|
1022
1304
|
res.end();
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
this.tokenStore.recordInferenceLedger({
|
|
1026
|
-
requestId,
|
|
1027
|
-
sellerKey,
|
|
1028
|
-
modelId,
|
|
1305
|
+
this.recordReconciledInference(
|
|
1306
|
+
route,
|
|
1029
1307
|
endpoint,
|
|
1030
|
-
status: "settled",
|
|
1031
|
-
promptTokens: 0,
|
|
1032
|
-
completionTokens: 0,
|
|
1033
|
-
billedMicros,
|
|
1034
|
-
prompt: this.inferPromptForHash(body)
|
|
1035
|
-
});
|
|
1036
|
-
logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
|
|
1037
1308
|
requestId,
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
billedMicros
|
|
1043
|
-
});
|
|
1309
|
+
{ promptTokens: 0, completionTokens: 0, billedMicros: Math.max(1, bytes) },
|
|
1310
|
+
this.parseSellerSettlementSummary(upstreamResponse.headers) ?? settlementTrailing.settlement ?? settlementExtractor.current(),
|
|
1311
|
+
this.inferPromptForHash(body)
|
|
1312
|
+
);
|
|
1044
1313
|
return;
|
|
1045
1314
|
}
|
|
1046
1315
|
|
|
1047
1316
|
const responseBody = await upstreamResponse.text();
|
|
1048
1317
|
res.send(responseBody);
|
|
1049
1318
|
const usage = this.readUsage(responseBody);
|
|
1050
|
-
this.
|
|
1051
|
-
|
|
1052
|
-
requestId,
|
|
1053
|
-
sellerKey,
|
|
1054
|
-
modelId,
|
|
1319
|
+
this.recordReconciledInference(
|
|
1320
|
+
route,
|
|
1055
1321
|
endpoint,
|
|
1056
|
-
status: "settled",
|
|
1057
|
-
promptTokens: usage.promptTokens,
|
|
1058
|
-
completionTokens: usage.completionTokens,
|
|
1059
|
-
billedMicros: usage.billedMicros,
|
|
1060
|
-
prompt: this.inferPromptForHash(body),
|
|
1061
|
-
response: responseBody
|
|
1062
|
-
});
|
|
1063
|
-
logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
|
|
1064
1322
|
requestId,
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
completionTokens: usage.completionTokens,
|
|
1071
|
-
billedMicros: usage.billedMicros
|
|
1072
|
-
});
|
|
1323
|
+
usage,
|
|
1324
|
+
this.parseSellerSettlementSummary(upstreamResponse.headers),
|
|
1325
|
+
this.inferPromptForHash(body),
|
|
1326
|
+
responseBody
|
|
1327
|
+
);
|
|
1073
1328
|
return;
|
|
1074
1329
|
} catch (routeError: unknown) {
|
|
1075
1330
|
lastError = routeError;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BuyerStore } from "./buyer-store.js";
|
|
2
|
+
import {
|
|
3
|
+
inspectClawtipWalletReadiness,
|
|
4
|
+
type ClawtipWalletStatus,
|
|
5
|
+
} from "./init-payment-options.js";
|
|
6
|
+
|
|
7
|
+
export interface DoctorClawtipWalletSummary {
|
|
8
|
+
status: ClawtipWalletStatus;
|
|
9
|
+
ready: boolean;
|
|
10
|
+
paymentMetadataPresent: boolean;
|
|
11
|
+
walletConfigPresent: boolean;
|
|
12
|
+
configsDirExists: boolean;
|
|
13
|
+
expectedPath: string;
|
|
14
|
+
alternatePaths: string[];
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function clawtipWalletStatusIcon(status: ClawtipWalletStatus): string {
|
|
19
|
+
if (status === "ready") {
|
|
20
|
+
return "✅";
|
|
21
|
+
}
|
|
22
|
+
if (status === "metadata_missing_wallet") {
|
|
23
|
+
return "❌";
|
|
24
|
+
}
|
|
25
|
+
if (status === "wallet_missing_metadata") {
|
|
26
|
+
return "🟡";
|
|
27
|
+
}
|
|
28
|
+
return "🔘";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readDoctorClawtipWallet(): DoctorClawtipWalletSummary {
|
|
32
|
+
const store = new BuyerStore();
|
|
33
|
+
try {
|
|
34
|
+
const readiness = inspectClawtipWalletReadiness(store.getPayment("clawtip"));
|
|
35
|
+
return {
|
|
36
|
+
status: readiness.status,
|
|
37
|
+
ready: readiness.status === "ready",
|
|
38
|
+
paymentMetadataPresent: Boolean(readiness.savedBinding),
|
|
39
|
+
walletConfigPresent: readiness.walletConfig.exists,
|
|
40
|
+
configsDirExists: readiness.walletConfig.configsDirExists,
|
|
41
|
+
expectedPath: readiness.walletConfig.expectedPath,
|
|
42
|
+
alternatePaths: readiness.walletConfig.alternatePaths,
|
|
43
|
+
message: readiness.message,
|
|
44
|
+
};
|
|
45
|
+
} finally {
|
|
46
|
+
store.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function printDoctorClawtipWallet(
|
|
51
|
+
wallet: DoctorClawtipWalletSummary,
|
|
52
|
+
writeLine: (line: string) => void,
|
|
53
|
+
): void {
|
|
54
|
+
writeLine("\n--- ClawTip Wallet ---");
|
|
55
|
+
writeLine(`${clawtipWalletStatusIcon(wallet.status)} ClawTip Wallet [${wallet.status}]`);
|
|
56
|
+
writeLine(` Payment metadata: ${wallet.paymentMetadataPresent ? "present" : "missing"}`);
|
|
57
|
+
writeLine(` Wallet config: ${wallet.walletConfigPresent ? "found" : "missing"}`);
|
|
58
|
+
writeLine(` Expected: ${wallet.expectedPath}`);
|
|
59
|
+
if (wallet.alternatePaths.length > 0) {
|
|
60
|
+
writeLine(` Nearby files: ${wallet.alternatePaths.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
writeLine(` Notes: ${wallet.message}`);
|
|
63
|
+
if (wallet.status === "metadata_missing_wallet") {
|
|
64
|
+
writeLine(" Action: Run `tb init` and choose ClawTip to bind the local wallet again.");
|
|
65
|
+
} else if (wallet.status === "wallet_missing_metadata") {
|
|
66
|
+
writeLine(" Action: Run `tb init` and choose ClawTip so TokenBuddy can save payment metadata.");
|
|
67
|
+
} else if (!wallet.ready) {
|
|
68
|
+
writeLine(" Action: Run `tb init` and choose ClawTip to bind a wallet before using ClawTip-backed purchases.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
type ModelCatalogEntry,
|
|
14
14
|
type SellerCatalogEntry,
|
|
15
15
|
} from "./seller-catalog.js";
|
|
16
|
+
import {
|
|
17
|
+
printDoctorClawtipWallet,
|
|
18
|
+
readDoctorClawtipWallet,
|
|
19
|
+
type DoctorClawtipWalletSummary,
|
|
20
|
+
} from "./doctor-clawtip-wallet.js";
|
|
16
21
|
|
|
17
22
|
export interface DoctorProviderView extends ProviderCandidate {
|
|
18
23
|
runtimeConfig?: ProviderRuntimeConfig;
|
|
@@ -34,6 +39,7 @@ export interface DoctorSellerEntry {
|
|
|
34
39
|
|
|
35
40
|
export interface DoctorDiagnostics {
|
|
36
41
|
access: DoctorAccessSummary;
|
|
42
|
+
clawtipWallet: DoctorClawtipWalletSummary;
|
|
37
43
|
models: DoctorModelsSummary;
|
|
38
44
|
providers: DoctorProviderView[];
|
|
39
45
|
sellers: DoctorSellersSummary;
|
|
@@ -749,6 +755,7 @@ export async function collectDoctorDiagnostics(options: DoctorCollectOptions): P
|
|
|
749
755
|
modelsError: models.error,
|
|
750
756
|
},
|
|
751
757
|
),
|
|
758
|
+
clawtipWallet: readDoctorClawtipWallet(),
|
|
752
759
|
models,
|
|
753
760
|
providers: options.providers,
|
|
754
761
|
sellers,
|
|
@@ -784,6 +791,7 @@ export async function collectDoctorModelsSummary(options: Omit<DoctorCollectOpti
|
|
|
784
791
|
|
|
785
792
|
export async function renderDoctorDiagnosticsProgressively(options: DoctorRenderOptions): Promise<DoctorDiagnostics> {
|
|
786
793
|
const writeLine = options.writeLine || defaultWriter;
|
|
794
|
+
const clawtipWallet = readDoctorClawtipWallet();
|
|
787
795
|
const fetches = startDoctorFetches(
|
|
788
796
|
options.controlPort,
|
|
789
797
|
options.proxyPort,
|
|
@@ -792,6 +800,8 @@ export async function renderDoctorDiagnosticsProgressively(options: DoctorRender
|
|
|
792
800
|
options.sellerRegistryUrl,
|
|
793
801
|
);
|
|
794
802
|
|
|
803
|
+
printDoctorClawtipWallet(clawtipWallet, writeLine);
|
|
804
|
+
|
|
795
805
|
writeLine("\n--- Access Interfaces ---");
|
|
796
806
|
writeLine("Checking local control plane and proxy endpoints...");
|
|
797
807
|
const [healthResult, proxyModelsResult] = await Promise.all([
|
|
@@ -843,6 +853,7 @@ export async function renderDoctorDiagnosticsProgressively(options: DoctorRender
|
|
|
843
853
|
|
|
844
854
|
return {
|
|
845
855
|
access,
|
|
856
|
+
clawtipWallet,
|
|
846
857
|
models,
|
|
847
858
|
providers: options.providers,
|
|
848
859
|
sellers: finalSellers,
|