@tokenbuddy/tokenbuddy 1.0.4 → 1.0.6

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 (42) hide show
  1. package/dist/src/buyer-store.d.ts +20 -0
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +73 -1
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts.map +1 -1
  6. package/dist/src/cli.js +390 -62
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/daemon.d.ts +6 -5
  9. package/dist/src/daemon.d.ts.map +1 -1
  10. package/dist/src/daemon.js +298 -92
  11. package/dist/src/daemon.js.map +1 -1
  12. package/dist/src/doctor-diagnostics.d.ts +97 -0
  13. package/dist/src/doctor-diagnostics.d.ts.map +1 -0
  14. package/dist/src/doctor-diagnostics.js +547 -0
  15. package/dist/src/doctor-diagnostics.js.map +1 -0
  16. package/dist/src/init-payment-options.d.ts +34 -0
  17. package/dist/src/init-payment-options.d.ts.map +1 -0
  18. package/dist/src/init-payment-options.js +90 -0
  19. package/dist/src/init-payment-options.js.map +1 -0
  20. package/dist/src/provider-install.d.ts +37 -2
  21. package/dist/src/provider-install.d.ts.map +1 -1
  22. package/dist/src/provider-install.js +317 -67
  23. package/dist/src/provider-install.js.map +1 -1
  24. package/dist/src/seller-catalog.d.ts +79 -0
  25. package/dist/src/seller-catalog.d.ts.map +1 -0
  26. package/dist/src/seller-catalog.js +126 -0
  27. package/dist/src/seller-catalog.js.map +1 -0
  28. package/dist/src/tb-proxyd.js +13 -2
  29. package/dist/src/tb-proxyd.js.map +1 -1
  30. package/package.json +4 -4
  31. package/src/buyer-store.ts +113 -1
  32. package/src/cli.ts +490 -67
  33. package/src/daemon.ts +346 -117
  34. package/src/doctor-diagnostics.ts +850 -0
  35. package/src/init-payment-options.ts +131 -0
  36. package/src/provider-install.ts +426 -76
  37. package/src/seller-catalog.ts +222 -0
  38. package/src/tb-proxyd.ts +14 -2
  39. package/tests/e2e.test.ts +9 -0
  40. package/tests/tokenbuddy.test.ts +628 -19
  41. package/bin/tb-proxyd.js +0 -2
  42. package/bin/tb.js +0 -3
