@openpalm/lib 0.9.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/README.md +83 -0
- package/package.json +30 -0
- package/src/control-plane/audit.ts +40 -0
- package/src/control-plane/channels.ts +196 -0
- package/src/control-plane/connection-mapping.ts +191 -0
- package/src/control-plane/connection-migration-flags.ts +40 -0
- package/src/control-plane/connection-profiles.ts +317 -0
- package/src/control-plane/core-asset-provider.ts +20 -0
- package/src/control-plane/core-assets.ts +292 -0
- package/src/control-plane/docker.ts +448 -0
- package/src/control-plane/env.ts +70 -0
- package/src/control-plane/fs-asset-provider.ts +61 -0
- package/src/control-plane/fs-registry-provider.ts +46 -0
- package/src/control-plane/lifecycle.ts +373 -0
- package/src/control-plane/memory-config.ts +424 -0
- package/src/control-plane/model-runner.ts +101 -0
- package/src/control-plane/paths.ts +77 -0
- package/src/control-plane/registry-provider.ts +19 -0
- package/src/control-plane/scheduler.ts +498 -0
- package/src/control-plane/secrets.ts +177 -0
- package/src/control-plane/setup-status.ts +31 -0
- package/src/control-plane/setup.test.ts +476 -0
- package/src/control-plane/setup.ts +474 -0
- package/src/control-plane/staging.ts +376 -0
- package/src/control-plane/types.ts +165 -0
- package/src/index.ts +295 -0
- package/src/logger.ts +14 -0
- package/src/provider-constants.ts +106 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared setup orchestration for the OpenPalm control plane.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the reusable core logic from the admin's POST /admin/setup handler
|
|
5
|
+
* so that both the CLI setup wizard and the admin UI can call `performSetup()`.
|
|
6
|
+
*
|
|
7
|
+
* This module does NOT include Docker operations (compose up, image pull, etc.)
|
|
8
|
+
* — those happen separately in the caller after setup completes.
|
|
9
|
+
*/
|
|
10
|
+
import { createLogger } from "../logger.js";
|
|
11
|
+
import {
|
|
12
|
+
PROVIDER_KEY_MAP,
|
|
13
|
+
EMBEDDING_DIMS,
|
|
14
|
+
OLLAMA_INSTACK_URL,
|
|
15
|
+
} from "../provider-constants.js";
|
|
16
|
+
import { ensureXdgDirs } from "./paths.js";
|
|
17
|
+
import {
|
|
18
|
+
ensureSecrets,
|
|
19
|
+
updateSecretsEnv,
|
|
20
|
+
ensureOpenCodeConfig,
|
|
21
|
+
} from "./secrets.js";
|
|
22
|
+
import { ensureConnectionProfilesStore, writeConnectionsDocument } from "./connection-profiles.js";
|
|
23
|
+
import { buildMem0Mapping } from "./connection-mapping.js";
|
|
24
|
+
import { writeMemoryConfig } from "./memory-config.js";
|
|
25
|
+
import { ensureOpenCodeSystemConfig, ensureAdminOpenCodeConfig, ensureMemoryDir } from "./core-assets.js";
|
|
26
|
+
import { applyInstall, createState, writeSetupTokenFile } from "./lifecycle.js";
|
|
27
|
+
import { detectLocalProviders } from "./model-runner.js";
|
|
28
|
+
import type { LocalProviderDetection } from "./model-runner.js";
|
|
29
|
+
import type { CoreAssetProvider } from "./core-asset-provider.js";
|
|
30
|
+
import type { ControlPlaneState, CapabilityAssignments } from "./types.js";
|
|
31
|
+
|
|
32
|
+
const logger = createLogger("setup");
|
|
33
|
+
|
|
34
|
+
// ── Types ────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export type SetupConnection = {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
provider: string;
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
apiKey: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SetupAssignments = {
|
|
45
|
+
llm: { connectionId: string; model: string; smallModel?: string };
|
|
46
|
+
embeddings: { connectionId: string; model: string; embeddingDims?: number };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type SetupInput = {
|
|
50
|
+
adminToken: string;
|
|
51
|
+
ownerName?: string;
|
|
52
|
+
ownerEmail?: string;
|
|
53
|
+
memoryUserId: string;
|
|
54
|
+
ollamaEnabled: boolean;
|
|
55
|
+
connections: SetupConnection[];
|
|
56
|
+
assignments: SetupAssignments;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type SetupResult = {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
/** Services that should be started after setup. */
|
|
63
|
+
started?: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type DetectedProvider = {
|
|
67
|
+
provider: string;
|
|
68
|
+
url: string;
|
|
69
|
+
available: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ── Validation ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** Safe env var key pattern: uppercase alphanumeric + underscores, starting with a letter. */
|
|
75
|
+
const SAFE_ENV_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
76
|
+
|
|
77
|
+
/** Valid connection ID pattern: starts with letter or digit, allows A-Z, a-z, 0-9, _, -. */
|
|
78
|
+
const CONNECTION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
79
|
+
|
|
80
|
+
/** Providers that are valid for setup wizard connections. */
|
|
81
|
+
const WIZARD_PROVIDERS = new Set([
|
|
82
|
+
"openai", "anthropic", "ollama", "groq", "together",
|
|
83
|
+
"mistral", "deepseek", "xai", "lmstudio", "model-runner",
|
|
84
|
+
"ollama-instack",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
export function validateSetupInput(input: unknown): { valid: boolean; errors: string[] } {
|
|
88
|
+
const errors: string[] = [];
|
|
89
|
+
|
|
90
|
+
if (typeof input !== "object" || input === null) {
|
|
91
|
+
return { valid: false, errors: ["Input must be a non-null object"] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const body = input as Record<string, unknown>;
|
|
95
|
+
|
|
96
|
+
// adminToken
|
|
97
|
+
if (typeof body.adminToken !== "string" || !body.adminToken) {
|
|
98
|
+
errors.push("adminToken is required and must be a non-empty string");
|
|
99
|
+
} else if (body.adminToken.length < 8) {
|
|
100
|
+
errors.push("adminToken must be at least 8 characters");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ownerName and ownerEmail are optional strings
|
|
104
|
+
if (body.ownerName !== undefined && typeof body.ownerName !== "string") {
|
|
105
|
+
errors.push("ownerName must be a string if provided");
|
|
106
|
+
}
|
|
107
|
+
if (body.ownerEmail !== undefined && typeof body.ownerEmail !== "string") {
|
|
108
|
+
errors.push("ownerEmail must be a string if provided");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// memoryUserId
|
|
112
|
+
if (body.memoryUserId !== undefined && typeof body.memoryUserId !== "string") {
|
|
113
|
+
errors.push("memoryUserId must be a string");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ollamaEnabled
|
|
117
|
+
if (body.ollamaEnabled !== undefined && typeof body.ollamaEnabled !== "boolean") {
|
|
118
|
+
errors.push("ollamaEnabled must be a boolean");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// connections
|
|
122
|
+
if (!Array.isArray(body.connections) || body.connections.length === 0) {
|
|
123
|
+
errors.push("connections array is required and must be non-empty");
|
|
124
|
+
} else {
|
|
125
|
+
const seenIds = new Set<string>();
|
|
126
|
+
for (let i = 0; i < body.connections.length; i++) {
|
|
127
|
+
const c = body.connections[i];
|
|
128
|
+
if (typeof c !== "object" || c === null) {
|
|
129
|
+
errors.push(`connections[${i}] must be an object`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const conn = c as Record<string, unknown>;
|
|
133
|
+
const id = typeof conn.id === "string" ? conn.id.trim() : "";
|
|
134
|
+
const provider = typeof conn.provider === "string" ? conn.provider.trim() : "";
|
|
135
|
+
|
|
136
|
+
if (!id) {
|
|
137
|
+
errors.push(`connections[${i}].id is required`);
|
|
138
|
+
} else if (!CONNECTION_ID_RE.test(id)) {
|
|
139
|
+
errors.push(`connections[${i}].id must start with a letter or digit (allowed: A-Z, a-z, 0-9, _, -)`);
|
|
140
|
+
} else if (seenIds.has(id)) {
|
|
141
|
+
errors.push(`Duplicate connection ID: ${id}`);
|
|
142
|
+
} else {
|
|
143
|
+
seenIds.add(id);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const name = typeof conn.name === "string" ? conn.name.trim() : "";
|
|
147
|
+
if (!name) {
|
|
148
|
+
errors.push(`connections[${i}].name is required`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!provider) {
|
|
152
|
+
errors.push(`connections[${i}].provider is required`);
|
|
153
|
+
} else if (!WIZARD_PROVIDERS.has(provider)) {
|
|
154
|
+
errors.push(`connections[${i}].provider "${provider}" is outside wizard scope`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// assignments
|
|
160
|
+
if (typeof body.assignments !== "object" || body.assignments === null) {
|
|
161
|
+
errors.push("assignments object is required");
|
|
162
|
+
} else {
|
|
163
|
+
const assignments = body.assignments as Record<string, unknown>;
|
|
164
|
+
const llm = assignments.llm;
|
|
165
|
+
const embeddings = assignments.embeddings;
|
|
166
|
+
|
|
167
|
+
if (typeof llm !== "object" || llm === null) {
|
|
168
|
+
errors.push("assignments.llm is required");
|
|
169
|
+
} else {
|
|
170
|
+
const llmObj = llm as Record<string, unknown>;
|
|
171
|
+
if (!llmObj.connectionId || typeof llmObj.connectionId !== "string") {
|
|
172
|
+
errors.push("assignments.llm.connectionId is required");
|
|
173
|
+
}
|
|
174
|
+
if (!llmObj.model || typeof llmObj.model !== "string") {
|
|
175
|
+
errors.push("assignments.llm.model is required");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof embeddings !== "object" || embeddings === null) {
|
|
180
|
+
errors.push("assignments.embeddings is required");
|
|
181
|
+
} else {
|
|
182
|
+
const embObj = embeddings as Record<string, unknown>;
|
|
183
|
+
if (!embObj.connectionId || typeof embObj.connectionId !== "string") {
|
|
184
|
+
errors.push("assignments.embeddings.connectionId is required");
|
|
185
|
+
}
|
|
186
|
+
if (!embObj.model || typeof embObj.model !== "string") {
|
|
187
|
+
errors.push("assignments.embeddings.model is required");
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
embObj.embeddingDims !== undefined &&
|
|
191
|
+
(typeof embObj.embeddingDims !== "number" ||
|
|
192
|
+
!Number.isInteger(embObj.embeddingDims) ||
|
|
193
|
+
embObj.embeddingDims < 1)
|
|
194
|
+
) {
|
|
195
|
+
errors.push("assignments.embeddings.embeddingDims must be a positive integer");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Cross-validate: assignment connectionIds must reference a connection
|
|
200
|
+
if (Array.isArray(body.connections) && errors.length === 0) {
|
|
201
|
+
const connectionIds = new Set(
|
|
202
|
+
(body.connections as Array<Record<string, unknown>>).map(
|
|
203
|
+
(c) => typeof c.id === "string" ? c.id.trim() : ""
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
const llmConnId =
|
|
207
|
+
typeof (llm as Record<string, unknown>)?.connectionId === "string"
|
|
208
|
+
? ((llm as Record<string, unknown>).connectionId as string)
|
|
209
|
+
: "";
|
|
210
|
+
const embConnId =
|
|
211
|
+
typeof (embeddings as Record<string, unknown>)?.connectionId === "string"
|
|
212
|
+
? ((embeddings as Record<string, unknown>).connectionId as string)
|
|
213
|
+
: "";
|
|
214
|
+
|
|
215
|
+
if (llmConnId && !connectionIds.has(llmConnId)) {
|
|
216
|
+
errors.push(`assignments.llm.connectionId "${llmConnId}" does not match any connection`);
|
|
217
|
+
}
|
|
218
|
+
if (embConnId && !connectionIds.has(embConnId)) {
|
|
219
|
+
errors.push(`assignments.embeddings.connectionId "${embConnId}" does not match any connection`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { valid: errors.length === 0, errors };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Secrets Builder ──────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build the env var map from connections + assignments.
|
|
231
|
+
*
|
|
232
|
+
* Returns a Record<string, string> of secrets.env updates that should be
|
|
233
|
+
* written during setup.
|
|
234
|
+
*/
|
|
235
|
+
export function buildSecretsFromSetup(input: SetupInput): Record<string, string> {
|
|
236
|
+
const updates: Record<string, string> = {};
|
|
237
|
+
|
|
238
|
+
// Admin token
|
|
239
|
+
updates.OPENPALM_ADMIN_TOKEN = input.adminToken;
|
|
240
|
+
updates.ADMIN_TOKEN = input.adminToken;
|
|
241
|
+
|
|
242
|
+
// Owner info — strip control characters to prevent env-file injection
|
|
243
|
+
const ownerName = (input.ownerName?.trim() ?? "").replace(/[\r\n\0]/g, "").slice(0, 200);
|
|
244
|
+
const ownerEmail = (input.ownerEmail?.trim() ?? "").replace(/[\r\n\0]/g, "").slice(0, 200);
|
|
245
|
+
if (ownerName) updates.OWNER_NAME = ownerName;
|
|
246
|
+
if (ownerEmail) updates.OWNER_EMAIL = ownerEmail;
|
|
247
|
+
|
|
248
|
+
// Resolve effective base URLs (Ollama in-stack override)
|
|
249
|
+
const effectiveConnections = input.connections.map((c) => {
|
|
250
|
+
if (input.ollamaEnabled && c.provider === "ollama") {
|
|
251
|
+
return { ...c, baseUrl: OLLAMA_INSTACK_URL };
|
|
252
|
+
}
|
|
253
|
+
return c;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Build connectionId -> envVarName map
|
|
257
|
+
const connEnvVarMap = buildConnectionEnvVarMap(effectiveConnections);
|
|
258
|
+
|
|
259
|
+
// Write API keys
|
|
260
|
+
for (const conn of effectiveConnections) {
|
|
261
|
+
if (!conn.apiKey) continue;
|
|
262
|
+
const envVar = connEnvVarMap.get(conn.id);
|
|
263
|
+
if (envVar) updates[envVar] = conn.apiKey;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// System LLM vars
|
|
267
|
+
const llmConnection = effectiveConnections.find((c) => c.id === input.assignments.llm.connectionId);
|
|
268
|
+
if (llmConnection) {
|
|
269
|
+
updates.SYSTEM_LLM_PROVIDER = llmConnection.provider;
|
|
270
|
+
updates.SYSTEM_LLM_MODEL = input.assignments.llm.model;
|
|
271
|
+
if (llmConnection.baseUrl) {
|
|
272
|
+
updates.SYSTEM_LLM_BASE_URL = llmConnection.baseUrl;
|
|
273
|
+
const normalizedUrl = llmConnection.baseUrl.replace(/\/+$/, "");
|
|
274
|
+
updates.OPENAI_BASE_URL = normalizedUrl.endsWith("/v1") ? normalizedUrl : `${normalizedUrl}/v1`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Memory user ID
|
|
279
|
+
updates.MEMORY_USER_ID = input.memoryUserId || "default_user";
|
|
280
|
+
|
|
281
|
+
return updates;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Connection Env Var Map Builder ───────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Build a Map<connectionId, envVarName> from connections, using PROVIDER_KEY_MAP
|
|
288
|
+
* for the canonical mapping and falling back to namespaced vars for duplicates.
|
|
289
|
+
*/
|
|
290
|
+
export function buildConnectionEnvVarMap(
|
|
291
|
+
connections: SetupConnection[]
|
|
292
|
+
): Map<string, string> {
|
|
293
|
+
const connEnvVarMap = new Map<string, string>();
|
|
294
|
+
const claimedEnvVars = new Set<string>();
|
|
295
|
+
|
|
296
|
+
for (const conn of connections) {
|
|
297
|
+
let envVarName = PROVIDER_KEY_MAP[conn.provider] ?? "OPENAI_API_KEY";
|
|
298
|
+
if (claimedEnvVars.has(envVarName)) {
|
|
299
|
+
envVarName = `${envVarName}_${conn.id}`;
|
|
300
|
+
}
|
|
301
|
+
const upperKey = envVarName.toUpperCase();
|
|
302
|
+
if (!SAFE_ENV_KEY_RE.test(upperKey)) {
|
|
303
|
+
logger.warn("skipping connection with unsafe env var key", { connectionId: conn.id, envVarName });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
claimedEnvVars.add(upperKey);
|
|
307
|
+
connEnvVarMap.set(conn.id, upperKey);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return connEnvVarMap;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Core Setup Orchestration ─────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Core setup orchestration -- shared by CLI and admin.
|
|
317
|
+
*
|
|
318
|
+
* Steps:
|
|
319
|
+
* 1. Validate input fields
|
|
320
|
+
* 2. Build connection env var map
|
|
321
|
+
* 3. Update secrets.env with API keys and system config
|
|
322
|
+
* 4. Build and write memory config via buildMem0Mapping()
|
|
323
|
+
* 5. Write connection profiles
|
|
324
|
+
* 6. Ensure OpenCode configs
|
|
325
|
+
* 7. Apply install via applyInstall()
|
|
326
|
+
*
|
|
327
|
+
* Does NOT include Docker operations (compose up, pull, etc.) — the caller
|
|
328
|
+
* handles those separately after setup completes.
|
|
329
|
+
*/
|
|
330
|
+
export async function performSetup(
|
|
331
|
+
input: SetupInput,
|
|
332
|
+
assetProvider: CoreAssetProvider,
|
|
333
|
+
opts?: { state?: ControlPlaneState }
|
|
334
|
+
): Promise<SetupResult> {
|
|
335
|
+
// ── Validate ─────────────────────────────────────────────────────────
|
|
336
|
+
const validation = validateSetupInput(input);
|
|
337
|
+
if (!validation.valid) {
|
|
338
|
+
return { ok: false, error: validation.errors.join("; ") };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
logger.info("performing setup", {
|
|
342
|
+
connectionCount: input.connections.length,
|
|
343
|
+
ollamaEnabled: input.ollamaEnabled,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ── Resolve state ────────────────────────────────────────────────────
|
|
347
|
+
const state = opts?.state ?? createState(input.adminToken);
|
|
348
|
+
|
|
349
|
+
// ── Resolve effective connections (Ollama in-stack override) ──────────
|
|
350
|
+
const effectiveConnections = input.connections.map((c) => {
|
|
351
|
+
if (input.ollamaEnabled && c.provider === "ollama") {
|
|
352
|
+
return { ...c, baseUrl: OLLAMA_INSTACK_URL };
|
|
353
|
+
}
|
|
354
|
+
return c;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ── Build connection env var map ─────────────────────────────────────
|
|
358
|
+
const connEnvVarMap = buildConnectionEnvVarMap(effectiveConnections);
|
|
359
|
+
|
|
360
|
+
// ── Build secrets.env updates ────────────────────────────────────────
|
|
361
|
+
const updates = buildSecretsFromSetup(input);
|
|
362
|
+
|
|
363
|
+
// ── Persist secrets.env ──────────────────────────────────────────────
|
|
364
|
+
try {
|
|
365
|
+
ensureXdgDirs();
|
|
366
|
+
ensureSecrets(state);
|
|
367
|
+
ensureConnectionProfilesStore(state.configDir);
|
|
368
|
+
updateSecretsEnv(state, updates);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
371
|
+
logger.error("failed to update secrets.env", { error: message });
|
|
372
|
+
return { ok: false, error: `Failed to update secrets.env: ${message}` };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Update state with new admin token
|
|
376
|
+
state.adminToken = input.adminToken;
|
|
377
|
+
writeSetupTokenFile(state);
|
|
378
|
+
|
|
379
|
+
// ── Build and persist Memory config ──────────────────────────────────
|
|
380
|
+
const llmConnectionId = input.assignments.llm.connectionId;
|
|
381
|
+
const embConnectionId = input.assignments.embeddings.connectionId;
|
|
382
|
+
const llmModel = input.assignments.llm.model;
|
|
383
|
+
const llmSmallModel = input.assignments.llm.smallModel || "";
|
|
384
|
+
const embModel = input.assignments.embeddings.model;
|
|
385
|
+
const embDims = input.assignments.embeddings.embeddingDims || 0;
|
|
386
|
+
|
|
387
|
+
const llmConnection = effectiveConnections.find((c) => c.id === llmConnectionId)!;
|
|
388
|
+
const embConnection = effectiveConnections.find((c) => c.id === embConnectionId)!;
|
|
389
|
+
|
|
390
|
+
const memoryModel = llmSmallModel || llmModel;
|
|
391
|
+
|
|
392
|
+
const llmEnvVar = connEnvVarMap.get(llmConnection.id)!;
|
|
393
|
+
const llmApiKeyEnvRef = llmConnection.apiKey ? `env:${llmEnvVar}` : "not-needed";
|
|
394
|
+
|
|
395
|
+
const embEnvVar = connEnvVarMap.get(embConnection.id)!;
|
|
396
|
+
const embApiKeyEnvRef = embConnection.apiKey ? `env:${embEnvVar}` : "not-needed";
|
|
397
|
+
|
|
398
|
+
const embLookupKey = `${embConnection.provider}/${embModel}`;
|
|
399
|
+
const resolvedDims = embDims || EMBEDDING_DIMS[embLookupKey] || 1536;
|
|
400
|
+
|
|
401
|
+
const omConfig = buildMem0Mapping({
|
|
402
|
+
llm: {
|
|
403
|
+
provider: llmConnection.provider,
|
|
404
|
+
baseUrl: llmConnection.baseUrl,
|
|
405
|
+
model: memoryModel,
|
|
406
|
+
apiKeyRef: llmApiKeyEnvRef,
|
|
407
|
+
},
|
|
408
|
+
embedder: {
|
|
409
|
+
provider: embConnection.provider,
|
|
410
|
+
baseUrl: embConnection.baseUrl,
|
|
411
|
+
model: embModel || "text-embedding-3-small",
|
|
412
|
+
apiKeyRef: embApiKeyEnvRef,
|
|
413
|
+
},
|
|
414
|
+
embeddingDims: resolvedDims,
|
|
415
|
+
customInstructions: "",
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
writeMemoryConfig(state.dataDir, omConfig);
|
|
419
|
+
|
|
420
|
+
// ── Write connection profiles ────────────────────────────────────────
|
|
421
|
+
const profilesInput = effectiveConnections.map((conn) => ({
|
|
422
|
+
id: conn.id,
|
|
423
|
+
name: conn.name,
|
|
424
|
+
provider: conn.provider,
|
|
425
|
+
baseUrl: conn.baseUrl,
|
|
426
|
+
hasApiKey: Boolean(conn.apiKey),
|
|
427
|
+
apiKeyEnvVar: connEnvVarMap.get(conn.id)!,
|
|
428
|
+
}));
|
|
429
|
+
|
|
430
|
+
writeConnectionsDocument(state.configDir, {
|
|
431
|
+
profiles: profilesInput,
|
|
432
|
+
assignments: {
|
|
433
|
+
llm: input.assignments.llm,
|
|
434
|
+
embeddings: {
|
|
435
|
+
connectionId: input.assignments.embeddings.connectionId,
|
|
436
|
+
model: input.assignments.embeddings.model,
|
|
437
|
+
embeddingDims: resolvedDims,
|
|
438
|
+
},
|
|
439
|
+
} as CapabilityAssignments,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ── Ensure OpenCode configs ──────────────────────────────────────────
|
|
443
|
+
ensureOpenCodeConfig();
|
|
444
|
+
ensureOpenCodeSystemConfig(assetProvider);
|
|
445
|
+
ensureAdminOpenCodeConfig(assetProvider);
|
|
446
|
+
ensureMemoryDir();
|
|
447
|
+
|
|
448
|
+
// ── Apply install (stages artifacts, no Docker) ──────────────────────
|
|
449
|
+
applyInstall(state, assetProvider);
|
|
450
|
+
|
|
451
|
+
logger.info("setup complete", {
|
|
452
|
+
connectionCount: input.connections.length,
|
|
453
|
+
llmProvider: llmConnection.provider,
|
|
454
|
+
llmModel,
|
|
455
|
+
embModel,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return { ok: true };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Provider Detection ───────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Detect available local providers in a setup-friendly format.
|
|
465
|
+
* Wraps detectLocalProviders() from model-runner.ts.
|
|
466
|
+
*/
|
|
467
|
+
export async function detectProviders(): Promise<DetectedProvider[]> {
|
|
468
|
+
const raw = await detectLocalProviders();
|
|
469
|
+
return raw.map((r: LocalProviderDetection) => ({
|
|
470
|
+
provider: r.provider,
|
|
471
|
+
url: r.url,
|
|
472
|
+
available: r.available,
|
|
473
|
+
}));
|
|
474
|
+
}
|