From 8e04393c191dd103009966b9da7a02d41d10f0c1 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 28 Aug 2025 20:44:00 +0200 Subject: [PATCH] implemented the new api call, no response elaboration yet --- api_webchat/controller.js | 2 +- js/api/openai_responses.js | 111 ++++++++++++++++++ js/mzta-special-commands.js | 2 +- js/workers/model-worker-openai_responses.js | 124 ++++++++++++++++++++ 4 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 js/api/openai_responses.js create mode 100644 js/workers/model-worker-openai_responses.js diff --git a/api_webchat/controller.js b/api_webchat/controller.js index d4a9cc52..5f29526a 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -49,7 +49,7 @@ let worker = null; switch (llm) { case "chatgpt_api": - worker = new Worker('../js/workers/model-worker-openai.js', { type: 'module' }); + worker = new Worker('../js/workers/model-worker-openai_responses.js', { type: 'module' }); break; case "google_gemini_api": worker = new Worker('../js/workers/model-worker-google_gemini.js', { type: 'module' }); diff --git a/js/api/openai_responses.js b/js/api/openai_responses.js new file mode 100644 index 00000000..9066132e --- /dev/null +++ b/js/api/openai_responses.js @@ -0,0 +1,111 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 - 2025 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Some original methods derived from https://github.com/ali-raheem/Aify/blob/4ece286095ea7a6cf89d696902e6b81b5d1c3a4b/plugin/html/API.js + + +export class OpenAI { + + apiKey = ''; + model = ''; + developer_messages = ''; + stream = false; + store = false; + + constructor(apiKey, model, developer_messages, stream, store) { + this.apiKey = apiKey; + this.model = model; + this.developer_messages = developer_messages; + this.stream = stream; + this.store = store; + } + + + fetchModels = async () => { + try{ + const response = await fetch("https://api.openai.com/v1/models", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer "+ this.apiKey + }, + }); + + if (!response.ok) { + const errorDetail = await response.text(); + let err_msg = "[ThunderAI] OpenAI API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail; + console.error(err_msg); + let output = {}; + output.ok = false; + output.error = errorDetail; + return output; + } + + let output = {}; + output.ok = true; + let output_response = await response.json(); + output.response = output_response.data.filter(item => item.id.startsWith('gpt-')).sort((a, b) => b.id.localeCompare(a.id)); + + return output; + }catch (error) { + console.error("[ThunderAI] OpenAI API request failed: " + error); + let output = {}; + output.is_exception = true; + output.ok = false; + output.error = "OpenAI API request failed: " + error; + return output; + } + } + + fetchResponse = async (messages, maxTokens = 0) => { + + let request_body = { + model: this.model, + input: messages, + stream: this.stream, + store: this.store, + ...(maxTokens > 0 ? { 'max_tokens': parseInt(maxTokens) } : {}) + } + + if(this.developer_messages !== ''){ + request_body.instructions = this.developer_messages; + } + + // console.log(">>>>>>>>>>> OpenAI API request: " + JSON.stringify(messages)); + + try { + const response = await fetch("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer "+ this.apiKey + }, + body: JSON.stringify(request_body), + }); + return response; + }catch (error) { + console.error("[ThunderAI] OpenAI Responses API request failed: " + error); + let output = {}; + output.is_exception = true; + output.ok = false; + output.error = "OpenAI Responses API request failed: " + error; + return output; + } + } + +} \ No newline at end of file diff --git a/js/mzta-special-commands.js b/js/mzta-special-commands.js index 0bbab812..a6ddaad3 100644 --- a/js/mzta-special-commands.js +++ b/js/mzta-special-commands.js @@ -44,7 +44,7 @@ this.do_debug = do_debug; switch (this.llm) { case "chatgpt_api": - this.worker = new Worker(new URL('./workers/model-worker-openai.js', import.meta.url), { type: 'module' }); + this.worker = new Worker(new URL('./workers/model-worker-openai_responses.js', import.meta.url), { type: 'module' }); break; case "google_gemini_api": this.worker = new Worker(new URL('./workers/model-worker-google_gemini.js', import.meta.url), { type: 'module' }); diff --git a/js/workers/model-worker-openai_responses.js b/js/workers/model-worker-openai_responses.js new file mode 100644 index 00000000..2e58e08b --- /dev/null +++ b/js/workers/model-worker-openai_responses.js @@ -0,0 +1,124 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 - 2025 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * This file contains a modified version of the code from the project at https://github.com/boxabirds/chatgpt-frontend-nobuild + * The original code has been released under the Apache License, Version 2.0. + */ + +import { OpenAI } from '../api/openai_responses.js'; +import { taLogger } from '../mzta-logger.js'; + +let chatgpt_api_key = null; +let chatgpt_model = ''; +let openai = null; +let stopStreaming = false; +let i18nStrings = null; +let do_debug = false; +let taLog = null + +let conversationHistory = []; +let assistantResponseAccumulator = ''; +let previous_response_id = -1; + +self.onmessage = async function(event) { + if (event.data.type === 'init') { + chatgpt_api_key = event.data.chatgpt_api_key; + chatgpt_model = event.data.chatgpt_model; + openai = new OpenAI(chatgpt_api_key, chatgpt_model, event.data.chatgpt_developer_messages, true, event.data.chatgpt_api_store); + do_debug = event.data.do_debug; + i18nStrings = event.data.i18nStrings; + taLog = new taLogger('model-worker-openai_responses', do_debug); + } else if (event.data.type === 'chatMessage') { + conversationHistory.push({ role: 'user', content: event.data.message }); + + const response = await openai.fetchResponse(conversationHistory); //4096); + postMessage({ type: 'messageSent' }); + + if (!response.ok) { + let error_message = ''; + let errorDetail = ''; + if(response.is_exception === true){ + error_message = response.error; + }else{ + try{ + const errorJSON = await response.json(); + errorDetail = JSON.stringify(errorJSON); + error_message = errorJSON.error.message; + }catch(e){ + error_message = response.statusText; + } + taLog.log("error_message: " + JSON.stringify(error_message)); + } + postMessage({ type: 'error', payload: i18nStrings["chatgpt_api_request_failed"] + ": " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail }); + throw new Error("[ThunderAI] OpenAI ChatGPT API request failed: " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ''; + + while (true) { + if (stopStreaming) { + stopStreaming = false; + reader.cancel(); + conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + break; + } + const { done, value } = await reader.read(); + if (done) { + conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + break; + } + // lots of low-level OpenAI response parsing stuff + const chunk = decoder.decode(value); + buffer += chunk; + taLog.log("buffer " + buffer); + const lines = buffer.split("\n"); + buffer = lines.pop(); + let parsedLines = []; + try{ + parsedLines = lines + .map((line) => line.replace(/^data: /, "").trim()) // Remove the "data: " prefix + .filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]" + // .map((line) => JSON.parse(line)); // Parse the JSON string + .map((line) => { + taLog.log("line: " + JSON.stringify(line)); + return JSON.parse(line); + }); + }catch(e){ + taLog.error("Error parsing lines: " + e); + } + + for (const parsedLine of parsedLines) { + console.log(">>>>>>>>>> parsedLine: " + JSON.stringify(parsedLine)); + const { content } = parsedLine; + // Update the UI with the new content + if (content) { + assistantResponseAccumulator += content; + postMessage({ type: 'newToken', payload: { token: content } }); + } + } + } + } else if (event.data.type === 'stop') { + stopStreaming = true; + } +};