@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
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,225 @@ interface UsageSummary {
61
52
  billedMicros: number;
62
53
  }
63
54
 
55
+ interface ResponsesStreamState {
56
+ itemId: string;
57
+ text: string;
58
+ contentPartStarted: boolean;
59
+ }
60
+
61
+ class ResponsesStreamNormalizer {
62
+ private pending = "";
63
+ private readonly state = new Map<string, ResponsesStreamState>();
64
+
65
+ public push(chunk: string): string {
66
+ this.pending += chunk;
67
+ const blocks = this.pending.split("\n\n");
68
+ this.pending = blocks.pop() || "";
69
+ return blocks
70
+ .map((block) => this.normalizeBlock(block))
71
+ .filter((block) => block.length > 0)
72
+ .join("\n\n");
73
+ }
74
+
75
+ public finish(): string {
76
+ if (!this.pending.trim()) {
77
+ return "";
78
+ }
79
+ const block = this.normalizeBlock(this.pending);
80
+ this.pending = "";
81
+ return block;
82
+ }
83
+
84
+ private normalizeBlock(block: string): string {
85
+ if (!block.trim()) {
86
+ return "";
87
+ }
88
+ // Each \n\n separates an event in SSE format
89
+ const subBlocks = block.split("\n\n");
90
+ const output: string[] = [];
91
+
92
+ for (const sub of subBlocks) {
93
+ if (!sub.trim() || sub.trim() === "data: [DONE]") {
94
+ if (sub.trim()) output.push(sub);
95
+ continue;
96
+ }
97
+
98
+ const lines = sub.split("\n");
99
+ const eventLine = lines.find((l) => l.startsWith("event:"));
100
+ const dataLine = lines.find((l) => l.startsWith("data:"));
101
+ if (!dataLine) {
102
+ output.push(sub);
103
+ continue;
104
+ }
105
+ const rawData = dataLine.replace(/^data:\s?/, "");
106
+ if (rawData === "[DONE]") {
107
+ output.push(sub);
108
+ continue;
109
+ }
110
+
111
+ let payload: any;
112
+ try {
113
+ payload = JSON.parse(rawData);
114
+ } catch {
115
+ output.push(sub);
116
+ continue;
117
+ }
118
+
119
+ const eventName =
120
+ (eventLine?.replace(/^event:\s?/, "") || payload?.type) as string;
121
+ if (!eventName || !eventName.startsWith("response.")) {
122
+ output.push(sub);
123
+ continue;
124
+ }
125
+
126
+ // When upstream already sends content_part.added, record it in state
127
+ if (
128
+ eventName === "response.content_part.added" &&
129
+ payload?.item_id
130
+ ) {
131
+ const current = this.state.get(payload.item_id as string);
132
+ if (current) current.contentPartStarted = true;
133
+ output.push(sub);
134
+ continue;
135
+ }
136
+
137
+ // response.output_item.added: inject content_part.added only if upstream hasn't
138
+ if (
139
+ eventName === "response.output_item.added" &&
140
+ payload?.item?.type === "message" &&
141
+ payload?.item?.id
142
+ ) {
143
+ const itemId = payload.item.id as string;
144
+ const current = this.getState(itemId);
145
+ const item = { ...payload.item };
146
+ item.content = [{ type: "output_text", text: "", annotations: [] }];
147
+ output.push(this.serializeEvent(eventName, {
148
+ ...payload,
149
+ output_index: payload.output_index ?? 0,
150
+ item
151
+ }));
152
+ if (!current.contentPartStarted) {
153
+ current.contentPartStarted = true;
154
+ output.push(this.serializeEvent("response.content_part.added", {
155
+ type: "response.content_part.added",
156
+ item_id: itemId,
157
+ output_index: payload.output_index ?? 0,
158
+ content_index: 0,
159
+ part: { type: "output_text", text: "", annotations: [] }
160
+ }));
161
+ }
162
+ continue;
163
+ }
164
+
165
+ // response.output_text.delta: inject content_part.added if missing
166
+ if (eventName === "response.output_text.delta" && payload?.item_id) {
167
+ const itemId = payload.item_id as string;
168
+ const current = this.getState(itemId);
169
+ if (!current.contentPartStarted) {
170
+ current.contentPartStarted = true;
171
+ output.push(this.serializeEvent("response.content_part.added", {
172
+ type: "response.content_part.added",
173
+ item_id: itemId,
174
+ output_index: payload.output_index ?? 0,
175
+ content_index: payload.content_index ?? 0,
176
+ part: { type: "output_text", text: "", annotations: [] }
177
+ }));
178
+ }
179
+ const deltaText =
180
+ typeof payload.delta === "string"
181
+ ? payload.delta
182
+ : typeof payload.delta?.text === "string"
183
+ ? payload.delta.text
184
+ : "";
185
+ current.text += deltaText;
186
+ output.push(this.serializeEvent(eventName, {
187
+ ...payload,
188
+ output_index: payload.output_index ?? 0,
189
+ content_index: payload.content_index ?? 0
190
+ }));
191
+ continue;
192
+ }
193
+
194
+ // response.output_text.done: also emit content_part.done
195
+ if (eventName === "response.output_text.done" && payload?.item_id) {
196
+ const itemId = payload.item_id as string;
197
+ const current = this.getState(itemId);
198
+ output.push(this.serializeEvent(eventName, {
199
+ ...payload,
200
+ output_index: payload.output_index ?? 0,
201
+ content_index: payload.content_index ?? 0
202
+ }));
203
+ output.push(this.serializeEvent("response.content_part.done", {
204
+ type: "response.content_part.done",
205
+ item_id: itemId,
206
+ output_index: payload.output_index ?? 0,
207
+ content_index: payload.content_index ?? 0,
208
+ part: { type: "output_text", text: current.text, annotations: [] }
209
+ }));
210
+ continue;
211
+ }
212
+
213
+ // response.output_item.done: normalize content to output_text type
214
+ if (
215
+ eventName === "response.output_item.done" &&
216
+ payload?.item?.type === "message" &&
217
+ payload?.item?.id
218
+ ) {
219
+ const itemId = payload.item.id as string;
220
+ const current = this.getState(itemId);
221
+ const item = {
222
+ ...payload.item,
223
+ content: [{ type: "output_text", text: current.text, annotations: [] }]
224
+ };
225
+ output.push(this.serializeEvent(eventName, {
226
+ ...payload,
227
+ output_index: payload.output_index ?? 0,
228
+ item
229
+ }));
230
+ continue;
231
+ }
232
+
233
+ // response.completed: patch output if empty
234
+ if (eventName === "response.completed" && payload?.response) {
235
+ const response = { ...payload.response };
236
+ if (!Array.isArray(response.output) || response.output.length === 0) {
237
+ const first = this.state.values().next()
238
+ .value as ResponsesStreamState | undefined;
239
+ if (first) {
240
+ response.output = [{
241
+ id: first.itemId,
242
+ type: "message",
243
+ status: "completed",
244
+ role: "assistant",
245
+ content: [{ type: "output_text", text: first.text, annotations: [] }]
246
+ }];
247
+ response.output_text = first.text;
248
+ }
249
+ }
250
+ output.push(this.serializeEvent(eventName, { ...payload, response }));
251
+ continue;
252
+ }
253
+
254
+ // All other events: pass through unchanged
255
+ output.push(sub);
256
+ }
257
+
258
+ return output.join("\n\n");
259
+ }
260
+
261
+ private getState(itemId: string): ResponsesStreamState {
262
+ const current = this.state.get(itemId);
263
+ if (current) return current;
264
+ const created = { itemId, text: "", contentPartStarted: false };
265
+ this.state.set(itemId, created);
266
+ return created;
267
+ }
268
+
269
+ private serializeEvent(name: string, data: any): string {
270
+ return `event: ${name}\ndata: ${JSON.stringify(data)}`;
271
+ }
272
+ }
273
+
64
274
  interface PurchaseCreateResponse {
65
275
  purchaseId?: string;
66
276
  purchase_id?: string;
@@ -91,13 +301,22 @@ export class TokenbuddyDaemon {
91
301
  private controlServer?: any;
92
302
  private proxyServer?: any;
93
303
  private selectionMode: "auto" | "manual";
304
+ private selectedSellerId?: string;
94
305
 
95
306
  private activePurchases = new Map<string, Promise<string>>();
96
307
 
97
308
  constructor(config: DaemonConfig) {
98
- this.config = config;
99
- this.selectionMode = config.selectionMode || "auto";
100
309
  this.tokenStore = new BuyerStore({ dbPath: config.dbPath });
310
+ const routingPreference =
311
+ this.tokenStore.getDaemonRuntimeConfig<SellerRoutingPreference>("routing")
312
+ ?.config;
313
+ this.config = config;
314
+ this.selectionMode =
315
+ config.selectionMode ||
316
+ (routingPreference?.mode === "fixed" ? "manual" : routingPreference?.mode) ||
317
+ "auto";
318
+ this.selectedSellerId =
319
+ config.selectedSellerId || routingPreference?.sellerId;
101
320
  }
102
321
 
103
322
  private activeControlPort(): number {
@@ -111,43 +330,25 @@ export class TokenbuddyDaemon {
111
330
  }
112
331
 
113
332
  private async fetchRegistry(): Promise<SellerRegistryDocument> {
114
- const response = await fetch(this.config.sellerRegistryUrl);
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;
333
+ return await fetchSellerRegistry(this.config.sellerRegistryUrl);
132
334
  }
133
335
 
134
336
  private runtimeSummary() {
337
+ const sellerRoutingMode = this.selectedSellerId ? "fixed" : this.selectionMode;
135
338
  return {
136
339
  status: "running",
137
340
  pid: process.pid,
138
341
  controlPort: this.activeControlPort(),
139
342
  proxyPort: this.activeProxyPort(),
140
343
  selectionMode: this.selectionMode,
344
+ sellerRoutingMode,
345
+ selectedSellerId: this.selectedSellerId,
141
346
  dbPath: this.config.dbPath,
142
347
  sellerRegistryUrl: this.config.sellerRegistryUrl,
143
348
  store: this.tokenStore.summary()
144
349
  };
145
350
  }
146
351
 
147
- private normalizeSellerUrl(seller: RegistrySeller): string {
148
- return seller.url.replace(/\/+$/, "");
149
- }
150
-
151
352
  private endpointProtocol(endpoint: string): string | undefined {
152
353
  if (endpoint === "/v1/chat/completions") {
153
354
  return "chat_completions";
@@ -173,19 +374,63 @@ export class TokenbuddyDaemon {
173
374
  return undefined;
174
375
  }
175
376
 
176
- private manifestProtocols(manifest: SellerManifest, seller: RegistrySeller): string[] {
177
- const protocols = manifest.supportedProtocols || manifest.supported_protocols || seller.supportedProtocols || [];
178
- return protocols.includes("anthropic_messages") && !protocols.includes("messages")
179
- ? [...protocols, "messages"]
180
- : protocols;
377
+ private stripLocalClaudeOneMMarker(modelId: string): string {
378
+ const trimmed = modelId.trimEnd();
379
+ return trimmed.toLowerCase().endsWith("[1m]")
380
+ ? trimmed.slice(0, -4).trimEnd()
381
+ : trimmed;
382
+ }
383
+
384
+ private resolveClaudeRoleModel(modelId: string): string {
385
+ const runtimeConfig = this.tokenStore.getProviderRuntimeConfig<ClaudeCodeModelMappingConfig>("claude-code")?.config;
386
+ const stripped = this.stripLocalClaudeOneMMarker(modelId);
387
+ if (!runtimeConfig || runtimeConfig.selectionKind !== "claude-role-mapping") {
388
+ return stripped;
389
+ }
390
+
391
+ const lowered = stripped.toLowerCase();
392
+ if (lowered === "haiku" || lowered.startsWith("claude-haiku")) {
393
+ return runtimeConfig.roles.haiku?.upstreamModel || runtimeConfig.fallbackModel || stripped;
394
+ }
395
+ if (lowered === "sonnet" || lowered.startsWith("claude-sonnet")) {
396
+ return runtimeConfig.roles.sonnet?.upstreamModel || runtimeConfig.fallbackModel || stripped;
397
+ }
398
+ if (lowered === "opus" || lowered.startsWith("claude-opus")) {
399
+ return runtimeConfig.roles.opus?.upstreamModel || runtimeConfig.fallbackModel || stripped;
400
+ }
401
+ return runtimeConfig.fallbackModel || stripped;
181
402
  }
182
403
 
183
- private manifestPaymentMethods(manifest: SellerManifest, seller: RegistrySeller): string[] {
184
- return manifest.paymentMethods || manifest.payment_methods || seller.paymentMethods || [];
404
+ private resolveRouteModelId(endpoint: string, body: unknown): { requestedModelId?: string; resolvedModelId?: string } {
405
+ const requestedModelId = this.extractModelId(endpoint, body);
406
+ if (!requestedModelId) {
407
+ return {};
408
+ }
409
+ if (this.endpointProtocol(endpoint) === "messages") {
410
+ return {
411
+ requestedModelId,
412
+ resolvedModelId: this.resolveClaudeRoleModel(requestedModelId)
413
+ };
414
+ }
415
+ return {
416
+ requestedModelId,
417
+ resolvedModelId: requestedModelId
418
+ };
185
419
  }
186
420
 
187
- private manifestModelIds(manifest: SellerManifest): string[] {
188
- return (manifest.models || []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0);
421
+ private applyResolvedModelToBody(
422
+ endpoint: string,
423
+ body: Record<string, unknown>,
424
+ resolvedModelId: string
425
+ ): Record<string, unknown> {
426
+ const nextBody: Record<string, unknown> = { ...body };
427
+ if ("model" in nextBody) {
428
+ nextBody.model = resolvedModelId;
429
+ }
430
+ if (endpoint === "/v1/responses" && "model_id" in nextBody) {
431
+ nextBody.model_id = resolvedModelId;
432
+ }
433
+ return nextBody;
189
434
  }
190
435
 
191
436
  private defaultPaymentMethod(): string | undefined {
@@ -206,13 +451,16 @@ export class TokenbuddyDaemon {
206
451
  const registry = await this.fetchRegistry();
207
452
  const defaultSellers = registry.sellers.filter((seller) => seller.id === registry.defaultSeller);
208
453
  const backupSellers = registry.sellers.filter((seller) => seller.id !== registry.defaultSeller);
209
- const sellers = this.selectionMode === "manual" ? defaultSellers : [...defaultSellers, ...backupSellers];
454
+ const manualSellers = this.selectedSellerId
455
+ ? registry.sellers.filter((seller) => seller.id === this.selectedSellerId)
456
+ : defaultSellers;
457
+ const sellers = this.selectionMode === "manual" ? manualSellers : [...defaultSellers, ...backupSellers];
210
458
 
211
459
  const routes: SellerRoute[] = [];
212
460
  for (const seller of sellers) {
213
461
  let manifest: SellerManifest;
214
462
  try {
215
- manifest = await this.fetchSellerManifest(seller);
463
+ manifest = await fetchSellerManifest(seller);
216
464
  } catch (error: unknown) {
217
465
  logger.warn("route.manifest.failed", "seller manifest unavailable during route selection", {
218
466
  sellerKey: seller.id,
@@ -223,9 +471,9 @@ export class TokenbuddyDaemon {
223
471
  continue;
224
472
  }
225
473
 
226
- const protocols = this.manifestProtocols(manifest, seller);
227
- const paymentMethods = this.manifestPaymentMethods(manifest, seller);
228
- const modelIds = this.manifestModelIds(manifest);
474
+ const protocols = manifestProtocols(manifest, seller);
475
+ const paymentMethods = manifestPaymentMethods(manifest, seller);
476
+ const modelIds = manifestModelIds(manifest);
229
477
  if (!protocols.includes(protocol) || !paymentMethods.includes(paymentMethod) || !modelIds.includes(modelId)) {
230
478
  continue;
231
479
  }
@@ -303,51 +551,10 @@ export class TokenbuddyDaemon {
303
551
  models: Array<{ id: string; sellerId: string; sellerName?: string; sellerUrl: string; supportedProtocols: string[]; paymentMethods: string[] }>;
304
552
  sellers: Array<{ id: string; name?: string; url: string; status: string; manifestSellerId?: string; errorMessage?: string }>;
305
553
  }> {
306
- const registry = await this.fetchRegistry();
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
- }));
554
+ const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
348
555
  return {
349
- models: sellerResults.flatMap((result) => result.models),
350
- sellers: sellerResults.map((result) => result.seller)
556
+ models: catalog.models,
557
+ sellers: catalog.sellers
351
558
  };
352
559
  }
353
560
 
@@ -409,7 +616,7 @@ export class TokenbuddyDaemon {
409
616
 
410
617
  private async getOrPurchaseToken(route: SellerRoute): Promise<string> {
411
618
  const sellerKey = route.seller.id;
412
- const sellerUrl = this.normalizeSellerUrl(route.seller);
619
+ const sellerUrl = normalizeSellerUrl(route.seller);
413
620
  const { modelId, paymentMethod } = route;
414
621
  const cached = this.tokenStore.getToken(sellerKey);
415
622
  const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
@@ -600,7 +807,7 @@ export class TokenbuddyDaemon {
600
807
  const timeoutMs = this.clawtipProofTimeoutMs();
601
808
  const payload = JSON.stringify({
602
809
  sellerKey: route.seller.id,
603
- sellerUrl: this.normalizeSellerUrl(route.seller),
810
+ sellerUrl: normalizeSellerUrl(route.seller),
604
811
  modelId: route.modelId,
605
812
  purchase: createData,
606
813
  paymentInstructions: createData.paymentInstructions || createData.payment_instructions,
@@ -686,7 +893,7 @@ export class TokenbuddyDaemon {
686
893
  private copyUpstreamHeaders(upstreamResponse: globalThis.Response, res: Response): void {
687
894
  upstreamResponse.headers.forEach((value, key) => {
688
895
  const lowerKey = key.toLowerCase();
689
- if (["connection", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
896
+ if (["connection", "content-encoding", "content-length", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
690
897
  return;
691
898
  }
692
899
  res.setHeader(key, value);
@@ -696,7 +903,8 @@ export class TokenbuddyDaemon {
696
903
  private async forwardProxyRequest(endpoint: string, req: Request, res: Response): Promise<void> {
697
904
  const startedAt = Date.now();
698
905
  const body = req.body || {};
699
- const modelId = this.extractModelId(endpoint, body);
906
+ const { requestedModelId, resolvedModelId } = this.resolveRouteModelId(endpoint, body);
907
+ const modelId = resolvedModelId;
700
908
  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
909
  const idempotencyKey = req.header("idempotency-key") || `idem_${crypto.randomBytes(12).toString("hex")}`;
702
910
 
@@ -718,15 +926,16 @@ export class TokenbuddyDaemon {
718
926
  requestId,
719
927
  sellerKey,
720
928
  model: modelId,
929
+ requestedModel: requestedModelId,
721
930
  endpoint,
722
931
  stream: Boolean((body as { stream?: unknown }).stream)
723
932
  });
724
933
  const token = await this.getOrPurchaseToken(route);
725
- const sellerUrl = this.normalizeSellerUrl(route.seller);
726
- const upstreamBody = {
934
+ const sellerUrl = normalizeSellerUrl(route.seller);
935
+ const upstreamBody = this.applyResolvedModelToBody(endpoint, {
727
936
  ...(body as Record<string, unknown>),
728
937
  requestId
729
- };
938
+ }, modelId);
730
939
 
731
940
  logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
732
941
  requestId,
@@ -786,13 +995,29 @@ export class TokenbuddyDaemon {
786
995
  return;
787
996
  }
788
997
  let bytes = 0;
998
+ const decoder = new TextDecoder();
999
+ const responsesStreamNormalizer = new ResponsesStreamNormalizer();
789
1000
  while (true) {
790
1001
  const { done, value } = await reader.read();
791
1002
  if (done) {
792
1003
  break;
793
1004
  }
794
1005
  bytes += value.byteLength;
795
- res.write(Buffer.from(value));
1006
+ if (endpoint === "/v1/responses") {
1007
+ const chunk = decoder.decode(value, { stream: true });
1008
+ const normalized = responsesStreamNormalizer.push(chunk);
1009
+ if (normalized.length > 0) {
1010
+ res.write(`${normalized}\n\n`);
1011
+ }
1012
+ } else {
1013
+ res.write(Buffer.from(value));
1014
+ }
1015
+ }
1016
+ if (endpoint === "/v1/responses") {
1017
+ const trailing = responsesStreamNormalizer.finish();
1018
+ if (trailing.length > 0) {
1019
+ res.write(`${trailing}\n\n`);
1020
+ }
796
1021
  }
797
1022
  res.end();
798
1023
  const billedMicros = Math.max(1, bytes);
@@ -1026,7 +1251,9 @@ export class TokenbuddyDaemon {
1026
1251
  const changes = previewProviderInstall({
1027
1252
  providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
1028
1253
  proxyUrl: String(req.body?.proxyUrl || ""),
1029
- model: String(req.body?.model || ""),
1254
+ model: typeof req.body?.model === "string" ? req.body.model : undefined,
1255
+ providerSelections: req.body?.providerSelections,
1256
+ sellerRouting: req.body?.sellerRouting,
1030
1257
  home: typeof req.body?.home === "string" ? req.body.home : undefined
1031
1258
  });
1032
1259
  logger.info("provider.install.previewed", "provider install previewed", {
@@ -1053,7 +1280,9 @@ export class TokenbuddyDaemon {
1053
1280
  const applied = applyProviderInstall({
1054
1281
  providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
1055
1282
  proxyUrl: String(req.body?.proxyUrl || ""),
1056
- model: String(req.body?.model || ""),
1283
+ model: typeof req.body?.model === "string" ? req.body.model : undefined,
1284
+ providerSelections: req.body?.providerSelections,
1285
+ sellerRouting: req.body?.sellerRouting,
1057
1286
  home: typeof req.body?.home === "string" ? req.body.home : undefined
1058
1287
  }, this.tokenStore);
1059
1288
  logger.info("provider.install.applied", "provider install applied", {