@tokenbuddy/tokenbuddy 1.0.36 → 1.0.37

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 (143) hide show
  1. package/dist/src/buyer-store.d.ts +6 -1
  2. package/dist/src/buyer-store.js +43 -4
  3. package/dist/src/cli.js +2 -2
  4. package/dist/src/daemon.d.ts +12 -0
  5. package/dist/src/daemon.js +791 -61
  6. package/dist/src/doctor-diagnostics.js +1 -6
  7. package/dist/src/provider-install.d.ts +2 -2
  8. package/dist/src/provider-install.js +248 -2
  9. package/dist/src/seller-catalog.d.ts +21 -0
  10. package/dist/src/seller-catalog.js +17 -0
  11. package/dist/src/seller-route-planner.d.ts +4 -1
  12. package/dist/src/seller-route-planner.js +3 -0
  13. package/dist/src/seller-routing-strategy.d.ts +3 -0
  14. package/dist/src/terminal-detect.d.ts +1 -1
  15. package/dist/src/terminal-detect.js +3 -2
  16. package/package.json +15 -2
  17. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  18. package/static/ui/assets/index-DkfztCkn.css +1 -0
  19. package/static/ui/index.html +2 -2
  20. package/dist/src/buyer-store.d.ts.map +0 -1
  21. package/dist/src/buyer-store.js.map +0 -1
  22. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  23. package/dist/src/clawtip-bootstrap.js.map +0 -1
  24. package/dist/src/cli.d.ts.map +0 -1
  25. package/dist/src/cli.js.map +0 -1
  26. package/dist/src/credit-tracker.d.ts.map +0 -1
  27. package/dist/src/credit-tracker.js.map +0 -1
  28. package/dist/src/daemon.d.ts.map +0 -1
  29. package/dist/src/daemon.js.map +0 -1
  30. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  31. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  32. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  33. package/dist/src/doctor-diagnostics.js.map +0 -1
  34. package/dist/src/index.d.ts.map +0 -1
  35. package/dist/src/index.js.map +0 -1
  36. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  37. package/dist/src/init-clawtip-activation.js.map +0 -1
  38. package/dist/src/init-payment-options.d.ts.map +0 -1
  39. package/dist/src/init-payment-options.js.map +0 -1
  40. package/dist/src/init-setup.d.ts.map +0 -1
  41. package/dist/src/init-setup.js.map +0 -1
  42. package/dist/src/model-index.d.ts.map +0 -1
  43. package/dist/src/model-index.js.map +0 -1
  44. package/dist/src/package-update.d.ts.map +0 -1
  45. package/dist/src/package-update.js.map +0 -1
  46. package/dist/src/prewarm-cache.d.ts.map +0 -1
  47. package/dist/src/prewarm-cache.js.map +0 -1
  48. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  49. package/dist/src/prewarm-scheduler.js.map +0 -1
  50. package/dist/src/provider-install.d.ts.map +0 -1
  51. package/dist/src/provider-install.js.map +0 -1
  52. package/dist/src/provider-routing-config.d.ts.map +0 -1
  53. package/dist/src/provider-routing-config.js.map +0 -1
  54. package/dist/src/registry-trust.d.ts.map +0 -1
  55. package/dist/src/registry-trust.js.map +0 -1
  56. package/dist/src/route-failover.d.ts.map +0 -1
  57. package/dist/src/route-failover.js.map +0 -1
  58. package/dist/src/seller-catalog.d.ts.map +0 -1
  59. package/dist/src/seller-catalog.js.map +0 -1
  60. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  61. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  62. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  63. package/dist/src/seller-metadata-cache.js.map +0 -1
  64. package/dist/src/seller-pool.d.ts.map +0 -1
  65. package/dist/src/seller-pool.js.map +0 -1
  66. package/dist/src/seller-route-planner.d.ts.map +0 -1
  67. package/dist/src/seller-route-planner.js.map +0 -1
  68. package/dist/src/seller-routing-config.d.ts.map +0 -1
  69. package/dist/src/seller-routing-config.js.map +0 -1
  70. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  71. package/dist/src/seller-routing-strategy.js.map +0 -1
  72. package/dist/src/stream-failover.d.ts.map +0 -1
  73. package/dist/src/stream-failover.js.map +0 -1
  74. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  75. package/dist/src/tb-clawtip-proof.js.map +0 -1
  76. package/dist/src/tb-proxyd.d.ts.map +0 -1
  77. package/dist/src/tb-proxyd.js.map +0 -1
  78. package/dist/src/terminal-detect.d.ts.map +0 -1
  79. package/dist/src/terminal-detect.js.map +0 -1
  80. package/dist/src/terminal-image.d.ts.map +0 -1
  81. package/dist/src/terminal-image.js.map +0 -1
  82. package/src/buyer-store.ts +0 -1090
  83. package/src/clawtip-bootstrap.ts +0 -65
  84. package/src/cli.ts +0 -2243
  85. package/src/credit-tracker.ts +0 -295
  86. package/src/daemon.ts +0 -5475
  87. package/src/doctor-clawtip-wallet.ts +0 -95
  88. package/src/doctor-diagnostics.ts +0 -1026
  89. package/src/index.ts +0 -16
  90. package/src/init-clawtip-activation.ts +0 -695
  91. package/src/init-payment-options.ts +0 -373
  92. package/src/init-setup.ts +0 -165
  93. package/src/model-index.ts +0 -278
  94. package/src/package-update.ts +0 -311
  95. package/src/prewarm-cache.ts +0 -485
  96. package/src/prewarm-scheduler.ts +0 -675
  97. package/src/provider-install.ts +0 -1006
  98. package/src/provider-routing-config.ts +0 -410
  99. package/src/registry-trust.ts +0 -51
  100. package/src/route-failover.ts +0 -304
  101. package/src/seller-catalog.ts +0 -505
  102. package/src/seller-concurrency-limiter.ts +0 -161
  103. package/src/seller-metadata-cache.ts +0 -91
  104. package/src/seller-pool.ts +0 -557
  105. package/src/seller-route-planner.ts +0 -513
  106. package/src/seller-routing-config.ts +0 -211
  107. package/src/seller-routing-strategy.ts +0 -362
  108. package/src/stream-failover.ts +0 -152
  109. package/src/tb-clawtip-proof.ts +0 -28
  110. package/src/tb-proxyd.ts +0 -101
  111. package/src/terminal-detect.ts +0 -333
  112. package/src/terminal-image.ts +0 -228
  113. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  114. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  115. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  116. package/tests/cli-routing.test.ts +0 -363
  117. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  118. package/tests/credit-tracker.test.ts +0 -165
  119. package/tests/daemon-413-fallback.test.ts +0 -92
  120. package/tests/daemon-classify.test.ts +0 -452
  121. package/tests/daemon-roles.test.ts +0 -92
  122. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  123. package/tests/e2e.test.ts +0 -366
  124. package/tests/image-generation-e2e.test.ts +0 -230
  125. package/tests/model-index.test.ts +0 -198
  126. package/tests/package-update.test.ts +0 -147
  127. package/tests/prewarm-cache.test.ts +0 -296
  128. package/tests/prewarm-scheduler.test.ts +0 -367
  129. package/tests/provider-routing-config.test.ts +0 -150
  130. package/tests/registry-trust.test.ts +0 -28
  131. package/tests/route-failover.test.ts +0 -222
  132. package/tests/seller-catalog-413.test.ts +0 -120
  133. package/tests/seller-catalog-utilities.test.ts +0 -124
  134. package/tests/seller-concurrency-limiter.test.ts +0 -83
  135. package/tests/seller-metadata-cache.test.ts +0 -89
  136. package/tests/seller-pool.test.ts +0 -365
  137. package/tests/seller-route-planner.test.ts +0 -312
  138. package/tests/seller-routing-config.test.ts +0 -124
  139. package/tests/seller-routing-strategy.test.ts +0 -167
  140. package/tests/stream-failover.test.ts +0 -52
  141. package/tests/thousand-seller.test.ts +0 -151
  142. package/tests/tokenbuddy.test.ts +0 -4043
  143. package/tsconfig.json +0 -8
