Skip to content

feat: add volcengine ark api provider #1986

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ For more information: [chatboxai.app](https://chatboxai.app/)
- Google Gemini Pro
- Ollama (enable access to local models like llama2, Mistral, Mixtral, codellama, vicuna, yi, and solar)
- ChatGLM-6B
- Volcengine Ark

- **Image Generation with Dall-E-3**
:art: Create the images of your imagination with Dall-E-3.
Expand Down
46 changes: 46 additions & 0 deletions src/renderer/components/ArkModelSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Select, MenuItem, FormControl, InputLabel, TextField } from '@mui/material'
import { ModelSettings } from '../../shared/types'
import { useTranslation } from 'react-i18next'
import { models } from '../packages/models/ark'

export interface Props {
arkModel: ModelSettings['arkModel']
arkEndpointId: ModelSettings['arkEndpointId']
onChange(arkModel: ModelSettings['arkModel'], arkEndpointId: ModelSettings['arkEndpointId']): void
className?: string
}

export default function ArkModelSelect(props: Props) {
const { t } = useTranslation()
return (
<FormControl fullWidth variant="outlined" margin="dense" className={props.className}>
<InputLabel htmlFor="model-select">{t('model')}</InputLabel>
<Select
label={t('model')}
id="model-select"
value={props.arkModel}
onChange={(e) => props.onChange(e.target.value as ModelSettings['arkModel'], props.arkEndpointId)}
>
{models.map((model) => (
<MenuItem key={model} value={model}>
{model}
</MenuItem>
))}
<MenuItem key="custom-model" value={'custom-model'}>
{t('Custom Model')}
</MenuItem>
</Select>
<TextField
margin="dense"
label={t('Model or Endpoint')}
type="text"
fullWidth
variant="outlined"
value={props.arkEndpointId || ''}
onChange={(e) =>
props.onChange(props.arkModel, e.target.value.trim())
}
/>
</FormControl>
)
}
1 change: 1 addition & 0 deletions src/renderer/i18n/locales/zh-Hans/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"View More Plans": "查看更多方案",
"Custom Model": "自定义模型",
"Custom Model Name": "自定义模型名",
"Endpoint": "接入点",
"advanced": "其他",
"Network Proxy": "Network Proxy",
"Proxy Address": "Proxy Address",
Expand Down
172 changes: 172 additions & 0 deletions src/renderer/packages/models/ark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { Message } from 'src/shared/types'
import { ApiError, ChatboxAIAPIError } from './errors'
import Base, { onResultChange } from './base'

interface Options {
arkApiKey: string
arkBaseURL: string
arkModel: ArkModel | 'custom-model'
arkEndpointId: string
temperature: number
topP: number
}

