@tokenbuddy/tokenbuddy 1.0.36 → 1.0.38
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 +7 -2
- package/dist/src/buyer-store.js +46 -7
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +15 -7
- package/dist/src/daemon.d.ts +12 -0
- package/dist/src/daemon.js +791 -61
- package/dist/src/doctor-diagnostics.js +1 -6
- package/dist/src/provider-install.d.ts +2 -2
- package/dist/src/provider-install.js +248 -2
- package/dist/src/seller-catalog.d.ts +21 -0
- package/dist/src/seller-catalog.js +17 -0
- package/dist/src/seller-route-planner.d.ts +4 -1
- package/dist/src/seller-route-planner.js +3 -0
- package/dist/src/seller-routing-strategy.d.ts +3 -0
- package/dist/src/terminal-detect.d.ts +1 -1
- package/dist/src/terminal-detect.js +3 -2
- package/dist/src/workdir.d.ts +10 -0
- package/dist/src/workdir.js +26 -0
- package/package.json +15 -2
- package/static/ui/assets/index-Djfl9tw5.js +271 -0
- package/static/ui/assets/index-DkfztCkn.css +1 -0
- package/static/ui/index.html +2 -2
- package/dist/src/buyer-store.d.ts.map +0 -1
- package/dist/src/buyer-store.js.map +0 -1
- package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
- package/dist/src/clawtip-bootstrap.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/credit-tracker.d.ts.map +0 -1
- package/dist/src/credit-tracker.js.map +0 -1
- package/dist/src/daemon.d.ts.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
- package/dist/src/doctor-clawtip-wallet.js.map +0 -1
- package/dist/src/doctor-diagnostics.d.ts.map +0 -1
- package/dist/src/doctor-diagnostics.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init-clawtip-activation.d.ts.map +0 -1
- package/dist/src/init-clawtip-activation.js.map +0 -1
- package/dist/src/init-payment-options.d.ts.map +0 -1
- package/dist/src/init-payment-options.js.map +0 -1
- package/dist/src/init-setup.d.ts.map +0 -1
- package/dist/src/init-setup.js.map +0 -1
- package/dist/src/model-index.d.ts.map +0 -1
- package/dist/src/model-index.js.map +0 -1
- package/dist/src/package-update.d.ts.map +0 -1
- package/dist/src/package-update.js.map +0 -1
- package/dist/src/prewarm-cache.d.ts.map +0 -1
- package/dist/src/prewarm-cache.js.map +0 -1
- package/dist/src/prewarm-scheduler.d.ts.map +0 -1
- package/dist/src/prewarm-scheduler.js.map +0 -1
- package/dist/src/provider-install.d.ts.map +0 -1
- package/dist/src/provider-install.js.map +0 -1
- package/dist/src/provider-routing-config.d.ts.map +0 -1
- package/dist/src/provider-routing-config.js.map +0 -1
- package/dist/src/registry-trust.d.ts.map +0 -1
- package/dist/src/registry-trust.js.map +0 -1
- package/dist/src/route-failover.d.ts.map +0 -1
- package/dist/src/route-failover.js.map +0 -1
- package/dist/src/seller-catalog.d.ts.map +0 -1
- package/dist/src/seller-catalog.js.map +0 -1
- package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
- package/dist/src/seller-concurrency-limiter.js.map +0 -1
- package/dist/src/seller-metadata-cache.d.ts.map +0 -1
- package/dist/src/seller-metadata-cache.js.map +0 -1
- package/dist/src/seller-pool.d.ts.map +0 -1
- package/dist/src/seller-pool.js.map +0 -1
- package/dist/src/seller-route-planner.d.ts.map +0 -1
- package/dist/src/seller-route-planner.js.map +0 -1
- package/dist/src/seller-routing-config.d.ts.map +0 -1
- package/dist/src/seller-routing-config.js.map +0 -1
- package/dist/src/seller-routing-strategy.d.ts.map +0 -1
- package/dist/src/seller-routing-strategy.js.map +0 -1
- package/dist/src/stream-failover.d.ts.map +0 -1
- package/dist/src/stream-failover.js.map +0 -1
- package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
- package/dist/src/tb-clawtip-proof.js.map +0 -1
- package/dist/src/tb-proxyd.d.ts.map +0 -1
- package/dist/src/tb-proxyd.js.map +0 -1
- package/dist/src/terminal-detect.d.ts.map +0 -1
- package/dist/src/terminal-detect.js.map +0 -1
- package/dist/src/terminal-image.d.ts.map +0 -1
- package/dist/src/terminal-image.js.map +0 -1
- package/src/buyer-store.ts +0 -1090
- package/src/clawtip-bootstrap.ts +0 -65
- package/src/cli.ts +0 -2243
- package/src/credit-tracker.ts +0 -295
- package/src/daemon.ts +0 -5475
- package/src/doctor-clawtip-wallet.ts +0 -95
- package/src/doctor-diagnostics.ts +0 -1026
- package/src/index.ts +0 -16
- package/src/init-clawtip-activation.ts +0 -695
- package/src/init-payment-options.ts +0 -373
- package/src/init-setup.ts +0 -165
- package/src/model-index.ts +0 -278
- package/src/package-update.ts +0 -311
- package/src/prewarm-cache.ts +0 -485
- package/src/prewarm-scheduler.ts +0 -675
- package/src/provider-install.ts +0 -1006
- package/src/provider-routing-config.ts +0 -410
- package/src/registry-trust.ts +0 -51
- package/src/route-failover.ts +0 -304
- package/src/seller-catalog.ts +0 -505
- package/src/seller-concurrency-limiter.ts +0 -161
- package/src/seller-metadata-cache.ts +0 -91
- package/src/seller-pool.ts +0 -557
- package/src/seller-route-planner.ts +0 -513
- package/src/seller-routing-config.ts +0 -211
- package/src/seller-routing-strategy.ts +0 -362
- package/src/stream-failover.ts +0 -152
- package/src/tb-clawtip-proof.ts +0 -28
- package/src/tb-proxyd.ts +0 -101
- package/src/terminal-detect.ts +0 -333
- package/src/terminal-image.ts +0 -228
- package/static/ui/assets/index-0MVXD7bH.css +0 -1
- package/static/ui/assets/index-BVbeDEwq.js +0 -271
- package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
- package/tests/cli-routing.test.ts +0 -363
- package/tests/control-plane-ui-endpoints.test.ts +0 -1630
- package/tests/credit-tracker.test.ts +0 -165
- package/tests/daemon-413-fallback.test.ts +0 -92
- package/tests/daemon-classify.test.ts +0 -452
- package/tests/daemon-roles.test.ts +0 -92
- package/tests/daemon-trusted-registry-cache.test.ts +0 -132
- package/tests/e2e.test.ts +0 -366
- package/tests/image-generation-e2e.test.ts +0 -230
- package/tests/model-index.test.ts +0 -198
- package/tests/package-update.test.ts +0 -147
- package/tests/prewarm-cache.test.ts +0 -296
- package/tests/prewarm-scheduler.test.ts +0 -367
- package/tests/provider-routing-config.test.ts +0 -150
- package/tests/registry-trust.test.ts +0 -28
- package/tests/route-failover.test.ts +0 -222
- package/tests/seller-catalog-413.test.ts +0 -120
- package/tests/seller-catalog-utilities.test.ts +0 -124
- package/tests/seller-concurrency-limiter.test.ts +0 -83
- package/tests/seller-metadata-cache.test.ts +0 -89
- package/tests/seller-pool.test.ts +0 -365
- package/tests/seller-route-planner.test.ts +0 -312
- package/tests/seller-routing-config.test.ts +0 -124
- package/tests/seller-routing-strategy.test.ts +0 -167
- package/tests/stream-failover.test.ts +0 -52
- package/tests/thousand-seller.test.ts +0 -151
- package/tests/tokenbuddy.test.ts +0 -4043
- package/tsconfig.json +0 -8
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { ModelIndex } from "../src/model-index.js";
|
|
2
|
-
import type { RegistrySeller } from "../src/seller-catalog.js";
|
|
3
|
-
|
|
4
|
-
function makeSeller(overrides: Partial<RegistrySeller> & { id: string; models?: string[] }): RegistrySeller {
|
|
5
|
-
return {
|
|
6
|
-
id: overrides.id,
|
|
7
|
-
name: overrides.name ?? overrides.id,
|
|
8
|
-
status: overrides.status,
|
|
9
|
-
url: overrides.url ?? `https://${overrides.id}.example.com`,
|
|
10
|
-
supportedProtocols: overrides.supportedProtocols ?? ["chat_completions"],
|
|
11
|
-
paymentMethods: overrides.paymentMethods ?? ["clawtip"],
|
|
12
|
-
models: overrides.models
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("ModelIndex", () => {
|
|
17
|
-
test("rebuilds an empty snapshot without throwing", () => {
|
|
18
|
-
const index = new ModelIndex();
|
|
19
|
-
index.rebuild([], { registryVersion: 1 });
|
|
20
|
-
|
|
21
|
-
expect(index.sellersFor("anything")).toEqual([]);
|
|
22
|
-
expect(index.knownModelIds()).toEqual([]);
|
|
23
|
-
const stats = index.stats();
|
|
24
|
-
expect(stats.sellerCount).toBe(0);
|
|
25
|
-
expect(stats.modelCount).toBe(0);
|
|
26
|
-
expect(stats.missingModelsCount).toBe(0);
|
|
27
|
-
expect(stats.registryVersion).toBe(1);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("rebuild populates byModel and bySeller from a registry snapshot", () => {
|
|
31
|
-
const index = new ModelIndex();
|
|
32
|
-
const sellers: RegistrySeller[] = [
|
|
33
|
-
makeSeller({ id: "s1", models: ["claude-sonnet-4-5", "gpt-4o"] }),
|
|
34
|
-
makeSeller({ id: "s2", models: ["gpt-4o", "deepseek-v4-pro"] })
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
index.rebuild(sellers, { registryVersion: 4, defaultSellerId: "s1" });
|
|
38
|
-
|
|
39
|
-
expect(index.sellersFor("claude-sonnet-4-5").map((s) => s.id)).toEqual(["s1"]);
|
|
40
|
-
expect(index.sellersFor("gpt-4o").map((s) => s.id)).toEqual(["s1", "s2"]);
|
|
41
|
-
expect(index.sellersFor("deepseek-v4-pro").map((s) => s.id)).toEqual(["s2"]);
|
|
42
|
-
expect(index.getSeller("s1")?.url).toBe("https://s1.example.com");
|
|
43
|
-
expect(index.getSeller("missing")).toBeUndefined();
|
|
44
|
-
expect(index.stats().defaultSellerId).toBe("s1");
|
|
45
|
-
expect(index.stats().registryVersion).toBe(4);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("model id matching is case-insensitive and trims whitespace", () => {
|
|
49
|
-
const index = new ModelIndex();
|
|
50
|
-
index.rebuild([makeSeller({ id: "s1", models: ["claude-sonnet-4-5"] })]);
|
|
51
|
-
|
|
52
|
-
expect(index.sellersFor("Claude-Sonnet-4-5").map((s) => s.id)).toEqual(["s1"]);
|
|
53
|
-
expect(index.sellersFor(" claude-sonnet-4-5 ").map((s) => s.id)).toEqual(["s1"]);
|
|
54
|
-
expect(index.sellersFor("CLAUDE-SONNET-4-5").map((s) => s.id)).toEqual(["s1"]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("filters by protocol and payment method", () => {
|
|
58
|
-
const index = new ModelIndex();
|
|
59
|
-
const sellers: RegistrySeller[] = [
|
|
60
|
-
makeSeller({
|
|
61
|
-
id: "s-chat",
|
|
62
|
-
models: ["gpt-4o"],
|
|
63
|
-
supportedProtocols: ["chat_completions"],
|
|
64
|
-
paymentMethods: ["clawtip"]
|
|
65
|
-
}),
|
|
66
|
-
makeSeller({
|
|
67
|
-
id: "s-msg",
|
|
68
|
-
models: ["gpt-4o"],
|
|
69
|
-
supportedProtocols: ["messages"],
|
|
70
|
-
paymentMethods: ["mock"]
|
|
71
|
-
})
|
|
72
|
-
];
|
|
73
|
-
index.rebuild(sellers);
|
|
74
|
-
|
|
75
|
-
const chatClawtip = index.sellersFor("gpt-4o", { protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
76
|
-
expect(chatClawtip.map((s) => s.id)).toEqual(["s-chat"]);
|
|
77
|
-
|
|
78
|
-
const messages = index.sellersFor("gpt-4o", { protocol: "messages" });
|
|
79
|
-
expect(messages.map((s) => s.id)).toEqual(["s-msg"]);
|
|
80
|
-
|
|
81
|
-
const messagesClawtip = index.sellersFor("gpt-4o", { protocol: "messages", paymentMethod: "clawtip" });
|
|
82
|
-
expect(messagesClawtip).toEqual([]);
|
|
83
|
-
|
|
84
|
-
const noFilter = index.sellersFor("gpt-4o");
|
|
85
|
-
expect(noFilter.map((s) => s.id).sort()).toEqual(["s-chat", "s-msg"]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("indexes only active buyer-visible registry sellers", () => {
|
|
89
|
-
const index = new ModelIndex();
|
|
90
|
-
index.rebuild([
|
|
91
|
-
makeSeller({ id: "legacy-no-status", models: ["gpt-4o"] }),
|
|
92
|
-
makeSeller({ id: "active", status: "active", models: ["gpt-4o"] }),
|
|
93
|
-
makeSeller({ id: "pending", status: "pending", models: ["gpt-4o"] }),
|
|
94
|
-
makeSeller({ id: "offline", status: "offline", models: ["gpt-4o"] })
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
expect(index.sellersFor("gpt-4o").map((seller) => seller.id)).toEqual(["legacy-no-status", "active"]);
|
|
98
|
-
expect(index.getSeller("pending")).toBeUndefined();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("sellers missing models field are excluded from lookups but still addressable by id", () => {
|
|
102
|
-
const index = new ModelIndex();
|
|
103
|
-
const sellers: RegistrySeller[] = [
|
|
104
|
-
makeSeller({ id: "ok", models: ["gpt-4o"] }),
|
|
105
|
-
makeSeller({ id: "broken" }) // no models
|
|
106
|
-
];
|
|
107
|
-
index.rebuild(sellers);
|
|
108
|
-
|
|
109
|
-
expect(index.sellersFor("gpt-4o").map((s) => s.id)).toEqual(["ok"]);
|
|
110
|
-
expect(index.getSeller("broken")?.id).toBe("broken");
|
|
111
|
-
expect(index.stats().missingModelsCount).toBe(1);
|
|
112
|
-
expect(index.stats().sellerCount).toBe(2);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("resolve returns matched flag and snapshot", () => {
|
|
116
|
-
const index = new ModelIndex();
|
|
117
|
-
index.rebuild([makeSeller({ id: "s1", models: ["gpt-4o"] })]);
|
|
118
|
-
|
|
119
|
-
const hit = index.resolve("gpt-4o");
|
|
120
|
-
expect(hit.matched).toBe(true);
|
|
121
|
-
expect(hit.sellers).toHaveLength(1);
|
|
122
|
-
|
|
123
|
-
const miss = index.resolve("unknown-model");
|
|
124
|
-
expect(miss.matched).toBe(false);
|
|
125
|
-
expect(miss.sellers).toEqual([]);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("rebuild replaces the previous snapshot atomically", () => {
|
|
129
|
-
const index = new ModelIndex();
|
|
130
|
-
index.rebuild([makeSeller({ id: "s1", models: ["m1"] })], { registryVersion: 1 });
|
|
131
|
-
expect(index.sellersFor("m1")).toHaveLength(1);
|
|
132
|
-
|
|
133
|
-
index.rebuild([makeSeller({ id: "s2", models: ["m2"] })], { registryVersion: 2 });
|
|
134
|
-
expect(index.sellersFor("m1")).toEqual([]);
|
|
135
|
-
expect(index.sellersFor("m2")).toHaveLength(1);
|
|
136
|
-
expect(index.stats().registryVersion).toBe(2);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("prune drops sellers that have not been seen within staleAfterMs", () => {
|
|
140
|
-
const index = new ModelIndex();
|
|
141
|
-
index.rebuild([
|
|
142
|
-
makeSeller({ id: "s1", models: ["gpt-4o", "m1"] }),
|
|
143
|
-
makeSeller({ id: "s2", models: ["m2"] })
|
|
144
|
-
]);
|
|
145
|
-
|
|
146
|
-
const now = 1_700_000_000_000;
|
|
147
|
-
const lastSeenAt = new Map<string, number>([
|
|
148
|
-
["s1", now - 1000], // fresh
|
|
149
|
-
["s2", now - 10 * 24 * 60 * 60 * 1000] // 10 days old
|
|
150
|
-
]);
|
|
151
|
-
|
|
152
|
-
const removed = index.prune(lastSeenAt, 7 * 24 * 60 * 60 * 1000, now);
|
|
153
|
-
expect(removed).toBe(1);
|
|
154
|
-
expect(index.getSeller("s1")).toBeDefined();
|
|
155
|
-
expect(index.getSeller("s2")).toBeUndefined();
|
|
156
|
-
// Models that belonged only to the pruned seller must be removed.
|
|
157
|
-
expect(index.sellersFor("m2")).toEqual([]);
|
|
158
|
-
// Models shared with the remaining seller must still resolve.
|
|
159
|
-
expect(index.sellersFor("gpt-4o")).toHaveLength(1);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("prune is a no-op when staleAfterMs is non-positive", () => {
|
|
163
|
-
const index = new ModelIndex();
|
|
164
|
-
index.rebuild([makeSeller({ id: "s1", models: ["m1"] })]);
|
|
165
|
-
expect(index.prune(new Map([["s1", 0]]), 0)).toBe(0);
|
|
166
|
-
expect(index.sellersFor("m1")).toHaveLength(1);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("ignores malformed model entries when populating the index", () => {
|
|
170
|
-
const index = new ModelIndex();
|
|
171
|
-
const sellers: RegistrySeller[] = [
|
|
172
|
-
{
|
|
173
|
-
id: "s1",
|
|
174
|
-
name: "s1",
|
|
175
|
-
url: "https://s1.example.com",
|
|
176
|
-
supportedProtocols: ["chat_completions"],
|
|
177
|
-
paymentMethods: ["clawtip"],
|
|
178
|
-
// Mixed valid / invalid entries; the index must skip the bad ones
|
|
179
|
-
// without throwing.
|
|
180
|
-
models: ["m1", "", " ", "m2"] as string[]
|
|
181
|
-
}
|
|
182
|
-
];
|
|
183
|
-
index.rebuild(sellers);
|
|
184
|
-
|
|
185
|
-
expect(index.sellersFor("m1")).toHaveLength(1);
|
|
186
|
-
expect(index.sellersFor("m2")).toHaveLength(1);
|
|
187
|
-
expect(index.knownModelIds().sort()).toEqual(["m1", "m2"]);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("knownModelIds returns a snapshot that does not mutate internal state", () => {
|
|
191
|
-
const index = new ModelIndex();
|
|
192
|
-
index.rebuild([makeSeller({ id: "s1", models: ["m1"] })]);
|
|
193
|
-
|
|
194
|
-
const ids = index.knownModelIds();
|
|
195
|
-
ids.push("hacked");
|
|
196
|
-
expect(index.knownModelIds()).toEqual(["m1"]);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as os from "os";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import {
|
|
5
|
-
checkPackageUpdate,
|
|
6
|
-
runPackageUpdate,
|
|
7
|
-
scheduleLaunchAgentRestart,
|
|
8
|
-
} from "../src/package-update.js";
|
|
9
|
-
|
|
10
|
-
function makeFetch(latestVersion: string, ok = true): typeof fetch {
|
|
11
|
-
return (async () => ({
|
|
12
|
-
ok,
|
|
13
|
-
status: ok ? 200 : 404,
|
|
14
|
-
json: async () => ({ "dist-tags": { latest: latestVersion } }),
|
|
15
|
-
})) as unknown as typeof fetch;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function writePackageJson(root: string, version: string, name = "@tokenbuddy/tokenbuddy"): string {
|
|
19
|
-
const binDir = path.join(root, "bin");
|
|
20
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
21
|
-
const binPath = path.join(binDir, "tb.js");
|
|
22
|
-
fs.writeFileSync(binPath, "", "utf8");
|
|
23
|
-
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name, version }), "utf8");
|
|
24
|
-
return binPath;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe("TokenBuddy package update", () => {
|
|
28
|
-
let tempRoot: string;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tokenbuddy-package-update-"));
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("checks npm latest for the scoped tokenbuddy package", async () => {
|
|
39
|
-
const binPath = writePackageJson(tempRoot, "1.0.0");
|
|
40
|
-
|
|
41
|
-
const check = await checkPackageUpdate({
|
|
42
|
-
fetch: makeFetch("1.2.0"),
|
|
43
|
-
argv: ["node", binPath],
|
|
44
|
-
cwd: tempRoot,
|
|
45
|
-
env: {},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
expect(check).toMatchObject({
|
|
49
|
-
packageName: "@tokenbuddy/tokenbuddy",
|
|
50
|
-
currentVersion: "1.0.0",
|
|
51
|
-
latestVersion: "1.2.0",
|
|
52
|
-
updateAvailable: true,
|
|
53
|
-
registryUrl: "https://registry.npmjs.org/%40tokenbuddy%2Ftokenbuddy",
|
|
54
|
-
installCommand: "npm install -g @tokenbuddy/tokenbuddy@1.2.0",
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("uses the unscoped package name only for legacy unscoped installs", async () => {
|
|
59
|
-
const binPath = writePackageJson(tempRoot, "1.0.0", "tokenbuddy");
|
|
60
|
-
|
|
61
|
-
const check = await checkPackageUpdate({
|
|
62
|
-
fetch: makeFetch("1.2.0"),
|
|
63
|
-
argv: ["node", binPath],
|
|
64
|
-
cwd: tempRoot,
|
|
65
|
-
env: {},
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
expect(check.packageName).toBe("tokenbuddy");
|
|
69
|
-
expect(check.installCommand).toBe("npm install -g tokenbuddy@1.2.0");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("installs the latest package and restarts tb-proxyd when an update is available", async () => {
|
|
73
|
-
const binPath = writePackageJson(tempRoot, "1.0.0");
|
|
74
|
-
const installCalls: Array<{ command: string; args: string[] }> = [];
|
|
75
|
-
const restartCalls: number[] = [];
|
|
76
|
-
|
|
77
|
-
const result = await runPackageUpdate(
|
|
78
|
-
{ apply: true, controlPort: 17820 },
|
|
79
|
-
{
|
|
80
|
-
fetch: makeFetch("1.1.0"),
|
|
81
|
-
argv: ["node", binPath],
|
|
82
|
-
cwd: tempRoot,
|
|
83
|
-
env: {},
|
|
84
|
-
runNpmInstall: (command, args) => {
|
|
85
|
-
installCalls.push({ command, args });
|
|
86
|
-
},
|
|
87
|
-
restartProxyd: async (controlPort) => {
|
|
88
|
-
restartCalls.push(controlPort);
|
|
89
|
-
return { attempted: true, restarted: true, method: "launchd" };
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
expect(result.install).toMatchObject({ attempted: true, succeeded: true });
|
|
95
|
-
expect(result.restart).toMatchObject({ attempted: true, restarted: true });
|
|
96
|
-
expect(installCalls).toEqual([{ command: "npm", args: ["install", "-g", "@tokenbuddy/tokenbuddy@1.1.0"] }]);
|
|
97
|
-
expect(restartCalls).toEqual([17820]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("does not install or restart when the current version is already latest", async () => {
|
|
101
|
-
const binPath = writePackageJson(tempRoot, "1.0.0");
|
|
102
|
-
const result = await runPackageUpdate(
|
|
103
|
-
{ apply: true, controlPort: 17820 },
|
|
104
|
-
{
|
|
105
|
-
fetch: makeFetch("1.0.0"),
|
|
106
|
-
argv: ["node", binPath],
|
|
107
|
-
cwd: tempRoot,
|
|
108
|
-
env: {},
|
|
109
|
-
runNpmInstall: () => {
|
|
110
|
-
throw new Error("install should not run");
|
|
111
|
-
},
|
|
112
|
-
restartProxyd: async () => {
|
|
113
|
-
throw new Error("restart should not run");
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
expect(result.install.attempted).toBe(false);
|
|
119
|
-
expect(result.restart.attempted).toBe(false);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("reports launchd restart scheduling without blocking on the current daemon process", () => {
|
|
123
|
-
const homeDir = path.join(tempRoot, "home");
|
|
124
|
-
const plistPath = path.join(homeDir, "Library", "LaunchAgents", "com.tokenbuddy.proxyd.plist");
|
|
125
|
-
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
126
|
-
fs.writeFileSync(plistPath, "", "utf8");
|
|
127
|
-
const children: string[][] = [];
|
|
128
|
-
|
|
129
|
-
const result = scheduleLaunchAgentRestart({
|
|
130
|
-
platform: "darwin",
|
|
131
|
-
homeDir,
|
|
132
|
-
spawn: ((command: string, args: string[]) => {
|
|
133
|
-
children.push([command, ...args]);
|
|
134
|
-
return { unref: () => undefined };
|
|
135
|
-
}) as unknown as typeof import("child_process").spawn,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(result).toMatchObject({
|
|
139
|
-
attempted: true,
|
|
140
|
-
scheduled: true,
|
|
141
|
-
method: "launchd",
|
|
142
|
-
plistPath,
|
|
143
|
-
});
|
|
144
|
-
expect(children[0]).toEqual(expect.arrayContaining(["sh", "-c"]));
|
|
145
|
-
expect(children[0][2]).toContain("launchctl kickstart -k");
|
|
146
|
-
});
|
|
147
|
-
});
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_PREWARM_TTL_MS,
|
|
3
|
-
PrewarmCache,
|
|
4
|
-
prewarmKey
|
|
5
|
-
} from "../src/prewarm-cache.js";
|
|
6
|
-
|
|
7
|
-
describe("PrewarmCache", () => {
|
|
8
|
-
function makeCandidate(overrides: { sellerId: string; healthScore?: number; url?: string }) {
|
|
9
|
-
return {
|
|
10
|
-
sellerId: overrides.sellerId,
|
|
11
|
-
url: overrides.url ?? `https://${overrides.sellerId}.example.com`,
|
|
12
|
-
healthScore: overrides.healthScore ?? 80
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
test("key encoding is collision-free and case-insensitive", () => {
|
|
17
|
-
const a = prewarmKey("gpt-4o", "chat_completions", "clawtip");
|
|
18
|
-
const b = prewarmKey("GPT-4O", "Chat_Completions", "ClawTip");
|
|
19
|
-
expect(a).toBe(b);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("get returns undefined for unknown keys; freshness reports empty", () => {
|
|
23
|
-
const cache = new PrewarmCache();
|
|
24
|
-
expect(cache.get("m1", "chat_completions", "clawtip")).toBeUndefined();
|
|
25
|
-
const f = cache.freshness("m1", "chat_completions", "clawtip");
|
|
26
|
-
expect(f.present).toBe(false);
|
|
27
|
-
expect(f.expired).toBe(true);
|
|
28
|
-
expect(f.expiringSoon).toBe(true);
|
|
29
|
-
expect(f.state).toBe("empty");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("beginWarming creates a new warming entry without mutating any prior candidates", () => {
|
|
33
|
-
const cache = new PrewarmCache();
|
|
34
|
-
cache.commitWarm({
|
|
35
|
-
modelId: "gpt-4o",
|
|
36
|
-
protocol: "chat_completions",
|
|
37
|
-
paymentMethod: "clawtip",
|
|
38
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const begin = cache.beginWarming("gpt-4o", "chat_completions", "clawtip");
|
|
42
|
-
expect(begin.hadPrevious).toBe(true);
|
|
43
|
-
expect(begin.entry.state).toBe("warming");
|
|
44
|
-
// Prior warm candidates are preserved while a new probe is in flight;
|
|
45
|
-
// a re-probe that finds nothing must not silently wipe the cache.
|
|
46
|
-
expect(begin.entry.candidates).toHaveLength(1);
|
|
47
|
-
expect(begin.entry.candidates[0].sellerId).toBe("s1");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("commitWarm resets warmedAt and replaces candidates", () => {
|
|
51
|
-
const fakeNow = (() => {
|
|
52
|
-
let t = 1_000_000;
|
|
53
|
-
return () => t;
|
|
54
|
-
})();
|
|
55
|
-
const cache = new PrewarmCache({ now: fakeNow });
|
|
56
|
-
cache.commitWarm({
|
|
57
|
-
modelId: "gpt-4o",
|
|
58
|
-
protocol: "chat_completions",
|
|
59
|
-
paymentMethod: "clawtip",
|
|
60
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
61
|
-
});
|
|
62
|
-
expect(cache.get("gpt-4o", "chat_completions", "clawtip")?.warmedAt).toBe(1_000_000);
|
|
63
|
-
|
|
64
|
-
fakeNow();
|
|
65
|
-
fakeNow();
|
|
66
|
-
const advanced = 1_000_000 + 9 * 60 * 1000; // 9 minutes later, still warm
|
|
67
|
-
const secondNow = (() => {
|
|
68
|
-
let t = advanced;
|
|
69
|
-
return () => t;
|
|
70
|
-
})();
|
|
71
|
-
const cache2 = new PrewarmCache({ now: secondNow });
|
|
72
|
-
cache2.commitWarm({
|
|
73
|
-
modelId: "gpt-4o",
|
|
74
|
-
protocol: "chat_completions",
|
|
75
|
-
paymentMethod: "clawtip",
|
|
76
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
77
|
-
});
|
|
78
|
-
cache2.commitWarm({
|
|
79
|
-
modelId: "gpt-4o",
|
|
80
|
-
protocol: "chat_completions",
|
|
81
|
-
paymentMethod: "clawtip",
|
|
82
|
-
candidates: [makeCandidate({ sellerId: "s2" }), makeCandidate({ sellerId: "s3" })]
|
|
83
|
-
});
|
|
84
|
-
const entry = cache2.get("gpt-4o", "chat_completions", "clawtip");
|
|
85
|
-
expect(entry?.warmedAt).toBe(advanced);
|
|
86
|
-
expect(entry?.state).toBe("warm");
|
|
87
|
-
expect(entry?.candidates.map((c) => c.sellerId).sort()).toEqual(["s2", "s3"]);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("commitWarm with zero candidates marks the entry as empty", () => {
|
|
91
|
-
const cache = new PrewarmCache();
|
|
92
|
-
const result = cache.commitWarm({
|
|
93
|
-
modelId: "gpt-4o",
|
|
94
|
-
protocol: "chat_completions",
|
|
95
|
-
paymentMethod: "clawtip",
|
|
96
|
-
candidates: []
|
|
97
|
-
});
|
|
98
|
-
expect(result.entry.state).toBe("empty");
|
|
99
|
-
expect(result.entry.candidates).toEqual([]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("commitWarm treats zero tokens-per-second as unknown", () => {
|
|
103
|
-
const cache = new PrewarmCache();
|
|
104
|
-
cache.commitWarm({
|
|
105
|
-
modelId: "gpt-4o",
|
|
106
|
-
protocol: "chat_completions",
|
|
107
|
-
paymentMethod: "clawtip",
|
|
108
|
-
candidates: [{
|
|
109
|
-
...makeCandidate({ sellerId: "s1" }),
|
|
110
|
-
avgTokensPerSecond: 0
|
|
111
|
-
}]
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
expect(cache.get("gpt-4o", "chat_completions", "clawtip")?.candidates[0].avgTokensPerSecond).toBeUndefined();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("freshness reports expiringSoon when within the last 10% of TTL", () => {
|
|
118
|
-
// Use a controllable `now` so we can advance time deterministically.
|
|
119
|
-
let now = 0;
|
|
120
|
-
const cache = new PrewarmCache({ now: () => now, defaultTtlMs: 1000 });
|
|
121
|
-
cache.commitWarm({
|
|
122
|
-
modelId: "gpt-4o",
|
|
123
|
-
protocol: "chat_completions",
|
|
124
|
-
paymentMethod: "clawtip",
|
|
125
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// At t=0: warmedAt=0, age=0, far from expiry.
|
|
129
|
-
const fresh = cache.freshness("gpt-4o", "chat_completions", "clawtip");
|
|
130
|
-
expect(fresh.expired).toBe(false);
|
|
131
|
-
expect(fresh.expiringSoon).toBe(false);
|
|
132
|
-
expect(fresh.remainingMs).toBe(1000);
|
|
133
|
-
|
|
134
|
-
// At t=850: 150ms left, not in the last 10%.
|
|
135
|
-
now = 850;
|
|
136
|
-
const midLife = cache.freshness("gpt-4o", "chat_completions", "clawtip");
|
|
137
|
-
expect(midLife.expired).toBe(false);
|
|
138
|
-
expect(midLife.expiringSoon).toBe(false);
|
|
139
|
-
expect(midLife.remainingMs).toBe(150);
|
|
140
|
-
|
|
141
|
-
// At t=950: 50ms left, in the last 10%.
|
|
142
|
-
now = 950;
|
|
143
|
-
const nearEnd = cache.freshness("gpt-4o", "chat_completions", "clawtip");
|
|
144
|
-
expect(nearEnd.expired).toBe(false);
|
|
145
|
-
expect(nearEnd.expiringSoon).toBe(true);
|
|
146
|
-
expect(nearEnd.remainingMs).toBe(50);
|
|
147
|
-
|
|
148
|
-
// At t=1100: past TTL, expired.
|
|
149
|
-
now = 1100;
|
|
150
|
-
const past = cache.freshness("gpt-4o", "chat_completions", "clawtip");
|
|
151
|
-
expect(past.expired).toBe(true);
|
|
152
|
-
expect(past.state).toBe("stale");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("recordFailure increments consecutive failures and marks entry stale", () => {
|
|
156
|
-
const cache = new PrewarmCache();
|
|
157
|
-
cache.commitWarm({
|
|
158
|
-
modelId: "gpt-4o",
|
|
159
|
-
protocol: "chat_completions",
|
|
160
|
-
paymentMethod: "clawtip",
|
|
161
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
162
|
-
});
|
|
163
|
-
const first = cache.recordFailure("gpt-4o", "chat_completions", "clawtip", "503");
|
|
164
|
-
expect(first?.consecutiveWarmingFailures).toBe(1);
|
|
165
|
-
expect(first?.state).toBe("stale");
|
|
166
|
-
const second = cache.recordFailure("gpt-4o", "chat_completions", "clawtip");
|
|
167
|
-
expect(second?.consecutiveWarmingFailures).toBe(2);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("recordFailure is a no-op when the key is unknown", () => {
|
|
171
|
-
const cache = new PrewarmCache();
|
|
172
|
-
expect(cache.recordFailure("missing", "chat_completions", "clawtip")).toBeUndefined();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("invalidateSeller drops the seller from every entry", () => {
|
|
176
|
-
const cache = new PrewarmCache();
|
|
177
|
-
cache.commitWarm({
|
|
178
|
-
modelId: "gpt-4o",
|
|
179
|
-
protocol: "chat_completions",
|
|
180
|
-
paymentMethod: "clawtip",
|
|
181
|
-
candidates: [makeCandidate({ sellerId: "s1" }), makeCandidate({ sellerId: "s2" })]
|
|
182
|
-
});
|
|
183
|
-
cache.commitWarm({
|
|
184
|
-
modelId: "claude-sonnet-4-5",
|
|
185
|
-
protocol: "chat_completions",
|
|
186
|
-
paymentMethod: "clawtip",
|
|
187
|
-
candidates: [makeCandidate({ sellerId: "s2" })]
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const affected = cache.invalidateSeller("s2");
|
|
191
|
-
expect(affected).toBe(2);
|
|
192
|
-
const gpt = cache.get("gpt-4o", "chat_completions", "clawtip");
|
|
193
|
-
expect(gpt?.candidates.map((c) => c.sellerId)).toEqual(["s1"]);
|
|
194
|
-
const claude = cache.get("claude-sonnet-4-5", "chat_completions", "clawtip");
|
|
195
|
-
expect(claude?.state).toBe("empty");
|
|
196
|
-
expect(claude?.candidates).toEqual([]);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test("invalidateKey removes a single entry entirely", () => {
|
|
200
|
-
const cache = new PrewarmCache();
|
|
201
|
-
cache.commitWarm({
|
|
202
|
-
modelId: "gpt-4o",
|
|
203
|
-
protocol: "chat_completions",
|
|
204
|
-
paymentMethod: "clawtip",
|
|
205
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
206
|
-
});
|
|
207
|
-
expect(cache.invalidateKey("gpt-4o", "chat_completions", "clawtip")).toBe(true);
|
|
208
|
-
expect(cache.invalidateKey("gpt-4o", "chat_completions", "clawtip")).toBe(false);
|
|
209
|
-
expect(cache.size()).toBe(0);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("evictExpired removes only entries past their TTL", () => {
|
|
213
|
-
// Use a single controllable clock so commit and evict see consistent time.
|
|
214
|
-
let now = 0;
|
|
215
|
-
const cache = new PrewarmCache({ now: () => now, defaultTtlMs: 1000 });
|
|
216
|
-
cache.commitWarm({
|
|
217
|
-
modelId: "gpt-4o",
|
|
218
|
-
protocol: "chat_completions",
|
|
219
|
-
paymentMethod: "clawtip",
|
|
220
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
221
|
-
});
|
|
222
|
-
cache.commitWarm({
|
|
223
|
-
modelId: "claude-sonnet-4-5",
|
|
224
|
-
protocol: "chat_completions",
|
|
225
|
-
paymentMethod: "clawtip",
|
|
226
|
-
candidates: [makeCandidate({ sellerId: "s2" })]
|
|
227
|
-
});
|
|
228
|
-
// Clock at 0; both fresh.
|
|
229
|
-
expect(cache.evictExpired()).toBe(0);
|
|
230
|
-
|
|
231
|
-
// Advance to t=500; still fresh.
|
|
232
|
-
now = 500;
|
|
233
|
-
expect(cache.evictExpired()).toBe(0);
|
|
234
|
-
|
|
235
|
-
// Advance to t=1500; both entries now older than TTL=1000.
|
|
236
|
-
now = 1500;
|
|
237
|
-
expect(cache.evictExpired()).toBe(2);
|
|
238
|
-
expect(cache.size()).toBe(0);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("isExpiringSoon returns true only inside the expiry window", () => {
|
|
242
|
-
const cache = new PrewarmCache({ now: () => 0, defaultTtlMs: 1000 });
|
|
243
|
-
cache.commitWarm({
|
|
244
|
-
modelId: "gpt-4o",
|
|
245
|
-
protocol: "chat_completions",
|
|
246
|
-
paymentMethod: "clawtip",
|
|
247
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
248
|
-
});
|
|
249
|
-
// At t=0: warmedAt==0, age==0, not expiring.
|
|
250
|
-
expect(cache.isExpiringSoon("gpt-4o", "chat_completions", "clawtip", 100, 200)).toBe(false);
|
|
251
|
-
// At t=850 (150ms left, within 100ms window? no).
|
|
252
|
-
expect(cache.isExpiringSoon("gpt-4o", "chat_completions", "clawtip", 100, 850)).toBe(false);
|
|
253
|
-
// At t=950 (50ms left, within 100ms window).
|
|
254
|
-
expect(cache.isExpiringSoon("gpt-4o", "chat_completions", "clawtip", 100, 950)).toBe(true);
|
|
255
|
-
// At t=1000 (expired; not in the "soon" window anymore).
|
|
256
|
-
expect(cache.isExpiringSoon("gpt-4o", "chat_completions", "clawtip", 100, 1000)).toBe(false);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("snapshot returns deep copies that do not mutate cache state", () => {
|
|
260
|
-
const cache = new PrewarmCache();
|
|
261
|
-
cache.commitWarm({
|
|
262
|
-
modelId: "gpt-4o",
|
|
263
|
-
protocol: "chat_completions",
|
|
264
|
-
paymentMethod: "clawtip",
|
|
265
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
266
|
-
});
|
|
267
|
-
const snap = cache.snapshot();
|
|
268
|
-
snap[0].candidates[0].healthScore = -1;
|
|
269
|
-
expect(cache.get("gpt-4o", "chat_completions", "clawtip")?.candidates[0].healthScore).not.toBe(-1);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("keys() decodes the cache key back into the model/protocol/payment triple", () => {
|
|
273
|
-
const cache = new PrewarmCache();
|
|
274
|
-
cache.commitWarm({
|
|
275
|
-
modelId: "gpt-4o",
|
|
276
|
-
protocol: "chat_completions",
|
|
277
|
-
paymentMethod: "clawtip",
|
|
278
|
-
candidates: [makeCandidate({ sellerId: "s1" })]
|
|
279
|
-
});
|
|
280
|
-
cache.commitWarm({
|
|
281
|
-
modelId: "claude-sonnet-4-5",
|
|
282
|
-
protocol: "messages",
|
|
283
|
-
paymentMethod: "clawtip",
|
|
284
|
-
candidates: [makeCandidate({ sellerId: "s2" })]
|
|
285
|
-
});
|
|
286
|
-
const keys = cache.keys().sort((a, b) => a.modelId.localeCompare(b.modelId));
|
|
287
|
-
expect(keys).toEqual([
|
|
288
|
-
{ modelId: "claude-sonnet-4-5", protocol: "messages", paymentMethod: "clawtip" },
|
|
289
|
-
{ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" }
|
|
290
|
-
]);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test("default TTL is 10 minutes", () => {
|
|
294
|
-
expect(DEFAULT_PREWARM_TTL_MS).toBe(600_000);
|
|
295
|
-
});
|
|
296
|
-
});
|