@@ -1,452 +0,0 @@
1
- import * as fs from "fs";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import { TokenbuddyDaemon } from "../src/daemon.js";
5
-
6
- describe("TokenbuddyDaemon classifyFailureStatus", () => {
7
- let daemon: TokenbuddyDaemon;
8
-
9
- beforeEach(() => {
10
- daemon = new TokenbuddyDaemon({
11
- controlPort: 0,
12
- proxyPort: 0,
13
- dbPath: ":memory:",
14
- sellerRegistryUrl: "http://127.0.0.1:1/registry",
15
- });
16
- });
17
-
18
- afterEach(() => {
19
- daemon.stop();
20
- });
21
-
22
- test("classifies auth failures", () => {
23
- expect((daemon as any).classifyFailureStatus(401)).toBe("auth_invalid");
24
- expect((daemon as any).classifyFailureStatus(403)).toBe("auth_invalid");
25
- });
26
-
27
- test("classifies insufficient funds", () => {
28
- expect((daemon as any).classifyFailureStatus(402)).toBe("insufficient_funds");
29
- });
30
-
31
- test("classifies hard request 4xx failures", () => {
32
- expect((daemon as any).classifyFailureStatus(400)).toBe("hard_4xx");
33
- expect((daemon as any).classifyFailureStatus(404)).toBe("hard_4xx");
34
- expect((daemon as any).classifyFailureStatus(422)).toBe("hard_4xx");
35
- });
36
-
37
- test("classifies retryable upstream failures", () => {
38
- expect((daemon as any).classifyFailureStatus(429)).toBe("soft_5xx");
39
- expect((daemon as any).classifyFailureStatus(500)).toBe("soft_5xx");
40
- expect((daemon as any).classifyFailureStatus(502)).toBe("soft_5xx");
41
- expect((daemon as any).classifyFailureStatus(503)).toBe("soft_5xx");
42
- });
43
-
44
- test("classifies seller capacity exhaustion separately from generic 429", () => {
45
- expect((daemon as any).classifyFailureStatus(429, JSON.stringify({
46
- error: { code: "busy_capacity" }
47
- }))).toBe("busy_capacity");
48
- expect((daemon as any).classifyFailureStatus(429, JSON.stringify({
49
- error: { code: "rate_limited" }
50
- }))).toBe("soft_5xx");
51
- });
52
-
53
- test("recognizes supported proxy endpoint protocols", () => {
54
- expect((daemon as any).endpointProtocol("/v1/chat/completions")).toBe("chat_completions");
55
- expect((daemon as any).endpointProtocol("/v1/responses")).toBe("responses");
56
- expect((daemon as any).endpointProtocol("/v1/messages")).toBe("messages");
57
- expect((daemon as any).endpointProtocol("/messages")).toBe("messages");
58
- expect((daemon as any).endpointProtocol("/v1/images/generations")).toBe("images_generations");
59
- expect((daemon as any).endpointProtocol("/v1/unknown")).toBeUndefined();
60
- });
61
-
62
- test("extracts model ids from supported body shapes", () => {
63
- expect((daemon as any).extractModelId("/v1/chat/completions", { model: " gpt-5.4 " })).toBe("gpt-5.4");
64
- expect((daemon as any).extractModelId("/v1/chat/completions", { model: " " })).toBeUndefined();
65
- expect((daemon as any).extractModelId("/v1/responses", { model_id: " resp-model " })).toBe("resp-model");
66
- expect((daemon as any).extractModelId("/v1/responses", { model_id: 42 })).toBeUndefined();
67
- expect((daemon as any).extractModelId("/v1/images/generations", { model: " gpt-image-2 " })).toBe("gpt-image-2");
68
- expect((daemon as any).extractModelId("/v1/responses", null)).toBeUndefined();
69
- });
70
-
71
- test("resolves and applies route model ids", () => {
72
- expect((daemon as any).resolveRouteModelId("/v1/chat/completions", { model: "gpt-5.4" })).toEqual({
73
- requestedModelId: "gpt-5.4",
74
- resolvedModelId: "gpt-5.4",
75
- });
76
- expect((daemon as any).resolveRouteModelId("/v1/chat/completions", {})).toEqual({});
77
- expect((daemon as any).applyResolvedModelToBody("/v1/chat/completions", { model: "old" }, "new")).toEqual({
78
- model: "new",
79
- });
80
- expect((daemon as any).applyResolvedModelToBody("/v1/responses", { model_id: "old" }, "new")).toEqual({
81
- model_id: "new",
82
- });
83
- expect((daemon as any).applyResolvedModelToBody("/v1/responses", { input: "hi" }, "new")).toEqual({
84
- input: "hi",
85
- });
86
- });
87
-
88
- test("chooses the default enabled payment method", () => {
89
- expect((daemon as any).defaultPaymentMethod()).toBeUndefined();
90
- (daemon as any).tokenStore.savePayment({ method: "mock", enabled: true, isDefault: false });
91
- expect((daemon as any).defaultPaymentMethod()).toBe("mock");
92
- (daemon as any).tokenStore.savePayment({ method: "clawtip", enabled: true, isDefault: true });
93
- expect((daemon as any).defaultPaymentMethod()).toBe("clawtip");
94
- });
95
-
96
- test("reads usage from chat and responses style bodies", () => {
97
- expect((daemon as any).readUsage("")).toEqual({
98
- promptTokens: 0,
99
- completionTokens: 0,
100
- cacheReadTokens: 0,
101
- billedMicros: 0,
102
- });
103
- expect((daemon as any).readUsage("not-json")).toEqual({
104
- promptTokens: 0,
105
- completionTokens: 0,
106
- cacheReadTokens: 0,
107
- billedMicros: 0,
108
- });
109
- expect((daemon as any).readUsage(JSON.stringify({ usage: { prompt_tokens: 2, completion_tokens: 3 } }))).toEqual({
110
- promptTokens: 2,
111
- completionTokens: 3,
112
- cacheReadTokens: 0,
113
- billedMicros: 20,
114
- });
115
- expect((daemon as any).readUsage(JSON.stringify({ usage: { input_tokens: 5, output_tokens: 7, prompt_tokens_details: { cached_tokens: 4 } } }))).toEqual({
116
- promptTokens: 5,
117
- completionTokens: 7,
118
- cacheReadTokens: 4,
119
- billedMicros: 48,
120
- });
121
- expect((daemon as any).readUsage(JSON.stringify({ usage: { input_tokens: 25, output_tokens: 75, total_tokens: 100 } }))).toEqual({
122
- promptTokens: 25,
123
- completionTokens: 75,
124
- cacheReadTokens: 0,
125
- billedMicros: 400,
126
- });
127
- });
128
-
129
- test("parses seller settlement headers", () => {
130
- expect((daemon as any).parseSellerSettlementSummary(new Headers())).toBeUndefined();
131
- expect((daemon as any).parseSellerSettlementSummary(new Headers({
132
- "x-tokenbuddy-settlement": JSON.stringify({
133
- request_id: "req-1",
134
- settled_micros: "20",
135
- remaining_credit_micros: "980",
136
- reserved_balance_micros: 1,
137
- spent_micros: 19,
138
- price_version: "v1",
139
- }),
140
- }))).toEqual({
141
- requestId: "req-1",
142
- settledMicros: 20,
143
- settledUsdMicros: undefined,
144
- remainingCreditMicros: 980,
145
- reservedBalanceMicros: 1,
146
- spentMicros: 19,
147
- priceVersion: "v1",
148
- });
149
- expect((daemon as any).parseSellerSettlementSummary(new Headers({
150
- "x-tokenbuddy-settlement": "{bad",
151
- }))).toBeUndefined();
152
- expect((daemon as any).parseSellerSettlementSummary(new Headers({
153
- "x-tokenbuddy-settlement": JSON.stringify({ requestId: "missing-numbers" }),
154
- }))).toBeUndefined();
155
- });
156
-
157
- test("detects insufficient funds responses", () => {
158
- expect((daemon as any).isInsufficientFundsResponse(500, "insufficient funds")).toBe(false);
159
- expect((daemon as any).isInsufficientFundsResponse(402, JSON.stringify({ error: { code: "insufficient_funds" } }))).toBe(true);
160
- expect((daemon as any).isInsufficientFundsResponse(402, JSON.stringify({ error: { message: "Insufficient funds available" } }))).toBe(true);
161
- expect((daemon as any).isInsufficientFundsResponse(402, "{not-json insufficient funds")).toBe(true);
162
- expect((daemon as any).isInsufficientFundsResponse(402, JSON.stringify({ error: { code: "other" } }))).toBe(false);
163
- });
164
-
165
- test("uses validated numeric environment defaults", () => {
166
- const previousPurchase = process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS;
167
- const previousRebuy = process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS;
168
- const previousProof = process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS;
169
- try {
170
- delete process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS;
171
- delete process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS;
172
- delete process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS;
173
- expect((daemon as any).autoPurchaseAmountUsdMicros()).toBe(2000000);
174
- expect((daemon as any).tokenRebuyMinBalanceMicros()).toBe(200000);
175
- expect((daemon as any).clawtipProofTimeoutMs()).toBe(120000);
176
-
177
- process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS = "300";
178
- process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS = "0";
179
- process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS = "1000";
180
- expect((daemon as any).autoPurchaseAmountUsdMicros()).toBe(300);
181
- expect((daemon as any).tokenRebuyMinBalanceMicros()).toBe(0);
182
- expect((daemon as any).clawtipProofTimeoutMs()).toBe(1000);
183
-
184
- process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS = "0";
185
- process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS = "-1";
186
- process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS = "999";
187
- expect((daemon as any).autoPurchaseAmountUsdMicros()).toBe(2000000);
188
- expect((daemon as any).tokenRebuyMinBalanceMicros()).toBe(200000);
189
- expect((daemon as any).clawtipProofTimeoutMs()).toBe(120000);
190
- } finally {
191
- restoreEnv("TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS", previousPurchase);
192
- restoreEnv("TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS", previousRebuy);
193
- restoreEnv("TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS", previousProof);
194
- }
195
- });
196
-
197
- test("uses validated request deadline and token expiry margin defaults", () => {
198
- const previousDeadline = process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
199
- const previousMargin = process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS;
200
- try {
201
- delete process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
202
- delete process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS;
203
- expect((daemon as any).requestDeadlineMs()).toBe(180000);
204
- expect((daemon as any).tokenExpirySafetyMarginMs()).toBe(60000);
205
-
206
- process.env.TB_PROXYD_REQUEST_DEADLINE_MS = "2500";
207
- process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS = "0";
208
- expect((daemon as any).requestDeadlineMs()).toBe(2500);
209
- expect((daemon as any).tokenExpirySafetyMarginMs()).toBe(0);
210
-
211
- process.env.TB_PROXYD_REQUEST_DEADLINE_MS = "999";
212
- process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS = "-1";
213
- expect((daemon as any).requestDeadlineMs()).toBe(180000);
214
- expect((daemon as any).tokenExpirySafetyMarginMs()).toBe(60000);
215
- } finally {
216
- restoreEnv("TB_PROXYD_REQUEST_DEADLINE_MS", previousDeadline);
217
- restoreEnv("TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS", previousMargin);
218
- }
219
- });
220
-
221
- test("infers prompt hash input for object bodies only", () => {
222
- expect((daemon as any).inferPromptForHash(null)).toBeUndefined();
223
- expect((daemon as any).inferPromptForHash("text")).toBeUndefined();
224
- expect((daemon as any).inferPromptForHash({ model: "gpt-5.4", messages: [] })).toBe(JSON.stringify({
225
- model: "gpt-5.4",
226
- messages: [],
227
- }));
228
- });
229
-
230
- test("resolves payment proof from supported mock and ClawTip sources", async () => {
231
- const previousProof = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF;
232
- const previousOldProof = process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
233
- const previousProofFile = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE;
234
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tokenbuddy-proof-"));
235
- const proofFile = path.join(tempDir, "proof.txt");
236
- const route = routeFor("clawtip");
237
- try {
238
- expect(await (daemon as any).resolvePaymentProof(routeFor("mock"), {}, "req-1")).toBe("mock-proof-data");
239
- await expect((daemon as any).resolvePaymentProof(routeFor("card"), {}, "req-1"))
240
- .rejects.toThrow("unsupported payment method");
241
-
242
- process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF = " proof-from-env ";
243
- expect(await (daemon as any).resolvePaymentProof(route, {}, "req-1")).toBe("proof-from-env");
244
-
245
- delete process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF;
246
- fs.writeFileSync(proofFile, " proof-from-file\n");
247
- process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE = proofFile;
248
- expect(await (daemon as any).resolvePaymentProof(route, {}, "req-1")).toBe("proof-from-file");
249
-
250
- delete process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE;
251
- await expect((daemon as any).resolvePaymentProof(route, {}, "req-1"))
252
- .rejects.toThrow("clawtip auto purchase requires");
253
- } finally {
254
- restoreEnv("TOKENBUDDY_CLAWTIP_PAYMENT_PROOF", previousProof);
255
- restoreEnv("TOKENBUDDY_CLAWTIP_PROOF_FILE", previousOldProof);
256
- restoreEnv("TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE", previousProofFile);
257
- fs.rmSync(tempDir, { recursive: true, force: true });
258
- }
259
- });
260
-
261
- test("copies only safe upstream headers", () => {
262
- const upstream = {
263
- headers: new Headers({
264
- "content-type": "application/json",
265
- "content-length": "20",
266
- connection: "keep-alive",
267
- "x-tokenbuddy-test": "ok",
268
- }),
269
- } as Response;
270
- const res = {
271
- setHeader: jest.fn(),
272
- };
273
-
274
- (daemon as any).copyUpstreamHeaders(upstream, res);
275
-
276
- expect(res.setHeader).toHaveBeenCalledWith("content-type", "application/json");
277
- expect(res.setHeader).toHaveBeenCalledWith("x-tokenbuddy-test", "ok");
278
- expect(res.setHeader).not.toHaveBeenCalledWith("content-length", expect.anything());
279
- expect(res.setHeader).not.toHaveBeenCalledWith("connection", expect.anything());
280
- });
281
-
282
- test("resolves focus sets from explicit config and environment", () => {
283
- const explicitDaemon = new TokenbuddyDaemon({
284
- controlPort: 0,
285
- proxyPort: 0,
286
- dbPath: ":memory:",
287
- sellerRegistryUrl: "http://127.0.0.1:1/registry",
288
- warmupModels: ["explicit-a", "explicit-b"],
289
- });
290
- const previousWarmup = process.env.TB_BUYER_WARMUP_MODELS;
291
- try {
292
- expect((explicitDaemon as any).resolveFocusSet()).toEqual(["explicit-a", "explicit-b"]);
293
-
294
- process.env.TB_BUYER_WARMUP_MODELS = " env-a, ,env-b ";
295
- expect((daemon as any).resolveFocusSet()).toEqual(["env-a", "env-b"]);
296
-
297
- delete process.env.TB_BUYER_WARMUP_MODELS;
298
- expect((daemon as any).resolveFocusSet()).toEqual([]);
299
- } finally {
300
- explicitDaemon.stop();
301
- restoreEnv("TB_BUYER_WARMUP_MODELS", previousWarmup);
302
- }
303
- });
304
-
305
- test("resolves prewarm protocol from the model index", () => {
306
- (daemon as any).modelIndex.rebuild([
307
- {
308
- id: "seller-chat",
309
- url: "https://seller-chat.example.com",
310
- models: ["chat-model"],
311
- supportedProtocols: ["chat_completions"],
312
- paymentMethods: ["clawtip"],
313
- },
314
- {
315
- id: "seller-messages",
316
- url: "https://seller-messages.example.com",
317
- models: ["message-model"],
318
- supportedProtocols: ["messages"],
319
- paymentMethods: ["clawtip"],
320
- },
321
- {
322
- id: "seller-responses",
323
- url: "https://seller-responses.example.com",
324
- models: ["response-model"],
325
- supportedProtocols: ["responses"],
326
- paymentMethods: ["clawtip"],
327
- },
328
- ]);
329
-
330
- expect((daemon as any).resolvePrewarmProtocol("chat-model")).toBe("chat_completions");
331
- expect((daemon as any).resolvePrewarmProtocol("message-model")).toBe("messages");
332
- expect((daemon as any).resolvePrewarmProtocol("response-model")).toBe("responses");
333
- expect((daemon as any).resolvePrewarmProtocol("missing-model")).toBeUndefined();
334
- });
335
-
336
- test("reuses fresh cached seller tokens", async () => {
337
- (daemon as any).tokenStore.saveToken(
338
- "seller-a",
339
- "cached-token",
340
- "model:gpt-5.4",
341
- 900000,
342
- "2099-01-01T00:00:00.000Z"
343
- );
344
-
345
- await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-cache"))
346
- .resolves.toBe("cached-token");
347
- });
348
-
349
- test("purchases and persists a token when cache is missing", async () => {
350
- const fetchMock = jest.spyOn(globalThis, "fetch").mockImplementation(async (url: string | URL | Request) => {
351
- const href = String(url);
352
- if (href === "https://seller-a.example.com/purchase/create") {
353
- return response({
354
- purchase_id: "purchase-1",
355
- status: "pending",
356
- payment_reference: "pay-ref",
357
- expires_at: "2099-01-01T00:00:00.000Z",
358
- credit_micros: 500000,
359
- currency: "USD",
360
- payment_instructions: { type: "mock" },
361
- quote: { amount: 5 },
362
- });
363
- }
364
- if (href === "https://seller-a.example.com/purchase/complete") {
365
- return response({
366
- access_token: "new-token",
367
- token_class: "model:gpt-5.4",
368
- credit_micros: 500000,
369
- currency: "USD",
370
- payment_reference: "pay-ref",
371
- status: "funded",
372
- });
373
- }
374
- throw new Error(`unexpected fetch ${href}`);
375
- }) as jest.MockedFunction<typeof fetch>;
376
-
377
- try {
378
- await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-buy"))
379
- .resolves.toBe("new-token");
380
- expect((daemon as any).tokenStore.getToken("seller-a")).toMatchObject({
381
- token: "new-token",
382
- balanceMicros: 500000,
383
- });
384
- expect((daemon as any).tokenStore.listPurchaseLedger()).toHaveLength(1);
385
- } finally {
386
- fetchMock.mockRestore();
387
- }
388
- });
389
-
390
- test("surfaces purchase create and complete failures", async () => {
391
- const fetchMock = jest.spyOn(globalThis, "fetch");
392
- try {
393
- fetchMock.mockResolvedValueOnce(response({ error: { message: "create denied" } }, false, 402));
394
- await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-create-fail"))
395
- .rejects.toThrow("create denied");
396
-
397
- fetchMock
398
- .mockResolvedValueOnce(response({ purchaseId: "purchase-2" }))
399
- .mockResolvedValueOnce(response({ error: { message: "complete denied" } }, false, 500));
400
- await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-complete-fail"))
401
- .rejects.toThrow("complete denied");
402
- } finally {
403
- fetchMock.mockRestore();
404
- }
405
- });
406
-
407
- test("validates required purchase response fields", async () => {
408
- const fetchMock = jest.spyOn(globalThis, "fetch");
409
- try {
410
- fetchMock.mockResolvedValueOnce(response({ status: "pending" }));
411
- await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-no-purchase"))
412
- .rejects.toThrow("purchase/create response missing purchaseId");
413
-
414
- fetchMock
415
- .mockResolvedValueOnce(response({ purchaseId: "purchase-3" }))
416
- .mockResolvedValueOnce(response({ status: "funded" }));
417
- await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-no-token"))
418
- .rejects.toThrow("purchase/complete response missing accessToken");
419
- } finally {
420
- fetchMock.mockRestore();
421
- }
422
- });
423
- });
424
-
425
- function restoreEnv(name: string, value: string | undefined): void {
426
- if (value === undefined) {
427
- delete process.env[name];
428
- } else {
429
- process.env[name] = value;
430
- }
431
- }
432
-
433
- function routeFor(paymentMethod: string): unknown {
434
- return {
435
- seller: {
436
- id: "seller-a",
437
- url: "https://seller-a.example.com",
438
- },
439
- manifest: null,
440
- protocol: "chat_completions",
441
- modelId: "gpt-5.4",
442
- paymentMethod,
443
- };
444
- }
445
-
446
- function response(body: unknown, ok = true, status = 200): Response {
447
- return {
448
- ok,
449
- status,
450
- json: async () => body,
451
- } as Response;
452
- }
@@ -1,92 +0,0 @@
1
- import { TokenbuddyDaemon } from "../src/daemon.js";
2
- import type { ClaudeCodeModelMappingConfig, SingleModelProviderRuntimeConfig } from "../src/provider-install.js";
3
-
4
- describe("TokenbuddyDaemon resolveClaudeRoleModel", () => {
5
- let daemon: TokenbuddyDaemon;
6
-
7
- beforeEach(() => {
8
- daemon = new TokenbuddyDaemon({
9
- controlPort: 0,
10
- proxyPort: 0,
11
- dbPath: ":memory:",
12
- sellerRegistryUrl: "http://127.0.0.1:1/registry",
13
- });
14
- });
15
-
16
- afterEach(() => {
17
- daemon.stop();
18
- });
19
-
20
- function saveConfig(config: ClaudeCodeModelMappingConfig | SingleModelProviderRuntimeConfig): void {
21
- (daemon as any).tokenStore.saveProviderRuntimeConfig("claude-code", config);
22
- }
23
-
24
- test("returns stripped model id when runtime config is absent", () => {
25
- expect((daemon as any).resolveClaudeRoleModel("claude-sonnet-4-6 [1M]")).toBe("claude-sonnet-4-6");
26
- });
27
-
28
- test("returns stripped model id when selection kind is not role mapping", () => {
29
- saveConfig({
30
- selectionKind: "single-model",
31
- defaultModel: "upstream-default",
32
- });
33
-
34
- expect((daemon as any).resolveClaudeRoleModel("sonnet [1M]")).toBe("sonnet");
35
- });
36
-
37
- test("maps haiku aliases to haiku upstream model", () => {
38
- saveConfig({
39
- selectionKind: "claude-role-mapping",
40
- roles: {
41
- haiku: { upstreamModel: "upstream-haiku" },
42
- },
43
- });
44
-
45
- expect((daemon as any).resolveClaudeRoleModel("haiku")).toBe("upstream-haiku");
46
- expect((daemon as any).resolveClaudeRoleModel("claude-haiku-4-5 [1M]")).toBe("upstream-haiku");
47
- });
48
-
49
- test("maps sonnet aliases to sonnet upstream model", () => {
50
- saveConfig({
51
- selectionKind: "claude-role-mapping",
52
- roles: {
53
- sonnet: { upstreamModel: "upstream-sonnet" },
54
- },
55
- });
56
-
57
- expect((daemon as any).resolveClaudeRoleModel("sonnet")).toBe("upstream-sonnet");
58
- expect((daemon as any).resolveClaudeRoleModel("claude-sonnet-4-6")).toBe("upstream-sonnet");
59
- });
60
-
61
- test("maps opus aliases to opus upstream model", () => {
62
- saveConfig({
63
- selectionKind: "claude-role-mapping",
64
- roles: {
65
- opus: { upstreamModel: "upstream-opus" },
66
- },
67
- });
68
-
69
- expect((daemon as any).resolveClaudeRoleModel("opus")).toBe("upstream-opus");
70
- expect((daemon as any).resolveClaudeRoleModel("claude-opus-4-7")).toBe("upstream-opus");
71
- });
72
-
73
- test("uses fallback for unknown roles and missing role bindings", () => {
74
- saveConfig({
75
- selectionKind: "claude-role-mapping",
76
- fallbackModel: "upstream-fallback",
77
- roles: {},
78
- });
79
-
80
- expect((daemon as any).resolveClaudeRoleModel("custom-role")).toBe("upstream-fallback");
81
- expect((daemon as any).resolveClaudeRoleModel("haiku")).toBe("upstream-fallback");
82
- });
83
-
84
- test("returns stripped model id when role mapping has no match or fallback", () => {
85
- saveConfig({
86
- selectionKind: "claude-role-mapping",
87
- roles: {},
88
- });
89
-
90
- expect((daemon as any).resolveClaudeRoleModel("custom-role [1M]")).toBe("custom-role");
91
- });
92
- });
@@ -1,132 +0,0 @@
1
- import * as http from "http";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import { AddressInfo } from "net";
5
- import { TokenbuddyDaemon } from "../src/daemon.js";
6
- import { BuyerStore } from "../src/buyer-store.js";
7
-
8
- describe("TokenbuddyDaemon trusted registry cache", () => {
9
- const TEMP_DB = path.resolve(__dirname, "../../data-test/trusted-registry-cache-test.db");
10
- let server: http.Server;
11
- let serverPort: number;
12
- let registryAvailable = true;
13
- let daemon: TokenbuddyDaemon | undefined;
14
-
15
- function rmDb(): void {
16
- for (const suffix of ["", "-wal", "-shm"]) {
17
- fs.rmSync(`${TEMP_DB}${suffix}`, { force: true });
18
- }
19
- }
20
-
21
- function startDaemon(): { controlPort: number; proxyPort: number } {
22
- daemon = new TokenbuddyDaemon({
23
- controlPort: 0,
24
- proxyPort: 0,
25
- dbPath: TEMP_DB,
26
- sellerRegistryUrl: `http://127.0.0.1:${serverPort}/registry/sellers`
27
- });
28
- daemon.start();
29
- return {
30
- controlPort: ((daemon as unknown as { controlServer: { address(): AddressInfo } }).controlServer.address()).port,
31
- proxyPort: ((daemon as unknown as { proxyServer: { address(): AddressInfo } }).proxyServer.address()).port
32
- };
33
- }
34
-
35
- async function stopDaemon(): Promise<void> {
36
- daemon?.stop();
37
- daemon = undefined;
38
- await new Promise<void>((resolve) => setTimeout(resolve, 50));
39
- }
40
-
41
- beforeAll((done) => {
42
- server = http.createServer((req, res) => {
43
- res.setHeader("Content-Type", "application/json");
44
- if (req.url === "/registry/sellers") {
45
- if (!registryAvailable) {
46
- res.statusCode = 500;
47
- res.end(JSON.stringify({ error: "registry unavailable" }));
48
- return;
49
- }
50
- res.statusCode = 200;
51
- res.end(JSON.stringify({
52
- version: 7,
53
- defaultSeller: "cached-seller",
54
- sellers: [
55
- {
56
- id: "cached-seller",
57
- name: "Cached Seller",
58
- status: "active",
59
- url: `http://127.0.0.1:${serverPort}/cached-seller`,
60
- supportedProtocols: ["chat_completions"],
61
- paymentMethods: ["mock"],
62
- models: ["gpt-cache"]
63
- }
64
- ]
65
- }));
66
- return;
67
- }
68
- if (req.url === "/cached-seller/manifest") {
69
- res.statusCode = 200;
70
- res.end(JSON.stringify({
71
- sellerId: "cached-seller",
72
- supportedProtocols: ["chat_completions"],
73
- paymentMethods: ["mock"],
74
- models: [{ id: "gpt-cache" }]
75
- }));
76
- return;
77
- }
78
- res.statusCode = 404;
79
- res.end(JSON.stringify({ error: "not found" }));
80
- });
81
- server.listen(0, "127.0.0.1", () => {
82
- serverPort = (server.address() as AddressInfo).port;
83
- done();
84
- });
85
- });
86
-
87
- afterAll((done) => {
88
- server.close(() => done());
89
- });
90
-
91
- beforeEach(() => {
92
- rmDb();
93
- registryAvailable = true;
94
- });
95
-
96
- afterEach(async () => {
97
- await stopDaemon();
98
- rmDb();
99
- });
100
-
101
- test("persists a trusted registry snapshot and reuses it after daemon restart", async () => {
102
- let ports = startDaemon();
103
- const firstModels = await (await fetch(`http://127.0.0.1:${ports.proxyPort}/v1/models`)).json() as any;
104
- expect(firstModels.data).toEqual(expect.arrayContaining([
105
- expect.objectContaining({ id: "gpt-cache", sellerId: "cached-seller" })
106
- ]));
107
-
108
- const store = new BuyerStore({ dbPath: TEMP_DB });
109
- const cache = store.getDaemonRuntimeConfig<any>("trusted-registry-snapshot")?.config;
110
- store.close();
111
- expect(cache).toMatchObject({
112
- schemaVersion: 1,
113
- registryUrl: `http://127.0.0.1:${serverPort}/registry/sellers`,
114
- version: 7,
115
- trust: {
116
- verified: false
117
- }
118
- });
119
- expect(cache.registrySha256).toMatch(/^[a-f0-9]{64}$/);
120
-
121
- await stopDaemon();
122
- registryAvailable = false;
123
-
124
- ports = startDaemon();
125
- const secondModelsResponse = await fetch(`http://127.0.0.1:${ports.proxyPort}/v1/models`);
126
- expect(secondModelsResponse.status).toBe(200);
127
- const secondModels = await secondModelsResponse.json() as any;
128
- expect(secondModels.data).toEqual([
129
- expect.objectContaining({ id: "gpt-cache", sellerId: "cached-seller" })
130
- ]);
131
- });
132
- });