@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
package/dist/src/daemon.js
CHANGED
|
@@ -4,20 +4,220 @@ 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
|
+
class ResponsesStreamNormalizer {
|
|
12
|
+
pending = "";
|
|
13
|
+
state = new Map();
|
|
14
|
+
push(chunk) {
|
|
15
|
+
this.pending += chunk;
|
|
16
|
+
const blocks = this.pending.split("\n\n");
|
|
17
|
+
this.pending = blocks.pop() || "";
|
|
18
|
+
return blocks
|
|
19
|
+
.map((block) => this.normalizeBlock(block))
|
|
20
|
+
.filter((block) => block.length > 0)
|
|
21
|
+
.join("\n\n");
|
|
22
|
+
}
|
|
23
|
+
finish() {
|
|
24
|
+
if (!this.pending.trim()) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
const block = this.normalizeBlock(this.pending);
|
|
28
|
+
this.pending = "";
|
|
29
|
+
return block;
|
|
30
|
+
}
|
|
31
|
+
normalizeBlock(block) {
|
|
32
|
+
if (!block.trim()) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
// Each \n\n separates an event in SSE format
|
|
36
|
+
const subBlocks = block.split("\n\n");
|
|
37
|
+
const output = [];
|
|
38
|
+
for (const sub of subBlocks) {
|
|
39
|
+
if (!sub.trim() || sub.trim() === "data: [DONE]") {
|
|
40
|
+
if (sub.trim())
|
|
41
|
+
output.push(sub);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const lines = sub.split("\n");
|
|
45
|
+
const eventLine = lines.find((l) => l.startsWith("event:"));
|
|
46
|
+
const dataLine = lines.find((l) => l.startsWith("data:"));
|
|
47
|
+
if (!dataLine) {
|
|
48
|
+
output.push(sub);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const rawData = dataLine.replace(/^data:\s?/, "");
|
|
52
|
+
if (rawData === "[DONE]") {
|
|
53
|
+
output.push(sub);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
let payload;
|
|
57
|
+
try {
|
|
58
|
+
payload = JSON.parse(rawData);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
output.push(sub);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const eventName = (eventLine?.replace(/^event:\s?/, "") || payload?.type);
|
|
65
|
+
if (!eventName || !eventName.startsWith("response.")) {
|
|
66
|
+
output.push(sub);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// When upstream already sends content_part.added, record it in state
|
|
70
|
+
if (eventName === "response.content_part.added" &&
|
|
71
|
+
payload?.item_id) {
|
|
72
|
+
const current = this.state.get(payload.item_id);
|
|
73
|
+
if (current)
|
|
74
|
+
current.contentPartStarted = true;
|
|
75
|
+
output.push(sub);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// response.output_item.added: inject content_part.added only if upstream hasn't
|
|
79
|
+
if (eventName === "response.output_item.added" &&
|
|
80
|
+
payload?.item?.type === "message" &&
|
|
81
|
+
payload?.item?.id) {
|
|
82
|
+
const itemId = payload.item.id;
|
|
83
|
+
const current = this.getState(itemId);
|
|
84
|
+
const item = { ...payload.item };
|
|
85
|
+
item.content = [{ type: "output_text", text: "", annotations: [] }];
|
|
86
|
+
output.push(this.serializeEvent(eventName, {
|
|
87
|
+
...payload,
|
|
88
|
+
output_index: payload.output_index ?? 0,
|
|
89
|
+
item
|
|
90
|
+
}));
|
|
91
|
+
if (!current.contentPartStarted) {
|
|
92
|
+
current.contentPartStarted = true;
|
|
93
|
+
output.push(this.serializeEvent("response.content_part.added", {
|
|
94
|
+
type: "response.content_part.added",
|
|
95
|
+
item_id: itemId,
|
|
96
|
+
output_index: payload.output_index ?? 0,
|
|
97
|
+
content_index: 0,
|
|
98
|
+
part: { type: "output_text", text: "", annotations: [] }
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// response.output_text.delta: inject content_part.added if missing
|
|
104
|
+
if (eventName === "response.output_text.delta" && payload?.item_id) {
|
|
105
|
+
const itemId = payload.item_id;
|
|
106
|
+
const current = this.getState(itemId);
|
|
107
|
+
if (!current.contentPartStarted) {
|
|
108
|
+
current.contentPartStarted = true;
|
|
109
|
+
output.push(this.serializeEvent("response.content_part.added", {
|
|
110
|
+
type: "response.content_part.added",
|
|
111
|
+
item_id: itemId,
|
|
112
|
+
output_index: payload.output_index ?? 0,
|
|
113
|
+
content_index: payload.content_index ?? 0,
|
|
114
|
+
part: { type: "output_text", text: "", annotations: [] }
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
const deltaText = typeof payload.delta === "string"
|
|
118
|
+
? payload.delta
|
|
119
|
+
: typeof payload.delta?.text === "string"
|
|
120
|
+
? payload.delta.text
|
|
121
|
+
: "";
|
|
122
|
+
current.text += deltaText;
|
|
123
|
+
output.push(this.serializeEvent(eventName, {
|
|
124
|
+
...payload,
|
|
125
|
+
output_index: payload.output_index ?? 0,
|
|
126
|
+
content_index: payload.content_index ?? 0
|
|
127
|
+
}));
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// response.output_text.done: also emit content_part.done
|
|
131
|
+
if (eventName === "response.output_text.done" && payload?.item_id) {
|
|
132
|
+
const itemId = payload.item_id;
|
|
133
|
+
const current = this.getState(itemId);
|
|
134
|
+
output.push(this.serializeEvent(eventName, {
|
|
135
|
+
...payload,
|
|
136
|
+
output_index: payload.output_index ?? 0,
|
|
137
|
+
content_index: payload.content_index ?? 0
|
|
138
|
+
}));
|
|
139
|
+
output.push(this.serializeEvent("response.content_part.done", {
|
|
140
|
+
type: "response.content_part.done",
|
|
141
|
+
item_id: itemId,
|
|
142
|
+
output_index: payload.output_index ?? 0,
|
|
143
|
+
content_index: payload.content_index ?? 0,
|
|
144
|
+
part: { type: "output_text", text: current.text, annotations: [] }
|
|
145
|
+
}));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// response.output_item.done: normalize content to output_text type
|
|
149
|
+
if (eventName === "response.output_item.done" &&
|
|
150
|
+
payload?.item?.type === "message" &&
|
|
151
|
+
payload?.item?.id) {
|
|
152
|
+
const itemId = payload.item.id;
|
|
153
|
+
const current = this.getState(itemId);
|
|
154
|
+
const item = {
|
|
155
|
+
...payload.item,
|
|
156
|
+
content: [{ type: "output_text", text: current.text, annotations: [] }]
|
|
157
|
+
};
|
|
158
|
+
output.push(this.serializeEvent(eventName, {
|
|
159
|
+
...payload,
|
|
160
|
+
output_index: payload.output_index ?? 0,
|
|
161
|
+
item
|
|
162
|
+
}));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// response.completed: patch output if empty
|
|
166
|
+
if (eventName === "response.completed" && payload?.response) {
|
|
167
|
+
const response = { ...payload.response };
|
|
168
|
+
if (!Array.isArray(response.output) || response.output.length === 0) {
|
|
169
|
+
const first = this.state.values().next()
|
|
170
|
+
.value;
|
|
171
|
+
if (first) {
|
|
172
|
+
response.output = [{
|
|
173
|
+
id: first.itemId,
|
|
174
|
+
type: "message",
|
|
175
|
+
status: "completed",
|
|
176
|
+
role: "assistant",
|
|
177
|
+
content: [{ type: "output_text", text: first.text, annotations: [] }]
|
|
178
|
+
}];
|
|
179
|
+
response.output_text = first.text;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
output.push(this.serializeEvent(eventName, { ...payload, response }));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// All other events: pass through unchanged
|
|
186
|
+
output.push(sub);
|
|
187
|
+
}
|
|
188
|
+
return output.join("\n\n");
|
|
189
|
+
}
|
|
190
|
+
getState(itemId) {
|
|
191
|
+
const current = this.state.get(itemId);
|
|
192
|
+
if (current)
|
|
193
|
+
return current;
|
|
194
|
+
const created = { itemId, text: "", contentPartStarted: false };
|
|
195
|
+
this.state.set(itemId, created);
|
|
196
|
+
return created;
|
|
197
|
+
}
|
|
198
|
+
serializeEvent(name, data) {
|
|
199
|
+
return `event: ${name}\ndata: ${JSON.stringify(data)}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
10
202
|
export class TokenbuddyDaemon {
|
|
11
203
|
config;
|
|
12
204
|
tokenStore;
|
|
13
205
|
controlServer;
|
|
14
206
|
proxyServer;
|
|
15
207
|
selectionMode;
|
|
208
|
+
selectedSellerId;
|
|
16
209
|
activePurchases = new Map();
|
|
17
210
|
constructor(config) {
|
|
18
|
-
this.config = config;
|
|
19
|
-
this.selectionMode = config.selectionMode || "auto";
|
|
20
211
|
this.tokenStore = new BuyerStore({ dbPath: config.dbPath });
|
|
212
|
+
const routingPreference = this.tokenStore.getDaemonRuntimeConfig("routing")
|
|
213
|
+
?.config;
|
|
214
|
+
this.config = config;
|
|
215
|
+
this.selectionMode =
|
|
216
|
+
config.selectionMode ||
|
|
217
|
+
(routingPreference?.mode === "fixed" ? "manual" : routingPreference?.mode) ||
|
|
218
|
+
"auto";
|
|
219
|
+
this.selectedSellerId =
|
|
220
|
+
config.selectedSellerId || routingPreference?.sellerId;
|
|
21
221
|
}
|
|
22
222
|
activeControlPort() {
|
|
23
223
|
const address = this.controlServer?.address?.();
|
|
@@ -28,39 +228,23 @@ export class TokenbuddyDaemon {
|
|
|
28
228
|
return typeof address === "object" && address ? address.port : this.config.proxyPort;
|
|
29
229
|
}
|
|
30
230
|
async fetchRegistry() {
|
|
31
|
-
|
|
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();
|
|
231
|
+
return await fetchSellerRegistry(this.config.sellerRegistryUrl);
|
|
48
232
|
}
|
|
49
233
|
runtimeSummary() {
|
|
234
|
+
const sellerRoutingMode = this.selectedSellerId ? "fixed" : this.selectionMode;
|
|
50
235
|
return {
|
|
51
236
|
status: "running",
|
|
52
237
|
pid: process.pid,
|
|
53
238
|
controlPort: this.activeControlPort(),
|
|
54
239
|
proxyPort: this.activeProxyPort(),
|
|
55
240
|
selectionMode: this.selectionMode,
|
|
241
|
+
sellerRoutingMode,
|
|
242
|
+
selectedSellerId: this.selectedSellerId,
|
|
56
243
|
dbPath: this.config.dbPath,
|
|
57
244
|
sellerRegistryUrl: this.config.sellerRegistryUrl,
|
|
58
245
|
store: this.tokenStore.summary()
|
|
59
246
|
};
|
|
60
247
|
}
|
|
61
|
-
normalizeSellerUrl(seller) {
|
|
62
|
-
return seller.url.replace(/\/+$/, "");
|
|
63
|
-
}
|
|
64
248
|
endpointProtocol(endpoint) {
|
|
65
249
|
if (endpoint === "/v1/chat/completions") {
|
|
66
250
|
return "chat_completions";
|
|
@@ -84,17 +268,55 @@ export class TokenbuddyDaemon {
|
|
|
84
268
|
}
|
|
85
269
|
return undefined;
|
|
86
270
|
}
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
return
|
|
90
|
-
?
|
|
91
|
-
:
|
|
271
|
+
stripLocalClaudeOneMMarker(modelId) {
|
|
272
|
+
const trimmed = modelId.trimEnd();
|
|
273
|
+
return trimmed.toLowerCase().endsWith("[1m]")
|
|
274
|
+
? trimmed.slice(0, -4).trimEnd()
|
|
275
|
+
: trimmed;
|
|
92
276
|
}
|
|
93
|
-
|
|
94
|
-
|
|
277
|
+
resolveClaudeRoleModel(modelId) {
|
|
278
|
+
const runtimeConfig = this.tokenStore.getProviderRuntimeConfig("claude-code")?.config;
|
|
279
|
+
const stripped = this.stripLocalClaudeOneMMarker(modelId);
|
|
280
|
+
if (!runtimeConfig || runtimeConfig.selectionKind !== "claude-role-mapping") {
|
|
281
|
+
return stripped;
|
|
282
|
+
}
|
|
283
|
+
const lowered = stripped.toLowerCase();
|
|
284
|
+
if (lowered === "haiku" || lowered.startsWith("claude-haiku")) {
|
|
285
|
+
return runtimeConfig.roles.haiku?.upstreamModel || runtimeConfig.fallbackModel || stripped;
|
|
286
|
+
}
|
|
287
|
+
if (lowered === "sonnet" || lowered.startsWith("claude-sonnet")) {
|
|
288
|
+
return runtimeConfig.roles.sonnet?.upstreamModel || runtimeConfig.fallbackModel || stripped;
|
|
289
|
+
}
|
|
290
|
+
if (lowered === "opus" || lowered.startsWith("claude-opus")) {
|
|
291
|
+
return runtimeConfig.roles.opus?.upstreamModel || runtimeConfig.fallbackModel || stripped;
|
|
292
|
+
}
|
|
293
|
+
return runtimeConfig.fallbackModel || stripped;
|
|
294
|
+
}
|
|
295
|
+
resolveRouteModelId(endpoint, body) {
|
|
296
|
+
const requestedModelId = this.extractModelId(endpoint, body);
|
|
297
|
+
if (!requestedModelId) {
|
|
298
|
+
return {};
|
|
299
|
+
}
|
|
300
|
+
if (this.endpointProtocol(endpoint) === "messages") {
|
|
301
|
+
return {
|
|
302
|
+
requestedModelId,
|
|
303
|
+
resolvedModelId: this.resolveClaudeRoleModel(requestedModelId)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
requestedModelId,
|
|
308
|
+
resolvedModelId: requestedModelId
|
|
309
|
+
};
|
|
95
310
|
}
|
|
96
|
-
|
|
97
|
-
|
|
311
|
+
applyResolvedModelToBody(endpoint, body, resolvedModelId) {
|
|
312
|
+
const nextBody = { ...body };
|
|
313
|
+
if ("model" in nextBody) {
|
|
314
|
+
nextBody.model = resolvedModelId;
|
|
315
|
+
}
|
|
316
|
+
if (endpoint === "/v1/responses" && "model_id" in nextBody) {
|
|
317
|
+
nextBody.model_id = resolvedModelId;
|
|
318
|
+
}
|
|
319
|
+
return nextBody;
|
|
98
320
|
}
|
|
99
321
|
defaultPaymentMethod() {
|
|
100
322
|
const payments = this.tokenStore.listPayments().filter((payment) => payment.enabled);
|
|
@@ -112,12 +334,15 @@ export class TokenbuddyDaemon {
|
|
|
112
334
|
const registry = await this.fetchRegistry();
|
|
113
335
|
const defaultSellers = registry.sellers.filter((seller) => seller.id === registry.defaultSeller);
|
|
114
336
|
const backupSellers = registry.sellers.filter((seller) => seller.id !== registry.defaultSeller);
|
|
115
|
-
const
|
|
337
|
+
const manualSellers = this.selectedSellerId
|
|
338
|
+
? registry.sellers.filter((seller) => seller.id === this.selectedSellerId)
|
|
339
|
+
: defaultSellers;
|
|
340
|
+
const sellers = this.selectionMode === "manual" ? manualSellers : [...defaultSellers, ...backupSellers];
|
|
116
341
|
const routes = [];
|
|
117
342
|
for (const seller of sellers) {
|
|
118
343
|
let manifest;
|
|
119
344
|
try {
|
|
120
|
-
manifest = await
|
|
345
|
+
manifest = await fetchSellerManifest(seller);
|
|
121
346
|
}
|
|
122
347
|
catch (error) {
|
|
123
348
|
logger.warn("route.manifest.failed", "seller manifest unavailable during route selection", {
|
|
@@ -128,9 +353,9 @@ export class TokenbuddyDaemon {
|
|
|
128
353
|
});
|
|
129
354
|
continue;
|
|
130
355
|
}
|
|
131
|
-
const protocols =
|
|
132
|
-
const paymentMethods =
|
|
133
|
-
const modelIds =
|
|
356
|
+
const protocols = manifestProtocols(manifest, seller);
|
|
357
|
+
const paymentMethods = manifestPaymentMethods(manifest, seller);
|
|
358
|
+
const modelIds = manifestModelIds(manifest);
|
|
134
359
|
if (!protocols.includes(protocol) || !paymentMethods.includes(paymentMethod) || !modelIds.includes(modelId)) {
|
|
135
360
|
continue;
|
|
136
361
|
}
|
|
@@ -190,52 +415,10 @@ export class TokenbuddyDaemon {
|
|
|
190
415
|
return route;
|
|
191
416
|
}
|
|
192
417
|
async listSellerBackedModels() {
|
|
193
|
-
const
|
|
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
|
-
}));
|
|
418
|
+
const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
|
|
236
419
|
return {
|
|
237
|
-
models:
|
|
238
|
-
sellers:
|
|
420
|
+
models: catalog.models,
|
|
421
|
+
sellers: catalog.sellers
|
|
239
422
|
};
|
|
240
423
|
}
|
|
241
424
|
readUsage(bodyText) {
|
|
@@ -286,7 +469,7 @@ export class TokenbuddyDaemon {
|
|
|
286
469
|
}
|
|
287
470
|
async getOrPurchaseToken(route) {
|
|
288
471
|
const sellerKey = route.seller.id;
|
|
289
|
-
const sellerUrl =
|
|
472
|
+
const sellerUrl = normalizeSellerUrl(route.seller);
|
|
290
473
|
const { modelId, paymentMethod } = route;
|
|
291
474
|
const cached = this.tokenStore.getToken(sellerKey);
|
|
292
475
|
const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
|
|
@@ -461,7 +644,7 @@ export class TokenbuddyDaemon {
|
|
|
461
644
|
const timeoutMs = this.clawtipProofTimeoutMs();
|
|
462
645
|
const payload = JSON.stringify({
|
|
463
646
|
sellerKey: route.seller.id,
|
|
464
|
-
sellerUrl:
|
|
647
|
+
sellerUrl: normalizeSellerUrl(route.seller),
|
|
465
648
|
modelId: route.modelId,
|
|
466
649
|
purchase: createData,
|
|
467
650
|
paymentInstructions: createData.paymentInstructions || createData.payment_instructions,
|
|
@@ -542,7 +725,7 @@ export class TokenbuddyDaemon {
|
|
|
542
725
|
copyUpstreamHeaders(upstreamResponse, res) {
|
|
543
726
|
upstreamResponse.headers.forEach((value, key) => {
|
|
544
727
|
const lowerKey = key.toLowerCase();
|
|
545
|
-
if (["connection", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
|
|
728
|
+
if (["connection", "content-encoding", "content-length", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
|
|
546
729
|
return;
|
|
547
730
|
}
|
|
548
731
|
res.setHeader(key, value);
|
|
@@ -551,7 +734,8 @@ export class TokenbuddyDaemon {
|
|
|
551
734
|
async forwardProxyRequest(endpoint, req, res) {
|
|
552
735
|
const startedAt = Date.now();
|
|
553
736
|
const body = req.body || {};
|
|
554
|
-
const
|
|
737
|
+
const { requestedModelId, resolvedModelId } = this.resolveRouteModelId(endpoint, body);
|
|
738
|
+
const modelId = resolvedModelId;
|
|
555
739
|
const requestId = req.header("x-request-id") || (body && typeof body === "object" ? body.requestId : undefined) || `proxy_req_${crypto.randomBytes(8).toString("hex")}`;
|
|
556
740
|
const idempotencyKey = req.header("idempotency-key") || `idem_${crypto.randomBytes(12).toString("hex")}`;
|
|
557
741
|
if (!modelId) {
|
|
@@ -570,15 +754,16 @@ export class TokenbuddyDaemon {
|
|
|
570
754
|
requestId,
|
|
571
755
|
sellerKey,
|
|
572
756
|
model: modelId,
|
|
757
|
+
requestedModel: requestedModelId,
|
|
573
758
|
endpoint,
|
|
574
759
|
stream: Boolean(body.stream)
|
|
575
760
|
});
|
|
576
761
|
const token = await this.getOrPurchaseToken(route);
|
|
577
|
-
const sellerUrl =
|
|
578
|
-
const upstreamBody = {
|
|
762
|
+
const sellerUrl = normalizeSellerUrl(route.seller);
|
|
763
|
+
const upstreamBody = this.applyResolvedModelToBody(endpoint, {
|
|
579
764
|
...body,
|
|
580
765
|
requestId
|
|
581
|
-
};
|
|
766
|
+
}, modelId);
|
|
582
767
|
logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
|
|
583
768
|
requestId,
|
|
584
769
|
sellerKey,
|
|
@@ -634,13 +819,30 @@ export class TokenbuddyDaemon {
|
|
|
634
819
|
return;
|
|
635
820
|
}
|
|
636
821
|
let bytes = 0;
|
|
822
|
+
const decoder = new TextDecoder();
|
|
823
|
+
const responsesStreamNormalizer = new ResponsesStreamNormalizer();
|
|
637
824
|
while (true) {
|
|
638
825
|
const { done, value } = await reader.read();
|
|
639
826
|
if (done) {
|
|
640
827
|
break;
|
|
641
828
|
}
|
|
642
829
|
bytes += value.byteLength;
|
|
643
|
-
|
|
830
|
+
if (endpoint === "/v1/responses") {
|
|
831
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
832
|
+
const normalized = responsesStreamNormalizer.push(chunk);
|
|
833
|
+
if (normalized.length > 0) {
|
|
834
|
+
res.write(`${normalized}\n\n`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
res.write(Buffer.from(value));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (endpoint === "/v1/responses") {
|
|
842
|
+
const trailing = responsesStreamNormalizer.finish();
|
|
843
|
+
if (trailing.length > 0) {
|
|
844
|
+
res.write(`${trailing}\n\n`);
|
|
845
|
+
}
|
|
644
846
|
}
|
|
645
847
|
res.end();
|
|
646
848
|
const billedMicros = Math.max(1, bytes);
|
|
@@ -867,7 +1069,9 @@ export class TokenbuddyDaemon {
|
|
|
867
1069
|
const changes = previewProviderInstall({
|
|
868
1070
|
providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
|
|
869
1071
|
proxyUrl: String(req.body?.proxyUrl || ""),
|
|
870
|
-
model:
|
|
1072
|
+
model: typeof req.body?.model === "string" ? req.body.model : undefined,
|
|
1073
|
+
providerSelections: req.body?.providerSelections,
|
|
1074
|
+
sellerRouting: req.body?.sellerRouting,
|
|
871
1075
|
home: typeof req.body?.home === "string" ? req.body.home : undefined
|
|
872
1076
|
});
|
|
873
1077
|
logger.info("provider.install.previewed", "provider install previewed", {
|
|
@@ -894,7 +1098,9 @@ export class TokenbuddyDaemon {
|
|
|
894
1098
|
const applied = applyProviderInstall({
|
|
895
1099
|
providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
|
|
896
1100
|
proxyUrl: String(req.body?.proxyUrl || ""),
|
|
897
|
-
model:
|
|
1101
|
+
model: typeof req.body?.model === "string" ? req.body.model : undefined,
|
|
1102
|
+
providerSelections: req.body?.providerSelections,
|
|
1103
|
+
sellerRouting: req.body?.sellerRouting,
|
|
898
1104
|
home: typeof req.body?.home === "string" ? req.body.home : undefined
|
|
899
1105
|
}, this.tokenStore);
|
|
900
1106
|
logger.info("provider.install.applied", "provider install applied", {
|