feat: Add tool calling support for models
- Introduce ToolsCallingIcon component for tool calling models - Add isToolCallingModel function in models config - Update ModelTags to support showing tool calling icon - Add tooltips to model icons for better UX - Update Chinese localization with tool calling translation - Modify Inputbar and SelectModelButton to accommodate new icon
This commit is contained in:
parent
3f82a692a2
commit
49d29d78da
@ -1,10 +1,16 @@
|
|||||||
|
import { Tooltip } from 'antd'
|
||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const ReasoningIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
const ReasoningIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Icon className="iconfont icon-thinking" {...(props as any)} />
|
<Tooltip title={t('models.reasoning')} placement="top">
|
||||||
|
<Icon className="iconfont icon-thinking" {...(props as any)} />
|
||||||
|
</Tooltip>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/renderer/src/components/Icons/ToolsCallingIcon.tsx
Normal file
31
src/renderer/src/components/Icons/ToolsCallingIcon.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ToolOutlined } from '@ant-design/icons'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import React, { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const ToolsCallingIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Tooltip title={t('models.tool_calling')} placement="top">
|
||||||
|
<Icon {...(props as any)} />
|
||||||
|
</Tooltip>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Icon = styled(ToolOutlined)`
|
||||||
|
color: #d97757;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-right: 6px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ToolsCallingIcon
|
||||||
@ -1,11 +1,17 @@
|
|||||||
import { EyeOutlined } from '@ant-design/icons'
|
import { EyeOutlined } from '@ant-design/icons'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Icon {...(props as any)} />
|
<Tooltip title={t('models.vision')} placement="top">
|
||||||
|
<Icon {...(props as any)} />
|
||||||
|
</Tooltip>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
import { GlobalOutlined } from '@ant-design/icons'
|
import { GlobalOutlined } from '@ant-design/icons'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const WebSearchIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
const WebSearchIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Icon {...(props as any)} />
|
<Tooltip title={t('models.websearch')} placement="top">
|
||||||
|
<Icon {...(props as any)} />
|
||||||
|
</Tooltip>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { isEmbeddingModel, isReasoningModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
import {
|
||||||
|
isEmbeddingModel,
|
||||||
|
isReasoningModel,
|
||||||
|
isToolCallingModel,
|
||||||
|
isVisionModel,
|
||||||
|
isWebSearchModel
|
||||||
|
} from '@renderer/config/models'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { isFreeModel } from '@renderer/utils'
|
import { isFreeModel } from '@renderer/utils'
|
||||||
import { Tag } from 'antd'
|
import { Tag } from 'antd'
|
||||||
@ -7,6 +13,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import ReasoningIcon from './Icons/ReasoningIcon'
|
import ReasoningIcon from './Icons/ReasoningIcon'
|
||||||
|
import ToolsCallingIcon from './Icons/ToolsCallingIcon'
|
||||||
import VisionIcon from './Icons/VisionIcon'
|
import VisionIcon from './Icons/VisionIcon'
|
||||||
import WebSearchIcon from './Icons/WebSearchIcon'
|
import WebSearchIcon from './Icons/WebSearchIcon'
|
||||||
|
|
||||||
@ -14,15 +21,17 @@ interface ModelTagsProps {
|
|||||||
model: Model
|
model: Model
|
||||||
showFree?: boolean
|
showFree?: boolean
|
||||||
showReasoning?: boolean
|
showReasoning?: boolean
|
||||||
|
showToolsCalling?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true, showReasoning = true }) => {
|
const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true, showReasoning = true, showToolsCalling = true }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{isVisionModel(model) && <VisionIcon />}
|
{isVisionModel(model) && <VisionIcon />}
|
||||||
{isWebSearchModel(model) && <WebSearchIcon />}
|
{isWebSearchModel(model) && <WebSearchIcon />}
|
||||||
{showReasoning && isReasoningModel(model) && <ReasoningIcon />}
|
{showReasoning && isReasoningModel(model) && <ReasoningIcon />}
|
||||||
|
{showToolsCalling && isToolCallingModel(model) && <ToolsCallingIcon />}
|
||||||
{isEmbeddingModel(model) && <Tag color="orange">{t('models.embedding')}</Tag>}
|
{isEmbeddingModel(model) && <Tag color="orange">{t('models.embedding')}</Tag>}
|
||||||
{showFree && isFreeModel(model) && <Tag color="green">{t('models.free')}</Tag>}
|
{showFree && isFreeModel(model) && <Tag color="green">{t('models.free')}</Tag>}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -134,6 +134,7 @@ import OpenAI from 'openai'
|
|||||||
|
|
||||||
import { getWebSearchTools } from './tools'
|
import { getWebSearchTools } from './tools'
|
||||||
|
|
||||||
|
// Vision models
|
||||||
const visionAllowedModels = [
|
const visionAllowedModels = [
|
||||||
'llava',
|
'llava',
|
||||||
'moondream',
|
'moondream',
|
||||||
@ -159,19 +160,33 @@ const visionAllowedModels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
|
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
|
||||||
|
|
||||||
export const VISION_REGEX = new RegExp(
|
export const VISION_REGEX = new RegExp(
|
||||||
`\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
|
`\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
|
||||||
'i'
|
'i'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Text to image models
|
||||||
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i
|
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i
|
||||||
|
|
||||||
|
// Reasoning models
|
||||||
export const REASONING_REGEX =
|
export const REASONING_REGEX =
|
||||||
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*)$/i
|
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*)$/i
|
||||||
|
|
||||||
|
// Embedding models
|
||||||
export const EMBEDDING_REGEX = /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
|
export const EMBEDDING_REGEX = /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
|
||||||
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
|
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
|
||||||
|
|
||||||
|
// Tool calling models
|
||||||
|
export const TOOL_CALLING_MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-4', 'gpt-4.5', 'claude']
|
||||||
|
export const TOOL_CALLING_REGEX = new RegExp(`\\b(?:${TOOL_CALLING_MODELS.join('|')})\\b`, 'i')
|
||||||
|
export function isToolCallingModel(model: Model): boolean {
|
||||||
|
if (['gemini', 'deepseek', 'anthropic'].includes(model.provider)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return TOOL_CALLING_REGEX.test(model.id)
|
||||||
|
}
|
||||||
|
|
||||||
export function getModelLogo(modelId: string) {
|
export function getModelLogo(modelId: string) {
|
||||||
const isLight = true
|
const isLight = true
|
||||||
|
|
||||||
@ -990,10 +1005,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
],
|
],
|
||||||
yi: [
|
yi: [
|
||||||
{ id: 'yi-lightning', name: 'Yi Lightning', provider: 'yi', group: 'yi-lightning', owned_by: '01.ai' },
|
{ id: 'yi-lightning', name: 'Yi Lightning', provider: 'yi', group: 'yi-lightning', owned_by: '01.ai' },
|
||||||
// yi-medium, yi-large, yi-vision 已被 yi-lightning 替代 (详见 https://archive.ph/0Idg3)
|
|
||||||
// { id: 'yi-medium', name: 'yi-medium', provider: 'yi', group: 'yi-medium', owned_by: '01.ai' },
|
|
||||||
// { id: 'yi-large', name: 'yi-large', provider: 'yi', group: 'yi-large', owned_by: '01.ai' },
|
|
||||||
// { id: 'yi-vision', name: 'yi-vision', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
|
|
||||||
{ id: 'yi-vision-v2', name: 'Yi Vision v2', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
|
{ id: 'yi-vision-v2', name: 'Yi Vision v2', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
|
||||||
],
|
],
|
||||||
zhipu: [
|
zhipu: [
|
||||||
|
|||||||
@ -480,7 +480,8 @@
|
|||||||
"vision": "视觉",
|
"vision": "视觉",
|
||||||
"websearch": "联网",
|
"websearch": "联网",
|
||||||
"edit": "编辑模型",
|
"edit": "编辑模型",
|
||||||
"no_matches": "无可用模型"
|
"no_matches": "无可用模型",
|
||||||
|
"tool_calling": "工具调用"
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"expand": "伸缩对话框",
|
"expand": "伸缩对话框",
|
||||||
|
|||||||
@ -685,7 +685,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
onMentionModel={(model) => onMentionModel(model, mentionFromKeyboard)}
|
onMentionModel={(model) => onMentionModel(model, mentionFromKeyboard)}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
/>
|
/>
|
||||||
<MCPToolsButton enabledMCPs={enabledMCPs} onEnableMCP={toggelEnableMCP} ToolbarButton={ToolbarButton} />
|
|
||||||
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
||||||
<ToolbarButton type="text" onClick={onEnableWebSearch}>
|
<ToolbarButton type="text" onClick={onEnableWebSearch}>
|
||||||
<GlobalOutlined
|
<GlobalOutlined
|
||||||
@ -693,6 +692,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
/>
|
/>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{showKnowledgeIcon && (
|
||||||
|
<KnowledgeBaseButton
|
||||||
|
selectedBases={selectedKnowledgeBases}
|
||||||
|
onSelect={handleKnowledgeBaseSelect}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
disabled={files.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MCPToolsButton enabledMCPs={enabledMCPs} onEnableMCP={toggelEnableMCP} ToolbarButton={ToolbarButton} />
|
||||||
|
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
||||||
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('chat.input.clear.content')}
|
title={t('chat.input.clear.content')}
|
||||||
@ -706,15 +715,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showKnowledgeIcon && (
|
|
||||||
<KnowledgeBaseButton
|
|
||||||
selectedBases={selectedKnowledgeBases}
|
|
||||||
onSelect={handleKnowledgeBaseSelect}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
disabled={files.length > 0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
|
||||||
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
||||||
<ToolbarButton type="text" onClick={onNewContext}>
|
<ToolbarButton type="text" onClick={onNewContext}>
|
||||||
<PicCenterOutlined />
|
<PicCenterOutlined />
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { ToolOutlined } from '@ant-design/icons'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPServer } from '@renderer/types'
|
||||||
import { Dropdown, Switch, Tooltip } from 'antd'
|
import { Dropdown, Switch, Tooltip } from 'antd'
|
||||||
@ -39,7 +40,7 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, onEnableMCP, ToolbarButton })
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [enableAll])
|
}, [activeServers, enableAll, enabledMCPs, onEnableMCP])
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<div ref={menuRef} className="ant-dropdown-menu">
|
<div ref={menuRef} className="ant-dropdown-menu">
|
||||||
@ -84,9 +85,9 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, onEnableMCP, ToolbarButton })
|
|||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={setIsOpen}
|
onOpenChange={setIsOpen}
|
||||||
overlayClassName="mention-models-dropdown">
|
overlayClassName="mention-models-dropdown">
|
||||||
<Tooltip placement="top" title="MCP Servers" arrow>
|
<Tooltip placement="top" title={t('settings.mcp.title')} arrow>
|
||||||
<ToolbarButton type="text" ref={dropdownRef}>
|
<ToolbarButton type="text" ref={dropdownRef}>
|
||||||
<i className="iconfont icon-mcp" style={{ fontSize: 18 }}></i>
|
<ToolOutlined style={{ color: enabledMCPs.length > 0 ? '#d97757' : 'var(--color-icon)' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
|||||||
<ModelName>
|
<ModelName>
|
||||||
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''}
|
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''}
|
||||||
</ModelName>
|
</ModelName>
|
||||||
<ModelTags model={model} showFree={false} showReasoning={false} />
|
<ModelTags model={model} showFree={false} showReasoning={false} showToolsCalling={false} />
|
||||||
</ButtonContent>
|
</ButtonContent>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user