src / config / configSchematics.ts
src / config / configSchematics.ts
import { createConfigSchematics } from "@lmstudio/sdk";
import { ImageProviderRegistry } from "../image_search/providers/ImageProviderRegistry";
import { ProviderRegistry } from "../providers/ProviderRegistry";
import {
DEFAULT_READ_WEBPAGE_CHUNK_SIZE,
DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS,
DEFAULT_SEARCH_MAX_SNIPPET_LENGTH,
} from "./Config";
/**
* Dynamically builds LM Studio plugin config schematics based on ProviderRegistry and ImageProviderRegistry metadata.
* Controls order: Default Web -> Web Priority -> Web API Keys -> Default Image -> Image Priority -> Image API Keys -> Performance -> Advanced -> Features -> Freeze
*/
export function buildConfigSchematics() {
const webSpecs = ProviderRegistry.getAllSpecs();
const imageSpecs = ImageProviderRegistry.getAllSpecs();
let schematics = createConfigSchematics();
// 1. Web Search Settings (Default & Priority)
const webOptions = [
{ value: "auto", displayName: "⚡ Auto (Priority Order)" },
...webSpecs.map((spec) => ({
value: spec.name,
displayName: `\u00A0\u00A0${spec.id} · ${spec.displayName || (spec as any).display_name || spec.name}${spec.requiresKey ? " ᴬᴾᴵ" : ""}`,
})),
];
schematics = schematics.field(
"webDefaultProvider",
"select",
{
displayName: "🌐 == 𝗗𝗲𝗳𝗮𝘂𝗹𝘁 𝗪𝗲𝗯 𝗣𝗿𝗼𝘃𝗶𝗱𝗲𝗿 ==",
hint: "`⚡ Auto (Priority Order)` : Automatically falls back according to the Provider Priority Order when one or more providers fail. If a provider has multiple keys, failover happens among those keys first before jumping to the next provider.\n`Specific Provider` : Only the selected provider is used (still fails over among its own keys). No fallback to other providers occurs.",
options: webOptions,
},
"auto"
);
const webLegend = webSpecs
.map((s) => `${s.id}=${s.displayName || (s as any).display_name || s.name}${s.requiresKey ? " ᴬᴾᴵ" : ""}`)
.join(" | ");
const defaultWebPriorityStr = webSpecs.map((s) => s.id).join(", ");
schematics = schematics.field(
"webProviderPriority",
"string",
{
displayName: "🌐 Web Providers Priority Order",
hint: `Priority order is only used when \`⚡ Auto (Priority Order)\` is selected.\n\n Available Providers : ${webLegend}`,
},
defaultWebPriorityStr
);
// Provider URL mapping for API key hints
const PROVIDER_URLS: Record<string, string> = {
exa: "https://exa.ai",
tavily: "https://tavily.com",
serper: "https://serper.dev",
brave: "https://brave.com/search/api",
firecrawl: "https://www.firecrawl.dev",
serpapi: "https://serpapi.com",
pexels: "https://www.pexels.com/api",
};
const formatProviderHint = (url: string): string => {
if (!url) {
return "Put a single comma (,) in between two or more API keys for failover rotation. You can put spaces before or after the comma for better readability.";
}
const containerWidth = 38;
const literalSpaces = Math.max(0, Math.round((containerWidth - url.length) / 2));
const totalSpaces = 4 + literalSpaces;
return `${" ".repeat(totalSpaces)}${url}\nPut a single comma (,) in between two or more API keys for failover rotation. You can put spaces before or after the comma for better readability.`;
};
// 2. Web Search API Keys (directly below Web Priority Order)
for (const spec of webSpecs) {
const envVarName = spec.envVar || (spec as any).env_var;
if (envVarName) {
const fieldKey = `${spec.name}Keys`;
const dispName = String(spec.displayName || (spec as any).display_name || spec.name);
const url = PROVIDER_URLS[spec.name.toLowerCase()] || "";
schematics = schematics.field(
fieldKey,
"string",
{
displayName: `🔑 ${spec.id} · ${dispName} API Key(s)`,
hint: formatProviderHint(url),
placeholder: "Enter API key(s)...",
isProtected: true,
},
""
);
}
}
// 3. Image Search Settings (Default & Priority)
const imageOptions = [
{ value: "auto", displayName: "⚡ Auto (Priority Order)" },
...imageSpecs.map((spec) => ({
value: spec.name,
displayName: `\u00A0\u00A0${spec.id} · ${spec.displayName || (spec as any).display_name || spec.name}${spec.requiresKey ? " ᴬᴾᴵ" : ""}`,
})),
];
schematics = schematics.field(
"imageDefaultProvider",
"select",
{
displayName: "🖼️ == 𝗗𝗲𝗳𝗮𝘂𝗹𝘁 𝗜𝗺𝗮𝗴𝗲 𝗣𝗿𝗼𝘃𝗶𝗱𝗲𝗿 ==",
hint: "`⚡ Auto (Priority Order)` : Automatically falls back according to the Provider Priority Order when one or more providers fail. If a provider has multiple keys, failover happens among those keys first before jumping to the next provider.\n`Specific Provider` : Only the selected provider is used (still fails over among its own keys). No fallback to other providers occurs.",
options: imageOptions,
},
"auto"
);
const imageLegend = imageSpecs
.map((s) => `${s.id}=${s.displayName || (s as any).display_name || s.name}${s.requiresKey ? " ᴬᴾᴵ" : ""}`)
.join(" | ");
const defaultImagePriorityStr = imageSpecs.map((s) => s.id).join(", ");
schematics = schematics.field(
"imageProviderPriority",
"string",
{
displayName: "🖼️ Image Providers Priority Order",
hint: `Priority order is only used when \`⚡ Auto (Priority Order)\` is selected.\n\n Available Providers : ${imageLegend}`,
},
defaultImagePriorityStr
);
// 4. Image Search API Keys (directly below Image Priority Order)
for (const spec of imageSpecs) {
if (spec.requiresKey && spec.envVar) {
const fieldKey = `${spec.name}Keys`;
const dispName = String(spec.displayName || (spec as any).display_name || spec.name);
const url = PROVIDER_URLS[spec.name.toLowerCase()] || "";
schematics = schematics.field(
fieldKey,
"string",
{
displayName: `🔑 ${spec.id} · ${dispName} API Key(s)`,
hint: formatProviderHint(url),
placeholder: "Enter API key(s)...",
isProtected: true,
},
""
);
}
}
// 5. Performance Settings
schematics = schematics
.field(
"httpTimeoutSeconds",
"numeric",
{
displayName: "⚡ HTTP Timeout (sec)",
hint: "Maximum time to wait for an HTTP response before cancelling the request.",
min: 1,
max: 300,
int: true,
},
30
)
.field(
"quotaCooldownHours",
"numeric",
{
displayName: "⚡ Quota Cooldown (hrs)",
hint: "When an API key reaches its quota or rate limit (HTTP 403/429), it is skipped for this duration before becoming available to use again.",
min: 0.1,
max: 720,
},
24
);
// 6. Advanced Settings
schematics = schematics
.field(
"searchMaxSnippetLength",
"numeric",
{
displayName: "⚙️ Max Snippet Size (chars)",
hint: "Maximum number of characters allowed for each web search result snippet.",
min: 50,
max: 5000,
int: true,
},
DEFAULT_SEARCH_MAX_SNIPPET_LENGTH
)
.field(
"searchMaxPayloadCharacters",
"numeric",
{
displayName: "⚙️ Max Payload Size (chars)",
hint: "Maximum total number of characters allowed for a single web search response.",
min: 300,
max: 100000,
int: true,
},
DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS
)
.field(
"searchMaxWebResults",
"numeric",
{
displayName: "⚙️ Max Web Results (1-30)",
hint: "Maximum number of web search results returned by the provider for each search request.",
min: 1,
max: 30,
int: true,
},
10
)
.field(
"searchMaxImages",
"numeric",
{
displayName: "⚙️ Max Image Results (1-30)",
hint: "Maximum number of images returned by the provider for each search request when the LLM does not explicitly request a specific number of images.",
min: 1,
max: 30,
int: true,
},
10
)
.field(
"readWebpageChunkSize",
"numeric",
{
displayName: "⚙️ Max Chunk Size (chars)",
hint: "Maximum number of characters `Read Webpage` feeds to the LLM in one response.",
min: 1,
max: 100000,
int: true,
},
DEFAULT_READ_WEBPAGE_CHUNK_SIZE
);
// 7. Features Settings
schematics = schematics
.field(
"prependTimeContext",
"boolean",
{
displayName: "🧠 Prepend Time Context",
hint: "Adds a reminder to use `Get Current Time` tool for time-sensitive queries.",
},
true
)
.field(
"prependSearchImagesContext",
"boolean",
{
displayName: "🧠 Prepend Image Context",
hint: "Adds a reminder to use the `Image Search` tool for concept explanation queries with images or diagrams, such as “Explain this with images or diagrams.” LLMs often overlook the image requirement part, so this setting adds a reminder. Keep it off if you don't need this behavior for such queries.",
},
true
)
.field(
"prependVisionContext",
"boolean",
{
displayName: "🧠 Prepend Vision Context",
hint: " Vision Capability Needed\nAdds a reminder to use the `Inspect Image` tool when you ask to analyze, explain, describe, or interpret images already present in the chat. This reminds the LLM to use the tool properly. Keep it off if you rarely ask for image analysis.",
},
true
);
return schematics.build();
}
export const configSchematics = buildConfigSchematics();
export const globalConfigSchematics = configSchematics;
export const perChatConfigSchematics = configSchematics;
import { createConfigSchematics } from "@lmstudio/sdk";
import { ImageProviderRegistry } from "../image_search/providers/ImageProviderRegistry";
import { ProviderRegistry } from "../providers/ProviderRegistry";
import {
DEFAULT_READ_WEBPAGE_CHUNK_SIZE,
DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS,
DEFAULT_SEARCH_MAX_SNIPPET_LENGTH,
} from "./Config";
/**
* Dynamically builds LM Studio plugin config schematics based on ProviderRegistry and ImageProviderRegistry metadata.
* Controls order: Default Web -> Web Priority -> Web API Keys -> Default Image -> Image Priority -> Image API Keys -> Performance -> Advanced -> Features -> Freeze
*/
export function buildConfigSchematics() {
const webSpecs = ProviderRegistry.getAllSpecs();
const imageSpecs = ImageProviderRegistry.getAllSpecs();
let schematics = createConfigSchematics();
// 1. Web Search Settings (Default & Priority)
const webOptions = [
{ value: "auto", displayName: "⚡ Auto (Priority Order)" },
...webSpecs.map((spec) => ({
value: spec.name,
displayName: `\u00A0\u00A0${spec.id} · ${spec.displayName || (spec as any).display_name || spec.name}${spec.requiresKey ? " ᴬᴾᴵ" : ""}`,
})),
];
schematics = schematics.field(
"webDefaultProvider",
"select",
{
displayName: "🌐 == 𝗗𝗲𝗳𝗮𝘂𝗹𝘁 𝗪𝗲𝗯 𝗣𝗿𝗼𝘃𝗶𝗱𝗲𝗿 ==",
hint: "`⚡ Auto (Priority Order)` : Automatically falls back according to the Provider Priority Order when one or more providers fail. If a provider has multiple keys, failover happens among those keys first before jumping to the next provider.\n`Specific Provider` : Only the selected provider is used (still fails over among its own keys). No fallback to other providers occurs.",
options: webOptions,
},
"auto"
);
const webLegend = webSpecs
.map((s) => `${s.id}=${s.displayName || (s as any).display_name || s.name}${s.requiresKey ? " ᴬᴾᴵ" : ""}`)
.join(" | ");
const defaultWebPriorityStr = webSpecs.map((s) => s.id).join(", ");
schematics = schematics.field(
"webProviderPriority",
"string",
{
displayName: "🌐 Web Providers Priority Order",
hint: `Priority order is only used when \`⚡ Auto (Priority Order)\` is selected.\n\n Available Providers : ${webLegend}`,
},
defaultWebPriorityStr
);
// Provider URL mapping for API key hints
const PROVIDER_URLS: Record<string, string> = {
exa: "https://exa.ai",
tavily: "https://tavily.com",
serper: "https://serper.dev",
brave: "https://brave.com/search/api",
firecrawl: "https://www.firecrawl.dev",
serpapi: "https://serpapi.com",
pexels: "https://www.pexels.com/api",
};
const formatProviderHint = (url: string): string => {
if (!url) {
return "Put a single comma (,) in between two or more API keys for failover rotation. You can put spaces before or after the comma for better readability.";
}
const containerWidth = 38;
const literalSpaces = Math.max(0, Math.round((containerWidth - url.length) / 2));
const totalSpaces = 4 + literalSpaces;
return `${" ".repeat(totalSpaces)}${url}\nPut a single comma (,) in between two or more API keys for failover rotation. You can put spaces before or after the comma for better readability.`;
};
// 2. Web Search API Keys (directly below Web Priority Order)
for (const spec of webSpecs) {
const envVarName = spec.envVar || (spec as any).env_var;
if (envVarName) {
const fieldKey = `${spec.name}Keys`;
const dispName = String(spec.displayName || (spec as any).display_name || spec.name);
const url = PROVIDER_URLS[spec.name.toLowerCase()] || "";
schematics = schematics.field(
fieldKey,
"string",
{
displayName: `🔑 ${spec.id} · ${dispName} API Key(s)`,
hint: formatProviderHint(url),
placeholder: "Enter API key(s)...",
isProtected: true,
},
""
);
}
}
// 3. Image Search Settings (Default & Priority)
const imageOptions = [
{ value: "auto", displayName: "⚡ Auto (Priority Order)" },
...imageSpecs.map((spec) => ({
value: spec.name,
displayName: `\u00A0\u00A0${spec.id} · ${spec.displayName || (spec as any).display_name || spec.name}${spec.requiresKey ? " ᴬᴾᴵ" : ""}`,
})),
];
schematics = schematics.field(
"imageDefaultProvider",
"select",
{
displayName: "🖼️ == 𝗗𝗲𝗳𝗮𝘂𝗹𝘁 𝗜𝗺𝗮𝗴𝗲 𝗣𝗿𝗼𝘃𝗶𝗱𝗲𝗿 ==",
hint: "`⚡ Auto (Priority Order)` : Automatically falls back according to the Provider Priority Order when one or more providers fail. If a provider has multiple keys, failover happens among those keys first before jumping to the next provider.\n`Specific Provider` : Only the selected provider is used (still fails over among its own keys). No fallback to other providers occurs.",
options: imageOptions,
},
"auto"
);
const imageLegend = imageSpecs
.map((s) => `${s.id}=${s.displayName || (s as any).display_name || s.name}${s.requiresKey ? " ᴬᴾᴵ" : ""}`)
.join(" | ");
const defaultImagePriorityStr = imageSpecs.map((s) => s.id).join(", ");
schematics = schematics.field(
"imageProviderPriority",
"string",
{
displayName: "🖼️ Image Providers Priority Order",
hint: `Priority order is only used when \`⚡ Auto (Priority Order)\` is selected.\n\n Available Providers : ${imageLegend}`,
},
defaultImagePriorityStr
);
// 4. Image Search API Keys (directly below Image Priority Order)
for (const spec of imageSpecs) {
if (spec.requiresKey && spec.envVar) {
const fieldKey = `${spec.name}Keys`;
const dispName = String(spec.displayName || (spec as any).display_name || spec.name);
const url = PROVIDER_URLS[spec.name.toLowerCase()] || "";
schematics = schematics.field(
fieldKey,
"string",
{
displayName: `🔑 ${spec.id} · ${dispName} API Key(s)`,
hint: formatProviderHint(url),
placeholder: "Enter API key(s)...",
isProtected: true,
},
""
);
}
}
// 5. Performance Settings
schematics = schematics
.field(
"httpTimeoutSeconds",
"numeric",
{
displayName: "⚡ HTTP Timeout (sec)",
hint: "Maximum time to wait for an HTTP response before cancelling the request.",
min: 1,
max: 300,
int: true,
},
30
)
.field(
"quotaCooldownHours",
"numeric",
{
displayName: "⚡ Quota Cooldown (hrs)",
hint: "When an API key reaches its quota or rate limit (HTTP 403/429), it is skipped for this duration before becoming available to use again.",
min: 0.1,
max: 720,
},
24
);
// 6. Advanced Settings
schematics = schematics
.field(
"searchMaxSnippetLength",
"numeric",
{
displayName: "⚙️ Max Snippet Size (chars)",
hint: "Maximum number of characters allowed for each web search result snippet.",
min: 50,
max: 5000,
int: true,
},
DEFAULT_SEARCH_MAX_SNIPPET_LENGTH
)
.field(
"searchMaxPayloadCharacters",
"numeric",
{
displayName: "⚙️ Max Payload Size (chars)",
hint: "Maximum total number of characters allowed for a single web search response.",
min: 300,
max: 100000,
int: true,
},
DEFAULT_SEARCH_MAX_PAYLOAD_CHARACTERS
)
.field(
"searchMaxWebResults",
"numeric",
{
displayName: "⚙️ Max Web Results (1-30)",
hint: "Maximum number of web search results returned by the provider for each search request.",
min: 1,
max: 30,
int: true,
},
10
)
.field(
"searchMaxImages",
"numeric",
{
displayName: "⚙️ Max Image Results (1-30)",
hint: "Maximum number of images returned by the provider for each search request when the LLM does not explicitly request a specific number of images.",
min: 1,
max: 30,
int: true,
},
10
)
.field(
"readWebpageChunkSize",
"numeric",
{
displayName: "⚙️ Max Chunk Size (chars)",
hint: "Maximum number of characters `Read Webpage` feeds to the LLM in one response.",
min: 1,
max: 100000,
int: true,
},
DEFAULT_READ_WEBPAGE_CHUNK_SIZE
);
// 7. Features Settings
schematics = schematics
.field(
"prependTimeContext",
"boolean",
{
displayName: "🧠 Prepend Time Context",
hint: "Adds a reminder to use `Get Current Time` tool for time-sensitive queries.",
},
true
)
.field(
"prependSearchImagesContext",
"boolean",
{
displayName: "🧠 Prepend Image Context",
hint: "Adds a reminder to use the `Image Search` tool for concept explanation queries with images or diagrams, such as “Explain this with images or diagrams.” LLMs often overlook the image requirement part, so this setting adds a reminder. Keep it off if you don't need this behavior for such queries.",
},
true
)
.field(
"prependVisionContext",
"boolean",
{
displayName: "🧠 Prepend Vision Context",
hint: " Vision Capability Needed\nAdds a reminder to use the `Inspect Image` tool when you ask to analyze, explain, describe, or interpret images already present in the chat. This reminds the LLM to use the tool properly. Keep it off if you rarely ask for image analysis.",
},
true
);
return schematics.build();
}
export const configSchematics = buildConfigSchematics();
export const globalConfigSchematics = configSchematics;
export const perChatConfigSchematics = configSchematics;