@juspay/neurolink 9.62.0 → 9.63.0

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.
@@ -73,6 +73,25 @@ export declare function handleCleanup(argv: AuthCommandArgs): Promise<void>;
73
73
  * Re-enables a previously disabled account so it can be used by the proxy pool again.
74
74
  */
75
75
  export declare function handleEnable(argv: AuthCommandArgs): Promise<void>;
76
+ /**
77
+ * Handle the set-primary subcommand
78
+ * `neurolink auth set-primary <email> [--config <path>]`
79
+ *
80
+ * Writes routing.primary-account to the proxy config YAML so the proxy
81
+ * tries this account first under fill-first/round-robin home semantics.
82
+ * Does not touch the token store.
83
+ */
84
+ export declare function handleSetPrimary(argv: AuthCommandArgs): Promise<void>;
85
+ /**
86
+ * Handle the get-primary subcommand
87
+ * `neurolink auth get-primary [--config <path>]`
88
+ */
89
+ export declare function handleGetPrimary(argv: AuthCommandArgs): Promise<void>;
90
+ /**
91
+ * Handle the clear-primary subcommand
92
+ * `neurolink auth clear-primary [--config <path>]`
93
+ */
94
+ export declare function handleClearPrimary(argv: AuthCommandArgs): Promise<void>;
76
95
  /**
77
96
  * Legacy main auth command handler
78
97
  * @deprecated Use subcommand handlers instead
@@ -739,6 +739,272 @@ export async function handleEnable(argv) {
739
739
  }
740
740
  }
741
741
  // =============================================================================
742
+ // PRIMARY ACCOUNT (proxy routing.primaryAccount in YAML)
743
+ // =============================================================================
744
+ const DEFAULT_PROXY_CONFIG_PATH = path.join(NEUROLINK_CONFIG_DIR, "proxy-config.yaml");
745
+ /** Lazy-load js-yaml. Returns undefined if unavailable; callers that need YAML
746
+ * output (rather than JSON fallback) should error with install guidance. */
747
+ async function tryLoadJsYaml() {
748
+ try {
749
+ return (await import(/* @vite-ignore */ "js-yaml"));
750
+ }
751
+ catch {
752
+ return undefined;
753
+ }
754
+ }
755
+ function isYamlPath(p) {
756
+ const lower = p.toLowerCase();
757
+ return lower.endsWith(".yaml") || lower.endsWith(".yml");
758
+ }
759
+ /** Read and parse a proxy config file. Returns an empty object when the file
760
+ * doesn't exist (caller can mutate and write). Detects YAML vs JSON by
761
+ * extension; falls back to JSON parsing if js-yaml is unavailable for a YAML
762
+ * file (errors loudly if neither parser can read it). */
763
+ async function readProxyConfigFile(filePath) {
764
+ const yamlExpected = isYamlPath(filePath);
765
+ let raw;
766
+ try {
767
+ raw = fs.readFileSync(filePath, "utf-8");
768
+ }
769
+ catch (err) {
770
+ const code = err.code;
771
+ if (code === "ENOENT") {
772
+ return {
773
+ data: {},
774
+ format: yamlExpected ? "yaml" : "json",
775
+ hadComments: false,
776
+ };
777
+ }
778
+ throw err;
779
+ }
780
+ const hadComments = /^\s*#/m.test(raw);
781
+ if (yamlExpected) {
782
+ const yaml = await tryLoadJsYaml();
783
+ if (yaml) {
784
+ const parsed = yaml.default?.load?.(raw) ?? yaml.load(raw);
785
+ return {
786
+ data: parsed && typeof parsed === "object"
787
+ ? parsed
788
+ : {},
789
+ format: "yaml",
790
+ hadComments,
791
+ };
792
+ }
793
+ // YAML expected but js-yaml absent — try JSON
794
+ try {
795
+ return { data: JSON.parse(raw), format: "json", hadComments };
796
+ }
797
+ catch {
798
+ throw new Error(`Cannot edit ${filePath}: js-yaml is not installed and the file is ` +
799
+ `not valid JSON. Install js-yaml (pnpm add -D js-yaml) or convert ` +
800
+ `the file to JSON.`);
801
+ }
802
+ }
803
+ return { data: JSON.parse(raw), format: "json", hadComments };
804
+ }
805
+ /** Serialize and write a proxy config file. */
806
+ async function writeProxyConfigFile(filePath, doc) {
807
+ let serialized;
808
+ if (doc.format === "yaml") {
809
+ const yaml = await tryLoadJsYaml();
810
+ if (!yaml) {
811
+ throw new Error(`Cannot write ${filePath} as YAML: js-yaml is not installed. ` +
812
+ `Install it (pnpm add -D js-yaml) or use a .json config path.`);
813
+ }
814
+ const dump = yaml.default?.dump ?? yaml.dump;
815
+ if (!dump) {
816
+ throw new Error(`Cannot write ${filePath} as YAML: js-yaml module does not expose ` +
817
+ `a dump function (unexpected version).`);
818
+ }
819
+ serialized = dump(doc.data, {
820
+ lineWidth: 100,
821
+ noRefs: true,
822
+ });
823
+ }
824
+ else {
825
+ serialized = `${JSON.stringify(doc.data, null, 2)}\n`;
826
+ }
827
+ const dir = path.dirname(filePath);
828
+ if (!fs.existsSync(dir)) {
829
+ fs.mkdirSync(dir, { recursive: true });
830
+ }
831
+ fs.writeFileSync(filePath, serialized, "utf-8");
832
+ }
833
+ function getRoutingObject(data) {
834
+ const routing = data.routing;
835
+ if (routing && typeof routing === "object" && !Array.isArray(routing)) {
836
+ return routing;
837
+ }
838
+ const fresh = {};
839
+ data.routing = fresh;
840
+ return fresh;
841
+ }
842
+ function readPrimaryFromRouting(routing) {
843
+ if (!routing) {
844
+ return undefined;
845
+ }
846
+ const kebab = routing["primary-account"];
847
+ if (typeof kebab === "string" && kebab.trim() !== "") {
848
+ return kebab.trim();
849
+ }
850
+ const camel = routing.primaryAccount;
851
+ if (typeof camel === "string" && camel.trim() !== "") {
852
+ return camel.trim();
853
+ }
854
+ return undefined;
855
+ }
856
+ /** Best-effort detection of a running proxy. Mirrors `proxy status` semantics
857
+ * without importing the proxy module. */
858
+ function detectRunningProxyPid() {
859
+ try {
860
+ const stateFile = path.join(NEUROLINK_CONFIG_DIR, "proxy-state.json");
861
+ if (!fs.existsSync(stateFile)) {
862
+ return undefined;
863
+ }
864
+ const parsed = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
865
+ if (!parsed.pid || typeof parsed.pid !== "number") {
866
+ return undefined;
867
+ }
868
+ process.kill(parsed.pid, 0);
869
+ return parsed.pid;
870
+ }
871
+ catch {
872
+ return undefined;
873
+ }
874
+ }
875
+ /**
876
+ * Handle the set-primary subcommand
877
+ * `neurolink auth set-primary <email> [--config <path>]`
878
+ *
879
+ * Writes routing.primary-account to the proxy config YAML so the proxy
880
+ * tries this account first under fill-first/round-robin home semantics.
881
+ * Does not touch the token store.
882
+ */
883
+ export async function handleSetPrimary(argv) {
884
+ const email = argv.email ?? (argv._ && argv._[2] ? String(argv._[2]) : undefined);
885
+ if (!email || email.trim() === "") {
886
+ logger.error(chalk.red("Missing required argument: <email>"));
887
+ logger.always(chalk.blue("\nUsage: neurolink auth set-primary <email> [--config <path>]\n" +
888
+ "Run 'neurolink auth list' to see authenticated accounts.\n"));
889
+ process.exit(1);
890
+ }
891
+ const trimmed = email.trim();
892
+ const filePath = argv.config ?? DEFAULT_PROXY_CONFIG_PATH;
893
+ try {
894
+ const doc = await readProxyConfigFile(filePath);
895
+ if (doc.hadComments) {
896
+ logger.always(chalk.yellow(`⚠ Note: existing YAML comments in ${filePath} will not be preserved.`));
897
+ }
898
+ const routing = getRoutingObject(doc.data);
899
+ delete routing.primaryAccount;
900
+ routing["primary-account"] = trimmed;
901
+ await writeProxyConfigFile(filePath, doc);
902
+ logger.always(chalk.green(`✓ Set primary account → ${trimmed}`));
903
+ logger.always(chalk.green(`✓ Saved to ${filePath}`));
904
+ // Token-store presence check (non-fatal)
905
+ const compoundKey = `anthropic:${trimmed}`;
906
+ const known = await defaultTokenStore.listByPrefix("anthropic:");
907
+ if (!known.includes(compoundKey)) {
908
+ logger.always("");
909
+ logger.always(chalk.yellow("⚠ This account is not currently authenticated. Run\n" +
910
+ " `neurolink auth login --add` to add it. The proxy will fall\n" +
911
+ " back to the first enabled account until then."));
912
+ }
913
+ // Restart hint
914
+ const pid = detectRunningProxyPid();
915
+ if (pid) {
916
+ logger.always("");
917
+ logger.always(chalk.yellow(`⚠ A proxy is currently running (PID ${pid}). Restart it to pick\n` +
918
+ " up the change: `neurolink proxy stop && neurolink proxy start`."));
919
+ }
920
+ }
921
+ catch (err) {
922
+ logger.error(chalk.red("Failed to set primary account:"));
923
+ logger.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
924
+ process.exit(1);
925
+ }
926
+ }
927
+ /**
928
+ * Handle the get-primary subcommand
929
+ * `neurolink auth get-primary [--config <path>]`
930
+ */
931
+ export async function handleGetPrimary(argv) {
932
+ const filePath = argv.config ?? DEFAULT_PROXY_CONFIG_PATH;
933
+ try {
934
+ if (!fs.existsSync(filePath)) {
935
+ logger.always(chalk.blue(`No proxy config file found at ${filePath}.`));
936
+ logger.always("Falling back to insertion-order index 0 (no primary configured).");
937
+ return;
938
+ }
939
+ const doc = await readProxyConfigFile(filePath);
940
+ const routing = typeof doc.data.routing === "object" && doc.data.routing
941
+ ? doc.data.routing
942
+ : undefined;
943
+ const primary = readPrimaryFromRouting(routing);
944
+ if (!primary) {
945
+ logger.always(chalk.blue(`No primary account configured. Falling back to insertion-order ` +
946
+ `index 0.`));
947
+ logger.always(`Source: ${filePath} (no \`routing.primaryAccount\` field)`);
948
+ return;
949
+ }
950
+ const compoundKey = `anthropic:${primary}`;
951
+ const known = await defaultTokenStore.listByPrefix("anthropic:");
952
+ const present = known.includes(compoundKey);
953
+ logger.always(chalk.bold(`Configured primary: ${primary}`));
954
+ logger.always(`Status: ${present
955
+ ? chalk.green(`authenticated (${compoundKey} present in token store)`)
956
+ : chalk.yellow(`not authenticated (token store has no ${compoundKey})`)}`);
957
+ logger.always(`Source: ${filePath}`);
958
+ }
959
+ catch (err) {
960
+ logger.error(chalk.red("Failed to read primary account:"));
961
+ logger.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
962
+ process.exit(1);
963
+ }
964
+ }
965
+ /**
966
+ * Handle the clear-primary subcommand
967
+ * `neurolink auth clear-primary [--config <path>]`
968
+ */
969
+ export async function handleClearPrimary(argv) {
970
+ const filePath = argv.config ?? DEFAULT_PROXY_CONFIG_PATH;
971
+ try {
972
+ if (!fs.existsSync(filePath)) {
973
+ logger.always(chalk.blue(`No proxy config file found at ${filePath}.`));
974
+ logger.always("Nothing to clear.");
975
+ return;
976
+ }
977
+ const doc = await readProxyConfigFile(filePath);
978
+ const routing = typeof doc.data.routing === "object" && doc.data.routing
979
+ ? doc.data.routing
980
+ : undefined;
981
+ const before = readPrimaryFromRouting(routing);
982
+ if (!before || !routing) {
983
+ logger.always(chalk.blue("No primary account was configured."));
984
+ return;
985
+ }
986
+ if (doc.hadComments) {
987
+ logger.always(chalk.yellow(`⚠ Note: existing YAML comments in ${filePath} will not be preserved.`));
988
+ }
989
+ delete routing.primaryAccount;
990
+ delete routing["primary-account"];
991
+ await writeProxyConfigFile(filePath, doc);
992
+ logger.always(chalk.green(`✓ Cleared primary account (was: ${before})`));
993
+ logger.always(chalk.green(`✓ Saved to ${filePath}`));
994
+ const pid = detectRunningProxyPid();
995
+ if (pid) {
996
+ logger.always("");
997
+ logger.always(chalk.yellow(`⚠ A proxy is currently running (PID ${pid}). Restart it to pick\n` +
998
+ " up the change: `neurolink proxy stop && neurolink proxy start`."));
999
+ }
1000
+ }
1001
+ catch (err) {
1002
+ logger.error(chalk.red("Failed to clear primary account:"));
1003
+ logger.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
1004
+ process.exit(1);
1005
+ }
1006
+ }
1007
+ // =============================================================================
742
1008
  // LEGACY HANDLER (for backward compatibility)
