@velum-labs/routekit-daemon 0.17.2 → 0.17.4

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 CHANGED
@@ -8,10 +8,10 @@
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, 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";
11
+ import { AccountActivityCoordinator, AccountAuthCoordinator, accountStoreEntries, CLIPROXY_API_KEY_ENV, CLIPROXY_BASE_URL_ENV, cliproxyAccountEntries, cliproxyAccountMatchesKind, cliproxyApiKey, cliproxyAuthDirectory, cliproxyBaseUrl, cliproxyCredentialValid, defaultSubscriptionAccountDirectory, RateLimitTracker, removeCliproxyAccount, removeSubscriptionAccount, renameSubscriptionAccount, sanitizeSubscriptionLabel, subscriptionAccountIdentity, subscriptionCredentialFingerprint } 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 { resolveLeaderboardConfig, startSwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
14
+ import { resolveCodexStartupModel, resolveLeaderboardConfig, startSwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
15
15
  import { accountKindForCliproxyAuthType, PROVIDERS, resolveAccountConnector } from "@velum-labs/routekit-registry";
16
16
  import { startRouter } from "@velum-labs/routekit-router";
17
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";
@@ -276,6 +276,7 @@ export async function startRouteKitDaemon(options) {
276
276
  let sidecarRef;
277
277
  let activeRouter;
278
278
  let accountActivity;
279
+ let accountAuth;
279
280
  let daemonTelemetry;
280
281
  let gatewayTelemetry;
281
282
  let record;
@@ -352,6 +353,18 @@ export async function startRouteKitDaemon(options) {
352
353
  accountActivity = new AccountActivityCoordinator({
353
354
  statePath: join(home, "usage", "account-activity.v1.json")
354
355
  });
356
+ mkdirSync(join(home, "subscriptions"), { recursive: true, mode: 0o700 });
357
+ accountAuth = new AccountAuthCoordinator({
358
+ statePath: join(home, "subscriptions", "account-auth.v1.json")
359
+ });
360
+ const activeCredentialFingerprints = () => new Map(accountStoreEntries(env).flatMap((entry) => entry.connector === "native"
361
+ ? [
362
+ [
363
+ subscriptionAccountIdentity(entry.subscriptionKind, entry.label),
364
+ subscriptionCredentialFingerprint(entry.path)
365
+ ]
366
+ ]
367
+ : []));
355
368
  const applyLeaderboardConfig = (config) => {
356
369
  leaderboardConfig = resolveLeaderboardConfig(config);
357
370
  callAttributions.configureBudget({
@@ -395,10 +408,12 @@ export async function startRouteKitDaemon(options) {
395
408
  env: routerEnv(),
396
409
  provenance,
397
410
  activity: accountActivity,
411
+ authHealth: accountAuth,
398
412
  drainGraceMs
399
413
  });
400
414
  await sidecar.reconcile(wantsCliproxySidecar(currentConfig));
401
415
  activeRouter = await startGeneration(currentConfig);
416
+ accountAuth.reconcileActiveCredentials(activeCredentialFingerprints());
402
417
  proxy = await startSwitchingGatewayProxy({
403
418
  target: activeRouter.url,
404
419
  host: options.host ?? "127.0.0.1",
@@ -471,6 +486,7 @@ export async function startRouteKitDaemon(options) {
471
486
  currentDocument = input.write ? readFileSync(configPath, "utf8") : nextDocument;
472
487
  revisions = nextRevisions;
473
488
  applyLeaderboardConfig(currentConfig);
489
+ accountAuth?.reconcileActiveCredentials(activeCredentialFingerprints());
474
490
  if (previousRouter !== undefined) {
475
491
  try {
476
492
  if (previousTarget !== undefined) {
@@ -625,7 +641,10 @@ export async function startRouteKitDaemon(options) {
625
641
  models,
626
642
  ...(currentConfig.defaultModel !== undefined
627
643
  ? { defaultModel: currentConfig.defaultModel }
628
- : {}),
644
+ : typeof body.default_model === "string" &&
645
+ models.some((model) => model.id === body.default_model)
646
+ ? { defaultModel: body.default_model }
647
+ : {}),
629
648
  revision: revisions.config
630
649
  };
631
650
  writeSnapshot(home, "catalog", "models", {
@@ -745,6 +764,9 @@ export async function startRouteKitDaemon(options) {
745
764
  label: entry.label,
746
765
  connector: entry.connector,
747
766
  credentialValid: member?.credentialValid ?? false,
767
+ ...(member?.upstreamAuthState !== undefined
768
+ ? { upstreamAuthState: member.upstreamAuthState }
769
+ : {}),
748
770
  configured: currentConfig.providers[entry.subscriptionKind] !== undefined,
749
771
  relayOpen: member?.relayReady === true &&
750
772
  currentConfig.providers[entry.subscriptionKind] !== undefined,
@@ -936,8 +958,16 @@ export async function startRouteKitDaemon(options) {
936
958
  const transaction = prepareAccountTransaction({
937
959
  home,
938
960
  configPath,
939
- accountPaths: prepared.map((entry) => entry.path),
940
- accountRoots: prepared.map((entry) => entry.directory),
961
+ accountPaths: [
962
+ ...prepared.map((entry) => entry.path),
963
+ ...(connector === "native"
964
+ ? [join(home, "subscriptions", "account-auth.v1.json")]
965
+ : [])
966
+ ],
967
+ accountRoots: [
968
+ ...prepared.map((entry) => entry.directory),
969
+ ...(connector === "native" ? [join(home, "subscriptions")] : [])
970
+ ],
941
971
  kind,
942
972
  provider,
943
973
  labels: prepared.map((entry) => entry.label)
@@ -957,6 +987,11 @@ export async function startRouteKitDaemon(options) {
957
987
  configRevision: true,
958
988
  accountRevision: true,
959
989
  beforeSwap: async () => {
990
+ if (connector === "native") {
991
+ for (const entry of prepared) {
992
+ accountAuth.activateFingerprint(subscriptionAccountIdentity(kind, entry.label), subscriptionCredentialFingerprint(entry.path));
993
+ }
994
+ }
960
995
  markAccountTransactionCommitted(transaction);
961
996
  if (connector === "cliproxy")
962
997
  await sidecar.refresh();
@@ -976,6 +1011,7 @@ export async function startRouteKitDaemon(options) {
976
1011
  const rollbackFailures = [];
977
1012
  try {
978
1013
  rollbackAccountTransaction(transaction, home);
1014
+ accountAuth?.reload();
979
1015
  }
980
1016
  catch (rollbackError) {
981
1017
  rollbackFailures.push(rollbackError);
@@ -1067,11 +1103,12 @@ export async function startRouteKitDaemon(options) {
1067
1103
  const nextDocument = disableProvider ? stringifyYaml(raw) : currentDocument;
1068
1104
  const nextConfig = disableProvider ? parseConfigDocument(nextDocument) : currentConfig;
1069
1105
  const activityPath = join(home, "usage", "account-activity.v1.json");
1106
+ const authPath = join(home, "subscriptions", "account-auth.v1.json");
1070
1107
  const transaction = prepareAccountTransaction({
1071
1108
  home,
1072
1109
  configPath,
1073
- accountPaths: [nativePath, activityPath],
1074
- accountRoots: [activeNativeDirectory, home],
1110
+ accountPaths: [nativePath, activityPath, authPath],
1111
+ accountRoots: [activeNativeDirectory, home, join(home, "subscriptions")],
1075
1112
  kind: nativeKind,
1076
1113
  provider: nativeKind,
1077
1114
  labels: [params.label]
@@ -1091,6 +1128,7 @@ export async function startRouteKitDaemon(options) {
1091
1128
  accountRevision: true,
1092
1129
  beforeSwap: () => {
1093
1130
  accountActivity.remove(subscriptionAccountIdentity(nativeKind, params.label));
1131
+ accountAuth.remove(subscriptionAccountIdentity(nativeKind, params.label));
1094
1132
  markAccountTransactionCommitted(transaction);
1095
1133
  }
1096
1134
  });
@@ -1106,6 +1144,7 @@ export async function startRouteKitDaemon(options) {
1106
1144
  try {
1107
1145
  rollbackAccountTransaction(transaction, home);
1108
1146
  accountActivity?.reload();
1147
+ accountAuth?.reload();
1109
1148
  }
1110
1149
  catch (rollbackError) {
1111
1150
  rollbackFailures.push(rollbackError);
@@ -1175,6 +1214,7 @@ export async function startRouteKitDaemon(options) {
1175
1214
  const targetPath = join(directory, `${params.target}.json`);
1176
1215
  const trackerPath = join(directory, ".state.json");
1177
1216
  const activityPath = join(home, "usage", "account-activity.v1.json");
1217
+ const authPath = join(home, "subscriptions", "account-auth.v1.json");
1178
1218
  if (!existsSync(sourcePath)) {
1179
1219
  throw new ControlError({
1180
1220
  code: "not_found",
@@ -1200,8 +1240,8 @@ export async function startRouteKitDaemon(options) {
1200
1240
  const transaction = prepareAccountTransaction({
1201
1241
  home,
1202
1242
  configPath,
1203
- accountPaths: [sourcePath, targetPath, trackerPath, activityPath],
1204
- accountRoots: [directory, home],
1243
+ accountPaths: [sourcePath, targetPath, trackerPath, activityPath, authPath],
1244
+ accountRoots: [directory, home, join(home, "subscriptions")],
1205
1245
  kind,
1206
1246
  provider: kind,
1207
1247
  labels: [params.source, params.target]
@@ -1217,6 +1257,7 @@ export async function startRouteKitDaemon(options) {
1217
1257
  accountRevision: true,
1218
1258
  beforeSwap: () => {
1219
1259
  accountActivity.rename(subscriptionAccountIdentity(kind, params.source), subscriptionAccountIdentity(kind, params.target));
1260
+ accountAuth.rename(subscriptionAccountIdentity(kind, params.source), subscriptionAccountIdentity(kind, params.target));
1220
1261
  markAccountTransactionCommitted(transaction);
1221
1262
  }
1222
1263
  });
@@ -1232,6 +1273,7 @@ export async function startRouteKitDaemon(options) {
1232
1273
  try {
1233
1274
  rollbackAccountTransaction(transaction, home);
1234
1275
  accountActivity?.reload();
1276
+ accountAuth?.reload();
1235
1277
  }
1236
1278
  catch (rollbackError) {
1237
1279
  rollbackFailures.push(rollbackError);
@@ -1447,10 +1489,59 @@ export async function startRouteKitDaemon(options) {
1447
1489
  },
1448
1490
  "launcher.prepare": async (params, context) => {
1449
1491
  const listed = await handlers["models.list"]({}, {
1450
- signal: new AbortController().signal,
1492
+ signal: context.signal,
1451
1493
  requestId: "internal"
1452
1494
  });
1453
- const model = params.model ?? listed.defaultModel ?? listed.models[0]?.id;
1495
+ let model = params.model ?? listed.defaultModel ?? listed.models[0]?.id;
1496
+ let codexSelection;
1497
+ if (params.tool === "codex") {
1498
+ const candidates = listed.models.flatMap((entry) => {
1499
+ const info = activeRouter.modelInfo(entry.id);
1500
+ if (info === undefined)
1501
+ return [];
1502
+ return [{
1503
+ id: info.id,
1504
+ nativeId: info.nativeModel,
1505
+ provider: info.provider,
1506
+ billingScope: info.billingMode,
1507
+ ...(info.createdAt !== undefined ? { createdAt: info.createdAt } : {}),
1508
+ ...(info.providerPriority !== undefined
1509
+ ? { providerPriority: info.providerPriority }
1510
+ : {}),
1511
+ ...(info.metadata?.architecture !== undefined
1512
+ ? { architecture: info.metadata.architecture }
1513
+ : {}),
1514
+ ...(info.metadata?.supportedParameters !== undefined
1515
+ ? { supportedParameters: info.metadata.supportedParameters }
1516
+ : {}),
1517
+ ...(info.reasoning !== null ? { reasoning: info.reasoning } : {})
1518
+ }];
1519
+ });
1520
+ try {
1521
+ const selected = await resolveCodexStartupModel({
1522
+ models: candidates,
1523
+ ...(listed.defaultModel !== undefined
1524
+ ? { preferredModel: listed.defaultModel }
1525
+ : {}),
1526
+ ...(params.model !== undefined ? { requestedModel: params.model } : {}),
1527
+ signal: context.signal
1528
+ });
1529
+ model = selected.model;
1530
+ codexSelection = {
1531
+ compatibleModelIds: [...selected.compatibleModelIds],
1532
+ models: [...selected.models]
1533
+ };
1534
+ }
1535
+ catch (error) {
1536
+ const message = error instanceof Error ? error.message : String(error);
1537
+ throw new ControlError({
1538
+ code: params.model !== undefined && message.startsWith("unknown model")
1539
+ ? "not_found"
1540
+ : "unavailable",
1541
+ message
1542
+ });
1543
+ }
1544
+ }
1454
1545
  if (model === undefined || !listed.models.some((entry) => entry.id === model)) {
1455
1546
  throw new ControlError({
1456
1547
  code: "not_found",
@@ -1464,7 +1555,8 @@ export async function startRouteKitDaemon(options) {
1464
1555
  model,
1465
1556
  gatewayUrl: dataUrl,
1466
1557
  authToken: dataTokenForPrincipal(tokens, dataTokenCache, dataAuth.token, context.principal),
1467
- env: {}
1558
+ env: {},
1559
+ ...(codexSelection !== undefined ? { codexSelection } : {})
1468
1560
  };
1469
1561
  },
1470
1562
  "tokens.issue": async (params, context) => {
@@ -1652,6 +1744,7 @@ export async function startRouteKitDaemon(options) {
1652
1744
  await proxy?.drain(drainGraceMs);
1653
1745
  await activeRouter?.close();
1654
1746
  accountActivity?.close();
1747
+ accountAuth?.close();
1655
1748
  await sidecar.close();
1656
1749
  await control?.close();
1657
1750
  if (portless?.enabled)
@@ -1691,6 +1784,7 @@ export async function startRouteKitDaemon(options) {
1691
1784
  await proxy?.close();
1692
1785
  await activeRouter?.close();
1693
1786
  accountActivity?.close();
1787
+ accountAuth?.close();
1694
1788
  await sidecarRef?.close();
1695
1789
  await control?.close();
1696
1790
  if (portless?.enabled)
@@ -10,19 +10,19 @@ import { ControlClient, ControlError, createServiceRecordStore } from "@velum-la
10
10
  import { parse as parseYaml } from "yaml";
11
11
  import { prepareAccountTransaction } from "../account-transaction.js";
12
12
  import { startRouteKitDaemon } from "../index.js";
13
- async function mockProvider() {
13
+ async function mockProvider(models = [
14
+ {
15
+ id: "mock-model",
16
+ object: "model",
17
+ capabilities: { streaming: "supported", tools: "degraded" },
18
+ supported_reasoning_levels: ["high"]
19
+ }
20
+ ]) {
14
21
  const server = createServer((req, res) => {
15
22
  if (req.url === "/v1/models") {
16
23
  res.setHeader("content-type", "application/json");
17
24
  res.end(JSON.stringify({
18
- data: [
19
- {
20
- id: "mock-model",
21
- object: "model",
22
- capabilities: { streaming: "supported", tools: "degraded" },
23
- supported_reasoning_levels: ["high"]
24
- }
25
- ]
25
+ data: models
26
26
  }));
27
27
  return;
28
28
  }
@@ -339,6 +339,94 @@ test("singleton daemon exposes authenticated control and a stable reloadable dat
339
339
  rmSync(root, { recursive: true, force: true });
340
340
  }
341
341
  });
342
+ test("local Codex preparation ranks an incompatible OpenAI default by native recency", async () => {
343
+ const root = mkdtempSync(join(tmpdir(), "routekit-daemon-codex-ranking-"));
344
+ const stateHome = join(root, "state");
345
+ const configPath = join(root, "router.yaml");
346
+ writeFileSync(configPath, "providers:\n openai: {}\ndefaultModel: openai/text-embedding-test\n");
347
+ const upstream = await mockProvider([
348
+ { id: "text-embedding-test", object: "model", created: 50 },
349
+ { id: "older-generation", object: "model", created: 100 },
350
+ { id: "newer-generation", object: "model", created: 200 }
351
+ ]);
352
+ const originalFetch = globalThis.fetch;
353
+ let openRouterFetches = 0;
354
+ globalThis.fetch = async (input, init) => {
355
+ const url = new URL(input instanceof Request ? input.url : input.toString());
356
+ if (url.hostname === "openrouter.ai") {
357
+ openRouterFetches += 1;
358
+ if (url.pathname === "/api/v1/models") {
359
+ return Response.json({
360
+ data: [
361
+ {
362
+ id: "openai/older-generation",
363
+ created: 900,
364
+ architecture: {
365
+ input_modalities: ["text"],
366
+ output_modalities: ["text"]
367
+ },
368
+ supported_parameters: ["tools"]
369
+ },
370
+ {
371
+ id: "openai/newer-generation",
372
+ created: 800,
373
+ architecture: {
374
+ input_modalities: ["text"],
375
+ output_modalities: ["text"]
376
+ },
377
+ supported_parameters: ["tools"]
378
+ }
379
+ ]
380
+ });
381
+ }
382
+ if (url.pathname === "/api/v1/embeddings/models") {
383
+ return Response.json({ data: [{ id: "openai/text-embedding-test" }] });
384
+ }
385
+ return Response.json({ data: [] });
386
+ }
387
+ return await originalFetch(input, init);
388
+ };
389
+ let daemon;
390
+ try {
391
+ daemon = await startRouteKitDaemon({
392
+ packageVersion: "1.2.3",
393
+ stateHome,
394
+ configPath,
395
+ port: 0,
396
+ portless: false,
397
+ env: {
398
+ ...process.env,
399
+ HOME: root,
400
+ ROUTEKIT_HOME: stateHome,
401
+ OPENAI_API_KEY: "test-key",
402
+ OPENAI_BASE_URL: upstream.url,
403
+ ROUTEKIT_PORTLESS: "0"
404
+ }
405
+ });
406
+ const client = new RouteKitControlClient({
407
+ url: daemon.record.url,
408
+ token: daemon.record.controlToken
409
+ });
410
+ const explicit = await client.call("launcher.prepare", {
411
+ tool: "codex",
412
+ model: "openai/older-generation"
413
+ });
414
+ assert.equal(explicit.model, "openai/older-generation");
415
+ assert.equal(openRouterFetches, 0);
416
+ const implicit = await client.call("launcher.prepare", { tool: "codex" });
417
+ assert.equal(implicit.model, "openai/newer-generation");
418
+ assert.equal(openRouterFetches, 4);
419
+ assert.equal(implicit.codexSelection?.models.find((model) => model.id === "openai/older-generation")?.createdAt, 100, "native OpenAI creation time wins over OpenRouter");
420
+ assert.equal(implicit.codexSelection?.models.find((model) => model.id === "openai/newer-generation")?.createdAt, 200);
421
+ }
422
+ finally {
423
+ globalThis.fetch = originalFetch;
424
+ if (daemon !== undefined)
425
+ await daemon.close();
426
+ await upstream.close();
427
+ rmSync(root, { recursive: true, force: true });
428
+ }
429
+ });
342
430
  test("daemon account activity persists last selection independently of leaderboard rollups", async () => {
343
431
  const root = mkdtempSync(join(tmpdir(), "routekit-daemon-activity-"));
344
432
  const stateHome = join(root, "state");
@@ -518,6 +606,12 @@ test("cleared persisted cooldown remains absent and eligible after daemon reload
518
606
  url: daemon.record.url,
519
607
  token: daemon.record.controlToken
520
608
  });
609
+ const prepared = await client.call("launcher.prepare", { tool: "codex" });
610
+ assert.equal(prepared.model, "codex/gpt-test-model");
611
+ assert.deepEqual(prepared.codexSelection?.compatibleModelIds, [
612
+ "codex/gpt-test-model"
613
+ ]);
614
+ assert.deepEqual(prepared.codexSelection?.models[0]?.architecture?.outputModalities, ["text"]);
521
615
  const before = await client.call("accounts.status", {});
522
616
  assert.equal(before.accounts[0]?.relayOpen, false);
523
617
  assert.deepEqual(before.accounts[0]?.readinessReasons, [
@@ -779,8 +873,8 @@ for (const kind of ["claude-code", "codex"]) {
779
873
  });
780
874
  assert.equal(doctor.checks.find((check) => check.name === "account/provider consistency")?.ok, true);
781
875
  await assert.rejects(client.call("launcher.prepare", { tool: "codex" }), (error) => error instanceof ControlError &&
782
- error.code === "not_found" &&
783
- /no model is available/.test(error.message));
876
+ error.code === "unavailable" &&
877
+ /no advertised model with text output and tool support/.test(error.message));
784
878
  assert.equal((await fetch(`${daemon.dataUrl}/health`)).status, 200);
785
879
  const dataToken = readFileSync(daemon.record.authTokenFile, "utf8").trim();
786
880
  const gatewayModels = await fetch(`${daemon.dataUrl}/v1/models`, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-daemon",
3
3
  "private": false,
4
- "version": "0.17.2",
4
+ "version": "0.17.4",
5
5
  "description": "Singleton RouteKit control daemon and stable model gateway.",
6
6
  "repository": {
7
7
  "type": "git",
@@ -35,14 +35,14 @@
35
35
  "dependencies": {
36
36
  "posthog-node": "5.46.1",
37
37
  "yaml": "2.9.0",
38
- "@velum-labs/routekit-accounts": "0.17.2",
39
- "@velum-labs/routekit-config": "0.17.2",
40
- "@velum-labs/routekit-control": "0.17.2",
41
- "@velum-labs/routekit-gateway": "0.17.2",
42
- "@velum-labs/routekit-registry": "0.17.2",
43
- "@velum-labs/routekit-router": "0.17.2",
44
- "@velum-labs/routekit-runtime": "0.17.2",
45
- "@velum-labs/routekit-telemetry-core": "0.17.2"
38
+ "@velum-labs/routekit-accounts": "0.17.4",
39
+ "@velum-labs/routekit-config": "0.17.4",
40
+ "@velum-labs/routekit-control": "0.17.4",
41
+ "@velum-labs/routekit-gateway": "0.17.4",
42
+ "@velum-labs/routekit-registry": "0.17.4",
43
+ "@velum-labs/routekit-router": "0.17.4",
44
+ "@velum-labs/routekit-runtime": "0.17.4",
45
+ "@velum-labs/routekit-telemetry-core": "0.17.4"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -b",