diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b35be7ba..8c29d11b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1474,5 +1474,25 @@ "prefs_OptionText_anthropic_max_tokens_Info": { "message": "The maximum number of tokens to generate in the completion. The token count of your prompt plus max_tokens cannot exceed the model's context length.", "description": "" + }, + "anthropic_empty_apikey": { + "message": "You've not added an API Key for the Anthropic API. Please insert one in the options page.", + "description": "" + }, + "anthropic_empty_model": { + "message": "You've not choosen a model for the Anthropic API. Please choose one in the options page.", + "description": "" + }, + "anthropic_empty_version": { + "message": "You've not added a version string for the Anthropic API. Please insert one in the options page.", + "description": "" + }, + "anthropic_api_request_failed": { + "message": "Anthropic API request failed", + "description": "" + }, + "anthropic_api_connecting": { + "message": "Attempting to connect to Anthropic API using the API key provided", + "description": "" } } \ No newline at end of file diff --git a/api_webchat/controller.js b/api_webchat/controller.js index 0666ae91..04b0478b 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -56,6 +56,12 @@ switch (llm) { case "openai_comp_api": worker = new Worker('../js/workers/model-worker-openai_comp.js', { type: 'module' }); break; + case "anthropic_api": + worker = new Worker('../js/workers/model-worker-anthropic.js', { type: 'module' }); + break; + default: + console.error('[ThunderAI] API WebChat Unknown LLM type:', llm); + break; } messagesArea.init(worker); @@ -113,6 +119,18 @@ switch (llm) { browser.runtime.sendMessage({command: "openai_comp_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id}); break; } + case "anthropic_api": { + let prefs_api = await browser.storage.sync.get({anthropic_api_key: '', anthropic_model: '', anthropic_version: '2023-06-01', anthropic_max_tokens: 4096, do_debug: false}); + let i18nStrings = {}; + i18nStrings["anthropic_api_request_failed"] = browser.i18n.getMessage('anthropic_api_request_failed'); + i18nStrings["error_connection_interrupted"] = browser.i18n.getMessage('error_connection_interrupted'); + messageInput.setModel(prefs_api.anthropic_model); + messagesArea.setLLMName("Anthropic"); + worker.postMessage({ type: 'init', anthropic_api_key: prefs_api.anthropic_api_key, anthropic_model: prefs_api.anthropic_model, anthropic_version: prefs_api.anthropic_version, anthropic_max_tokens: prefs_api.anthropic_max_tokens, do_debug: prefs_api.do_debug, i18nStrings: i18nStrings}); + messagesArea.appendUserMessage(browser.i18n.getMessage("anthropic_api_connecting") + " " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.anthropic_model + "\"...", "info"); + browser.runtime.sendMessage({command: "anthropic_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id}); + break; + } } //let prefs_ph = await browser.storage.sync.get({placeholders_use_default_value: false}); diff --git a/js/api/anthropic.js b/js/api/anthropic.js index ffb9c9e2..662b45df 100644 --- a/js/api/anthropic.js +++ b/js/api/anthropic.js @@ -27,7 +27,7 @@ export class Anthropic { max_tokens = 4096; stream = false; - constructor(apiKey, version, model, max_tokens, stream) { + constructor(apiKey, version, model, max_tokens = 4096, stream = false) { this.apiKey = apiKey; this.version = version; this.model = model; @@ -84,6 +84,7 @@ export class Anthropic { "Content-Type": "application/json", "x-api-key": this.apiKey, "anthropic-version": this.version, + "anthropic-dangerous-direct-browser-access": "true", }, body: JSON.stringify({ model: this.model, diff --git a/js/workers/model-worker-anthropic.js b/js/workers/model-worker-anthropic.js new file mode 100644 index 00000000..a16578c9 --- /dev/null +++ b/js/workers/model-worker-anthropic.js @@ -0,0 +1,148 @@ +/* + * 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 { Anthropic } from '../api/anthropic.js'; +import { taLogger } from '../mzta-logger.js'; + +let anthropic_api_key = null; +let anthropic_model = ''; +let anthropic = 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') { + anthropic_api_key = event.data.anthropic_api_key; + anthropic_model = event.data.anthropic_model; + anthropic = new Anthropic(anthropic_api_key, event.data.anthropic_version, anthropic_model, event.data.anthropic_max_tokens, true); + do_debug = event.data.do_debug; + i18nStrings = event.data.i18nStrings; + taLog = new taLogger('model-worker-anthropic', do_debug); + } else if (event.data.type === 'chatMessage') { + conversationHistory.push({ role: 'user', content: event.data.message }); + + const response = await anthropic.fetchResponse(conversationHistory); + 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["anthropic_api_request_failed"] + ": " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail }); + throw new Error("[ThunderAI] Anthropic 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 Anthropic response parsing stuff + const chunk = decoder.decode(value); + buffer += chunk; + taLog.log("buffer " + buffer); + const lines = buffer.split("\n"); + buffer = lines.pop(); + + for (const line of lines) { + const cleanLine = line.trim(); + + // Ignore ping events + if (cleanLine === '' || cleanLine.startsWith('event: ping')) { + continue; + } + + // Remove "data: " and parse the JSON + if (cleanLine.startsWith('data: ')) { + const jsonPart = cleanLine.replace(/^data: /, ''); + let parsedData = null; + + try { + parsedData = JSON.parse(jsonPart); + } catch (e) { + taLog.error("JSON parse error: " + e); + continue; + } + + // Events handling + switch (parsedData.type) { + case 'content_block_delta': + if (parsedData.delta && parsedData.delta.text) { + const token = parsedData.delta.text; + assistantResponseAccumulator += token; + postMessage({ type: 'newToken', payload: { token } }); + } + break; + + case 'content_block_start': + // optional + break; + + case 'message_start': + // optional + break; + + case 'message_stop': + conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + return; // end the loop + } + } + } + + } + } else if (event.data.type === 'stop') { + stopStreaming = true; + } +}; diff --git a/mzta-background.js b/mzta-background.js index 5c91c855..09e8394b 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -652,6 +652,61 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ } break; // openai_comp_api - END + case 'anthropic_api': + { + // We are using the Anthropic API + + let rand_call_id5 = '_anthropic_' + generateCallID(); + + const listener5 = (message, sender, sendResponse) => { + + function handleAnthropicApi(createdTab) { + let mailMessageId5 = -1; + if(mailMessage) mailMessageId5 = mailMessage.id; + + // check if the config is present, or give a message error + if (prefs.anthropic_api_key == '') { + browser.tabs.sendMessage(createdTab.id, { command: "api_error", error: browser.i18n.getMessage('anthropic_empty_apikey')}); + return; + } + if (prefs.anthropic_model == '') { + browser.tabs.sendMessage(createdTab.id, { command: "api_error", error: browser.i18n.getMessage('anthropic_empty_model')}); + return; + } + if (prefs.anthropic_version == '') { + browser.tabs.sendMessage(createdTab.id, { command: "api_error", error: browser.i18n.getMessage('anthropic_empty_version')}); + 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, prompt_info: prompt_info}); + taLog.log('[OpenAI ChatGPT] Connection succeded!'); + browser.runtime.onMessage.removeListener(listener5); + } + + if (message.command === "anthropic_api_ready_"+rand_call_id5) { + return handleAnthropicApi(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; // anthropic_api - END + default: taLog.error("Unknown API connection type: " + prefs.connection_type); break;