743
1009
  // =============================================================================
744
1010
  /**
@@ -66,6 +66,50 @@ function getProcessStatus(pid) {
66
66
  return "not_running";
67
67
  }
68
68
  }
69
+ /** Resolve the primary-account info shown in /status. Reads the operator's
70
+ * configured email from proxy config and cross-checks it against the token
71
+ * store; falls back to the first enabled anthropic account when not set or
72
+ * when the configured account isn't currently usable. */
73
+ async function resolveStatusPrimaryAccount(proxyConfig) {
74
+ const configured = proxyConfig?.routing?.primaryAccount?.trim() || null;
75
+ let enabledAnthropicKeys = [];
76
+ try {
77
+ const { tokenStore } = await import("../../lib/auth/tokenStore.js");
78
+ const all = await tokenStore.listByPrefix("anthropic:");
79
+ const filtered = [];
80
+ for (const key of all) {
81
+ const disabled = await tokenStore.isDisabled(key);
82
+ if (!disabled) {
83
+ filtered.push(key);
84
+ }
85
+ }
86
+ enabledAnthropicKeys = filtered;
87
+ }
88
+ catch (err) {
89
+ logger.debug(`[proxy] /status: failed to enumerate anthropic accounts: ${err instanceof Error ? err.message : String(err)}`);
90
+ }
91
+ if (configured) {
92
+ const configuredKey = `anthropic:${configured}`;
93
+ if (enabledAnthropicKeys.includes(configuredKey)) {
94
+ return {
95
+ configured,
96
+ key: configuredKey,
97
+ label: configured,
98
+ source: "configured",
99
+ };
100
+ }
101
+ }
102
+ const fallbackKey = enabledAnthropicKeys[0] ?? null;
103
+ const fallbackLabel = fallbackKey
104
+ ? (fallbackKey.split(":")[1] ?? null)
105
+ : null;
106
+ return {
107
+ configured,
108
+ key: fallbackKey,
109
+ label: fallbackLabel,
110
+ source: "fallback",
111
+ };
112
+ }
69
113
  /**
70
114
  * Check if the launchd service is loaded and actively managing the proxy.
71
115
  * Returns true if launchctl reports the service as running.
@@ -605,14 +649,42 @@ async function loadProxyStartConfiguration(argv, spinner) {
605
649
  passthroughModels: proxyConfig.routing.passthroughModels,
606
650
  });
607
651
  }
652
+ const primaryAccountKey = await resolveBootPrimaryAccountKey(proxyConfig?.routing?.primaryAccount);
608
653
  return {
609
654
  configPath,
610
655
  proxyConfig,
611
656
  strategy,
612
657
  modelRouter,
613
658
  passthrough: argv.passthrough ?? false,
659
+ primaryAccountKey,
614
660
  };
615
661
  }
662
+ /** Resolve the operator's configured primary email to a stable token-store
663
+ * key (anthropic:<email>). Cross-checks the token store and emits a one-time
664
+ * startup warning if the configured account isn't authenticated — but still
665
+ * returns the key so it activates automatically once the user runs
666
+ * `auth login --add`. */
667
+ async function resolveBootPrimaryAccountKey(primaryEmail) {
668
+ const trimmed = primaryEmail?.trim();
669
+ if (!trimmed) {
670
+ return undefined;
671
+ }
672
+ const key = `anthropic:${trimmed}`;
673
+ try {
674
+ const { tokenStore } = await import("../../lib/auth/tokenStore.js");
675
+ const known = await tokenStore.listByPrefix("anthropic:");
676
+ if (!known.includes(key)) {
677
+ logger.warn(`[proxy] WARN: configured routing.primaryAccount=${trimmed} not ` +
678
+ `found in token store; falling back to first enabled account. ` +
679
+ `Run \`neurolink auth login --add\` to authenticate it, or ` +
680
+ `\`neurolink auth clear-primary\` to remove the setting.`);
681
+ }
682
+ }
683
+ catch (err) {
684
+ logger.debug(`[proxy] could not validate primary account against token store: ${err instanceof Error ? err.message : String(err)}`);
685
+ }
686
+ return key;
687
+ }
616
688
  async function createProxyStartApp(params) {
617
689
  const { createClaudeProxyRoutes } = await import("../../lib/server/routes/claudeProxyRoutes.js");
618
690
  const { Hono } = await import("hono");
@@ -632,7 +704,7 @@ async function createProxyStartApp(params) {
632
704
  },
633
705
  }, 502);
