From 5d75226f40266afff498ecc1123d656394df75b2 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 28 Aug 2025 20:44:00 +0200 Subject: [PATCH 1/6] 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 7434816e..809c7a1f 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -46,7 +46,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 2ac7ac38..bf0671f0 100644 --- a/js/mzta-special-commands.js +++ b/js/mzta-special-commands.js @@ -37,7 +37,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; + } +}; From 8e04393c191dd103009966b9da7a02d41d10f0c1 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 28 Aug 2025 20:44:00 +0200 Subject: [PATCH 2/6] 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; + } +}; From 65ee959f0de16a912c1560bd70fbde4d8c612fc4 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 22 Dec 2025 23:46:33 +0100 Subject: [PATCH 3/6] improved openai responses see #407 --- js/api/openai_responses.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/js/api/openai_responses.js b/js/api/openai_responses.js index 9066132e..f84c0f4e 100644 --- a/js/api/openai_responses.js +++ b/js/api/openai_responses.js @@ -27,7 +27,13 @@ export class OpenAI { stream = false; store = false; - constructor(apiKey, model, developer_messages, stream, store) { + constructor({ + apiKey = '', + model = '', + developer_messages = '', + stream = false, + store = false + } = {}) { this.apiKey = apiKey; this.model = model; this.developer_messages = developer_messages; @@ -59,7 +65,7 @@ export class OpenAI { 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)); + output.response = output_response.data.filter(item => item.id.startsWith('gpt-') || item.id.startsWith('o1-') || item.id.startsWith('o4-') || item.id.startsWith('o3-')).sort((a, b) => b.id.localeCompare(a.id)); return output; }catch (error) { @@ -79,7 +85,7 @@ export class OpenAI { input: messages, stream: this.stream, store: this.store, - ...(maxTokens > 0 ? { 'max_tokens': parseInt(maxTokens) } : {}) + ...(maxTokens > 0 ? { 'max_completion_tokens': parseInt(maxTokens) } : {}) } if(this.developer_messages !== ''){ From 713415c1d2d43d9964ba9269cbb6602a29d1bb8b Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 23 Dec 2025 00:29:00 +0100 Subject: [PATCH 4/6] now using the openai responses API. see #407 --- js/api/openai_responses.js | 16 +++++--- js/workers/model-worker-openai_responses.js | 42 +++++++++++++++------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/js/api/openai_responses.js b/js/api/openai_responses.js index f84c0f4e..f4f4977a 100644 --- a/js/api/openai_responses.js +++ b/js/api/openai_responses.js @@ -27,13 +27,13 @@ export class OpenAI { stream = false; store = false; - constructor({ + constructor( apiKey = '', model = '', developer_messages = '', stream = false, store = false - } = {}) { + ) { this.apiKey = apiKey; this.model = model; this.developer_messages = developer_messages; @@ -78,14 +78,20 @@ export class OpenAI { } } - fetchResponse = async (messages, maxTokens = 0) => { + fetchResponse = async (messages, maxTokens = 0, previous_response_id = null) => { + + const input = messages.map(msg => ({ + role: msg.role, + content: [{ type: "input_text", text: msg.content }] + })); let request_body = { model: this.model, - input: messages, + input: input, stream: this.stream, store: this.store, - ...(maxTokens > 0 ? { 'max_completion_tokens': parseInt(maxTokens) } : {}) + ...(maxTokens > 0 ? { 'max_output_tokens': parseInt(maxTokens) } : {}), + ...(previous_response_id && this.store ? { 'previous_response_id': previous_response_id } : {}) } if(this.developer_messages !== ''){ diff --git a/js/workers/model-worker-openai_responses.js b/js/workers/model-worker-openai_responses.js index 2e58e08b..cb1562d5 100644 --- a/js/workers/model-worker-openai_responses.js +++ b/js/workers/model-worker-openai_responses.js @@ -29,11 +29,11 @@ let openai = null; let stopStreaming = false; let i18nStrings = null; let do_debug = false; -let taLog = null +let taLog = null; let conversationHistory = []; let assistantResponseAccumulator = ''; -let previous_response_id = -1; +let previous_response_id = null; self.onmessage = async function(event) { if (event.data.type === 'init') { @@ -43,10 +43,19 @@ self.onmessage = async function(event) { do_debug = event.data.do_debug; i18nStrings = event.data.i18nStrings; taLog = new taLogger('model-worker-openai_responses', do_debug); + previous_response_id = null; } else if (event.data.type === 'chatMessage') { conversationHistory.push({ role: 'user', content: event.data.message }); - const response = await openai.fetchResponse(conversationHistory); //4096); + let messagesToSend = conversationHistory; + if (previous_response_id) { + messagesToSend = [conversationHistory[conversationHistory.length - 1]]; + taLog.log("previous_response_id: " + previous_response_id); + } else { + taLog.log("no previous_response_id"); + } + + const response = await openai.fetchResponse(messagesToSend, 0, previous_response_id); postMessage({ type: 'messageSent' }); if (!response.ok) { @@ -97,24 +106,33 @@ self.onmessage = async function(event) { let parsedLines = []; try{ parsedLines = lines + .map((line) => line.trim()) + .filter((line) => line.startsWith("data:")) .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); - }); + try { + taLog.log("line: " + JSON.stringify(line)); + return JSON.parse(line); + } catch (e) { + taLog.warn("JSON parse warning, skipped line: " + line + " - " + e.message); + return null; + } + }) + .filter((parsed) => parsed !== null); }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 } }); + if (parsedLine.type === 'response.created' && parsedLine.response && parsedLine.response.id){ + previous_response_id = parsedLine.response.id; + } else if (parsedLine.type === 'response.output_text.delta' && parsedLine.delta) { + assistantResponseAccumulator += parsedLine.delta; + postMessage({ type: 'newToken', payload: { token: parsedLine.delta } }); + // } else if (parsedLine.type === 'response.completed' && parsedLine.response && parsedLine.response.id) { + // previous_response_id = parsedLine.response.id; } } } From f6141f88521493c5ddb15b2b3a35d4074b742b1c Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 23 Dec 2025 00:29:00 +0100 Subject: [PATCH 5/6] old openai api files removed. see #407 --- js/api/openai.js | 129 ---------------------------- js/workers/model-worker-openai.js | 136 ------------------------------ 2 files changed, 265 deletions(-) delete mode 100644 js/api/openai.js delete mode 100644 js/workers/model-worker-openai.js diff --git a/js/api/openai.js b/js/api/openai.js deleted file mode 100644 index 6d589c4c..00000000 --- a/js/api/openai.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 = false, - store = false - } = {}) { - 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) => { - - if(this.developer_messages !== ''){ - messages.push({role: "developer", content: [{"type": "text", "text": this.developer_messages}]}); - } - - // console.log(">>>>>>>>>>> OpenAI API request: " + JSON.stringify(messages)); - - try { - const response = await fetch("https://api.openai.com/v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer "+ this.apiKey - }, - body: JSON.stringify({ - model: this.model, - messages: messages, - stream: this.stream, - store: this.store, - ...(maxTokens > 0 ? { 'max_tokens': parseInt(maxTokens) } : {}) - }), - }); - return response; - }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; - } - } - - async countTokensUsingAPI(model, text) { - const response = await fetch('https://api.openai.com/v1/engines/'+model+'/tokenizer', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.apiKey - }, - body: JSON.stringify({ text }) - }); - - const data = await response.json(); - return data.token_count; - } - -} diff --git a/js/workers/model-worker-openai.js b/js/workers/model-worker-openai.js deleted file mode 100644 index 1cd9055b..00000000 --- a/js/workers/model-worker-openai.js +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.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 = ''; - -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({ - apiKey: chatgpt_api_key, - model: chatgpt_model, - developer_messages: event.data.chatgpt_developer_messages, - stream: true, - store: event.data.chatgpt_api_store - }); - do_debug = event.data.do_debug; - i18nStrings = event.data.i18nStrings; - taLog = new taLogger('model-worker-openai', 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) => { - try { - taLog.log("line: " + JSON.stringify(line)); - return JSON.parse(line); - } catch (e) { - taLog.warn("JSON parse warning, skipped line: " + line + " - " + e.message); - return null; - } - }) - .filter((parsed) => parsed !== null); - }catch(e){ - taLog.error("Error parsing lines: " + e); - } - - for (const parsedLine of parsedLines) { - const { choices } = parsedLine; - const { delta } = choices[0]; - const { content } = delta; - // 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; - } -}; From 85a473dd3dc4208c5b62bf20db9a6b5ad235fa65 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 22 Dec 2025 23:55:00 +0100 Subject: [PATCH 6/6] include fix. see #407 --- pages/_lib/connection-ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/_lib/connection-ui.js b/pages/_lib/connection-ui.js index ef285b3a..9bbcf41a 100644 --- a/pages/_lib/connection-ui.js +++ b/pages/_lib/connection-ui.js @@ -17,7 +17,7 @@ */ import { prefs_default } from '../../options/mzta-options-default.js'; -import { OpenAI } from '../../js/api/openai.js'; +import { OpenAI } from '../../js/api/openai_responses.js'; import { Ollama } from '../../js/api/ollama.js'; import { OpenAIComp } from '../../js/api/openai_comp.js' import { GoogleGemini } from '../../js/api/google_gemini.js';