@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.
Files changed (56) hide show
  1. package/dist/src/buyer-store.d.ts +48 -1
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +144 -17
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +17 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +560 -63
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/daemon.d.ts +11 -5
  10. package/dist/src/daemon.d.ts.map +1 -1
  11. package/dist/src/daemon.js +574 -161
  12. package/dist/src/daemon.js.map +1 -1
  13. package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
  14. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
  15. package/dist/src/doctor-clawtip-wallet.js +54 -0
  16. package/dist/src/doctor-clawtip-wallet.js.map +1 -0
  17. package/dist/src/doctor-diagnostics.d.ts +99 -0
  18. package/dist/src/doctor-diagnostics.d.ts.map +1 -0
  19. package/dist/src/doctor-diagnostics.js +552 -0
  20. package/dist/src/doctor-diagnostics.js.map +1 -0
  21. package/dist/src/init-clawtip-activation.d.ts +48 -0
  22. package/dist/src/init-clawtip-activation.d.ts.map +1 -0
  23. package/dist/src/init-clawtip-activation.js +395 -0
  24. package/dist/src/init-clawtip-activation.js.map +1 -0
  25. package/dist/src/init-payment-options.d.ts +56 -0
  26. package/dist/src/init-payment-options.d.ts.map +1 -0
  27. package/dist/src/init-payment-options.js +165 -0
  28. package/dist/src/init-payment-options.js.map +1 -0
  29. package/dist/src/provider-install.d.ts +37 -2
  30. package/dist/src/provider-install.d.ts.map +1 -1
  31. package/dist/src/provider-install.js +317 -67
  32. package/dist/src/provider-install.js.map +1 -1
  33. package/dist/src/seller-catalog.d.ts +79 -0
  34. package/dist/src/seller-catalog.d.ts.map +1 -0
  35. package/dist/src/seller-catalog.js +126 -0
  36. package/dist/src/seller-catalog.js.map +1 -0
  37. package/dist/src/tb-proxyd.js +13 -2
  38. package/dist/src/tb-proxyd.js.map +1 -1
  39. package/dist/src/terminal-image.d.ts +22 -0
  40. package/dist/src/terminal-image.d.ts.map +1 -0
  41. package/dist/src/terminal-image.js +135 -0
  42. package/dist/src/terminal-image.js.map +1 -0
  43. package/package.json +1 -1
  44. package/src/buyer-store.ts +253 -18
  45. package/src/cli.ts +709 -68
  46. package/src/daemon.ts +651 -167
  47. package/src/doctor-clawtip-wallet.ts +70 -0
  48. package/src/doctor-diagnostics.ts +861 -0
  49. package/src/init-clawtip-activation.ts +487 -0
  50. package/src/init-payment-options.ts +249 -0
  51. package/src/provider-install.ts +426 -76
  52. package/src/seller-catalog.ts +222 -0
  53. package/src/tb-proxyd.ts +14 -2
  54. package/src/terminal-image.ts +187 -0
  55. package/tests/e2e.test.ts +88 -5
  56. package/tests/tokenbuddy.test.ts +1362 -27
@@ -4,20 +4,302 @@ import { spawn } from "child_process";
4
4
  import * as fs from "fs";
5
5
  import { createModuleLogger } from "@tokenbuddy/logging";
6
6
  import { BuyerStore } from "./buyer-store.js";
7
- import { applyProviderInstall, detectProviders, previewProviderInstall, rollbackProviderInstall } from "./provider-install.js";
7
+ import { applyProviderInstall, detectProviders, previewProviderInstall, rollbackProviderInstall, } from "./provider-install.js";
8
+ import { discoverSellerBackedModels, fetchSellerManifest, fetchSellerRegistry, manifestModelIds, manifestPaymentMethods, manifestProtocols, normalizeSellerUrl, } from "./seller-catalog.js";
8
9
  const logger = createModuleLogger("tb-proxyd");
9
10
  const PROXY_JSON_BODY_LIMIT = "10mb";
11
+ function numericHeaderField(value) {
12
+ if (typeof value === "number" && Number.isFinite(value)) {
13
+ return value;
14
+ }
15
+ if (typeof value === "string" && value.trim().length > 0) {
16
+ const parsed = Number(value);
17
+ return Number.isFinite(parsed) ? parsed : undefined;
18
+ }
19
+ return undefined;
20
+ }
21
+ class ResponsesStreamNormalizer {
22
+ pending = "";
23
+ state = new Map();
24
+ push(chunk) {
25
+ this.pending += chunk;
26
+ const blocks = this.pending.split("\n\n");
27
+ this.pending = blocks.pop() || "";
28
+ return blocks
29
+ .map((block) => this.normalizeBlock(block))
30
+ .filter((block) => block.length > 0)
31
+ .join("\n\n");
32
+ }
33
+ finish() {
34
+ if (!this.pending.trim()) {
35
+ return "";
36
+ }
37
+ const block = this.normalizeBlock(this.pending);
38
+ this.pending = "";
39
+ return block;
40
+ }
41
+ normalizeBlock(block) {
42
+ if (!block.trim()) {
43
+ return "";
44
+ }
45
+ // Each \n\n separates an event in SSE format
46
+ const subBlocks = block.split("\n\n");
47
+ const output = [];
48
+ for (const sub of subBlocks) {
49
+ if (!sub.trim() || sub.trim() === "data: [DONE]") {
50
+ if (sub.trim())
51
+ output.push(sub);
52
+ continue;
53
+ }
54
+ const lines = sub.split("\n");
55
+ const eventLine = lines.find((l) => l.startsWith("event:"));
56
+ const dataLine = lines.find((l) => l.startsWith("data:"));
57
+ if (!dataLine) {
58
+ output.push(sub);
59
+ continue;
60
+ }
61
+ const rawData = dataLine.replace(/^data:\s?/, "");
62
+ if (rawData === "[DONE]") {
63
+ output.push(sub);
64
+ continue;
65
+ }
66
+ let payload;
67
+ try {
68
+ payload = JSON.parse(rawData);
69
+ }
70
+ catch {
71
+ output.push(sub);
72
+ continue;
73
+ }
74
+ const eventName = (eventLine?.replace(/^event:\s?/, "") || payload?.type);
75
+ if (!eventName || !eventName.startsWith("response.")) {
76
+ output.push(sub);
77
+ continue;
78
+ }
79
+ // When upstream already sends content_part.added, record it in state
80
+ if (eventName === "response.content_part.added" &&
81
+ payload?.item_id) {
82
+ const current = this.state.get(payload.item_id);
83
+ if (current)
84
+ current.contentPartStarted = true;
85
+ output.push(sub);
86
+ continue;
87
+ }
88
+ // response.output_item.added: inject content_part.added only if upstream hasn't
89
+ if (eventName === "response.output_item.added" &&
90
+ payload?.item?.type === "message" &&
91
+ payload?.item?.id) {
92
+ const itemId = payload.item.id;
93
+ const current = this.getState(itemId);
94
+ const item = { ...payload.item };
95
+ item.content = [{ type: "output_text", text: "", annotations: [] }];
96
+ output.push(this.serializeEvent(eventName, {
97
+ ...payload,
98
+ output_index: payload.output_index ?? 0,
99
+ item
100
+ }));
101
+ if (!current.contentPartStarted) {
102
+ current.contentPartStarted = true;
103
+ output.push(this.serializeEvent("response.content_part.added", {
104
+ type: "response.content_part.added",
105
+ item_id: itemId,
106
+ output_index: payload.output_index ?? 0,
107
+ content_index: 0,
108
+ part: { type: "output_text", text: "", annotations: [] }
109
+ }));
110
+ }
111
+ continue;
112
+ }
113
+ // response.output_text.delta: inject content_part.added if missing
114
+ if (eventName === "response.output_text.delta" && payload?.item_id) {
115
+ const itemId = payload.item_id;
116
+ const current = this.getState(itemId);
117
+ if (!current.contentPartStarted) {
118
+ current.contentPartStarted = true;
119
+ output.push(this.serializeEvent("response.content_part.added", {
120
+ type: "response.content_part.added",
121
+ item_id: itemId,
122
+ output_index: payload.output_index ?? 0,
123
+ content_index: payload.content_index ?? 0,
124
+ part: { type: "output_text", text: "", annotations: [] }
125
+ }));
126
+ }
127
+ const deltaText = typeof payload.delta === "string"
128
+ ? payload.delta
129
+ : typeof payload.delta?.text === "string"
130
+ ? payload.delta.text
131
+ : "";
132
+ current.text += deltaText;
133
+ output.push(this.serializeEvent(eventName, {
134
+ ...payload,
135
+ output_index: payload.output_index ?? 0,
136
+ content_index: payload.content_index ?? 0
137
+ }));
138
+ continue;
139
+ }
140
+ // response.output_text.done: also emit content_part.done
141
+ if (eventName === "response.output_text.done" && payload?.item_id) {
142
+ const itemId = payload.item_id;
143
+ const current = this.getState(itemId);
144
+ output.push(this.serializeEvent(eventName, {
145
+ ...payload,
146
+ output_index: payload.output_index ?? 0,
147
+ content_index: payload.content_index ?? 0
148
+ }));
149
+ output.push(this.serializeEvent("response.content_part.done", {
150
+ type: "response.content_part.done",
151
+ item_id: itemId,
152
+ output_index: payload.output_index ?? 0,
153
+ content_index: payload.content_index ?? 0,
154
+ part: { type: "output_text", text: current.text, annotations: [] }
155
+ }));
156
+ continue;
157
+ }
158
+ // response.output_item.done: normalize content to output_text type
159
+ if (eventName === "response.output_item.done" &&
160
+ payload?.item?.type === "message" &&
161
+ payload?.item?.id) {
162
+ const itemId = payload.item.id;
163
+ const current = this.getState(itemId);
164
+ const item = {
165
+ ...payload.item,
166
+ content: [{ type: "output_text", text: current.text, annotations: [] }]
167
+ };
168
+ output.push(this.serializeEvent(eventName, {
169
+ ...payload,
170
+ output_index: payload.output_index ?? 0,
171
+ item
172
+ }));
173
+ continue;
174
+ }
175
+ // response.completed: patch output if empty
176
+ if (eventName === "response.completed" && payload?.response) {
177
+ const response = { ...payload.response };
178
+ if (!Array.isArray(response.output) || response.output.length === 0) {
179
+ const first = this.state.values().next()
180
+ .value;
181
+ if (first) {
182
+ response.output = [{
183
+ id: first.itemId,
184
+ type: "message",
185
+ status: "completed",
186
+ role: "assistant",
187
+ content: [{ type: "output_text", text: first.text, annotations: [] }]
188
+ }];
189
+ response.output_text = first.text;
190
+ }
191
+ }
192
+ output.push(this.serializeEvent(eventName, { ...payload, response }));
193
+ continue;
194
+ }
195
+ // All other events: pass through unchanged
196
+ output.push(sub);
197
+ }
198
+ return output.join("\n\n");
199
+ }
200
+ getState(itemId) {
201
+ const current = this.state.get(itemId);
202
+ if (current)
203
+ return current;
204
+ const created = { itemId, text: "", contentPartStarted: false };
205
+ this.state.set(itemId, created);
206
+ return created;
207
+ }
208
+ serializeEvent(name, data) {
209
+ return `event: ${name}\ndata: ${JSON.stringify(data)}`;
210
+ }
211
+ }
212
+ class SellerSettlementStreamExtractor {
213
+ pending = "";
214
+ settlement;
215
+ push(chunk) {
216
+ this.pending += chunk;
217
+ const blocks = this.pending.split("\n\n");
218
+ this.pending = blocks.pop() || "";
219
+ return blocks
220
+ .map((block) => this.processBlock(block))
221
+ .filter((block) => block.length > 0)
222
+ .join("\n\n");
223
+ }
224
+ finish() {
225
+ const downstream = this.pending.trim() ? this.processBlock(this.pending) : "";
226
+ this.pending = "";
227
+ return { downstream, settlement: this.settlement };
228
+ }
229
+ current() {
230
+ return this.settlement;
231
+ }
232
+ processBlock(block) {
233
+ if (!block.trim()) {
234
+ return "";
235
+ }
236
+ const lines = block.split("\n");
237
+ const eventLine = lines.find((line) => line.startsWith("event:"));
238
+ const eventName = eventLine?.replace(/^event:\s?/, "").trim();
239
+ if (eventName !== "tokenbuddy.settlement") {
240
+ return block;
241
+ }
242
+ const dataLine = lines.find((line) => line.startsWith("data:"));
243
+ if (!dataLine) {
244
+ return "";
245
+ }
246
+ const parsed = parseSellerSettlementObject(dataLine.replace(/^data:\s?/, ""));
247
+ if (parsed) {
248
+ this.settlement = parsed;
249
+ }
250
+ return "";
251
+ }
252
+ }
253
+ function parseSellerSettlementObject(raw) {
254
+ try {
255
+ const parsed = JSON.parse(raw);
256
+ const requestId = typeof parsed.requestId === "string"
257
+ ? parsed.requestId
258
+ : typeof parsed.request_id === "string"
259
+ ? parsed.request_id
260
+ : undefined;
261
+ const settledMicros = numericHeaderField(parsed.settledMicros ?? parsed.settled_micros);
262
+ const remainingCreditMicros = numericHeaderField(parsed.remainingCreditMicros ?? parsed.remaining_credit_micros);
263
+ if (!requestId || settledMicros === undefined || remainingCreditMicros === undefined) {
264
+ return undefined;
265
+ }
266
+ return {
267
+ requestId,
268
+ settledMicros,
269
+ settledUsdMicros: numericHeaderField(parsed.settledUsdMicros ?? parsed.settled_usd_micros),
270
+ remainingCreditMicros,
271
+ reservedBalanceMicros: numericHeaderField(parsed.reservedBalanceMicros ?? parsed.reserved_balance_micros),
272
+ spentMicros: numericHeaderField(parsed.spentMicros ?? parsed.spent_micros),
273
+ priceVersion: typeof parsed.priceVersion === "string"
274
+ ? parsed.priceVersion
275
+ : typeof parsed.price_version === "string"
276
+ ? parsed.price_version
277
+ : undefined
278
+ };
279
+ }
280
+ catch {
281
+ return undefined;
282
+ }
283
+ }
10
284
  export class TokenbuddyDaemon {
11
285
  config;
12
286
  tokenStore;
13
287
  controlServer;
14
288
  proxyServer;
15
289
  selectionMode;
290
+ selectedSellerId;
16
291
  activePurchases = new Map();
17
292
  constructor(config) {
18
- this.config = config;
19
- this.selectionMode = config.selectionMode || "auto";
20
293
  this.tokenStore = new BuyerStore({ dbPath: config.dbPath });
294
+ const routingPreference = this.tokenStore.getDaemonRuntimeConfig("routing")
295
+ ?.config;
296
+ this.config = config;
297
+ this.selectionMode =
298
+ config.selectionMode ||
299
+ (routingPreference?.mode === "fixed" ? "manual" : routingPreference?.mode) ||
300
+ "auto";
301
+ this.selectedSellerId =
302
+ config.selectedSellerId || routingPreference?.sellerId;
21
303
  }
22
304
  activeControlPort() {
23
305
  const address = this.controlServer?.address?.();
@@ -28,39 +310,23 @@ export class TokenbuddyDaemon {
28
310
  return typeof address === "object" && address ? address.port : this.config.proxyPort;
29
311
  }
30
312
  async fetchRegistry() {
31
- const response = await fetch(this.config.sellerRegistryUrl);
32
- if (!response.ok) {
33
- throw new Error(`registry returned ${response.status}`);
34
- }
35
- const data = await response.json();
36
- if (!data || !Array.isArray(data.sellers)) {
37
- throw new Error("registry response missing sellers");
38
- }
39
- return data;
40
- }
41
- async fetchSellerManifest(seller) {
42
- const baseUrl = seller.url.replace(/\/+$/, "");
43
- const response = await fetch(`${baseUrl}/manifest`);
44
- if (!response.ok) {
45
- throw new Error(`manifest returned ${response.status}`);
46
- }
47
- return await response.json();
313
+ return await fetchSellerRegistry(this.config.sellerRegistryUrl);
48
314
  }
49
315
  runtimeSummary() {
316
+ const sellerRoutingMode = this.selectedSellerId ? "fixed" : this.selectionMode;
50
317
  return {
51
318
  status: "running",
52
319
  pid: process.pid,
53
320
  controlPort: this.activeControlPort(),
54
321
  proxyPort: this.activeProxyPort(),
55
322
  selectionMode: this.selectionMode,
323
+ sellerRoutingMode,
324
+ selectedSellerId: this.selectedSellerId,
56
325
  dbPath: this.config.dbPath,
57
326
  sellerRegistryUrl: this.config.sellerRegistryUrl,
58
327
  store: this.tokenStore.summary()
59
328
  };
60
329
  }
61
- normalizeSellerUrl(seller) {
62
- return seller.url.replace(/\/+$/, "");
63
- }
64
330
  endpointProtocol(endpoint) {
65
331
  if (endpoint === "/v1/chat/completions") {
66
332
  return "chat_completions";
@@ -84,17 +350,55 @@ export class TokenbuddyDaemon {
84
350
  }
85
351
  return undefined;
86
352
  }
87
- manifestProtocols(manifest, seller) {
88
- const protocols = manifest.supportedProtocols || manifest.supported_protocols || seller.supportedProtocols || [];
89
- return protocols.includes("anthropic_messages") && !protocols.includes("messages")
90
- ? [...protocols, "messages"]
91
- : protocols;
353
+ stripLocalClaudeOneMMarker(modelId) {
354
+ const trimmed = modelId.trimEnd();
355
+ return trimmed.toLowerCase().endsWith("[1m]")
356
+ ? trimmed.slice(0, -4).trimEnd()
357
+ : trimmed;
92
358
  }
93
- manifestPaymentMethods(manifest, seller) {
94
- return manifest.paymentMethods || manifest.payment_methods || seller.paymentMethods || [];
359
+ resolveClaudeRoleModel(modelId) {
360
+ const runtimeConfig = this.tokenStore.getProviderRuntimeConfig("claude-code")?.config;
361
+ const stripped = this.stripLocalClaudeOneMMarker(modelId);
362
+ if (!runtimeConfig || runtimeConfig.selectionKind !== "claude-role-mapping") {
363
+ return stripped;
364
+ }
365
+ const lowered = stripped.toLowerCase();
366
+ if (lowered === "haiku" || lowered.startsWith("claude-haiku")) {
367
+ return runtimeConfig.roles.haiku?.upstreamModel || runtimeConfig.fallbackModel || stripped;
368
+ }
369
+ if (lowered === "sonnet" || lowered.startsWith("claude-sonnet")) {
370
+ return runtimeConfig.roles.sonnet?.upstreamModel || runtimeConfig.fallbackModel || stripped;
371
+ }
372
+ if (lowered === "opus" || lowered.startsWith("claude-opus")) {
373
+ return runtimeConfig.roles.opus?.upstreamModel || runtimeConfig.fallbackModel || stripped;
374
+ }
375
+ return runtimeConfig.fallbackModel || stripped;
95
376
  }
96
- manifestModelIds(manifest) {
97
- return (manifest.models || []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0);
377
+ resolveRouteModelId(endpoint, body) {
378
+ const requestedModelId = this.extractModelId(endpoint, body);
379
+ if (!requestedModelId) {
380
+ return {};
381
+ }
382
+ if (this.endpointProtocol(endpoint) === "messages") {
383
+ return {
384
+ requestedModelId,
385
+ resolvedModelId: this.resolveClaudeRoleModel(requestedModelId)
386
+ };
387
+ }
388
+ return {
389
+ requestedModelId,
390
+ resolvedModelId: requestedModelId
391
+ };
392
+ }
393
+ applyResolvedModelToBody(endpoint, body, resolvedModelId) {
394
+ const nextBody = { ...body };
395
+ if ("model" in nextBody) {
396
+ nextBody.model = resolvedModelId;
397
+ }
398
+ if (endpoint === "/v1/responses" && "model_id" in nextBody) {
399
+ nextBody.model_id = resolvedModelId;
400
+ }
401
+ return nextBody;
98
402
  }
99
403
  defaultPaymentMethod() {
100
404
  const payments = this.tokenStore.listPayments().filter((payment) => payment.enabled);
@@ -112,12 +416,15 @@ export class TokenbuddyDaemon {
112
416
  const registry = await this.fetchRegistry();
113
417
  const defaultSellers = registry.sellers.filter((seller) => seller.id === registry.defaultSeller);
114
418
  const backupSellers = registry.sellers.filter((seller) => seller.id !== registry.defaultSeller);
115
- const sellers = this.selectionMode === "manual" ? defaultSellers : [...defaultSellers, ...backupSellers];
419
+ const manualSellers = this.selectedSellerId
420
+ ? registry.sellers.filter((seller) => seller.id === this.selectedSellerId)
421
+ : defaultSellers;
422
+ const sellers = this.selectionMode === "manual" ? manualSellers : [...defaultSellers, ...backupSellers];
116
423
  const routes = [];
117
424
  for (const seller of sellers) {
118
425
  let manifest;
119
426
  try {
120
- manifest = await this.fetchSellerManifest(seller);
427
+ manifest = await fetchSellerManifest(seller);
121
428
  }
122
429
  catch (error) {
123
430
  logger.warn("route.manifest.failed", "seller manifest unavailable during route selection", {
@@ -128,9 +435,9 @@ export class TokenbuddyDaemon {
128
435
  });
129
436
  continue;
130
437
  }
131
- const protocols = this.manifestProtocols(manifest, seller);
132
- const paymentMethods = this.manifestPaymentMethods(manifest, seller);
133
- const modelIds = this.manifestModelIds(manifest);
438
+ const protocols = manifestProtocols(manifest, seller);
439
+ const paymentMethods = manifestPaymentMethods(manifest, seller);
440
+ const modelIds = manifestModelIds(manifest);
134
441
  if (!protocols.includes(protocol) || !paymentMethods.includes(paymentMethod) || !modelIds.includes(modelId)) {
135
442
  continue;
136
443
  }
@@ -190,52 +497,10 @@ export class TokenbuddyDaemon {
190
497
  return route;
191
498
  }
192
499
  async listSellerBackedModels() {
193
- const registry = await this.fetchRegistry();
194
- const sellerResults = await Promise.all(registry.sellers.map(async (seller) => {
195
- try {
196
- const manifest = await this.fetchSellerManifest(seller);
197
- const protocols = this.manifestProtocols(manifest, seller);
198
- const paymentMethods = this.manifestPaymentMethods(manifest, seller);
199
- const models = (manifest.models || []).map((model) => ({
200
- id: model.id,
201
- sellerId: seller.id,
202
- sellerName: seller.name,
203
- sellerUrl: seller.url,
204
- supportedProtocols: protocols,
205
- paymentMethods
206
- }));
207
- return {
208
- seller: {
209
- id: seller.id,
210
- name: seller.name,
211
- url: seller.url,
212
- status: "ok",
213
- manifestSellerId: manifest.sellerId || manifest.seller_id || seller.id
214
- },
215
- models
216
- };
217
- }
218
- catch (error) {
219
- const errorMessage = error instanceof Error ? error.message : String(error);
220
- logger.warn("models.refresh.seller_failed", "seller manifest refresh failed", {
221
- sellerId: seller.id,
222
- errorMessage
223
- });
224
- return {
225
- seller: {
226
- id: seller.id,
227
- name: seller.name,
228
- url: seller.url,
229
- status: "failed",
230
- errorMessage
231
- },
232
- models: []
233
- };
234
- }
235
- }));
500
+ const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
236
501
  return {
237
- models: sellerResults.flatMap((result) => result.models),
238
- sellers: sellerResults.map((result) => result.seller)
502
+ models: catalog.models,
503
+ sellers: catalog.sellers
239
504
  };
240
505
  }
241
506
  readUsage(bodyText) {
@@ -261,6 +526,125 @@ export class TokenbuddyDaemon {
261
526
  return fallback;
262
527
  }
263
528
  }
529
+ parseSellerSettlementSummary(headers) {
530
+ const raw = headers.get("x-tokenbuddy-settlement");
531
+ if (!raw) {
532
+ return undefined;
533
+ }
534
+ return parseSellerSettlementObject(raw);
535
+ }
536
+ recordReconciledInference(route, endpoint, requestId, usage, settlement, prompt, response) {
537
+ if (settlement) {
538
+ this.tokenStore.reconcileTokenBalance({
539
+ sellerKey: route.seller.id,
540
+ balanceMicros: settlement.remainingCreditMicros,
541
+ reservedMicros: settlement.reservedBalanceMicros ?? 0,
542
+ spentMicros: settlement.spentMicros ?? 0,
543
+ balanceSource: "seller_settlement_summary"
544
+ });
545
+ }
546
+ const settledMicros = settlement?.settledMicros;
547
+ this.tokenStore.recordInferenceLedger({
548
+ requestId: settlement?.requestId || requestId,
549
+ sellerKey: route.seller.id,
550
+ modelId: route.modelId,
551
+ endpoint,
552
+ status: settlement ? "settled" : "estimated",
553
+ promptTokens: usage.promptTokens,
554
+ completionTokens: usage.completionTokens,
555
+ billedMicros: settledMicros ?? usage.billedMicros,
556
+ estimatedMicros: usage.billedMicros,
557
+ settledMicros,
558
+ settledUsdMicros: settlement?.settledUsdMicros,
559
+ priceVersion: settlement?.priceVersion,
560
+ balanceSnapshotMicros: settlement?.remainingCreditMicros,
561
+ balanceSource: settlement ? "seller_authoritative" : "estimated",
562
+ prompt,
563
+ response
564
+ });
565
+ logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
566
+ requestId: settlement?.requestId || requestId,
567
+ sellerKey: route.seller.id,
568
+ model: route.modelId,
569
+ endpoint,
570
+ status: settlement ? "settled" : "estimated",
571
+ estimatedMicros: usage.billedMicros,
572
+ settledMicros,
573
+ balanceSource: settlement ? "seller_authoritative" : "estimated"
574
+ });
575
+ }
576
+ async refreshSellerBalance(route, token, balanceSource) {
577
+ const sellerKey = route.seller.id;
578
+ const sellerUrl = normalizeSellerUrl(route.seller);
579
+ const response = await fetch(`${sellerUrl}/v1/balance`, {
580
+ headers: { "Authorization": `Bearer ${token}` }
581
+ });
582
+ if (!response.ok) {
583
+ logger.warn("token.balance_refresh.failed", "seller balance refresh failed", {
584
+ sellerKey,
585
+ model: route.modelId,
586
+ status: response.status
587
+ });
588
+ return undefined;
589
+ }
590
+ const data = await response.json();
591
+ const creditMicros = numericHeaderField(data.creditMicros ?? data.credit_micros) ?? 0;
592
+ const reservedMicros = numericHeaderField(data.reservedMicros ?? data.reserved_micros) ?? 0;
593
+ const spentMicros = numericHeaderField(data.spentMicros ?? data.spent_micros) ?? 0;
594
+ const snapshot = {
595
+ creditMicros,
596
+ reservedMicros,
597
+ spentMicros,
598
+ availableMicros: Math.max(0, creditMicros - reservedMicros)
599
+ };
600
+ this.tokenStore.reconcileTokenBalance({
601
+ sellerKey,
602
+ balanceMicros: snapshot.availableMicros,
603
+ reservedMicros,
604
+ spentMicros,
605
+ balanceSource
606
+ });
607
+ logger.info("token.balance_refresh.succeeded", "seller balance refreshed", {
608
+ sellerKey,
609
+ model: route.modelId,
610
+ availableMicros: snapshot.availableMicros,
611
+ reservedMicros,
612
+ spentMicros,
613
+ balanceSource
614
+ });
615
+ return snapshot;
616
+ }
617
+ isInsufficientFundsResponse(status, bodyText) {
618
+ if (status !== 402) {
619
+ return false;
620
+ }
621
+ try {
622
+ const parsed = JSON.parse(bodyText);
623
+ const code = parsed.error?.code || "";
624
+ const message = parsed.error?.message || "";
625
+ return code === "insufficient_funds" || /insufficient funds/i.test(message);
626
+ }
627
+ catch {
628
+ return /insufficient funds/i.test(bodyText);
629
+ }
630
+ }
631
+ async recoverFromInsufficientFunds(route, token) {
632
+ const sellerKey = route.seller.id;
633
+ this.tokenStore.markTokenStale(sellerKey);
634
+ const snapshot = await this.refreshSellerBalance(route, token, "seller_402_refresh");
635
+ const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
636
+ if (!snapshot || snapshot.availableMicros <= rebuyMinBalanceMicros) {
637
+ logger.info("purchase.retry_after_402.started", "seller 402 triggered one-shot auto purchase retry", {
638
+ sellerKey,
639
+ model: route.modelId,
640
+ availableMicros: snapshot?.availableMicros ?? 0,
641
+ rebuyMinBalanceMicros
642
+ });
643
+ return await this.getOrPurchaseToken(route);
644
+ }
645
+ const cached = this.tokenStore.getToken(sellerKey);
646
+ return cached?.token || token;
647
+ }
264
648
  inferPromptForHash(body) {
265
649
  if (!body || typeof body !== "object") {
266
650
  return undefined;
@@ -286,7 +670,7 @@ export class TokenbuddyDaemon {
286
670
  }
287
671
  async getOrPurchaseToken(route) {
288
672
  const sellerKey = route.seller.id;
289
- const sellerUrl = this.normalizeSellerUrl(route.seller);
673
+ const sellerUrl = normalizeSellerUrl(route.seller);
290
674
  const { modelId, paymentMethod } = route;
291
675
  const cached = this.tokenStore.getToken(sellerKey);
292
676
  const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
@@ -461,7 +845,7 @@ export class TokenbuddyDaemon {
461
845
  const timeoutMs = this.clawtipProofTimeoutMs();
462
846
  const payload = JSON.stringify({
463
847
  sellerKey: route.seller.id,
464
- sellerUrl: this.normalizeSellerUrl(route.seller),
848
+ sellerUrl: normalizeSellerUrl(route.seller),
465
849
  modelId: route.modelId,
466
850
  purchase: createData,
467
851
  paymentInstructions: createData.paymentInstructions || createData.payment_instructions,
@@ -542,7 +926,7 @@ export class TokenbuddyDaemon {
542
926
  copyUpstreamHeaders(upstreamResponse, res) {
543
927
  upstreamResponse.headers.forEach((value, key) => {
544
928
  const lowerKey = key.toLowerCase();
545
- if (["connection", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
929
+ if (["connection", "content-encoding", "content-length", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
546
930
  return;
547
931
  }
548
932
  res.setHeader(key, value);
@@ -551,7 +935,8 @@ export class TokenbuddyDaemon {
551
935
  async forwardProxyRequest(endpoint, req, res) {
552
936
  const startedAt = Date.now();
553
937
  const body = req.body || {};
554
- const modelId = this.extractModelId(endpoint, body);
938
+ const { requestedModelId, resolvedModelId } = this.resolveRouteModelId(endpoint, body);
939
+ const modelId = resolvedModelId;
555
940
  const requestId = req.header("x-request-id") || (body && typeof body === "object" ? body.requestId : undefined) || `proxy_req_${crypto.randomBytes(8).toString("hex")}`;
556
941
  const idempotencyKey = req.header("idempotency-key") || `idem_${crypto.randomBytes(12).toString("hex")}`;
557
942
  if (!modelId) {
@@ -570,23 +955,16 @@ export class TokenbuddyDaemon {
570
955
  requestId,
571
956
  sellerKey,
572
957
  model: modelId,
958
+ requestedModel: requestedModelId,
573
959
  endpoint,
574
960
  stream: Boolean(body.stream)
575
961
  });
576
- const token = await this.getOrPurchaseToken(route);
577
- const sellerUrl = this.normalizeSellerUrl(route.seller);
578
- const upstreamBody = {
962
+ const sellerUrl = normalizeSellerUrl(route.seller);
963
+ const upstreamBody = this.applyResolvedModelToBody(endpoint, {
579
964
  ...body,
580
965
  requestId
581
- };
582
- logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
583
- requestId,
584
- sellerKey,
585
- model: modelId,
586
- endpoint,
587
- stream: Boolean(body.stream)
588
- });
589
- const upstreamResponse = await fetch(`${sellerUrl}${endpoint}`, {
966
+ }, modelId);
967
+ const sendSellerRequest = async (token) => fetch(`${sellerUrl}${endpoint}`, {
590
968
  method: "POST",
591
969
  headers: {
592
970
  "Content-Type": "application/json",
@@ -596,25 +974,64 @@ export class TokenbuddyDaemon {
596
974
  },
597
975
  body: JSON.stringify(upstreamBody)
598
976
  });
977
+ logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
978
+ requestId,
979
+ sellerKey,
980
+ model: modelId,
981
+ endpoint,
982
+ stream: Boolean(body.stream)
983
+ });
984
+ let token = await this.getOrPurchaseToken(route);
985
+ let upstreamResponse = await sendSellerRequest(token);
599
986
  if (!upstreamResponse.ok) {
600
987
  const errorBody = await upstreamResponse.text();
601
- logger.warn("proxy.upstream_fetch.failed", "proxy upstream fetch returned non-ok status", {
602
- requestId,
603
- sellerKey,
604
- model: modelId,
605
- endpoint,
606
- status: upstreamResponse.status,
607
- durationMs: Date.now() - startedAt
608
- });
609
- if (this.shouldFailoverStatus(upstreamResponse.status) && routeIndex < routes.length - 1) {
610
- lastError = new Error(`seller ${sellerKey} returned ${upstreamResponse.status}`);
611
- this.logFailover(route, endpoint, routeIndex, "upstream_status", upstreamResponse.status);
612
- continue;
988
+ if (this.isInsufficientFundsResponse(upstreamResponse.status, errorBody)) {
989
+ token = await this.recoverFromInsufficientFunds(route, token);
990
+ upstreamResponse = await sendSellerRequest(token);
991
+ if (upstreamResponse.ok) {
992
+ logger.info("proxy.retry_after_402.succeeded", "seller request succeeded after one-shot auto purchase retry", {
993
+ requestId,
994
+ sellerKey,
995
+ model: modelId,
996
+ endpoint,
997
+ durationMs: Date.now() - startedAt
998
+ });
999
+ }
1000
+ else {
1001
+ const retryErrorBody = await upstreamResponse.text();
1002
+ logger.warn("proxy.retry_after_402.failed", "seller request still failed after one-shot auto purchase retry", {
1003
+ requestId,
1004
+ sellerKey,
1005
+ model: modelId,
1006
+ endpoint,
1007
+ status: upstreamResponse.status,
1008
+ durationMs: Date.now() - startedAt
1009
+ });
1010
+ this.copyUpstreamHeaders(upstreamResponse, res);
1011
+ res.status(upstreamResponse.status);
1012
+ res.send(retryErrorBody);
1013
+ return;
1014
+ }
1015
+ }
1016
+ else {
1017
+ logger.warn("proxy.upstream_fetch.failed", "proxy upstream fetch returned non-ok status", {
1018
+ requestId,
1019
+ sellerKey,
1020
+ model: modelId,
1021
+ endpoint,
1022
+ status: upstreamResponse.status,
1023
+ durationMs: Date.now() - startedAt
1024
+ });
1025
+ if (this.shouldFailoverStatus(upstreamResponse.status) && routeIndex < routes.length - 1) {
1026
+ lastError = new Error(`seller ${sellerKey} returned ${upstreamResponse.status}`);
1027
+ this.logFailover(route, endpoint, routeIndex, "upstream_status", upstreamResponse.status);
1028
+ continue;
1029
+ }
1030
+ this.copyUpstreamHeaders(upstreamResponse, res);
1031
+ res.status(upstreamResponse.status);
1032
+ res.send(errorBody);
1033
+ return;
613
1034
  }
614
- this.copyUpstreamHeaders(upstreamResponse, res);
615
- res.status(upstreamResponse.status);
616
- res.send(errorBody);
617
- return;
618
1035
  }
619
1036
  this.copyUpstreamHeaders(upstreamResponse, res);
620
1037
  res.status(upstreamResponse.status);
@@ -634,64 +1051,56 @@ export class TokenbuddyDaemon {
634
1051
  return;
635
1052
  }
636
1053
  let bytes = 0;
1054
+ const decoder = new TextDecoder();
1055
+ const responsesStreamNormalizer = new ResponsesStreamNormalizer();
1056
+ const settlementExtractor = new SellerSettlementStreamExtractor();
637
1057
  while (true) {
638
1058
  const { done, value } = await reader.read();
639
1059
  if (done) {
640
1060
  break;
641
1061
  }
642
1062
  bytes += value.byteLength;
643
- res.write(Buffer.from(value));
1063
+ const chunk = decoder.decode(value, { stream: true });
1064
+ const sellerChunk = settlementExtractor.push(chunk);
1065
+ if (sellerChunk.length === 0) {
1066
+ continue;
1067
+ }
1068
+ if (endpoint === "/v1/responses") {
1069
+ const normalized = responsesStreamNormalizer.push(sellerChunk);
1070
+ if (normalized.length > 0) {
1071
+ res.write(`${normalized}\n\n`);
1072
+ }
1073
+ }
1074
+ else {
1075
+ res.write(sellerChunk);
1076
+ }
1077
+ }
1078
+ const settlementTrailing = settlementExtractor.finish();
1079
+ if (settlementTrailing.downstream.length > 0) {
1080
+ if (endpoint === "/v1/responses") {
1081
+ const normalized = responsesStreamNormalizer.push(settlementTrailing.downstream);
1082
+ if (normalized.length > 0) {
1083
+ res.write(`${normalized}\n\n`);
1084
+ }
1085
+ }
1086
+ else {
1087
+ res.write(settlementTrailing.downstream);
1088
+ }
1089
+ }
1090
+ if (endpoint === "/v1/responses") {
1091
+ const trailing = responsesStreamNormalizer.finish();
1092
+ if (trailing.length > 0) {
1093
+ res.write(`${trailing}\n\n`);
1094
+ }
644
1095
  }
645
1096
  res.end();
646
- const billedMicros = Math.max(1, bytes);
647
- this.tokenStore.deductBalance(sellerKey, billedMicros);
648
- this.tokenStore.recordInferenceLedger({
649
- requestId,
650
- sellerKey,
651
- modelId,
652
- endpoint,
653
- status: "settled",
654
- promptTokens: 0,
655
- completionTokens: 0,
656
- billedMicros,
657
- prompt: this.inferPromptForHash(body)
658
- });
659
- logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
660
- requestId,
661
- sellerKey,
662
- model: modelId,
663
- endpoint,
664
- status: "settled",
665
- billedMicros
666
- });
1097
+ this.recordReconciledInference(route, endpoint, requestId, { promptTokens: 0, completionTokens: 0, billedMicros: Math.max(1, bytes) }, this.parseSellerSettlementSummary(upstreamResponse.headers) ?? settlementTrailing.settlement ?? settlementExtractor.current(), this.inferPromptForHash(body));
667
1098
  return;
668
1099
  }
669
1100
  const responseBody = await upstreamResponse.text();
670
1101
  res.send(responseBody);
671
1102
  const usage = this.readUsage(responseBody);
672
- this.tokenStore.deductBalance(sellerKey, usage.billedMicros);
673
- this.tokenStore.recordInferenceLedger({
674
- requestId,
675
- sellerKey,
676
- modelId,
677
- endpoint,
678
- status: "settled",
679
- promptTokens: usage.promptTokens,
680
- completionTokens: usage.completionTokens,
681
- billedMicros: usage.billedMicros,
682
- prompt: this.inferPromptForHash(body),
683
- response: responseBody
684
- });
685
- logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
686
- requestId,
687
- sellerKey,
688
- model: modelId,
689
- endpoint,
690
- status: "settled",
691
- promptTokens: usage.promptTokens,
692
- completionTokens: usage.completionTokens,
693
- billedMicros: usage.billedMicros
694
- });
1103
+ this.recordReconciledInference(route, endpoint, requestId, usage, this.parseSellerSettlementSummary(upstreamResponse.headers), this.inferPromptForHash(body), responseBody);
695
1104
  return;
696
1105
  }
697
1106
  catch (routeError) {
@@ -867,7 +1276,9 @@ export class TokenbuddyDaemon {
867
1276
  const changes = previewProviderInstall({
868
1277
  providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
869
1278
  proxyUrl: String(req.body?.proxyUrl || ""),
870
- model: String(req.body?.model || ""),
1279
+ model: typeof req.body?.model === "string" ? req.body.model : undefined,
1280
+ providerSelections: req.body?.providerSelections,
1281
+ sellerRouting: req.body?.sellerRouting,
871
1282
  home: typeof req.body?.home === "string" ? req.body.home : undefined
872
1283
  });
873
1284
  logger.info("provider.install.previewed", "provider install previewed", {
@@ -894,7 +1305,9 @@ export class TokenbuddyDaemon {
894
1305
  const applied = applyProviderInstall({
895
1306
  providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
896
1307
  proxyUrl: String(req.body?.proxyUrl || ""),
897
- model: String(req.body?.model || ""),
1308
+ model: typeof req.body?.model === "string" ? req.body.model : undefined,
1309
+ providerSelections: req.body?.providerSelections,
1310
+ sellerRouting: req.body?.sellerRouting,
898
1311
  home: typeof req.body?.home === "string" ? req.body.home : undefined
899
1312
  }, this.tokenStore);
900
1313
  logger.info("provider.install.applied", "provider install applied", {