Initial commit

This commit is contained in:
Joey Yakimowich-Payne 2025-10-02 10:58:43 -06:00
commit 8d1a045333
No known key found for this signature in database
GPG key ID: DDF6AF5B21B407D4
333 changed files with 41117 additions and 0 deletions

View file

@ -0,0 +1,187 @@
import QtQuick
import qs.modules.common.functions as CF
ApiStrategy {
property bool isReasoning: false
readonly property string fileMimeTypeVarName: "MIME_TYPE"
readonly property string base64VarName: "FILE_B64"
readonly property string filePathVarName: "FILE_PATH"
readonly property string base64Placeholder: "{{ FILE_B64 }}"
readonly property string mimePlaceholder: "{{ MIME_TYPE }}"
readonly property string dataUrlPlaceholder: "{{ DATA_URL }}"
readonly property string filePartPlaceholder: "{{ FILE_PART_JSON }}"
function buildEndpoint(model: AiModel): string {
// console.log("[AI] Endpoint: " + model.endpoint);
return model.endpoint;
}
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>, filePath: string) {
// Map messages; if a file is attached and it's an image, include an image_url content part with data URL
const hasFile = !!(filePath && filePath.length > 0);
const isOpenRouter = (model.endpoint || "").indexOf("openrouter.ai") !== -1;
const mappedMessages = [
{ role: "system", content: systemPrompt },
...messages.map((message, idx, arr) => {
const isLastUser = (idx === arr.length - 1) && message.role === "user";
if (isLastUser && hasFile) {
// Defer building the right content part (image or text) to the shell prelude
// by inserting a placeholder JSON object here.
return {
role: message.role,
content: [
{ type: "text", text: message.rawContent },
filePartPlaceholder
]
};
}
return { role: message.role, content: message.rawContent };
})
];
let baseData = {
"model": model.model,
"messages": mappedMessages,
"stream": true,
"tools": tools,
"temperature": temperature,
};
return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
}
function buildAuthorizationHeader(apiKeyEnvVarName: string): string {
return `-H "Authorization: Bearer \$\{${apiKeyEnvVarName}\}"`;
}
function parseResponseLine(line, message) {
// Remove 'data: ' prefix if present and trim whitespace
let cleanData = line.trim();
if (cleanData.startsWith("data:")) {
cleanData = cleanData.slice(5).trim();
}
// console.log("[AI] OpenAI: Data:", cleanData);
// Handle special cases
if (!cleanData) return {};
if (cleanData.startsWith(":")) {
// Some providers emit comment lines starting with ':'; show them as debug
// const dbg = cleanData.slice(1).trim();
// if (dbg.length > 0) {
// message.rawContent += `\n\n[debug] ${dbg}\n`;
// message.content += `\n\n[debug] ${dbg}\n`;
// }
return {};
}
if (cleanData === "[DONE]") {
return { finished: true };
}
// Real stuff
try {
const dataJson = JSON.parse(cleanData);
if (dataJson.error) {
const err = dataJson.error;
const errMsg = `[Error] ${err.type ?? ""} ${err.code ?? ""} ${err.param ? "(" + err.param + ")" : ""}\n${err.message ?? JSON.stringify(err)}`;
message.rawContent += `\n\n${errMsg}\n`;
message.content += `\n\n${errMsg}\n`;
return { finished: true };
}
let newContent = "";
const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content;
const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content;
if (responseContent && responseContent.length > 0) {
if (isReasoning) {
isReasoning = false;
const endBlock = "\n\n</think>\n\n";
message.content += endBlock;
message.rawContent += endBlock;
}
newContent = responseContent;
} else if (responseReasoning && responseReasoning.length > 0) {
if (!isReasoning) {
isReasoning = true;
const startBlock = "\n\n<think>\n\n";
message.rawContent += startBlock;
message.content += startBlock;
}
newContent = responseReasoning;
}
message.content += newContent;
message.rawContent += newContent;
// Usage metadata
if (dataJson.usage) {
return {
tokenUsage: {
input: dataJson.usage.prompt_tokens ?? -1,
output: dataJson.usage.completion_tokens ?? -1,
total: dataJson.usage.total_tokens ?? -1
}
};
}
if (dataJson.done) {
return { finished: true };
}
} catch (e) {
console.log("[AI] OpenAI: Could not parse line: ", e);
message.rawContent += line;
message.content += line;
}
return {};
}
function onRequestFinished(message) {
// OpenAI format doesn't need special finish handling
return {};
}
function reset() {
isReasoning = false;
}
function buildScriptFileSetup(filePath) {
if (!filePath || filePath.length === 0) return "";
let content = "";
content += `${filePathVarName}='${CF.StringUtils.shellSingleQuoteEscape(filePath)}'\n`;
content += `${fileMimeTypeVarName}=$(file -b --mime-type "\$${filePathVarName}")\n`;
// Try GNU coreutils base64 with no wrap; fallback to generic base64 and strip newlines
content += `${base64VarName}=$(base64 -w 0 "\$${filePathVarName}" 2>/dev/null || base64 "\$${filePathVarName}" | tr -d '\n')\n`;
// Build FILE_PART_JSON depending on MIME: image -> image_url with data URL; text/docs -> inline text
content += 'if [[ "$' + fileMimeTypeVarName + '" == image/* ]]; then\n';
content += " FILE_PART_JSON=$(printf '{\"type\":\"image_url\",\"image_url\":{\"url\":\"data:%s;base64,%s\"}}' \"$" + fileMimeTypeVarName + "\" \"$" + base64VarName + "\")\n";
content += 'else\n';
// Try to produce text content
content += ' if [[ "$' + fileMimeTypeVarName + '" == text/* ]] || [[ "$' + fileMimeTypeVarName + '" == application/json ]] || [[ "$' + fileMimeTypeVarName + '" == application/xml ]] || [[ "$' + fileMimeTypeVarName + '" =~ \+json$ ]] || [[ "$' + fileMimeTypeVarName + '" =~ \+xml$ ]]; then\n';
content += " FILE_TEXT_JSON=$(jq -Rsa . \"$" + filePathVarName + "\")\n";
content += ' elif [[ "$' + fileMimeTypeVarName + '" == application/vnd.openxmlformats-officedocument.wordprocessingml.document ]]; then\n';
content += ' if command -v pandoc >/dev/null 2>&1; then\n';
content += " FILE_TEXT_JSON=$(pandoc -t plain \"$" + filePathVarName + "\" 2>/dev/null | jq -Rsa .)\n";
content += ' else\n';
content += " FILE_TEXT_JSON=$(printf \"DOCX attachment provided but pandoc not found. Install pandoc to extract text\\nFile: %s\" \"$" + filePathVarName + "\" | jq -Rsa .)\n";
content += ' fi\n';
content += ' else\n';
content += " FILE_TEXT_JSON=$(printf \"Attached file (%s) included as base64 (first 8KB):\\n%s\" \"$" + fileMimeTypeVarName + "\" \"$(printf %s \\\"$" + base64VarName + "\\\" | cut -c1-8192)\" | jq -Rsa .)\n";
content += ' fi\n';
content += " FILE_PART_JSON=$(printf '{\"type\":\"text\",\"text\":%s}' \"$FILE_TEXT_JSON\")\n";
content += 'fi\n';
return content;
}
function finalizeScriptContent(scriptContent) {
// Replace the QUOTED placeholder ("{{ FILE_PART_JSON }}") with FILE_PART_JSON (raw JSON object)
const quotedPartPlaceholder = `\"${filePartPlaceholder}\"`;
let result = scriptContent.replace(quotedPartPlaceholder, `\$FILE_PART_JSON`);
// Also support older placeholder if present
const quotedDataUrlPlaceholder = `\"${dataUrlPlaceholder}\"`;
result = result.replace(quotedDataUrlPlaceholder, `\"data:\\$${fileMimeTypeVarName};base64,\\$${base64VarName}\"`);
return result;
}
}