Added multi-api support and advanced mode
This commit is contained in:
@@ -17,6 +17,8 @@ const DEFAULT_SETTINGS = {
|
||||
apiKey: "",
|
||||
apiKeys: [],
|
||||
activeApiKeyId: "",
|
||||
apiConfigs: [],
|
||||
activeApiConfigId: "",
|
||||
apiBaseUrl: "https://api.openai.com/v1",
|
||||
apiKeyHeader: "Authorization",
|
||||
apiKeyPrefix: "Bearer ",
|
||||
@@ -91,6 +93,99 @@ chrome.runtime.onInstalled.addListener(async () => {
|
||||
: `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
updates.apiKeys = [{ id, name: "Default", key: stored.apiKey }];
|
||||
updates.activeApiKeyId = id;
|
||||
} else if (hasApiKeys && stored.activeApiKeyId) {
|
||||
const exists = stored.apiKeys.some((key) => key.id === stored.activeApiKeyId);
|
||||
if (!exists) {
|
||||
updates.activeApiKeyId = stored.apiKeys[0].id;
|
||||
}
|
||||
} else if (hasApiKeys && !stored.activeApiKeyId) {
|
||||
updates.activeApiKeyId = stored.apiKeys[0].id;
|
||||
}
|
||||
|
||||
const hasApiConfigs =
|
||||
Array.isArray(stored.apiConfigs) && stored.apiConfigs.length > 0;
|
||||
|
||||
if (!hasApiConfigs) {
|
||||
const fallbackKeyId =
|
||||
updates.activeApiKeyId ||
|
||||
stored.activeApiKeyId ||
|
||||
stored.apiKeys?.[0]?.id ||
|
||||
"";
|
||||
const id = crypto?.randomUUID
|
||||
? crypto.randomUUID()
|
||||
: `config-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
updates.apiConfigs = [
|
||||
{
|
||||
id,
|
||||
name: "Default",
|
||||
apiBaseUrl: stored.apiBaseUrl || DEFAULT_SETTINGS.apiBaseUrl,
|
||||
apiKeyHeader: stored.apiKeyHeader || DEFAULT_SETTINGS.apiKeyHeader,
|
||||
apiKeyPrefix: stored.apiKeyPrefix || DEFAULT_SETTINGS.apiKeyPrefix,
|
||||
model: stored.model || DEFAULT_SETTINGS.model,
|
||||
apiKeyId: fallbackKeyId,
|
||||
apiUrl: "",
|
||||
requestTemplate: "",
|
||||
advanced: false
|
||||
}
|
||||
];
|
||||
updates.activeApiConfigId = id;
|
||||
} else if (stored.activeApiConfigId) {
|
||||
const exists = stored.apiConfigs.some(
|
||||
(config) => config.id === stored.activeApiConfigId
|
||||
);
|
||||
if (!exists) {
|
||||
updates.activeApiConfigId = stored.apiConfigs[0].id;
|
||||
}
|
||||
const fallbackKeyId =
|
||||
updates.activeApiKeyId ||
|
||||
stored.activeApiKeyId ||
|
||||
stored.apiKeys?.[0]?.id ||
|
||||
"";
|
||||
const normalizedConfigs = stored.apiConfigs.map((config) => ({
|
||||
...config,
|
||||
apiKeyId: config.apiKeyId || fallbackKeyId,
|
||||
apiUrl: config.apiUrl || "",
|
||||
requestTemplate: config.requestTemplate || "",
|
||||
advanced: Boolean(config.advanced)
|
||||
}));
|
||||
const needsUpdate = normalizedConfigs.some((config, index) => {
|
||||
const original = stored.apiConfigs[index];
|
||||
return (
|
||||
config.apiKeyId !== original.apiKeyId ||
|
||||
(config.apiUrl || "") !== (original.apiUrl || "") ||
|
||||
(config.requestTemplate || "") !== (original.requestTemplate || "") ||
|
||||
Boolean(config.advanced) !== Boolean(original.advanced)
|
||||
);
|
||||
});
|
||||
if (needsUpdate) {
|
||||
updates.apiConfigs = normalizedConfigs;
|
||||
}
|
||||
} else {
|
||||
updates.activeApiConfigId = stored.apiConfigs[0].id;
|
||||
const fallbackKeyId =
|
||||
updates.activeApiKeyId ||
|
||||
stored.activeApiKeyId ||
|
||||
stored.apiKeys?.[0]?.id ||
|
||||
"";
|
||||
const normalizedConfigs = stored.apiConfigs.map((config) => ({
|
||||
...config,
|
||||
apiKeyId: config.apiKeyId || fallbackKeyId,
|
||||
apiUrl: config.apiUrl || "",
|
||||
requestTemplate: config.requestTemplate || "",
|
||||
advanced: Boolean(config.advanced)
|
||||
}));
|
||||
const needsUpdate = normalizedConfigs.some((config, index) => {
|
||||
const original = stored.apiConfigs[index];
|
||||
return (
|
||||
config.apiKeyId !== original.apiKeyId ||
|
||||
(config.apiUrl || "") !== (original.apiUrl || "") ||
|
||||
(config.requestTemplate || "") !== (original.requestTemplate || "") ||
|
||||
Boolean(config.advanced) !== Boolean(original.advanced)
|
||||
);
|
||||
});
|
||||
if (needsUpdate) {
|
||||
updates.apiConfigs = normalizedConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length) {
|
||||
@@ -188,6 +283,9 @@ async function handleAnalysisRequest(port, payload, signal) {
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
apiMode,
|
||||
apiUrl,
|
||||
requestTemplate,
|
||||
apiBaseUrl,
|
||||
apiKeyHeader,
|
||||
apiKeyPrefix,
|
||||
@@ -199,19 +297,31 @@ async function handleAnalysisRequest(port, payload, signal) {
|
||||
tabId
|
||||
} = payload || {};
|
||||
|
||||
if (!apiBaseUrl) {
|
||||
safePost(port, { type: "ERROR", message: "Missing API base URL." });
|
||||
return;
|
||||
}
|
||||
const isAdvanced = apiMode === "advanced";
|
||||
if (isAdvanced) {
|
||||
if (!apiUrl) {
|
||||
safePost(port, { type: "ERROR", message: "Missing API URL." });
|
||||
return;
|
||||
}
|
||||
if (!requestTemplate) {
|
||||
safePost(port, { type: "ERROR", message: "Missing request template." });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!apiBaseUrl) {
|
||||
safePost(port, { type: "ERROR", message: "Missing API base URL." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiKeyHeader && !apiKey) {
|
||||
safePost(port, { type: "ERROR", message: "Missing API key." });
|
||||
return;
|
||||
}
|
||||
if (apiKeyHeader && !apiKey) {
|
||||
safePost(port, { type: "ERROR", message: "Missing API key." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
safePost(port, { type: "ERROR", message: "Missing model name." });
|
||||
return;
|
||||
if (!model) {
|
||||
safePost(port, { type: "ERROR", message: "Missing model name." });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!postingText) {
|
||||
@@ -230,20 +340,35 @@ async function handleAnalysisRequest(port, payload, signal) {
|
||||
openKeepalive(tabId);
|
||||
|
||||
try {
|
||||
await streamChatCompletion({
|
||||
apiKey,
|
||||
apiBaseUrl,
|
||||
apiKeyHeader,
|
||||
apiKeyPrefix,
|
||||
model,
|
||||
systemPrompt: systemPrompt || "",
|
||||
userMessage,
|
||||
signal,
|
||||
onDelta: (text) => {
|
||||
streamState.outputText += text;
|
||||
broadcast({ type: "DELTA", text });
|
||||
}
|
||||
});
|
||||
if (isAdvanced) {
|
||||
await streamCustomCompletion({
|
||||
apiKey,
|
||||
apiUrl,
|
||||
requestTemplate,
|
||||
systemPrompt: systemPrompt || "",
|
||||
userMessage,
|
||||
signal,
|
||||
onDelta: (text) => {
|
||||
streamState.outputText += text;
|
||||
broadcast({ type: "DELTA", text });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await streamChatCompletion({
|
||||
apiKey,
|
||||
apiBaseUrl,
|
||||
apiKeyHeader,
|
||||
apiKeyPrefix,
|
||||
model,
|
||||
systemPrompt: systemPrompt || "",
|
||||
userMessage,
|
||||
signal,
|
||||
onDelta: (text) => {
|
||||
streamState.outputText += text;
|
||||
broadcast({ type: "DELTA", text });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
broadcast({ type: "DONE" });
|
||||
} finally {
|
||||
@@ -268,6 +393,73 @@ function buildAuthHeader(apiKeyHeader, apiKeyPrefix, apiKey) {
|
||||
};
|
||||
}
|
||||
|
||||
function replaceQuotedToken(template, token, value) {
|
||||
const quoted = `"${token}"`;
|
||||
const jsonValue = JSON.stringify(value ?? "");
|
||||
return template.split(quoted).join(jsonValue);
|
||||
}
|
||||
|
||||
function replaceTemplateTokens(template, replacements) {
|
||||
let output = template || "";
|
||||
for (const [token, value] of Object.entries(replacements)) {
|
||||
output = replaceQuotedToken(output, token, value ?? "");
|
||||
output = output.split(token).join(value ?? "");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function replaceUrlTokens(url, replacements) {
|
||||
let output = url || "";
|
||||
for (const [token, value] of Object.entries(replacements)) {
|
||||
output = output.split(token).join(encodeURIComponent(value ?? ""));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function buildTemplateBody(template, replacements) {
|
||||
const filled = replaceTemplateTokens(template, replacements);
|
||||
try {
|
||||
return JSON.parse(filled);
|
||||
} catch {
|
||||
throw new Error("Invalid request template JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
async function readSseStream(response, onDelta) {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
// OpenAI-compatible SSE stream; parse incremental deltas from data lines.
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("data:")) continue;
|
||||
|
||||
const data = trimmed.slice(5).trim();
|
||||
if (!data) continue;
|
||||
if (data === "[DONE]") return;
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const delta = parsed?.choices?.[0]?.delta?.content;
|
||||
if (delta) onDelta(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function streamChatCompletion({
|
||||
apiKey,
|
||||
apiBaseUrl,
|
||||
@@ -309,39 +501,42 @@ async function streamChatCompletion({
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
|
||||
throw new Error(`API error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
// OpenAI streams Server-Sent Events; parse incremental deltas from data lines.
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("data:")) continue;
|
||||
|
||||
const data = trimmed.slice(5).trim();
|
||||
if (!data) continue;
|
||||
if (data === "[DONE]") return;
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const delta = parsed?.choices?.[0]?.delta?.content;
|
||||
if (delta) onDelta(delta);
|
||||
}
|
||||
}
|
||||
await readSseStream(response, onDelta);
|
||||
}
|
||||
|
||||
async function streamCustomCompletion({
|
||||
apiKey,
|
||||
apiUrl,
|
||||
requestTemplate,
|
||||
systemPrompt,
|
||||
userMessage,
|
||||
signal,
|
||||
onDelta
|
||||
}) {
|
||||
const replacements = {
|
||||
PROMPT_GOES_HERE: userMessage,
|
||||
SYSTEM_PROMPT_GOES_HERE: systemPrompt,
|
||||
API_KEY_GOES_HERE: apiKey
|
||||
};
|
||||
const resolvedUrl = replaceUrlTokens(apiUrl, replacements);
|
||||
const body = buildTemplateBody(requestTemplate, replacements);
|
||||
|
||||
const response = await fetch(resolvedUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`API error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
await readSseStream(response, onDelta);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user