| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- /**
- * factory.ts - EmbeddingProvider factory with config precedence.
- *
- * Resolution order (first match wins):
- * 1. Explicit `kind` argument or `--provider` CLI flag → forces a kind
- * 2. `QMD_EMBED_ENDPOINT` env var present and non-empty → "openai"
- * 3. Config file (`~/.config/qmd/config.json`) `embedProvider.kind` → that kind
- * 4. Otherwise → "local" (legacy / backward-compat)
- *
- * Backward compat invariant: when neither `QMD_EMBED_ENDPOINT` nor
- * `~/.config/qmd/config.json` mentions a provider, callers get the same
- * `LocalLlamaCppProvider` they had before this change.
- */
- import { existsSync, readFileSync } from "node:fs";
- import { homedir } from "node:os";
- import { join } from "node:path";
- import { LocalLlamaCppProvider } from "./local.js";
- import { OpenAIEmbeddingsProvider, } from "./openai.js";
- import { AutoFallbackEmbeddingProvider, } from "./autofallback.js";
- export function defaultConfigPath() {
- const xdg = process.env.XDG_CONFIG_HOME;
- const base = xdg ? xdg : join(homedir(), ".config");
- return join(base, "qmd", "config.json");
- }
- /**
- * Load `~/.config/qmd/config.json` if present. Returns an empty object on
- * any read/parse error so we silently fall back to env/local.
- */
- export function loadConfigFile(path = defaultConfigPath()) {
- if (!existsSync(path))
- return {};
- try {
- const raw = readFileSync(path, "utf-8");
- const parsed = JSON.parse(raw);
- if (parsed && typeof parsed === "object")
- return parsed;
- }
- catch {
- // Ignore — invalid JSON, missing read perm, etc.
- }
- return {};
- }
- /**
- * Resolve the provider kind without instantiating anything. Useful for
- * logging and tests.
- */
- export function resolveProviderKind(opts = {}) {
- const env = opts.env ?? process.env;
- const cfg = loadConfigFile(opts.configPath);
- // 1. Explicit kind argument
- if (opts.kind)
- return opts.kind;
- // 2a. Explicit env override
- const envKind = env.QMD_EMBED_PROVIDER?.trim().toLowerCase();
- if (envKind === "local" || envKind === "openai")
- return envKind;
- // 2b. Endpoint env present → openai
- if (env.QMD_EMBED_ENDPOINT && env.QMD_EMBED_ENDPOINT.trim() !== "") {
- return "openai";
- }
- // 3. Config file
- if (cfg.embedProvider?.kind === "local" || cfg.embedProvider?.kind === "openai") {
- return cfg.embedProvider.kind;
- }
- if (cfg.embedProvider?.endpoint && cfg.embedProvider.endpoint.trim() !== "") {
- return "openai";
- }
- // 4. Default
- return "local";
- }
- /**
- * Factory entry point — returns the appropriate `EmbeddingProvider`.
- * Throws if `openai` kind is requested but no endpoint is configured.
- */
- export function createEmbeddingProvider(opts = {}) {
- const env = opts.env ?? process.env;
- const cfg = loadConfigFile(opts.configPath);
- const kind = resolveProviderKind(opts);
- if (kind === "local") {
- return new LocalLlamaCppProvider(opts.local ?? {});
- }
- // OpenAI
- const endpoint = opts.openai?.endpoint ??
- env.QMD_EMBED_ENDPOINT ??
- cfg.embedProvider?.endpoint;
- if (!endpoint || endpoint.trim() === "") {
- throw new Error('createEmbeddingProvider: kind="openai" requires an endpoint. ' +
- "Set QMD_EMBED_ENDPOINT env var, or `embedProvider.endpoint` in " +
- "~/.config/qmd/config.json, or pass `openai.endpoint`.");
- }
- const apiKey = opts.openai?.apiKey ??
- env.QMD_EMBED_API_KEY ??
- cfg.embedProvider?.apiKey;
- const modelId = opts.openai?.modelId ??
- env.QMD_EMBED_MODEL_ID ??
- cfg.embedProvider?.modelId ??
- "embeddinggemma";
- const upstreamModel = opts.openai?.upstreamModel ??
- env.QMD_EMBED_UPSTREAM_MODEL ??
- cfg.embedProvider?.upstreamModel;
- const batchSizeRaw = opts.openai?.batchSize ??
- parsePositiveInt(env.QMD_EMBED_BATCH_SIZE) ??
- cfg.embedProvider?.batchSize;
- const timeoutMsRaw = opts.openai?.timeoutMs ??
- parsePositiveInt(env.QMD_EMBED_TIMEOUT_MS) ??
- cfg.embedProvider?.timeoutMs;
- const openaiProvider = new OpenAIEmbeddingsProvider({
- endpoint,
- apiKey,
- modelId,
- upstreamModel,
- batchSize: batchSizeRaw,
- timeoutMs: timeoutMsRaw,
- fetchImpl: opts.openai?.fetchImpl,
- retryBackoffsMs: opts.openai?.retryBackoffsMs,
- sleep: opts.openai?.sleep,
- now: opts.openai?.now,
- });
- // Should we wrap with AutoFallback? Resolution: arg → env → config → false.
- const autoFallback = resolveAutoFallback(opts, env, cfg);
- if (!autoFallback)
- return openaiProvider;
- return new AutoFallbackEmbeddingProvider({
- primary: openaiProvider,
- fallback: new LocalLlamaCppProvider(opts.local ?? { modelId }),
- ...(opts.autoFallbackOverrides ?? {}),
- });
- }
- function resolveAutoFallback(opts, env, cfg) {
- if (typeof opts.autoFallback === "boolean")
- return opts.autoFallback;
- const envVal = env.QMD_EMBED_AUTO_FALLBACK?.trim().toLowerCase();
- if (envVal === "1" || envVal === "true" || envVal === "yes")
- return true;
- if (envVal === "0" || envVal === "false" || envVal === "no")
- return false;
- if (typeof cfg.embedProvider?.autoFallback === "boolean") {
- return cfg.embedProvider.autoFallback;
- }
- return false;
- }
- // ─────────────────────────── Helpers ────────────────────────────────────────
- function parsePositiveInt(v) {
- if (!v)
- return undefined;
- const parsed = Number.parseInt(v, 10);
- if (!Number.isFinite(parsed) || parsed <= 0)
- return undefined;
- return parsed;
- }
|