@tokenbuddy/tokenbuddy 1.0.12 → 1.0.13
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 +61 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +12 -0
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +47 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +287 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/credit-tracker.d.ts +26 -0
- package/dist/src/credit-tracker.d.ts.map +1 -1
- package/dist/src/credit-tracker.js +8 -0
- package/dist/src/credit-tracker.js.map +1 -1
- package/dist/src/daemon.d.ts +29 -3
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +292 -65
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
- package/dist/src/doctor-clawtip-wallet.js +13 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +63 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +39 -1
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +103 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -1
- package/dist/src/init-clawtip-activation.js +60 -0
- package/dist/src/init-clawtip-activation.js.map +1 -1
- package/dist/src/init-payment-options.d.ts +124 -0
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +68 -0
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/model-index.d.ts +9 -0
- package/dist/src/model-index.d.ts.map +1 -1
- package/dist/src/model-index.js.map +1 -1
- package/dist/src/prewarm-cache.d.ts +89 -0
- package/dist/src/prewarm-cache.d.ts.map +1 -1
- package/dist/src/prewarm-cache.js +14 -1
- package/dist/src/prewarm-cache.js.map +1 -1
- package/dist/src/prewarm-scheduler.d.ts +62 -3
- package/dist/src/prewarm-scheduler.d.ts.map +1 -1
- package/dist/src/prewarm-scheduler.js +39 -8
- package/dist/src/prewarm-scheduler.js.map +1 -1
- package/dist/src/provider-install.d.ts +89 -3
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +77 -19
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/route-failover.d.ts +48 -0
- package/dist/src/route-failover.d.ts.map +1 -1
- package/dist/src/route-failover.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +158 -10
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +79 -5
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-metadata-cache.d.ts +29 -0
- package/dist/src/seller-metadata-cache.d.ts.map +1 -0
- package/dist/src/seller-metadata-cache.js +71 -0
- package/dist/src/seller-metadata-cache.js.map +1 -0
- package/dist/src/seller-pool.d.ts +71 -0
- package/dist/src/seller-pool.d.ts.map +1 -1
- package/dist/src/seller-pool.js +6 -1
- package/dist/src/seller-pool.js.map +1 -1
- package/dist/src/seller-route-planner.d.ts +118 -0
- package/dist/src/seller-route-planner.d.ts.map +1 -0
- package/dist/src/seller-route-planner.js +160 -0
- package/dist/src/seller-route-planner.js.map +1 -0
- package/dist/src/seller-routing-config.d.ts +69 -0
- package/dist/src/seller-routing-config.d.ts.map +1 -0
- package/dist/src/seller-routing-config.js +164 -0
- package/dist/src/seller-routing-config.js.map +1 -0
- package/dist/src/seller-routing-strategy.d.ts +118 -0
- package/dist/src/seller-routing-strategy.d.ts.map +1 -0
- package/dist/src/seller-routing-strategy.js +183 -0
- package/dist/src/seller-routing-strategy.js.map +1 -0
- package/dist/src/stream-failover.d.ts +23 -0
- package/dist/src/stream-failover.d.ts.map +1 -1
- package/dist/src/stream-failover.js +4 -0
- package/dist/src/stream-failover.js.map +1 -1
- package/dist/src/tb-proxyd.js +7 -21
- package/dist/src/tb-proxyd.js.map +1 -1
- package/dist/src/terminal-detect.d.ts +51 -0
- package/dist/src/terminal-detect.d.ts.map +1 -1
- package/dist/src/terminal-detect.js +42 -0
- package/dist/src/terminal-detect.js.map +1 -1
- package/dist/src/terminal-image.d.ts +41 -0
- package/dist/src/terminal-image.d.ts.map +1 -1
- package/dist/src/terminal-image.js +15 -0
- package/dist/src/terminal-image.js.map +1 -1
- package/package.json +1 -1
- package/src/buyer-store.ts +61 -0
- package/src/cli.ts +330 -68
- package/src/credit-tracker.ts +26 -0
- package/src/daemon.ts +363 -72
- package/src/doctor-clawtip-wallet.ts +25 -0
- package/src/doctor-diagnostics.ts +63 -1
- package/src/index.ts +4 -0
- package/src/init-clawtip-activation.ts +103 -0
- package/src/init-payment-options.ts +124 -0
- package/src/model-index.ts +9 -0
- package/src/prewarm-cache.ts +99 -1
- package/src/prewarm-scheduler.ts +97 -12
- package/src/provider-install.ts +125 -27
- package/src/route-failover.ts +48 -0
- package/src/seller-catalog.ts +158 -12
- package/src/seller-metadata-cache.ts +91 -0
- package/src/seller-pool.ts +77 -1
- package/src/seller-route-planner.ts +323 -0
- package/src/seller-routing-config.ts +198 -0
- package/src/seller-routing-strategy.ts +316 -0
- package/src/stream-failover.ts +23 -0
- package/src/tb-proxyd.ts +7 -23
- package/src/terminal-detect.ts +51 -0
- package/src/terminal-image.ts +41 -0
- package/tests/cli-routing.test.ts +287 -0
- package/tests/daemon-classify.test.ts +431 -0
- package/tests/daemon-roles.test.ts +92 -0
- package/tests/seller-catalog-utilities.test.ts +70 -0
- package/tests/seller-metadata-cache.test.ts +89 -0
- package/tests/seller-route-planner.test.ts +150 -0
- package/tests/seller-routing-config.test.ts +111 -0
- package/tests/seller-routing-strategy.test.ts +166 -0
- package/tests/tokenbuddy.test.ts +446 -34
- /package/{src → tests}/credit-tracker.test.ts +0 -0
- /package/{src → tests}/model-index.test.ts +0 -0
- /package/{src → tests}/prewarm-cache.test.ts +0 -0
- /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
- /package/{src → tests}/route-failover.test.ts +0 -0
- /package/{src → tests}/seller-catalog-413.test.ts +0 -0
- /package/{src → tests}/seller-pool.test.ts +0 -0
- /package/{src → tests}/stream-failover.test.ts +0 -0
- /package/{src → tests}/thousand-seller.test.ts +0 -0
package/tests/tokenbuddy.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { TokenbuddyDaemon } from "../src/daemon.js";
|
|
|
2
2
|
import { BuyerStore, resolveBuyerStorePath, type PaymentConfig } from "../src/buyer-store.js";
|
|
3
3
|
import type { ProviderRuntimeConfig } from "../src/provider-install.js";
|
|
4
4
|
import {
|
|
5
|
+
buildLaunchdPlistContent,
|
|
5
6
|
buildCli,
|
|
6
7
|
fetchClawtipBootstrap,
|
|
7
8
|
normalizeClawtipBootstrapResourceUrl,
|
|
@@ -44,6 +45,7 @@ import * as fs from "fs";
|
|
|
44
45
|
import http from "http";
|
|
45
46
|
import { AddressInfo } from "net";
|
|
46
47
|
import zlib from "zlib";
|
|
48
|
+
import { resolveModuleLogFile } from "@tokenbuddy/logging";
|
|
47
49
|
|
|
48
50
|
const TEMP_BUYER_DB = path.resolve(__dirname, "../../data-test/buyer-cache-test.db");
|
|
49
51
|
const TEMP_STORE_ROOT = path.resolve(__dirname, "../../data-test/buyer-store-test");
|
|
@@ -69,7 +71,7 @@ describe("TokenBuddy CLI command surface", () => {
|
|
|
69
71
|
.filter(command => command !== "help")
|
|
70
72
|
.sort();
|
|
71
73
|
|
|
72
|
-
expect(commandNames).toEqual(["doctor", "init", "models", "payment"]);
|
|
74
|
+
expect(commandNames).toEqual(["doctor", "init", "models", "payment", "routing"]);
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
test("tb payment help only exposes list, add, and remove", () => {
|
|
@@ -106,6 +108,49 @@ describe("TokenBuddy CLI command surface", () => {
|
|
|
106
108
|
"tb-proxyd": "./bin/tb-proxyd.js"
|
|
107
109
|
});
|
|
108
110
|
});
|
|
111
|
+
|
|
112
|
+
test("launchd plist pins tb-proxyd ports and seller registry", () => {
|
|
113
|
+
const plist = buildLaunchdPlistContent({
|
|
114
|
+
label: "com.tokenbuddy.proxyd",
|
|
115
|
+
nodePath: "/opt/homebrew/bin/node",
|
|
116
|
+
scriptPath: "/opt/homebrew/bin/tb-proxyd.js",
|
|
117
|
+
stdoutPath: "/Users/example/.tokenbuddy-store/tb-proxyd.stdout.log",
|
|
118
|
+
stderrPath: "/Users/example/.tokenbuddy-store/tb-proxyd.stderr.log",
|
|
119
|
+
controlPort: 17820,
|
|
120
|
+
proxyPort: 17821,
|
|
121
|
+
sellerRegistryUrl: "https://tb-wallet-bootstrap.fly.dev/registry/sellers",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(plist).toContain("<key>EnvironmentVariables</key>");
|
|
125
|
+
expect(plist).toContain("<key>TB_PROXYD_CONTROL_PORT</key>");
|
|
126
|
+
expect(plist).toContain("<string>17820</string>");
|
|
127
|
+
expect(plist).toContain("<key>TB_PROXYD_PROXY_PORT</key>");
|
|
128
|
+
expect(plist).toContain("<string>17821</string>");
|
|
129
|
+
expect(plist).toContain("<key>TB_PROXYD_SELLER_REGISTRY_URL</key>");
|
|
130
|
+
expect(plist).toContain("<string>https://tb-wallet-bootstrap.fly.dev/registry/sellers</string>");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("launchd plist can inject the ClawTip proof provider without embedding proofs", () => {
|
|
134
|
+
const plist = buildLaunchdPlistContent({
|
|
135
|
+
label: "com.tokenbuddy.proxyd",
|
|
136
|
+
nodePath: "/opt/homebrew/bin/node",
|
|
137
|
+
scriptPath: "/opt/homebrew/bin/tb-proxyd.js",
|
|
138
|
+
stdoutPath: "/Users/example/.tokenbuddy-store/tb-proxyd.stdout.log",
|
|
139
|
+
stderrPath: "/Users/example/.tokenbuddy-store/tb-proxyd.stderr.log",
|
|
140
|
+
controlPort: 17820,
|
|
141
|
+
proxyPort: 17821,
|
|
142
|
+
sellerRegistryUrl: "https://tb-wallet-bootstrap.fly.dev/registry/sellers",
|
|
143
|
+
clawtipProofCommand: "/Users/example/bin/tokenbuddy-clawtip-proof-openclaw.sh",
|
|
144
|
+
clawtipProofTimeoutMs: 180000,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(plist).toContain("<key>TB_PROXYD_CLAWTIP_PROOF_COMMAND</key>");
|
|
148
|
+
expect(plist).toContain("<string>/Users/example/bin/tokenbuddy-clawtip-proof-openclaw.sh</string>");
|
|
149
|
+
expect(plist).toContain("<key>TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS</key>");
|
|
150
|
+
expect(plist).toContain("<string>180000</string>");
|
|
151
|
+
expect(plist).not.toContain("payCredential");
|
|
152
|
+
expect(plist).not.toContain("PAYMENT_PROOF");
|
|
153
|
+
});
|
|
109
154
|
});
|
|
110
155
|
|
|
111
156
|
describe("BuyerStore safe SQLite persistence", () => {
|
|
@@ -198,7 +243,6 @@ describe("BuyerStore safe SQLite persistence", () => {
|
|
|
198
243
|
selectionKind: "single-model",
|
|
199
244
|
protocolPreference: "responses",
|
|
200
245
|
defaultModel: "gpt-5.5",
|
|
201
|
-
sellerId: "seller-a",
|
|
202
246
|
});
|
|
203
247
|
store.saveDaemonRuntimeConfig("routing", {
|
|
204
248
|
mode: "fixed",
|
|
@@ -209,7 +253,6 @@ describe("BuyerStore safe SQLite persistence", () => {
|
|
|
209
253
|
providerId: "opencode",
|
|
210
254
|
config: expect.objectContaining({
|
|
211
255
|
defaultModel: "gpt-5.5",
|
|
212
|
-
sellerId: "seller-a",
|
|
213
256
|
}),
|
|
214
257
|
});
|
|
215
258
|
expect(store.getDaemonRuntimeConfig("routing")).toMatchObject({
|
|
@@ -475,6 +518,87 @@ describe("TokenBuddy payment CLI", () => {
|
|
|
475
518
|
expect(logs.join("\n")).toContain("Mock payment method registered");
|
|
476
519
|
expect(logs.join("\n")).toContain("removed");
|
|
477
520
|
});
|
|
521
|
+
|
|
522
|
+
test("routing set/show updates buyer routing config without requiring tb-proxyd", async () => {
|
|
523
|
+
await new Promise<void>((resolve) => controlServer.close(() => {
|
|
524
|
+
controlServerOpen = false;
|
|
525
|
+
resolve();
|
|
526
|
+
}));
|
|
527
|
+
const output: string[] = [];
|
|
528
|
+
jest.spyOn(console, "log").mockImplementation((message?: unknown) => {
|
|
529
|
+
output.push(String(message));
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
await buildCli().parseAsync([
|
|
533
|
+
"node",
|
|
534
|
+
"tb",
|
|
535
|
+
"routing",
|
|
536
|
+
"set",
|
|
537
|
+
"fixedSet",
|
|
538
|
+
"--seller-set",
|
|
539
|
+
"seller-a,seller-b",
|
|
540
|
+
"--scorer",
|
|
541
|
+
"speed"
|
|
542
|
+
]);
|
|
543
|
+
|
|
544
|
+
const store = new BuyerStore({ root: CLI_STORE_ROOT });
|
|
545
|
+
expect(store.getDaemonRuntimeConfig("routing")).toMatchObject({
|
|
546
|
+
config: {
|
|
547
|
+
mode: "fixedSet",
|
|
548
|
+
sellerIds: ["seller-a", "seller-b"],
|
|
549
|
+
scorer: "speed"
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
store.close();
|
|
553
|
+
|
|
554
|
+
output.length = 0;
|
|
555
|
+
await buildCli().parseAsync(["node", "tb", "routing", "show", "--json"]);
|
|
556
|
+
const parsed = JSON.parse(output[0]) as any;
|
|
557
|
+
expect(parsed.routing).toEqual({
|
|
558
|
+
mode: "fixedSet",
|
|
559
|
+
sellerIds: ["seller-a", "seller-b"],
|
|
560
|
+
scorer: "speed"
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await buildCli().parseAsync([
|
|
564
|
+
"node",
|
|
565
|
+
"tb",
|
|
566
|
+
"routing",
|
|
567
|
+
"set",
|
|
568
|
+
"fixed",
|
|
569
|
+
"--seller",
|
|
570
|
+
"seller-c",
|
|
571
|
+
"--scorer",
|
|
572
|
+
"discount"
|
|
573
|
+
]);
|
|
574
|
+
let refreshed = new BuyerStore({ root: CLI_STORE_ROOT });
|
|
575
|
+
expect(refreshed.getDaemonRuntimeConfig("routing")).toMatchObject({
|
|
576
|
+
config: {
|
|
577
|
+
mode: "fixed",
|
|
578
|
+
sellerId: "seller-c",
|
|
579
|
+
scorer: "discount"
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
refreshed.close();
|
|
583
|
+
|
|
584
|
+
await buildCli().parseAsync([
|
|
585
|
+
"node",
|
|
586
|
+
"tb",
|
|
587
|
+
"routing",
|
|
588
|
+
"set",
|
|
589
|
+
"fullAuto",
|
|
590
|
+
"--scorer",
|
|
591
|
+
"balanced"
|
|
592
|
+
]);
|
|
593
|
+
refreshed = new BuyerStore({ root: CLI_STORE_ROOT });
|
|
594
|
+
expect(refreshed.getDaemonRuntimeConfig("routing")).toMatchObject({
|
|
595
|
+
config: {
|
|
596
|
+
mode: "fullAuto",
|
|
597
|
+
scorer: "balanced"
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
refreshed.close();
|
|
601
|
+
});
|
|
478
602
|
});
|
|
479
603
|
|
|
480
604
|
describe("TokenBuddy init payment options", () => {
|
|
@@ -1485,6 +1609,19 @@ describe("Provider install planning", () => {
|
|
|
1485
1609
|
]));
|
|
1486
1610
|
});
|
|
1487
1611
|
|
|
1612
|
+
test("treats existing non-TokenBuddy OpenCode config as installed, not configured", () => {
|
|
1613
|
+
const providers = detectProviders({ home: PROVIDER_HOME });
|
|
1614
|
+
|
|
1615
|
+
expect(providers).toEqual(expect.arrayContaining([
|
|
1616
|
+
expect.objectContaining({
|
|
1617
|
+
id: "opencode",
|
|
1618
|
+
status: "installed",
|
|
1619
|
+
configured: false,
|
|
1620
|
+
executablePath: expect.stringContaining(path.join("provider-bin", "opencode")),
|
|
1621
|
+
}),
|
|
1622
|
+
]));
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1488
1625
|
test("applies provider config and rolls back existing and created files", () => {
|
|
1489
1626
|
const store = new BuyerStore({ root: PROVIDER_STORE_ROOT });
|
|
1490
1627
|
try {
|
|
@@ -1496,7 +1633,6 @@ describe("Provider install planning", () => {
|
|
|
1496
1633
|
selectionKind: "single-model",
|
|
1497
1634
|
protocolPreference: "responses",
|
|
1498
1635
|
defaultModel: "gpt-4",
|
|
1499
|
-
sellerId: "seller-one",
|
|
1500
1636
|
},
|
|
1501
1637
|
"claude-code": {
|
|
1502
1638
|
selectionKind: "claude-role-mapping",
|
|
@@ -1524,25 +1660,18 @@ describe("Provider install planning", () => {
|
|
|
1524
1660
|
selectionKind: "single-model",
|
|
1525
1661
|
protocolPreference: "chat_completions",
|
|
1526
1662
|
defaultModel: "gpt-4",
|
|
1527
|
-
sellerId: "seller-one",
|
|
1528
1663
|
},
|
|
1529
1664
|
opencode: {
|
|
1530
1665
|
selectionKind: "single-model",
|
|
1531
1666
|
protocolPreference: "responses",
|
|
1532
1667
|
defaultModel: "gpt-4",
|
|
1533
|
-
sellerId: "seller-one",
|
|
1534
1668
|
},
|
|
1535
1669
|
hermes: {
|
|
1536
1670
|
selectionKind: "single-model",
|
|
1537
1671
|
protocolPreference: "chat_completions",
|
|
1538
1672
|
defaultModel: "gpt-4",
|
|
1539
|
-
sellerId: "seller-one",
|
|
1540
1673
|
},
|
|
1541
1674
|
},
|
|
1542
|
-
sellerRouting: {
|
|
1543
|
-
mode: "fixed",
|
|
1544
|
-
sellerId: "seller-one",
|
|
1545
|
-
},
|
|
1546
1675
|
home: PROVIDER_HOME
|
|
1547
1676
|
}, store);
|
|
1548
1677
|
expect(applied).toEqual(expect.arrayContaining([
|
|
@@ -1563,20 +1692,24 @@ describe("Provider install planning", () => {
|
|
|
1563
1692
|
expect(claude.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe("claude-haiku-4-5");
|
|
1564
1693
|
expect(claude.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME).toBe("MiniMax-M2.7-highspeed");
|
|
1565
1694
|
expect(store.getProviderRuntimeConfig("claude-code")).toBeDefined();
|
|
1566
|
-
expect(store.getDaemonRuntimeConfig("routing")).
|
|
1567
|
-
|
|
1568
|
-
mode: "fixed",
|
|
1569
|
-
sellerId: "seller-one",
|
|
1570
|
-
}),
|
|
1571
|
-
});
|
|
1695
|
+
expect(store.getDaemonRuntimeConfig("routing")).toBeUndefined();
|
|
1696
|
+
expect(store.getProviderRuntimeConfig("opencode")?.config).not.toHaveProperty("sellerId");
|
|
1572
1697
|
|
|
1573
1698
|
const openclaw = JSON.parse(fs.readFileSync(path.join(PROVIDER_HOME, ".openclaw", "config.json"), "utf8"));
|
|
1574
1699
|
expect(openclaw.keep).toBe("field");
|
|
1575
1700
|
expect(openclaw.api_url).toBe(proxyUrl);
|
|
1576
1701
|
const opencode = JSON.parse(fs.readFileSync(path.join(PROVIDER_HOME, ".config", "opencode", "opencode.json"), "utf8"));
|
|
1577
1702
|
expect(opencode.share).toBe("disabled");
|
|
1703
|
+
expect(JSON.stringify(opencode)).not.toContain("sellerId");
|
|
1578
1704
|
expect(opencode.provider.tokenbuddy.options.baseURL).toBe(`${proxyUrl}/v1`);
|
|
1579
1705
|
expect(opencode.provider.tokenbuddy.models["gpt-4"].name).toBe("gpt-4");
|
|
1706
|
+
expect(detectProviders({ home: PROVIDER_HOME })).toEqual(expect.arrayContaining([
|
|
1707
|
+
expect.objectContaining({
|
|
1708
|
+
id: "opencode",
|
|
1709
|
+
status: "configured",
|
|
1710
|
+
configured: true,
|
|
1711
|
+
}),
|
|
1712
|
+
]));
|
|
1580
1713
|
expect(fs.existsSync(path.join(PROVIDER_HOME, ".hermes", "settings.json"))).toBe(true);
|
|
1581
1714
|
expect(store.getProviderInstallSnapshot("codex")).toBeDefined();
|
|
1582
1715
|
|
|
@@ -1595,20 +1728,12 @@ describe("Provider install planning", () => {
|
|
|
1595
1728
|
expect(fs.existsSync(path.join(PROVIDER_HOME, ".hermes", "settings.json"))).toBe(false);
|
|
1596
1729
|
expect(store.getProviderInstallSnapshot("codex")).toBeUndefined();
|
|
1597
1730
|
expect(store.getProviderRuntimeConfig("claude-code")).toBeUndefined();
|
|
1598
|
-
expect(store.getDaemonRuntimeConfig("routing")).toBeUndefined();
|
|
1599
1731
|
} finally {
|
|
1600
1732
|
store.close();
|
|
1601
1733
|
}
|
|
1602
1734
|
});
|
|
1603
1735
|
|
|
1604
|
-
test("opencode provider install uses @ai-sdk/openai
|
|
1605
|
-
// 锁住不变量:v1.0.12+ tb-proxy install opencode 必须默认用 @ai-sdk/openai。
|
|
1606
|
-
// v1.0.11 改成 @ai-sdk/openai-responses 在 opencode 1.14.28 desktop 上
|
|
1607
|
-
// 报 ProviderModelNotFoundError(SDK 是 dynamic model,opencode 1.14.x 期望静态
|
|
1608
|
-
// languageModel 映射,schema mismatch),对 opencode 用户实际跑不通。
|
|
1609
|
-
// 切回 @ai-sdk/openai 后走 chat completions 协议,opencode 用户能 work
|
|
1610
|
-
// (走 tbs-719577/tbs-825edb 上游)。buyer 端 /v1/responses 协议支持仍完整,
|
|
1611
|
-
// 等 opencode 1.15+ 支持 openai-responses custom provider 再切默认。
|
|
1736
|
+
test("opencode provider install uses @ai-sdk/openai-compatible for chat completions", () => {
|
|
1612
1737
|
const config: ProviderRuntimeConfig = {
|
|
1613
1738
|
selectionKind: "single-model",
|
|
1614
1739
|
protocolPreference: "chat_completions",
|
|
@@ -1624,7 +1749,7 @@ describe("Provider install planning", () => {
|
|
|
1624
1749
|
expect(change).toBeDefined();
|
|
1625
1750
|
expect(change?.content).toBeDefined();
|
|
1626
1751
|
const parsed = JSON.parse(change!.content!);
|
|
1627
|
-
expect(parsed.provider.tokenbuddy.npm).toBe("@ai-sdk/openai");
|
|
1752
|
+
expect(parsed.provider.tokenbuddy.npm).toBe("@ai-sdk/openai-compatible");
|
|
1628
1753
|
expect(parsed.model).toBe("tokenbuddy/gpt-5.4");
|
|
1629
1754
|
expect(parsed.provider.tokenbuddy.options.baseURL).toBe("http://127.0.0.1:17821/v1");
|
|
1630
1755
|
});
|
|
@@ -1803,6 +1928,12 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1803
1928
|
if (body.stream) {
|
|
1804
1929
|
res.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
1805
1930
|
res.write("data: {\"id\":\"chatcmpl-stream\",\"choices\":[{\"delta\":{\"content\":\"hello\"}}]}\n\n");
|
|
1931
|
+
if (body.requestId === "stream_req_slow_after_headers") {
|
|
1932
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
1933
|
+
res.write("data: {\"id\":\"chatcmpl-stream\",\"choices\":[{\"delta\":{\"content\":\" later\"}}]}\n\n");
|
|
1934
|
+
res.end("data: [DONE]\n\n");
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1806
1937
|
res.write("event: tokenbuddy.settlement\n");
|
|
1807
1938
|
res.write(`data: ${JSON.stringify({
|
|
1808
1939
|
requestId: body.requestId || "stream_req_mock",
|
|
@@ -2284,6 +2415,8 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
2284
2415
|
const body = await response.text();
|
|
2285
2416
|
expect(body).toContain("chatcmpl-stream");
|
|
2286
2417
|
expect(body).toContain("[DONE]");
|
|
2418
|
+
expect(body).not.toContain("}data:");
|
|
2419
|
+
expect(body).toContain("}\n\ndata: [DONE]");
|
|
2287
2420
|
expect(body).not.toContain("tokenbuddy.settlement");
|
|
2288
2421
|
expect(sellerRequests.find((request) => request.url === "/v1/chat/completions" && request.body?.stream)?.idempotencyKey).toBe("idem-stream-preserved");
|
|
2289
2422
|
|
|
@@ -2304,6 +2437,35 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
2304
2437
|
expect(publicOutput).not.toContain("chatcmpl-stream");
|
|
2305
2438
|
});
|
|
2306
2439
|
|
|
2440
|
+
test("does not abort an active SSE stream after seller response headers arrive", async () => {
|
|
2441
|
+
const previousDeadline = process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
|
|
2442
|
+
process.env.TB_PROXYD_REQUEST_DEADLINE_MS = "50";
|
|
2443
|
+
try {
|
|
2444
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2445
|
+
method: "POST",
|
|
2446
|
+
headers: { "Content-Type": "application/json" },
|
|
2447
|
+
body: JSON.stringify({
|
|
2448
|
+
model: "gpt-4",
|
|
2449
|
+
stream: true,
|
|
2450
|
+
messages: [{ role: "user", content: "slow active stream" }],
|
|
2451
|
+
requestId: "stream_req_slow_after_headers"
|
|
2452
|
+
})
|
|
2453
|
+
});
|
|
2454
|
+
|
|
2455
|
+
expect(response.ok).toBe(true);
|
|
2456
|
+
const body = await response.text();
|
|
2457
|
+
expect(body).toContain("hello");
|
|
2458
|
+
expect(body).toContain(" later");
|
|
2459
|
+
expect(body).toContain("[DONE]");
|
|
2460
|
+
} finally {
|
|
2461
|
+
if (previousDeadline === undefined) {
|
|
2462
|
+
delete process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
|
|
2463
|
+
} else {
|
|
2464
|
+
process.env.TB_PROXYD_REQUEST_DEADLINE_MS = previousDeadline;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2307
2469
|
test("passes through responses SSE bytes unchanged for OpenAI Responses API clients", async () => {
|
|
2308
2470
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/responses`, {
|
|
2309
2471
|
method: "POST",
|
|
@@ -2347,13 +2509,15 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
2347
2509
|
});
|
|
2348
2510
|
});
|
|
2349
2511
|
|
|
2350
|
-
describe("TokenBuddy
|
|
2512
|
+
describe("TokenBuddy seller routing strategies", () => {
|
|
2351
2513
|
let server: http.Server;
|
|
2352
2514
|
let sellerPort: number;
|
|
2353
2515
|
let daemon: TokenbuddyDaemon;
|
|
2354
2516
|
let daemonProxyPort: number;
|
|
2355
2517
|
let daemonControlPort: number;
|
|
2356
2518
|
const events: Array<{ seller: string; url?: string }> = [];
|
|
2519
|
+
let primaryPurchaseSucceeds = false;
|
|
2520
|
+
let primaryInferenceFails = false;
|
|
2357
2521
|
const dbPath = path.resolve(__dirname, "../../data-test/manual-routing-test.db");
|
|
2358
2522
|
|
|
2359
2523
|
const readJsonBody = (req: http.IncomingMessage): Promise<any> => new Promise((resolve) => {
|
|
@@ -2421,11 +2585,48 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2421
2585
|
if (req.url === "/primary/purchase/create") {
|
|
2422
2586
|
expect(body.paymentMethod).toBe("mock");
|
|
2423
2587
|
events.push({ seller: "primary-seller", url: req.url });
|
|
2588
|
+
if (primaryPurchaseSucceeds) {
|
|
2589
|
+
res.end(JSON.stringify({
|
|
2590
|
+
purchaseId: "pur_primary_123",
|
|
2591
|
+
status: "pending",
|
|
2592
|
+
creditMicros: 2000000,
|
|
2593
|
+
currency: "USD",
|
|
2594
|
+
expiresAt: new Date(Date.now() + 86400 * 1000).toISOString()
|
|
2595
|
+
}));
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2424
2598
|
res.statusCode = 503;
|
|
2425
2599
|
res.end(JSON.stringify({ error: { code: "seller_unavailable" } }));
|
|
2426
2600
|
return;
|
|
2427
2601
|
}
|
|
2428
2602
|
|
|
2603
|
+
if (req.url === "/primary/purchase/complete") {
|
|
2604
|
+
events.push({ seller: "primary-seller", url: req.url });
|
|
2605
|
+
res.end(JSON.stringify({
|
|
2606
|
+
purchaseId: "pur_primary_123",
|
|
2607
|
+
status: "active",
|
|
2608
|
+
accessToken: "tok_primary_token_abc",
|
|
2609
|
+
tokenClass: "model:gpt-manual",
|
|
2610
|
+
creditMicros: 2000000,
|
|
2611
|
+
currency: "USD"
|
|
2612
|
+
}));
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
if (req.url === "/primary/v1/chat/completions") {
|
|
2617
|
+
events.push({ seller: "primary-seller", url: req.url });
|
|
2618
|
+
if (primaryInferenceFails) {
|
|
2619
|
+
res.statusCode = 500;
|
|
2620
|
+
res.end(JSON.stringify({ error: { code: "upstream_failed", message: "primary seller failed" } }));
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
res.end(JSON.stringify({
|
|
2624
|
+
id: "primary-chat",
|
|
2625
|
+
usage: { prompt_tokens: 4, completion_tokens: 5 }
|
|
2626
|
+
}));
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2429
2630
|
if (req.url === "/backup/purchase/create") {
|
|
2430
2631
|
expect(body.paymentMethod).toBe("mock");
|
|
2431
2632
|
events.push({ seller: "backup-seller", url: req.url });
|
|
@@ -2483,6 +2684,8 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2483
2684
|
|
|
2484
2685
|
beforeEach(() => {
|
|
2485
2686
|
events.length = 0;
|
|
2687
|
+
primaryPurchaseSucceeds = false;
|
|
2688
|
+
primaryInferenceFails = false;
|
|
2486
2689
|
rmSqliteFiles(dbPath);
|
|
2487
2690
|
const store = new BuyerStore({ dbPath });
|
|
2488
2691
|
store.savePayment({
|
|
@@ -2498,7 +2701,11 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2498
2701
|
proxyPort: 0,
|
|
2499
2702
|
dbPath,
|
|
2500
2703
|
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
2501
|
-
|
|
2704
|
+
sellerRouting: {
|
|
2705
|
+
mode: "fixed",
|
|
2706
|
+
sellerId: "primary-seller",
|
|
2707
|
+
scorer: "balanced"
|
|
2708
|
+
}
|
|
2502
2709
|
});
|
|
2503
2710
|
daemon.start();
|
|
2504
2711
|
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
@@ -2510,9 +2717,11 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2510
2717
|
rmSqliteFiles(dbPath);
|
|
2511
2718
|
});
|
|
2512
2719
|
|
|
2513
|
-
test("uses only the
|
|
2720
|
+
test("fixed routing uses only the configured seller and does not fail over to backup", async () => {
|
|
2514
2721
|
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
2515
2722
|
expect(status.selectionMode).toBe("manual");
|
|
2723
|
+
expect(status.sellerRoutingMode).toBe("fixed");
|
|
2724
|
+
expect(status.selectedSellerId).toBe("primary-seller");
|
|
2516
2725
|
|
|
2517
2726
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2518
2727
|
method: "POST",
|
|
@@ -2539,7 +2748,7 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2539
2748
|
expect(inferences.inferences.some((entry: any) => entry.sellerKey === "backup-seller")).toBe(false);
|
|
2540
2749
|
});
|
|
2541
2750
|
|
|
2542
|
-
test("
|
|
2751
|
+
test("fixed routing pins to the configured seller id", async () => {
|
|
2543
2752
|
daemon.stop();
|
|
2544
2753
|
events.length = 0;
|
|
2545
2754
|
daemon = new TokenbuddyDaemon({
|
|
@@ -2547,8 +2756,11 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2547
2756
|
proxyPort: 0,
|
|
2548
2757
|
dbPath,
|
|
2549
2758
|
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
2550
|
-
|
|
2551
|
-
|
|
2759
|
+
sellerRouting: {
|
|
2760
|
+
mode: "fixed",
|
|
2761
|
+
sellerId: "backup-seller",
|
|
2762
|
+
scorer: "balanced"
|
|
2763
|
+
}
|
|
2552
2764
|
});
|
|
2553
2765
|
daemon.start();
|
|
2554
2766
|
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
@@ -2556,6 +2768,7 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2556
2768
|
|
|
2557
2769
|
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
2558
2770
|
expect(status.selectionMode).toBe("manual");
|
|
2771
|
+
expect(status.sellerRoutingMode).toBe("fixed");
|
|
2559
2772
|
expect(status.selectedSellerId).toBe("backup-seller");
|
|
2560
2773
|
|
|
2561
2774
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
@@ -2569,7 +2782,7 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2569
2782
|
|
|
2570
2783
|
expect(response.ok).toBe(true);
|
|
2571
2784
|
// v1.2: the buyer no longer fetches the seller manifest per request.
|
|
2572
|
-
// The backup-seller is selected via
|
|
2785
|
+
// The backup-seller is selected via the fixed seller routing config; the manifest
|
|
2573
2786
|
// is sourced from the registry's `models` field.
|
|
2574
2787
|
expect(events).toEqual([
|
|
2575
2788
|
{ seller: "backup-seller", url: "/backup/purchase/create" },
|
|
@@ -2577,4 +2790,203 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2577
2790
|
{ seller: "backup-seller", url: "/backup/v1/chat/completions" }
|
|
2578
2791
|
]);
|
|
2579
2792
|
});
|
|
2793
|
+
|
|
2794
|
+
test("daemon loads fixed routing from buyer runtime config", async () => {
|
|
2795
|
+
daemon.stop();
|
|
2796
|
+
events.length = 0;
|
|
2797
|
+
const store = new BuyerStore({ dbPath });
|
|
2798
|
+
store.saveDaemonRuntimeConfig("routing", {
|
|
2799
|
+
mode: "fixed",
|
|
2800
|
+
sellerId: "backup-seller",
|
|
2801
|
+
scorer: "balanced"
|
|
2802
|
+
});
|
|
2803
|
+
store.close();
|
|
2804
|
+
|
|
2805
|
+
daemon = new TokenbuddyDaemon({
|
|
2806
|
+
controlPort: 0,
|
|
2807
|
+
proxyPort: 0,
|
|
2808
|
+
dbPath,
|
|
2809
|
+
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`
|
|
2810
|
+
});
|
|
2811
|
+
daemon.start();
|
|
2812
|
+
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
2813
|
+
daemonProxyPort = ((daemon as any).proxyServer.address() as AddressInfo).port;
|
|
2814
|
+
|
|
2815
|
+
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
2816
|
+
expect(status.sellerRoutingMode).toBe("fixed");
|
|
2817
|
+
expect(status.selectedSellerId).toBe("backup-seller");
|
|
2818
|
+
|
|
2819
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2820
|
+
method: "POST",
|
|
2821
|
+
headers: { "Content-Type": "application/json" },
|
|
2822
|
+
body: JSON.stringify({
|
|
2823
|
+
model: "gpt-manual",
|
|
2824
|
+
messages: [{ role: "user", content: "buyer store routing should be active" }]
|
|
2825
|
+
})
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
expect(response.ok).toBe(true);
|
|
2829
|
+
expect(events).toEqual([
|
|
2830
|
+
{ seller: "backup-seller", url: "/backup/purchase/create" },
|
|
2831
|
+
{ seller: "backup-seller", url: "/backup/purchase/complete" },
|
|
2832
|
+
{ seller: "backup-seller", url: "/backup/v1/chat/completions" }
|
|
2833
|
+
]);
|
|
2834
|
+
});
|
|
2835
|
+
|
|
2836
|
+
test("fixedSet routing only uses sellers in the configured pool", async () => {
|
|
2837
|
+
daemon.stop();
|
|
2838
|
+
events.length = 0;
|
|
2839
|
+
daemon = new TokenbuddyDaemon({
|
|
2840
|
+
controlPort: 0,
|
|
2841
|
+
proxyPort: 0,
|
|
2842
|
+
dbPath,
|
|
2843
|
+
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
2844
|
+
sellerRouting: {
|
|
2845
|
+
mode: "fixedSet",
|
|
2846
|
+
sellerIds: ["backup-seller"],
|
|
2847
|
+
scorer: "balanced"
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
daemon.start();
|
|
2851
|
+
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
2852
|
+
daemonProxyPort = ((daemon as any).proxyServer.address() as AddressInfo).port;
|
|
2853
|
+
|
|
2854
|
+
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
2855
|
+
expect(status.selectionMode).toBe("manual");
|
|
2856
|
+
expect(status.sellerRoutingMode).toBe("fixedSet");
|
|
2857
|
+
expect(status.selectedSellerId).toBeUndefined();
|
|
2858
|
+
|
|
2859
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2860
|
+
method: "POST",
|
|
2861
|
+
headers: { "Content-Type": "application/json" },
|
|
2862
|
+
body: JSON.stringify({
|
|
2863
|
+
model: "gpt-manual",
|
|
2864
|
+
messages: [{ role: "user", content: "fixedSet should stay inside the configured pool" }]
|
|
2865
|
+
})
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
expect(response.ok).toBe(true);
|
|
2869
|
+
expect(events).toEqual([
|
|
2870
|
+
{ seller: "backup-seller", url: "/backup/purchase/create" },
|
|
2871
|
+
{ seller: "backup-seller", url: "/backup/purchase/complete" },
|
|
2872
|
+
{ seller: "backup-seller", url: "/backup/v1/chat/completions" }
|
|
2873
|
+
]);
|
|
2874
|
+
});
|
|
2875
|
+
|
|
2876
|
+
test("fullAuto routing fails over from a 500 primary seller to the backup seller", async () => {
|
|
2877
|
+
daemon.stop();
|
|
2878
|
+
events.length = 0;
|
|
2879
|
+
primaryPurchaseSucceeds = true;
|
|
2880
|
+
primaryInferenceFails = true;
|
|
2881
|
+
const requestId = "auto_failover_500_log_detail";
|
|
2882
|
+
const rawPrompt = "raw prompt must stay out of tb-proxyd logs: tb-log-secret-500";
|
|
2883
|
+
daemon = new TokenbuddyDaemon({
|
|
2884
|
+
controlPort: 0,
|
|
2885
|
+
proxyPort: 0,
|
|
2886
|
+
dbPath,
|
|
2887
|
+
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
2888
|
+
sellerRouting: {
|
|
2889
|
+
mode: "fullAuto",
|
|
2890
|
+
scorer: "balanced"
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
daemon.start();
|
|
2894
|
+
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
2895
|
+
daemonProxyPort = ((daemon as any).proxyServer.address() as AddressInfo).port;
|
|
2896
|
+
|
|
2897
|
+
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
2898
|
+
expect(status.selectionMode).toBe("auto");
|
|
2899
|
+
expect(status.sellerRoutingMode).toBe("fullAuto");
|
|
2900
|
+
expect(status.selectedSellerId).toBeUndefined();
|
|
2901
|
+
|
|
2902
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2903
|
+
method: "POST",
|
|
2904
|
+
headers: { "Content-Type": "application/json" },
|
|
2905
|
+
body: JSON.stringify({
|
|
2906
|
+
model: "gpt-manual",
|
|
2907
|
+
messages: [{ role: "user", content: rawPrompt }],
|
|
2908
|
+
requestId
|
|
2909
|
+
})
|
|
2910
|
+
});
|
|
2911
|
+
|
|
2912
|
+
expect(response.ok).toBe(true);
|
|
2913
|
+
expect((await response.json() as any).id).toBe("backup-chat");
|
|
2914
|
+
expect(events).toEqual([
|
|
2915
|
+
{ seller: "primary-seller", url: "/primary/purchase/create" },
|
|
2916
|
+
{ seller: "primary-seller", url: "/primary/purchase/complete" },
|
|
2917
|
+
{ seller: "primary-seller", url: "/primary/v1/chat/completions" },
|
|
2918
|
+
{ seller: "primary-seller", url: "/primary/v1/chat/completions" },
|
|
2919
|
+
{ seller: "primary-seller", url: "/primary/v1/chat/completions" },
|
|
2920
|
+
{ seller: "backup-seller", url: "/backup/purchase/create" },
|
|
2921
|
+
{ seller: "backup-seller", url: "/backup/purchase/complete" },
|
|
2922
|
+
{ seller: "backup-seller", url: "/backup/v1/chat/completions" }
|
|
2923
|
+
]);
|
|
2924
|
+
|
|
2925
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2926
|
+
const logFile = resolveModuleLogFile("tb-proxyd");
|
|
2927
|
+
const logs = fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
|
|
2928
|
+
const requestLogs = logs
|
|
2929
|
+
.split("\n")
|
|
2930
|
+
.filter((line) => line.includes(`requestId=${requestId}`))
|
|
2931
|
+
.join("\n");
|
|
2932
|
+
expect(requestLogs).toContain("event=route.failover.retry_scheduled");
|
|
2933
|
+
expect(requestLogs).toContain("event=route.failover.triggered");
|
|
2934
|
+
expect(requestLogs).toContain("event=purchase.create.started");
|
|
2935
|
+
expect(requestLogs).toContain("event=purchase.ledger.recorded");
|
|
2936
|
+
expect(requestLogs).toContain("event=inference.ledger.recorded");
|
|
2937
|
+
expect(requestLogs).toContain("bodySummary=");
|
|
2938
|
+
expect(requestLogs).not.toContain("upstreamBody=");
|
|
2939
|
+
expect(logs).not.toContain(rawPrompt);
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
test("fullAuto routing logs purchase failure failover before trying the backup seller", async () => {
|
|
2943
|
+
daemon.stop();
|
|
2944
|
+
events.length = 0;
|
|
2945
|
+
primaryPurchaseSucceeds = false;
|
|
2946
|
+
daemon = new TokenbuddyDaemon({
|
|
2947
|
+
controlPort: 0,
|
|
2948
|
+
proxyPort: 0,
|
|
2949
|
+
dbPath,
|
|
2950
|
+
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
2951
|
+
sellerRouting: {
|
|
2952
|
+
mode: "fullAuto",
|
|
2953
|
+
scorer: "balanced"
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
daemon.start();
|
|
2957
|
+
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
2958
|
+
daemonProxyPort = ((daemon as any).proxyServer.address() as AddressInfo).port;
|
|
2959
|
+
|
|
2960
|
+
const requestId = "auto_purchase_failover_log_detail";
|
|
2961
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2962
|
+
method: "POST",
|
|
2963
|
+
headers: { "Content-Type": "application/json" },
|
|
2964
|
+
body: JSON.stringify({
|
|
2965
|
+
model: "gpt-manual",
|
|
2966
|
+
messages: [{ role: "user", content: "purchase failure should fail over" }],
|
|
2967
|
+
requestId
|
|
2968
|
+
})
|
|
2969
|
+
});
|
|
2970
|
+
|
|
2971
|
+
expect(response.ok).toBe(true);
|
|
2972
|
+
expect((await response.json() as any).id).toBe("backup-chat");
|
|
2973
|
+
expect(events).toEqual([
|
|
2974
|
+
{ seller: "primary-seller", url: "/primary/purchase/create" },
|
|
2975
|
+
{ seller: "backup-seller", url: "/backup/purchase/create" },
|
|
2976
|
+
{ seller: "backup-seller", url: "/backup/purchase/complete" },
|
|
2977
|
+
{ seller: "backup-seller", url: "/backup/v1/chat/completions" }
|
|
2978
|
+
]);
|
|
2979
|
+
|
|
2980
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2981
|
+
const logFile = resolveModuleLogFile("tb-proxyd");
|
|
2982
|
+
const logs = fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
|
|
2983
|
+
const requestLogs = logs
|
|
2984
|
+
.split("\n")
|
|
2985
|
+
.filter((line) => line.includes(`requestId=${requestId}`))
|
|
2986
|
+
.join("\n");
|
|
2987
|
+
expect(requestLogs).toContain("event=route.failover.triggered");
|
|
2988
|
+
expect(requestLogs).toContain("reason=purchase_failed");
|
|
2989
|
+
expect(requestLogs).toContain("controllerAction=retry_same_seller");
|
|
2990
|
+
expect(requestLogs).not.toContain("event=route.failover.retry_scheduled");
|
|
2991
|
+
});
|
|
2580
2992
|
});
|