Feat: url context for Gemini models (#7931)

* feat: Add URL Context ability for Gemini Models

* feat: Adding URL Context Button to tool bar and make it visible only when gemini models selected.
It is not working (adding urlContext tools) for now.

* fix: trying to force enable UrlContext function

* fix: enableUrlContext indication reverted

* feat: migration script for refreshing tool order to add URL Context button.

* fix: optimize migrate.ts

* fix: upgrade version

---------

Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
ous50 | ousfifty | 欧式fifty 2025-07-14 23:52:19 +08:00 committed by GitHub
parent 1b129636ed
commit 71917eb0ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 117 additions and 2 deletions

View File

@ -443,7 +443,7 @@ export class GeminiAPIClient extends BaseApiClient<
messages: GeminiSdkMessageParam[]
metadata: Record<string, any>
}> => {
const { messages, mcpTools, maxTokens, enableWebSearch, enableGenerateImage } = coreRequest
const { messages, mcpTools, maxTokens, enableWebSearch, enableUrlContext, enableGenerateImage } = coreRequest
// 1. 处理系统消息
let systemInstruction = assistant.prompt
@ -483,6 +483,12 @@ export class GeminiAPIClient extends BaseApiClient<
})
}
if (enableUrlContext) {
tools.push({
urlContext: {}
})
}
if (isGemmaModel(model) && assistant.prompt) {
const isFirstMessage = history.length === 0
if (isFirstMessage && messageContents) {

View File

@ -84,6 +84,7 @@ export interface ResponseChunkTransformerContext {
isStreaming: boolean
isEnabledToolCalling: boolean
isEnabledWebSearch: boolean
isEnabledUrlContext: boolean
isEnabledReasoning: boolean
mcpTools: MCPTool[]
provider: Provider

View File

@ -55,6 +55,7 @@ export const ResponseTransformMiddleware: CompletionsMiddleware =
isStreaming: params.streamOutput || false,
isEnabledToolCalling: (params.mcpTools && params.mcpTools.length > 0) || false,
isEnabledWebSearch: params.enableWebSearch || false,
isEnabledUrlContext: params.enableUrlContext || false,
isEnabledReasoning: params.enableReasoning || false,
mcpTools: params.mcpTools || [],
provider: ctx.apiClientInstance?.provider

View File

@ -49,6 +49,7 @@ export interface CompletionsParams {
// 功能开关
streamOutput: boolean
enableWebSearch?: boolean
enableUrlContext?: boolean
enableReasoning?: boolean
enableGenerateImage?: boolean

View File

@ -39,3 +39,18 @@ export function getWebSearchTools(model: Model): ChatCompletionTool[] {
}
return []
}
export function getUrlContextTools(model: Model): ChatCompletionTool[] {
if (model.id.includes('gemini')) {
return [
{
type: 'function',
function: {
name: 'urlContext'
}
}
]
}
return []
}

View File

@ -211,6 +211,7 @@
"input.web_search.button.ok": "Go to Settings",
"input.web_search.enable": "Enable web search",
"input.web_search.enable_content": "Need to check web search connectivity in settings first",
"input.url_context": "URL Context",
"input.web_search.no_web_search": "Disable Web Search",
"input.web_search.no_web_search.description": "Do not enable web search",
"input.web_search.settings": "Web Search Settings",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "ウェブ検索を無効にする",
"input.web_search.no_web_search.description": "ウェブ検索を無効にする",
"input.web_search.settings": "ウェブ検索設定",
"input.url_context": "URLコンテキスト",
"message.new.branch": "新しいブランチ",
"message.new.branch.created": "新しいブランチが作成されました",
"message.new.context": "新しいコンテキスト",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "Отключить веб-поиск",
"input.web_search.no_web_search.description": "Отключить веб-поиск",
"input.web_search.settings": "Настройки веб-поиска",
"input.url_context": "Контекст страницы",
"message.new.branch": "Новая ветка",
"message.new.branch.created": "Новая ветка создана",
"message.new.context": "Новый контекст",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "不使用网络",
"input.web_search.no_web_search.description": "不启用网络搜索功能",
"input.web_search.settings": "网络搜索设置",
"input.url_context": "网页上下文",
"message.new.branch": "分支",
"message.new.branch.created": "新分支已创建",
"message.new.context": "清除上下文",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "關閉網路搜尋",
"input.web_search.no_web_search.description": "關閉網路搜尋",
"input.web_search.settings": "網路搜尋設定",
"input.url_context": "網頁上下文",
"message.new.branch": "分支",
"message.new.branch.created": "新分支已建立",
"message.new.context": "新上下文",

View File

@ -14,6 +14,7 @@ import {
FileSearch,
Globe,
Languages,
Link,
LucideSquareTerminal,
Maximize,
MessageSquareDiff,
@ -36,6 +37,7 @@ import MentionModelsButton, { MentionModelsButtonRef } from './MentionModelsButt
import NewContextButton from './NewContextButton'
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton'
import UrlContextButton, { UrlContextButtonRef } from './UrlContextbutton'
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
export interface InputbarToolsRef {
@ -128,6 +130,7 @@ const InputbarTools = ({
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
const webSearchButtonRef = useRef<WebSearchButtonRef | null>(null)
const thinkingButtonRef = useRef<ThinkingButtonRef | null>(null)
const urlContextButtonRef = useRef<UrlContextButtonRef | null>(null)
const toolOrder = useAppSelector((state) => state.inputTools.toolOrder)
const isCollapse = useAppSelector((state) => state.inputTools.isCollapsed)
@ -230,6 +233,15 @@ const InputbarTools = ({
webSearchButtonRef.current?.openQuickPanel()
}
},
{
label: t('chat.input.url_context'),
description: '',
icon: <Link />,
isMenu: true,
action: () => {
urlContextButtonRef.current?.openQuickPanel()
}
},
{
label: couldAddImageFile ? t('chat.input.upload') : t('chat.input.upload.document'),
description: '',
@ -328,6 +340,12 @@ const InputbarTools = ({
label: t('chat.input.web_search'),
component: <WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
},
{
key: 'url_context',
label: t('chat.input.url_context'),
component: <UrlContextButton ref={urlContextButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />,
condition: model.id.toLowerCase().includes('gemini')
},
{
key: 'knowledge_base',
label: t('chat.input.knowledge_base'),

View File

@ -0,0 +1,44 @@
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types'
import { Tooltip } from 'antd'
import { Link } from 'lucide-react'
import { FC, memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
export interface UrlContextButtonRef {
openQuickPanel: () => void
}
interface Props {
ref?: React.RefObject<UrlContextButtonRef | null>
assistant: Assistant
ToolbarButton: any
}
const UrlContextButton: FC<Props> = ({ assistant, ToolbarButton }) => {
const { t } = useTranslation()
const { updateAssistant } = useAssistant(assistant.id)
const urlContentNewState = !assistant.enableUrlContext
const handleToggle = useCallback(() => {
setTimeout(() => {
updateAssistant({ ...assistant, enableUrlContext: urlContentNewState })
}, 100)
}, [assistant, urlContentNewState, updateAssistant])
return (
<Tooltip placement="top" title={t('chat.input.url_context')} arrow>
<ToolbarButton type="text" onClick={handleToggle}>
<Link
size={18}
style={{
color: assistant.enableUrlContext ? 'var(--color-link)' : 'var(--color-icon)'
}}
/>
</ToolbarButton>
</Tooltip>
)
}
export default memo(UrlContextButton)

View File

@ -354,6 +354,8 @@ export async function fetchChatCompletion({
model.id.includes('sonar') ||
false
const enableUrlContext = assistant.enableUrlContext || false
const enableGenerateImage =
isGenerateImageModel(model) && (isSupportedDisableGenerationModel(model) ? assistant.enableGenerateImage : true)
@ -370,6 +372,7 @@ export async function fetchChatCompletion({
streamOutput: assistant.settings?.streamOutput || false,
enableReasoning,
enableWebSearch,
enableUrlContext,
enableGenerateImage
},
{

View File

@ -54,7 +54,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 120,
version: 121,
blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate
},

View File

@ -11,6 +11,7 @@ export const DEFAULT_TOOL_ORDER: ToolOrder = {
'attachment',
'thinking',
'web_search',
'url_context',
'knowledge_base',
'mcp_tools',
'generate_image',

View File

@ -1763,6 +1763,24 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'121': (state: RootState) => {
try {
const { toolOrder } = state.inputTools
const urlContextKey = 'url_context'
const webSearchIndex = toolOrder.visible.indexOf('web_search')
const knowledgeBaseIndex = toolOrder.visible.indexOf('knowledge_base')
if (webSearchIndex !== -1) {
toolOrder.visible.splice(webSearchIndex, 0, urlContextKey)
} else if (knowledgeBaseIndex !== -1) {
toolOrder.visible.splice(knowledgeBaseIndex, 0, urlContextKey)
} else {
toolOrder.visible.push(urlContextKey)
}
return state
} catch (error) {
return state
}
}
}

View File

@ -23,6 +23,8 @@ export type Assistant = {
/** enableWebSearch 代表使用模型内置网络搜索功能 */
enableWebSearch?: boolean
webSearchProviderId?: WebSearchProvider['id']
// enableUrlContext 是 Gemini 的特有功能
enableUrlContext?: boolean
enableGenerateImage?: boolean
mcpServers?: MCPServer[]
knowledgeRecognition?: 'off' | 'on'