@tokenbuddy/tokenbuddy 1.0.5 → 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 +48 -1
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +144 -17
- 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 +560 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +11 -5
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +574 -161
- 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 +99 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -0
- package/dist/src/doctor-diagnostics.js +552 -0
- package/dist/src/doctor-diagnostics.js.map +1 -0
- 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 +56 -0
- package/dist/src/init-payment-options.d.ts.map +1 -0
- package/dist/src/init-payment-options.js +165 -0
- package/dist/src/init-payment-options.js.map +1 -0
- package/dist/src/provider-install.d.ts +37 -2
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +317 -67
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +79 -0
- package/dist/src/seller-catalog.d.ts.map +1 -0
- package/dist/src/seller-catalog.js +126 -0
- package/dist/src/seller-catalog.js.map +1 -0
- package/dist/src/tb-proxyd.js +13 -2
- package/dist/src/tb-proxyd.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 +253 -18
- package/src/cli.ts +709 -68
- package/src/daemon.ts +651 -167
- package/src/doctor-clawtip-wallet.ts +70 -0
- package/src/doctor-diagnostics.ts +861 -0
- package/src/init-clawtip-activation.ts +487 -0
- package/src/init-payment-options.ts +249 -0
- package/src/provider-install.ts +426 -76
- package/src/seller-catalog.ts +222 -0
- package/src/tb-proxyd.ts +14 -2
- package/src/terminal-image.ts +187 -0
- package/tests/e2e.test.ts +88 -5
- package/tests/tokenbuddy.test.ts +1362 -27
package/src/daemon.ts
CHANGED
|
@@ -9,8 +9,22 @@ import {
|
|
|
9
9
|
applyProviderInstall,
|
|
10
10
|
detectProviders,
|
|
11
11
|
previewProviderInstall,
|
|
12
|
-
rollbackProviderInstall
|
|
12
|
+
rollbackProviderInstall,
|
|
13
|
+
type ClaudeCodeModelMappingConfig,
|
|
13
14
|
} from "./provider-install.js";
|
|
15
|
+
import {
|
|
16
|
+
discoverSellerBackedModels,
|
|
17
|
+
fetchSellerManifest,
|
|
18
|
+
fetchSellerRegistry,
|
|
19
|
+
manifestModelIds,
|
|
20
|
+
manifestPaymentMethods,
|
|
21
|
+
manifestProtocols,
|
|
22
|
+
normalizeSellerUrl,
|
|
23
|
+
type RegistrySeller,
|
|
24
|
+
type SellerManifest,
|
|
25
|
+
type SellerRegistryDocument,
|
|
26
|
+
type SellerRoutingPreference,
|
|
27
|
+
} from "./seller-catalog.js";
|
|
14
28
|
|
|
15
29
|
const logger = createModuleLogger("tb-proxyd");
|
|
16
30
|
const PROXY_JSON_BODY_LIMIT = "10mb";
|
|
@@ -21,30 +35,7 @@ export interface DaemonConfig {
|
|
|
21
35
|
dbPath: string;
|
|
22
36
|
sellerRegistryUrl: string;
|
|
23
37
|
selectionMode?: "auto" | "manual";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
interface RegistrySeller {
|
|
27
|
-
id: string;
|
|
28
|
-
name?: string;
|
|
29
|
-
url: string;
|
|
30
|
-
supportedProtocols?: string[];
|
|
31
|
-
paymentMethods?: string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface SellerRegistryDocument {
|
|
35
|
-
version: number;
|
|
36
|
-
defaultSeller?: string;
|
|
37
|
-
sellers: RegistrySeller[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface SellerManifest {
|
|
41
|
-
sellerId?: string;
|
|
42
|
-
seller_id?: string;
|
|
43
|
-
supportedProtocols?: string[];
|
|
44
|
-
supported_protocols?: string[];
|
|
45
|
-
paymentMethods?: string[];
|
|
46
|
-
payment_methods?: string[];
|
|
47
|
-
models?: Array<{ id: string; [key: string]: unknown }>;
|
|
38
|
+
selectedSellerId?: string;
|
|
48
39
|
}
|
|
49
40
|
|
|
50
41
|
interface SellerRoute {
|
|
@@ -61,6 +52,330 @@ interface UsageSummary {
|
|
|
61
52
|
billedMicros: number;
|
|
62
53
|
}
|
|
63
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
|
+
|
|
83
|
+
interface ResponsesStreamState {
|
|
84
|
+
itemId: string;
|
|
85
|
+
text: string;
|
|
86
|
+
contentPartStarted: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class ResponsesStreamNormalizer {
|
|
90
|
+
private pending = "";
|
|
91
|
+
private readonly state = new Map<string, ResponsesStreamState>();
|
|
92
|
+
|
|
93
|
+
public push(chunk: string): string {
|
|
94
|
+
this.pending += chunk;
|
|
95
|
+
const blocks = this.pending.split("\n\n");
|
|
96
|
+
this.pending = blocks.pop() || "";
|
|
97
|
+
return blocks
|
|
98
|
+
.map((block) => this.normalizeBlock(block))
|
|
99
|
+
.filter((block) => block.length > 0)
|
|
100
|
+
.join("\n\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public finish(): string {
|
|
104
|
+
if (!this.pending.trim()) {
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
const block = this.normalizeBlock(this.pending);
|
|
108
|
+
this.pending = "";
|
|
109
|
+
return block;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private normalizeBlock(block: string): string {
|
|
113
|
+
if (!block.trim()) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
// Each \n\n separates an event in SSE format
|
|
117
|
+
const subBlocks = block.split("\n\n");
|
|
118
|
+
const output: string[] = [];
|
|
119
|
+
|
|
120
|
+
for (const sub of subBlocks) {
|
|
121
|
+
if (!sub.trim() || sub.trim() === "data: [DONE]") {
|
|
122
|
+
if (sub.trim()) output.push(sub);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const lines = sub.split("\n");
|
|
127
|
+
const eventLine = lines.find((l) => l.startsWith("event:"));
|
|
128
|
+
const dataLine = lines.find((l) => l.startsWith("data:"));
|
|
129
|
+
if (!dataLine) {
|
|
130
|
+
output.push(sub);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const rawData = dataLine.replace(/^data:\s?/, "");
|
|
134
|
+
if (rawData === "[DONE]") {
|
|
135
|
+
output.push(sub);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let payload: any;
|
|
140
|
+
try {
|
|
141
|
+
payload = JSON.parse(rawData);
|
|
142
|
+
} catch {
|
|
143
|
+
output.push(sub);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const eventName =
|
|
148
|
+
(eventLine?.replace(/^event:\s?/, "") || payload?.type) as string;
|
|
149
|
+
if (!eventName || !eventName.startsWith("response.")) {
|
|
150
|
+
output.push(sub);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// When upstream already sends content_part.added, record it in state
|
|
155
|
+
if (
|
|
156
|
+
eventName === "response.content_part.added" &&
|
|
157
|
+
payload?.item_id
|
|
158
|
+
) {
|
|
159
|
+
const current = this.state.get(payload.item_id as string);
|
|
160
|
+
if (current) current.contentPartStarted = true;
|
|
161
|
+
output.push(sub);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// response.output_item.added: inject content_part.added only if upstream hasn't
|
|
166
|
+
if (
|
|
167
|
+
eventName === "response.output_item.added" &&
|
|
168
|
+
payload?.item?.type === "message" &&
|
|
169
|
+
payload?.item?.id
|
|
170
|
+
) {
|
|
171
|
+
const itemId = payload.item.id as string;
|
|
172
|
+
const current = this.getState(itemId);
|
|
173
|
+
const item = { ...payload.item };
|
|
174
|
+
item.content = [{ type: "output_text", text: "", annotations: [] }];
|
|
175
|
+
output.push(this.serializeEvent(eventName, {
|
|
176
|
+
...payload,
|
|
177
|
+
output_index: payload.output_index ?? 0,
|
|
178
|
+
item
|
|
179
|
+
}));
|
|
180
|
+
if (!current.contentPartStarted) {
|
|
181
|
+
current.contentPartStarted = true;
|
|
182
|
+
output.push(this.serializeEvent("response.content_part.added", {
|
|
183
|
+
type: "response.content_part.added",
|
|
184
|
+
item_id: itemId,
|
|
185
|
+
output_index: payload.output_index ?? 0,
|
|
186
|
+
content_index: 0,
|
|
187
|
+
part: { type: "output_text", text: "", annotations: [] }
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// response.output_text.delta: inject content_part.added if missing
|
|
194
|
+
if (eventName === "response.output_text.delta" && payload?.item_id) {
|
|
195
|
+
const itemId = payload.item_id as string;
|
|
196
|
+
const current = this.getState(itemId);
|
|
197
|
+
if (!current.contentPartStarted) {
|
|
198
|
+
current.contentPartStarted = true;
|
|
199
|
+
output.push(this.serializeEvent("response.content_part.added", {
|
|
200
|
+
type: "response.content_part.added",
|
|
201
|
+
item_id: itemId,
|
|
202
|
+
output_index: payload.output_index ?? 0,
|
|
203
|
+
content_index: payload.content_index ?? 0,
|
|
204
|
+
part: { type: "output_text", text: "", annotations: [] }
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
const deltaText =
|
|
208
|
+
typeof payload.delta === "string"
|
|
209
|
+
? payload.delta
|
|
210
|
+
: typeof payload.delta?.text === "string"
|
|
211
|
+
? payload.delta.text
|
|
212
|
+
: "";
|
|
213
|
+
current.text += deltaText;
|
|
214
|
+
output.push(this.serializeEvent(eventName, {
|
|
215
|
+
...payload,
|
|
216
|
+
output_index: payload.output_index ?? 0,
|
|
217
|
+
content_index: payload.content_index ?? 0
|
|
218
|
+
}));
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// response.output_text.done: also emit content_part.done
|
|
223
|
+
if (eventName === "response.output_text.done" && payload?.item_id) {
|
|
224
|
+
const itemId = payload.item_id as string;
|
|
225
|
+
const current = this.getState(itemId);
|
|
226
|
+
output.push(this.serializeEvent(eventName, {
|
|
227
|
+
...payload,
|
|
228
|
+
output_index: payload.output_index ?? 0,
|
|
229
|
+
content_index: payload.content_index ?? 0
|
|
230
|
+
}));
|
|
231
|
+
output.push(this.serializeEvent("response.content_part.done", {
|
|
232
|
+
type: "response.content_part.done",
|
|
233
|
+
item_id: itemId,
|
|
234
|
+
output_index: payload.output_index ?? 0,
|
|
235
|
+
content_index: payload.content_index ?? 0,
|
|
236
|
+
part: { type: "output_text", text: current.text, annotations: [] }
|
|
237
|
+
}));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// response.output_item.done: normalize content to output_text type
|
|
242
|
+
if (
|
|
243
|
+
eventName === "response.output_item.done" &&
|
|
244
|
+
payload?.item?.type === "message" &&
|
|
245
|
+
payload?.item?.id
|
|
246
|
+
) {
|
|
247
|
+
const itemId = payload.item.id as string;
|
|
248
|
+
const current = this.getState(itemId);
|
|
249
|
+
const item = {
|
|
250
|
+
...payload.item,
|
|
251
|
+
content: [{ type: "output_text", text: current.text, annotations: [] }]
|
|
252
|
+
};
|
|
253
|
+
output.push(this.serializeEvent(eventName, {
|
|
254
|
+
...payload,
|
|
255
|
+
output_index: payload.output_index ?? 0,
|
|
256
|
+
item
|
|
257
|
+
}));
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// response.completed: patch output if empty
|
|
262
|
+
if (eventName === "response.completed" && payload?.response) {
|
|
263
|
+
const response = { ...payload.response };
|
|
264
|
+
if (!Array.isArray(response.output) || response.output.length === 0) {
|
|
265
|
+
const first = this.state.values().next()
|
|
266
|
+
.value as ResponsesStreamState | undefined;
|
|
267
|
+
if (first) {
|
|
268
|
+
response.output = [{
|
|
269
|
+
id: first.itemId,
|
|
270
|
+
type: "message",
|
|
271
|
+
status: "completed",
|
|
272
|
+
role: "assistant",
|
|
273
|
+
content: [{ type: "output_text", text: first.text, annotations: [] }]
|
|
274
|
+
}];
|
|
275
|
+
response.output_text = first.text;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
output.push(this.serializeEvent(eventName, { ...payload, response }));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// All other events: pass through unchanged
|
|
283
|
+
output.push(sub);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return output.join("\n\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private getState(itemId: string): ResponsesStreamState {
|
|
290
|
+
const current = this.state.get(itemId);
|
|
291
|
+
if (current) return current;
|
|
292
|
+
const created = { itemId, text: "", contentPartStarted: false };
|
|
293
|
+
this.state.set(itemId, created);
|
|
294
|
+
return created;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private serializeEvent(name: string, data: any): string {
|
|
298
|
+
return `event: ${name}\ndata: ${JSON.stringify(data)}`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
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
|
+
|
|
64
379
|
interface PurchaseCreateResponse {
|
|
65
380
|
purchaseId?: string;
|
|
66
381
|
purchase_id?: string;
|
|
@@ -91,13 +406,22 @@ export class TokenbuddyDaemon {
|
|
|
91
406
|
private controlServer?: any;
|
|
92
407
|
private proxyServer?: any;
|
|
93
408
|
private selectionMode: "auto" | "manual";
|
|
409
|
+
private selectedSellerId?: string;
|
|
94
410
|
|
|
95
411
|
private activePurchases = new Map<string, Promise<string>>();
|
|
96
412
|
|
|
97
413
|
constructor(config: DaemonConfig) {
|
|
98
|
-
this.config = config;
|
|
99
|
-
this.selectionMode = config.selectionMode || "auto";
|
|
100
414
|
this.tokenStore = new BuyerStore({ dbPath: config.dbPath });
|
|
415
|
+
const routingPreference =
|
|
416
|
+
this.tokenStore.getDaemonRuntimeConfig<SellerRoutingPreference>("routing")
|
|
417
|
+
?.config;
|
|
418
|
+
this.config = config;
|
|
419
|
+
this.selectionMode =
|
|
420
|
+
config.selectionMode ||
|
|
421
|
+
(routingPreference?.mode === "fixed" ? "manual" : routingPreference?.mode) ||
|
|
422
|
+
"auto";
|
|
423
|
+
this.selectedSellerId =
|
|
424
|
+
config.selectedSellerId || routingPreference?.sellerId;
|
|
101
425
|
}
|
|
102
426
|
|
|
103
427
|
private activeControlPort(): number {
|
|
@@ -111,43 +435,25 @@ export class TokenbuddyDaemon {
|
|
|
111
435
|
}
|
|
112
436
|
|
|
113
437
|
private async fetchRegistry(): Promise<SellerRegistryDocument> {
|
|
114
|
-
|
|
115
|
-
if (!response.ok) {
|
|
116
|
-
throw new Error(`registry returned ${response.status}`);
|
|
117
|
-
}
|
|
118
|
-
const data = await response.json() as SellerRegistryDocument;
|
|
119
|
-
if (!data || !Array.isArray(data.sellers)) {
|
|
120
|
-
throw new Error("registry response missing sellers");
|
|
121
|
-
}
|
|
122
|
-
return data;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private async fetchSellerManifest(seller: RegistrySeller): Promise<SellerManifest> {
|
|
126
|
-
const baseUrl = seller.url.replace(/\/+$/, "");
|
|
127
|
-
const response = await fetch(`${baseUrl}/manifest`);
|
|
128
|
-
if (!response.ok) {
|
|
129
|
-
throw new Error(`manifest returned ${response.status}`);
|
|
130
|
-
}
|
|
131
|
-
return await response.json() as SellerManifest;
|
|
438
|
+
return await fetchSellerRegistry(this.config.sellerRegistryUrl);
|
|
132
439
|
}
|
|
133
440
|
|
|
134
441
|
private runtimeSummary() {
|
|
442
|
+
const sellerRoutingMode = this.selectedSellerId ? "fixed" : this.selectionMode;
|
|
135
443
|
return {
|
|
136
444
|
status: "running",
|
|
137
445
|
pid: process.pid,
|
|
138
446
|
controlPort: this.activeControlPort(),
|
|
139
447
|
proxyPort: this.activeProxyPort(),
|
|
140
448
|
selectionMode: this.selectionMode,
|
|
449
|
+
sellerRoutingMode,
|
|
450
|
+
selectedSellerId: this.selectedSellerId,
|
|
141
451
|
dbPath: this.config.dbPath,
|
|
142
452
|
sellerRegistryUrl: this.config.sellerRegistryUrl,
|
|
143
453
|
store: this.tokenStore.summary()
|
|
144
454
|
};
|
|
145
455
|
}
|
|
146
456
|
|
|
147
|
-
private normalizeSellerUrl(seller: RegistrySeller): string {
|
|
148
|
-
return seller.url.replace(/\/+$/, "");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
457
|
private endpointProtocol(endpoint: string): string | undefined {
|
|
152
458
|
if (endpoint === "/v1/chat/completions") {
|
|
153
459
|
return "chat_completions";
|
|
@@ -173,19 +479,63 @@ export class TokenbuddyDaemon {
|
|
|
173
479
|
return undefined;
|
|
174
480
|
}
|
|
175
481
|
|
|
176
|
-
private
|
|
177
|
-
const
|
|
178
|
-
return
|
|
179
|
-
?
|
|
180
|
-
:
|
|
482
|
+
private stripLocalClaudeOneMMarker(modelId: string): string {
|
|
483
|
+
const trimmed = modelId.trimEnd();
|
|
484
|
+
return trimmed.toLowerCase().endsWith("[1m]")
|
|
485
|
+
? trimmed.slice(0, -4).trimEnd()
|
|
486
|
+
: trimmed;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private resolveClaudeRoleModel(modelId: string): string {
|
|
490
|
+
const runtimeConfig = this.tokenStore.getProviderRuntimeConfig<ClaudeCodeModelMappingConfig>("claude-code")?.config;
|
|
491
|
+
const stripped = this.stripLocalClaudeOneMMarker(modelId);
|
|
492
|
+
if (!runtimeConfig || runtimeConfig.selectionKind !== "claude-role-mapping") {
|
|
493
|
+
return stripped;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const lowered = stripped.toLowerCase();
|
|
497
|
+
if (lowered === "haiku" || lowered.startsWith("claude-haiku")) {
|
|
498
|
+
return runtimeConfig.roles.haiku?.upstreamModel || runtimeConfig.fallbackModel || stripped;
|
|
499
|
+
}
|
|
500
|
+
if (lowered === "sonnet" || lowered.startsWith("claude-sonnet")) {
|
|
501
|
+
return runtimeConfig.roles.sonnet?.upstreamModel || runtimeConfig.fallbackModel || stripped;
|
|
502
|
+
}
|
|
503
|
+
if (lowered === "opus" || lowered.startsWith("claude-opus")) {
|
|
504
|
+
return runtimeConfig.roles.opus?.upstreamModel || runtimeConfig.fallbackModel || stripped;
|
|
505
|
+
}
|
|
506
|
+
return runtimeConfig.fallbackModel || stripped;
|
|
181
507
|
}
|
|
182
508
|
|
|
183
|
-
private
|
|
184
|
-
|
|
509
|
+
private resolveRouteModelId(endpoint: string, body: unknown): { requestedModelId?: string; resolvedModelId?: string } {
|
|
510
|
+
const requestedModelId = this.extractModelId(endpoint, body);
|
|
511
|
+
if (!requestedModelId) {
|
|
512
|
+
return {};
|
|
513
|
+
}
|
|
514
|
+
if (this.endpointProtocol(endpoint) === "messages") {
|
|
515
|
+
return {
|
|
516
|
+
requestedModelId,
|
|
517
|
+
resolvedModelId: this.resolveClaudeRoleModel(requestedModelId)
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
requestedModelId,
|
|
522
|
+
resolvedModelId: requestedModelId
|
|
523
|
+
};
|
|
185
524
|
}
|
|
186
525
|
|
|
187
|
-
private
|
|
188
|
-
|
|
526
|
+
private applyResolvedModelToBody(
|
|
527
|
+
endpoint: string,
|
|
528
|
+
body: Record<string, unknown>,
|
|
529
|
+
resolvedModelId: string
|
|
530
|
+
): Record<string, unknown> {
|
|
531
|
+
const nextBody: Record<string, unknown> = { ...body };
|
|
532
|
+
if ("model" in nextBody) {
|
|
533
|
+
nextBody.model = resolvedModelId;
|
|
534
|
+
}
|
|
535
|
+
if (endpoint === "/v1/responses" && "model_id" in nextBody) {
|
|
536
|
+
nextBody.model_id = resolvedModelId;
|
|
537
|
+
}
|
|
538
|
+
return nextBody;
|
|
189
539
|
}
|
|
190
540
|
|
|
191
541
|
private defaultPaymentMethod(): string | undefined {
|
|
@@ -206,13 +556,16 @@ export class TokenbuddyDaemon {
|
|
|
206
556
|
const registry = await this.fetchRegistry();
|
|
207
557
|
const defaultSellers = registry.sellers.filter((seller) => seller.id === registry.defaultSeller);
|
|
208
558
|
const backupSellers = registry.sellers.filter((seller) => seller.id !== registry.defaultSeller);
|
|
209
|
-
const
|
|
559
|
+
const manualSellers = this.selectedSellerId
|
|
560
|
+
? registry.sellers.filter((seller) => seller.id === this.selectedSellerId)
|
|
561
|
+
: defaultSellers;
|
|
562
|
+
const sellers = this.selectionMode === "manual" ? manualSellers : [...defaultSellers, ...backupSellers];
|
|
210
563
|
|
|
211
564
|
const routes: SellerRoute[] = [];
|
|
212
565
|
for (const seller of sellers) {
|
|
213
566
|
let manifest: SellerManifest;
|
|
214
567
|
try {
|
|
215
|
-
manifest = await
|
|
568
|
+
manifest = await fetchSellerManifest(seller);
|
|
216
569
|
} catch (error: unknown) {
|
|
217
570
|
logger.warn("route.manifest.failed", "seller manifest unavailable during route selection", {
|
|
218
571
|
sellerKey: seller.id,
|
|
@@ -223,9 +576,9 @@ export class TokenbuddyDaemon {
|
|
|
223
576
|
continue;
|
|
224
577
|
}
|
|
225
578
|
|
|
226
|
-
const protocols =
|
|
227
|
-
const paymentMethods =
|
|
228
|
-
const modelIds =
|
|
579
|
+
const protocols = manifestProtocols(manifest, seller);
|
|
580
|
+
const paymentMethods = manifestPaymentMethods(manifest, seller);
|
|
581
|
+
const modelIds = manifestModelIds(manifest);
|
|
229
582
|
if (!protocols.includes(protocol) || !paymentMethods.includes(paymentMethod) || !modelIds.includes(modelId)) {
|
|
230
583
|
continue;
|
|
231
584
|
}
|
|
@@ -303,51 +656,10 @@ export class TokenbuddyDaemon {
|
|
|
303
656
|
models: Array<{ id: string; sellerId: string; sellerName?: string; sellerUrl: string; supportedProtocols: string[]; paymentMethods: string[] }>;
|
|
304
657
|
sellers: Array<{ id: string; name?: string; url: string; status: string; manifestSellerId?: string; errorMessage?: string }>;
|
|
305
658
|
}> {
|
|
306
|
-
const
|
|
307
|
-
const sellerResults = await Promise.all(registry.sellers.map(async (seller) => {
|
|
308
|
-
try {
|
|
309
|
-
const manifest = await this.fetchSellerManifest(seller);
|
|
310
|
-
const protocols = this.manifestProtocols(manifest, seller);
|
|
311
|
-
const paymentMethods = this.manifestPaymentMethods(manifest, seller);
|
|
312
|
-
const models = (manifest.models || []).map((model) => ({
|
|
313
|
-
id: model.id,
|
|
314
|
-
sellerId: seller.id,
|
|
315
|
-
sellerName: seller.name,
|
|
316
|
-
sellerUrl: seller.url,
|
|
317
|
-
supportedProtocols: protocols,
|
|
318
|
-
paymentMethods
|
|
319
|
-
}));
|
|
320
|
-
return {
|
|
321
|
-
seller: {
|
|
322
|
-
id: seller.id,
|
|
323
|
-
name: seller.name,
|
|
324
|
-
url: seller.url,
|
|
325
|
-
status: "ok",
|
|
326
|
-
manifestSellerId: manifest.sellerId || manifest.seller_id || seller.id
|
|
327
|
-
},
|
|
328
|
-
models
|
|
329
|
-
};
|
|
330
|
-
} catch (error: unknown) {
|
|
331
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
332
|
-
logger.warn("models.refresh.seller_failed", "seller manifest refresh failed", {
|
|
333
|
-
sellerId: seller.id,
|
|
334
|
-
errorMessage
|
|
335
|
-
});
|
|
336
|
-
return {
|
|
337
|
-
seller: {
|
|
338
|
-
id: seller.id,
|
|
339
|
-
name: seller.name,
|
|
340
|
-
url: seller.url,
|
|
341
|
-
status: "failed",
|
|
342
|
-
errorMessage
|
|
343
|
-
},
|
|
344
|
-
models: []
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
}));
|
|
659
|
+
const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
|
|
348
660
|
return {
|
|
349
|
-
models:
|
|
350
|
-
sellers:
|
|
661
|
+
models: catalog.models,
|
|
662
|
+
sellers: catalog.sellers
|
|
351
663
|
};
|
|
352
664
|
}
|
|
353
665
|
|
|
@@ -381,6 +693,138 @@ export class TokenbuddyDaemon {
|
|
|
381
693
|
}
|
|
382
694
|
}
|
|
383
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
|
+
|
|
384
828
|
private inferPromptForHash(body: unknown): string | undefined {
|
|
385
829
|
if (!body || typeof body !== "object") {
|
|
386
830
|
return undefined;
|
|
@@ -409,7 +853,7 @@ export class TokenbuddyDaemon {
|
|
|
409
853
|
|
|
410
854
|
private async getOrPurchaseToken(route: SellerRoute): Promise<string> {
|
|
411
855
|
const sellerKey = route.seller.id;
|
|
412
|
-
const sellerUrl =
|
|
856
|
+
const sellerUrl = normalizeSellerUrl(route.seller);
|
|
413
857
|
const { modelId, paymentMethod } = route;
|
|
414
858
|
const cached = this.tokenStore.getToken(sellerKey);
|
|
415
859
|
const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
|
|
@@ -600,7 +1044,7 @@ export class TokenbuddyDaemon {
|
|
|
600
1044
|
const timeoutMs = this.clawtipProofTimeoutMs();
|
|
601
1045
|
const payload = JSON.stringify({
|
|
602
1046
|
sellerKey: route.seller.id,
|
|
603
|
-
sellerUrl:
|
|
1047
|
+
sellerUrl: normalizeSellerUrl(route.seller),
|
|
604
1048
|
modelId: route.modelId,
|
|
605
1049
|
purchase: createData,
|
|
606
1050
|
paymentInstructions: createData.paymentInstructions || createData.payment_instructions,
|
|
@@ -686,7 +1130,7 @@ export class TokenbuddyDaemon {
|
|
|
686
1130
|
private copyUpstreamHeaders(upstreamResponse: globalThis.Response, res: Response): void {
|
|
687
1131
|
upstreamResponse.headers.forEach((value, key) => {
|
|
688
1132
|
const lowerKey = key.toLowerCase();
|
|
689
|
-
if (["connection", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
|
|
1133
|
+
if (["connection", "content-encoding", "content-length", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
|
|
690
1134
|
return;
|
|
691
1135
|
}
|
|
692
1136
|
res.setHeader(key, value);
|
|
@@ -696,7 +1140,8 @@ export class TokenbuddyDaemon {
|
|
|
696
1140
|
private async forwardProxyRequest(endpoint: string, req: Request, res: Response): Promise<void> {
|
|
697
1141
|
const startedAt = Date.now();
|
|
698
1142
|
const body = req.body || {};
|
|
699
|
-
const
|
|
1143
|
+
const { requestedModelId, resolvedModelId } = this.resolveRouteModelId(endpoint, body);
|
|
1144
|
+
const modelId = resolvedModelId;
|
|
700
1145
|
const requestId = req.header("x-request-id") || (body && typeof body === "object" ? (body as { requestId?: string }).requestId : undefined) || `proxy_req_${crypto.randomBytes(8).toString("hex")}`;
|
|
701
1146
|
const idempotencyKey = req.header("idempotency-key") || `idem_${crypto.randomBytes(12).toString("hex")}`;
|
|
702
1147
|
|
|
@@ -718,24 +1163,16 @@ export class TokenbuddyDaemon {
|
|
|
718
1163
|
requestId,
|
|
719
1164
|
sellerKey,
|
|
720
1165
|
model: modelId,
|
|
1166
|
+
requestedModel: requestedModelId,
|
|
721
1167
|
endpoint,
|
|
722
1168
|
stream: Boolean((body as { stream?: unknown }).stream)
|
|
723
1169
|
});
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
const upstreamBody = {
|
|
1170
|
+
const sellerUrl = normalizeSellerUrl(route.seller);
|
|
1171
|
+
const upstreamBody = this.applyResolvedModelToBody(endpoint, {
|
|
727
1172
|
...(body as Record<string, unknown>),
|
|
728
1173
|
requestId
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
|
|
732
|
-
requestId,
|
|
733
|
-
sellerKey,
|
|
734
|
-
model: modelId,
|
|
735
|
-
endpoint,
|
|
736
|
-
stream: Boolean((body as { stream?: unknown }).stream)
|
|
737
|
-
});
|
|
738
|
-
const upstreamResponse = await fetch(`${sellerUrl}${endpoint}`, {
|
|
1174
|
+
}, modelId);
|
|
1175
|
+
const sendSellerRequest = async (token: string) => fetch(`${sellerUrl}${endpoint}`, {
|
|
739
1176
|
method: "POST",
|
|
740
1177
|
headers: {
|
|
741
1178
|
"Content-Type": "application/json",
|
|
@@ -746,8 +1183,45 @@ export class TokenbuddyDaemon {
|
|
|
746
1183
|
body: JSON.stringify(upstreamBody)
|
|
747
1184
|
});
|
|
748
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
|
+
|
|
749
1196
|
if (!upstreamResponse.ok) {
|
|
750
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 {
|
|
751
1225
|
logger.warn("proxy.upstream_fetch.failed", "proxy upstream fetch returned non-ok status", {
|
|
752
1226
|
requestId,
|
|
753
1227
|
sellerKey,
|
|
@@ -765,6 +1239,7 @@ export class TokenbuddyDaemon {
|
|
|
765
1239
|
res.status(upstreamResponse.status);
|
|
766
1240
|
res.send(errorBody);
|
|
767
1241
|
return;
|
|
1242
|
+
}
|
|
768
1243
|
}
|
|
769
1244
|
|
|
770
1245
|
this.copyUpstreamHeaders(upstreamResponse, res);
|
|
@@ -786,65 +1261,70 @@ export class TokenbuddyDaemon {
|
|
|
786
1261
|
return;
|
|
787
1262
|
}
|
|
788
1263
|
let bytes = 0;
|
|
1264
|
+
const decoder = new TextDecoder();
|
|
1265
|
+
const responsesStreamNormalizer = new ResponsesStreamNormalizer();
|
|
1266
|
+
const settlementExtractor = new SellerSettlementStreamExtractor();
|
|
789
1267
|
while (true) {
|
|
790
1268
|
const { done, value } = await reader.read();
|
|
791
1269
|
if (done) {
|
|
792
1270
|
break;
|
|
793
1271
|
}
|
|
794
1272
|
bytes += value.byteLength;
|
|
795
|
-
|
|
1273
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1274
|
+
const sellerChunk = settlementExtractor.push(chunk);
|
|
1275
|
+
if (sellerChunk.length === 0) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (endpoint === "/v1/responses") {
|
|
1279
|
+
const normalized = responsesStreamNormalizer.push(sellerChunk);
|
|
1280
|
+
if (normalized.length > 0) {
|
|
1281
|
+
res.write(`${normalized}\n\n`);
|
|
1282
|
+
}
|
|
1283
|
+
} else {
|
|
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);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (endpoint === "/v1/responses") {
|
|
1299
|
+
const trailing = responsesStreamNormalizer.finish();
|
|
1300
|
+
if (trailing.length > 0) {
|
|
1301
|
+
res.write(`${trailing}\n\n`);
|
|
1302
|
+
}
|
|
796
1303
|
}
|
|
797
1304
|
res.end();
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
this.tokenStore.recordInferenceLedger({
|
|
801
|
-
requestId,
|
|
802
|
-
sellerKey,
|
|
803
|
-
modelId,
|
|
1305
|
+
this.recordReconciledInference(
|
|
1306
|
+
route,
|
|
804
1307
|
endpoint,
|
|
805
|
-
status: "settled",
|
|
806
|
-
promptTokens: 0,
|
|
807
|
-
completionTokens: 0,
|
|
808
|
-
billedMicros,
|
|
809
|
-
prompt: this.inferPromptForHash(body)
|
|
810
|
-
});
|
|
811
|
-
logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
|
|
812
1308
|
requestId,
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
billedMicros
|
|
818
|
-
});
|
|
1309
|
+
{ promptTokens: 0, completionTokens: 0, billedMicros: Math.max(1, bytes) },
|
|
1310
|
+
this.parseSellerSettlementSummary(upstreamResponse.headers) ?? settlementTrailing.settlement ?? settlementExtractor.current(),
|
|
1311
|
+
this.inferPromptForHash(body)
|
|
1312
|
+
);
|
|
819
1313
|
return;
|
|
820
1314
|
}
|
|
821
1315
|
|
|
822
1316
|
const responseBody = await upstreamResponse.text();
|
|
823
1317
|
res.send(responseBody);
|
|
824
1318
|
const usage = this.readUsage(responseBody);
|
|
825
|
-
this.
|
|
826
|
-
|
|
827
|
-
requestId,
|
|
828
|
-
sellerKey,
|
|
829
|
-
modelId,
|
|
1319
|
+
this.recordReconciledInference(
|
|
1320
|
+
route,
|
|
830
1321
|
endpoint,
|
|
831
|
-
status: "settled",
|
|
832
|
-
promptTokens: usage.promptTokens,
|
|
833
|
-
completionTokens: usage.completionTokens,
|
|
834
|
-
billedMicros: usage.billedMicros,
|
|
835
|
-
prompt: this.inferPromptForHash(body),
|
|
836
|
-
response: responseBody
|
|
837
|
-
});
|
|
838
|
-
logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
|
|
839
1322
|
requestId,
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
completionTokens: usage.completionTokens,
|
|
846
|
-
billedMicros: usage.billedMicros
|
|
847
|
-
});
|
|
1323
|
+
usage,
|
|
1324
|
+
this.parseSellerSettlementSummary(upstreamResponse.headers),
|
|
1325
|
+
this.inferPromptForHash(body),
|
|
1326
|
+
responseBody
|
|
1327
|
+
);
|
|
848
1328
|
return;
|
|
849
1329
|
} catch (routeError: unknown) {
|
|
850
1330
|
lastError = routeError;
|
|
@@ -1026,7 +1506,9 @@ export class TokenbuddyDaemon {
|
|
|
1026
1506
|
const changes = previewProviderInstall({
|
|
1027
1507
|
providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
|
|
1028
1508
|
proxyUrl: String(req.body?.proxyUrl || ""),
|
|
1029
|
-
model:
|
|
1509
|
+
model: typeof req.body?.model === "string" ? req.body.model : undefined,
|
|
1510
|
+
providerSelections: req.body?.providerSelections,
|
|
1511
|
+
sellerRouting: req.body?.sellerRouting,
|
|
1030
1512
|
home: typeof req.body?.home === "string" ? req.body.home : undefined
|
|
1031
1513
|
});
|
|
1032
1514
|
logger.info("provider.install.previewed", "provider install previewed", {
|
|
@@ -1053,7 +1535,9 @@ export class TokenbuddyDaemon {
|
|
|
1053
1535
|
const applied = applyProviderInstall({
|
|
1054
1536
|
providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
|
|
1055
1537
|
proxyUrl: String(req.body?.proxyUrl || ""),
|
|
1056
|
-
model:
|
|
1538
|
+
model: typeof req.body?.model === "string" ? req.body.model : undefined,
|
|
1539
|
+
providerSelections: req.body?.providerSelections,
|
|
1540
|
+
sellerRouting: req.body?.sellerRouting,
|
|
1057
1541
|
home: typeof req.body?.home === "string" ? req.body.home : undefined
|
|
1058
1542
|
}, this.tokenStore);
|
|
1059
1543
|
logger.info("provider.install.applied", "provider install applied", {
|