182 lines · 6.3 KB · TypeScript
RawDownload
import { config as loadEnvironment } from "dotenv";import { Schema } from "effect";import { existsSync } from "node:fs";import { homedir } from "node:os";import { dirname, join, resolve } from "node:path";import { fileURLToPath } from "node:url";import { loadPersistedConfig, type ProviderConfig } from "./persisted-config";import { parseBooleanFlag } from "../core/validation"; const positiveIntegerSchema = Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0)); export interface Config {  host: string;  port: number;  api_key?: string;  cors_origins?: string[];  inference_host: string;  inference_port: number;   data_dir: string;  db_path: string;  models_dir: string;  sglang_python?: string;  llama_bin?: string;  mlx_python?: string;  strict_openai_models: boolean;  providers: ProviderConfig[];} export const loadDotEnvironment = (): string | undefined => {  const candidates = [    resolve(process.cwd(), ".env"),    resolve(process.cwd(), "..", ".env"),    resolve(process.cwd(), "..", "..", ".env"),  ];   const envPath = candidates.find((pathValue) => existsSync(pathValue));  if (envPath) {    loadEnvironment({ path: envPath });  }  return envPath;}; const defaultModelsDirectory = (): string =>  process.platform === "win32" ? join(homedir(), "models") : "/models"; export const createConfig = (): Config => {  loadDotEnvironment();   // Anchor defaults to the controller package root (two levels up from src/config/)  // so the data dir lands at <repo>/data regardless of the cwd the process started from.  const controllerRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");  const defaultDataDirectory = resolve(controllerRoot, "..", "data");   const isLoopbackHost = (value: string): boolean => {    const normalized = value.trim().toLowerCase();    return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";  };   const normalizeOrigin = (value: string): string | null => {    try {      const origin = new URL(value.trim()).origin;      return origin === "null" ? null : origin;    } catch {      return null;    }  };   const parseCorsOrigins = (value: string | undefined): string[] => {    const defaults = [      "http://localhost:3000",      "http://127.0.0.1:3000",      "http://localhost:3001",      "http://127.0.0.1:3001",      "http://host.docker.internal:3000",      "http://host.docker.internal:3001",    ];    const candidates =      value && value.trim().length > 0 ? value.split(",").map((entry) => entry.trim()) : defaults;    return [      ...new Set(        candidates          .map((entry) => normalizeOrigin(entry))          .filter((entry): entry is string => Boolean(entry)),      ),    ];  };   const environmentSchema = Schema.Struct({    LOCAL_STUDIO_HOST: Schema.String,    LOCAL_STUDIO_PORT: positiveIntegerSchema,    LOCAL_STUDIO_API_KEY: Schema.optional(Schema.String),    LOCAL_STUDIO_ALLOW_UNAUTHENTICATED: Schema.optional(Schema.String),    LOCAL_STUDIO_CORS_ORIGINS: Schema.optional(Schema.String),    LOCAL_STUDIO_INFERENCE_HOST: Schema.String,    LOCAL_STUDIO_INFERENCE_PORT: positiveIntegerSchema,     LOCAL_STUDIO_DATA_DIR: Schema.String,    LOCAL_STUDIO_DB_PATH: Schema.optional(Schema.String),    LOCAL_STUDIO_MODELS_DIR: Schema.String,    LOCAL_STUDIO_SGLANG_PYTHON: Schema.optional(Schema.String),    LOCAL_STUDIO_LLAMA_BIN: Schema.optional(Schema.String),    LOCAL_STUDIO_MLX_PYTHON: Schema.optional(Schema.String),    LOCAL_STUDIO_STRICT_OPENAI_MODELS: Schema.optional(Schema.String),  });   const coercePositiveInteger = (    key: "LOCAL_STUDIO_PORT" | "LOCAL_STUDIO_INFERENCE_PORT",    fallback: number,  ): number => {    const value = process.env[key];    return value === undefined ? fallback : Number(value);  };   const parsed = Schema.decodeUnknownSync(environmentSchema, {    onExcessProperty: "preserve",  })({    ...process.env,    LOCAL_STUDIO_HOST: process.env["LOCAL_STUDIO_HOST"] ?? "127.0.0.1",    LOCAL_STUDIO_PORT: coercePositiveInteger("LOCAL_STUDIO_PORT", 8080),    LOCAL_STUDIO_INFERENCE_HOST: process.env["LOCAL_STUDIO_INFERENCE_HOST"] ?? "localhost",    LOCAL_STUDIO_INFERENCE_PORT: coercePositiveInteger("LOCAL_STUDIO_INFERENCE_PORT", 8000),    LOCAL_STUDIO_DATA_DIR: process.env["LOCAL_STUDIO_DATA_DIR"] ?? defaultDataDirectory,    LOCAL_STUDIO_MODELS_DIR: process.env["LOCAL_STUDIO_MODELS_DIR"] ?? defaultModelsDirectory(),  });  const host = parsed.LOCAL_STUDIO_HOST.trim() || "127.0.0.1";   const strictOpenAIModelsEnabled = parseBooleanFlag(parsed.LOCAL_STUDIO_STRICT_OPENAI_MODELS);   // The db default follows the resolved data dir so overriding LOCAL_STUDIO_DATA_DIR  // alone keeps the database inside it.  const dataDirectory = resolve(parsed.LOCAL_STUDIO_DATA_DIR);  const databasePath = resolve(    parsed.LOCAL_STUDIO_DB_PATH ?? resolve(dataDirectory, "controller.db"),  );   const config: Config = {    host,    port: parsed.LOCAL_STUDIO_PORT,    inference_host: parsed.LOCAL_STUDIO_INFERENCE_HOST.trim() || "localhost",    inference_port: parsed.LOCAL_STUDIO_INFERENCE_PORT,     data_dir: dataDirectory,    db_path: databasePath,    models_dir: resolve(parsed.LOCAL_STUDIO_MODELS_DIR),    strict_openai_models: strictOpenAIModelsEnabled,    cors_origins: parseCorsOrigins(parsed.LOCAL_STUDIO_CORS_ORIGINS),    providers: [],  };   if (parsed.LOCAL_STUDIO_API_KEY) {    config.api_key = parsed.LOCAL_STUDIO_API_KEY;  }   const allowUnauthenticated = parseBooleanFlag(parsed.LOCAL_STUDIO_ALLOW_UNAUTHENTICATED);  if (!config.api_key && !allowUnauthenticated && !isLoopbackHost(host)) {    throw new Error(      "LOCAL_STUDIO_API_KEY is required when binding the controller to a non-loopback host. Set LOCAL_STUDIO_ALLOW_UNAUTHENTICATED=true only for trusted local environments.",    );  }   if (parsed.LOCAL_STUDIO_SGLANG_PYTHON) {    config.sglang_python = parsed.LOCAL_STUDIO_SGLANG_PYTHON;  }  if (parsed.LOCAL_STUDIO_LLAMA_BIN) {    config.llama_bin = parsed.LOCAL_STUDIO_LLAMA_BIN;  }  if (parsed.LOCAL_STUDIO_MLX_PYTHON) {    config.mlx_python = parsed.LOCAL_STUDIO_MLX_PYTHON;  }   const persisted = loadPersistedConfig(config.data_dir);  if (persisted.models_dir) {    config.models_dir = resolve(persisted.models_dir);  }   if (Array.isArray(persisted.providers)) {    config.providers = persisted.providers;  }   return config;};