export default class VolcArk extends Base {
public name = 'VolcengineArk'

public options: Options
constructor(options: Options) {
super()
this.options = options
this.options.arkBaseURL = this.options.arkBaseURL || 'https://ark.cn-beijing.volces.com/api/v3'
}

async callChatCompletion(
rawMessages: Message[],
signal?: AbortSignal,
onResultChange?: onResultChange
): Promise<string> {
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<string> {
const model = this.options.arkEndpointId

rawMessages = injectModelSystemPrompt(rawMessages)

const messages = await populateGPTMessage(rawMessages)
return this.requestChatCompletionsStream(
{
messages,
model,
max_tokens: undefined,
temperature: this.options.temperature,
top_p: this.options.topP,
stream: true,
},
signal,
onResultChange
)
}

async requestChatCompletionsStream(
requestBody: Record<string, any>,
signal?: AbortSignal,
onResultChange?: onResultChange
): Promise<string> {
const apiPath = '/chat/completions'
const response = await this.post(`${this.options.arkBaseURL}${apiPath}`, this.getHeaders(), requestBody, 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
}

async requestChatCompletionsNotStream(
requestBody: Record<string, any>,
signal?: AbortSignal,
onResultChange?: onResultChange
): Promise<string> {
const apiPath = '/chat/completions'
const response = await this.post(`${this.options.arkBaseURL}${apiPath}`, this.getHeaders(), requestBody, signal)
const json = await response.json()
if (json.error) {
throw new ApiError(`Error from OpenAI: ${JSON.stringify(json)}`)
}
if (onResultChange) {
onResultChange(json.choices[0].message.content)
}
return json.choices[0].message.content
}

getHeaders() {
const headers: Record<string, string> = {
Authorization: `Bearer ${this.options.arkApiKey}`,
'Content-Type': 'application/json',
}
return headers
}
}

export const arkModelConfigs = {
'doubao-1.5-pro-256k': {
maxTokens: 12_288,
maxContextTokens: 131_072,
},
'doubao-1.5-pro-32k': {
maxTokens: 12_288,
maxContextTokens: 32_768,
},
'doubao-1.5-vision-pro-32k': {
maxTokens: 12_288,
maxContextTokens: 32_768,
},
'deepseek-r1': {
maxTokens: 8192,
maxContextTokens: 64_000,
},
'deepseek-v3': {
maxTokens: 8192,
maxContextTokens: 64_000,
},
}
export type ArkModel = keyof typeof arkModelConfigs
export const models = Array.from(Object.keys(arkModelConfigs)).sort() as ArkModel[]

export async function populateGPTMessage(rawMessages: Message[]): Promise<OpenAIMessage[]> {
const messages: OpenAIMessage[] = rawMessages.map((m) => ({
role: m.role,
content: m.content,
}))
return messages
}

export function injectModelSystemPrompt(messages: Message[]) {
const metadataPrompt = `
Current date: ${new Date().toISOString()}

`
let hasInjected = false
return messages.map((m) => {
if (m.role === 'system' && !hasInjected) {
m = { ...m }
m.content = metadataPrompt + m.content
hasInjected = true
}
return m
})
}

export interface OpenAIMessage {
role: 'system' | 'user' | 'assistant'
content: string
name?: string
}
12 changes: 11 additions & 1 deletion src/renderer/packages/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SiliconFlow from './siliconflow'
import LMStudio from './lmstudio'
import Claude from './claude'
import PPIO from './ppio'

import VolcArk from './ark'

export function getModel(setting: Settings, config: Config) {
switch (setting.aiProvider) {
Expand All @@ -24,6 +24,8 @@ export function getModel(setting: Settings, config: Config) {
return new SiliconFlow(setting)
case ModelProvider.PPIO:
return new PPIO(setting)
case ModelProvider.VolcengineArk:
return new VolcArk(setting)
default:
throw new Error('Cannot find model with provider: ' + setting.aiProvider)
}
Expand All @@ -37,6 +39,7 @@ export const aiProviderNameHash = {
[ModelProvider.Ollama]: 'Ollama',
[ModelProvider.SiliconFlow]: 'SiliconCloud API',
[ModelProvider.PPIO]: 'PPIO',
[ModelProvider.VolcengineArk]: 'Volcengine Ark',
}

export const AIModelProviderMenuOptionList = [
Expand Down Expand Up @@ -76,6 +79,11 @@ export const AIModelProviderMenuOptionList = [
label: aiProviderNameHash[ModelProvider.PPIO],
disabled: false,
},
{
value: ModelProvider.VolcengineArk,
label: aiProviderNameHash[ModelProvider.VolcengineArk],
disabled: false,
},
]

export function getModelDisplayName(settings: Settings, sessionType: SessionType): string {
Expand Down Expand Up @@ -105,6 +113,8 @@ export function getModelDisplayName(settings: Settings, sessionType: SessionType
return `SiliconCloud (${settings.siliconCloudModel})`
case ModelProvider.PPIO:
return `PPIO (${settings.ppioModel})`
case ModelProvider.VolcengineArk:
return `Ark (${settings.arkModel})`
default:
return 'unknown'
}
Expand Down
79 changes: 79 additions & 0 deletions src/renderer/pages/SettingDialog/ArkSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 ArkModelSelect from '../../components/ArkModelSelect'
import TextFieldReset from '@/components/TextFieldReset'

interface ModelConfigProps {
settingsEdit: ModelSettings
setSettingsEdit: (settings: ModelSettings) => void
}

export default function VolcArkSetting(props: ModelConfigProps) {
const { settingsEdit, setSettingsEdit } = props
const { t } = useTranslation()
return (
<Box>
<PasswordTextField
label={t('api key')}
value={settingsEdit.arkApiKey}
setValue={(value) => {
setSettingsEdit({ ...settingsEdit, arkApiKey: value })
}}
placeholder="xxxxxxxxxxxxxxxxxxxxxxxx"
/>
<>
<TextFieldReset
margin="dense"
label={t('Base URL')}
type="text"
fullWidth
variant="outlined"
value={settingsEdit.arkBaseURL}
placeholder="https://ark.cn-beijing.volces.com/api/v3"
defaultValue='https://ark.cn-beijing.volces.com/api/v3'
onValueChange={(value) => {
value = value.trim()
if (value.length > 4 && !value.startsWith('http')) {
value = 'https://' + value
}
setSettingsEdit({ ...settingsEdit, arkBaseURL: value })
}}
/>
</>
<ArkModelSelect
arkModel={settingsEdit.arkModel}
arkEndpointId={settingsEdit.arkEndpointId}
onChange={(arkModel, arkEndpointId) =>
setSettingsEdit({ ...settingsEdit, arkModel, arkEndpointId })
}
/>
<Accordion>
<AccordionSummary aria-controls="panel1a-content">
<Typography>
{t('token')}{' '}
</Typography>
</AccordionSummary>
<AccordionDetails>
<TemperatureSlider
value={settingsEdit.temperature}
onChange={(value) => setSettingsEdit({ ...settingsEdit, temperature: value })}
/>
<TopPSlider
topP={settingsEdit.topP}
setTopP={(v) => setSettingsEdit({ ...settingsEdit, topP: v })}
/>
<MaxContextMessageCountSlider
value={settingsEdit.openaiMaxContextMessageCount}
onChange={(v) => setSettingsEdit({ ...settingsEdit, openaiMaxContextMessageCount: v })}
/>
</AccordionDetails>
</Accordion>
</Box>
)
}
6 changes: 5 additions & 1 deletion src/renderer/pages/SettingDialog/ModelSettingTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MaxContextMessageCountSlider from '@/components/MaxContextMessageCountSli
import TemperatureSlider from '@/components/TemperatureSlider'
import ClaudeSetting from './ClaudeSetting'
import PPIOSetting from './PPIOSetting'
import VolcArkSetting from './ArkSetting'

interface ModelConfigProps {
settingsEdit: ModelSettings
Expand Down Expand Up @@ -76,7 +77,7 @@ export default function ModelSettingTab(props: ModelConfigProps) {
)}


{settingsEdit.aiProvider === ModelProvider.SiliconFlow && (
{settingsEdit.aiProvider === ModelProvider.SiliconFlow && (
<SiliconFlowSetting settingsEdit={settingsEdit} setSettingsEdit={setSettingsEdit} />
)}
{settingsEdit.aiProvider === ModelProvider.Claude && (
Expand All @@ -85,6 +86,9 @@ export default function ModelSettingTab(props: ModelConfigProps) {
{settingsEdit.aiProvider === ModelProvider.PPIO && (
<PPIOSetting settingsEdit={settingsEdit} setSettingsEdit={setSettingsEdit} />
)}
{settingsEdit.aiProvider === ModelProvider.VolcengineArk && (
<VolcArkSetting settingsEdit={settingsEdit} setSettingsEdit={setSettingsEdit} />
)}
</Box>
)
}
Loading