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, 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\n\n"; message.content += endBlock; message.rawContent += endBlock; } newContent = responseContent; } else if (responseReasoning && responseReasoning.length > 0) { if (!isReasoning) { isReasoning = true; const startBlock = "\n\n\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; } }