65257eb3d5
* feat(ModelList): add support for 'supported_text_delta' in model configuration - Introduced a new boolean property 'supported_text_delta' to the model configuration, allowing models to indicate support for text delta outputs. - Updated the AddModelPopup and ModelEditContent components to handle this new property, including UI elements for user interaction. - Enhanced migration logic to set default values for existing models based on compatibility checks. - Added corresponding translations for the new property in the i18n files. * feat(OpenAIApiClient): enhance support for Qwen MT model and system message handling - Added support for Qwen MT model in OpenAIApiClient, including translation options based on target language. - Updated system message handling to accommodate models that do not support system messages. - Introduced utility functions to identify Qwen MT models and their compatibility with text delta outputs. - Enhanced TextChunkMiddleware to handle text accumulation based on model capabilities. - Updated model configuration to include Qwen MT in the list of excluded models for function calling. * feat(i18n): add translations for 'supported_text_delta' in multiple languages - Introduced new translation entries for the 'supported_text_delta' property in English, Japanese, Russian, and Traditional Chinese localization files. - Updated the corresponding labels and tooltips to enhance user experience across different languages. * refactor(ModelEditContent): reposition 'supported_text_delta' input for improved UI layout - Moved the 'supported_text_delta' Form.Item from its previous location to enhance the user interface and maintain consistency in the model editing experience. - Ensured that the switch input for 'supported_text_delta' is now displayed in a more logical order within the form. * fix(TextChunkMiddleware): update condition for supported_text_delta check - Changed the condition in TextChunkMiddleware to explicitly check for 'supported_text_delta' being false, improving clarity in the logic. - Updated test cases to reflect the new structure of model configurations, ensuring 'supported_text_delta' is consistently set to true for relevant models. * feat(migrate): add support for 'supported_text_delta' in assistant models - Updated migration logic to set 'supported_text_delta' for both default and specific models within assistants. - Implemented checks to ensure compatibility with text delta outputs, enhancing model configuration consistency. * feat(ModelList): add 'supported_text_delta' to model addition logic - Enhanced the model addition process in EditModelsPopup, NewApiAddModelPopup, and NewApiBatchAddModelPopup to include the 'supported_text_delta' property. - Ensured consistency across components by setting 'supported_text_delta' to true when adding models, improving compatibility with text delta outputs. * feat(migrate): streamline model text delta support in migration logic - Refactored migration logic to utilize a new helper function for updating the 'supported_text_delta' property across various model types, enhancing code clarity and reducing redundancy. - Ensured that all relevant models, including those in assistants and LLM providers, are correctly configured for text delta compatibility during migration. * feat(OpenAIApiClient): integrate language mapping for Qwen MT model translations - Updated the OpenAIApiClient to utilize a new utility function for mapping target languages to Qwen MT model names, enhancing translation accuracy. - Refactored migration logic to ensure default tool use mode is set for assistants lacking this configuration, improving user experience. - Added a new utility function for language mapping in the utils module, supporting better integration with translation features. * feat(ModelList): update model addition logic to determine 'supported_text_delta' - Integrated a new utility function to assess model compatibility with text delta outputs during the model addition process in AddModelPopup. - Simplified the logic for setting the 'supported_text_delta' property, enhancing clarity and ensuring accurate model configuration. * feat(ModelList): unify model addition logic for 'supported_text_delta' - Refactored model addition across AddModelPopup, EditModelsPopup, NewApiAddModelPopup, and NewApiBatchAddModelPopup to consistently determine 'supported_text_delta' using a utility function. - Simplified the logic for setting 'supported_text_delta', enhancing clarity and ensuring accurate model configuration across components.
416 lines
17 KiB
TypeScript
416 lines
17 KiB
TypeScript
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
|
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
|
|
import {
|
|
isEmbeddingModel,
|
|
isFunctionCallingModel,
|
|
isReasoningModel,
|
|
isRerankModel,
|
|
isVisionModel,
|
|
isWebSearchModel
|
|
} from '@renderer/config/models'
|
|
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
|
|
import { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
|
|
import { getDefaultGroupName, getDifference, getUnion } from '@renderer/utils'
|
|
import { Button, Checkbox, Divider, Flex, Form, Input, InputNumber, message, Modal, Select, Switch } from 'antd'
|
|
import { ChevronDown, ChevronUp } from 'lucide-react'
|
|
import { FC, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import styled from 'styled-components'
|
|
|
|
interface ModelEditContentProps {
|
|
provider: Provider
|
|
model: Model
|
|
onUpdateModel: (model: Model) => void
|
|
open: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
const symbols = ['$', '¥', '€', '£']
|
|
const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdateModel, open, onClose }) => {
|
|
const [form] = Form.useForm()
|
|
const { t } = useTranslation()
|
|
const [showMoreSettings, setShowMoreSettings] = useState(false)
|
|
const [currencySymbol, setCurrencySymbol] = useState(model.pricing?.currencySymbol || '$')
|
|
const [isCustomCurrency, setIsCustomCurrency] = useState(!symbols.includes(model.pricing?.currencySymbol || '$'))
|
|
const [modelCapabilities, setModelCapabilities] = useState(model.capabilities || [])
|
|
const [supportedTextDelta, setSupportedTextDelta] = useState(model.supported_text_delta)
|
|
|
|
const labelWidth = useDynamicLabelWidth([t('settings.models.add.endpoint_type.label')])
|
|
|
|
const onFinish = (values: any) => {
|
|
const finalCurrencySymbol = isCustomCurrency ? values.customCurrencySymbol : values.currencySymbol
|
|
const updatedModel: Model = {
|
|
...model,
|
|
id: values.id || model.id,
|
|
name: values.name || model.name,
|
|
group: values.group || model.group,
|
|
endpoint_type: provider.id === 'new-api' ? values.endpointType : model.endpoint_type,
|
|
capabilities: modelCapabilities,
|
|
supported_text_delta: supportedTextDelta,
|
|
pricing: {
|
|
input_per_million_tokens: Number(values.input_per_million_tokens) || 0,
|
|
output_per_million_tokens: Number(values.output_per_million_tokens) || 0,
|
|
currencySymbol: finalCurrencySymbol || '$'
|
|
}
|
|
}
|
|
onUpdateModel(updatedModel)
|
|
setShowMoreSettings(false)
|
|
onClose()
|
|
}
|
|
|
|
const handleClose = () => {
|
|
setShowMoreSettings(false)
|
|
setModelCapabilities(model.capabilities || [])
|
|
onClose()
|
|
}
|
|
|
|
const currencyOptions = [
|
|
...symbols.map((symbol) => ({ label: symbol, value: symbol })),
|
|
{ label: t('models.price.custom'), value: 'custom' }
|
|
]
|
|
|
|
return (
|
|
<Modal
|
|
title={t('models.edit')}
|
|
open={open}
|
|
onCancel={handleClose}
|
|
footer={null}
|
|
transitionName="animation-move-down"
|
|
centered
|
|
afterOpenChange={(visible) => {
|
|
if (visible) {
|
|
form.getFieldInstance('id')?.focus()
|
|
} else {
|
|
setShowMoreSettings(false)
|
|
}
|
|
}}>
|
|
<Form
|
|
form={form}
|
|
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }}
|
|
labelAlign="left"
|
|
colon={false}
|
|
style={{ marginTop: 15 }}
|
|
initialValues={{
|
|
id: model.id,
|
|
name: model.name,
|
|
group: model.group,
|
|
endpointType: model.endpoint_type,
|
|
input_per_million_tokens: model.pricing?.input_per_million_tokens ?? 0,
|
|
output_per_million_tokens: model.pricing?.output_per_million_tokens ?? 0,
|
|
currencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
|
? model.pricing?.currencySymbol || '$'
|
|
: 'custom',
|
|
customCurrencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
|
? ''
|
|
: model.pricing?.currencySymbol || ''
|
|
}}
|
|
onFinish={onFinish}>
|
|
<Form.Item
|
|
name="id"
|
|
label={t('settings.models.add.model_id.label')}
|
|
tooltip={t('settings.models.add.model_id.tooltip')}
|
|
rules={[{ required: true }]}>
|
|
<Flex justify="space-between" gap={5}>
|
|
<Input
|
|
placeholder={t('settings.models.add.model_id.placeholder')}
|
|
spellCheck={false}
|
|
maxLength={200}
|
|
disabled={true}
|
|
value={model.id}
|
|
onChange={(e) => {
|
|
const value = e.target.value
|
|
form.setFieldValue('name', value)
|
|
form.setFieldValue('group', getDefaultGroupName(value))
|
|
}}
|
|
/>
|
|
<Button
|
|
onClick={() => {
|
|
//copy model id
|
|
const val = form.getFieldValue('name')
|
|
navigator.clipboard.writeText((val.id || model.id) as string)
|
|
message.success(t('message.copied'))
|
|
}}>
|
|
<CopyIcon /> {t('chat.topics.copy.title')}
|
|
</Button>
|
|
</Flex>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="name"
|
|
label={t('settings.models.add.model_name.label')}
|
|
tooltip={t('settings.models.add.model_name.tooltip')}>
|
|
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="group"
|
|
label={t('settings.models.add.group_name.label')}
|
|
tooltip={t('settings.models.add.group_name.tooltip')}>
|
|
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
|
</Form.Item>
|
|
{provider.id === 'new-api' && (
|
|
<Form.Item
|
|
name="endpointType"
|
|
label={t('settings.models.add.endpoint_type.label')}
|
|
tooltip={t('settings.models.add.endpoint_type.tooltip')}
|
|
rules={[{ required: true, message: t('settings.models.add.endpoint_type.required') }]}>
|
|
<Select placeholder={t('settings.models.add.endpoint_type.placeholder')}>
|
|
{endpointTypeOptions.map((opt) => (
|
|
<Select.Option key={opt.value} value={opt.value}>
|
|
{t(opt.label)}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
)}
|
|
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
|
|
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
|
|
<Button
|
|
color="default"
|
|
variant="filled"
|
|
icon={showMoreSettings ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
iconPosition="end"
|
|
onClick={() => setShowMoreSettings(!showMoreSettings)}
|
|
style={{ color: 'var(--color-text-3)' }}>
|
|
{t('settings.moresetting.label')}
|
|
</Button>
|
|
<Button type="primary" htmlType="submit" size="middle">
|
|
{t('common.save')}
|
|
</Button>
|
|
</Flex>
|
|
</Form.Item>
|
|
{showMoreSettings && (
|
|
<div style={{ marginBottom: 8 }}>
|
|
<Divider style={{ margin: '16px 0 16px 0' }} />
|
|
<TypeTitle>{t('models.type.select')}:</TypeTitle>
|
|
{(() => {
|
|
const defaultTypes = [
|
|
...(isVisionModel(model) ? ['vision'] : []),
|
|
...(isReasoningModel(model) ? ['reasoning'] : []),
|
|
...(isFunctionCallingModel(model) ? ['function_calling'] : []),
|
|
...(isWebSearchModel(model) ? ['web_search'] : []),
|
|
...(isEmbeddingModel(model) ? ['embedding'] : []),
|
|
...(isRerankModel(model) ? ['rerank'] : [])
|
|
]
|
|
|
|
// 合并现有选择和默认类型用于前端展示
|
|
const selectedTypes = getUnion(
|
|
modelCapabilities?.filter((t) => t.isUserSelected).map((t) => t.type) || [],
|
|
getDifference(
|
|
defaultTypes,
|
|
modelCapabilities?.filter((t) => t.isUserSelected === false).map((t) => t.type) || []
|
|
)
|
|
)
|
|
|
|
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
|
|
|
|
const isRerankDisabled = selectedTypes.includes('embedding')
|
|
const isEmbeddingDisabled = selectedTypes.includes('rerank')
|
|
|
|
const showTypeConfirmModal = (newCapability: ModelCapability) => {
|
|
const onUpdateType = selectedTypes?.find((t) => t === newCapability.type)
|
|
window.modal.confirm({
|
|
title: t('settings.moresetting.warn'),
|
|
content: t('settings.moresetting.check.warn'),
|
|
okText: t('settings.moresetting.check.confirm'),
|
|
cancelText: t('common.cancel'),
|
|
okButtonProps: { danger: true },
|
|
cancelButtonProps: { type: 'primary' },
|
|
onOk: () => {
|
|
if (onUpdateType) {
|
|
const updatedTypes = selectedTypes?.map((t) => {
|
|
if (t === newCapability.type) {
|
|
return { type: t, isUserSelected: true }
|
|
}
|
|
if (
|
|
(onUpdateType !== t && onUpdateType === 'rerank') ||
|
|
(onUpdateType === 'embedding' && onUpdateType !== t)
|
|
) {
|
|
return { type: t, isUserSelected: false }
|
|
}
|
|
return { type: t }
|
|
})
|
|
setModelCapabilities(updatedTypes as ModelCapability[])
|
|
} else {
|
|
const updatedTypes = selectedTypes?.map((t) => {
|
|
if (
|
|
(newCapability.type !== t && newCapability.type === 'rerank') ||
|
|
(newCapability.type === 'embedding' && newCapability.type !== t)
|
|
) {
|
|
return { type: t, isUserSelected: false }
|
|
}
|
|
return { type: t }
|
|
})
|
|
setModelCapabilities([...(updatedTypes as ModelCapability[]), newCapability])
|
|
}
|
|
},
|
|
onCancel: () => {},
|
|
centered: true
|
|
})
|
|
}
|
|
|
|
const handleTypeChange = (types: string[]) => {
|
|
const diff = types.length > selectedTypes.length
|
|
if (diff) {
|
|
const newCapability = getDifference(types, selectedTypes) // checkbox的特性,确保了newCapability只有一个元素
|
|
showTypeConfirmModal({
|
|
type: newCapability[0] as ModelType,
|
|
isUserSelected: true
|
|
})
|
|
} else {
|
|
const disabledTypes = getDifference(selectedTypes, types)
|
|
const onUpdateType = modelCapabilities?.find((t) => t.type === disabledTypes[0])
|
|
if (onUpdateType) {
|
|
const updatedTypes = modelCapabilities?.map((t) => {
|
|
if (t.type === disabledTypes[0]) {
|
|
return { ...t, isUserSelected: false }
|
|
}
|
|
if (
|
|
(onUpdateType !== t && onUpdateType.type === 'rerank') ||
|
|
(onUpdateType.type === 'embedding' && onUpdateType !== t && t.isUserSelected === false)
|
|
) {
|
|
return { ...t, isUserSelected: true }
|
|
}
|
|
return t
|
|
})
|
|
setModelCapabilities(updatedTypes || [])
|
|
} else {
|
|
const updatedTypes = modelCapabilities?.map((t) => {
|
|
if (
|
|
(disabledTypes[0] === 'rerank' && t.type !== 'rerank') ||
|
|
(disabledTypes[0] === 'embedding' && t.type !== 'embedding' && t.isUserSelected === false)
|
|
) {
|
|
return { ...t, isUserSelected: true }
|
|
}
|
|
return t
|
|
})
|
|
setModelCapabilities([
|
|
...(updatedTypes ?? []),
|
|
{ type: disabledTypes[0] as ModelType, isUserSelected: false }
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleResetTypes = () => {
|
|
setModelCapabilities([])
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
|
|
<Checkbox.Group
|
|
value={selectedTypes}
|
|
onChange={handleTypeChange}
|
|
options={[
|
|
{
|
|
label: t('models.type.vision'),
|
|
value: 'vision',
|
|
disabled: isDisabled
|
|
},
|
|
{
|
|
label: t('models.type.websearch'),
|
|
value: 'web_search',
|
|
disabled: isDisabled
|
|
},
|
|
{
|
|
label: t('models.type.rerank'),
|
|
value: 'rerank',
|
|
disabled: isRerankDisabled
|
|
},
|
|
{
|
|
label: t('models.type.embedding'),
|
|
value: 'embedding',
|
|
disabled: isEmbeddingDisabled
|
|
},
|
|
{
|
|
label: t('models.type.reasoning'),
|
|
value: 'reasoning',
|
|
disabled: isDisabled
|
|
},
|
|
{
|
|
label: t('models.type.function_calling'),
|
|
value: 'function_calling',
|
|
disabled: isDisabled
|
|
}
|
|
]}
|
|
/>
|
|
<Button size="small" onClick={handleResetTypes}>
|
|
{t('common.reset')}
|
|
</Button>
|
|
</Flex>
|
|
</div>
|
|
)
|
|
})()}
|
|
<Form.Item
|
|
name="supported_text_delta"
|
|
label={t('settings.models.add.supported_text_delta.label')}
|
|
tooltip={t('settings.models.add.supported_text_delta.tooltip')}>
|
|
<Switch checked={supportedTextDelta} onChange={(checked) => setSupportedTextDelta(checked)} />
|
|
</Form.Item>
|
|
<TypeTitle>{t('models.price.price')}</TypeTitle>
|
|
<Form.Item name="currencySymbol" label={t('models.price.currency')} style={{ marginBottom: 10 }}>
|
|
<Select
|
|
style={{ width: '100px' }}
|
|
options={currencyOptions}
|
|
onChange={(value) => {
|
|
if (value === 'custom') {
|
|
setIsCustomCurrency(true)
|
|
setCurrencySymbol(form.getFieldValue('customCurrencySymbol') || '')
|
|
} else {
|
|
setIsCustomCurrency(false)
|
|
setCurrencySymbol(value)
|
|
}
|
|
}}
|
|
dropdownMatchSelectWidth={false}
|
|
/>
|
|
</Form.Item>
|
|
|
|
{isCustomCurrency && (
|
|
<Form.Item
|
|
name="customCurrencySymbol"
|
|
label={t('models.price.custom_currency')}
|
|
style={{ marginBottom: 10 }}
|
|
rules={[{ required: isCustomCurrency }]}>
|
|
<Input
|
|
style={{ width: '100px' }}
|
|
placeholder={t('models.price.custom_currency_placeholder')}
|
|
maxLength={5}
|
|
onChange={(e) => setCurrencySymbol(e.target.value)}
|
|
/>
|
|
</Form.Item>
|
|
)}
|
|
|
|
<Form.Item label={t('models.price.input')} name="input_per_million_tokens">
|
|
<InputNumber
|
|
placeholder="0.00"
|
|
min={0}
|
|
step={0.01}
|
|
precision={2}
|
|
style={{ width: '240px' }}
|
|
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item label={t('models.price.output')} name="output_per_million_tokens">
|
|
<InputNumber
|
|
placeholder="0.00"
|
|
min={0}
|
|
step={0.01}
|
|
precision={2}
|
|
style={{ width: '240px' }}
|
|
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
|
|
/>
|
|
</Form.Item>
|
|
</div>
|
|
)}
|
|
</Form>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
const TypeTitle = styled.div`
|
|
margin: 12px 0;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
`
|
|
|
|
export default ModelEditContent
|