Project Files
src / index.ts
import { type PluginContext } from "@lmstudio/sdk";
import { toolsProvider } from "./toolsProvider";
import { configSchematics, globalConfigSchematics } from "./config";
import { generate } from "./generator";
import { preprocess } from "./preprocessor";
import {
checkVisionPrimerStatus,
loadVisionPrimerModel,
} from "./helpers/visionCapabilityPrimer.js";
import fs from "fs";
import path from "path";
import os from "os";
export async function main(context: PluginContext) {
// Register plugin components
context.withConfigSchematics(configSchematics);
context.withGlobalConfigSchematics(globalConfigSchematics);
context.withPromptPreprocessor(preprocess);
context.withGenerator(generate);
context.withToolsProvider(toolsProvider);
// Vision Capability Primer (HYBRID: quick checks awaited, load fire-and-forget)
// Reason: `lms load` may block (auth/model discovery) and cause Plugin Loader timeouts.
const primerConfig = {
modelKey: "qwen/qwen3-vl-4b",
contextLength: 4096,
gpuMode: "off" as const,
ttlSeconds: 3600,
identifier: "vision-capability-priming",
};
const quickCheck = await checkVisionPrimerStatus(primerConfig);
if (quickCheck.alreadyLoaded) {
console.debug(
"[VisionPrimer] Model already loaded, injecting lastUsedModel immediately"
);
await injectLastUsedModelIntoNewestChat(
primerConfig.modelKey,
primerConfig.identifier
);
(globalThis as any).__dtc_visionPrimerResult = { ok: true, alreadyLoaded: true };
} else if (quickCheck.needsLoad && quickCheck.lmsCli) {
console.debug("[VisionPrimer] Model needs loading, starting fire-and-forget load");
const loadPromise = loadVisionPrimerModel(quickCheck.lmsCli, primerConfig);
(globalThis as any).__dtc_visionPrimerPromise = loadPromise;
loadPromise
.then(async (loadResult) => {
(globalThis as any).__dtc_visionPrimerResult = loadResult;
if (loadResult.ok) {
console.debug(
`[VisionPrimer] Model loaded: ${loadResult.size} in ${loadResult.loadTimeSec}s`
);
await injectLastUsedModelIntoNewestChat(
primerConfig.modelKey,
primerConfig.identifier
);
} else {
console.warn("[VisionPrimer] Load failed:", loadResult.error);
}
})
.catch((err) => {
console.warn("[VisionPrimer] Unexpected load error:", err?.message || err);
});
} else if (quickCheck.userFacingError) {
console.warn("[VisionPrimer] User-facing error:", quickCheck.error);
(globalThis as any).__dtc_visionPrimerResult = {
ok: false,
notInstalled: quickCheck.notInstalled,
userFacingError: quickCheck.userFacingError,
error: quickCheck.error,
};
} else if (quickCheck.infrastructureError) {
console.warn("[VisionPrimer] Infrastructure error (silent):", quickCheck.error);
}
}
/**
* Inject lastUsedModel into the newest conversation.json file, but ONLY if:
* 1. We (ceveyne/gemini-compat-endpoint) are in the plugins array
* 2. There is no lastUsedModel entry yet
*
* @param modelKey - Model key for indexedModelIdentifier (e.g., "qwen/qwen3-vl-4b")
* @param identifier - Runtime identifier (must match lms load --identifier!)
*/
async function injectLastUsedModelIntoNewestChat(
modelKey: string,
identifier?: string
): Promise<void> {
try {
const conversationsDir = path.join(os.homedir(), ".lmstudio", "conversations");
const dirExists = await fs.promises.stat(conversationsDir).then(() => true).catch(() => false);
if (!dirExists) {
console.info("[VisionPrimer] Conversations directory not found, skipping");
return;
}
const files = await fs.promises.readdir(conversationsDir);
const conversationFiles = files.filter(f => f.endsWith(".conversation.json"));
if (conversationFiles.length === 0) {
console.info("[VisionPrimer] No conversation files found");
return;
}
// Find newest conversation by creation time
let newestFile: string | null = null;
let newestBirthtime = 0;
for (const file of conversationFiles) {
const filePath = path.join(conversationsDir, file);
try {
const stats = await fs.promises.stat(filePath);
if (stats.birthtimeMs > newestBirthtime) {
newestBirthtime = stats.birthtimeMs;
newestFile = file;
}
} catch {
// Skip files we can't stat
}
}
if (!newestFile) {
console.info("[VisionPrimer] Could not determine newest conversation");
return;
}
const filePath = path.join(conversationsDir, newestFile);
const raw = await fs.promises.readFile(filePath, "utf8");
const conversation = JSON.parse(raw);
// Check if we're in the plugins array
const ourPluginId = "ceveyne/gemini-compat-endpoint";
const isActive = Array.isArray(conversation.plugins) && conversation.plugins.includes(ourPluginId);
if (!isActive) {
console.info(`[VisionPrimer] We're not active in newest chat (${newestFile}), skipping`);
return;
}
// Check if lastUsedModel already exists
if (conversation.lastUsedModel) {
console.info(`[VisionPrimer] lastUsedModel already exists in ${newestFile}, skipping`);
return;
}
// Inject lastUsedModel
const runtimeIdentifier = identifier ?? modelKey;
conversation.lastUsedModel = {
indexedModelIdentifier: modelKey,
identifier: runtimeIdentifier, // ← Referenz auf bereits geladenes Modell!
instanceLoadTimeConfig: { fields: [] },
instanceOperationTimeConfig: { fields: [] }
};
await fs.promises.writeFile(filePath, JSON.stringify(conversation, null, 2), "utf8");
// First touch to ensure LM Studio's file watcher picks up the change
const now = new Date();
await fs.promises.utimes(filePath, now, now);
console.info(`[VisionPrimer] ✓ Injected lastUsedModel (model=${modelKey}, id=${runtimeIdentifier}) into ${newestFile}`);
console.info(`[VisionPrimer] ✓ First touch to ${newestFile}`);
// Second touch after 2 seconds to ensure UI update
setTimeout(async () => {
try {
const now2 = new Date();
await fs.promises.utimes(filePath, now2, now2);
console.info(`[VisionPrimer] ✓ Second touch to ${newestFile}`);
} catch (e: any) {
console.warn("[VisionPrimer] Failed second touch:", e.message || e);
}
}, 2000);
} catch (e: any) {
console.warn("[VisionPrimer] Failed to process newest chat:", e.message || e);
}
}