@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.
- package/dist/src/buyer-store.d.ts +20 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +73 -1
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +390 -62
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +6 -5
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +298 -92
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +97 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -0
- package/dist/src/doctor-diagnostics.js +547 -0
- package/dist/src/doctor-diagnostics.js.map +1 -0
- package/dist/src/init-payment-options.d.ts +34 -0
- package/dist/src/init-payment-options.d.ts.map +1 -0
- package/dist/src/init-payment-options.js +90 -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/package.json +4 -4
- package/src/buyer-store.ts +113 -1
- package/src/cli.ts +490 -67
- package/src/daemon.ts +346 -117
- package/src/doctor-diagnostics.ts +850 -0
- package/src/init-payment-options.ts +131 -0
- package/src/provider-install.ts +426 -76
- package/src/seller-catalog.ts +222 -0
- package/src/tb-proxyd.ts +14 -2
- package/tests/e2e.test.ts +9 -0
- package/tests/tokenbuddy.test.ts +628 -19
- package/bin/tb-proxyd.js +0 -2
- 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) => {
|