@@ -0,0 +1,222 @@
1
+ import { createModuleLogger } from "@tokenbuddy/logging";
2
+
3
+ const logger = createModuleLogger("tb-proxyd");
4
+
5
+ export type ProtocolPreference = "chat_completions" | "responses" | "messages";
6
+
7
+ export interface RegistrySeller {
8
+ id: string;
9
+ name?: string;
10
+ url: string;
11
+ supportedProtocols?: string[];
12
+ paymentMethods?: string[];
13
+ }
14
+
15
+ export interface SellerRegistryDocument {
16
+ version: number;
17
+ defaultSeller?: string;
18
+ sellers: RegistrySeller[];
19
+ }
20
+
21
+ export interface SellerManifest {
22
+ sellerId?: string;
23
+ seller_id?: string;
24
+ supportedProtocols?: string[];
25
+ supported_protocols?: string[];
26
+ paymentMethods?: string[];
27
+ payment_methods?: string[];
28
+ models?: ManifestModelRecord[];
29
+ selection?: {
30
+ discountRatio?: number;
31
+ discount_ratio?: number;
32
+ };
33
+ }
34
+
35
+ export interface ManifestModelRecord {
36
+ id: string;
37
+ inputPriceMicrosPer1m?: number;
38
+ outputPriceMicrosPer1m?: number;
39
+ input_price_micros_per_1m?: number;
40
+ output_price_micros_per_1m?: number;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export interface ModelCatalogEntry {
45
+ id: string;
46
+ sellerId: string;
47
+ sellerName?: string;
48
+ sellerUrl: string;
49
+ supportedProtocols: string[];
50
+ paymentMethods: string[];
51
+ inputPriceMicrosPer1m?: number;
52
+ outputPriceMicrosPer1m?: number;
53
+ }
54
+
55
+ export interface SellerCatalogEntry {
56
+ id: string;
57
+ name?: string;
58
+ url: string;
59
+ status: string;
60
+ manifestSellerId?: string;
61
+ discountRatio?: number;
62
+ modelCount?: number;
63
+ supportedProtocols?: string[];
64
+ paymentMethods?: string[];
65
+ errorMessage?: string;
66
+ }
67
+
68
+ export interface SellerCatalogResult {
69
+ registryUrl: string;
70
+ version: number;
71
+ defaultSeller?: string;
72
+ models: ModelCatalogEntry[];
73
+ sellers: SellerCatalogEntry[];
74
+ }
75
+
76
+ export type SellerRoutingMode = "auto" | "fixed";
77
+
78
+ export interface SellerRoutingPreference {
79
+ mode: SellerRoutingMode;
80
+ sellerId?: string;
81
+ }
82
+
83
+ export function normalizeSellerUrl(seller: RegistrySeller): string {
84
+ return seller.url.replace(/\/+$/, "");
85
+ }
86
+
87
+ export function manifestProtocols(manifest: SellerManifest, seller: RegistrySeller): string[] {
88
+ const protocols = manifest.supportedProtocols || manifest.supported_protocols || seller.supportedProtocols || [];
89
+ return protocols.includes("anthropic_messages") && !protocols.includes("messages")
90
+ ? [...protocols, "messages"]
91
+ : protocols;
92
+ }
93
+
94
+ export function manifestPaymentMethods(manifest: SellerManifest, seller: RegistrySeller): string[] {
95
+ return manifest.paymentMethods || manifest.payment_methods || seller.paymentMethods || [];
96
+ }
97
+
98
+ export function manifestModelIds(manifest: SellerManifest): string[] {
99
+ return (manifest.models || [])
100
+ .map((model) => model.id)
101
+ .filter((id): id is string => typeof id === "string" && id.trim().length > 0)
102
+ .map((id) => id.trim());
103
+ }
104
+
105
+ function numericPriceField(value: unknown): number | undefined {
106
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
107
+ }
108
+
109
+ function manifestModels(manifest: SellerManifest): ManifestModelRecord[] {
110
+ return (manifest.models || [])
111
+ .filter((model): model is ManifestModelRecord => Boolean(model?.id && typeof model.id === "string"));
112
+ }
113
+
114
+ export async function fetchSellerRegistry(registryUrl: string): Promise<SellerRegistryDocument> {
115
+ const response = await fetch(registryUrl);
116
+ if (!response.ok) {
117
+ throw new Error(`registry returned ${response.status}`);
118
+ }
119
+ const data = await response.json() as SellerRegistryDocument;
120
+ if (!data || !Array.isArray(data.sellers)) {
121
+ throw new Error("registry response missing sellers");
122
+ }
123
+ return data;
124
+ }
125
+
126
+ export async function fetchSellerManifest(seller: RegistrySeller): Promise<SellerManifest> {
127
+ const response = await fetch(`${normalizeSellerUrl(seller)}/manifest`);
128
+ if (!response.ok) {
129
+ throw new Error(`manifest returned ${response.status}`);
130
+ }
131
+ return await response.json() as SellerManifest;
132
+ }
133
+
134
+ export async function discoverSellerBackedModels(registryUrl: string): Promise<SellerCatalogResult> {
135
+ const registry = await fetchSellerRegistry(registryUrl);
136
+ const sellerResults = await Promise.all(registry.sellers.map(async (seller) => {
137
+ try {
138
+ const manifest = await fetchSellerManifest(seller);
139
+ const protocols = manifestProtocols(manifest, seller);
140
+ const paymentMethods = manifestPaymentMethods(manifest, seller);
141
+ const models = manifestModels(manifest).map((model) => ({
142
+ id: model.id.trim(),
143
+ sellerId: seller.id,
144
+ sellerName: seller.name,
145
+ sellerUrl: seller.url,
146
+ supportedProtocols: protocols,
147
+ paymentMethods,
148
+ inputPriceMicrosPer1m: numericPriceField(model.inputPriceMicrosPer1m) ?? numericPriceField(model.input_price_micros_per_1m),
149
+ outputPriceMicrosPer1m: numericPriceField(model.outputPriceMicrosPer1m) ?? numericPriceField(model.output_price_micros_per_1m),
150
+ }));
151
+ return {
152
+ seller: {
153
+ id: seller.id,
154
+ name: seller.name,
155
+ url: seller.url,
156
+ status: "ok",
157
+ manifestSellerId: manifest.sellerId || manifest.seller_id || seller.id,
158
+ discountRatio: manifest.selection?.discountRatio ?? manifest.selection?.discount_ratio,
159
+ modelCount: models.length,
160
+ supportedProtocols: protocols,
161
+ paymentMethods,
162
+ },
163
+ models
164
+ };
165
+ } catch (error: unknown) {
166
+ const errorMessage = error instanceof Error ? error.message : String(error);
167
+ logger.warn("models.refresh.seller_failed", "seller manifest refresh failed", {
168
+ sellerId: seller.id,
169
+ errorMessage
170
+ });
171
+ return {
172
+ seller: {
173
+ id: seller.id,
174
+ name: seller.name,
175
+ url: seller.url,
176
+ status: "failed",
177
+ errorMessage
178
+ },
179
+ models: [] as ModelCatalogEntry[]
180
+ };
181
+ }
182
+ }));
183
+
184
+ return {
185
+ registryUrl,
186
+ version: registry.version,
187
+ defaultSeller: registry.defaultSeller,
188
+ models: sellerResults.flatMap((entry) => entry.models),
189
+ sellers: sellerResults.map((entry) => entry.seller)
190
+ };
191
+ }
192
+
193
+ export function filterCatalogByProtocol(
194
+ models: ModelCatalogEntry[],
195
+ protocol: ProtocolPreference
196
+ ): ModelCatalogEntry[] {
197
+ return models.filter((entry) => entry.supportedProtocols.includes(protocol));
198
+ }
199
+
200
+ export function filterCatalogBySeller(
201
+ models: ModelCatalogEntry[],
202
+ sellerId: string | undefined
203
+ ): ModelCatalogEntry[] {
204
+ if (!sellerId) {
205
+ return models;
206
+ }
207
+ return models.filter((entry) => entry.sellerId === sellerId);
208
+ }
209
+
210
+ export function dedupeCatalogEntries(models: ModelCatalogEntry[]): ModelCatalogEntry[] {
211
+ const seen = new Set<string>();
212
+ const output: ModelCatalogEntry[] = [];
213
+ for (const entry of models) {
214
+ const key = `${entry.sellerId}:${entry.id}`;
215
+ if (seen.has(key)) {
216
+ continue;
217
+ }
218
+ seen.add(key);
219
+ output.push(entry);
220
+ }
221
+ return output;
222
+ }
package/src/tb-proxyd.ts CHANGED
@@ -9,13 +9,15 @@ const controlPort = parsePortEnv("TB_PROXYD_CONTROL_PORT", 17820);
9
9
  const proxyPort = parsePortEnv("TB_PROXYD_PROXY_PORT", 17821);
10
10
  const sellerRegistryUrl = process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
11
11
  const selectionMode = parseSelectionModeEnv();
12
+ const selectedSellerId = parseSelectedSellerIdEnv();
12
13
 
13
14
  const daemon = new TokenbuddyDaemon({
14
15
  controlPort,
15
16
  proxyPort,
16
17
  dbPath,
17
18
  sellerRegistryUrl,
18
- selectionMode
19
+ selectionMode,
20
+ selectedSellerId
19
21
  });
20
22
 
21
23
  logger.info("proxy.process.initializing", "tb-proxyd process initializing", {
@@ -23,7 +25,8 @@ logger.info("proxy.process.initializing", "tb-proxyd process initializing", {
23
25
  controlPort,
24
26
  proxyPort,
25
27
  sellerRegistryUrl,
26
- selectionMode
28
+ selectionMode,
29
+ selectedSellerId
27
30
  });
28
31
  daemon.start();
29
32
 
@@ -58,3 +61,12 @@ function parseSelectionModeEnv(): "auto" | "manual" {
58
61
  }
59
62
  throw new Error("TB_PROXYD_SELECTION_MODE must be auto or manual");
60
63
  }
64
+
65
+ function parseSelectedSellerIdEnv(): string | undefined {
66
+ const rawValue = process.env.TB_PROXYD_SELECTED_SELLER_ID;
67
+ if (!rawValue) {
68
+ return undefined;
69
+ }
70
+ const trimmed = rawValue.trim();
71
+ return trimmed || undefined;
72
+ }
package/tests/e2e.test.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { buildApp as buildBootstrapApp } from "../../wallet-bootstrap/src/server.js";
2
2
  import { buildSellerApp } from "../../seller/src/server.js";
3
3
  import { TokenbuddyDaemon } from "../src/daemon.js";
4
+ import { BuyerStore } from "../src/buyer-store.js";
4
5
  import { AdminClient } from "../../admin-cli/src/client.js";
5
6
  import * as path from "path";
6
7
  import * as fs from "fs";
@@ -39,6 +40,14 @@ describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
39
40
  if (fs.existsSync(TEMP_BUYER_DB)) {
40
41
  try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
41
42
  }
43
+ const buyerStore = new BuyerStore({ dbPath: TEMP_BUYER_DB });
44
+ buyerStore.savePayment({
45
+ method: "mock",
46
+ enabled: true,
47
+ isDefault: true,
48
+ config: { channel: "e2e-test" }
49
+ });
50
+ buyerStore.close();
42
51
 
43
52
  // 1. Launch Mock Upstream OpenAI completions server
44
53
  upstreamServer = http.createServer((req, res) => {