634
706
  });
635
- const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough);
707
+ const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey);
636
708
  for (const route of routeGroup.routes) {
637
709
  const method = route.method.toLowerCase();
638
710
  app[method](route.path, async (c) => {
@@ -761,6 +833,7 @@ async function createProxyStartApp(params) {
761
833
  passthrough: params.passthrough,
762
834
  version: PROXY_VERSION,
763
835
  });
836
+ const primaryAccount = await resolveStatusPrimaryAccount(params.proxyConfig);
764
837
  return c.json({
765
838
  status: "running",
766
839
  ready: health.ready,
@@ -789,6 +862,7 @@ async function createProxyStartApp(params) {
789
862
  rateLimits: account.rateLimitCount,
790
863
  cooling: false, // No persistent cooldown — always active
791
864
  })),
865
+ primaryAccount,
792
866
  },
793
867
  config: params.proxyConfig
794
868
  ? { hasRouting: !!params.proxyConfig.routing }
@@ -1057,7 +1131,7 @@ async function startProxyCommandHandler(argv) {
1057
1131
  }
1058
1132
  const loadedEnvFile = await loadProxyStartEnv(argv, spinner);
1059
1133
  const { neurolink, cleanupLogs } = await createProxyNeurolinkRuntime(devPaths?.logsDir);
1060
- const { proxyConfig, strategy, modelRouter, passthrough } = await loadProxyStartConfiguration(argv, spinner);
1134
+ const { proxyConfig, strategy, modelRouter, passthrough, primaryAccountKey, } = await loadProxyStartConfiguration(argv, spinner);
1061
1135
  if (spinner) {
1062
1136
  spinner.text = "Configuring server...";
1063
1137
  }
@@ -1071,6 +1145,7 @@ async function startProxyCommandHandler(argv) {
1071
1145
  port,
1072
1146
  host,
1073
1147
  proxyConfig,
1148
+ primaryAccountKey,
1074
1149
  });
