@tokenbuddy/tokenbuddy 1.0.11 → 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 -17
- 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 -25
- 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 +447 -33
- /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,21 +1728,15 @@ 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.10+ tb-proxy install opencode 必须默认走 Responses API 协议,
|
|
1606
|
-
// 而不是 chat completions。原因:code.shoestravel.xin 等上游原生 SSE 事件链
|
|
1607
|
-
// 才是 Responses API 风格(response.created / response.output_text.delta / response.completed),
|
|
1608
|
-
// buyer 端 SseUsageExtractor 解析 usage 字段更稳定。改回 @ai-sdk/openai 需先
|
|
1609
|
-
// 评估 5-seller 架构是否仍能端到端 work。
|
|
1736
|
+
test("opencode provider install uses @ai-sdk/openai-compatible for chat completions", () => {
|
|
1610
1737
|
const config: ProviderRuntimeConfig = {
|
|
1611
1738
|
selectionKind: "single-model",
|
|
1612
|
-
protocolPreference: "
|
|
1739
|
+
protocolPreference: "chat_completions",
|
|
1613
1740
|
defaultModel: "gpt-5.4",
|
|
1614
1741
|
};
|
|
1615
1742
|
const changes = previewProviderInstall({
|
|
@@ -1622,7 +1749,7 @@ describe("Provider install planning", () => {
|
|
|
1622
1749
|
expect(change).toBeDefined();
|
|
1623
1750
|
expect(change?.content).toBeDefined();
|
|
1624
1751
|
const parsed = JSON.parse(change!.content!);
|
|
1625
|
-
expect(parsed.provider.tokenbuddy.npm).toBe("@ai-sdk/openai-
|
|
1752
|
+
expect(parsed.provider.tokenbuddy.npm).toBe("@ai-sdk/openai-compatible");
|
|
1626
1753
|
expect(parsed.model).toBe("tokenbuddy/gpt-5.4");
|
|
1627
1754
|
expect(parsed.provider.tokenbuddy.options.baseURL).toBe("http://127.0.0.1:17821/v1");
|
|
1628
1755
|
});
|
|
@@ -1801,6 +1928,12 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1801
1928
|
if (body.stream) {
|
|
1802
1929
|
res.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
1803
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
|
+
}
|
|
1804
1937
|
res.write("event: tokenbuddy.settlement\n");
|
|
1805
1938
|
res.write(`data: ${JSON.stringify({
|
|
1806
1939
|
requestId: body.requestId || "stream_req_mock",
|
|
@@ -2282,6 +2415,8 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
2282
2415
|
const body = await response.text();
|
|
2283
2416
|
expect(body).toContain("chatcmpl-stream");
|
|
2284
2417
|
expect(body).toContain("[DONE]");
|
|
2418
|
+
expect(body).not.toContain("}data:");
|
|
2419
|
+
expect(body).toContain("}\n\ndata: [DONE]");
|
|
2285
2420
|
expect(body).not.toContain("tokenbuddy.settlement");
|
|
2286
2421
|
expect(sellerRequests.find((request) => request.url === "/v1/chat/completions" && request.body?.stream)?.idempotencyKey).toBe("idem-stream-preserved");
|
|
2287
2422
|
|
|
@@ -2302,6 +2437,35 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
2302
2437
|
expect(publicOutput).not.toContain("chatcmpl-stream");
|
|
2303
2438
|
});
|
|
2304
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
|
+
|
|
2305
2469
|
test("passes through responses SSE bytes unchanged for OpenAI Responses API clients", async () => {
|
|
2306
2470
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/responses`, {
|
|
2307
2471
|
method: "POST",
|
|
@@ -2345,13 +2509,15 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
2345
2509
|
});
|
|
2346
2510
|
});
|
|
2347
2511
|
|
|
2348
|
-
describe("TokenBuddy
|
|
2512
|
+
describe("TokenBuddy seller routing strategies", () => {
|
|
2349
2513
|
let server: http.Server;
|
|
2350
2514
|
let sellerPort: number;
|
|
2351
2515
|
let daemon: TokenbuddyDaemon;
|
|
2352
2516
|
let daemonProxyPort: number;
|
|
2353
2517
|
let daemonControlPort: number;
|
|
2354
2518
|
const events: Array<{ seller: string; url?: string }> = [];
|
|
2519
|
+
let primaryPurchaseSucceeds = false;
|
|
2520
|
+
let primaryInferenceFails = false;
|
|
2355
2521
|
const dbPath = path.resolve(__dirname, "../../data-test/manual-routing-test.db");
|
|
2356
2522
|
|
|
2357
2523
|
const readJsonBody = (req: http.IncomingMessage): Promise<any> => new Promise((resolve) => {
|
|
@@ -2419,11 +2585,48 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2419
2585
|
if (req.url === "/primary/purchase/create") {
|
|
2420
2586
|
expect(body.paymentMethod).toBe("mock");
|
|
2421
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
|
+
}
|
|
2422
2598
|
res.statusCode = 503;
|
|
2423
2599
|
res.end(JSON.stringify({ error: { code: "seller_unavailable" } }));
|
|
2424
2600
|
return;
|
|
2425
2601
|
}
|
|
2426
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
|
+
|
|
2427
2630
|
if (req.url === "/backup/purchase/create") {
|
|
2428
2631
|
expect(body.paymentMethod).toBe("mock");
|
|
2429
2632
|
events.push({ seller: "backup-seller", url: req.url });
|
|
@@ -2481,6 +2684,8 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2481
2684
|
|
|
2482
2685
|
beforeEach(() => {
|
|
2483
2686
|
events.length = 0;
|
|
2687
|
+
primaryPurchaseSucceeds = false;
|
|
2688
|
+
primaryInferenceFails = false;
|
|
2484
2689
|
rmSqliteFiles(dbPath);
|
|
2485
2690
|
const store = new BuyerStore({ dbPath });
|
|
2486
2691
|
store.savePayment({
|
|
@@ -2496,7 +2701,11 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2496
2701
|
proxyPort: 0,
|
|
2497
2702
|
dbPath,
|
|
2498
2703
|
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
2499
|
-
|
|
2704
|
+
sellerRouting: {
|
|
2705
|
+
mode: "fixed",
|
|
2706
|
+
sellerId: "primary-seller",
|
|
2707
|
+
scorer: "balanced"
|
|
2708
|
+
}
|
|
2500
2709
|
});
|
|
2501
2710
|
daemon.start();
|
|
2502
2711
|
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
@@ -2508,9 +2717,11 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2508
2717
|
rmSqliteFiles(dbPath);
|
|
2509
2718
|
});
|
|
2510
2719
|
|
|
2511
|
-
test("uses only the
|
|
2720
|
+
test("fixed routing uses only the configured seller and does not fail over to backup", async () => {
|
|
2512
2721
|
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
2513
2722
|
expect(status.selectionMode).toBe("manual");
|
|
2723
|
+
expect(status.sellerRoutingMode).toBe("fixed");
|
|
2724
|
+
expect(status.selectedSellerId).toBe("primary-seller");
|
|
2514
2725
|
|
|
2515
2726
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2516
2727
|
method: "POST",
|
|
@@ -2537,7 +2748,7 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2537
2748
|
expect(inferences.inferences.some((entry: any) => entry.sellerKey === "backup-seller")).toBe(false);
|
|
2538
2749
|
});
|
|
2539
2750
|
|
|
2540
|
-
test("
|
|
2751
|
+
test("fixed routing pins to the configured seller id", async () => {
|
|
2541
2752
|
daemon.stop();
|
|
2542
2753
|
events.length = 0;
|
|
2543
2754
|
daemon = new TokenbuddyDaemon({
|
|
@@ -2545,8 +2756,11 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2545
2756
|
proxyPort: 0,
|
|
2546
2757
|
dbPath,
|
|
2547
2758
|
sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
|
|
2548
|
-
|
|
2549
|
-
|
|
2759
|
+
sellerRouting: {
|
|
2760
|
+
mode: "fixed",
|
|
2761
|
+
sellerId: "backup-seller",
|
|
2762
|
+
scorer: "balanced"
|
|
2763
|
+
}
|
|
2550
2764
|
});
|
|
2551
2765
|
daemon.start();
|
|
2552
2766
|
daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
|
|
@@ -2554,6 +2768,7 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2554
2768
|
|
|
2555
2769
|
const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
|
|
2556
2770
|
expect(status.selectionMode).toBe("manual");
|
|
2771
|
+
expect(status.sellerRoutingMode).toBe("fixed");
|
|
2557
2772
|
expect(status.selectedSellerId).toBe("backup-seller");
|
|
2558
2773
|
|
|
2559
2774
|
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
@@ -2567,7 +2782,7 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2567
2782
|
|
|
2568
2783
|
expect(response.ok).toBe(true);
|
|
2569
2784
|
// v1.2: the buyer no longer fetches the seller manifest per request.
|
|
2570
|
-
// The backup-seller is selected via
|
|
2785
|
+
// The backup-seller is selected via the fixed seller routing config; the manifest
|
|
2571
2786
|
// is sourced from the registry's `models` field.
|
|
2572
2787
|
expect(events).toEqual([
|
|
2573
2788
|
{ seller: "backup-seller", url: "/backup/purchase/create" },
|
|
@@ -2575,4 +2790,203 @@ describe("TokenBuddy manual routing mode", () => {
|
|
|
2575
2790
|
{ seller: "backup-seller", url: "/backup/v1/chat/completions" }
|
|
2576
2791
|
]);
|
|
2577
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
|
+
});
|
|
2578
2992
|
});
|