diff --git a/src/renderer/components/InfiniAIModelSelect.tsx b/src/renderer/components/InfiniAIModelSelect.tsx new file mode 100644 index 000000000..ac6f36b04 --- /dev/null +++ b/src/renderer/components/InfiniAIModelSelect.tsx @@ -0,0 +1,99 @@ +import { Select, MenuItem, FormControl, InputLabel, TextField } from '@mui/material' +import { ModelSettings } from '../../shared/types' +import { useTranslation } from 'react-i18next' +import { useState, useEffect } from 'react' + +export interface Props { + model: ModelSettings['infiniaiModel'] + infiniaiHost: string + infiniaiKey?: string + onChange(model: string): void + className?: string +} + +export default function InfiniAIModelSelect(props: Props) { + const { t } = useTranslation() + const [models, setModels] = useState([]) + const [loading, setLoading] = useState(true) + const [customModel, setCustomModel] = useState('') + + useEffect(() => { + if (!props.infiniaiHost) return + + const fetchModels = async () => { + try { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (props.infiniaiKey) { + headers['Authorization'] = `Bearer ${props.infiniaiKey}` + } + + const response = await fetch(`${props.infiniaiHost}/models`, { + method: 'GET', + headers + }) + + const data = await response.json() + if (data.data) { + const modelIds = data.data.map((m: any) => m.id) + setModels(modelIds) + } + } catch (error) { + console.error('Failed to fetch InfiniAI models:', error) + } finally { + setLoading(false) + } + } + + fetchModels() + }, [props.infiniaiHost, props.infiniaiKey]) + + useEffect(() => { + if (props.model !== 'custom-model' && props.model) { + setCustomModel(props.model) + } + }, [props.model]) + + const handleModelChange = (value: string) => { + if (value === 'custom-model') { + props.onChange(customModel || '') + } else { + props.onChange(value) + setCustomModel(value) + } + } + + return ( + + {t('model')} + + {props.model === 'custom-model' && ( + { + setCustomModel(e.target.value) + props.onChange(e.target.value) + }} + /> + )} + + ) +} diff --git a/src/renderer/packages/models/index.ts b/src/renderer/packages/models/index.ts index 6645ad9cc..62f4418bf 100644 --- a/src/renderer/packages/models/index.ts +++ b/src/renderer/packages/models/index.ts @@ -6,6 +6,7 @@ import SiliconFlow from './siliconflow' import LMStudio from './lmstudio' import Claude from './claude' import PPIO from './ppio' +import InfiniAI from './infiniai' export function getModel(setting: Settings, config: Config) { @@ -24,6 +25,8 @@ export function getModel(setting: Settings, config: Config) { return new SiliconFlow(setting) case ModelProvider.PPIO: return new PPIO(setting) + case ModelProvider.InfiniAI: + return new InfiniAI(setting) default: throw new Error('Cannot find model with provider: ' + setting.aiProvider) } @@ -37,6 +40,7 @@ export const aiProviderNameHash = { [ModelProvider.Ollama]: 'Ollama', [ModelProvider.SiliconFlow]: 'SiliconCloud API', [ModelProvider.PPIO]: 'PPIO', + [ModelProvider.InfiniAI]: 'InfiniAI API', } export const AIModelProviderMenuOptionList = [ @@ -76,6 +80,11 @@ export const AIModelProviderMenuOptionList = [ label: aiProviderNameHash[ModelProvider.PPIO], disabled: false, }, + { + value: ModelProvider.InfiniAI, + label: aiProviderNameHash[ModelProvider.InfiniAI], + disabled: false, + }, ] export function getModelDisplayName(settings: Settings, sessionType: SessionType): string { @@ -105,6 +114,8 @@ export function getModelDisplayName(settings: Settings, sessionType: SessionType return `SiliconCloud (${settings.siliconCloudModel})` case ModelProvider.PPIO: return `PPIO (${settings.ppioModel})` + case ModelProvider.InfiniAI: + return `InfiniAI (${settings.infiniaiModel})` default: return 'unknown' } diff --git a/src/renderer/packages/models/infiniai.ts b/src/renderer/packages/models/infiniai.ts new file mode 100644 index 000000000..850854f32 --- /dev/null +++ b/src/renderer/packages/models/infiniai.ts @@ -0,0 +1,94 @@ +import { Message } from 'src/shared/types' +import { ApiError } from './errors' +import Base, { onResultChange } from './base' + +interface Options { + infiniaiKey: string + infiniaiHost: string + infiniaiModel: string + temperature: number + topP: number +} + +export default class InfiniAI extends Base { + public name = 'InfiniAI' + + public options: Options + constructor(options: Options) { + super() + this.options = options + this.options.infiniaiHost = this.options.infiniaiHost || 'https://api.infiniai.com/v1' + } + + async callChatCompletion( + rawMessages: Message[], + signal?: AbortSignal, + onResultChange?: onResultChange + ): Promise { + const messages = rawMessages.map((m) => ({ + role: m.role, + content: m.content, + })) + + const response = await this.post( + `${this.options.infiniaiHost}/chat/completions`, + this.getHeaders(), + { + messages, + model: this.options.infiniaiModel, + 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 InfiniAI: ${JSON.stringify(data)}`) + } + const text = data.choices[0]?.delta?.content + if (text !== undefined) { + result += text + if (onResultChange) { + onResultChange(result) + } + } + }) + return result + } + + async listModels(): Promise { + const res = await this.get(`${this.options.infiniaiHost}/models`, this.getHeaders()) + const json = await res.json() + if (!json['data']) { + throw new ApiError(JSON.stringify(json)) + } + return json['data'].map((m: any) => m['id']) + } + + getHeaders() { + const headers: Record = { + Authorization: `Bearer ${this.options.infiniaiKey}`, + 'Content-Type': 'application/json', + } + return headers + } + + async get(url: string, headers: Record) { + const res = await fetch(url, { + method: 'GET', + headers, + }) + if (!res.ok) { + const err = await res.text().catch((e) => null) + throw new ApiError(`Status Code ${res.status}, ${err}`) + } + return res + } +} diff --git a/src/renderer/pages/SettingDialog/InfiniAISetting.tsx b/src/renderer/pages/SettingDialog/InfiniAISetting.tsx new file mode 100644 index 000000000..07672c9c8 --- /dev/null +++ b/src/renderer/pages/SettingDialog/InfiniAISetting.tsx @@ -0,0 +1,72 @@ +import { Typography, Box, TextField } 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 InfiniAIModelSelect from '../../components/InfiniAIModelSelect' + +interface ModelConfigProps { + settingsEdit: ModelSettings + setSettingsEdit: (settings: ModelSettings) => void +} + +export default function InfiniAISetting(props: ModelConfigProps) { + const { settingsEdit, setSettingsEdit } = props + const { t } = useTranslation() + return ( + + { + setSettingsEdit({ ...settingsEdit, infiniaiKey: value }) + }} + placeholder="sk_xxxxxxxxxxxxxxxxxxxxxxxx" + /> + + { + setSettingsEdit({ ...settingsEdit, infiniaiHost: e.target.value }) + }} + placeholder="https://api.infiniai.com/v1" + /> + + + + + {t('model')} & {t('token')}{' '} + + + + + setSettingsEdit({ ...settingsEdit, infiniaiModel: model }) + } + /> + 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 34e5f5c80..b7b625451 100644 --- a/src/renderer/pages/SettingDialog/ModelSettingTab.tsx +++ b/src/renderer/pages/SettingDialog/ModelSettingTab.tsx @@ -10,6 +10,7 @@ import MaxContextMessageCountSlider from '@/components/MaxContextMessageCountSli import TemperatureSlider from '@/components/TemperatureSlider' import ClaudeSetting from './ClaudeSetting' import PPIOSetting from './PPIOSetting' +import InfiniAISetting from './InfiniAISetting' interface ModelConfigProps { settingsEdit: ModelSettings @@ -85,6 +86,9 @@ export default function ModelSettingTab(props: ModelConfigProps) { {settingsEdit.aiProvider === ModelProvider.PPIO && ( )} + {settingsEdit.aiProvider === ModelProvider.InfiniAI && ( + + )} ) } diff --git a/src/shared/defaults.ts b/src/shared/defaults.ts index b4496f75f..e172af925 100644 --- a/src/shared/defaults.ts +++ b/src/shared/defaults.ts @@ -56,6 +56,10 @@ export function settings(): Settings { ppioKey: '', ppioModel: 'deepseek/deepseek-r1/community', + infiniaiHost: 'https://cloud.infini-ai.com/maas/v1', + infiniaiKey: '', + infiniaiModel: 'deepseek-r1', + autoGenerateTitle: true, } } diff --git a/src/shared/types.ts b/src/shared/types.ts index bd7b3b0ad..c825f2d92 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -70,6 +70,7 @@ export enum ModelProvider { SiliconFlow = 'silicon-flow', LMStudio = 'lm-studio', PPIO = 'ppio', + InfiniAI = 'infiniai', } export interface ModelSettings { @@ -121,6 +122,11 @@ export interface ModelSettings { ppioKey: string ppioModel: string + // infiniai + infiniaiHost: string + infiniaiKey: string + infiniaiModel: string | 'custom-model' + temperature: number topP: number openaiMaxContextMessageCount: number