From 3073cf69a3f0e0a6f4a0a48b178dfe7b1572d405 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 6 Jan 2025 00:21:58 +0100 Subject: [PATCH] first working version of the google gemini api. see #204 --- _locales/en/messages.json | 16 ++++++++ api_webchat/controller.js | 18 +++++++- js/api/google_gemini.js | 36 ++++++++++++++-- js/workers/model-worker-google_gemini.js | 43 ++++++++++---------- mzta-background.js | 52 +++++++++++++++++++++++- 5 files changed, 139 insertions(+), 26 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 629031f8..fa3979ac 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -914,5 +914,21 @@ "GoogleGemini_Models_Error_fetching": { "message": "Error trying to fetch Google Gemini models", "description": "" + }, + "google_gemini_api_request_failed": { + "message": "Google Gemini API request failed", + "description": "" + }, + "google_gemini_api_connecting": { + "message": "Attempting to connect to Google Gemini using the API key provided", + "description": "" + }, + "google_gemini_empty_apikey": { + "message": "You've not added an API Key for the Google Gemini API. Please insert one in the options page.", + "description": "" + }, + "google_gemini_empty_model": { + "message": "You've not choosen a model for the Google Gemini API. Please choose one in the options page.", + "description": "" } } \ No newline at end of file diff --git a/api_webchat/controller.js b/api_webchat/controller.js index aaed0d59..d10c33ec 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -47,6 +47,9 @@ switch (llm) { case "chatgpt_api": worker = new Worker('../js/workers/model-worker-openai.js', { type: 'module' }); break; + case "google_gemini_api": + worker = new Worker('../js/workers/model-worker-google_gemini.js', { type: 'module' }); + break; case "ollama_api": worker = new Worker('../js/workers/model-worker-ollama.js', { type: 'module' }); break; @@ -62,7 +65,7 @@ messageInput.init(worker); messageInput.setMessagesArea(messagesArea); switch (llm) { - case "chatgpt_api": + case "chatgpt_api": { let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: '', do_debug: false}); let i18nStrings = {}; i18nStrings["chatgpt_api_request_failed"] = browser.i18n.getMessage('chatgpt_api_request_failed'); @@ -73,6 +76,19 @@ switch (llm) { messagesArea.appendUserMessage(browser.i18n.getMessage("chagpt_api_connecting") + " " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.chatgpt_model + "\"...", "info"); browser.runtime.sendMessage({command: "openai_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id}); break; + } + case "google_gemini_api": { + let prefs_api = await browser.storage.sync.get({google_gemini_api_key: '', google_gemini_model: '', do_debug: false}); + let i18nStrings = {}; + i18nStrings["google_gemini_api_request_failed"] = browser.i18n.getMessage('google_gemini_api_request_failed'); + i18nStrings["error_connection_interrupted"] = browser.i18n.getMessage('error_connection_interrupted'); + messageInput.setModel(prefs_api.google_gemini_model); + messagesArea.setLLMName("Google Gemini"); + worker.postMessage({ type: 'init', google_gemini_api_key: prefs_api.google_gemini_api_key, google_gemini_model: prefs_api.google_gemini_model, do_debug: prefs_api.do_debug, i18nStrings: i18nStrings}); + messagesArea.appendUserMessage(browser.i18n.getMessage("google_gemini_api_connecting") + " " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.google_gemini_model + "\"...", "info"); + browser.runtime.sendMessage({command: "google_gemini_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id}); + break; + } case "ollama_api": { let prefs_api = await browser.storage.sync.get({ollama_host: '', ollama_model: '', do_debug: false}); let i18nStrings = {}; diff --git a/js/api/google_gemini.js b/js/api/google_gemini.js index ba8e7185..a15c3c71 100644 --- a/js/api/google_gemini.js +++ b/js/api/google_gemini.js @@ -67,9 +67,39 @@ export class GoogleGemini { } } -//TODO - fetchResponse = async (messages, maxTokens = 0) => { - + fetchResponse = async (messages, system_instruction = '') => { + try { + + let google_gemini_body = { + contents:messages + }; + + if(system_instruction !== '') { + google_gemini_body.system_instruction = { + parts:{ + text: system_instruction + } + } + } + + console.log("[ThunderAI] Google Gemini API request: " + JSON.stringify(google_gemini_body)); + + const response = await fetch("https://generativelanguage.googleapis.com/v1beta/models/" + this.model + ":generateContent?key=" + this.apiKey + (this.stream ? '&alt=sse' : ''), { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(google_gemini_body), + }); + return response; + }catch (error) { + console.error("[ThunderAI] Google Gemini API request failed: " + error); + let output = {}; + output.is_exception = true; + output.ok = false; + output.error = "Google Gemini API request failed: " + error; + return output; + } } } \ No newline at end of file diff --git a/js/workers/model-worker-google_gemini.js b/js/workers/model-worker-google_gemini.js index 3f364682..d50d5e1e 100644 --- a/js/workers/model-worker-google_gemini.js +++ b/js/workers/model-worker-google_gemini.js @@ -20,12 +20,12 @@ * The original code has been released under the Apache License, Version 2.0. */ -import { OpenAI } from '../api/openai.js'; +import { GoogleGemini } from '../api/google_gemini.js'; import { taLogger } from '../mzta-logger.js'; -let chatgpt_api_key = null; -let chatgpt_model = ''; -let openai = null; +let google_gemini_api_key = null; +let google_gemini_model = ''; +let google_gemini = null; let stopStreaming = false; let i18nStrings = null; let do_debug = false; @@ -36,16 +36,16 @@ 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(chatgpt_api_key, chatgpt_model, true); + google_gemini_api_key = event.data.google_gemini_api_key; + google_gemini_model = event.data.google_gemini_model; + google_gemini = new GoogleGemini(google_gemini_api_key, google_gemini_model, true); do_debug = event.data.do_debug; i18nStrings = event.data.i18nStrings; - taLog = new taLogger('model-worker-openai', do_debug); + taLog = new taLogger('model-worker-google_gemini', do_debug); } else if (event.data.type === 'chatMessage') { - conversationHistory.push({ role: 'user', content: event.data.message }); + conversationHistory.push({ role: 'user', parts: [{"text": event.data.message}] }); - const response = await openai.fetchResponse(conversationHistory); //4096); + const response = await google_gemini.fetchResponse(conversationHistory); postMessage({ type: 'messageSent' }); if (!response.ok) { @@ -63,8 +63,8 @@ self.onmessage = async function(event) { } 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); + postMessage({ type: 'error', payload: i18nStrings["google_gemini_api_request_failed"] + ": " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail }); + throw new Error("[ThunderAI] Google Gemini API request failed: " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail); } const reader = response.body.getReader(); @@ -75,19 +75,19 @@ self.onmessage = async function(event) { if (stopStreaming) { stopStreaming = false; reader.cancel(); - conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + conversationHistory.push({ role: 'model', parts: [{"text": assistantResponseAccumulator}] }); assistantResponseAccumulator = ''; postMessage({ type: 'tokensDone' }); break; } const { done, value } = await reader.read(); if (done) { - conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + conversationHistory.push({ role: 'model', parts: [{"text": assistantResponseAccumulator}] }); assistantResponseAccumulator = ''; postMessage({ type: 'tokensDone' }); break; } - // lots of low-level OpenAI response parsing stuff + // lots of low-level Google Gemini response parsing stuff const chunk = decoder.decode(value); buffer += chunk; taLog.log("buffer " + buffer); @@ -108,13 +108,14 @@ self.onmessage = async function(event) { } for (const parsedLine of parsedLines) { - const { choices } = parsedLine; - const { delta } = choices[0]; - const { content } = delta; + const { candidates } = parsedLine; + const { content } = candidates[0]; + const { parts } = content; + const { text } = parts[0]; // Update the UI with the new content - if (content) { - assistantResponseAccumulator += content; - postMessage({ type: 'newToken', payload: { token: content } }); + if (text) { + assistantResponseAccumulator += text; + postMessage({ type: 'newToken', payload: { token: text } }); } } } diff --git a/mzta-background.js b/mzta-background.js index 99c90f24..4d202899 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -400,6 +400,56 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ break; // chatgpt_api - END + case 'google_gemini_api': + // We are using the Google Gemini API + + let rand_call_id5 = '_google_gemini_' + generateCallID(); + + const listener5 = (message, sender, sendResponse) => { + + function handleChatGptApi(createdTab) { + let mailMessageId5 = -1; + if(mailMessage) mailMessageId5 = mailMessage.id; + + // check if the config is present, or give a message error + if (prefs.chatgpt_api_key == '') { + browser.tabs.sendMessage(createdTab.id, { command: "api_error", error: browser.i18n.getMessage('google_gemini_empty_apikey')}); + return; + } + if (prefs.chatgpt_model == '') { + browser.tabs.sendMessage(createdTab.id, { command: "api_error", error: browser.i18n.getMessage('google_gemini_empty_model')}); + return; + } + //console.log(">>>>>>>>>> sender: " + JSON.stringify(sender)); + browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId5, do_custom_text: do_custom_text}); + taLog.log('[Google Gemini] Connection succeded!'); + browser.runtime.onMessage.removeListener(listener5); + } + + if (message.command === "google_gemini_api_ready_"+rand_call_id5) { + return handleChatGptApi(sender.tab); + } + return false; + } + + browser.runtime.onMessage.addListener(listener5); + + let win_options5 = { + url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type+'&call_id='+rand_call_id5+'&ph_def_val='+(prefs.placeholders_use_default_value?'1':'0')), + type: "popup", + } + + taLog.log("[chatgpt_api] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height); + + if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){ + win_options5.width = prefs.chatgpt_win_width, + win_options5.height = prefs.chatgpt_win_height + } + + await browser.windows.create(win_options5); + + break; // google_gemini_api - END + case 'ollama_api': // We are using the Ollama API @@ -506,7 +556,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ await browser.windows.create(win_options4); - break; // openai_comp_api + break; // openai_comp_api - END default: taLog.error("Unknown API connection type: " + prefs.connection_type); break;