src / config / sources / LMStudioConfigurationSource.ts
src / config / sources / LMStudioConfigurationSource.ts
import { ParsedConfig } from "@lmstudio/sdk";
import { ImageProviderRegistry } from "../../image_search/providers/ImageProviderRegistry";
import { ProviderRegistry } from "../../providers/ProviderRegistry";
import { IConfigurationSource } from "../IConfigurationSource";
import { PrioritySlotEditor } from "../ui/PrioritySlotEditor";
export type AnyParsedConfig = ParsedConfig<any>;
/**
* Configuration source reading settings from LM Studio SDK's PluginConfig settings panel.
* Uses PrioritySlotEditor to normalize numeric IDs (1,2,3) or names into string[] priority arrays at the UI boundary.
* Supports dual global (Normal) and per-chat configuration schematics.
*/
export class LMStudioConfigurationSource implements IConfigurationSource {
private globalPluginConfig?: AnyParsedConfig;
private perChatPluginConfig?: AnyParsedConfig;
private webEditor: PrioritySlotEditor;
private imageEditor: PrioritySlotEditor;
constructor(globalConfig?: AnyParsedConfig, perChatConfig?: AnyParsedConfig) {
this.globalPluginConfig = globalConfig;
this.perChatPluginConfig = perChatConfig;
const webOptions = ProviderRegistry.getAllSpecs().map((s) => ({
id: s.id,
value: s.name,
displayName: String(s.displayName || (s as any).display_name || s.name),
}));
this.webEditor = new PrioritySlotEditor(webOptions);
const imageOptions = ImageProviderRegistry.getAllSpecs().map((s) => ({
id: s.id,
value: s.name,
displayName: String(s.displayName || (s as any).display_name || s.name),
}));
this.imageEditor = new PrioritySlotEditor(imageOptions);
}
public setPluginConfig(pluginConfig: ParsedConfig<any>): void {
this.globalPluginConfig = pluginConfig;
}
public setConfigs(globalConfig?: AnyParsedConfig, perChatConfig?: AnyParsedConfig): void {
this.globalPluginConfig = globalConfig;
this.perChatPluginConfig = perChatConfig;
}
private getActiveConfig(): AnyParsedConfig | undefined {
return this.perChatPluginConfig ?? this.globalPluginConfig;
}
public getWebDefaultProvider(): string | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("webDefaultProvider") as string;
return val && val !== "auto" ? val.trim().toLowerCase() : undefined;
} catch {
return undefined;
}
}
public getWebProviderPriority(): string[] | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
// 1. Check dropdown slots first (Primary & Fallback)
const chosenSlots: string[] = [];
for (const key of ["webPrioritySlot1", "webPrioritySlot2"]) {
try {
const val = (config.get(key as any) as string)?.trim()?.toLowerCase();
if (val && val !== "none") {
chosenSlots.push(val);
}
} catch {
// Slot field not present
}
}
// If user chose at least one dropdown option (other than "none"), dropdowns take precedence
if (chosenSlots.length > 0) {
return this.webEditor.normalizeOrder(chosenSlots);
}
// 2. Both dropdowns are "none" -> follow Custom Web Priority Order text box
try {
const customVal = config.get("webProviderPriority" as any) as string;
if (customVal && customVal.trim()) {
return this.webEditor.normalizeOrder(customVal);
}
} catch {
// webProviderPriority not present
}
return undefined;
}
private static cachedApiKeys: Map<string, string[]> = new Map();
public getProviderKeys(providerName: string, category?: string): string[] | undefined {
const config = this.globalPluginConfig ?? this.perChatPluginConfig;
const norm = providerName.toLowerCase().trim();
const cacheKey = category ? `${category.toLowerCase()}:${norm}` : norm;
if (config) {
const candidateKeys = [
...(category ? [`${category.toLowerCase()}.${norm}Keys`, `${category.toLowerCase()}_${norm}Keys`] : []),
`${norm}Keys`,
];
for (const fieldKey of candidateKeys) {
try {
const val = config.get(fieldKey as any) as string;
if (val && val.trim()) {
const keys = val.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
if (keys.length > 0) {
LMStudioConfigurationSource.cachedApiKeys.set(cacheKey, keys);
return keys;
}
}
} catch {
// Key field not present in schematics map under this candidate key
}
}
}
return LMStudioConfigurationSource.cachedApiKeys.get(cacheKey);
}
public getImageDefaultProvider(): string | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("imageDefaultProvider") as string;
return val && val !== "auto" ? val.trim().toLowerCase() : undefined;
} catch {
return undefined;
}
}
public getImageProviderPriority(): string[] | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
// 1. Check dropdown slots first (Primary & Fallback)
const chosenSlots: string[] = [];
for (const key of ["imagePrioritySlot1", "imagePrioritySlot2"]) {
try {
const val = (config.get(key as any) as string)?.trim()?.toLowerCase();
if (val && val !== "none") {
chosenSlots.push(val);
}
} catch {
// Slot field not present
}
}
// If user chose at least one dropdown option (other than "none"), dropdowns take precedence
if (chosenSlots.length > 0) {
return this.imageEditor.normalizeOrder(chosenSlots);
}
// 2. Both dropdowns are "none" -> follow Custom Image Priority Order text box
try {
const customVal = config.get("imageProviderPriority" as any) as string;
if (customVal && customVal.trim()) {
return this.imageEditor.normalizeOrder(customVal);
}
} catch {
// imageProviderPriority not present
}
return undefined;
}
public getHttpTimeoutSeconds(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("httpTimeoutSeconds") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getQuotaCooldownHours(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("quotaCooldownHours") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getSearchMaxSnippetLength(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxSnippetLength") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getSearchMaxPayloadCharacters(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxPayloadCharacters") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getPrependTimeContext(): boolean | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("prependTimeContext") as boolean;
return typeof val === "boolean" ? val : undefined;
} catch {
return undefined;
}
}
public getPrependSearchImagesContext(): boolean | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("prependSearchImagesContext") as boolean;
return typeof val === "boolean" ? val : undefined;
} catch {
return undefined;
}
}
public getPrependVisionContext(): boolean | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("prependVisionContext") as boolean;
return typeof val === "boolean" ? val : undefined;
} catch {
return undefined;
}
}
public getMaxImageResults(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxImages" as any) as number;
return typeof val === "number" && val > 0 ? Math.floor(val) : undefined;
} catch {
return undefined;
}
}
public getMaxWebResults(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxWebResults" as any) as number;
return typeof val === "number" && val > 0 ? Math.floor(val) : undefined;
} catch {
return undefined;
}
}
public getReadWebpageChunkSize(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("readWebpageChunkSize" as any) as number;
return typeof val === "number" && val > 0 ? Math.floor(val) : undefined;
} catch {
return undefined;
}
}
}
import { ParsedConfig } from "@lmstudio/sdk";
import { ImageProviderRegistry } from "../../image_search/providers/ImageProviderRegistry";
import { ProviderRegistry } from "../../providers/ProviderRegistry";
import { IConfigurationSource } from "../IConfigurationSource";
import { PrioritySlotEditor } from "../ui/PrioritySlotEditor";
export type AnyParsedConfig = ParsedConfig<any>;
/**
* Configuration source reading settings from LM Studio SDK's PluginConfig settings panel.
* Uses PrioritySlotEditor to normalize numeric IDs (1,2,3) or names into string[] priority arrays at the UI boundary.
* Supports dual global (Normal) and per-chat configuration schematics.
*/
export class LMStudioConfigurationSource implements IConfigurationSource {
private globalPluginConfig?: AnyParsedConfig;
private perChatPluginConfig?: AnyParsedConfig;
private webEditor: PrioritySlotEditor;
private imageEditor: PrioritySlotEditor;
constructor(globalConfig?: AnyParsedConfig, perChatConfig?: AnyParsedConfig) {
this.globalPluginConfig = globalConfig;
this.perChatPluginConfig = perChatConfig;
const webOptions = ProviderRegistry.getAllSpecs().map((s) => ({
id: s.id,
value: s.name,
displayName: String(s.displayName || (s as any).display_name || s.name),
}));
this.webEditor = new PrioritySlotEditor(webOptions);
const imageOptions = ImageProviderRegistry.getAllSpecs().map((s) => ({
id: s.id,
value: s.name,
displayName: String(s.displayName || (s as any).display_name || s.name),
}));
this.imageEditor = new PrioritySlotEditor(imageOptions);
}
public setPluginConfig(pluginConfig: ParsedConfig<any>): void {
this.globalPluginConfig = pluginConfig;
}
public setConfigs(globalConfig?: AnyParsedConfig, perChatConfig?: AnyParsedConfig): void {
this.globalPluginConfig = globalConfig;
this.perChatPluginConfig = perChatConfig;
}
private getActiveConfig(): AnyParsedConfig | undefined {
return this.perChatPluginConfig ?? this.globalPluginConfig;
}
public getWebDefaultProvider(): string | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("webDefaultProvider") as string;
return val && val !== "auto" ? val.trim().toLowerCase() : undefined;
} catch {
return undefined;
}
}
public getWebProviderPriority(): string[] | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
// 1. Check dropdown slots first (Primary & Fallback)
const chosenSlots: string[] = [];
for (const key of ["webPrioritySlot1", "webPrioritySlot2"]) {
try {
const val = (config.get(key as any) as string)?.trim()?.toLowerCase();
if (val && val !== "none") {
chosenSlots.push(val);
}
} catch {
// Slot field not present
}
}
// If user chose at least one dropdown option (other than "none"), dropdowns take precedence
if (chosenSlots.length > 0) {
return this.webEditor.normalizeOrder(chosenSlots);
}
// 2. Both dropdowns are "none" -> follow Custom Web Priority Order text box
try {
const customVal = config.get("webProviderPriority" as any) as string;
if (customVal && customVal.trim()) {
return this.webEditor.normalizeOrder(customVal);
}
} catch {
// webProviderPriority not present
}
return undefined;
}
private static cachedApiKeys: Map<string, string[]> = new Map();
public getProviderKeys(providerName: string, category?: string): string[] | undefined {
const config = this.globalPluginConfig ?? this.perChatPluginConfig;
const norm = providerName.toLowerCase().trim();
const cacheKey = category ? `${category.toLowerCase()}:${norm}` : norm;
if (config) {
const candidateKeys = [
...(category ? [`${category.toLowerCase()}.${norm}Keys`, `${category.toLowerCase()}_${norm}Keys`] : []),
`${norm}Keys`,
];
for (const fieldKey of candidateKeys) {
try {
const val = config.get(fieldKey as any) as string;
if (val && val.trim()) {
const keys = val.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
if (keys.length > 0) {
LMStudioConfigurationSource.cachedApiKeys.set(cacheKey, keys);
return keys;
}
}
} catch {
// Key field not present in schematics map under this candidate key
}
}
}
return LMStudioConfigurationSource.cachedApiKeys.get(cacheKey);
}
public getImageDefaultProvider(): string | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("imageDefaultProvider") as string;
return val && val !== "auto" ? val.trim().toLowerCase() : undefined;
} catch {
return undefined;
}
}
public getImageProviderPriority(): string[] | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
// 1. Check dropdown slots first (Primary & Fallback)
const chosenSlots: string[] = [];
for (const key of ["imagePrioritySlot1", "imagePrioritySlot2"]) {
try {
const val = (config.get(key as any) as string)?.trim()?.toLowerCase();
if (val && val !== "none") {
chosenSlots.push(val);
}
} catch {
// Slot field not present
}
}
// If user chose at least one dropdown option (other than "none"), dropdowns take precedence
if (chosenSlots.length > 0) {
return this.imageEditor.normalizeOrder(chosenSlots);
}
// 2. Both dropdowns are "none" -> follow Custom Image Priority Order text box
try {
const customVal = config.get("imageProviderPriority" as any) as string;
if (customVal && customVal.trim()) {
return this.imageEditor.normalizeOrder(customVal);
}
} catch {
// imageProviderPriority not present
}
return undefined;
}
public getHttpTimeoutSeconds(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("httpTimeoutSeconds") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getQuotaCooldownHours(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("quotaCooldownHours") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getSearchMaxSnippetLength(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxSnippetLength") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getSearchMaxPayloadCharacters(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxPayloadCharacters") as number;
return typeof val === "number" && val > 0 ? val : undefined;
} catch {
return undefined;
}
}
public getPrependTimeContext(): boolean | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("prependTimeContext") as boolean;
return typeof val === "boolean" ? val : undefined;
} catch {
return undefined;
}
}
public getPrependSearchImagesContext(): boolean | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("prependSearchImagesContext") as boolean;
return typeof val === "boolean" ? val : undefined;
} catch {
return undefined;
}
}
public getPrependVisionContext(): boolean | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("prependVisionContext") as boolean;
return typeof val === "boolean" ? val : undefined;
} catch {
return undefined;
}
}
public getMaxImageResults(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxImages" as any) as number;
return typeof val === "number" && val > 0 ? Math.floor(val) : undefined;
} catch {
return undefined;
}
}
public getMaxWebResults(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("searchMaxWebResults" as any) as number;
return typeof val === "number" && val > 0 ? Math.floor(val) : undefined;
} catch {
return undefined;
}
}
public getReadWebpageChunkSize(): number | undefined {
const config = this.getActiveConfig();
if (!config) return undefined;
try {
const val = config.get("readWebpageChunkSize" as any) as number;
return typeof val === "number" && val > 0 ? Math.floor(val) : undefined;
} catch {
return undefined;
}
}
}