@tokenbuddy/tokenbuddy 1.0.5 → 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 (40) 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 +1 -1
  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
@@ -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
- 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();
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
- 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;
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
- manifestPaymentMethods(manifest, seller) {
94
- return manifest.paymentMethods || manifest.payment_methods || seller.paymentMethods || [];
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
- manifestModelIds(manifest) {
97
- return (manifest.models || []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0);
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 sellers = this.selectionMode === "manual" ? defaultSellers : [...defaultSellers, ...backupSellers];
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 this.fetchSellerManifest(seller);
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 = this.manifestProtocols(manifest, seller);
132
- const paymentMethods = this.manifestPaymentMethods(manifest, seller);
133
- const modelIds = this.manifestModelIds(manifest);
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 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
- }));
418
+ const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
236
419
  return {
237
- models: sellerResults.flatMap((result) => result.models),
238
- sellers: sellerResults.map((result) => result.seller)
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 = this.normalizeSellerUrl(route.seller);
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: this.normalizeSellerUrl(route.seller),
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 modelId = this.extractModelId(endpoint, body);
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 = this.normalizeSellerUrl(route.seller);
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
- res.write(Buffer.from(value));
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: String(req.body?.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: String(req.body?.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", {