Project Files
src / config.ts
import { createConfigSchematics } from "@lmstudio/sdk";
import { homedir } from "os";
import path from "path";
import { FIND_IMAGE_BACKENDS_ROOT, FIND_IMAGE_MODELS_ROOT, findImageDataPath } from "./paths.js";
/** Sentinel value for multimodalMaxNewEmbeddingsPerRun: at or above this, the run is unlimited (same convention as retrievalLimit's "25 = all"). */
export const MULTIMODAL_MAX_NEW_EMBEDDINGS_UNLIMITED = 100;
export const defaultPluginSettings = {
// ── Search Settings (per-chat) ────────────────────────────────────────────
allowByteIdenticalResults: false,
retrievalLimit: 5,
gpsToleranceMeters: 0,
createdToleranceMinutes: 0,
multimodalMaxNewEmbeddingsPerRun: 25,
// ── Multimodal Search (Qwen3-VL-Embedding) ───────────────────────────────
multimodalSearchEnabled: true,
multimodalEmbeddingBackend: "integrated-local-gguf",
llamaServerTTL: 0,
multimodalEmbeddingServicePort: 8765,
multimodalEmbeddingLmStudioUrl: "http://127.0.0.1:1234",
multimodalEmbeddingModel: "qwen/qwen3-vl-embedding-8b",
multimodalEmbeddingModelPath: path.join(
FIND_IMAGE_MODELS_ROOT,
"Qwen3-VL-Embedding-8B-GGUF",
),
multimodalEmbeddingGgufBinaryPath: path.join(
FIND_IMAGE_BACKENDS_ROOT,
"qwen3-vl-embedding",
"llama-server"
),
multimodalEmbeddingDimension: 1024,
multimodalEmbeddingContextSize: 8192,
multimodalEmbeddingGgufGpuLayers: 999,
multimodalEmbeddingDataStorePath: findImageDataPath("multimodal_embeddings.sqlite3"),
embeddingMinScore: 30,
embeddingDisplayMultiplier: 1.1,
rerankingEnabled: false,
rerankingModelPath: path.join(
FIND_IMAGE_MODELS_ROOT,
"Qwen3-VL-Reranker-8B-GGUF",
),
rerankingServicePort: 8766,
rerankingContextSize: 8192,
rerankingBatchSize: 8192,
rerankingUbatchSize: 8192,
rerankingDisplayMultiplier: 1.2,
rerankingMinScore: 0.05,
rerankingCandidateMultiplier: 2,
rerankingMaxCandidates: 25,
multimodalFlushEvery: 5,
// ── Search Sources ───────────────────────────────────────────────────────────
usePreviewsForQueryEmbedding: false,
contentDirectories: [] as string[],
searchChatAttachments: true,
searchWorkingDirectories: true,
searchDrawThingsProjects: true,
drawThingsProjectsDirectory: path.join(
homedir(),
"Library",
"Containers",
"com.liuliu.draw-things",
"Data",
"Documents",
),
// ── Cleanup Tools ─────────────────────────────────────────────────────────
overwriteToken: "",
} as const;
/**
* Per-chat config. Unlike globalConfigSchematics (application-wide, read via
* ctl.getGlobalPluginConfig()), changes to these fields take effect
* immediately for the current conversation (read via ctl.getPluginConfig()).
* Only search-tuning knobs belong here — nothing that reflects a global
* resource (model paths, ports, directories, credentials).
*/
export const configSchematics = createConfigSchematics()
// ── Search Settings ───────────────────────────────────────────────────────
.field(
"allowByteIdenticalResults",
"boolean",
{
displayName: "Allow byte-identical Results",
subtitle: "Show every matching file, including byte-identical copies. When off, only the newest duplicate is shown.",
engineDoesNotSupport: false,
},
defaultPluginSettings.allowByteIdenticalResults,
)
.field(
"retrievalLimit",
"numeric",
{
int: true,
min: 1,
max: 25,
displayName: "Max Results",
subtitle: "Maximum number of generations (25 = all)",
slider: { min: 1, max: 25, step: 1 },
},
defaultPluginSettings.retrievalLimit,
)
.field(
"gpsToleranceMeters",
"numeric",
{
int: true,
min: 0,
max: 100_000,
displayName: "GPS Tolerance",
subtitle: "Search radius in metres around the reference GPS coordinates (0 = exact coordinates).",
slider: { min: 0, max: 5_000, step: 50 },
engineDoesNotSupport: true,
},
defaultPluginSettings.gpsToleranceMeters,
)
.field(
"createdToleranceMinutes",
"numeric",
{
int: true,
min: 0,
max: 43_200,
displayName: "Created Tolerance",
subtitle: "Symmetric +/- time window in minutes around the reference creation time (0 = exact time).",
slider: { min: 0, max: 1_440, step: 1 },
engineDoesNotSupport: true,
},
defaultPluginSettings.createdToleranceMinutes,
)
.field(
"multimodalMaxNewEmbeddingsPerRun",
"numeric",
{
int: true,
min: 1,
max: MULTIMODAL_MAX_NEW_EMBEDDINGS_UNLIMITED,
displayName: "Max New Multimodal Embeddings per Run",
subtitle: `Maximum newly generated image embeddings per index run (0 = disabled, ${MULTIMODAL_MAX_NEW_EMBEDDINGS_UNLIMITED} = unlimited)`,
slider: { min: 0, max: MULTIMODAL_MAX_NEW_EMBEDDINGS_UNLIMITED, step: 1 },
engineDoesNotSupport: false,
},
defaultPluginSettings.multimodalMaxNewEmbeddingsPerRun,
)
.build();
export const globalConfigSchematics = createConfigSchematics()
// ── Multimodal Search (Qwen3-VL-Embedding) ───────────────────────────────
.field(
"multimodalSearchEnabled",
"boolean",
{
displayName: "Multimodal Search",
subtitle: "Use Qwen3-VL-Embedding for text-to-image and image-to-image retrieval. When off, no images are indexed/embedded and no query (including image-target queries) uses multimodal search — metadata/keyword search only.",
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalSearchEnabled,
)
.field(
"multimodalEmbeddingBackend",
"select",
{
displayName: "Multimodal Embedding Backend",
subtitle: "Backend for Qwen multimodal embeddings. Integrated Local GGUF uses a native llama-server process.",
options: [
{ value: "integrated-local-gguf", displayName: "Integrated Local GGUF" },
{ value: "lmstudio-api", displayName: "LM Studio API" },
],
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalEmbeddingBackend,
)
.field(
"llamaServerTTL",
"numeric",
{
int: true,
min: 0,
max: 120,
displayName: "llama-server Idle Timeout",
subtitle: "Minutes to keep the local llama-server running after its last activity. 0 stops it immediately.",
engineDoesNotSupport: true,
},
defaultPluginSettings.llamaServerTTL,
)
.field(
"multimodalEmbeddingServicePort",
"numeric",
{
int: true,
min: 1,
max: 65535,
displayName: "Multimodal Service Port",
subtitle: "Port for the local llama-server embedding process. Always bound to localhost.",
engineDoesNotSupport: false,
},
defaultPluginSettings.multimodalEmbeddingServicePort,
)
.field(
"multimodalEmbeddingLmStudioUrl",
"string",
{
displayName: "Multimodal LM Studio URL",
subtitle: "Preferred future LM Studio API URL for multimodal embeddings.",
placeholder: "http://127.0.0.1:1234",
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalEmbeddingLmStudioUrl,
)
.field(
"multimodalEmbeddingModel",
"string",
{
displayName: "Multimodal Embedding Model",
subtitle: "Model identity stored with Qwen multimodal vectors.",
placeholder: "qwen/qwen3-vl-embedding-8b",
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalEmbeddingModel,
)
.field(
"multimodalEmbeddingModelPath",
"string",
{
displayName: "Multimodal Model Path",
subtitle: "Local GGUF model path: an unambiguous directory or an explicit main .gguf file, with a matching mmproj .gguf file alongside it.",
placeholder: path.join(homedir(), "Documents", "Models", "Qwen3-VL-Embedding-8B-GGUF"),
engineDoesNotSupport: false,
},
defaultPluginSettings.multimodalEmbeddingModelPath,
)
.field(
"multimodalEmbeddingGgufBinaryPath",
"string",
{
displayName: "GGUF Embedding Binary Path",
subtitle: "Absolute path to a llama-server executable built from the qwen3-vl-embedding llama.cpp fork.",
placeholder: path.join(homedir(), "Documents", "Repos", "qwen3-vl-embedding", "llama.cpp", "build", "bin", "llama-server"),
engineDoesNotSupport: false,
},
defaultPluginSettings.multimodalEmbeddingGgufBinaryPath,
)
.field(
"multimodalEmbeddingDimension",
"numeric",
{
int: true,
min: 64,
max: 4096,
displayName: "Multimodal Embedding Dimension",
subtitle: "Matryoshka output dimension used for Qwen multimodal vectors.",
slider: { min: 64, max: 4096, step: 64 },
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalEmbeddingDimension,
)
.field(
"multimodalEmbeddingContextSize",
"numeric",
{
int: true,
min: 1024,
max: 32768,
displayName: "Multimodal Context Size",
subtitle: "Context window used by the local multimodal embedding runtime.",
slider: { min: 1024, max: 32768, step: 1024 },
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalEmbeddingContextSize,
)
.field(
"multimodalEmbeddingGgufGpuLayers",
"numeric",
{
int: true,
min: 1,
max: 999,
displayName: "GGUF GPU Layers",
subtitle: "GPU offload layers passed to llama-server (-ngl). GPU is required; use 999 for full offload with a verified patched binary.",
slider: { min: 1, max: 999, step: 1 },
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalEmbeddingGgufGpuLayers,
)
.field(
"multimodalEmbeddingDataStorePath",
"string",
{
displayName: "Multimodal Embedding Data Store Path",
subtitle: "SQLite database used to store multimodal embeddings.",
placeholder: findImageDataPath("multimodal_embeddings.sqlite3"),
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalEmbeddingDataStorePath,
)
.field(
"embeddingMinScore",
"numeric",
{
int: true,
min: 0,
max: 100,
displayName: "Embedding Minimum Score",
subtitle: "Minimum raw cosine similarity, expressed as a whole-number percent, for Embedding recall.",
slider: { min: 0, max: 100, step: 5 },
engineDoesNotSupport: true,
},
defaultPluginSettings.embeddingMinScore,
)
.field(
"embeddingDisplayMultiplier",
"numeric",
{
min: 0.1,
max: 10,
displayName: "Embedding Display Multiplier",
subtitle: "Scales only the displayed Embedding score; recall thresholds and ordering always use the unscaled value.",
slider: { min: 0.1, max: 10, step: 0.1 },
engineDoesNotSupport: true,
},
defaultPluginSettings.embeddingDisplayMultiplier,
)
.field(
"rerankingEnabled",
"boolean",
{
displayName: "Enable Reranking",
subtitle: "Use the configured Q8_0 reranker after Embedding recall for eligible content queries.",
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingEnabled,
)
.field(
"rerankingServicePort",
"numeric",
{
int: true,
min: 1,
max: 65535,
displayName: "Reranking Service Port",
subtitle: "Temporary localhost port for the reranker. It must differ from the embedding service port.",
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingServicePort,
)
.field(
"rerankingModelPath",
"string",
{
displayName: "Reranking Model Path",
subtitle: "Local Q8_0 reranker GGUF file or its directory, with its F16 mmproj alongside it.",
placeholder: path.join(homedir(), "Documents", "Models", "Qwen3-VL-Reranker-8B-Q8_0.gguf"),
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingModelPath,
)
.field(
"rerankingContextSize",
"numeric",
{
int: true,
min: 1024,
max: 32768,
displayName: "Reranking Context Size",
subtitle: "Maximum tokens for one reranker query-document pair.",
slider: { min: 1024, max: 32768, step: 1024 },
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingContextSize,
)
.field(
"rerankingBatchSize",
"numeric",
{
int: true,
min: 512,
max: 32768,
displayName: "Reranking Batch Size",
subtitle: "Physical prompt-processing batch size for the temporary reranker server.",
slider: { min: 512, max: 32768, step: 512 },
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingBatchSize,
)
.field(
"rerankingUbatchSize",
"numeric",
{
int: true,
min: 512,
max: 32768,
displayName: "Reranking Microbatch Size",
subtitle: "Maximum tokens processed per reranker prompt chunk.",
slider: { min: 512, max: 32768, step: 512 },
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingUbatchSize,
)
.field(
"rerankingDisplayMultiplier",
"numeric",
{
min: 0.1,
max: 10,
displayName: "Reranking Display Multiplier",
subtitle: "Scales only the displayed reranker score; recall and reranker ordering always use the unscaled value.",
slider: { min: 0.1, max: 10, step: 0.1 },
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingDisplayMultiplier,
)
.field(
"rerankingMinScore",
"numeric",
{
min: 0,
max: 1,
displayName: "Reranking Minimum Score",
subtitle: "Minimum raw reranker score required to show a reranked candidate; 0 keeps all candidates for calibration.",
slider: { min: 0, max: 1, step: 0.01 },
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingMinScore,
)
.field(
"rerankingCandidateMultiplier",
"numeric",
{
int: true,
min: 1,
max: 20,
displayName: "Reranking Candidate Multiplier",
subtitle: "Multiplies Max Results to determine how many Embedding recall candidates the reranker evaluates before applying the final result limit.",
slider: { min: 1, max: 20, step: 1 },
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingCandidateMultiplier,
)
.field(
"rerankingMaxCandidates",
"numeric",
{
int: true,
min: 1,
max: 100,
displayName: "Maximum Reranking Candidates",
subtitle: "Hard cap for one reranking request.",
engineDoesNotSupport: true,
},
defaultPluginSettings.rerankingMaxCandidates,
)
.field(
"multimodalFlushEvery",
"numeric",
{
int: true,
min: 1,
max: 100,
displayName: "Multimodal Store Flush Interval",
subtitle: "Persist Qwen image embeddings after this many newly embedded images.",
slider: { min: 1, max: 25, step: 1 },
engineDoesNotSupport: true,
},
defaultPluginSettings.multimodalFlushEvery,
)
.field(
"usePreviewsForQueryEmbedding",
"boolean",
{
displayName: "Use Previews for Image Retrieval",
subtitle: "Use standardized preview images instead of high-resolution originals. Speeds up the retrieval, but may affect quality.",
engineDoesNotSupport: false,
},
defaultPluginSettings.usePreviewsForQueryEmbedding,
)
// ── Search Source 1: Image Directories ────────────────────────────────────
.field(
"contentDirectories",
"stringArray",
{
displayName: "Image Directories",
subtitle: "Search and visually index image files. Metadata is used when available but not required.",
},
[...defaultPluginSettings.contentDirectories],
)
// ── Search Source 2: LM Studio Chat Attachments ───────────────────────────
.field(
"searchChatAttachments",
"boolean",
{
displayName: "Search Chat Attachments",
subtitle: "Search and visually index images attached to LM Studio chats (~/.lmstudio/user-files)",
engineDoesNotSupport: false,
},
defaultPluginSettings.searchChatAttachments,
)
// ── Search Source 3: LM Studio Working Directories ───────────────────────────
.field(
"searchWorkingDirectories",
"boolean",
{
displayName: "Search Working Directories",
subtitle: "Search and visually index images in LM Studio chat working-directories (~/.lmstudio/working-directories)",
},
defaultPluginSettings.searchWorkingDirectories,
)
// ── Search Source 4: Draw Things Projects (.sqlite3) ──────────────────────
.field(
"searchDrawThingsProjects",
"boolean",
{
displayName: "Search Draw Things Projects",
subtitle: "Search through Draw Things project files with full generation history.",
},
defaultPluginSettings.searchDrawThingsProjects,
)
.field(
"drawThingsProjectsDirectory",
"string",
{
displayName: "Projects Directory",
subtitle: "Directory containing Draw Things project files.",
},
defaultPluginSettings.drawThingsProjectsDirectory,
)
// ── Cleanup Tools ─────────────────────────────────────────────────────────
.field(
"overwriteToken",
"string",
{
displayName: "Overwrite Token",
subtitle: "Secret token required to overwrite a project file in-place via clean_project. Leave empty to disable in-place overwrite.",
engineDoesNotSupport: true,
},
defaultPluginSettings.overwriteToken,
)
.build();