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:
kangfenmao 2025-03-08 20:10:38 +08:00
parent 3f82a692a2
commit 49d29d78da
10 changed files with 97 additions and 26 deletions

View File

@ -1,10 +1,16 @@
import { Tooltip } from 'antd'
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const ReasoningIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
const { t } = useTranslation()
return (
<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>
)
}

View 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

View File

@ -1,11 +1,17 @@
import { EyeOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd'
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
const { t } = useTranslation()
return (
<Container>
<Icon {...(props as any)} />
<Tooltip title={t('models.vision')} placement="top">
<Icon {...(props as any)} />
</Tooltip>
</Container>
)
}

View File

@ -1,11 +1,17 @@
import { GlobalOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd'
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const WebSearchIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
const { t } = useTranslation()
return (
<Container>
<Icon {...(props as any)} />
<Tooltip title={t('models.websearch')} placement="top">
<Icon {...(props as any)} />
</Tooltip>
</Container>
)
}

View File

@ -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 { isFreeModel } from '@renderer/utils'
import { Tag } from 'antd'
@ -7,6 +13,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import ReasoningIcon from './Icons/ReasoningIcon'
import ToolsCallingIcon from './Icons/ToolsCallingIcon'
import VisionIcon from './Icons/VisionIcon'
import WebSearchIcon from './Icons/WebSearchIcon'
@ -14,15 +21,17 @@ interface ModelTagsProps {
model: Model
showFree?: 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()
return (
<Container>
{isVisionModel(model) && <VisionIcon />}
{isWebSearchModel(model) && <WebSearchIcon />}
{showReasoning && isReasoningModel(model) && <ReasoningIcon />}
{showToolsCalling && isToolCallingModel(model) && <ToolsCallingIcon />}
{isEmbeddingModel(model) && <Tag color="orange">{t('models.embedding')}</Tag>}
{showFree && isFreeModel(model) && <Tag color="green">{t('models.free')}</Tag>}
</Container>

View File

@ -134,6 +134,7 @@ import OpenAI from 'openai'
import { getWebSearchTools } from './tools'
// Vision models
const visionAllowedModels = [
'llava',
'moondream',
@ -159,19 +160,33 @@ const visionAllowedModels = [
]
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
export const VISION_REGEX = new RegExp(
`\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
'i'
)
// Text to image models
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i
// Reasoning models
export const REASONING_REGEX =
/^(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 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) {
const isLight = true
@ -990,10 +1005,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
],
yi: [
{ 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' }
],
zhipu: [

View File

@ -479,8 +479,9 @@
},
"vision": "视觉",
"websearch": "联网",
"edit": "编辑模型",
"no_matches": "无可用模型"
"edit": "编辑模型",
"no_matches": "无可用模型",
"tool_calling": "工具调用"
},
"navbar": {
"expand": "伸缩对话框",

View File

@ -685,7 +685,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
onMentionModel={(model) => onMentionModel(model, mentionFromKeyboard)}
ToolbarButton={ToolbarButton}
/>
<MCPToolsButton enabledMCPs={enabledMCPs} onEnableMCP={toggelEnableMCP} ToolbarButton={ToolbarButton} />
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
<ToolbarButton type="text" onClick={onEnableWebSearch}>
<GlobalOutlined
@ -693,6 +692,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
/>
</ToolbarButton>
</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>
<Popconfirm
title={t('chat.input.clear.content')}
@ -706,15 +715,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
</ToolbarButton>
</Popconfirm>
</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>
<ToolbarButton type="text" onClick={onNewContext}>
<PicCenterOutlined />

View File

@ -1,3 +1,4 @@
import { ToolOutlined } from '@ant-design/icons'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
import { Dropdown, Switch, Tooltip } from 'antd'
@ -39,7 +40,7 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, onEnableMCP, ToolbarButton })
}
})
}
}, [enableAll])
}, [activeServers, enableAll, enabledMCPs, onEnableMCP])
const menu = (
<div ref={menuRef} className="ant-dropdown-menu">
@ -84,9 +85,9 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, onEnableMCP, ToolbarButton })
open={isOpen}
onOpenChange={setIsOpen}
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}>
<i className="iconfont icon-mcp" style={{ fontSize: 18 }}></i>
<ToolOutlined style={{ color: enabledMCPs.length > 0 ? '#d97757' : 'var(--color-icon)' }} />
</ToolbarButton>
</Tooltip>
</Dropdown>

View File

@ -39,7 +39,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
<ModelName>
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''}
</ModelName>
<ModelTags model={model} showFree={false} showReasoning={false} />
<ModelTags model={model} showFree={false} showReasoning={false} showToolsCalling={false} />
</ButtonContent>
</DropdownButton>
)