src / generator.ts
src / generator.ts
import type { GeneratorController, Chat } from "@lmstudio/sdk";
import { configSchematics, globalConfigSchematics } from "./config";
import { toMessages } from "./messages";
import { consumeStream } from "./stream";
import { ToolBridge } from "./toolBridge";
import { completeDeferredAnalyses } from "./analysisHandoff";
import { isTitleRequest, generateTitle } from "./titleGeneration";
export function completionUrl(input: string): string {
const url = new URL(input.trim());
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
throw new Error("Base URL must be an HTTP(S) address without credentials, query or fragment.");
}
const path = url.pathname.replace(/\/+$/, "");
url.pathname = path.endsWith("/chat/completions") ? path : `${path.endsWith("/v1") ? path : `${path}/v1`}/chat/completions`;
return url.toString();
}
export async function generate(ctl: GeneratorController, history: Chat): Promise<void> {
const config = ctl.getPluginConfig(configSchematics);
const global = ctl.getGlobalPluginConfig(globalConfigSchematics);
const model = config.get("model").trim();
if (!model) throw new Error("Set the Strata model ID.");
const endpoint = completionUrl(global.get("baseUrl"));
const key = global.get("apiKey").trim();
const headers = { "Content-Type": "application/json", ...(key ? { Authorization: `Bearer ${key}` } : {}) };
if (isTitleRequest(history)) {
await generateTitle(ctl, history, endpoint, model, headers);
return;
}
const bridge = new ToolBridge(ctl.getToolDefinitions());
const tools = bridge.tools;
const messages = await completeDeferredAnalyses(
history, await toMessages(history, ctl.client, ctl.abortSignal, {
getSnapshotRoot: () => ctl.getWorkingDirectory(), warn: message => console.warn("[Strata]", message),
}), () => ctl.getWorkingDirectory(), ctl.abortSignal,
async (imageUrl, prompt) => {
const response = await fetch(endpoint, {
method: "POST", headers, signal: ctl.abortSignal, redirect: "error",
body: JSON.stringify({ model, stream: false, reasoning_effort: "none", max_tokens: 2048,
messages: [{ role: "user", content: [
{ type: "text", text: prompt }, { type: "image_url", image_url: { url: imageUrl } },
] }],
}),
});
if (!response.ok) {
await response.body?.cancel();
throw new Error(`Strata image-analysis subrequest failed: HTTP ${response.status}.`);
}
const result = await response.json() as { error?: { message?: string }; choices?: Array<{ message?: { content?: unknown } }> };
if (result.error) throw new Error(`Strata image-analysis subrequest failed: ${result.error.message ?? "unknown error"}`);
const content = result.choices?.[0]?.message?.content;
if (typeof content !== "string" || !content.trim()) throw new Error("Strata image-analysis subrequest returned no answer.");
return content;
},
);
const payload = {
model,
messages: bridge.mapHistory(messages),
stream: true,
...(tools.length ? { tools, parallel_tool_calls: false } : {}),
...(config.get("thinking") !== "default" ? { reasoning_effort: config.get("thinking") } : {}),
...(config.get("maxTokens") > 0 ? { max_tokens: config.get("maxTokens") } : {}),
...(config.get("thinkingBudget") > 0 ? { reasoning_budget_tokens: config.get("thinkingBudget") } : {}),
};
const response = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify(payload),
signal: ctl.abortSignal,
redirect: "error",
});
if (!response.ok) {
await response.body?.cancel();
throw new Error(`Strata HTTP ${response.status}. Check Base URL, API key, model and vision setup in Strata.`);
}
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
await response.body?.cancel();
throw new Error("Strata did not return an SSE stream (text/event-stream). Check the endpoint.");
}
await consumeStream(response.body, ctl, bridge);
}
import type { GeneratorController, Chat } from "@lmstudio/sdk";
import { configSchematics, globalConfigSchematics } from "./config";
import { toMessages } from "./messages";
import { consumeStream } from "./stream";
import { ToolBridge } from "./toolBridge";
import { completeDeferredAnalyses } from "./analysisHandoff";
import { isTitleRequest, generateTitle } from "./titleGeneration";
export function completionUrl(input: string): string {
const url = new URL(input.trim());
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
throw new Error("Base URL must be an HTTP(S) address without credentials, query or fragment.");
}
const path = url.pathname.replace(/\/+$/, "");
url.pathname = path.endsWith("/chat/completions") ? path : `${path.endsWith("/v1") ? path : `${path}/v1`}/chat/completions`;
return url.toString();
}
export async function generate(ctl: GeneratorController, history: Chat): Promise<void> {
const config = ctl.getPluginConfig(configSchematics);
const global = ctl.getGlobalPluginConfig(globalConfigSchematics);
const model = config.get("model").trim();
if (!model) throw new Error("Set the Strata model ID.");
const endpoint = completionUrl(global.get("baseUrl"));
const key = global.get("apiKey").trim();
const headers = { "Content-Type": "application/json", ...(key ? { Authorization: `Bearer ${key}` } : {}) };
if (isTitleRequest(history)) {
await generateTitle(ctl, history, endpoint, model, headers);
return;
}
const bridge = new ToolBridge(ctl.getToolDefinitions());
const tools = bridge.tools;
const messages = await completeDeferredAnalyses(
history, await toMessages(history, ctl.client, ctl.abortSignal, {
getSnapshotRoot: () => ctl.getWorkingDirectory(), warn: message => console.warn("[Strata]", message),
}), () => ctl.getWorkingDirectory(), ctl.abortSignal,
async (imageUrl, prompt) => {
const response = await fetch(endpoint, {
method: "POST", headers, signal: ctl.abortSignal, redirect: "error",
body: JSON.stringify({ model, stream: false, reasoning_effort: "none", max_tokens: 2048,
messages: [{ role: "user", content: [
{ type: "text", text: prompt }, { type: "image_url", image_url: { url: imageUrl } },
] }],
}),
});
if (!response.ok) {
await response.body?.cancel();
throw new Error(`Strata image-analysis subrequest failed: HTTP ${response.status}.`);
}
const result = await response.json() as { error?: { message?: string }; choices?: Array<{ message?: { content?: unknown } }> };
if (result.error) throw new Error(`Strata image-analysis subrequest failed: ${result.error.message ?? "unknown error"}`);
const content = result.choices?.[0]?.message?.content;
if (typeof content !== "string" || !content.trim()) throw new Error("Strata image-analysis subrequest returned no answer.");
return content;
},
);
const payload = {
model,
messages: bridge.mapHistory(messages),
stream: true,
...(tools.length ? { tools, parallel_tool_calls: false } : {}),
...(config.get("thinking") !== "default" ? { reasoning_effort: config.get("thinking") } : {}),
...(config.get("maxTokens") > 0 ? { max_tokens: config.get("maxTokens") } : {}),
...(config.get("thinkingBudget") > 0 ? { reasoning_budget_tokens: config.get("thinkingBudget") } : {}),
};
const response = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify(payload),
signal: ctl.abortSignal,
redirect: "error",
});
if (!response.ok) {
await response.body?.cancel();
throw new Error(`Strata HTTP ${response.status}. Check Base URL, API key, model and vision setup in Strata.`);
}
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
await response.body?.cancel();
throw new Error("Strata did not return an SSE stream (text/event-stream). Check the endpoint.");
}
await consumeStream(response.body, ctl, bridge);
}