1075
1150
  await initializeProxyOpenTelemetry();
1076
1151
  if (spinner) {
@@ -56,6 +56,14 @@ export declare class AuthCommandFactory {
56
56
  * Build options for enable subcommand
57
57
  */
58
58
  private static buildEnableOptions;
59
+ /**
60
+ * Build options for set-primary subcommand
61
+ */
62
+ private static buildSetPrimaryOptions;
63
+ /**
64
+ * Build options for get-primary / clear-primary subcommands.
65
+ */
66
+ private static buildPrimaryConfigOption;
59
67
  /**
60
68
  * Auth provider choices for multi-provider commands
61
69
  */
@@ -63,6 +63,18 @@ export class AuthCommandFactory {
63
63
  .command("enable <account>", "Re-enable a previously disabled account", (yargs) => this.buildEnableOptions(yargs), async (argv) => {
64
64
  const { handleEnable } = await import("../commands/auth.js");
65
65
  await handleEnable(argv);
66
+ })
67
+ .command("set-primary <email>", "Set the proxy's primary (home) Anthropic account", (yargs) => this.buildSetPrimaryOptions(yargs), async (argv) => {
68
+ const { handleSetPrimary } = await import("../commands/auth.js");
69
+ await handleSetPrimary(argv);
70
+ })
71
+ .command("get-primary", "Show the proxy's currently configured primary account", (yargs) => this.buildPrimaryConfigOption(yargs), async (argv) => {
72
+ const { handleGetPrimary } = await import("../commands/auth.js");
73
+ await handleGetPrimary(argv);
74
+ })
75
+ .command("clear-primary", "Clear the proxy's configured primary account", (yargs) => this.buildPrimaryConfigOption(yargs), async (argv) => {
76
+ const { handleClearPrimary } = await import("../commands/auth.js");
77
+ await handleClearPrimary(argv);
66
78
  })
67
79
  .command("providers", "List available authentication providers", (yargs) => yargs.option("format", {
68
80
  type: "string",
@@ -110,6 +122,9 @@ export class AuthCommandFactory {
110
122
  .example("$0 auth cleanup", "Remove expired and disabled accounts")
111
123
  .example("$0 auth cleanup --force", "Remove stale accounts without confirmation")
112
124
  .example("$0 auth enable anthropic:1-VjRIq", "Re-enable a disabled account")
125
+ .example("$0 auth set-primary alice@example.com", "Make alice@example.com the proxy's primary (home) account")
126
+ .example("$0 auth get-primary", "Show the proxy's currently configured primary account")
127
+ .example("$0 auth clear-primary", "Remove the configured primary account (use insertion-order fallback)")
113
128
  .help();
114
129
  },
115
130
  handler: () => {
@@ -257,6 +272,32 @@ export class AuthCommandFactory {
257
272
  })
258
273
  .example("$0 auth enable anthropic:1-VjRIq", "Re-enable a disabled account");
259
274
  }
275
+ /**
276
+ * Build options for set-primary subcommand
277
+ */
278
+ static buildSetPrimaryOptions(yargs) {
279
+ return yargs
280
+ .positional("email", {
281
+ type: "string",
282
+ description: "Email/label of the Anthropic account to make primary (home)",
283
+ demandOption: true,
284
+ })
285
+ .option("config", {
286
+ type: "string",
287
+ description: "Path to the proxy config YAML (default: ~/.neurolink/proxy-config.yaml)",
288
+ })
289
+ .example("$0 auth set-primary alice@example.com", "Write routing.primary-account to the default proxy config")
290
+ .example("$0 auth set-primary alice@example.com --config ./proxy.yaml", "Use a non-default config path");
291
+ }
292
+ /**
293
+ * Build options for get-primary / clear-primary subcommands.
294
+ */
295
+ static buildPrimaryConfigOption(yargs) {
296
+ return yargs.option("config", {
297
+ type: "string",
298
+ description: "Path to the proxy config YAML (default: ~/.neurolink/proxy-config.yaml)",
299
+ });
300
+ }
260
301
  /**
261
302
  * Auth provider choices for multi-provider commands
262
303
  */
@@ -336,6 +336,20 @@ function parseRoutingConfig(raw) {
336
336
  if (Array.isArray(rawPassthrough)) {
337
337
  result.passthroughModels = rawPassthrough.map(String);
338
338
  }
339
+ // Primary account (accept kebab-case or camelCase). Email or label of the
340
+ // Anthropic account that should be tried first ("home"). Resolved to a
341
+ // stable key (anthropic:<email>) at proxy boot; absence preserves the
342
+ // pre-existing insertion-order behavior.
343
+ const rawPrimary = (raw["primary-account"] ?? raw.primaryAccount);
344
+ if (rawPrimary !== undefined) {
345
+ if (typeof rawPrimary === "string" && rawPrimary.trim() !== "") {
346
+ result.primaryAccount = rawPrimary.trim();
347
+ }
348
+ else {
349
+ logger.warn(`[proxy-config] Ignoring routing.primaryAccount: expected non-empty ` +
350
+ `string, got ${typeof rawPrimary}`);
351
+ }
352
+ }
339
353
  return result;
340
354
  }
341
355
  // ---------------------------------------------------------------------------
@@ -10,7 +10,18 @@
10
10
  * Without a router, models are passed through to the Anthropic provider.
11
11
  */
12
12
  import type { ModelRouter } from "../../proxy/modelRouter.js";
13
- import type { ParsedClaudeError, ParsedClaudeRequest, RouteGroup } from "../../types/index.js";
13
+ import type { ParsedClaudeError, ParsedClaudeRequest, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState } from "../../types/index.js";
14
+ /** Resolve the configured primary's stable key to its current index in the
15
+ * request's enabledAccounts list. Returns 0 (insertion-order fallback) when
16
+ * no key is configured or the key cannot be matched (account disabled/
17
+ * removed). The resolution is per-request because enabledAccounts membership
18
+ * can shift between requests. */
19
+ declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[]): number;
20
+ /** If the configured home primary is no longer cooling, reset
21
+ * primaryAccountIndex back to its index so traffic returns to the preferred
22
+ * account once its rate limit window expires. Called at the start of each
23
+ * request. Home is resolved fresh per call via resolveHomeIndex. */
24
+ declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[]): void;
14
25
  /**
15
26
  * Create Claude-compatible proxy routes.
16
27
  *
@@ -21,7 +32,7 @@ import type { ParsedClaudeError, ParsedClaudeRequest, RouteGroup } from "../../t
21
32
  * @param basePath - Base path prefix (default: "" since Claude API uses /v1/...).
22
33
  * @returns RouteGroup with Claude-compatible endpoints.
23
34
  */
24
- export declare function createClaudeProxyRoutes(modelRouter?: ModelRouter, basePath?: string, accountStrategy?: "round-robin" | "fill-first", passthroughMode?: boolean): RouteGroup;
35
+ export declare function createClaudeProxyRoutes(modelRouter?: ModelRouter, basePath?: string, accountStrategy?: "round-robin" | "fill-first", passthroughMode?: boolean, primaryAccountKey?: string): RouteGroup;
25
36
  export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
26
37
  /**
27
38
  * Parse a Claude error payload when available.
@@ -42,3 +53,14 @@ export declare function buildProxyFallbackOptions(parsed: ParsedClaudeRequest, o
42
53
  * carry transient HTML responses (e.g. 520 pages) inside `error.message`.
43
54
  */
44
55
  export declare function isTransientHttpFailure(status: number, errBody: string): boolean;
56
+ export declare const __testHooks: {
57
+ resolveHomeIndex: typeof resolveHomeIndex;
58
+ maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
59
+ setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
60
+ getConfiguredPrimaryAccountKey: () => string | undefined;
61
+ setPrimaryAccountIndex: (index: number) => void;
62
+ getPrimaryAccountIndex: () => number;
63
+ setAccountRuntimeState: (key: string, state: Partial<RuntimeAccountState>) => void;
64
+ resetAllRuntimeState: () => void;
65
+ };
66
+ export {};