diff --git a/public/index.html b/public/index.html
index 47dfc8c7a..ad010165c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1118,7 +1118,14 @@
diff --git a/public/notes/content.md b/public/notes/content.md
index e2b64c967..8437f2781 100644
--- a/public/notes/content.md
+++ b/public/notes/content.md
@@ -388,6 +388,15 @@ If your subscription tier is Paper, Tablet or Scroll use only Euterpe model othe
_Lost API keys can't be restored! Make sure to keep it safe!_
+### Window.ai
+
+You can use window.ai browser extension to access AI models with SillyTavern.
+
+1. Install a browser extension from: [windowai.io](https://windowai.io/)
+2. Create an OpenRouter account: [openrouter.ai](https://openrouter.ai/)
+3. Select OpenRouter as a provider in Window.ai extension.
+4. Use OpenAI API provider and enable "Use Window.ai" option in SillyTavern
+
## Poe
### API key
diff --git a/public/script.js b/public/script.js
index f3b0a59ba..e68dcc5a6 100644
--- a/public/script.js
+++ b/public/script.js
@@ -3725,6 +3725,10 @@ function changeMainAPI() {
main_api = selectedVal;
online_status = "no_connection";
+ if (main_api == 'openai' && oai_settings.use_window_ai) {
+ $('#api_button_openai').trigger('click');
+ }
+
if (main_api == "koboldhorde") {
is_get_status = true;
getStatus();
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 80c93f6cb..c8c664f3a 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -104,6 +104,7 @@ const default_settings = {
jailbreak_system: false,
reverse_proxy: '',
legacy_streaming: false,
+ use_window_ai: false,
};
const oai_settings = {
@@ -129,6 +130,7 @@ const oai_settings = {
jailbreak_system: false,
reverse_proxy: '',
legacy_streaming: false,
+ use_window_ai: false,
};
let openai_setting_names;
@@ -550,6 +552,41 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
"logit_bias": logit_bias,
};
+ if (oai_settings.use_window_ai) {
+ if (!('ai' in window)) {
+ return showWindowExtensionError();
+ }
+
+ async function* windowStreamingFunction(res) {
+ yield (res?.message?.content || '');
+ }
+
+ const generatePromise = window.ai.generateText(
+ {
+ messages: openai_msgs_tosend,
+ },
+ {
+ temperature: parseFloat(oai_settings.temp_openai),
+ maxTokens: oai_settings.openai_max_tokens,
+ onStreamResult: windowStreamingFunction,
+ }
+ );
+
+ if (stream) {
+ return windowStreamingFunction;
+ }
+
+ try {
+ const [{ message }] = await generatePromise;
+ windowStreamingFunction(message);
+ return message?.content;
+ } catch (err) {
+ const text = parseWindowError(err);
+ toastr.error(text, 'Window.ai returned an error');
+ throw err;
+ }
+ }
+
const generate_url = '/generate_openai';
const response = await fetch(generate_url, {
method: 'POST',
@@ -614,6 +651,30 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
}
}
+function parseWindowError(err) {
+ let text = 'Unknown error';
+
+ switch (err) {
+ case "NOT_AUTHENTICATED":
+ text = 'Incorrect API key / auth';
+ break;
+ case "MODEL_REJECTED_REQUEST":
+ text = 'AI model refused to fulfill a request';
+ break;
+ case "PERMISSION_DENIED":
+ text = 'User denied permission to the app';
+ break;
+ case "REQUEST_NOT_FOUND":
+ text = 'Permission request popup timed out';
+ break;
+ case "INVALID_REQUEST":
+ text = 'Malformed request';
+ break;
+ }
+
+ return text;
+}
+
async function calculateLogitBias() {
const body = JSON.stringify(oai_settings.bias_presets[oai_settings.bias_preset_selected]);
let result = {};
@@ -813,10 +874,27 @@ function loadOpenAISettings(data, settings) {
$('#openai_logit_bias_preset').append(option);
}
$('#openai_logit_bias_preset').trigger('change');
+
+ $('#use_window_ai').prop('checked', oai_settings.use_window_ai);
+ $('#openai_form').toggle(!oai_settings.use_window_ai);
}
async function getStatusOpen() {
if (is_get_status_openai) {
+ if (oai_settings.use_window_ai) {
+ let status;
+
+ if ('ai' in window) {
+ status = 'Valid';
+ }
+ else {
+ showWindowExtensionError();
+ status = 'no_connection';
+ }
+
+ setOnlineStatus(status);
+ return resultCheckStatusOpen();
+ }
let data = {
reverse_proxy: oai_settings.reverse_proxy,
@@ -851,6 +929,15 @@ async function getStatusOpen() {
}
}
+function showWindowExtensionError() {
+ toastr.error('Get it here:
windowai.io', 'Extension is not installed', {
+ escapeHtml: false,
+ timeOut: 0,
+ extendedTimeOut: 0,
+ preventDuplicates: true,
+ });
+}
+
function resultCheckStatusOpen() {
is_api_button_press_openai = false;
checkOnlineStatus();
@@ -1221,6 +1308,13 @@ function onReverseProxyInput() {
async function onConnectButtonClick(e) {
e.stopPropagation();
+
+ if (oai_settings.use_window_ai) {
+ is_get_status_openai = true;
+ is_api_button_press_openai = true;
+ return await getStatusOpen();
+ }
+
const api_key_openai = $('#api_key_openai').val().trim();
if (api_key_openai.length) {
@@ -1386,6 +1480,15 @@ $(document).ready(function () {
saveSettingsDebounced();
});
+ $('#use_window_ai').on('input', function() {
+ oai_settings.use_window_ai = !!$(this).prop('checked');
+ $('#openai_form').toggle(!oai_settings.use_window_ai);
+ setOnlineStatus('no_connection');
+ resultCheckStatusOpen();
+ $('#api_button_openai').trigger('click');
+ saveSettingsDebounced();
+ });
+
$("#api_button_openai").on("click", onConnectButtonClick);
$("#openai_reverse_proxy").on("input", onReverseProxyInput);
$("#model_openai_select").on("change", onModelChange);