reworked advanced mode and token parser to support Ollama

This commit is contained in:
2026-01-18 14:10:41 -05:00
parent 62f82fe2e3
commit 2d208784b4
3 changed files with 109 additions and 81 deletions

View File

@@ -10,8 +10,6 @@ const DEFAULT_SETTINGS = {
activeEnvConfigId: "", activeEnvConfigId: "",
profiles: [], profiles: [],
apiBaseUrl: "https://api.openai.com/v1", apiBaseUrl: "https://api.openai.com/v1",
apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer ",
model: "gpt-5.2", model: "gpt-5.2",
systemPrompt: "", systemPrompt: "",
tasks: DEFAULT_TASKS, tasks: DEFAULT_TASKS,
@@ -25,6 +23,8 @@ const DEFAULT_SETTINGS = {
const OUTPUT_STORAGE_KEY = "lastOutput"; const OUTPUT_STORAGE_KEY = "lastOutput";
const AUTO_RUN_KEY = "autoRunDefaultTask"; const AUTO_RUN_KEY = "autoRunDefaultTask";
const SHORTCUT_RUN_KEY = "runShortcutId"; const SHORTCUT_RUN_KEY = "runShortcutId";
const DEFAULT_API_KEY_HEADER = "Authorization";
const DEFAULT_API_KEY_PREFIX = "Bearer ";
let activeAbortController = null; let activeAbortController = null;
let keepalivePort = null; let keepalivePort = null;
const streamState = { const streamState = {
@@ -112,8 +112,6 @@ chrome.runtime.onInstalled.addListener(async () => {
id, id,
name: "Default", name: "Default",
apiBaseUrl: stored.apiBaseUrl || DEFAULT_SETTINGS.apiBaseUrl, apiBaseUrl: stored.apiBaseUrl || DEFAULT_SETTINGS.apiBaseUrl,
apiKeyHeader: stored.apiKeyHeader || DEFAULT_SETTINGS.apiKeyHeader,
apiKeyPrefix: stored.apiKeyPrefix || DEFAULT_SETTINGS.apiKeyPrefix,
model: stored.model || DEFAULT_SETTINGS.model, model: stored.model || DEFAULT_SETTINGS.model,
apiKeyId: fallbackKeyId, apiKeyId: fallbackKeyId,
apiUrl: "", apiUrl: "",
@@ -409,6 +407,12 @@ async function handleAnalysisRequest(port, payload, signal) {
} = payload || {}; } = payload || {};
const isAdvanced = apiMode === "advanced"; const isAdvanced = apiMode === "advanced";
const resolvedApiKeyHeader = isAdvanced
? ""
: apiKeyHeader || DEFAULT_API_KEY_HEADER;
const resolvedApiKeyPrefix = isAdvanced
? ""
: apiKeyPrefix ?? DEFAULT_API_KEY_PREFIX;
if (isAdvanced) { if (isAdvanced) {
if (!apiUrl) { if (!apiUrl) {
safePost(port, { type: "ERROR", message: "Missing API URL." }); safePost(port, { type: "ERROR", message: "Missing API URL." });
@@ -420,7 +424,7 @@ async function handleAnalysisRequest(port, payload, signal) {
return; return;
} }
if (apiKeyHeader && !apiKey) { if (resolvedApiKeyHeader && !apiKey) {
safePost(port, { type: "ERROR", message: "Missing API key." }); safePost(port, { type: "ERROR", message: "Missing API key." });
return; return;
} }
@@ -456,8 +460,8 @@ async function handleAnalysisRequest(port, payload, signal) {
apiKey, apiKey,
apiUrl, apiUrl,
requestTemplate, requestTemplate,
apiKeyHeader, apiKeyHeader: resolvedApiKeyHeader,
apiKeyPrefix, apiKeyPrefix: resolvedApiKeyPrefix,
apiBaseUrl, apiBaseUrl,
model, model,
systemPrompt: systemPrompt || "", systemPrompt: systemPrompt || "",
@@ -472,8 +476,8 @@ async function handleAnalysisRequest(port, payload, signal) {
await streamChatCompletion({ await streamChatCompletion({
apiKey, apiKey,
apiBaseUrl, apiBaseUrl,
apiKeyHeader, apiKeyHeader: resolvedApiKeyHeader,
apiKeyPrefix, apiKeyPrefix: resolvedApiKeyPrefix,
model, model,
systemPrompt: systemPrompt || "", systemPrompt: systemPrompt || "",
userMessage, userMessage,
@@ -536,16 +540,42 @@ function buildTemplateBody(template, replacements) {
try { try {
return JSON.parse(filled); return JSON.parse(filled);
} catch { } catch {
throw new Error("Invalid request template JSON."); throw new Error("Invalid request template JSON." + filled);
} }
} }
function extractStreamDelta(parsed) {
if (!parsed) return "";
const openAiDelta = parsed?.choices?.[0]?.delta?.content;
if (openAiDelta) return openAiDelta;
const openAiMessage = parsed?.choices?.[0]?.message?.content;
if (openAiMessage) return openAiMessage;
const ollamaMessage = parsed?.message?.content;
if (ollamaMessage) return ollamaMessage;
if (typeof parsed?.response === "string") return parsed.response;
if (typeof parsed?.content === "string") return parsed.content;
return "";
}
function parseStreamLine(line) {
const trimmed = line.trim();
if (!trimmed) return null;
if (trimmed.startsWith("event:") || trimmed.startsWith("id:")) {
return null;
}
if (trimmed.startsWith("data:")) {
const data = trimmed.slice(5).trim();
return data || null;
}
return trimmed;
}
async function readSseStream(response, onDelta) { async function readSseStream(response, onDelta) {
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ""; let buffer = "";
// OpenAI-compatible SSE stream; parse incremental deltas from data lines. // OpenAI-compatible SSE or newline-delimited JSON streaming.
while (true) { while (true) {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) break; if (done) break;
@@ -555,24 +585,33 @@ async function readSseStream(response, onDelta) {
buffer = lines.pop() || ""; buffer = lines.pop() || "";
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const payload = parseStreamLine(line);
if (!trimmed.startsWith("data:")) continue; if (!payload) continue;
if (payload === "[DONE]") return;
const data = trimmed.slice(5).trim();
if (!data) continue;
if (data === "[DONE]") return;
let parsed; let parsed;
try { try {
parsed = JSON.parse(data); parsed = JSON.parse(payload);
} catch { } catch {
continue; continue;
} }
const delta = parsed?.choices?.[0]?.delta?.content; const delta = extractStreamDelta(parsed);
if (delta) onDelta(delta); if (delta) onDelta(delta);
if (parsed?.done === true) return;
} }
} }
const tail = parseStreamLine(buffer);
if (!tail) return;
if (tail === "[DONE]") return;
try {
const parsed = JSON.parse(tail);
const delta = extractStreamDelta(parsed);
if (delta) onDelta(delta);
} catch {
// Ignore trailing parse failures.
}
} }
async function streamChatCompletion({ async function streamChatCompletion({
@@ -636,8 +675,8 @@ async function streamCustomCompletion({
onDelta onDelta
}) { }) {
const replacements = { const replacements = {
PROMPT_GOES_HERE: userMessage,
SYSTEM_PROMPT_GOES_HERE: systemPrompt, SYSTEM_PROMPT_GOES_HERE: systemPrompt,
PROMPT_GOES_HERE: userMessage,
API_KEY_GOES_HERE: apiKey, API_KEY_GOES_HERE: apiKey,
MODEL_GOES_HERE: model || "", MODEL_GOES_HERE: model || "",
API_BASE_URL_GOES_HERE: apiBaseUrl || "" API_BASE_URL_GOES_HERE: apiBaseUrl || ""

View File

@@ -186,13 +186,16 @@ function normalizeConfigList(list) {
: []; : [];
} }
const DEFAULT_API_KEY_HEADER = "Authorization";
const DEFAULT_API_KEY_PREFIX = "Bearer ";
const TEMPLATE_PLACEHOLDERS = [ const TEMPLATE_PLACEHOLDERS = [
"PROMPT_GOES_HERE",
"SYSTEM_PROMPT_GOES_HERE", "SYSTEM_PROMPT_GOES_HERE",
"PROMPT_GOES_HERE",
"API_KEY_GOES_HERE", "API_KEY_GOES_HERE",
"MODEL_GOES_HERE", "MODEL_GOES_HERE",
"API_BASE_URL_GOES_HERE" "API_BASE_URL_GOES_HERE"
]; ].sort((a, b) => b.length - a.length);
function buildTemplateValidationSource(template) { function buildTemplateValidationSource(template) {
let output = template || ""; let output = template || "";
@@ -203,10 +206,28 @@ function buildTemplateValidationSource(template) {
return output; return output;
} }
function normalizeTemplateInput(template) {
return (template || "")
.replace(/\uFEFF/g, "")
.replace(/[\u200B-\u200D\u2060]/g, "")
.replace(/[\u2028\u2029]/g, "\n")
.replace(/[\u0000-\u001F]/g, (char) =>
char === "\n" || char === "\r" || char === "\t" ? char : " "
)
.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, " ");
}
function isValidTemplateJson(template) { function isValidTemplateJson(template) {
if (!template) return false; if (!template) return false;
const normalized = normalizeTemplateInput(template);
try { try {
JSON.parse(buildTemplateValidationSource(template)); JSON.parse(normalized);
return true;
} catch {
// Fall through to placeholder-neutralized parsing.
}
try {
JSON.parse(buildTemplateValidationSource(normalized));
return true; return true;
} catch { } catch {
return false; return false;
@@ -1017,8 +1038,6 @@ async function handleAnalyze() {
apiConfigs = [], apiConfigs = [],
activeApiConfigId = "", activeApiConfigId = "",
apiBaseUrl, apiBaseUrl,
apiKeyHeader,
apiKeyPrefix,
model, model,
systemPrompt, systemPrompt,
resume resume
@@ -1028,8 +1047,6 @@ async function handleAnalyze() {
"apiConfigs", "apiConfigs",
"activeApiConfigId", "activeApiConfigId",
"apiBaseUrl", "apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model", "model",
"systemPrompt", "systemPrompt",
"resume" "resume"
@@ -1071,8 +1088,8 @@ async function handleAnalyze() {
const resolvedApiUrl = activeConfig?.apiUrl || ""; const resolvedApiUrl = activeConfig?.apiUrl || "";
const resolvedTemplate = activeConfig?.requestTemplate || ""; const resolvedTemplate = activeConfig?.requestTemplate || "";
const resolvedApiBaseUrl = activeConfig?.apiBaseUrl || apiBaseUrl || ""; const resolvedApiBaseUrl = activeConfig?.apiBaseUrl || apiBaseUrl || "";
const resolvedApiKeyHeader = activeConfig?.apiKeyHeader ?? apiKeyHeader ?? ""; const resolvedApiKeyHeader = isAdvanced ? "" : DEFAULT_API_KEY_HEADER;
const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? ""; const resolvedApiKeyPrefix = isAdvanced ? "" : DEFAULT_API_KEY_PREFIX;
const resolvedModel = activeConfig?.model || model || ""; const resolvedModel = activeConfig?.model || model || "";
const resolvedKeys = normalizeConfigList(apiKeys).filter( const resolvedKeys = normalizeConfigList(apiKeys).filter(

View File

@@ -26,9 +26,7 @@ const toc = document.querySelector(".toc");
const tocResizer = document.getElementById("tocResizer"); const tocResizer = document.getElementById("tocResizer");
const OPENAI_DEFAULTS = { const OPENAI_DEFAULTS = {
apiBaseUrl: "https://api.openai.com/v1", apiBaseUrl: "https://api.openai.com/v1"
apiKeyHeader: "Authorization",
apiKeyPrefix: "Bearer "
}; };
const DEFAULT_MODEL = "gpt-5.2"; const DEFAULT_MODEL = "gpt-5.2";
const DEFAULT_SYSTEM_PROMPT = ""; const DEFAULT_SYSTEM_PROMPT = "";
@@ -476,12 +474,12 @@ function normalizeConfigList(list) {
} }
const TEMPLATE_PLACEHOLDERS = [ const TEMPLATE_PLACEHOLDERS = [
"PROMPT_GOES_HERE",
"SYSTEM_PROMPT_GOES_HERE", "SYSTEM_PROMPT_GOES_HERE",
"PROMPT_GOES_HERE",
"API_KEY_GOES_HERE", "API_KEY_GOES_HERE",
"MODEL_GOES_HERE", "MODEL_GOES_HERE",
"API_BASE_URL_GOES_HERE" "API_BASE_URL_GOES_HERE"
]; ].sort((a, b) => b.length - a.length);
function buildTemplateValidationSource(template) { function buildTemplateValidationSource(template) {
let output = template || ""; let output = template || "";
@@ -492,10 +490,28 @@ function buildTemplateValidationSource(template) {
return output; return output;
} }
function normalizeTemplateInput(template) {
return (template || "")
.replace(/\uFEFF/g, "")
.replace(/[\u200B-\u200D\u2060]/g, "")
.replace(/[\u2028\u2029]/g, "\n")
.replace(/[\u0000-\u001F]/g, (char) =>
char === "\n" || char === "\r" || char === "\t" ? char : " "
)
.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, " ");
}
function isValidTemplateJson(template) { function isValidTemplateJson(template) {
if (!template) return false; if (!template) return false;
const normalized = normalizeTemplateInput(template);
try { try {
JSON.parse(buildTemplateValidationSource(template)); JSON.parse(normalized);
return true;
} catch {
// Fall through to placeholder-neutralized parsing.
}
try {
JSON.parse(buildTemplateValidationSource(normalized));
return true; return true;
} catch { } catch {
return false; return false;
@@ -549,8 +565,6 @@ function readApiConfigFromCard(card) {
const nameInput = card.querySelector(".api-config-name"); const nameInput = card.querySelector(".api-config-name");
const keySelect = card.querySelector(".api-config-key-select"); const keySelect = card.querySelector(".api-config-key-select");
const baseInput = card.querySelector(".api-config-base"); const baseInput = card.querySelector(".api-config-base");
const headerInput = card.querySelector(".api-config-header");
const prefixInput = card.querySelector(".api-config-prefix");
const modelInput = card.querySelector(".api-config-model"); const modelInput = card.querySelector(".api-config-model");
const urlInput = card.querySelector(".api-config-url"); const urlInput = card.querySelector(".api-config-url");
const templateInput = card.querySelector(".api-config-template"); const templateInput = card.querySelector(".api-config-template");
@@ -562,8 +576,6 @@ function readApiConfigFromCard(card) {
name: (nameInput?.value || "Default").trim(), name: (nameInput?.value || "Default").trim(),
apiKeyId: keySelect?.value || "", apiKeyId: keySelect?.value || "",
apiBaseUrl: (baseInput?.value || "").trim(), apiBaseUrl: (baseInput?.value || "").trim(),
apiKeyHeader: (headerInput?.value || "").trim(),
apiKeyPrefix: prefixInput?.value || "",
model: (modelInput?.value || "").trim(), model: (modelInput?.value || "").trim(),
apiUrl: (urlInput?.value || "").trim(), apiUrl: (urlInput?.value || "").trim(),
requestTemplate: (templateInput?.value || "").trim(), requestTemplate: (templateInput?.value || "").trim(),
@@ -623,30 +635,6 @@ function buildApiConfigCard(config) {
baseField.appendChild(baseLabel); baseField.appendChild(baseLabel);
baseField.appendChild(baseInput); baseField.appendChild(baseInput);
const headerField = document.createElement("div");
headerField.className = "field advanced-only";
const headerLabel = document.createElement("label");
headerLabel.textContent = "API Key Header";
const headerInput = document.createElement("input");
headerInput.type = "text";
headerInput.placeholder = OPENAI_DEFAULTS.apiKeyHeader;
headerInput.value = config.apiKeyHeader || "";
headerInput.className = "api-config-header";
headerField.appendChild(headerLabel);
headerField.appendChild(headerInput);
const prefixField = document.createElement("div");
prefixField.className = "field advanced-only";
const prefixLabel = document.createElement("label");
prefixLabel.textContent = "API Key Prefix";
const prefixInput = document.createElement("input");
prefixInput.type = "text";
prefixInput.placeholder = OPENAI_DEFAULTS.apiKeyPrefix;
prefixInput.value = config.apiKeyPrefix || "";
prefixInput.className = "api-config-prefix";
prefixField.appendChild(prefixLabel);
prefixField.appendChild(prefixInput);
const modelField = document.createElement("div"); const modelField = document.createElement("div");
modelField.className = "field basic-only"; modelField.className = "field basic-only";
const modelLabel = document.createElement("label"); const modelLabel = document.createElement("label");
@@ -674,7 +662,7 @@ function buildApiConfigCard(config) {
const templateField = document.createElement("div"); const templateField = document.createElement("div");
templateField.className = "field advanced-only"; templateField.className = "field advanced-only";
const templateLabel = document.createElement("label"); const templateLabel = document.createElement("label");
templateLabel.textContent = "Request JSON template"; templateLabel.textContent = "Request JSON body";
const templateInput = document.createElement("textarea"); const templateInput = document.createElement("textarea");
templateInput.rows = 8; templateInput.rows = 8;
templateInput.placeholder = [ templateInput.placeholder = [
@@ -747,8 +735,6 @@ function buildApiConfigCard(config) {
id: newApiConfigId(), id: newApiConfigId(),
name, name,
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl, apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
apiKeyHeader: OPENAI_DEFAULTS.apiKeyHeader,
apiKeyPrefix: OPENAI_DEFAULTS.apiKeyPrefix,
model: DEFAULT_MODEL, model: DEFAULT_MODEL,
apiUrl: "", apiUrl: "",
requestTemplate: "", requestTemplate: "",
@@ -787,8 +773,6 @@ function buildApiConfigCard(config) {
resetBtn.textContent = "Reset to OpenAI"; resetBtn.textContent = "Reset to OpenAI";
resetBtn.addEventListener("click", () => { resetBtn.addEventListener("click", () => {
baseInput.value = OPENAI_DEFAULTS.apiBaseUrl; baseInput.value = OPENAI_DEFAULTS.apiBaseUrl;
headerInput.value = OPENAI_DEFAULTS.apiKeyHeader;
prefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix;
modelInput.value = DEFAULT_MODEL; modelInput.value = DEFAULT_MODEL;
urlInput.value = ""; urlInput.value = "";
templateInput.value = ""; templateInput.value = "";
@@ -808,8 +792,6 @@ function buildApiConfigCard(config) {
const updateSelect = () => updateEnvApiOptions(); const updateSelect = () => updateEnvApiOptions();
nameInput.addEventListener("input", updateSelect); nameInput.addEventListener("input", updateSelect);
baseInput.addEventListener("input", updateSelect); baseInput.addEventListener("input", updateSelect);
headerInput.addEventListener("input", updateSelect);
prefixInput.addEventListener("input", updateSelect);
modelInput.addEventListener("input", updateSelect); modelInput.addEventListener("input", updateSelect);
urlInput.addEventListener("input", updateSelect); urlInput.addEventListener("input", updateSelect);
templateInput.addEventListener("input", updateSelect); templateInput.addEventListener("input", updateSelect);
@@ -830,8 +812,6 @@ function buildApiConfigCard(config) {
card.appendChild(nameField); card.appendChild(nameField);
card.appendChild(keyField); card.appendChild(keyField);
card.appendChild(baseField); card.appendChild(baseField);
card.appendChild(headerField);
card.appendChild(prefixField);
card.appendChild(modelField); card.appendChild(modelField);
card.appendChild(urlField); card.appendChild(urlField);
card.appendChild(templateField); card.appendChild(templateField);
@@ -3479,7 +3459,7 @@ function updateSidebarErrors() {
} }
} }
if (!defaultApiConfig.advanced && defaultApiConfig?.apiKeyHeader) { if (!defaultApiConfig.advanced) {
const key = enabledApiKeys.find( const key = enabledApiKeys.find(
(entry) => entry.id === defaultApiConfig?.apiKeyId (entry) => entry.id === defaultApiConfig?.apiKeyId
); );
@@ -3534,8 +3514,6 @@ async function loadSettings() {
activeEnvConfigId = "", activeEnvConfigId = "",
profiles = [], profiles = [],
apiBaseUrl = "", apiBaseUrl = "",
apiKeyHeader = "",
apiKeyPrefix = "",
model = "", model = "",
systemPrompt = "", systemPrompt = "",
resume = "", resume = "",
@@ -3559,8 +3537,6 @@ async function loadSettings() {
"activeEnvConfigId", "activeEnvConfigId",
"profiles", "profiles",
"apiBaseUrl", "apiBaseUrl",
"apiKeyHeader",
"apiKeyPrefix",
"model", "model",
"systemPrompt", "systemPrompt",
"resume", "resume",
@@ -3724,8 +3700,6 @@ async function loadSettings() {
id: newApiConfigId(), id: newApiConfigId(),
name: "Default", name: "Default",
apiBaseUrl: apiBaseUrl || OPENAI_DEFAULTS.apiBaseUrl, apiBaseUrl: apiBaseUrl || OPENAI_DEFAULTS.apiBaseUrl,
apiKeyHeader: apiKeyHeader || OPENAI_DEFAULTS.apiKeyHeader,
apiKeyPrefix: apiKeyPrefix || OPENAI_DEFAULTS.apiKeyPrefix,
model: model || DEFAULT_MODEL, model: model || DEFAULT_MODEL,
apiKeyId: resolvedActiveId || resolvedKeys[0]?.id || "", apiKeyId: resolvedActiveId || resolvedKeys[0]?.id || "",
apiUrl: "", apiUrl: "",
@@ -4046,8 +4020,6 @@ addApiConfigBtn.addEventListener("click", () => {
id: newApiConfigId(), id: newApiConfigId(),
name, name,
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl, apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
apiKeyHeader: OPENAI_DEFAULTS.apiKeyHeader,
apiKeyPrefix: OPENAI_DEFAULTS.apiKeyPrefix,
model: DEFAULT_MODEL, model: DEFAULT_MODEL,
apiUrl: "", apiUrl: "",
requestTemplate: "", requestTemplate: "",