routstrd 0.1.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.
@@ -0,0 +1,177 @@
1
+ import { randomBytes } from "crypto";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { readFile, writeFile } from "fs/promises";
4
+ import { dirname, join } from "path";
5
+ import type { RoutstrdConfig } from "../utils/config";
6
+ import { logger } from "../utils/logger";
7
+ import type { SdkStore } from "@routstr/sdk";
8
+
9
+ const OPENCLAW_CONFIG_PATH = join(process.env.HOME || "", ".openclaw/openclaw.json");
10
+ const OPENCLAW_PROVIDER_ID = "routstr";
11
+ const OPENCLAW_API_BASE = "http://localhost:8008/v1";
12
+ const OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
13
+ const OPENCLAW_DEFAULT_FALLBACK_MODEL = "routstr/kimi-k2.5";
14
+ const OPENCLAW_CLIENT_ID = "openclaw";
15
+ const OPENCLAW_NAME = "OpenClaw";
16
+
17
+ type RoutstrModel = {
18
+ id: string;
19
+ name?: string;
20
+ };
21
+
22
+ type OpenClawModelEntry = {
23
+ id: string;
24
+ name: string;
25
+ reasoning: boolean;
26
+ };
27
+
28
+ type OpenClawConfig = {
29
+ models?: {
30
+ providers?: Record<string, {
31
+ baseUrl?: string;
32
+ apiKey?: string;
33
+ api?: string;
34
+ models?: OpenClawModelEntry[];
35
+ }>;
36
+ };
37
+ agents?: {
38
+ defaults?: {
39
+ model?: {
40
+ primary?: string;
41
+ fallbacks?: string[];
42
+ };
43
+ models?: Record<string, {
44
+ alias?: string;
45
+ }>;
46
+ };
47
+ };
48
+ };
49
+
50
+ function toAlias(modelId: string): string {
51
+ if (modelId === "claude-sonnet-4.5") return "sonnet-4.5";
52
+ if (modelId === "claude-opus-4.5") return "opus-4.5";
53
+ if (modelId === "gemini-3-pro-preview") return "gemini-3-pro";
54
+ if (modelId === "gemini-3-flash-preview") return "gemini-3-flash";
55
+ if (modelId === "kimi-k2-thinking") return "kimi-k2";
56
+ if (modelId === "deepseek-v3.2-speciale") return "deepseek-special";
57
+ if (modelId === "grok-code-fast-1") return "grok-code";
58
+ return modelId;
59
+ }
60
+
61
+ function generateApiKey(): string {
62
+ const bytes = randomBytes(24);
63
+ return `sk-${bytes.toString("hex")}`;
64
+ }
65
+
66
+ export async function installOpenClawIntegration(
67
+ config: RoutstrdConfig,
68
+ store: SdkStore,
69
+ ): Promise<void> {
70
+ logger.log("\nInstalling routstr models in openclaw.json...");
71
+
72
+ const port = config.port || 8008;
73
+
74
+ // Get or create clientId entry for OpenClaw
75
+ const state = store.getState();
76
+ const existingClient = (state.clientIds || []).find(
77
+ (c: { clientId: string }) => c.clientId === OPENCLAW_CLIENT_ID,
78
+ );
79
+
80
+ let apiKey: string;
81
+ if (existingClient) {
82
+ apiKey = existingClient.apiKey;
83
+ logger.log(`Using existing API key for ${OPENCLAW_NAME}`);
84
+ } else {
85
+ apiKey = generateApiKey();
86
+ // Add new clientId entry using proper store action
87
+ store.getState().setClientIds((prev) => [
88
+ ...(prev || []),
89
+ {
90
+ clientId: OPENCLAW_CLIENT_ID,
91
+ name: OPENCLAW_NAME,
92
+ apiKey,
93
+ createdAt: Date.now(),
94
+ },
95
+ ]);
96
+ logger.log(`Created new API key for ${OPENCLAW_NAME}`);
97
+ }
98
+
99
+ let openclawConfig: OpenClawConfig = {};
100
+
101
+ try {
102
+ if (existsSync(OPENCLAW_CONFIG_PATH)) {
103
+ const content = await readFile(OPENCLAW_CONFIG_PATH, "utf-8");
104
+ openclawConfig = JSON.parse(content) as OpenClawConfig;
105
+ }
106
+ } catch {
107
+ openclawConfig = {};
108
+ }
109
+
110
+ if (!openclawConfig.models) {
111
+ openclawConfig.models = {};
112
+ }
113
+ if (!openclawConfig.models.providers) {
114
+ openclawConfig.models.providers = {};
115
+ }
116
+ if (!openclawConfig.agents) {
117
+ openclawConfig.agents = {};
118
+ }
119
+ if (!openclawConfig.agents.defaults) {
120
+ openclawConfig.agents.defaults = {};
121
+ }
122
+
123
+ try {
124
+ mkdirSync(dirname(OPENCLAW_CONFIG_PATH), { recursive: true });
125
+
126
+ const response = await fetch(`http://localhost:${port}/models`);
127
+ const data = await response.json() as { output?: { models: RoutstrModel[] } };
128
+ const models = data.output?.models || [];
129
+
130
+ if (models.length === 0) {
131
+ logger.log("No models found from routstr daemon.");
132
+ return;
133
+ }
134
+
135
+ const providerModels: OpenClawModelEntry[] = models.map((model) => ({
136
+ id: model.id,
137
+ name: model.name || model.id,
138
+ reasoning: true,
139
+ }));
140
+
141
+ openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
142
+ baseUrl: OPENCLAW_API_BASE,
143
+ apiKey,
144
+ api: "openai-completions",
145
+ models: providerModels,
146
+ };
147
+
148
+ const availableModelIds = new Set(providerModels.map((model) => model.id));
149
+ const primaryId = availableModelIds.has("gpt-5.3-codex") ? "gpt-5.3-codex" : providerModels[0]?.id;
150
+ const fallbackId = availableModelIds.has("minimax-m2.5")
151
+ ? "minimax-m2.5"
152
+ : providerModels.find((model) => model.id !== primaryId)?.id;
153
+
154
+ if (primaryId) {
155
+ openclawConfig.agents.defaults.model = {
156
+ primary: `${OPENCLAW_PROVIDER_ID}/${primaryId}`,
157
+ fallbacks: fallbackId ? [`${OPENCLAW_PROVIDER_ID}/${fallbackId}`] : [],
158
+ };
159
+ } else {
160
+ openclawConfig.agents.defaults.model = {
161
+ primary: OPENCLAW_DEFAULT_PRIMARY_MODEL,
162
+ fallbacks: [OPENCLAW_DEFAULT_FALLBACK_MODEL],
163
+ };
164
+ }
165
+
166
+ // const aliasMap: Record<string, { alias?: string }> = {};
167
+ // for (const model of providerModels) {
168
+ // aliasMap[`${OPENCLAW_PROVIDER_ID}/${model.id}`] = { alias: toAlias(model.id) };
169
+ // }
170
+ // openclawConfig.agents.defaults.models = aliasMap;
171
+
172
+ await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(openclawConfig, null, 2));
173
+ logger.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
174
+ } catch (error) {
175
+ logger.error("Failed to install models in openclaw.json:", error);
176
+ }
177
+ }
@@ -0,0 +1,120 @@
1
+ import { randomBytes } from "crypto";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { readFile, writeFile } from "fs/promises";
4
+ import { dirname, join } from "path";
5
+ import type { RoutstrdConfig } from "../utils/config";
6
+ import { logger } from "../utils/logger";
7
+ import type { SdkStore } from "@routstr/sdk";
8
+
9
+ const OPENCODE_CONFIG_PATH = join(process.env.HOME || "", ".config/opencode/opencode.json");
10
+ const OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
11
+ const OPENCODE_CLIENT_ID = "opencode";
12
+ const OPENCODE_NAME = "OpenCode";
13
+
14
+ type RoutstrModel = {
15
+ id: string;
16
+ name?: string;
17
+ };
18
+
19
+ function generateApiKey(): string {
20
+ const bytes = randomBytes(24);
21
+ return `sk-${bytes.toString("hex")}`;
22
+ }
23
+
24
+ export async function installOpencodeIntegration(
25
+ config: RoutstrdConfig,
26
+ store: SdkStore,
27
+ ): Promise<void> {
28
+ logger.log("\nInstalling routstr models in opencode.json...");
29
+
30
+ const port = config.port || 8008;
31
+
32
+ // Get or create clientId entry for OpenCode
33
+ const state = store.getState();
34
+ const existingClient = (state.clientIds || []).find(
35
+ (c: { clientId: string }) => c.clientId === OPENCODE_CLIENT_ID,
36
+ );
37
+
38
+ let apiKey: string;
39
+ if (existingClient) {
40
+ apiKey = existingClient.apiKey;
41
+ logger.log(`Using existing API key for ${OPENCODE_NAME}`);
42
+ } else {
43
+ apiKey = generateApiKey();
44
+ // Add new clientId entry using proper store action
45
+ store.getState().setClientIds((prev) => [
46
+ ...(prev || []),
47
+ {
48
+ clientId: OPENCODE_CLIENT_ID,
49
+ name: OPENCODE_NAME,
50
+ apiKey,
51
+ createdAt: Date.now(),
52
+ },
53
+ ]);
54
+ logger.log(`Created new API key for ${OPENCODE_NAME}`);
55
+ }
56
+
57
+ let opencodeConfig: {
58
+ provider?: Record<string, {
59
+ npm?: string;
60
+ name?: string;
61
+ options?: {
62
+ baseURL?: string;
63
+ apiKey?: string;
64
+ includeUsage?: boolean;
65
+ };
66
+ models?: Record<string, { name: string }>;
67
+ }>;
68
+ small_model?: string;
69
+ };
70
+
71
+ try {
72
+ if (existsSync(OPENCODE_CONFIG_PATH)) {
73
+ const content = await readFile(OPENCODE_CONFIG_PATH, "utf-8");
74
+ opencodeConfig = JSON.parse(content);
75
+ } else {
76
+ opencodeConfig = { provider: {} };
77
+ }
78
+ } catch {
79
+ opencodeConfig = { provider: {} };
80
+ }
81
+
82
+ if (!opencodeConfig.provider) {
83
+ opencodeConfig.provider = {};
84
+ }
85
+
86
+ try {
87
+ mkdirSync(dirname(OPENCODE_CONFIG_PATH), { recursive: true });
88
+
89
+ const response = await fetch(`http://localhost:${port}/models`);
90
+ const data = await response.json() as { output?: { models: RoutstrModel[] } };
91
+ const models = data.output?.models || [];
92
+
93
+ if (models.length === 0) {
94
+ logger.log("No models found from routstr daemon.");
95
+ return;
96
+ }
97
+
98
+ const modelsObj: Record<string, { name: string }> = {};
99
+ for (const model of models) {
100
+ modelsObj[model.id] = { name: model.name || model.id };
101
+ }
102
+
103
+ opencodeConfig.provider["routstr"] = {
104
+ npm: "@ai-sdk/openai-compatible",
105
+ name: "routstr",
106
+ options: {
107
+ baseURL: `http://localhost:${port}/`,
108
+ apiKey,
109
+ includeUsage: true,
110
+ },
111
+ models: modelsObj,
112
+ };
113
+ opencodeConfig.small_model = OPENCODE_SMALL_MODEL;
114
+
115
+ await writeFile(OPENCODE_CONFIG_PATH, JSON.stringify(opencodeConfig, null, 2));
116
+ logger.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
117
+ } catch (error) {
118
+ logger.error("Failed to install models in opencode.json:", error);
119
+ }
120
+ }
@@ -0,0 +1,116 @@
1
+ import { randomBytes } from "crypto";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { readFile, writeFile } from "fs/promises";
4
+ import { dirname, join } from "path";
5
+ import type { RoutstrdConfig } from "../utils/config";
6
+ import { logger } from "../utils/logger";
7
+ import type { SdkStore } from "@routstr/sdk";
8
+
9
+ const PI_CONFIG_PATH = join(process.env.HOME || "", ".pi/agent/models.json");
10
+ const PI_CLIENT_ID = "pi-agent";
11
+ const PI_NAME = "Pi Agent";
12
+
13
+ type RoutstrModel = {
14
+ id: string;
15
+ name?: string;
16
+ };
17
+
18
+ type PiModelEntry = {
19
+ id: string;
20
+ };
21
+
22
+ type PiProviderConfig = {
23
+ baseUrl?: string;
24
+ api?: string;
25
+ apiKey?: string;
26
+ models?: PiModelEntry[];
27
+ };
28
+
29
+ type PiConfig = {
30
+ providers?: Record<string, PiProviderConfig>;
31
+ };
32
+
33
+ function generateApiKey(): string {
34
+ const bytes = randomBytes(24);
35
+ return `sk-${bytes.toString("hex")}`;
36
+ }
37
+
38
+ export async function installPiIntegration(
39
+ config: RoutstrdConfig,
40
+ store: SdkStore,
41
+ ): Promise<void> {
42
+ logger.log("\nInstalling routstr models in pi models.json...");
43
+
44
+ const port = config.port || 8008;
45
+ const baseUrl = `http://localhost:${port}/v1`;
46
+
47
+ // Get or create clientId entry for Pi Agent
48
+ const state = store.getState();
49
+ const existingClient = (state.clientIds || []).find(
50
+ (c: { clientId: string }) => c.clientId === PI_CLIENT_ID,
51
+ );
52
+
53
+ let apiKey: string;
54
+ if (existingClient) {
55
+ apiKey = existingClient.apiKey;
56
+ logger.log(`Using existing API key for ${PI_NAME}`);
57
+ } else {
58
+ apiKey = generateApiKey();
59
+ // Add new clientId entry using proper store action
60
+ store.getState().setClientIds((prev) => [
61
+ ...(prev || []),
62
+ {
63
+ clientId: PI_CLIENT_ID,
64
+ name: PI_NAME,
65
+ apiKey,
66
+ createdAt: Date.now(),
67
+ },
68
+ ]);
69
+ logger.log(`Created new API key for ${PI_NAME}`);
70
+ }
71
+
72
+ let piConfig: PiConfig = {};
73
+
74
+ try {
75
+ if (existsSync(PI_CONFIG_PATH)) {
76
+ const content = await readFile(PI_CONFIG_PATH, "utf-8");
77
+ piConfig = JSON.parse(content) as PiConfig;
78
+ }
79
+ } catch {
80
+ piConfig = {};
81
+ }
82
+
83
+ if (!piConfig.providers) {
84
+ piConfig.providers = {};
85
+ }
86
+
87
+ try {
88
+ // Ensure directory exists
89
+ mkdirSync(dirname(PI_CONFIG_PATH), { recursive: true });
90
+
91
+ const response = await fetch(`http://localhost:${port}/models`);
92
+ const data = await response.json() as { output?: { models: RoutstrModel[] } };
93
+ const models = data.output?.models || [];
94
+
95
+ if (models.length === 0) {
96
+ logger.log("No models found from routstr daemon.");
97
+ return;
98
+ }
99
+
100
+ const providerModels: PiModelEntry[] = models.map((model) => ({
101
+ id: model.id,
102
+ }));
103
+
104
+ piConfig.providers["routstr"] = {
105
+ baseUrl,
106
+ api: "openai-completions",
107
+ apiKey,
108
+ models: providerModels,
109
+ };
110
+
111
+ await writeFile(PI_CONFIG_PATH, JSON.stringify(piConfig, null, 2));
112
+ logger.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
113
+ } catch (error) {
114
+ logger.error("Failed to install models in pi models.json:", error);
115
+ }
116
+ }
@@ -0,0 +1,90 @@
1
+ import { LOG_FILE } from "./utils/config";
2
+ import { logger } from "./utils/logger";
3
+ import { existsSync, mkdirSync } from "fs";
4
+ import { dirname, join } from "path";
5
+
6
+ export async function startDaemon(
7
+ options: { port?: string; provider?: string } = {},
8
+ ): Promise<void> {
9
+ const args: string[] = [];
10
+ const port = options.port || "8008";
11
+ const pollIntervalMs = 250;
12
+ const startupTimeoutMs = 10 * 60 * 1000;
13
+
14
+ try {
15
+ const controller = new AbortController();
16
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
17
+ const existing = await fetch(`http://localhost:${port}/health`, {
18
+ signal: controller.signal,
19
+ });
20
+ clearTimeout(timeoutId);
21
+ if (existing.ok) {
22
+ logger.log(`Routstr daemon already running on http://localhost:${port}`);
23
+ return;
24
+ }
25
+ } catch {
26
+ // Daemon is not running yet; continue with startup.
27
+ }
28
+
29
+ if (options.port) {
30
+ args.push("--port", options.port);
31
+ }
32
+ if (options.provider) {
33
+ args.push("--provider", options.provider);
34
+ }
35
+
36
+ // Ensure log directory exists
37
+ const logDir = dirname(LOG_FILE);
38
+ if (!existsSync(logDir)) {
39
+ mkdirSync(logDir, { recursive: true });
40
+ }
41
+
42
+ // Use shell redirection to append stdout/stderr to log file
43
+ // Bun.file() overwrites, so we need shell >> for appending
44
+ const daemonScript = `${import.meta.dir}/daemon/index.ts`;
45
+ const shellCmd = `bun run "${daemonScript}" ${args.map(a => `'${a}'`).join(" ")} >> "${LOG_FILE}" 2>&1`;
46
+
47
+ const proc = Bun.spawn(["sh", "-c", shellCmd], {
48
+ stdout: "inherit",
49
+ stderr: "inherit",
50
+ stdin: "ignore",
51
+ detached: true,
52
+ });
53
+
54
+ proc.unref();
55
+
56
+ let exitCode: number | null = null;
57
+ proc.exited.then((code) => {
58
+ exitCode = code;
59
+ });
60
+
61
+ const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs);
62
+ for (let i = 0; i < maxPolls; i++) {
63
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
64
+
65
+ if (exitCode !== null) {
66
+ throw new Error(
67
+ `Daemon process exited early with code ${exitCode}. Check logs at ${LOG_FILE}`,
68
+ );
69
+ }
70
+
71
+ try {
72
+ const controller = new AbortController();
73
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
74
+ const res = await fetch(`http://localhost:${port}/health`, {
75
+ signal: controller.signal,
76
+ });
77
+ clearTimeout(timeoutId);
78
+ if (res.ok) {
79
+ logger.log(`Routstr daemon started (PID: ${proc.pid}).`);
80
+ return;
81
+ }
82
+ } catch {
83
+ // Not ready yet
84
+ }
85
+ }
86
+
87
+ throw new Error(
88
+ `Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs at ${LOG_FILE}`,
89
+ );
90
+ }