Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for GPT4Free Local API #54

Merged
merged 5 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading