From bd87adc34bdf09dc28dde3a1d29828d3f4c1d24f Mon Sep 17 00:00:00 2001 From: DarokCx <77368869+DarokCx@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:07:44 -0400 Subject: [PATCH 1/4] Added Featherless.ai Integration --- package-lock.json | 16 +- package.json | 1 + src/renderer/packages/models/featherlessai.ts | 139 ++++++++++++++++++ src/renderer/packages/models/index.ts | 19 +++ .../SettingDialog/FeatherlessAISetting.tsx | 80 ++++++++++ .../pages/SettingDialog/ModelSettingTab.tsx | 4 + src/shared/defaults.ts | 5 + src/shared/types.ts | 8 + 8 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 src/renderer/packages/models/featherlessai.ts create mode 100644 src/renderer/pages/SettingDialog/FeatherlessAISetting.tsx diff --git a/package-lock.json b/package-lock.json index a9c59ae0..004840a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,7 @@ "electron": "^22.3.13", "electron-builder": "^24.13.3", "electron-devtools-installer": "^3.2.0", + "electron-reload": "^2.0.0-alpha.1", "electronmon": "^2.0.2", "eslint": "^8.42.0", "eslint-config-airbnb-base": "^15.0.0", @@ -7500,9 +7501,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "version": "1.0.30001655", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", + "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", "dev": true, "funding": [ { @@ -9705,6 +9706,15 @@ "node": ">=4.0.0" } }, + "node_modules/electron-reload": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz", + "integrity": "sha512-hTde7gv0TEqxbxlB3pj2CwoyCQ9sdiQrcP8GkpzhosxyVeYM3mZbMEVKCZK3L0fED7Mz5A9IWmK7zEvi4H3P1g==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2" + } + }, "node_modules/electron-store": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.1.0.tgz", diff --git a/package.json b/package.json index 6708b34a..21a5feb2 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "electron": "^22.3.13", "electron-builder": "^24.13.3", "electron-devtools-installer": "^3.2.0", + "electron-reload": "^2.0.0-alpha.1", "electronmon": "^2.0.2", "eslint": "^8.42.0", "eslint-config-airbnb-base": "^15.0.0", diff --git a/src/renderer/packages/models/featherlessai.ts b/src/renderer/packages/models/featherlessai.ts new file mode 100644 index 00000000..b3e686a6 --- /dev/null +++ b/src/renderer/packages/models/featherlessai.ts @@ -0,0 +1,139 @@ +import { Message } from 'src/shared/types' +import { ApiError, ChatboxAIAPIError } from './errors' +import Base, { onResultChange } from './base' + +interface Options { + featherlessKey: string + apiHost: string + apiPath?: string + model: Model | 'custom-model' + openaiCustomModel?: string + temperature: number + topP: number +} + +export default class FeatherlessAI extends Base { + public name = 'FeatherlessAI' + + public options: Options + constructor(options: Options) { + super() + this.options = options + if (this.options.apiHost && this.options.apiHost.trim().length === 0) { + this.options.apiHost = 'https://api.featerless.ai' + } + if (this.options.apiHost && this.options.apiHost.startsWith('https://openrouter.ai/api/v1')) { + this.options.apiHost = 'https://openrouter.ai/api' + } + if (this.options.apiPath && !this.options.apiPath.startsWith('/')) { + this.options.apiPath = '/' + this.options.apiPath + } + } + + async callChatCompletion(rawMessages: Message[], signal?: AbortSignal, onResultChange?: onResultChange): Promise { + try { + return await this._callChatCompletion(rawMessages, signal, onResultChange) + } catch (e) { + if (e instanceof ApiError && e.message.includes('Invalid content type. image_url is only supported by certain models.')) { + throw ChatboxAIAPIError.fromCodeName('model_not_support_image', 'model_not_support_image') + } + throw e + } + } + + async _callChatCompletion(rawMessages: Message[], signal?: AbortSignal, onResultChange?: onResultChange): Promise { + let messages = await populateOpenAIMessage(rawMessages, this.options.model) + + const model = this.options.model === 'custom-model' + ? this.options.openaiCustomModel || '' + : this.options.model + messages = injectModelSystemPrompt(model, messages) + + const apiPath = this.options.apiPath || '/v1/chat/completions' + const response = await this.post( + `${this.options.apiHost}${apiPath}`, + this.getHeaders(), + { + messages, + model, + max_tokens: this.options.model === 'custom-model' ? undefined : openaiModelConfigs[this.options.model].maxTokens, + temperature: this.options.temperature, + top_p: this.options.topP, + stream: true, + }, + signal + ) + let result = '' + await this.handleSSE(response, (message) => { + if (message === '[DONE]') { + return + } + const data = JSON.parse(message) + if (data.error) { + throw new ApiError(`Error from OpenAI: ${JSON.stringify(data)}`) + } + const text = data.choices[0]?.delta?.content + if (text !== undefined) { + result += text + if (onResultChange) { + onResultChange(result) + } + } + }) + return result + } + + getHeaders() { + const headers: Record = { + Authorization: `Bearer ${this.options.featherlessKey}`, + 'Content-Type': 'application/json', + 'X-Title': 'chatbox', + } + if (this.options.apiHost.includes('openrouter.ai')) { + headers['HTTP-Referer'] = 'https://localhost:3000/' + } + return headers + } + +} +//todo: implement the following functions +// Ref: https://platform.openai.com/docs/models/gpt-4 +export const openaiModelConfigs = { + 'anthracite-org/magnum-v2-72b': { + maxTokens: 4096, + maxContextTokens: 4096, + }, + +} +export type Model = keyof typeof openaiModelConfigs +export const models = Array.from(Object.keys(openaiModelConfigs)).sort() as Model[] + +export async function populateOpenAIMessage(rawMessages: Message[], model: Model | 'custom-model'): Promise { + return populateOpenAIMessageText(rawMessages) +} + +export async function populateOpenAIMessageText(rawMessages: Message[]): Promise { + const messages: OpenAIMessage[] = rawMessages.map((m) => ({ + role: m.role, + content: m.content, + })) + return messages +} + +export function injectModelSystemPrompt(model: string, messages: OpenAIMessage[]) { + for (const message of messages) { + if (message.role === 'system') { + if (typeof message.content == 'string') { + message.content = `Current model: ${model}\n\n` + message.content + } + break + } + } + return messages +} + +export interface OpenAIMessage { + role: 'system' | 'user' | 'assistant' + content: string + name?: string +} diff --git a/src/renderer/packages/models/index.ts b/src/renderer/packages/models/index.ts index 23c5c07e..fc7acfa0 100644 --- a/src/renderer/packages/models/index.ts +++ b/src/renderer/packages/models/index.ts @@ -3,6 +3,7 @@ import { Settings, Config, ModelProvider, SessionType, ModelSettings, Session } import ChatboxAI from './chatboxai' import Ollama from './ollama' import SiliconFlow from './siliconflow' +import FeatherlessAI from './featherlessai' export function getModel(setting: Settings, config: Config) { switch (setting.aiProvider) { @@ -10,6 +11,8 @@ export function getModel(setting: Settings, config: Config) { return new ChatboxAI(setting, config) case ModelProvider.OpenAI: return new OpenAI(setting) + case ModelProvider.FeatherlessAI: + return new FeatherlessAI(setting) case ModelProvider.Ollama: return new Ollama(setting) case ModelProvider.SiliconFlow: @@ -21,6 +24,7 @@ export function getModel(setting: Settings, config: Config) { export const aiProviderNameHash = { [ModelProvider.OpenAI]: 'OpenAI API', + [ModelProvider.FeatherlessAI]: 'FeatherlessAI', [ModelProvider.ChatboxAI]: 'Chatbox AI', [ModelProvider.Ollama]: 'Ollama', [ModelProvider.SiliconFlow]: 'SiliconCloud API', @@ -38,6 +42,11 @@ export const AIModelProviderMenuOptionList = [ label: aiProviderNameHash[ModelProvider.OpenAI], disabled: false, }, + { + value: ModelProvider.FeatherlessAI, + label: aiProviderNameHash[ModelProvider.FeatherlessAI], + disabled: false, + }, { value: ModelProvider.Ollama, label: aiProviderNameHash[ModelProvider.Ollama], @@ -64,6 +73,16 @@ export function getModelDisplayName(settings: Settings, sessionType: SessionType return `OpenAI Custom Model (${name})` } return settings.model || 'unknown' + case ModelProvider.FeatherlessAI: + if (settings.model === 'custom-model') { + let name = settings.featherlessCustomModel || '' + if (name.length >= 10) { + name = name.slice(0, 10) + '...' + } + return `FeatherlessAI Custom Model (${name})` + } + return settings.model || 'unknown' + case ModelProvider.ChatboxAI: const model = settings.chatboxAIModel || 'chatboxai-3.5' return model.replace('chatboxai-', 'Chatbox AI ') diff --git a/src/renderer/pages/SettingDialog/FeatherlessAISetting.tsx b/src/renderer/pages/SettingDialog/FeatherlessAISetting.tsx new file mode 100644 index 00000000..f874f83a --- /dev/null +++ b/src/renderer/pages/SettingDialog/FeatherlessAISetting.tsx @@ -0,0 +1,80 @@ +import { Typography, Box } from '@mui/material' +import { ModelSettings } from '../../../shared/types' +import { useTranslation } from 'react-i18next' +import { Accordion, AccordionSummary, AccordionDetails } from '../../components/Accordion' +import TemperatureSlider from '../../components/TemperatureSlider' +import TopPSlider from '../../components/TopPSlider' +import PasswordTextField from '../../components/PasswordTextField' +import MaxContextMessageCountSlider from '../../components/MaxContextMessageCountSlider' +import OpenAIModelSelect from '../../components/OpenAIModelSelect' +import TextFieldReset from '@/components/TextFieldReset' + +interface ModelConfigProps { + settingsEdit: ModelSettings + setSettingsEdit: (settings: ModelSettings) => void +} + +export default function FeatherlessAISetting(props: ModelConfigProps) { + const { settingsEdit, setSettingsEdit } = props + const { t } = useTranslation() + return ( + + { + setSettingsEdit({ ...settingsEdit, featherlessKey: value }) + }} + placeholder="rc-xxxxxxxxxxxxxxxxxxxxxxxx" + /> + <> + { + value = value.trim() + if (value.length > 4 && !value.startsWith('http')) { + value = 'https://' + value + } + setSettingsEdit({ ...settingsEdit, apiHost: value }) + }} + /> + + + + + {t('model')} & {t('token')}{' '} + + + + + setSettingsEdit({ ...settingsEdit, model, featherlessCustomModel }) + } + /> + + setSettingsEdit({ ...settingsEdit, temperature: value })} + /> + setSettingsEdit({ ...settingsEdit, topP: v })} + /> + setSettingsEdit({ ...settingsEdit, openaiMaxContextMessageCount: v })} + /> + + + + ) +} diff --git a/src/renderer/pages/SettingDialog/ModelSettingTab.tsx b/src/renderer/pages/SettingDialog/ModelSettingTab.tsx index 9e33d570..2d8d10b6 100644 --- a/src/renderer/pages/SettingDialog/ModelSettingTab.tsx +++ b/src/renderer/pages/SettingDialog/ModelSettingTab.tsx @@ -2,6 +2,7 @@ import { Divider, Box } from '@mui/material' import { ModelProvider, ModelSettings } from '../../../shared/types' import OpenAISetting from './OpenAISetting' import ChatboxAISetting from './ChatboxAISetting' +import FeatherlessAISetting from './FeatherlessAISetting' import AIProviderSelect from '../../components/AIProviderSelect' import { OllamaHostInput, OllamaModelSelect } from './OllamaSetting' import SiliconFlowSetting from './SiliconFlowSetting' @@ -25,6 +26,9 @@ export default function ModelSettingTab(props: ModelConfigProps) { {settingsEdit.aiProvider === ModelProvider.OpenAI && ( )} + {settingsEdit.aiProvider === ModelProvider.FeatherlessAI && ( + + )} {settingsEdit.aiProvider === ModelProvider.ChatboxAI && ( )} diff --git a/src/shared/defaults.ts b/src/shared/defaults.ts index 504c79c4..a930835c 100644 --- a/src/shared/defaults.ts +++ b/src/shared/defaults.ts @@ -44,6 +44,11 @@ export function settings(): Settings { siliconCloudHost: 'https://api.siliconflow.cn', siliconCloudKey: '', siliconCloudModel: 'THUDM/glm-4-9b-chat', + + featherlessKey:'', + featherlessApiHost: '', + featherlessApiPath: '', + featherlessModel: 'custom-model', } } diff --git a/src/shared/types.ts b/src/shared/types.ts index 8b0d08c2..1c04c069 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -66,6 +66,7 @@ export enum ModelProvider { OpenAI = 'openai', Ollama = 'ollama', SiliconFlow = 'silicon-flow', + FeatherlessAI = "FeatherlessAI", } export interface ModelSettings { @@ -76,6 +77,13 @@ export interface ModelSettings { apiHost: string model: Model | 'custom-model' openaiCustomModel?: string + + // featherlessai + featherlessKey: string + featherlessApiHost: string + featherlessApiPath: string + featherlessModel: Model | 'custom-model' + featherlessCustomModel?: string // azure azureEndpoint: string From 49ddcffd881bd6dc091a6cb5e614254c0e6a6953 Mon Sep 17 00:00:00 2001 From: DarokCx <77368869+DarokCx@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:13:39 -0400 Subject: [PATCH 2/4] Merge from main --- src/renderer/components/AIProviderSelect.tsx | 88 +++--- src/renderer/components/Accordion.tsx | 3 +- src/renderer/components/Header.tsx | 9 +- src/renderer/components/InputBox.tsx | 51 ++-- src/renderer/components/Markdown.tsx | 105 +++---- src/renderer/components/Message.tsx | 81 +++-- src/renderer/components/MessageErrTips.tsx | 44 +-- src/renderer/components/MessageList.tsx | 28 +- src/renderer/components/MiniButton.tsx | 20 +- src/renderer/components/OpenAIModelSelect.tsx | 4 +- src/renderer/components/SessionItem.tsx | 8 +- src/renderer/components/SessionList.tsx | 9 +- .../components/SiliconFlowModelSelect.tsx | 13 +- src/renderer/components/StyledMenu.tsx | 2 +- src/renderer/components/TextFieldReset.tsx | 24 +- src/renderer/components/Toasts.tsx | 2 +- src/renderer/components/Toolbar.tsx | 22 +- src/renderer/hooks/useAppTheme.ts | 10 +- .../hooks/useDefaultSystemLanguage.ts | 2 +- src/renderer/packages/event.ts | 2 +- src/renderer/packages/models/base.ts | 36 ++- src/renderer/packages/models/chatboxai.ts | 14 +- src/renderer/packages/models/errors.ts | 68 +++-- src/renderer/packages/models/featherlessai.ts | 33 +- src/renderer/packages/models/ollama.ts | 16 +- src/renderer/packages/models/openai.ts | 34 ++- src/renderer/packages/platform.ts | 4 +- src/renderer/packages/prompts.ts | 2 +- src/renderer/packages/remote.ts | 16 +- src/renderer/pages/AboutWindow.tsx | 32 +- src/renderer/pages/ChatConfigWindow.tsx | 20 +- src/renderer/pages/CleanWindow.tsx | 3 +- src/renderer/pages/CopilotWindow.tsx | 36 +-- .../SettingDialog/AdvancedSettingTab.tsx | 19 +- .../pages/SettingDialog/ChatSettingTab.tsx | 2 +- .../pages/SettingDialog/ChatboxAISetting.tsx | 286 +++++++++--------- .../pages/SettingDialog/DisplaySettingTab.tsx | 4 +- .../SettingDialog/FeatherlessAISetting.tsx | 2 +- .../pages/SettingDialog/ModelSettingTab.tsx | 7 +- .../pages/SettingDialog/OllamaSetting.tsx | 36 ++- .../pages/SettingDialog/OpenAISetting.tsx | 2 +- src/renderer/setup/ga_init.ts | 5 +- src/renderer/setup/sentry_init.ts | 3 +- src/renderer/storage/StoreStorage.ts | 2 +- src/renderer/stores/atoms.ts | 6 +- src/renderer/stores/premiumActions.ts | 10 +- src/renderer/stores/sessionActions.ts | 39 +-- src/shared/defaults.ts | 6 +- src/shared/types.ts | 4 +- 49 files changed, 634 insertions(+), 640 deletions(-) diff --git a/src/renderer/components/AIProviderSelect.tsx b/src/renderer/components/AIProviderSelect.tsx index e081a3b8..1a819318 100644 --- a/src/renderer/components/AIProviderSelect.tsx +++ b/src/renderer/components/AIProviderSelect.tsx @@ -2,11 +2,11 @@ import { Chip, MenuItem, Typography } from '@mui/material' import { ModelProvider, ModelSettings } from '../../shared/types' import { useTranslation } from 'react-i18next' import { AIModelProviderMenuOptionList } from '../packages/models' -import * as React from 'react'; -import Button from '@mui/material/Button'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import StyledMenu from './StyledMenu'; -import StarIcon from '@mui/icons-material/Star'; +import * as React from 'react' +import Button from '@mui/material/Button' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import StyledMenu from './StyledMenu' +import StarIcon from '@mui/icons-material/Star' interface ModelConfigProps { settings: ModelSettings @@ -19,29 +19,25 @@ export default function AIProviderSelect(props: ModelConfigProps) { const { settings, setSettings, className, hideCustomProviderManage } = props const { t } = useTranslation() - const [menuAnchorEl, setMenuAnchorEl] = React.useState(null); - const menuState = Boolean(menuAnchorEl); + const [menuAnchorEl, setMenuAnchorEl] = React.useState(null) + const menuState = Boolean(menuAnchorEl) const openMenu = (event: React.MouseEvent) => { - setMenuAnchorEl(event.currentTarget); - }; + setMenuAnchorEl(event.currentTarget) + } const closeMenu = () => { - setMenuAnchorEl(null); - }; + setMenuAnchorEl(null) + } return ( <> - + {t('Model Provider')}: -
- - { - AIModelProviderMenuOptionList.map((provider) => ( - { - setSettings({ - ...settings, - aiProvider: provider.value as ModelProvider, - }) - closeMenu() - }} - > - - {provider.label} - {provider.featured && ( - - )} - - )) - } + {AIModelProviderMenuOptionList.map((provider) => ( + { + setSettings({ + ...settings, + aiProvider: provider.value as ModelProvider, + }) + closeMenu() + }} + > + + {provider.label} + {provider.featured && ( + + )} + + ))}
) } - diff --git a/src/renderer/components/Accordion.tsx b/src/renderer/components/Accordion.tsx index a224a87d..10a9fc23 100644 --- a/src/renderer/components/Accordion.tsx +++ b/src/renderer/components/Accordion.tsx @@ -8,8 +8,7 @@ export const Accordion = styled((props: AccordionProps) => ( ))(({ theme }) => ({ border: `1px solid ${theme.palette.divider}`, - '&:not(:last-child)': { - }, + '&:not(:last-child)': {}, '&:before': { display: 'none', }, diff --git a/src/renderer/components/Header.tsx b/src/renderer/components/Header.tsx index 07c0bc86..ce3553fb 100644 --- a/src/renderer/components/Header.tsx +++ b/src/renderer/components/Header.tsx @@ -7,7 +7,7 @@ import * as sessionActions from '../stores/sessionActions' import Toolbar from './Toolbar' import { cn } from '@/lib/utils' -interface Props { } +interface Props {} export default function Header(props: Props) { const theme = useTheme() @@ -15,12 +15,9 @@ export default function Header(props: Props) { const setChatConfigDialogSession = useSetAtom(atoms.chatConfigDialogAtom) useEffect(() => { - if ( - currentSession.name === 'Untitled' - && currentSession.messages.length >= 2 - ) { + if (currentSession.name === 'Untitled' && currentSession.messages.length >= 2) { sessionActions.generateName(currentSession.id) - return + return } }, [currentSession.messages.length]) diff --git a/src/renderer/components/InputBox.tsx b/src/renderer/components/InputBox.tsx index 15a3cf1a..f24f7745 100644 --- a/src/renderer/components/InputBox.tsx +++ b/src/renderer/components/InputBox.tsx @@ -5,10 +5,7 @@ import { useTranslation } from 'react-i18next' import * as atoms from '../stores/atoms' import { useSetAtom } from 'jotai' import * as sessionActions from '../stores/sessionActions' -import { - SendHorizontal, - Settings2, -} from 'lucide-react' +import { SendHorizontal, Settings2 } from 'lucide-react' import { cn } from '@/lib/utils' import icon from '../static/icon.png' import { trackingEvent } from '@/packages/event' @@ -50,13 +47,7 @@ export default function InputBox(props: Props) { } const onKeyDown = (event: React.KeyboardEvent) => { - if ( - event.keyCode === 13 && - !event.shiftKey && - !event.ctrlKey && - !event.altKey && - !event.metaKey - ) { + if (event.keyCode === 13 && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { event.preventDefault() handleSubmit() return @@ -71,7 +62,8 @@ export default function InputBox(props: Props) { const [easterEgg, setEasterEgg] = useState(false) return ( -
-
-
- +
+ { setEasterEgg(true) setTimeout(() => setEasterEgg(false), 1000) @@ -89,20 +83,23 @@ export default function InputBox(props: Props) { > - setChatConfigDialogSession(sessionActions.getCurrentSession())} tooltipTitle={ -
+
{t('Customize settings for the current conversation')}
} - tooltipPlacement='top' + tooltipPlacement="top" > - +
-
- + } - tooltipPlacement='top' + tooltipPlacement="top" onClick={() => handleSubmit()} > - +
-
+