@velum-labs/routekit-daemon 0.16.1 → 0.16.3
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/index.js +51 -37
- package/dist/test/daemon.test.js +100 -19
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -8,19 +8,18 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
10
10
|
import { basename, dirname, join } from "node:path";
|
|
11
|
-
import { AccountActivityCoordinator, CLIPROXY_API_KEY_ENV, CLIPROXY_BASE_URL_ENV,
|
|
11
|
+
import { AccountActivityCoordinator, accountStoreEntries, CLIPROXY_API_KEY_ENV, CLIPROXY_BASE_URL_ENV, cliproxyAccountEntries, cliproxyAccountMatchesKind, cliproxyApiKey, cliproxyAuthDirectory, cliproxyBaseUrl, cliproxyCredentialValid, defaultSubscriptionAccountDirectory, RateLimitTracker, removeCliproxyAccount, removeSubscriptionAccount, renameSubscriptionAccount, sanitizeSubscriptionLabel, subscriptionAccountIdentity } from "@velum-labs/routekit-accounts";
|
|
12
12
|
import { configuredProviderIds, globalRouterConfigPath, parseRouterConfigDocument, routekitHome, writeRouterConfig } from "@velum-labs/routekit-config";
|
|
13
13
|
import { createRouteKitControlHandler, ROUTEKIT_CONTROL_CAPABILITY } from "@velum-labs/routekit-control";
|
|
14
|
-
import { startSwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
|
|
15
|
-
import {
|
|
16
|
-
import { PROVIDERS, accountKindForCliproxyAuthType, resolveAccountConnector } from "@velum-labs/routekit-registry";
|
|
14
|
+
import { resolveLeaderboardConfig, startSwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
|
|
15
|
+
import { accountKindForCliproxyAuthType, PROVIDERS, resolveAccountConnector } from "@velum-labs/routekit-registry";
|
|
17
16
|
import { startRouter } from "@velum-labs/routekit-router";
|
|
18
17
|
import { acquireLifecycleLock, CONTROL_PROTOCOL_VERSION, ControlClient, ControlError, createPortlessSession, createServiceRecordStore, createTokenStore, encodeJoinCredential, extendCleanupGrace, generateControlToken, nextServiceGeneration, processIdentity, registerCleanup, SERVICE_HOME_MODE, startControlServer, supervisorFromEnv, writeFileAtomic } from "@velum-labs/routekit-runtime";
|
|
19
18
|
import { createConsentManager } from "@velum-labs/routekit-telemetry-core";
|
|
20
19
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
21
|
-
import { createCliproxySidecar } from "./cliproxy-sidecar.js";
|
|
22
20
|
import { cleanupAccountTransaction, markAccountTransactionCommitted, prepareAccountTransaction, recoverAccountTransactions, rollbackAccountTransaction } from "./account-transaction.js";
|
|
23
21
|
import { CallAttributionStore, callInspection } from "./call-attribution-store.js";
|
|
22
|
+
import { createCliproxySidecar } from "./cliproxy-sidecar.js";
|
|
24
23
|
import { aggregateInspections, buildLeaderboardResult, defaultLeaderboardWindow, LeaderboardRollupStore } from "./leaderboard.js";
|
|
25
24
|
export const ROUTEKIT_DAEMON_KIND = "daemon";
|
|
26
25
|
export const ROUTEKIT_PRODUCT = "routekit";
|
|
@@ -111,9 +110,7 @@ function readRevisions(home) {
|
|
|
111
110
|
accounts: typeof parsed.accounts === "number" && Number.isSafeInteger(parsed.accounts)
|
|
112
111
|
? parsed.accounts
|
|
113
112
|
: 0,
|
|
114
|
-
daemon: typeof parsed.daemon === "number" && Number.isSafeInteger(parsed.daemon)
|
|
115
|
-
? parsed.daemon
|
|
116
|
-
: 0
|
|
113
|
+
daemon: typeof parsed.daemon === "number" && Number.isSafeInteger(parsed.daemon) ? parsed.daemon : 0
|
|
117
114
|
};
|
|
118
115
|
}
|
|
119
116
|
catch {
|
|
@@ -170,7 +167,7 @@ function providerCredentialAvailable(provider, accounts, env) {
|
|
|
170
167
|
return accounts.some((entry) => entry.subscriptionKind === provider);
|
|
171
168
|
}
|
|
172
169
|
if (provider === "cliproxy") {
|
|
173
|
-
return (
|
|
170
|
+
return (env[CLIPROXY_API_KEY_ENV] ?? "").length > 0 || cliproxyApiKey(env) !== undefined;
|
|
174
171
|
}
|
|
175
172
|
const info = PROVIDERS[provider];
|
|
176
173
|
if (info?.keyEnv === undefined)
|
|
@@ -203,7 +200,7 @@ function safeCliproxyCredentialBlob(kind, value) {
|
|
|
203
200
|
const type = typeof record.type === "string" ? record.type : undefined;
|
|
204
201
|
const classified = type === undefined
|
|
205
202
|
? undefined
|
|
206
|
-
: accountKindForCliproxyAuthType(type) ?? resolveAccountConnector(type)?.kind;
|
|
203
|
+
: (accountKindForCliproxyAuthType(type) ?? resolveAccountConnector(type)?.kind);
|
|
207
204
|
if (classified !== kind || !cliproxyCredentialValid(record, type)) {
|
|
208
205
|
throw new ControlError({
|
|
209
206
|
code: "bad_request",
|
|
@@ -396,10 +393,12 @@ export async function startRouteKitDaemon(options) {
|
|
|
396
393
|
};
|
|
397
394
|
}
|
|
398
395
|
});
|
|
399
|
-
portless = await createPortlessSession(options.portless ?? env.ROUTEKIT_PORTLESS !== "0", {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
:
|
|
396
|
+
portless = await createPortlessSession(options.portless ?? env.ROUTEKIT_PORTLESS !== "0", {
|
|
397
|
+
project: "routekit",
|
|
398
|
+
ownerLabel: "routekit-daemon",
|
|
399
|
+
bareNames: []
|
|
400
|
+
});
|
|
401
|
+
const dataUrl = portless.enabled ? portless.register("gateway", proxy.port()) : proxy.url();
|
|
403
402
|
const replaceRouter = async (nextConfig, nextDocument, input) => {
|
|
404
403
|
// Sidecar reconcile runs before the generation commits; any failure
|
|
405
404
|
// below must put the sidecar back to the still-live currentConfig.
|
|
@@ -708,6 +707,9 @@ export async function startRouteKitDaemon(options) {
|
|
|
708
707
|
inFlight: 0,
|
|
709
708
|
lastSelected: false,
|
|
710
709
|
active: false,
|
|
710
|
+
...(entry.credentialValid
|
|
711
|
+
? {}
|
|
712
|
+
: { readinessReasons: [{ code: "credential_invalid" }] }),
|
|
711
713
|
models: []
|
|
712
714
|
};
|
|
713
715
|
}
|
|
@@ -730,6 +732,11 @@ export async function startRouteKitDaemon(options) {
|
|
|
730
732
|
: {}),
|
|
731
733
|
lastSelected: member?.lastSelected ?? false,
|
|
732
734
|
active: member?.lastSelected ?? false,
|
|
735
|
+
...(member?.readinessReasons !== undefined
|
|
736
|
+
? { readinessReasons: member.readinessReasons }
|
|
737
|
+
: member === undefined
|
|
738
|
+
? { readinessReasons: [{ code: "credential_invalid" }] }
|
|
739
|
+
: {}),
|
|
733
740
|
models: member?.models ?? [],
|
|
734
741
|
...(member?.limits !== undefined ? { limits: member.limits } : {})
|
|
735
742
|
};
|
|
@@ -797,8 +804,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
797
804
|
const label = connector === "native"
|
|
798
805
|
? sanitizeSubscriptionLabel(account.label)
|
|
799
806
|
: safeCliproxyLabel(account.label);
|
|
800
|
-
if (label !== account.label ||
|
|
801
|
-
(connector === "native" && label.startsWith("."))) {
|
|
807
|
+
if (label !== account.label || (connector === "native" && label.startsWith("."))) {
|
|
802
808
|
throw new ControlError({
|
|
803
809
|
code: "bad_request",
|
|
804
810
|
message: "account label must already be normalized"
|
|
@@ -887,8 +893,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
887
893
|
});
|
|
888
894
|
}
|
|
889
895
|
}
|
|
890
|
-
const unchanged = prepared.every((entry) => existsSync(entry.path) &&
|
|
891
|
-
readFileSync(entry.path, "utf8") === entry.content);
|
|
896
|
+
const unchanged = prepared.every((entry) => existsSync(entry.path) && readFileSync(entry.path, "utf8") === entry.content);
|
|
892
897
|
if (unchanged &&
|
|
893
898
|
currentConfig.providers[provider] !== undefined) {
|
|
894
899
|
return;
|
|
@@ -1023,8 +1028,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
1023
1028
|
!Array.isArray(raw.providers)
|
|
1024
1029
|
? { ...raw.providers }
|
|
1025
1030
|
: {};
|
|
1026
|
-
const disableProvider = !hasRemainingAccount &&
|
|
1027
|
-
currentConfig.providers[nativeKind] !== undefined;
|
|
1031
|
+
const disableProvider = !hasRemainingAccount && currentConfig.providers[nativeKind] !== undefined;
|
|
1028
1032
|
if (disableProvider) {
|
|
1029
1033
|
for (const providerKey of nativeKind === "claude-code"
|
|
1030
1034
|
? ["claude-code", "claudeCode", "claude"]
|
|
@@ -1037,12 +1041,8 @@ export async function startRouteKitDaemon(options) {
|
|
|
1037
1041
|
delete raw.defaultModel;
|
|
1038
1042
|
}
|
|
1039
1043
|
}
|
|
1040
|
-
const nextDocument = disableProvider
|
|
1041
|
-
|
|
1042
|
-
: currentDocument;
|
|
1043
|
-
const nextConfig = disableProvider
|
|
1044
|
-
? parseConfigDocument(nextDocument)
|
|
1045
|
-
: currentConfig;
|
|
1044
|
+
const nextDocument = disableProvider ? stringifyYaml(raw) : currentDocument;
|
|
1045
|
+
const nextConfig = disableProvider ? parseConfigDocument(nextDocument) : currentConfig;
|
|
1046
1046
|
const activityPath = join(home, "usage", "account-activity.v1.json");
|
|
1047
1047
|
const transaction = prepareAccountTransaction({
|
|
1048
1048
|
home,
|
|
@@ -1054,7 +1054,9 @@ export async function startRouteKitDaemon(options) {
|
|
|
1054
1054
|
labels: [params.label]
|
|
1055
1055
|
});
|
|
1056
1056
|
try {
|
|
1057
|
-
const result = removeSubscriptionAccount(nativeKind, params.label, {
|
|
1057
|
+
const result = removeSubscriptionAccount(nativeKind, params.label, {
|
|
1058
|
+
accountsDirectory: activeNativeDirectory
|
|
1059
|
+
});
|
|
1058
1060
|
removed = result.removed;
|
|
1059
1061
|
if (!result.removed) {
|
|
1060
1062
|
cleanupAccountTransaction(transaction);
|
|
@@ -1235,6 +1237,22 @@ export async function startRouteKitDaemon(options) {
|
|
|
1235
1237
|
"accounts.usage": async (_params, context) => {
|
|
1236
1238
|
return await activeRouter.usage(context.signal);
|
|
1237
1239
|
},
|
|
1240
|
+
"accounts.resetCredits": async (params, context) => {
|
|
1241
|
+
try {
|
|
1242
|
+
return {
|
|
1243
|
+
kind: params.kind,
|
|
1244
|
+
label: params.label,
|
|
1245
|
+
resetCredits: await activeRouter.listResetCredits(params.kind, params.label, context.signal)
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1250
|
+
if (message.includes("is not enrolled") || message.includes("no codex account pool")) {
|
|
1251
|
+
throw new ControlError({ code: "not_found", message });
|
|
1252
|
+
}
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
},
|
|
1238
1256
|
"accounts.redeemReset": async (params, context) => {
|
|
1239
1257
|
try {
|
|
1240
1258
|
const result = await activeRouter.redeemReset({
|
|
@@ -1252,9 +1270,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
1252
1270
|
label: result.label,
|
|
1253
1271
|
redeemRequestId: result.redeemRequestId,
|
|
1254
1272
|
...(result.creditId !== undefined ? { creditId: result.creditId } : {}),
|
|
1255
|
-
...(result.windowsReset !== undefined
|
|
1256
|
-
? { windowsReset: result.windowsReset }
|
|
1257
|
-
: {}),
|
|
1273
|
+
...(result.windowsReset !== undefined ? { windowsReset: result.windowsReset } : {}),
|
|
1258
1274
|
usage: result.usage
|
|
1259
1275
|
};
|
|
1260
1276
|
}
|
|
@@ -1289,9 +1305,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
1289
1305
|
const missingProviders = [
|
|
1290
1306
|
...new Set(accounts
|
|
1291
1307
|
.filter((entry) => {
|
|
1292
|
-
const provider = entry.connector === "cliproxy"
|
|
1293
|
-
? "cliproxy"
|
|
1294
|
-
: entry.subscriptionKind;
|
|
1308
|
+
const provider = entry.connector === "cliproxy" ? "cliproxy" : entry.subscriptionKind;
|
|
1295
1309
|
return currentConfig.providers[provider] === undefined;
|
|
1296
1310
|
})
|
|
1297
1311
|
.map((entry) => entry.subscriptionKind))
|
|
@@ -1364,7 +1378,9 @@ export async function startRouteKitDaemon(options) {
|
|
|
1364
1378
|
if (model === undefined || !listed.models.some((entry) => entry.id === model)) {
|
|
1365
1379
|
throw new ControlError({
|
|
1366
1380
|
code: "not_found",
|
|
1367
|
-
message: params.model === undefined
|
|
1381
|
+
message: params.model === undefined
|
|
1382
|
+
? "no model is available"
|
|
1383
|
+
: `unknown model: ${params.model}`
|
|
1368
1384
|
});
|
|
1369
1385
|
}
|
|
1370
1386
|
return {
|
|
@@ -1381,9 +1397,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
1381
1397
|
label: params.label,
|
|
1382
1398
|
plane: params.plane,
|
|
1383
1399
|
role: "admin",
|
|
1384
|
-
createdBy: params.createdBy ??
|
|
1385
|
-
context.principal?.label ??
|
|
1386
|
-
"control"
|
|
1400
|
+
createdBy: params.createdBy ?? context.principal?.label ?? "control"
|
|
1387
1401
|
});
|
|
1388
1402
|
if (issued.plane === "data") {
|
|
1389
1403
|
dataTokenCache.set(issued.label, issued.token);
|
package/dist/test/daemon.test.js
CHANGED
|
@@ -8,8 +8,8 @@ import { CLIPROXY_PINNED_VERSION } from "@velum-labs/routekit-accounts";
|
|
|
8
8
|
import { RouteKitControlClient } from "@velum-labs/routekit-control";
|
|
9
9
|
import { ControlClient, ControlError, createServiceRecordStore } from "@velum-labs/routekit-runtime";
|
|
10
10
|
import { parse as parseYaml } from "yaml";
|
|
11
|
-
import { startRouteKitDaemon } from "../index.js";
|
|
12
11
|
import { prepareAccountTransaction } from "../account-transaction.js";
|
|
12
|
+
import { startRouteKitDaemon } from "../index.js";
|
|
13
13
|
async function mockProvider() {
|
|
14
14
|
const server = createServer((req, res) => {
|
|
15
15
|
if (req.url === "/v1/models") {
|
|
@@ -58,7 +58,9 @@ async function withMockAnthropicDiscovery(run) {
|
|
|
58
58
|
globalThis.fetch = async (input, init) => {
|
|
59
59
|
const url = new URL(input instanceof Request ? input.url : input.toString());
|
|
60
60
|
if (url.hostname === "api.anthropic.com" && url.pathname === "/v1/models") {
|
|
61
|
-
return new Response(JSON.stringify({ data: [{ id: "claude-test-model", type: "model" }] }), {
|
|
61
|
+
return new Response(JSON.stringify({ data: [{ id: "claude-test-model", type: "model" }] }), {
|
|
62
|
+
headers: { "content-type": "application/json" }
|
|
63
|
+
});
|
|
62
64
|
}
|
|
63
65
|
return await originalFetch(input, init);
|
|
64
66
|
};
|
|
@@ -69,7 +71,7 @@ async function withMockAnthropicDiscovery(run) {
|
|
|
69
71
|
globalThis.fetch = originalFetch;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
|
-
async function withMockNativeDiscovery(kind, run) {
|
|
74
|
+
async function withMockNativeDiscovery(kind, run, options = {}) {
|
|
73
75
|
if (kind === "claude-code")
|
|
74
76
|
return await withMockAnthropicDiscovery(run);
|
|
75
77
|
const originalFetch = globalThis.fetch;
|
|
@@ -80,6 +82,16 @@ async function withMockNativeDiscovery(kind, run) {
|
|
|
80
82
|
url.pathname.endsWith("/models")) {
|
|
81
83
|
return Response.json({ models: [{ slug: "gpt-test-model" }] });
|
|
82
84
|
}
|
|
85
|
+
if (options.healthyUsage === true &&
|
|
86
|
+
url.hostname === "chatgpt.com" &&
|
|
87
|
+
url.pathname === "/backend-api/wham/usage") {
|
|
88
|
+
return Response.json({
|
|
89
|
+
plan_type: "plus",
|
|
90
|
+
rate_limit: {
|
|
91
|
+
primary_window: { used_percent: 10, reset_at: Date.now() / 1000 + 3_600 }
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
83
95
|
return await originalFetch(input, init);
|
|
84
96
|
};
|
|
85
97
|
try {
|
|
@@ -168,6 +180,9 @@ test("singleton daemon exposes authenticated control and a stable reloadable dat
|
|
|
168
180
|
await assert.rejects(client.call("models.info", { model: "openai/not-real" }), (error) => error instanceof ControlError &&
|
|
169
181
|
error.code === "not_found" &&
|
|
170
182
|
/unknown model/.test(error.message));
|
|
183
|
+
await assert.rejects(client.call("accounts.resetCredits", { kind: "codex", label: "work" }), (error) => error instanceof ControlError &&
|
|
184
|
+
error.code === "not_found" &&
|
|
185
|
+
/no codex account pool/.test(error.message));
|
|
171
186
|
const beforeUrl = status.dataUrl;
|
|
172
187
|
const snapshot = await client.call("config.get", {});
|
|
173
188
|
await assert.rejects(client.call("config.update", {
|
|
@@ -473,6 +488,69 @@ test("daemon account activity persists last selection independently of leaderboa
|
|
|
473
488
|
rmSync(root, { recursive: true, force: true });
|
|
474
489
|
}
|
|
475
490
|
});
|
|
491
|
+
test("cleared persisted cooldown remains absent and eligible after daemon reload", async () => {
|
|
492
|
+
const root = mkdtempSync(join(tmpdir(), "routekit-daemon-cooldown-reload-"));
|
|
493
|
+
const stateHome = join(root, "state");
|
|
494
|
+
const configPath = join(root, "router.yaml");
|
|
495
|
+
const accountsDirectory = join(stateHome, "subscriptions", "codex");
|
|
496
|
+
const statePath = join(accountsDirectory, ".state.json");
|
|
497
|
+
const coolingUntil = Date.now() / 1000 + 3_600;
|
|
498
|
+
mkdirSync(accountsDirectory, { recursive: true });
|
|
499
|
+
writeFileSync(join(accountsDirectory, "work.json"), `${JSON.stringify(nativeCredential("codex"))}\n`);
|
|
500
|
+
writeFileSync(statePath, JSON.stringify({ members: [{ id: "work", coolingUntil }] }));
|
|
501
|
+
writeFileSync(configPath, "providers:\n codex: {}\ndefaultModel: codex/gpt-test-model\n");
|
|
502
|
+
try {
|
|
503
|
+
await withMockNativeDiscovery("codex", async () => {
|
|
504
|
+
const daemon = await startRouteKitDaemon({
|
|
505
|
+
packageVersion: "1.2.3",
|
|
506
|
+
stateHome,
|
|
507
|
+
configPath,
|
|
508
|
+
port: 0,
|
|
509
|
+
portless: false,
|
|
510
|
+
env: {
|
|
511
|
+
HOME: root,
|
|
512
|
+
ROUTEKIT_HOME: stateHome,
|
|
513
|
+
ROUTEKIT_PORTLESS: "0"
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
try {
|
|
517
|
+
const client = new RouteKitControlClient({
|
|
518
|
+
url: daemon.record.url,
|
|
519
|
+
token: daemon.record.controlToken
|
|
520
|
+
});
|
|
521
|
+
const before = await client.call("accounts.status", {});
|
|
522
|
+
assert.equal(before.accounts[0]?.relayOpen, false);
|
|
523
|
+
assert.deepEqual(before.accounts[0]?.readinessReasons, [
|
|
524
|
+
{ code: "cooldown_active", until: coolingUntil }
|
|
525
|
+
]);
|
|
526
|
+
const usage = await client.call("accounts.usage", {});
|
|
527
|
+
const recovered = usage.accountSets[0]?.members[0];
|
|
528
|
+
assert.equal(recovered?.coolingUntil, undefined);
|
|
529
|
+
assert.equal(recovered?.poolEligible, true);
|
|
530
|
+
assert.equal(recovered?.relayReady, true);
|
|
531
|
+
assert.deepEqual(recovered?.readinessReasons, []);
|
|
532
|
+
const persisted = JSON.parse(readFileSync(statePath, "utf8"));
|
|
533
|
+
assert.equal(persisted.members[0]?.coolingUntil, undefined);
|
|
534
|
+
assert.equal(persisted.members[0]?.cooldownRevision, 2);
|
|
535
|
+
const daemonStatus = await client.call("daemon.status", {});
|
|
536
|
+
await client.call("daemon.reload", { expectedRevision: daemonStatus.configRevision });
|
|
537
|
+
const afterReload = await client.call("accounts.status", {});
|
|
538
|
+
assert.equal(afterReload.accounts[0]?.relayOpen, true);
|
|
539
|
+
assert.deepEqual(afterReload.accounts[0]?.readinessReasons, []);
|
|
540
|
+
const afterUsage = await client.call("accounts.usage", {});
|
|
541
|
+
assert.equal(afterUsage.accountSets[0]?.members[0]?.coolingUntil, undefined);
|
|
542
|
+
assert.equal(afterUsage.accountSets[0]?.members[0]?.poolEligible, true);
|
|
543
|
+
assert.equal(JSON.parse(readFileSync(statePath, "utf8")).members[0]?.coolingUntil, undefined);
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
await daemon.close();
|
|
547
|
+
}
|
|
548
|
+
}, { healthyUsage: true });
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
rmSync(root, { recursive: true, force: true });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
476
554
|
for (const kind of ["claude-code", "codex"]) {
|
|
477
555
|
test(`native ${kind} account rename preserves routing and usage state`, async () => {
|
|
478
556
|
const root = mkdtempSync(join(tmpdir(), `routekit-daemon-rename-${kind}-`));
|
|
@@ -763,12 +841,7 @@ test("native removal failure before deletion cleans its prepared transaction", a
|
|
|
763
841
|
mode: 0o600
|
|
764
842
|
});
|
|
765
843
|
symlinkSync(externalPath, accountPath);
|
|
766
|
-
writeFileSync(configPath, [
|
|
767
|
-
"providers:",
|
|
768
|
-
" openai: {}",
|
|
769
|
-
"defaultModel: openai/mock-model",
|
|
770
|
-
""
|
|
771
|
-
].join("\n"));
|
|
844
|
+
writeFileSync(configPath, ["providers:", " openai: {}", "defaultModel: openai/mock-model", ""].join("\n"));
|
|
772
845
|
const upstream = await mockProvider();
|
|
773
846
|
const daemon = await startRouteKitDaemon({
|
|
774
847
|
packageVersion: "1.2.3",
|
|
@@ -1019,11 +1092,10 @@ test("daemon owns the cliproxy sidecar: spawn, restart, account routing, shutdow
|
|
|
1019
1092
|
assert.ok(await waitFor(() => readFileSync(markerPath, "utf8").trim().split("\n").length === 3, 10_000), "accounts.sync did not restart the managed sidecar");
|
|
1020
1093
|
assert.equal(processAlive(respawnedPid), false);
|
|
1021
1094
|
const refreshedStatus = await client.call("accounts.status", {});
|
|
1022
|
-
assert.equal(refreshedStatus.accounts.find((entry) => entry.label === "antigravity-user@example.com")
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
assert.equal(refreshedStatus.accounts.find((entry) => entry.label === "broken-account")
|
|
1026
|
-
?.credentialValid, false);
|
|
1095
|
+
assert.equal(refreshedStatus.accounts.find((entry) => entry.label === "antigravity-user@example.com")
|
|
1096
|
+
?.credentialValid, true);
|
|
1097
|
+
assert.equal(refreshedStatus.accounts.find((entry) => entry.label === "kimi-invalid")?.credentialValid, false);
|
|
1098
|
+
assert.equal(refreshedStatus.accounts.find((entry) => entry.label === "broken-account")?.credentialValid, false);
|
|
1027
1099
|
const syncedPid = Number(readFileSync(markerPath, "utf8").trim().split("\n")[2]);
|
|
1028
1100
|
// Unclassified/corrupt auth files remain removable using the kind shown
|
|
1029
1101
|
// by accounts.list rather than becoming stuck in the store.
|
|
@@ -1072,7 +1144,9 @@ test("daemon owns the cliproxy sidecar: spawn, restart, account routing, shutdow
|
|
|
1072
1144
|
}
|
|
1073
1145
|
]
|
|
1074
1146
|
};
|
|
1075
|
-
const activated = await client.call("accounts.enrollActivate", activationParams, {
|
|
1147
|
+
const activated = await client.call("accounts.enrollActivate", activationParams, {
|
|
1148
|
+
idempotencyKey: "activate-grok"
|
|
1149
|
+
});
|
|
1076
1150
|
assert.equal(activated.activated, true);
|
|
1077
1151
|
assert.equal(activated.configRevision, beforeActivation.configRevision + 1);
|
|
1078
1152
|
assert.equal(activated.accountRevision, beforeActivation.accountRevision + 1);
|
|
@@ -1081,11 +1155,14 @@ test("daemon owns the cliproxy sidecar: spawn, restart, account routing, shutdow
|
|
|
1081
1155
|
assert.equal(existsSync(join(stateHome, "account-transactions")), false);
|
|
1082
1156
|
// A fresh transport retry converges on the committed state without
|
|
1083
1157
|
// incrementing either revision again.
|
|
1084
|
-
const replayed = await client.call("accounts.enrollActivate", activationParams, {
|
|
1158
|
+
const replayed = await client.call("accounts.enrollActivate", activationParams, {
|
|
1159
|
+
idempotencyKey: "activate-grok-retry"
|
|
1160
|
+
});
|
|
1085
1161
|
assert.equal(replayed.configRevision, activated.configRevision);
|
|
1086
1162
|
assert.equal(replayed.accountRevision, activated.accountRevision);
|
|
1087
1163
|
const activatedStatus = await client.call("accounts.status", {});
|
|
1088
|
-
assert.equal(activatedStatus.accounts.find((entry) => entry.label === "xai-transaction@example.com")
|
|
1164
|
+
assert.equal(activatedStatus.accounts.find((entry) => entry.label === "xai-transaction@example.com")
|
|
1165
|
+
?.configured, true);
|
|
1089
1166
|
const beforeClaude = await client.call("daemon.status", {});
|
|
1090
1167
|
const claudeActivation = {
|
|
1091
1168
|
kind: "claude-code",
|
|
@@ -1113,14 +1190,18 @@ test("daemon owns the cliproxy sidecar: spawn, restart, account routing, shutdow
|
|
|
1113
1190
|
assert.equal((await client.call("daemon.status", {})).configRevision, beforeClaude.configRevision);
|
|
1114
1191
|
assert.equal((await client.call("daemon.status", {})).accountRevision, beforeClaude.accountRevision);
|
|
1115
1192
|
await withMockAnthropicDiscovery(async () => {
|
|
1116
|
-
const claudeActivated = await client.call("accounts.enrollActivate", claudeActivation, {
|
|
1193
|
+
const claudeActivated = await client.call("accounts.enrollActivate", claudeActivation, {
|
|
1194
|
+
idempotencyKey: "activate-claude"
|
|
1195
|
+
});
|
|
1117
1196
|
assert.equal(claudeActivated.activated, true);
|
|
1118
1197
|
assert.equal(claudeActivated.configRevision, beforeClaude.configRevision + 1);
|
|
1119
1198
|
assert.equal(claudeActivated.accountRevision, beforeClaude.accountRevision + 1);
|
|
1120
1199
|
assert.equal(existsSync(claudePath), true);
|
|
1121
1200
|
assert.match(readFileSync(configPath, "utf8"), /claude-code:/);
|
|
1122
1201
|
assert.doesNotMatch(JSON.stringify(claudeActivated), /claude-transaction-access|claude-transaction-refresh/);
|
|
1123
|
-
const claudeReplay = await client.call("accounts.enrollActivate", claudeActivation, {
|
|
1202
|
+
const claudeReplay = await client.call("accounts.enrollActivate", claudeActivation, {
|
|
1203
|
+
idempotencyKey: "activate-claude-retry"
|
|
1204
|
+
});
|
|
1124
1205
|
assert.equal(claudeReplay.configRevision, claudeActivated.configRevision);
|
|
1125
1206
|
assert.equal(claudeReplay.accountRevision, claudeActivated.accountRevision);
|
|
1126
1207
|
assert.equal((await client.call("accounts.status", {})).accounts.find((entry) => entry.subscriptionKind === "claude-code" && entry.label === "claude-work")?.configured, true);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-daemon",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.16.
|
|
4
|
+
"version": "0.16.3",
|
|
5
5
|
"description": "Singleton RouteKit control daemon and stable model gateway.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"yaml": "2.9.0",
|
|
37
|
-
"@velum-labs/routekit-accounts": "0.16.
|
|
38
|
-
"@velum-labs/routekit-config": "0.16.
|
|
39
|
-
"@velum-labs/routekit-control": "0.16.
|
|
40
|
-
"@velum-labs/routekit-gateway": "0.16.
|
|
41
|
-
"@velum-labs/routekit-registry": "0.16.
|
|
42
|
-
"@velum-labs/routekit-router": "0.16.
|
|
43
|
-
"@velum-labs/routekit-
|
|
44
|
-
"@velum-labs/routekit-
|
|
37
|
+
"@velum-labs/routekit-accounts": "0.16.3",
|
|
38
|
+
"@velum-labs/routekit-config": "0.16.3",
|
|
39
|
+
"@velum-labs/routekit-control": "0.16.3",
|
|
40
|
+
"@velum-labs/routekit-gateway": "0.16.3",
|
|
41
|
+
"@velum-labs/routekit-registry": "0.16.3",
|
|
42
|
+
"@velum-labs/routekit-router": "0.16.3",
|
|
43
|
+
"@velum-labs/routekit-telemetry-core": "0.16.3",
|
|
44
|
+
"@velum-labs/routekit-runtime": "0.16.3"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsc -b",
|