routstrd 0.2.6 → 0.2.8

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.
@@ -3,44 +3,20 @@ import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
5
  import { logger } from "../utils/logger";
6
- import type { SdkStore } from "@routstr/sdk";
7
6
  import type { IntegrationConfig, RoutstrModel } from "./registry";
8
- import { generateApiKey } from "./registry";
7
+ import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
9
8
 
10
9
  export async function installClaudeCodeIntegration(
11
10
  config: RoutstrdConfig,
12
- store: SdkStore,
11
+ apiKey: string,
13
12
  integrationConfig: IntegrationConfig,
14
13
  ): Promise<void> {
15
- const { clientId, name, configPath } = integrationConfig;
14
+ const { name, configPath } = integrationConfig;
16
15
 
17
16
  logger.log(`\nInstalling routstr configuration in ${configPath}...`);
17
+ logger.log(`Using API key for ${name}`);
18
18
 
19
- const port = config.port || 8008;
20
-
21
- // Get or create clientId entry
22
- const state = store.getState();
23
- const existingClient = (state.clientIds || []).find(
24
- (c: { clientId: string }) => c.clientId === clientId,
25
- );
26
-
27
- let apiKey: string;
28
- if (existingClient) {
29
- apiKey = existingClient.apiKey;
30
- logger.log(`Using existing API key for ${name}`);
31
- } else {
32
- apiKey = generateApiKey();
33
- store.getState().setClientIds((prev) => [
34
- ...(prev || []),
35
- {
36
- clientId,
37
- name,
38
- apiKey,
39
- createdAt: Date.now(),
40
- },
41
- ]);
42
- logger.log(`Created new API key for ${name}`);
43
- }
19
+ const baseUrl = getDaemonBaseUrl(config);
44
20
 
45
21
  let settings: {
46
22
  env?: Record<string, string>;
@@ -60,23 +36,26 @@ export async function installClaudeCodeIntegration(
60
36
  }
61
37
 
62
38
  settings.env["ANTHROPIC_AUTH_TOKEN"] = apiKey;
63
- settings.env["ANTHROPIC_BASE_URL"] = `http://localhost:${port}`;
39
+ settings.env["ANTHROPIC_BASE_URL"] = baseUrl;
64
40
 
65
41
  try {
66
- const response = await fetch(`http://localhost:${port}/models`);
67
- const data = await response.json() as { output?: { models: RoutstrModel[] } };
68
- const models = data.output?.models || [];
42
+ const data = await callDaemon("/models");
43
+ const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
69
44
 
70
45
  if (models.length >= 3) {
71
- settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = models[0].id;
72
- settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = models[1].id;
73
- settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = models[2].id;
74
- logger.log(`Set Claude models: Opus=${models[0].id}, Sonnet=${models[1].id}, Haiku=${models[2].id}`);
46
+ const opus = models[0]!;
47
+ const sonnet = models[1]!;
48
+ const haiku = models[2]!;
49
+ settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = opus.id;
50
+ settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = sonnet.id;
51
+ settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = haiku.id;
52
+ logger.log(`Set Claude models: Opus=${opus.id}, Sonnet=${sonnet.id}, Haiku=${haiku.id}`);
75
53
  } else if (models.length > 0) {
54
+ const model = models[0]!;
76
55
  logger.log(`Only ${models.length} models available, falling back to defaults.`);
77
- settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = models[0].id;
78
- settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = models[0].id;
79
- settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = models[0].id;
56
+ settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = model.id;
57
+ settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = model.id;
58
+ settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = model.id;
80
59
  } else {
81
60
  logger.log("No models available from routstr daemon.");
82
61
  }
@@ -3,9 +3,8 @@ import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
5
  import { logger } from "../utils/logger";
6
- import type { SdkStore } from "@routstr/sdk";
7
6
  import type { IntegrationConfig, RoutstrModel } from "./registry";
8
- import { generateApiKey } from "./registry";
7
+ import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
9
8
 
10
9
  const OPENCLAW_PROVIDER_ID = "routstr";
11
10
  const OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
@@ -52,39 +51,15 @@ function toAlias(modelId: string): string {
52
51
 
53
52
  export async function installOpenClawIntegration(
54
53
  config: RoutstrdConfig,
55
- store: SdkStore,
54
+ apiKey: string,
56
55
  integrationConfig: IntegrationConfig,
57
56
  ): Promise<void> {
58
- const { clientId, name, configPath } = integrationConfig;
57
+ const { name, configPath } = integrationConfig;
59
58
 
60
59
  logger.log("\nInstalling routstr models in openclaw.json...");
60
+ logger.log(`Using API key for ${name}`);
61
61
 
62
- const port = config.port || 8008;
63
-
64
- // Get or create clientId entry for OpenClaw
65
- const state = store.getState();
66
- const existingClient = (state.clientIds || []).find(
67
- (c: { clientId: string }) => c.clientId === clientId,
68
- );
69
-
70
- let apiKey: string;
71
- if (existingClient) {
72
- apiKey = existingClient.apiKey;
73
- logger.log(`Using existing API key for ${name}`);
74
- } else {
75
- apiKey = generateApiKey();
76
- // Add new clientId entry using proper store action
77
- store.getState().setClientIds((prev) => [
78
- ...(prev || []),
79
- {
80
- clientId,
81
- name,
82
- apiKey,
83
- createdAt: Date.now(),
84
- },
85
- ]);
86
- logger.log(`Created new API key for ${name}`);
87
- }
62
+ const baseUrl = getDaemonBaseUrl(config);
88
63
 
89
64
  let openclawConfig: OpenClawConfig = {};
90
65
 
@@ -113,9 +88,8 @@ export async function installOpenClawIntegration(
113
88
  try {
114
89
  mkdirSync(dirname(configPath), { recursive: true });
115
90
 
116
- const response = await fetch(`http://localhost:${port}/models`);
117
- const data = await response.json() as { output?: { models: RoutstrModel[] } };
118
- const models = data.output?.models || [];
91
+ const data = await callDaemon("/models");
92
+ const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
119
93
 
120
94
  if (models.length === 0) {
121
95
  logger.log("No models found from routstr daemon.");
@@ -129,7 +103,7 @@ export async function installOpenClawIntegration(
129
103
  }));
130
104
 
131
105
  openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
132
- baseUrl: `http://localhost:${port}/v1`,
106
+ baseUrl: `${baseUrl}/v1`,
133
107
  apiKey,
134
108
  api: "openai-completions",
135
109
  models: providerModels,
@@ -3,47 +3,22 @@ import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
5
  import { logger } from "../utils/logger";
6
- import type { SdkStore } from "@routstr/sdk";
7
6
  import type { IntegrationConfig, RoutstrModel } from "./registry";
8
- import { generateApiKey } from "./registry";
7
+ import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
9
8
 
10
9
  const OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
11
10
 
12
11
  export async function installOpencodeIntegration(
13
12
  config: RoutstrdConfig,
14
- store: SdkStore,
13
+ apiKey: string,
15
14
  integrationConfig: IntegrationConfig,
16
15
  ): Promise<void> {
17
- const { clientId, name, configPath } = integrationConfig;
16
+ const { name, configPath } = integrationConfig;
18
17
 
19
18
  logger.log("\nInstalling routstr models in opencode.json...");
19
+ logger.log(`Using API key for ${name}`);
20
20
 
21
- const port = config.port || 8008;
22
-
23
- // Get or create clientId entry for OpenCode
24
- const state = store.getState();
25
- const existingClient = (state.clientIds || []).find(
26
- (c: { clientId: string }) => c.clientId === clientId,
27
- );
28
-
29
- let apiKey: string;
30
- if (existingClient) {
31
- apiKey = existingClient.apiKey;
32
- logger.log(`Using existing API key for ${name}`);
33
- } else {
34
- apiKey = generateApiKey();
35
- // Add new clientId entry using proper store action
36
- store.getState().setClientIds((prev) => [
37
- ...(prev || []),
38
- {
39
- clientId,
40
- name,
41
- apiKey,
42
- createdAt: Date.now(),
43
- },
44
- ]);
45
- logger.log(`Created new API key for ${name}`);
46
- }
21
+ const baseUrl = getDaemonBaseUrl(config);
47
22
 
48
23
  let opencodeConfig: {
49
24
  provider?: Record<string, {
@@ -77,9 +52,8 @@ export async function installOpencodeIntegration(
77
52
  try {
78
53
  mkdirSync(dirname(configPath), { recursive: true });
79
54
 
80
- const response = await fetch(`http://localhost:${port}/models`);
81
- const data = await response.json() as { output?: { models: RoutstrModel[] } };
82
- const models = data.output?.models || [];
55
+ const data = await callDaemon("/models");
56
+ const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
83
57
 
84
58
  if (models.length === 0) {
85
59
  logger.log("No models found from routstr daemon.");
@@ -95,7 +69,7 @@ export async function installOpencodeIntegration(
95
69
  npm: "@ai-sdk/openai-compatible",
96
70
  name: "routstr",
97
71
  options: {
98
- baseURL: `http://localhost:${port}/`,
72
+ baseURL: `${baseUrl}/`,
99
73
  apiKey,
100
74
  includeUsage: true,
101
75
  },
@@ -3,9 +3,8 @@ import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
5
  import { logger } from "../utils/logger";
6
- import type { SdkStore } from "@routstr/sdk";
7
6
  import type { IntegrationConfig, RoutstrModel } from "./registry";
8
- import { generateApiKey } from "./registry";
7
+ import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
9
8
 
10
9
  type PiModelEntry = {
11
10
  id: string;
@@ -24,40 +23,15 @@ type PiConfig = {
24
23
 
25
24
  export async function installPiIntegration(
26
25
  config: RoutstrdConfig,
27
- store: SdkStore,
26
+ apiKey: string,
28
27
  integrationConfig: IntegrationConfig,
29
28
  ): Promise<void> {
30
- const { clientId, name, configPath } = integrationConfig;
29
+ const { name, configPath } = integrationConfig;
31
30
 
32
31
  logger.log("\nInstalling routstr models in pi models.json...");
32
+ logger.log(`Using API key for ${name}`);
33
33
 
34
- const port = config.port || 8008;
35
- const baseUrl = `http://localhost:${port}/v1`;
36
-
37
- // Get or create clientId entry for Pi Agent
38
- const state = store.getState();
39
- const existingClient = (state.clientIds || []).find(
40
- (c: { clientId: string }) => c.clientId === clientId,
41
- );
42
-
43
- let apiKey: string;
44
- if (existingClient) {
45
- apiKey = existingClient.apiKey;
46
- logger.log(`Using existing API key for ${name}`);
47
- } else {
48
- apiKey = generateApiKey();
49
- // Add new clientId entry using proper store action
50
- store.getState().setClientIds((prev) => [
51
- ...(prev || []),
52
- {
53
- clientId,
54
- name,
55
- apiKey,
56
- createdAt: Date.now(),
57
- },
58
- ]);
59
- logger.log(`Created new API key for ${name}`);
60
- }
34
+ const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
61
35
 
62
36
  let piConfig: PiConfig = {};
63
37
 
@@ -78,9 +52,8 @@ export async function installPiIntegration(
78
52
  // Ensure directory exists
79
53
  mkdirSync(dirname(configPath), { recursive: true });
80
54
 
81
- const response = await fetch(`http://localhost:${port}/models`);
82
- const data = await response.json() as { output?: { models: RoutstrModel[] } };
83
- const models = data.output?.models || [];
55
+ const data = await callDaemon("/models");
56
+ const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
84
57
 
85
58
  if (models.length === 0) {
86
59
  logger.log("No models found from routstr daemon.");
@@ -1,7 +1,5 @@
1
- import { randomBytes } from "crypto";
2
1
  import { join } from "path";
3
2
  import type { RoutstrdConfig } from "../utils/config";
4
- import type { SdkStore } from "@routstr/sdk";
5
3
  import { installOpencodeIntegration } from "./opencode";
6
4
  import { installPiIntegration } from "./pi";
7
5
  import { installOpenClawIntegration } from "./openclaw";
@@ -18,14 +16,9 @@ export type RoutstrModel = {
18
16
  name?: string;
19
17
  };
20
18
 
21
- export function generateApiKey(): string {
22
- const bytes = randomBytes(24);
23
- return `sk-${bytes.toString("hex")}`;
24
- }
25
-
26
19
  export type IntegrationFn = (
27
20
  config: RoutstrdConfig,
28
- store: SdkStore,
21
+ apiKey: string,
29
22
  integrationConfig: IntegrationConfig,
30
23
  ) => Promise<void>;
31
24
 
@@ -60,16 +53,15 @@ export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
60
53
  };
61
54
 
62
55
  export async function runIntegrationsForClients(
63
- clientIds: Array<{ clientId: string }>,
56
+ clientIds: Array<{ clientId: string; apiKey?: string }>,
64
57
  config: RoutstrdConfig,
65
- store: SdkStore,
66
58
  ): Promise<void> {
67
59
  for (const client of clientIds) {
68
60
  const integrationFn = CLIENT_INTEGRATIONS[client.clientId];
69
61
  const integrationConfig = CLIENT_CONFIGS[client.clientId];
70
- if (integrationFn && integrationConfig) {
62
+ if (integrationFn && integrationConfig && client.apiKey) {
71
63
  try {
72
- await integrationFn(config, store, integrationConfig);
64
+ await integrationFn(config, client.apiKey, integrationConfig);
73
65
  } catch (error) {
74
66
  console.error(`Integration failed for ${client.clientId}:`, error);
75
67
  }
@@ -1,5 +1,5 @@
1
1
  import type { UsageTrackingEntry } from "../../daemon/types.ts";
2
- import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
2
+ import { callDaemon, getNpubSuffix, isDaemonRunning, loadConfig } from "../../utils/daemon-client.ts";
3
3
  import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts";
4
4
 
5
5
  export interface BalanceKey {
@@ -82,12 +82,24 @@ export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
82
82
  const result = await callDaemon(`/usage?limit=${limit}`);
83
83
  if (result.error) return null;
84
84
 
85
- // The daemon returns { output: [...] } where output is the entries array directly
85
+ // The daemon returns { output: [...] } where output is the entries array directly.
86
+ // In remote daemon mode, client IDs are suffixed with the last 7 chars of our npub.
87
+ // Fetch the full daemon result, but only display/aggregate entries for our clients.
86
88
  const entries = result.output as UsageTrackingEntry[] | undefined;
87
89
  const entriesArray = Array.isArray(entries) ? entries : [];
88
-
89
- // Calculate totals from entries
90
- const totals = entriesArray.reduce(
90
+ const suffix = getNpubSuffix(await loadConfig());
91
+ const suffixStr = suffix ? `-${suffix}` : null;
92
+ const visibleEntries = suffixStr
93
+ ? entriesArray
94
+ .filter((entry) => entry.client?.endsWith(suffixStr))
95
+ .map((entry) => ({
96
+ ...entry,
97
+ client: entry.client?.slice(0, -suffixStr.length),
98
+ }))
99
+ : entriesArray;
100
+
101
+ // Calculate totals from visible entries
102
+ const totals = visibleEntries.reduce(
91
103
  (acc, entry) => ({
92
104
  promptTokens: acc.promptTokens + entry.promptTokens,
93
105
  completionTokens: acc.completionTokens + entry.completionTokens,
@@ -98,8 +110,8 @@ export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
98
110
  );
99
111
 
100
112
  return {
101
- entries: entriesArray,
102
- totalEntries: entriesArray.length,
113
+ entries: visibleEntries,
114
+ totalEntries: visibleEntries.length,
103
115
  totalSatsCost: totals.satsCost,
104
116
  recentSatsCost: totals.satsCost, // For now, recent = total since we don't have time window
105
117
  limit,