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 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>
) )
} }

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 { 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>
) )
} }

View File

@ -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>
) )
} }

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 { 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>

View File

@ -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: [

View File

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

View File

@ -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 />

View File

@ -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>

View File

@ -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>
) )