Skip to content

Commit

Permalink
Add support for GPT4Free Local API (#54)
Browse files Browse the repository at this point in the history
The extension now supports integration with the locally hosted API
feature of [the gpt4free project](https://github.com/xtekky/gpt4free).
You can easily host your own gpt4free API locally, and connect it to the
extension. This opens up many possibilities, allowing for a great
variety of up-to-date providers.

Read more here:
- https://github.com/xtekky/gpt4free/blob/main/docs/interference.md
- Help page:
https://github.com/XInTheDark/raycast-g4f/wiki/Help-page:-GPT4Free-Local-API
  • Loading branch information
XInTheDark authored Jun 23, 2024
1 parent 3fa12b0 commit 37472ca
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 17 deletions.
Binary file added assets/box-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,19 @@
"description": "Check for extension updates from the official GitHub source",
"icon": "update-icon.png",
"mode": "view"
},
{
"name": "configureG4FLocalApi",
"title": "Configure GPT4Free Local API",
"description": "Configure GPT4Free Local API settings",
"icon": "box-icon.png",
"mode": "view"
}
],
"preferences": [
{
"name": "gptProvider",
"title": "Default GPT Provider",
"title": "Default Provider",
"description": "The default provider and model used in this extension.",
"required": false,
"type": "dropdown",
Expand Down Expand Up @@ -241,6 +248,10 @@
{
"title": "Google Gemini (requires API Key)",
"value": "GoogleGemini"
},
{
"title": "GPT4Free Local API",
"value": "G4FLocal"
}
],
"default": "GPT35"
Expand Down
4 changes: 2 additions & 2 deletions src/aiChat.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export default function Chat({ launchContext }) {
}
>
<Form.TextArea id="chatText" title="Chat Transcript" />
<Form.Description title="GPT Model" text="The provider and model used for this chat." />
<Form.Description title="Provider" text="The provider and model used for this chat." />
<Form.Dropdown id="provider" defaultValue={providers.default_provider_string()}>
{providers.ChatProvidersReact}
</Form.Dropdown>
Expand Down Expand Up @@ -416,7 +416,7 @@ export default function Chat({ launchContext }) {
second: "2-digit",
})}`}
/>
<Form.Description title="GPT Model" text="The provider and model used for this chat." />
<Form.Description title="Provider" text="The provider and model used for this chat." />
<Form.Dropdown id="provider" defaultValue={defaultProviderString}>
{providers.ChatProvidersReact}
</Form.Dropdown>
Expand Down
14 changes: 6 additions & 8 deletions src/api/Providers/deepinfra.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,27 @@ export const getDeepInfraResponse = async function* (chat, options, max_retries
const reader = response.body;
for await (let chunk of reader) {
const str = chunk.toString();

let lines = str.split("\n");

for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (line.startsWith("data: ")) {
let chunk = line.substring(6);
if (chunk.trim() === "[DONE]") return; // trim() is important

let data = JSON.parse(chunk);
let choice = data["choices"][0];
// python: if "content" in choice["delta"] and choice["delta"]["content"]:
if ("delta" in choice && "content" in choice["delta"] && choice["delta"]["content"]) {
let delta = choice["delta"]["content"];
try {
let data = JSON.parse(chunk);
let delta = data["choices"][0]["delta"]["content"];
if (first) {
delta = delta.trimStart();
}
if (delta) {
first = false;
yield delta;
}
}
} catch (e) {} // eslint-disable-line
}
} // readline
}
}
} catch (e) {
if (max_retries > 0) {
Expand Down
4 changes: 4 additions & 0 deletions src/api/Providers/g4f.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { G4F } from "g4f";
const g4f = new G4F();
import { messages_to_json } from "../../classes/message";

export const G4FProvider = {
GPT: g4f.providers.GPT,
Expand All @@ -10,5 +11,8 @@ export const getG4FResponse = async (chat, options) => {
// replace "g4f_provider" key in options with "provider"
options.provider = options.g4f_provider; // this is a member of G4FProvider enum
delete options.g4f_provider;

chat = messages_to_json(chat);

return await g4f.chatCompletion(chat, options);
};
136 changes: 136 additions & 0 deletions src/api/Providers/g4f_local.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// This module allows communication and requests to the local G4F API.
// Read more here: https://github.com/xtekky/gpt4free/blob/main/docs/interference.md

import { exec } from "child_process";
import fetch from "node-fetch";

import { Storage } from "../storage";
import { messages_to_json } from "../../classes/message";

import { environment, Form } from "@raycast/api";

// constants
const DEFAULT_MODEL = "meta-ai";
export const DEFAULT_TIMEOUT = "900";

const BASE_URL = "http://localhost:1337/v1";
const API_URL = "http://localhost:1337/v1/chat/completions";
const MODELS_URL = "http://localhost:1337/v1/models";

// main function
export const G4FLocalProvider = "G4FLocalProvider";
export const getG4FLocalResponse = async function* (chat, options) {
if (!(await isG4FRunning())) {
await startG4F();
}

chat = messages_to_json(chat);
const model = await getSelectedG4FModel();

const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: model,
stream: options.stream,
messages: chat,
}),
});

// Important! we assume that the response is a stream, as this is true for most G4F models.
// If in the future this is not the case, we should add separate handling for non-streaming responses.
const reader = response.body;
for await (let chunk of reader) {
const str = chunk.toString();
let lines = str.split("\n");
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (line.startsWith("data: ")) {
let chunk = line.substring(6);
if (chunk.trim() === "[DONE]") return; // trim() is important

try {
let data = JSON.parse(chunk);
let delta = data["choices"][0]["delta"]["content"];
if (delta) {
yield delta;
}
} catch (e) {} // eslint-disable-line
}
}
}
};

/// utilities

// check if the G4F API is running
// with a request timeout of 0.5 seconds (since it's localhost)
const isG4FRunning = async () => {
try {
const controller = new AbortController();
setTimeout(() => controller.abort(), 500);
const response = await fetch(BASE_URL, { signal: controller.signal });
return response.ok;
} catch (e) {
return false;
}
};

// get available models
const getG4FModels = async () => {
try {
const response = await fetch(MODELS_URL);
return (await response.json()).data || [];
} catch (e) {
return [];
}
};

// get available models as dropdown component
export const getG4FModelsDropdown = async () => {
const models = await getG4FModels();
return (
<Form.Dropdown id="model" title="Model" defaultValue={await getSelectedG4FModel()}>
{models.map((model) => {
return <Form.Dropdown.Item title={model.id} key={model.id} value={model.id} />;
})}
</Form.Dropdown>
);
};

// get G4F executable path from storage
export const getG4FExecutablePath = async () => {
return await Storage.read("g4f_executable", "g4f");
};

// get the currently selected G4F model from storage
const getSelectedG4FModel = async () => {
return await Storage.read("g4f_model", DEFAULT_MODEL);
};

// get G4F API timeout (in seconds) from storage
export const getG4FTimeout = async () => {
return parseInt(await Storage.read("g4f_timeout", DEFAULT_TIMEOUT)) || parseInt(DEFAULT_TIMEOUT);
};

// start the G4F API
const startG4F = async () => {
const exe = await getG4FExecutablePath();
const timeout_s = await getG4FTimeout();
const START_COMMAND = `export PATH="/opt/homebrew/bin:$PATH"; ( ${exe} api ) & sleep ${timeout_s} ; kill $!`;
const dirPath = environment.supportPath;
try {
const child = exec(START_COMMAND, { cwd: dirPath });
console.log("G4F API Process ID:", child.pid);
child.stderr.on("data", (data) => {
console.log("g4f >", data);
});
// sleep for some time to allow the API to start
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log(`G4F API started with timeout ${timeout_s}`);
} catch (e) {
console.log(e);
}
};
4 changes: 1 addition & 3 deletions src/api/Providers/google_gemini.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ export const getGoogleGeminiResponse = async (chat, options, stream_update, max_
response = await geminiChat.ask(query, { safetySettings: safetySettings });
return response;
}
} catch (e) {
continue;
}
} catch (e) {} // eslint-disable-line
}
} catch (e) {
// if all API keys fail, we allow a few retries
Expand Down
3 changes: 3 additions & 0 deletions src/api/gpt.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,9 @@ export const chatCompletion = async (chat, options, stream_update = null, status
} else if (provider === providers.G4FProvider) {
// G4F
response = await providers.getG4FResponse(chat, options);
} else if (provider === providers.G4FLocalProvider) {
// G4F Local
response = await providers.getG4FLocalResponse(chat, options);
}

// stream = false
Expand Down
8 changes: 7 additions & 1 deletion src/api/providers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ export { ReplicateProvider, getReplicateResponse };
import { GeminiProvider, getGoogleGeminiResponse } from "./Providers/google_gemini";
export { GeminiProvider, getGoogleGeminiResponse };

// G4F Local module
import { G4FLocalProvider, getG4FLocalResponse } from "./Providers/g4f_local";
export { G4FLocalProvider, getG4FLocalResponse };

/// All providers info
// {provider, model, stream, extra options}
// { provider internal name, {provider, model, stream, extra options} }
// prettier-ignore
export const providers_info = {
GPT35: { provider: NexraProvider, model: "chatgpt", stream: true },
Expand All @@ -51,6 +55,7 @@ export const providers_info = {
ReplicateLlama3_70B: { provider: ReplicateProvider, model: "meta/meta-llama-3-70b-instruct", stream: true },
ReplicateMixtral_8x7B: { provider: ReplicateProvider, model: "mistralai/mixtral-8x7b-instruct-v0.1", stream: true },
GoogleGemini: { provider: GeminiProvider, model: "gemini-1.5-flash-latest", stream: true },
G4FLocal: { provider: G4FLocalProvider, stream: true },
};

/// Chat providers (user-friendly names)
Expand All @@ -69,6 +74,7 @@ export const chat_providers = [
["Replicate (meta-llama-3-70b)", "ReplicateLlama3_70B"],
["Replicate (mixtral-8x7b)", "ReplicateMixtral_8x7B"],
["Google Gemini (requires API Key)", "GoogleGemini"],
["GPT4Free Local API", "G4FLocal"],
];

export const ChatProvidersReact = chat_providers.map((x) => {
Expand Down
3 changes: 2 additions & 1 deletion src/api/storage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export const Storage = {
await Storage.run_sync();
},

// combined read function - read from local storage, fallback to file storage
// combined read function - read from local storage, fallback to file storage.
// also writes the default value to local storage if it is provided and key is not found
read: async (key, default_value = undefined) => {
let value;
if (await Storage.localStorage_has(key)) {
Expand Down
64 changes: 64 additions & 0 deletions src/configureG4FLocalApi.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getG4FExecutablePath, getG4FTimeout, DEFAULT_TIMEOUT, getG4FModelsDropdown } from "./api/Providers/g4f_local";
import { Storage } from "./api/storage";
import { help_action } from "./helpers/helpPage";

import { Form, ActionPanel, Action, useNavigation, showToast, Toast } from "@raycast/api";
import { useState, useEffect } from "react";

export default function ConfigureG4FLocalApi() {
const [executablePath, setExecutablePath] = useState("");
const [timeout, setTimeout] = useState("");
const [modelsDropdown, setModelsDropdown] = useState([]);
const [rendered, setRendered] = useState(false);

const { pop } = useNavigation();

useEffect(() => {
(async () => {
setExecutablePath(await getG4FExecutablePath());
setTimeout((await getG4FTimeout()).toString());
setModelsDropdown(await getG4FModelsDropdown());
setRendered(true);
})();
}, []);

return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm
title="Save"
onSubmit={async (values) => {
pop();
await Storage.write("g4f_executable", values.g4f_executable);
await Storage.write("g4f_timeout", values.g4f_timeout || DEFAULT_TIMEOUT);
await Storage.write("g4f_model", values.model);
await showToast(Toast.Style.Success, "Configuration Saved");
}}
/>
{help_action("g4fLocal")}
</ActionPanel>
}
>
<Form.Description text="Configure the GPT4Free Local API. Select 'Help' for the full guide." />
<Form.TextField
id="g4f_executable"
title="G4F Executable Path"
value={executablePath}
onChange={(x) => {
if (rendered) setExecutablePath(x);
}}
/>
<Form.TextField
id="g4f_timeout"
title="G4F API Timeout (in seconds)"
info="After this timeout, the G4F API will be stopped. It will be automatically started again when used. This saves resources when the API is not in use."
value={timeout}
onChange={(x) => {
if (rendered) setTimeout(x);
}}
/>
{modelsDropdown}
</Form>
);
}
2 changes: 1 addition & 1 deletion src/genImage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function genImage() {
if (err) {
toast(Toast.Style.Failure, "Error saving image");
console.log("Error saving image. Current path: " + __dirname);
console.error(err);
console.log(err);
imagePath = "";
}
});
Expand Down
1 change: 1 addition & 0 deletions src/helpers/helpPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const helpPages = {
aiChat: "https://github.com/XInTheDark/raycast-g4f/wiki/Help-page:-AI-Chat",
customAICommands: "https://github.com/XInTheDark/raycast-g4f/wiki/Help-page:-Custom-AI-Commands",
genImage: "https://github.com/XInTheDark/raycast-g4f/wiki/Help-page:-Generate-Images",
g4fLocal: "https://github.com/XInTheDark/raycast-g4f/wiki/Help-page:-GPT4Free-Local-API",
default: "https://github.com/XInTheDark/raycast-g4f/blob/main/README.md",
};

Expand Down

0 comments on commit 37472ca

Please sign in to comment.