Initial commit
This commit is contained in:
commit
8d1a045333
333 changed files with 41117 additions and 0 deletions
187
ii/services/ai/OpenAiApiStrategy.qml
Normal file
187
ii/services/ai/OpenAiApiStrategy.qml
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue