@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.
- 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 +1 -1
- 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/tests/tokenbuddy.test.ts
CHANGED
|
@@ -8,10 +8,21 @@ import {
|
|
|
8
8
|
rollbackProviderInstall
|
|
9
9
|
} from "../src/provider-install.js";
|
|
10
10
|
import { detectTerminals } from "../src/terminal-detect.js";
|
|
11
|
+
import {
|
|
12
|
+
buildInitSuccessMessage,
|
|
13
|
+
buildInitTerminalOptions,
|
|
14
|
+
detectExistingClawtipBinding,
|
|
15
|
+
INIT_COMING_SOON_PAYMENT_OPTIONS,
|
|
16
|
+
INIT_PAYMENT_OPTIONS,
|
|
17
|
+
noteInitComingSoonPayments,
|
|
18
|
+
OTHER_TERMINAL_OPTION,
|
|
19
|
+
validateInitTerminalSelection,
|
|
20
|
+
} from "../src/init-payment-options.js";
|
|
11
21
|
import * as path from "path";
|
|
12
22
|
import * as fs from "fs";
|
|
13
23
|
import http from "http";
|
|
14
24
|
import { AddressInfo } from "net";
|
|
25
|
+
import zlib from "zlib";
|
|
15
26
|
|
|
16
27
|
const TEMP_BUYER_DB = path.resolve(__dirname, "../../data-test/buyer-cache-test.db");
|
|
17
28
|
const TEMP_STORE_ROOT = path.resolve(__dirname, "../../data-test/buyer-store-test");
|
|
@@ -125,6 +136,47 @@ describe("BuyerStore safe SQLite persistence", () => {
|
|
|
125
136
|
expect(store.listPendingPurchases()).toEqual([]);
|
|
126
137
|
expect(store.listPurchaseLedger()).toEqual([]);
|
|
127
138
|
expect(store.listInferenceLedger()).toEqual([]);
|
|
139
|
+
expect(store.summary()).toMatchObject({
|
|
140
|
+
providerRuntimeConfigCount: 0,
|
|
141
|
+
daemonRuntimeConfigCount: 0,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("stores provider runtime config and daemon routing config in the buyer store", () => {
|
|
146
|
+
store.saveProviderRuntimeConfig("opencode", {
|
|
147
|
+
selectionKind: "single-model",
|
|
148
|
+
protocolPreference: "responses",
|
|
149
|
+
defaultModel: "gpt-5.5",
|
|
150
|
+
sellerId: "seller-a",
|
|
151
|
+
});
|
|
152
|
+
store.saveDaemonRuntimeConfig("routing", {
|
|
153
|
+
mode: "fixed",
|
|
154
|
+
sellerId: "seller-a",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(store.getProviderRuntimeConfig("opencode")).toMatchObject({
|
|
158
|
+
providerId: "opencode",
|
|
159
|
+
config: expect.objectContaining({
|
|
160
|
+
defaultModel: "gpt-5.5",
|
|
161
|
+
sellerId: "seller-a",
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
expect(store.getDaemonRuntimeConfig("routing")).toMatchObject({
|
|
165
|
+
configKey: "routing",
|
|
166
|
+
config: expect.objectContaining({
|
|
167
|
+
mode: "fixed",
|
|
168
|
+
sellerId: "seller-a",
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
expect(store.summary()).toMatchObject({
|
|
172
|
+
providerRuntimeConfigCount: 1,
|
|
173
|
+
daemonRuntimeConfigCount: 1,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(store.removeProviderRuntimeConfig("opencode")).toBe(true);
|
|
177
|
+
expect(store.removeDaemonRuntimeConfig("routing")).toBe(true);
|
|
178
|
+
expect(store.getProviderRuntimeConfig("opencode")).toBeUndefined();
|
|
179
|
+
expect(store.getDaemonRuntimeConfig("routing")).toBeUndefined();
|
|
128
180
|
});
|
|
129
181
|
|
|
130
182
|
test("stores payment config and pending purchases with safe references", () => {
|
|
@@ -374,31 +426,201 @@ describe("TokenBuddy payment CLI", () => {
|
|
|
374
426
|
});
|
|
375
427
|
});
|
|
376
428
|
|
|
429
|
+
describe("TokenBuddy init payment options", () => {
|
|
430
|
+
test("init hides mock payment and shows coming-soon agent wallets as unavailable", () => {
|
|
431
|
+
const noteMessages: string[] = [];
|
|
432
|
+
noteInitComingSoonPayments((message?: string) => {
|
|
433
|
+
noteMessages.push(String(message || ""));
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(INIT_PAYMENT_OPTIONS).toEqual([
|
|
437
|
+
expect.objectContaining({ value: "clawtip" })
|
|
438
|
+
]);
|
|
439
|
+
expect(INIT_PAYMENT_OPTIONS.map((option) => option.value)).not.toContain("mock");
|
|
440
|
+
|
|
441
|
+
const notes = noteMessages.join("\n");
|
|
442
|
+
expect(INIT_COMING_SOON_PAYMENT_OPTIONS).toEqual(expect.arrayContaining([
|
|
443
|
+
expect.objectContaining({ id: "wechat-pay", label: "WeChat Pay" }),
|
|
444
|
+
expect.objectContaining({ id: "alipay-agent-payment", label: "Alipay Agent Payment" }),
|
|
445
|
+
expect.objectContaining({ id: "coinbase-smart-wallet", label: "Coinbase Smart Wallet" })
|
|
446
|
+
]));
|
|
447
|
+
expect(notes).toContain("WeChat Pay(接入中)");
|
|
448
|
+
expect(notes).toContain("Alipay Agent Payment(接入中)");
|
|
449
|
+
expect(notes).toContain("Coinbase Smart Wallet(接入中)");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("detects an existing local clawtip binding from saved payment config", () => {
|
|
453
|
+
const binding = detectExistingClawtipBinding({
|
|
454
|
+
method: "clawtip",
|
|
455
|
+
enabled: false,
|
|
456
|
+
isDefault: false,
|
|
457
|
+
updatedAt: "2026-05-30T00:00:00.000Z",
|
|
458
|
+
config: {
|
|
459
|
+
orderNo: "order_123",
|
|
460
|
+
resourceUrl: "https://example.test/pay"
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(binding).toEqual(expect.objectContaining({
|
|
465
|
+
orderNo: "order_123",
|
|
466
|
+
resourceUrl: "https://example.test/pay",
|
|
467
|
+
config: expect.objectContaining({
|
|
468
|
+
orderNo: "order_123",
|
|
469
|
+
resourceUrl: "https://example.test/pay"
|
|
470
|
+
})
|
|
471
|
+
}));
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("builds a clear init success message with summary lines", () => {
|
|
475
|
+
const message = buildInitSuccessMessage([
|
|
476
|
+
"2 programming terminals configured for TokenBuddy.",
|
|
477
|
+
"ClawTip wallet already bound locally; activation skipped."
|
|
478
|
+
]);
|
|
479
|
+
|
|
480
|
+
expect(message).toContain("✅ TokenBuddy setup completed successfully.");
|
|
481
|
+
expect(message).toContain("- 2 programming terminals configured for TokenBuddy.");
|
|
482
|
+
expect(message).toContain("- ClawTip wallet already bound locally; activation skipped.");
|
|
483
|
+
expect(message).toContain("Run `tb doctor` to audit status anytime.");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("builds terminal options with configured entries marked as already installed", () => {
|
|
487
|
+
const options = buildInitTerminalOptions([
|
|
488
|
+
{
|
|
489
|
+
id: "codex",
|
|
490
|
+
name: "Codex CLI",
|
|
491
|
+
detected: true,
|
|
492
|
+
configured: true,
|
|
493
|
+
status: "configured",
|
|
494
|
+
configPath: "/tmp/codex.toml",
|
|
495
|
+
reason: "Configured at ~/.codex/config.toml"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: "hermes",
|
|
499
|
+
name: "Hermes Terminal",
|
|
500
|
+
detected: true,
|
|
501
|
+
configured: false,
|
|
502
|
+
status: "installed",
|
|
503
|
+
configPath: "/tmp/hermes.json",
|
|
504
|
+
reason: "Installed, TokenBuddy config missing"
|
|
505
|
+
}
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
expect(options).toEqual([
|
|
509
|
+
expect.objectContaining({
|
|
510
|
+
value: "codex:installed",
|
|
511
|
+
label: "Codex CLI(已安装)"
|
|
512
|
+
}),
|
|
513
|
+
expect.objectContaining({
|
|
514
|
+
value: "hermes",
|
|
515
|
+
label: "Hermes Terminal"
|
|
516
|
+
}),
|
|
517
|
+
expect.objectContaining({
|
|
518
|
+
value: OTHER_TERMINAL_OPTION.value,
|
|
519
|
+
label: OTHER_TERMINAL_OPTION.label
|
|
520
|
+
})
|
|
521
|
+
]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("requires at least one actionable terminal choice", () => {
|
|
525
|
+
expect(validateInitTerminalSelection([])).toBe("Select at least one terminal or choose Other.");
|
|
526
|
+
expect(validateInitTerminalSelection(["codex:installed"])).toBe(
|
|
527
|
+
"Installed terminals are already configured. Select another terminal or choose Other."
|
|
528
|
+
);
|
|
529
|
+
expect(validateInitTerminalSelection(["other"])).toBeUndefined();
|
|
530
|
+
expect(validateInitTerminalSelection(["hermes"])).toBeUndefined();
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
377
534
|
describe("TokenBuddy JSON inspection commands", () => {
|
|
378
535
|
let controlServer: http.Server;
|
|
536
|
+
let proxyServer: http.Server;
|
|
379
537
|
let controlPort: number;
|
|
538
|
+
let proxyPort: number;
|
|
380
539
|
let previousControlPort: string | undefined;
|
|
381
540
|
let previousProxyPort: string | undefined;
|
|
382
541
|
|
|
383
542
|
beforeEach((done) => {
|
|
384
543
|
previousControlPort = process.env.TB_PROXYD_CONTROL_PORT;
|
|
385
544
|
previousProxyPort = process.env.TB_PROXYD_PROXY_PORT;
|
|
545
|
+
proxyServer = http.createServer((req, res) => {
|
|
546
|
+
res.setHeader("Content-Type", "application/json");
|
|
547
|
+
if (req.url === "/v1/models") {
|
|
548
|
+
res.end(JSON.stringify({
|
|
549
|
+
object: "list",
|
|
550
|
+
data: [
|
|
551
|
+
{ id: "gpt-4", sellerId: "json-test-seller" }
|
|
552
|
+
]
|
|
553
|
+
}));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
res.statusCode = 404;
|
|
557
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
558
|
+
});
|
|
386
559
|
controlServer = http.createServer((req, res) => {
|
|
387
560
|
res.setHeader("Content-Type", "application/json");
|
|
561
|
+
if (req.url === "/health") {
|
|
562
|
+
res.end(JSON.stringify({
|
|
563
|
+
status: "ok",
|
|
564
|
+
controlPort,
|
|
565
|
+
proxyPort
|
|
566
|
+
}));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
388
569
|
if (req.url === "/status") {
|
|
389
570
|
res.end(JSON.stringify({
|
|
390
571
|
status: "running",
|
|
391
572
|
pid: 12345,
|
|
392
573
|
controlPort,
|
|
393
|
-
proxyPort
|
|
574
|
+
proxyPort
|
|
575
|
+
}));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (req.url === "/sellers") {
|
|
579
|
+
res.end(JSON.stringify({
|
|
580
|
+
registryUrl: "https://example.test/registry/sellers",
|
|
581
|
+
version: 7,
|
|
582
|
+
defaultSeller: "json-test-seller",
|
|
583
|
+
sellers: [
|
|
584
|
+
{
|
|
585
|
+
id: "json-test-seller",
|
|
586
|
+
name: "JSON Seller",
|
|
587
|
+
url: "https://seller.example.test",
|
|
588
|
+
supportedProtocols: ["responses"],
|
|
589
|
+
paymentMethods: ["mock"],
|
|
590
|
+
discountRatio: 0.25,
|
|
591
|
+
status: "configured"
|
|
592
|
+
}
|
|
593
|
+
]
|
|
394
594
|
}));
|
|
395
595
|
return;
|
|
396
596
|
}
|
|
397
597
|
if (req.url === "/models") {
|
|
398
598
|
res.end(JSON.stringify({
|
|
399
599
|
object: "list",
|
|
600
|
+
registryUrl: "https://example.test/registry/sellers",
|
|
400
601
|
data: [
|
|
401
|
-
{
|
|
602
|
+
{
|
|
603
|
+
id: "gpt-4",
|
|
604
|
+
sellerId: "json-test-seller",
|
|
605
|
+
sellerName: "JSON Seller",
|
|
606
|
+
sellerUrl: "https://seller.example.test",
|
|
607
|
+
supportedProtocols: ["responses"],
|
|
608
|
+
paymentMethods: ["mock"],
|
|
609
|
+
inputPriceMicrosPer1m: 1000000,
|
|
610
|
+
outputPriceMicrosPer1m: 3000000
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
sellers: [
|
|
614
|
+
{
|
|
615
|
+
id: "json-test-seller",
|
|
616
|
+
name: "JSON Seller",
|
|
617
|
+
url: "https://seller.example.test",
|
|
618
|
+
supportedProtocols: ["responses"],
|
|
619
|
+
paymentMethods: ["mock"],
|
|
620
|
+
discountRatio: 0.25,
|
|
621
|
+
status: "ok",
|
|
622
|
+
modelCount: 1
|
|
623
|
+
}
|
|
402
624
|
]
|
|
403
625
|
}));
|
|
404
626
|
return;
|
|
@@ -406,11 +628,14 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
406
628
|
res.statusCode = 404;
|
|
407
629
|
res.end(JSON.stringify({ error: "not_found" }));
|
|
408
630
|
});
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
631
|
+
proxyServer.listen(0, "127.0.0.1", () => {
|
|
632
|
+
proxyPort = (proxyServer.address() as AddressInfo).port;
|
|
633
|
+
controlServer.listen(0, "127.0.0.1", () => {
|
|
634
|
+
controlPort = (controlServer.address() as AddressInfo).port;
|
|
635
|
+
process.env.TB_PROXYD_CONTROL_PORT = String(controlPort);
|
|
636
|
+
process.env.TB_PROXYD_PROXY_PORT = String(proxyPort);
|
|
637
|
+
done();
|
|
638
|
+
});
|
|
414
639
|
});
|
|
415
640
|
});
|
|
416
641
|
|
|
@@ -426,7 +651,7 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
426
651
|
process.env.TB_PROXYD_PROXY_PORT = previousProxyPort;
|
|
427
652
|
}
|
|
428
653
|
jest.restoreAllMocks();
|
|
429
|
-
controlServer.close(done);
|
|
654
|
+
controlServer.close(() => proxyServer.close(done));
|
|
430
655
|
});
|
|
431
656
|
|
|
432
657
|
test("doctor --json reports daemon and provider state", async () => {
|
|
@@ -442,7 +667,7 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
442
667
|
expect(parsed.daemon).toMatchObject({
|
|
443
668
|
running: true,
|
|
444
669
|
controlPort,
|
|
445
|
-
proxyPort
|
|
670
|
+
proxyPort,
|
|
446
671
|
fixAvailable: true
|
|
447
672
|
});
|
|
448
673
|
expect(parsed.repair).toMatchObject({
|
|
@@ -450,12 +675,46 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
450
675
|
attempted: false,
|
|
451
676
|
fixed: false
|
|
452
677
|
});
|
|
678
|
+
expect(parsed.access).toMatchObject({
|
|
679
|
+
token: "TOKENBUDDY_PROXY",
|
|
680
|
+
controlBaseUrl: `http://127.0.0.1:${controlPort}`,
|
|
681
|
+
proxyBaseUrl: `http://127.0.0.1:${proxyPort}`,
|
|
682
|
+
});
|
|
683
|
+
expect(parsed.access.endpoints).toEqual(expect.arrayContaining([
|
|
684
|
+
expect.objectContaining({ id: "control.health", available: true }),
|
|
685
|
+
expect.objectContaining({ id: "proxy.openai", available: true, token: "TOKENBUDDY_PROXY" })
|
|
686
|
+
]));
|
|
687
|
+
expect(parsed.sellers).toMatchObject({
|
|
688
|
+
available: true,
|
|
689
|
+
registryUrl: "https://example.test/registry/sellers",
|
|
690
|
+
defaultSeller: "json-test-seller",
|
|
691
|
+
});
|
|
692
|
+
expect(parsed.models).toMatchObject({
|
|
693
|
+
available: true,
|
|
694
|
+
count: 1,
|
|
695
|
+
registryUrl: "https://example.test/registry/sellers",
|
|
696
|
+
});
|
|
453
697
|
expect(parsed.providers).toEqual(expect.arrayContaining([
|
|
454
698
|
expect.objectContaining({ id: "codex" }),
|
|
455
699
|
expect.objectContaining({ id: "claude-code" })
|
|
456
700
|
]));
|
|
457
701
|
});
|
|
458
702
|
|
|
703
|
+
test("doctor prints progress messages without repeating the model list", async () => {
|
|
704
|
+
const output: string[] = [];
|
|
705
|
+
jest.spyOn(console, "log").mockImplementation((message?: unknown) => {
|
|
706
|
+
output.push(String(message));
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
await buildCli().parseAsync(["node", "tb", "doctor"]);
|
|
710
|
+
|
|
711
|
+
const joined = output.join("\n");
|
|
712
|
+
expect(joined).toContain("Checking local control plane and proxy endpoints...");
|
|
713
|
+
expect(joined).toContain("Refreshing seller registry...");
|
|
714
|
+
expect(joined).toContain("Model catalog hidden in `tb doctor`. Run `tb models` for the current model summary.");
|
|
715
|
+
expect(joined).not.toContain("Unique models:");
|
|
716
|
+
});
|
|
717
|
+
|
|
459
718
|
test("models --json returns daemon model data", async () => {
|
|
460
719
|
const output: string[] = [];
|
|
461
720
|
jest.spyOn(console, "log").mockImplementation((message?: unknown) => {
|
|
@@ -466,47 +725,127 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
466
725
|
|
|
467
726
|
expect(output).toHaveLength(1);
|
|
468
727
|
const parsed = JSON.parse(output[0]) as any;
|
|
469
|
-
expect(parsed.
|
|
470
|
-
{
|
|
728
|
+
expect(parsed.grouped).toEqual([
|
|
729
|
+
expect.objectContaining({
|
|
730
|
+
id: "gpt-4",
|
|
731
|
+
sellerCount: 1,
|
|
732
|
+
discountRange: "0.25",
|
|
733
|
+
priceRange: "in $1 / out $3"
|
|
734
|
+
})
|
|
471
735
|
]);
|
|
472
736
|
});
|
|
737
|
+
|
|
738
|
+
test("models prints the doctor-style grouped model summary", async () => {
|
|
739
|
+
const output: string[] = [];
|
|
740
|
+
jest.spyOn(console, "log").mockImplementation((message?: unknown) => {
|
|
741
|
+
output.push(String(message));
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
await buildCli().parseAsync(["node", "tb", "models"]);
|
|
745
|
+
|
|
746
|
+
const joined = output.join("\n");
|
|
747
|
+
expect(joined).toContain("Model catalog refresh complete.");
|
|
748
|
+
expect(joined).toContain("Unique models: 1");
|
|
749
|
+
expect(joined).toContain("Seller offers: 1");
|
|
750
|
+
expect(joined).toContain("Model ID");
|
|
751
|
+
expect(joined).toContain("Seller Count");
|
|
752
|
+
expect(joined).toContain("Discount Range");
|
|
753
|
+
expect(joined).toContain("Price Range");
|
|
754
|
+
expect(joined).toContain("gpt-4");
|
|
755
|
+
expect(joined).toContain("0.25");
|
|
756
|
+
expect(joined).toContain("$1");
|
|
757
|
+
expect(joined).toContain("$3");
|
|
758
|
+
});
|
|
473
759
|
});
|
|
474
760
|
|
|
475
761
|
describe("Provider install planning", () => {
|
|
476
762
|
const PROVIDER_HOME = path.resolve(__dirname, "../../data-test/provider-home");
|
|
477
763
|
const PROVIDER_STORE_ROOT = path.resolve(__dirname, "../../data-test/provider-store");
|
|
764
|
+
const PROVIDER_BIN_ROOT = path.resolve(__dirname, "../../data-test/provider-bin");
|
|
478
765
|
const proxyUrl = "http://127.0.0.1:17821";
|
|
766
|
+
let previousPath: string | undefined;
|
|
767
|
+
|
|
768
|
+
function writeExecutable(name: string): void {
|
|
769
|
+
const executablePath = path.join(PROVIDER_BIN_ROOT, name);
|
|
770
|
+
fs.writeFileSync(executablePath, "#!/bin/sh\nexit 0\n", "utf8");
|
|
771
|
+
fs.chmodSync(executablePath, 0o755);
|
|
772
|
+
}
|
|
479
773
|
|
|
480
774
|
beforeEach(() => {
|
|
481
775
|
rmDir(PROVIDER_HOME);
|
|
482
776
|
rmDir(PROVIDER_STORE_ROOT);
|
|
777
|
+
rmDir(PROVIDER_BIN_ROOT);
|
|
483
778
|
fs.mkdirSync(path.join(PROVIDER_HOME, ".codex"), { recursive: true });
|
|
484
779
|
fs.mkdirSync(path.join(PROVIDER_HOME, ".claude"), { recursive: true });
|
|
485
780
|
fs.mkdirSync(path.join(PROVIDER_HOME, ".openclaw"), { recursive: true });
|
|
781
|
+
fs.mkdirSync(path.join(PROVIDER_HOME, ".config", "opencode"), { recursive: true });
|
|
782
|
+
fs.mkdirSync(PROVIDER_BIN_ROOT, { recursive: true });
|
|
783
|
+
writeExecutable("codex");
|
|
784
|
+
writeExecutable("claude");
|
|
785
|
+
writeExecutable("openclaw");
|
|
786
|
+
writeExecutable("opencode");
|
|
787
|
+
writeExecutable("hermes");
|
|
788
|
+
previousPath = process.env.PATH;
|
|
789
|
+
process.env.PATH = `${PROVIDER_BIN_ROOT}${path.delimiter}${previousPath || ""}`;
|
|
486
790
|
fs.writeFileSync(path.join(PROVIDER_HOME, ".codex", "config.toml"), "approval_policy = \"never\"\n", "utf8");
|
|
487
791
|
fs.writeFileSync(path.join(PROVIDER_HOME, ".claude", "settings.json"), JSON.stringify({ theme: "dark" }, null, 2), "utf8");
|
|
488
792
|
fs.writeFileSync(path.join(PROVIDER_HOME, ".openclaw", "config.json"), JSON.stringify({ keep: "field" }, null, 2), "utf8");
|
|
793
|
+
fs.writeFileSync(path.join(PROVIDER_HOME, ".config", "opencode", "opencode.json"), JSON.stringify({ share: "disabled" }, null, 2), "utf8");
|
|
489
794
|
});
|
|
490
795
|
|
|
491
796
|
afterEach(() => {
|
|
492
797
|
rmDir(PROVIDER_HOME);
|
|
493
798
|
rmDir(PROVIDER_STORE_ROOT);
|
|
799
|
+
rmDir(PROVIDER_BIN_ROOT);
|
|
800
|
+
if (previousPath === undefined) {
|
|
801
|
+
delete process.env.PATH;
|
|
802
|
+
} else {
|
|
803
|
+
process.env.PATH = previousPath;
|
|
804
|
+
}
|
|
494
805
|
});
|
|
495
806
|
|
|
496
807
|
test("detects providers and previews without mutating files", () => {
|
|
497
808
|
const codexBefore = fs.readFileSync(path.join(PROVIDER_HOME, ".codex", "config.toml"), "utf8");
|
|
498
809
|
const providers = detectProviders({ home: PROVIDER_HOME });
|
|
499
810
|
expect(providers).toEqual(expect.arrayContaining([
|
|
500
|
-
expect.objectContaining({ id: "codex",
|
|
501
|
-
expect.objectContaining({ id: "claude-code",
|
|
502
|
-
expect.objectContaining({ id: "openclaw",
|
|
503
|
-
expect.objectContaining({ id: "hermes",
|
|
811
|
+
expect.objectContaining({ id: "codex", status: "configured", configured: true }),
|
|
812
|
+
expect.objectContaining({ id: "claude-code", status: "configured", configured: true }),
|
|
813
|
+
expect.objectContaining({ id: "openclaw", status: "configured", configured: true }),
|
|
814
|
+
expect.objectContaining({ id: "hermes", status: "installed", configured: false })
|
|
504
815
|
]));
|
|
505
816
|
|
|
506
817
|
const changes = previewProviderInstall({
|
|
507
818
|
providers: ["codex", "claude-code", "openclaw", "hermes"],
|
|
508
819
|
proxyUrl,
|
|
509
|
-
|
|
820
|
+
providerSelections: {
|
|
821
|
+
codex: {
|
|
822
|
+
selectionKind: "single-model",
|
|
823
|
+
protocolPreference: "responses",
|
|
824
|
+
defaultModel: "gpt-4",
|
|
825
|
+
},
|
|
826
|
+
"claude-code": {
|
|
827
|
+
selectionKind: "claude-role-mapping",
|
|
828
|
+
protocolPreference: "messages",
|
|
829
|
+
fallbackModel: "MiniMax-M2.7-highspeed",
|
|
830
|
+
roles: {
|
|
831
|
+
sonnet: {
|
|
832
|
+
upstreamModel: "MiniMax-M2.7-highspeed",
|
|
833
|
+
displayName: "MiniMax-M2.7-highspeed",
|
|
834
|
+
declareOneM: true,
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
openclaw: {
|
|
839
|
+
selectionKind: "single-model",
|
|
840
|
+
protocolPreference: "chat_completions",
|
|
841
|
+
defaultModel: "gpt-4",
|
|
842
|
+
},
|
|
843
|
+
hermes: {
|
|
844
|
+
selectionKind: "single-model",
|
|
845
|
+
protocolPreference: "chat_completions",
|
|
846
|
+
defaultModel: "gpt-4",
|
|
847
|
+
},
|
|
848
|
+
},
|
|
510
849
|
home: PROVIDER_HOME
|
|
511
850
|
});
|
|
512
851
|
|
|
@@ -517,13 +856,89 @@ describe("Provider install planning", () => {
|
|
|
517
856
|
expect(fs.readFileSync(path.join(PROVIDER_HOME, ".codex", "config.toml"), "utf8")).toBe(codexBefore);
|
|
518
857
|
});
|
|
519
858
|
|
|
859
|
+
test("reports installed-only providers when executable or native config hints exist", () => {
|
|
860
|
+
fs.rmSync(path.join(PROVIDER_HOME, ".openclaw", "config.json"), { force: true });
|
|
861
|
+
fs.writeFileSync(path.join(PROVIDER_HOME, ".openclaw", "openclaw.json"), JSON.stringify({ profile: "default" }, null, 2), "utf8");
|
|
862
|
+
fs.mkdirSync(path.join(PROVIDER_HOME, ".hermes"), { recursive: true });
|
|
863
|
+
fs.writeFileSync(path.join(PROVIDER_HOME, ".hermes", "config.yaml"), "model: gpt-4\n", "utf8");
|
|
864
|
+
|
|
865
|
+
const providers = detectProviders({ home: PROVIDER_HOME });
|
|
866
|
+
expect(providers).toEqual(expect.arrayContaining([
|
|
867
|
+
expect.objectContaining({
|
|
868
|
+
id: "openclaw",
|
|
869
|
+
status: "installed",
|
|
870
|
+
configured: false,
|
|
871
|
+
executablePath: expect.stringContaining(path.join("provider-bin", "openclaw")),
|
|
872
|
+
observedPaths: expect.arrayContaining([path.join(PROVIDER_HOME, ".openclaw", "openclaw.json")]),
|
|
873
|
+
}),
|
|
874
|
+
expect.objectContaining({
|
|
875
|
+
id: "hermes",
|
|
876
|
+
status: "installed",
|
|
877
|
+
configured: false,
|
|
878
|
+
executablePath: expect.stringContaining(path.join("provider-bin", "hermes")),
|
|
879
|
+
observedPaths: expect.arrayContaining([path.join(PROVIDER_HOME, ".hermes", "config.yaml")]),
|
|
880
|
+
}),
|
|
881
|
+
]));
|
|
882
|
+
});
|
|
883
|
+
|
|
520
884
|
test("applies provider config and rolls back existing and created files", () => {
|
|
521
885
|
const store = new BuyerStore({ root: PROVIDER_STORE_ROOT });
|
|
522
886
|
try {
|
|
523
887
|
const applied = applyProviderInstall({
|
|
524
|
-
providers: ["codex", "claude-code", "openclaw", "hermes"],
|
|
888
|
+
providers: ["codex", "claude-code", "openclaw", "opencode", "hermes"],
|
|
525
889
|
proxyUrl,
|
|
526
|
-
|
|
890
|
+
providerSelections: {
|
|
891
|
+
codex: {
|
|
892
|
+
selectionKind: "single-model",
|
|
893
|
+
protocolPreference: "responses",
|
|
894
|
+
defaultModel: "gpt-4",
|
|
895
|
+
sellerId: "seller-one",
|
|
896
|
+
},
|
|
897
|
+
"claude-code": {
|
|
898
|
+
selectionKind: "claude-role-mapping",
|
|
899
|
+
protocolPreference: "messages",
|
|
900
|
+
fallbackModel: "MiniMax-M2.7-highspeed",
|
|
901
|
+
roles: {
|
|
902
|
+
sonnet: {
|
|
903
|
+
upstreamModel: "MiniMax-M2.7-highspeed",
|
|
904
|
+
displayName: "MiniMax-M2.7-highspeed",
|
|
905
|
+
declareOneM: true,
|
|
906
|
+
},
|
|
907
|
+
opus: {
|
|
908
|
+
upstreamModel: "MiniMax-M2.7-highspeed",
|
|
909
|
+
displayName: "MiniMax-M2.7-highspeed",
|
|
910
|
+
declareOneM: true,
|
|
911
|
+
},
|
|
912
|
+
haiku: {
|
|
913
|
+
upstreamModel: "MiniMax-M2.7-highspeed",
|
|
914
|
+
displayName: "MiniMax-M2.7-highspeed",
|
|
915
|
+
declareOneM: false,
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
openclaw: {
|
|
920
|
+
selectionKind: "single-model",
|
|
921
|
+
protocolPreference: "chat_completions",
|
|
922
|
+
defaultModel: "gpt-4",
|
|
923
|
+
sellerId: "seller-one",
|
|
924
|
+
},
|
|
925
|
+
opencode: {
|
|
926
|
+
selectionKind: "single-model",
|
|
927
|
+
protocolPreference: "responses",
|
|
928
|
+
defaultModel: "gpt-4",
|
|
929
|
+
sellerId: "seller-one",
|
|
930
|
+
},
|
|
931
|
+
hermes: {
|
|
932
|
+
selectionKind: "single-model",
|
|
933
|
+
protocolPreference: "chat_completions",
|
|
934
|
+
defaultModel: "gpt-4",
|
|
935
|
+
sellerId: "seller-one",
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
sellerRouting: {
|
|
939
|
+
mode: "fixed",
|
|
940
|
+
sellerId: "seller-one",
|
|
941
|
+
},
|
|
527
942
|
home: PROVIDER_HOME
|
|
528
943
|
}, store);
|
|
529
944
|
expect(applied).toEqual(expect.arrayContaining([
|
|
@@ -539,15 +954,30 @@ describe("Provider install planning", () => {
|
|
|
539
954
|
const claude = JSON.parse(fs.readFileSync(path.join(PROVIDER_HOME, ".claude", "settings.json"), "utf8"));
|
|
540
955
|
expect(claude.theme).toBe("dark");
|
|
541
956
|
expect(claude.env.ANTHROPIC_BASE_URL).toBe(proxyUrl);
|
|
957
|
+
expect(claude.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("claude-sonnet-4-6[1M]");
|
|
958
|
+
expect(claude.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe("claude-opus-4-7[1M]");
|
|
959
|
+
expect(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe("claude-haiku-4-5");
|
|
960
|
+
expect(claude.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME).toBe("MiniMax-M2.7-highspeed");
|
|
961
|
+
expect(store.getProviderRuntimeConfig("claude-code")).toBeDefined();
|
|
962
|
+
expect(store.getDaemonRuntimeConfig("routing")).toMatchObject({
|
|
963
|
+
config: expect.objectContaining({
|
|
964
|
+
mode: "fixed",
|
|
965
|
+
sellerId: "seller-one",
|
|
966
|
+
}),
|
|
967
|
+
});
|
|
542
968
|
|
|
543
969
|
const openclaw = JSON.parse(fs.readFileSync(path.join(PROVIDER_HOME, ".openclaw", "config.json"), "utf8"));
|
|
544
970
|
expect(openclaw.keep).toBe("field");
|
|
545
971
|
expect(openclaw.api_url).toBe(proxyUrl);
|
|
972
|
+
const opencode = JSON.parse(fs.readFileSync(path.join(PROVIDER_HOME, ".config", "opencode", "opencode.json"), "utf8"));
|
|
973
|
+
expect(opencode.share).toBe("disabled");
|
|
974
|
+
expect(opencode.provider.tokenbuddy.options.baseURL).toBe(`${proxyUrl}/v1`);
|
|
975
|
+
expect(opencode.provider.tokenbuddy.models["gpt-4"].name).toBe("gpt-4");
|
|
546
976
|
expect(fs.existsSync(path.join(PROVIDER_HOME, ".hermes", "settings.json"))).toBe(true);
|
|
547
977
|
expect(store.getProviderInstallSnapshot("codex")).toBeDefined();
|
|
548
978
|
|
|
549
979
|
const rolledBack = rollbackProviderInstall({
|
|
550
|
-
providers: ["codex", "claude-code", "openclaw", "hermes"],
|
|
980
|
+
providers: ["codex", "claude-code", "openclaw", "opencode", "hermes"],
|
|
551
981
|
home: PROVIDER_HOME
|
|
552
982
|
}, store);
|
|
553
983
|
|
|
@@ -557,8 +987,11 @@ describe("Provider install planning", () => {
|
|
|
557
987
|
]));
|
|
558
988
|
expect(fs.readFileSync(path.join(PROVIDER_HOME, ".codex", "config.toml"), "utf8")).toBe("approval_policy = \"never\"\n");
|
|
559
989
|
expect(JSON.parse(fs.readFileSync(path.join(PROVIDER_HOME, ".openclaw", "config.json"), "utf8"))).toEqual({ keep: "field" });
|
|
990
|
+
expect(JSON.parse(fs.readFileSync(path.join(PROVIDER_HOME, ".config", "opencode", "opencode.json"), "utf8"))).toEqual({ share: "disabled" });
|
|
560
991
|
expect(fs.existsSync(path.join(PROVIDER_HOME, ".hermes", "settings.json"))).toBe(false);
|
|
561
992
|
expect(store.getProviderInstallSnapshot("codex")).toBeUndefined();
|
|
993
|
+
expect(store.getProviderRuntimeConfig("claude-code")).toBeUndefined();
|
|
994
|
+
expect(store.getDaemonRuntimeConfig("routing")).toBeUndefined();
|
|
562
995
|
} finally {
|
|
563
996
|
store.close();
|
|
564
997
|
}
|
|
@@ -703,6 +1136,29 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
703
1136
|
idempotencyKey: req.headers["idempotency-key"] as string | undefined,
|
|
704
1137
|
body
|
|
705
1138
|
});
|
|
1139
|
+
if (body.requestId === "responses_req_stream_shape") {
|
|
1140
|
+
res.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
1141
|
+
res.write('event: response.created\ndata: {"type":"response.created","response":{"id":"resp_stream_shape","object":"response","model":"gpt-4.1-mini","status":"in_progress","output":[]}}\n\n');
|
|
1142
|
+
res.write('event: response.output_item.added\ndata: {"type":"response.output_item.added","item":{"type":"message","id":"item_stream_shape","role":"assistant","status":"in_progress"},"sequence_number":1}\n\n');
|
|
1143
|
+
res.write('event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"hello","item_id":"item_stream_shape","sequence_number":2}\n\n');
|
|
1144
|
+
res.write('event: response.output_text.done\ndata: {"type":"response.output_text.done","item_id":"item_stream_shape","sequence_number":3}\n\n');
|
|
1145
|
+
res.write('event: response.output_item.done\ndata: {"type":"response.output_item.done","item":{"type":"message","id":"item_stream_shape","status":"completed"},"sequence_number":4}\n\n');
|
|
1146
|
+
res.end('event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_stream_shape","object":"response","model":"gpt-4.1-mini","status":"completed","output":[],"usage":{"input_tokens":10,"output_tokens":1,"total_tokens":11}},"sequence_number":5}\n\n');
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (body.requestId === "responses_req_br") {
|
|
1150
|
+
const compressed = zlib.brotliCompressSync(Buffer.from(JSON.stringify({
|
|
1151
|
+
id: "resp-br",
|
|
1152
|
+
usage: { input_tokens: 7, output_tokens: 9 }
|
|
1153
|
+
})));
|
|
1154
|
+
res.writeHead(200, {
|
|
1155
|
+
"Content-Type": "application/json",
|
|
1156
|
+
"Content-Encoding": "br",
|
|
1157
|
+
"Content-Length": compressed.byteLength
|
|
1158
|
+
});
|
|
1159
|
+
res.end(compressed);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
706
1162
|
res.end(JSON.stringify({
|
|
707
1163
|
id: "resp-mock",
|
|
708
1164
|
usage: { input_tokens: 7, output_tokens: 9 }
|
|
@@ -772,6 +1228,28 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
772
1228
|
prompt: "raw control prompt",
|
|
773
1229
|
response: "raw control response"
|
|
774
1230
|
});
|
|
1231
|
+
seedStore.saveProviderRuntimeConfig("claude-code", {
|
|
1232
|
+
selectionKind: "claude-role-mapping",
|
|
1233
|
+
protocolPreference: "messages",
|
|
1234
|
+
fallbackModel: "claude-3-5-sonnet",
|
|
1235
|
+
roles: {
|
|
1236
|
+
sonnet: {
|
|
1237
|
+
upstreamModel: "claude-3-5-sonnet",
|
|
1238
|
+
displayName: "Claude Sonnet Mock",
|
|
1239
|
+
declareOneM: true
|
|
1240
|
+
},
|
|
1241
|
+
opus: {
|
|
1242
|
+
upstreamModel: "claude-3-5-sonnet",
|
|
1243
|
+
displayName: "Claude Opus Mock",
|
|
1244
|
+
declareOneM: true
|
|
1245
|
+
},
|
|
1246
|
+
haiku: {
|
|
1247
|
+
upstreamModel: "claude-3-5-sonnet",
|
|
1248
|
+
displayName: "Claude Haiku Mock",
|
|
1249
|
+
declareOneM: false
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
775
1253
|
seedStore.close();
|
|
776
1254
|
|
|
777
1255
|
daemon = new TokenbuddyDaemon({
|
|
@@ -954,6 +1432,19 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
954
1432
|
expect(largeResponses.ok).toBe(true);
|
|
955
1433
|
expect((await largeResponses.json() as any).id).toBe("resp-mock");
|
|
956
1434
|
|
|
1435
|
+
const compressedResponses = await fetch(`${proxyUrl}/v1/responses`, {
|
|
1436
|
+
method: "POST",
|
|
1437
|
+
headers: { "Content-Type": "application/json" },
|
|
1438
|
+
body: JSON.stringify({
|
|
1439
|
+
model: "gpt-4.1-mini",
|
|
1440
|
+
input: "raw compressed responses prompt secret",
|
|
1441
|
+
requestId: "responses_req_br"
|
|
1442
|
+
})
|
|
1443
|
+
});
|
|
1444
|
+
expect(compressedResponses.ok).toBe(true);
|
|
1445
|
+
expect(compressedResponses.headers.get("content-encoding")).toBeNull();
|
|
1446
|
+
expect((await compressedResponses.json() as any).id).toBe("resp-br");
|
|
1447
|
+
|
|
957
1448
|
for (const endpoint of ["/v1/messages", "/messages"]) {
|
|
958
1449
|
const message = await fetch(`${proxyUrl}${endpoint}`, {
|
|
959
1450
|
method: "POST",
|
|
@@ -985,6 +1476,30 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
985
1476
|
expect(publicOutput).not.toContain("raw anthropic prompt secret");
|
|
986
1477
|
});
|
|
987
1478
|
|
|
1479
|
+
test("maps Claude role aliases to upstream models before message routing", async () => {
|
|
1480
|
+
for (const model of ["sonnet", "claude-sonnet-4-6[1M]"]) {
|
|
1481
|
+
const message = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/messages`, {
|
|
1482
|
+
method: "POST",
|
|
1483
|
+
headers: { "Content-Type": "application/json" },
|
|
1484
|
+
body: JSON.stringify({
|
|
1485
|
+
model,
|
|
1486
|
+
messages: [{ role: "user", content: "role mapping request" }]
|
|
1487
|
+
})
|
|
1488
|
+
});
|
|
1489
|
+
expect(message.ok).toBe(true);
|
|
1490
|
+
expect((await message.json() as any).id).toBe("msg-mock");
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const messageRequests = sellerRequests.filter((request) => request.url === "/v1/messages");
|
|
1494
|
+
expect(messageRequests).toEqual(expect.arrayContaining([
|
|
1495
|
+
expect.objectContaining({
|
|
1496
|
+
body: expect.objectContaining({
|
|
1497
|
+
model: "claude-3-5-sonnet"
|
|
1498
|
+
})
|
|
1499
|
+
})
|
|
1500
|
+
]));
|
|
1501
|
+
});
|
|
1502
|
+
|
|
988
1503
|
test("passes through streaming chat responses and records safe ledger metadata", async () => {
|
|
989
1504
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
990
1505
|
method: "POST",
|
|
@@ -1016,6 +1531,28 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1016
1531
|
expect(publicOutput).not.toContain("chatcmpl-stream");
|
|
1017
1532
|
});
|
|
1018
1533
|
|
|
1534
|
+
test("normalizes responses SSE shape for OpenCode-compatible consumers", async () => {
|
|
1535
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/responses`, {
|
|
1536
|
+
method: "POST",
|
|
1537
|
+
headers: { "Content-Type": "application/json" },
|
|
1538
|
+
body: JSON.stringify({
|
|
1539
|
+
model: "gpt-4.1-mini",
|
|
1540
|
+
input: "shape normalization",
|
|
1541
|
+
requestId: "responses_req_stream_shape",
|
|
1542
|
+
stream: true
|
|
1543
|
+
})
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
expect(response.ok).toBe(true);
|
|
1547
|
+
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
|
1548
|
+
const body = await response.text();
|
|
1549
|
+
expect(body).toContain("response.content_part.added");
|
|
1550
|
+
expect(body).toContain("response.content_part.done");
|
|
1551
|
+
expect(body).toContain("\"item_id\":\"item_stream_shape\"");
|
|
1552
|
+
expect(body).toContain("\"output_text\":\"hello\"");
|
|
1553
|
+
expect(body).toContain("\"content\":[{\"type\":\"output_text\",\"text\":\"hello\",\"annotations\":[]}]");
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1019
1556
|
test("fails closed when no compatible seller can serve the requested model", async () => {
|
|
1020
1557
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
1021
1558
|
method: "POST",
|
|
@@ -1108,6 +1645,41 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
1108
1645
|
return;
|
|
1109
1646
|
}
|
|
1110
1647
|
|
|
1648
|
+
if (req.url === "/backup/purchase/create") {
|
|
1649
|
+
expect(body.paymentMethod).toBe("mock");
|
|
1650
|
+
events.push({ seller: "backup-seller", url: req.url });
|
|
1651
|
+
res.end(JSON.stringify({
|
|
1652
|
+
purchaseId: "pur_backup_123",
|
|
1653
|
+
status: "pending",
|
|
1654
|
+
creditMicros: 2000000,
|
|
1655
|
+
currency: "USD",
|
|
1656
|
+
expiresAt: new Date(Date.now() + 86400 * 1000).toISOString()
|
|
1657
|
+
}));
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (req.url === "/backup/purchase/complete") {
|
|
1662
|
+
events.push({ seller: "backup-seller", url: req.url });
|
|
1663
|
+
res.end(JSON.stringify({
|
|
1664
|
+
purchaseId: "pur_backup_123",
|
|
1665
|
+
status: "active",
|
|
1666
|
+
accessToken: "tok_backup_token_abc",
|
|
1667
|
+
tokenClass: "model:gpt-manual",
|
|
1668
|
+
creditMicros: 2000000,
|
|
1669
|
+
currency: "USD"
|
|
1670
|
+
}));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (req.url === "/backup/v1/chat/completions") {
|
|
1675
|
+
events.push({ seller: "backup-seller", url: req.url });
|
|
1676
|
+
res.end(JSON.stringify({
|
|
1677
|
+
id: "backup-chat",
|
|
1678
|
+
usage: { prompt_tokens: 4, completion_tokens: 5 }
|
|
1679
|
+
}));
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1111
1683
|
if (req.url?.startsWith("/backup/")) {
|
|
1112
1684
|
events.push({ seller: "backup-seller", url: req.url });
|
|
1113
1685
|
res.end(JSON.stringify({ id: "backup-should-not-run" }));
|
|
@@ -1183,4 +1755,41 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
1183
1755
|
expect(purchases.purchases.some((entry: any) => entry.sellerKey === "backup-seller")).toBe(false);
|
|
1184
1756
|
expect(inferences.inferences.some((entry: any) => entry.sellerKey === "backup-seller")).toBe(false);
|
|
1185
1757
|
});
|
|
1758
|
+
|
|
1759
|
+
test("pins manual routing to the configured seller id when selectedSellerId is set", async () => {
|
|
1760
|
+
daemon.stop();
|
|
1761
|
+
events.length = 0;
|
|
1762
|
+
daemon = new TokenbuddyDaemon({
|
|
1763
|
+
controlPort: 0,
|
|
1764
|
+
proxyPort: 0,
|
|
1765
|
+
dbPath,
|
|
1766
|
+
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
1767
|
+
selectionMode: "manual",
|
|
1768
|
+
selectedSellerId: "backup-seller"
|
|
1769
|
+
});
|
|
1770
|
+
daemon.start();
|
|
1771
|
+
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
1772
|
+
daemonProxyPort = ((daemon as any).proxyServer.address() as AddressInfo).port;
|
|
1773
|
+
|
|
1774
|
+
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
1775
|
+
expect(status.selectionMode).toBe("manual");
|
|
1776
|
+
expect(status.selectedSellerId).toBe("backup-seller");
|
|
1777
|
+
|
|
1778
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
1779
|
+
method: "POST",
|
|
1780
|
+
headers: { "Content-Type": "application/json" },
|
|
1781
|
+
body: JSON.stringify({
|
|
1782
|
+
model: "gpt-manual",
|
|
1783
|
+
messages: [{ role: "user", content: "manual selected seller should stay pinned" }]
|
|
1784
|
+
})
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
expect(response.ok).toBe(true);
|
|
1788
|
+
expect(events).toEqual([
|
|
1789
|
+
{ seller: "backup-seller", url: "/backup/manifest" },
|
|
1790
|
+
{ seller: "backup-seller", url: "/backup/purchase/create" },
|
|
1791
|
+
{ seller: "backup-seller", url: "/backup/purchase/complete" },
|
|
1792
|
+
{ seller: "backup-seller", url: "/backup/v1/chat/completions" }
|
|
1793
|
+
]);
|
|
1794
|
+
});
|
|
1186
1795
|
});
|