refactor: 将网络搜索参数用于模型内置搜索 (#10213)

* Remove local provider option files and use external packages

Replace local implementation of XAI and OpenRouter provider options with
external packages (@ai-sdk/xai and @openrouter/ai-sdk-provider). Update
web search plugin to support additional providers including OpenAI Chat
and OpenRouter, with improved configuration mapping.

* Bump @cherrystudio/ai-core to v1.0.0-alpha.17

fix i18n

* fix(i18n): Auto update translations for PR #10213

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
SuYao 2025-09-17 15:10:30 +08:00 committed by GitHub
parent 1481149e51
commit a8bf55abc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 359 additions and 173 deletions

View File

@ -108,7 +108,7 @@
"@aws-sdk/client-bedrock-runtime": "^3.840.0", "@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/client-s3": "^3.840.0", "@aws-sdk/client-s3": "^3.840.0",
"@biomejs/biome": "2.2.4", "@biomejs/biome": "2.2.4",
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.16", "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17",
"@cherrystudio/embedjs": "^0.1.31", "@cherrystudio/embedjs": "^0.1.31",
"@cherrystudio/embedjs-libsql": "^0.1.31", "@cherrystudio/embedjs-libsql": "^0.1.31",
"@cherrystudio/embedjs-loader-csv": "^0.1.31", "@cherrystudio/embedjs-loader-csv": "^0.1.31",

View File

@ -1,6 +1,6 @@
{ {
"name": "@cherrystudio/ai-core", "name": "@cherrystudio/ai-core",
"version": "1.0.0-alpha.16", "version": "1.0.0-alpha.17",
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK", "description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.mjs", "module": "dist/index.mjs",

View File

@ -59,7 +59,7 @@ export function createGoogleOptions(options: ExtractProviderOptions<'google'>) {
/** /**
* OpenRouter供应商选项的便捷函数 * OpenRouter供应商选项的便捷函数
*/ */
export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'>) { export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'> | Record<string, any>) {
return createProviderOptions('openrouter', options) return createProviderOptions('openrouter', options)
} }

View File

@ -1,38 +0,0 @@
export type OpenRouterProviderOptions = {
models?: string[]
/**
* https://openrouter.ai/docs/use-cases/reasoning-tokens
* One of `max_tokens` or `effort` is required.
* If `exclude` is true, reasoning will be removed from the response. Default is false.
*/
reasoning?: {
exclude?: boolean
} & (
| {
max_tokens: number
}
| {
effort: 'high' | 'medium' | 'low'
}
)
/**
* A unique identifier representing your end-user, which can
* help OpenRouter to monitor and detect abuse.
*/
user?: string
extraBody?: Record<string, unknown>
/**
* Enable usage accounting to get detailed token usage information.
* https://openrouter.ai/docs/use-cases/usage-accounting
*/
usage?: {
/**
* When true, includes token usage information in the response.
*/
include: boolean
}
}

View File

@ -2,9 +2,8 @@ import { type AnthropicProviderOptions } from '@ai-sdk/anthropic'
import { type GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' import { type GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'
import { type OpenAIResponsesProviderOptions } from '@ai-sdk/openai' import { type OpenAIResponsesProviderOptions } from '@ai-sdk/openai'
import { type SharedV2ProviderMetadata } from '@ai-sdk/provider' import { type SharedV2ProviderMetadata } from '@ai-sdk/provider'
import { type XaiProviderOptions } from '@ai-sdk/xai'
import { type OpenRouterProviderOptions } from './openrouter' import { type OpenRouterProviderOptions } from '@openrouter/ai-sdk-provider'
import { type XaiProviderOptions } from './xai'
export type ProviderOptions<T extends keyof SharedV2ProviderMetadata> = SharedV2ProviderMetadata[T] export type ProviderOptions<T extends keyof SharedV2ProviderMetadata> = SharedV2ProviderMetadata[T]

View File

@ -1,86 +0,0 @@
// copy from @ai-sdk/xai/xai-chat-options.ts
// 如果@ai-sdk/xai暴露出了xaiProviderOptions就删除这个文件
import { z } from 'zod'
const webSourceSchema = z.object({
type: z.literal('web'),
country: z.string().length(2).optional(),
excludedWebsites: z.array(z.string()).max(5).optional(),
allowedWebsites: z.array(z.string()).max(5).optional(),
safeSearch: z.boolean().optional()
})
const xSourceSchema = z.object({
type: z.literal('x'),
xHandles: z.array(z.string()).optional()
})
const newsSourceSchema = z.object({
type: z.literal('news'),
country: z.string().length(2).optional(),
excludedWebsites: z.array(z.string()).max(5).optional(),
safeSearch: z.boolean().optional()
})
const rssSourceSchema = z.object({
type: z.literal('rss'),
links: z.array(z.url()).max(1) // currently only supports one RSS link
})
const searchSourceSchema = z.discriminatedUnion('type', [
webSourceSchema,
xSourceSchema,
newsSourceSchema,
rssSourceSchema
])
export const xaiProviderOptions = z.object({
/**
* reasoning effort for reasoning models
* only supported by grok-3-mini and grok-3-mini-fast models
*/
reasoningEffort: z.enum(['low', 'high']).optional(),
searchParameters: z
.object({
/**
* search mode preference
* - "off": disables search completely
* - "auto": model decides whether to search (default)
* - "on": always enables search
*/
mode: z.enum(['off', 'auto', 'on']),
/**
* whether to return citations in the response
* defaults to true
*/
returnCitations: z.boolean().optional(),
/**
* start date for search data (ISO8601 format: YYYY-MM-DD)
*/
fromDate: z.string().optional(),
/**
* end date for search data (ISO8601 format: YYYY-MM-DD)
*/
toDate: z.string().optional(),
/**
* maximum number of search results to consider
* defaults to 20
*/
maxSearchResults: z.number().min(1).max(50).optional(),
/**
* data sources to search from
* defaults to ["web", "x"] if not specified
*/
sources: z.array(searchSourceSchema).optional()
})
.optional()
})
export type XaiProviderOptions = z.infer<typeof xaiProviderOptions>

View File

@ -7,5 +7,9 @@ export const BUILT_IN_PLUGIN_PREFIX = 'built-in:'
export { googleToolsPlugin } from './googleToolsPlugin' export { googleToolsPlugin } from './googleToolsPlugin'
export { createLoggingPlugin } from './logging' export { createLoggingPlugin } from './logging'
export { createPromptToolUsePlugin } from './toolUsePlugin/promptToolUsePlugin' export { createPromptToolUsePlugin } from './toolUsePlugin/promptToolUsePlugin'
export type { PromptToolUseConfig, ToolUseRequestContext, ToolUseResult } from './toolUsePlugin/type' export type {
export { webSearchPlugin } from './webSearchPlugin' PromptToolUseConfig,
ToolUseRequestContext,
ToolUseResult
} from './toolUsePlugin/type'
export { webSearchPlugin, type WebSearchPluginConfig } from './webSearchPlugin'

View File

@ -3,13 +3,16 @@ import { google } from '@ai-sdk/google'
import { openai } from '@ai-sdk/openai' import { openai } from '@ai-sdk/openai'
import { ProviderOptionsMap } from '../../../options/types' import { ProviderOptionsMap } from '../../../options/types'
import { OpenRouterSearchConfig } from './openrouter'
/** /**
* AI SDK * AI SDK
*/ */
type OpenAISearchConfig = Parameters<typeof openai.tools.webSearchPreview>[0] export type OpenAISearchConfig = NonNullable<Parameters<typeof openai.tools.webSearch>[0]>
type AnthropicSearchConfig = Parameters<typeof anthropic.tools.webSearch_20250305>[0] export type OpenAISearchPreviewConfig = NonNullable<Parameters<typeof openai.tools.webSearchPreview>[0]>
type GoogleSearchConfig = Parameters<typeof google.tools.googleSearch>[0] export type AnthropicSearchConfig = NonNullable<Parameters<typeof anthropic.tools.webSearch_20250305>[0]>
export type GoogleSearchConfig = NonNullable<Parameters<typeof google.tools.googleSearch>[0]>
export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParameters']>
/** /**
* *
@ -18,10 +21,12 @@ type GoogleSearchConfig = Parameters<typeof google.tools.googleSearch>[0]
*/ */
export interface WebSearchPluginConfig { export interface WebSearchPluginConfig {
openai?: OpenAISearchConfig openai?: OpenAISearchConfig
'openai-chat'?: OpenAISearchPreviewConfig
anthropic?: AnthropicSearchConfig anthropic?: AnthropicSearchConfig
xai?: ProviderOptionsMap['xai']['searchParameters'] xai?: ProviderOptionsMap['xai']['searchParameters']
google?: GoogleSearchConfig google?: GoogleSearchConfig
'google-vertex'?: GoogleSearchConfig 'google-vertex'?: GoogleSearchConfig
openrouter?: OpenRouterSearchConfig
} }
/** /**
@ -31,6 +36,7 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
google: {}, google: {},
'google-vertex': {}, 'google-vertex': {},
openai: {}, openai: {},
'openai-chat': {},
xai: { xai: {
mode: 'on', mode: 'on',
returnCitations: true, returnCitations: true,
@ -39,6 +45,14 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
}, },
anthropic: { anthropic: {
maxUses: 5 maxUses: 5
},
openrouter: {
plugins: [
{
id: 'web',
max_results: 5
}
]
} }
} }

View File

@ -6,7 +6,7 @@ import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google' import { google } from '@ai-sdk/google'
import { openai } from '@ai-sdk/openai' import { openai } from '@ai-sdk/openai'
import { createXaiOptions, mergeProviderOptions } from '../../../options' import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
import { definePlugin } from '../../' import { definePlugin } from '../../'
import type { AiRequestContext } from '../../types' import type { AiRequestContext } from '../../types'
import { DEFAULT_WEB_SEARCH_CONFIG, WebSearchPluginConfig } from './helper' import { DEFAULT_WEB_SEARCH_CONFIG, WebSearchPluginConfig } from './helper'
@ -31,6 +31,13 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
} }
break break
} }
case 'openai-chat': {
if (config['openai-chat']) {
if (!params.tools) params.tools = {}
params.tools.web_search_preview = openai.tools.webSearchPreview(config['openai-chat'])
}
break
}
case 'anthropic': { case 'anthropic': {
if (config.anthropic) { if (config.anthropic) {
@ -56,6 +63,14 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
} }
break break
} }
case 'openrouter': {
if (config.openrouter) {
const searchOptions = createOpenRouterOptions(config.openrouter)
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
}
break
}
} }
return params return params

View File

@ -0,0 +1,26 @@
export type OpenRouterSearchConfig = {
plugins?: Array<{
id: 'web'
/**
* Maximum number of search results to include (default: 5)
*/
max_results?: number
/**
* Custom search prompt to guide the search query
*/
search_prompt?: string
}>
/**
* Built-in web search options for models that support native web search
*/
web_search_options?: {
/**
* Maximum number of search results to include
*/
max_results?: number
/**
* Custom search prompt to guide the search query
*/
search_prompt?: string
}
}

View File

@ -25,7 +25,8 @@ export const baseProviderIds = [
'xai', 'xai',
'azure', 'azure',
'azure-responses', 'azure-responses',
'deepseek' 'deepseek',
'openrouter'
] as const ] as const
/** /**
@ -38,6 +39,10 @@ export const baseProviderIdSchema = z.enum(baseProviderIds)
*/ */
export type BaseProviderId = z.infer<typeof baseProviderIdSchema> export type BaseProviderId = z.infer<typeof baseProviderIdSchema>
export const isBaseProvider = (id: ProviderId): id is BaseProviderId => {
return baseProviderIdSchema.safeParse(id).success
}
type BaseProvider = { type BaseProvider = {
id: BaseProviderId id: BaseProviderId
name: string name: string

View File

@ -1,3 +1,4 @@
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { MCPTool, Message, Model, Provider } from '@renderer/types' import type { MCPTool, Message, Model, Provider } from '@renderer/types'
import type { Chunk } from '@renderer/types/chunk' import type { Chunk } from '@renderer/types/chunk'
@ -26,6 +27,8 @@ export interface AiSdkMiddlewareConfig {
enableUrlContext: boolean enableUrlContext: boolean
mcpTools?: MCPTool[] mcpTools?: MCPTool[]
uiMessages?: Message[] uiMessages?: Message[]
// 内置搜索配置
webSearchPluginConfig?: WebSearchPluginConfig
} }
/** /**

View File

@ -30,9 +30,8 @@ export function buildPlugins(
} }
// 1. 模型内置搜索 // 1. 模型内置搜索
if (middlewareConfig.enableWebSearch) { if (middlewareConfig.enableWebSearch && middlewareConfig.webSearchPluginConfig) {
// 内置了默认搜索参数如果改的话可以传config进去 plugins.push(webSearchPlugin(middlewareConfig.webSearchPluginConfig))
plugins.push(webSearchPlugin())
} }
// 2. 支持工具调用时添加搜索插件 // 2. 支持工具调用时添加搜索插件
if (middlewareConfig.isSupportedToolUse || middlewareConfig.isPromptToolUse) { if (middlewareConfig.isSupportedToolUse || middlewareConfig.isPromptToolUse) {

View File

@ -5,6 +5,8 @@
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge' import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
import { vertex } from '@ai-sdk/google-vertex/edge' import { vertex } from '@ai-sdk/google-vertex/edge'
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
import { isBaseProvider } from '@cherrystudio/ai-core/core/providers/schemas'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { import {
isGenerateImageModel, isGenerateImageModel,
@ -16,8 +18,11 @@ import {
isWebSearchModel isWebSearchModel
} from '@renderer/config/models' } from '@renderer/config/models'
import { getAssistantSettings, getDefaultModel } from '@renderer/services/AssistantService' import { getAssistantSettings, getDefaultModel } from '@renderer/services/AssistantService'
import store from '@renderer/store'
import { CherryWebSearchConfig } from '@renderer/store/websearch'
import { type Assistant, type MCPTool, type Provider } from '@renderer/types' import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
import type { StreamTextParams } from '@renderer/types/aiCoreTypes' import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
import type { ModelMessage, Tool } from 'ai' import type { ModelMessage, Tool } from 'ai'
import { stepCountIs } from 'ai' import { stepCountIs } from 'ai'
@ -25,6 +30,7 @@ import { getAiSdkProviderId } from '../provider/factory'
import { setupToolsConfig } from '../utils/mcp' import { setupToolsConfig } from '../utils/mcp'
import { buildProviderOptions } from '../utils/options' import { buildProviderOptions } from '../utils/options'
import { getAnthropicThinkingBudget } from '../utils/reasoning' import { getAnthropicThinkingBudget } from '../utils/reasoning'
import { buildProviderBuiltinWebSearchConfig } from '../utils/websearch'
import { getTemperature, getTopP } from './modelParameters' import { getTemperature, getTopP } from './modelParameters'
const logger = loggerService.withContext('parameterBuilder') const logger = loggerService.withContext('parameterBuilder')
@ -42,6 +48,7 @@ export async function buildStreamTextParams(
options: { options: {
mcpTools?: MCPTool[] mcpTools?: MCPTool[]
webSearchProviderId?: string webSearchProviderId?: string
webSearchConfig?: CherryWebSearchConfig
requestOptions?: { requestOptions?: {
signal?: AbortSignal signal?: AbortSignal
timeout?: number timeout?: number
@ -57,6 +64,7 @@ export async function buildStreamTextParams(
enableGenerateImage: boolean enableGenerateImage: boolean
enableUrlContext: boolean enableUrlContext: boolean
} }
webSearchPluginConfig?: WebSearchPluginConfig
}> { }> {
const { mcpTools } = options const { mcpTools } = options
@ -93,6 +101,12 @@ export async function buildStreamTextParams(
// } // }
// 构建真正的 providerOptions // 构建真正的 providerOptions
const webSearchConfig: CherryWebSearchConfig = {
maxResults: store.getState().websearch.maxResults,
excludeDomains: store.getState().websearch.excludeDomains,
searchWithTime: store.getState().websearch.searchWithTime
}
const providerOptions = buildProviderOptions(assistant, model, provider, { const providerOptions = buildProviderOptions(assistant, model, provider, {
enableReasoning, enableReasoning,
enableWebSearch, enableWebSearch,
@ -109,15 +123,21 @@ export async function buildStreamTextParams(
maxTokens -= getAnthropicThinkingBudget(assistant, model) maxTokens -= getAnthropicThinkingBudget(assistant, model)
} }
// google-vertex | google-vertex-anthropic let webSearchPluginConfig: WebSearchPluginConfig | undefined = undefined
if (enableWebSearch) { if (enableWebSearch) {
if (isBaseProvider(aiSdkProviderId)) {
webSearchPluginConfig = buildProviderBuiltinWebSearchConfig(aiSdkProviderId, webSearchConfig)
}
if (!tools) { if (!tools) {
tools = {} tools = {}
} }
if (aiSdkProviderId === 'google-vertex') { if (aiSdkProviderId === 'google-vertex') {
tools.google_search = vertex.tools.googleSearch({}) as ProviderDefinedTool tools.google_search = vertex.tools.googleSearch({}) as ProviderDefinedTool
} else if (aiSdkProviderId === 'google-vertex-anthropic') { } else if (aiSdkProviderId === 'google-vertex-anthropic') {
tools.web_search = vertexAnthropic.tools.webSearch_20250305({}) as ProviderDefinedTool tools.web_search = vertexAnthropic.tools.webSearch_20250305({
maxUses: webSearchConfig.maxResults,
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
}) as ProviderDefinedTool
} }
} }
@ -151,7 +171,8 @@ export async function buildStreamTextParams(
return { return {
params, params,
modelId: model.id, modelId: model.id,
capabilities: { enableReasoning, enableWebSearch, enableGenerateImage, enableUrlContext } capabilities: { enableReasoning, enableWebSearch, enableGenerateImage, enableUrlContext },
webSearchPluginConfig
} }
} }

View File

@ -1,6 +1,13 @@
import {
AnthropicSearchConfig,
OpenAISearchConfig,
WebSearchPluginConfig
} from '@cherrystudio/ai-core/core/plugins/built-in/webSearchPlugin/helper'
import { BaseProviderId } from '@cherrystudio/ai-core/provider'
import { isOpenAIWebSearchChatCompletionOnlyModel } from '@renderer/config/models' import { isOpenAIWebSearchChatCompletionOnlyModel } from '@renderer/config/models'
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '@renderer/config/prompts' import { CherryWebSearchConfig } from '@renderer/store/websearch'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
export function getWebSearchParams(model: Model): Record<string, any> { export function getWebSearchParams(model: Model): Record<string, any> {
if (model.provider === 'hunyuan') { if (model.provider === 'hunyuan') {
@ -21,11 +28,78 @@ export function getWebSearchParams(model: Model): Record<string, any> {
web_search_options: {} web_search_options: {}
} }
} }
if (model.provider === 'openrouter') {
return {
plugins: [{ id: 'web', search_prompts: WEB_SEARCH_PROMPT_FOR_OPENROUTER }]
}
}
return {} return {}
} }
/**
* range in [0, 100]
* @param maxResults
*/
function mapMaxResultToOpenAIContextSize(maxResults: number): OpenAISearchConfig['searchContextSize'] {
if (maxResults <= 33) return 'low'
if (maxResults <= 66) return 'medium'
return 'high'
}
export function buildProviderBuiltinWebSearchConfig(
providerId: BaseProviderId,
webSearchConfig: CherryWebSearchConfig
): WebSearchPluginConfig {
switch (providerId) {
case 'openai': {
return {
openai: {
searchContextSize: mapMaxResultToOpenAIContextSize(webSearchConfig.maxResults)
}
}
}
case 'openai-chat': {
return {
'openai-chat': {
searchContextSize: mapMaxResultToOpenAIContextSize(webSearchConfig.maxResults)
}
}
}
case 'anthropic': {
const anthropicSearchOptions: AnthropicSearchConfig = {
maxUses: webSearchConfig.maxResults,
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
}
return {
anthropic: anthropicSearchOptions
}
}
case 'xai': {
return {
xai: {
maxSearchResults: webSearchConfig.maxResults,
returnCitations: true,
sources: [
{
type: 'web',
excludedWebsites: mapRegexToPatterns(webSearchConfig.excludeDomains)
},
{ type: 'news' },
{ type: 'x' }
],
mode: 'on'
}
}
}
case 'openrouter': {
return {
openrouter: {
plugins: [
{
id: 'web',
max_results: webSearchConfig.maxResults
}
]
}
}
}
default: {
throw new Error(`Unsupported provider: ${providerId}`)
}
}
}

View File

@ -229,6 +229,11 @@ export const isGPT5SeriesModel = (model: Model) => {
return modelId.includes('gpt-5') return modelId.includes('gpt-5')
} }
export const isGPT5SeriesReasoningModel = (model: Model) => {
const modelId = getLowerBaseModelName(model.id)
return modelId.includes('gpt-5') && !modelId.includes('chat')
}
export const isGeminiModel = (model: Model) => { export const isGeminiModel = (model: Model) => {
const modelId = getLowerBaseModelName(model.id) const modelId = getLowerBaseModelName(model.id)
return modelId.includes('gemini') return modelId.includes('gemini')

View File

@ -660,7 +660,12 @@
"title": "Topics", "title": "Topics",
"unpin": "Unpin Topic" "unpin": "Unpin Topic"
}, },
"translate": "Translate" "translate": "Translate",
"web_search": {
"warning": {
"openai": "The GPT-5 model's minimal reasoning effort does not support web search."
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "Automatically update to latest version", "auto_update_to_latest": "Automatically update to latest version",

View File

@ -132,7 +132,6 @@
}, },
"title": "API 服务器" "title": "API 服务器"
}, },
"assistants": { "assistants": {
"abbr": "助手", "abbr": "助手",
"clear": { "clear": {
@ -661,7 +660,12 @@
"title": "话题", "title": "话题",
"unpin": "取消固定" "unpin": "取消固定"
}, },
"translate": "翻译" "translate": "翻译",
"web_search": {
"warning": {
"openai": "GPT5 模型 minimal 思考强度不支持网络搜索"
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "检查更新并安装最新版本", "auto_update_to_latest": "检查更新并安装最新版本",

View File

@ -660,7 +660,12 @@
"title": "話題", "title": "話題",
"unpin": "取消固定" "unpin": "取消固定"
}, },
"translate": "翻譯" "translate": "翻譯",
"web_search": {
"warning": {
"openai": "GPT-5 模型的最小推理力度不支援網路搜尋。"
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "檢查更新並安裝最新版本", "auto_update_to_latest": "檢查更新並安裝最新版本",

View File

@ -362,8 +362,9 @@
"translate": "Μετάφραση στο {{target_language}}", "translate": "Μετάφραση στο {{target_language}}",
"translating": "Μετάφραση...", "translating": "Μετάφραση...",
"upload": { "upload": {
"attachment": "Μεταφόρτωση συνημμένου",
"document": "Φόρτωση έγγραφου (το μοντέλο δεν υποστηρίζει εικόνες)", "document": "Φόρτωση έγγραφου (το μοντέλο δεν υποστηρίζει εικόνες)",
"label": "Φόρτωση εικόνας ή έγγραφου", "image_or_document": "Μεταφόρτωση εικόνας ή εγγράφου",
"upload_from_local": "Μεταφόρτωση αρχείου από τον υπολογιστή..." "upload_from_local": "Μεταφόρτωση αρχείου από τον υπολογιστή..."
}, },
"url_context": "Περιεχόμενο ιστοσελίδας", "url_context": "Περιεχόμενο ιστοσελίδας",
@ -659,7 +660,12 @@
"title": "Θέματα", "title": "Θέματα",
"unpin": "Ξεκαρφίτσωμα" "unpin": "Ξεκαρφίτσωμα"
}, },
"translate": "Μετάφραση" "translate": "Μετάφραση",
"web_search": {
"warning": {
"openai": "Το μοντέλο GPT5 με ελάχιστη ένταση σκέψης δεν υποστηρίζει αναζήτηση στο διαδίκτυο"
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "Έλεγχος για ενημερώσεις και εγκατάσταση της τελευταίας έκδοσης", "auto_update_to_latest": "Έλεγχος για ενημερώσεις και εγκατάσταση της τελευταίας έκδοσης",

View File

@ -362,8 +362,9 @@
"translate": "Traducir a {{target_language}}", "translate": "Traducir a {{target_language}}",
"translating": "Traduciendo...", "translating": "Traduciendo...",
"upload": { "upload": {
"attachment": "Subir archivo adjunto",
"document": "Subir documento (el modelo no admite imágenes)", "document": "Subir documento (el modelo no admite imágenes)",
"label": "Subir imagen o documento", "image_or_document": "Subir imagen o documento",
"upload_from_local": "Subir archivo local..." "upload_from_local": "Subir archivo local..."
}, },
"url_context": "Contexto de la página web", "url_context": "Contexto de la página web",
@ -659,7 +660,12 @@
"title": "Tema", "title": "Tema",
"unpin": "Quitar fijación" "unpin": "Quitar fijación"
}, },
"translate": "Traducir" "translate": "Traducir",
"web_search": {
"warning": {
"openai": "El modelo GPT5 con intensidad de pensamiento mínima no admite búsqueda en la web."
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "Comprobar actualizaciones e instalar la versión más reciente", "auto_update_to_latest": "Comprobar actualizaciones e instalar la versión más reciente",

View File

@ -362,8 +362,9 @@
"translate": "Traduire en {{target_language}}", "translate": "Traduire en {{target_language}}",
"translating": "Traduction en cours...", "translating": "Traduction en cours...",
"upload": { "upload": {
"attachment": "Télécharger la pièce jointe",
"document": "Télécharger un document (le modèle ne prend pas en charge les images)", "document": "Télécharger un document (le modèle ne prend pas en charge les images)",
"label": "Télécharger une image ou un document", "image_or_document": "Télécharger une image ou un document",
"upload_from_local": "Télécharger un fichier local..." "upload_from_local": "Télécharger un fichier local..."
}, },
"url_context": "Contexte de la page web", "url_context": "Contexte de la page web",
@ -659,7 +660,12 @@
"title": "Sujet", "title": "Sujet",
"unpin": "Annuler le fixage" "unpin": "Annuler le fixage"
}, },
"translate": "Traduire" "translate": "Traduire",
"web_search": {
"warning": {
"openai": "Le modèle GPT5 avec une intensité de réflexion minimale ne prend pas en charge la recherche sur Internet."
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "Vérifier les mises à jour et installer la dernière version", "auto_update_to_latest": "Vérifier les mises à jour et installer la dernière version",

View File

@ -362,8 +362,9 @@
"translate": "{{target_language}}に翻訳", "translate": "{{target_language}}に翻訳",
"translating": "翻訳中...", "translating": "翻訳中...",
"upload": { "upload": {
"attachment": "添付ファイルをアップロード",
"document": "ドキュメントをアップロード(モデルは画像をサポートしません)", "document": "ドキュメントをアップロード(モデルは画像をサポートしません)",
"label": "画像またはドキュメントをアップロード", "image_or_document": "画像またはドキュメントをアップロード",
"upload_from_local": "ローカルファイルをアップロード..." "upload_from_local": "ローカルファイルをアップロード..."
}, },
"url_context": "URLコンテキスト", "url_context": "URLコンテキスト",
@ -659,7 +660,12 @@
"title": "トピック", "title": "トピック",
"unpin": "固定解除" "unpin": "固定解除"
}, },
"translate": "翻訳" "translate": "翻訳",
"web_search": {
"warning": {
"openai": "GPT5モデルの最小思考強度ではネット検索はサポートされません"
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "最新バージョンを自動的に更新する", "auto_update_to_latest": "最新バージョンを自動的に更新する",

View File

@ -362,8 +362,9 @@
"translate": "Traduzir para {{target_language}}", "translate": "Traduzir para {{target_language}}",
"translating": "Traduzindo...", "translating": "Traduzindo...",
"upload": { "upload": {
"attachment": "Carregar anexo",
"document": "Carregar documento (o modelo não suporta imagens)", "document": "Carregar documento (o modelo não suporta imagens)",
"label": "Carregar imagem ou documento", "image_or_document": "Carregar imagem ou documento",
"upload_from_local": "Fazer upload de arquivo local..." "upload_from_local": "Fazer upload de arquivo local..."
}, },
"url_context": "Contexto da Página da Web", "url_context": "Contexto da Página da Web",
@ -659,7 +660,12 @@
"title": "Tópicos", "title": "Tópicos",
"unpin": "Desfixar" "unpin": "Desfixar"
}, },
"translate": "Traduzir" "translate": "Traduzir",
"web_search": {
"warning": {
"openai": "O modelo GPT5 com intensidade mínima de pensamento não suporta pesquisa na web"
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "Verificar atualizações e instalar a versão mais recente", "auto_update_to_latest": "Verificar atualizações e instalar a versão mais recente",

View File

@ -362,8 +362,9 @@
"translate": "Перевести на {{target_language}}", "translate": "Перевести на {{target_language}}",
"translating": "Перевод...", "translating": "Перевод...",
"upload": { "upload": {
"attachment": "Загрузить вложение",
"document": "Загрузить документ (модель не поддерживает изображения)", "document": "Загрузить документ (модель не поддерживает изображения)",
"label": "Загрузить изображение или документ", "image_or_document": "Загрузить изображение или документ",
"upload_from_local": "Загрузить локальный файл..." "upload_from_local": "Загрузить локальный файл..."
}, },
"url_context": "Контекст страницы", "url_context": "Контекст страницы",
@ -659,7 +660,12 @@
"title": "Топики", "title": "Топики",
"unpin": "Открепленные темы" "unpin": "Открепленные темы"
}, },
"translate": "Перевести" "translate": "Перевести",
"web_search": {
"warning": {
"openai": "Модель GPT5 с минимальной интенсивностью мышления не поддерживает поиск в интернете"
}
}
}, },
"code": { "code": {
"auto_update_to_latest": "Автоматически обновлять до последней версии", "auto_update_to_latest": "Автоматически обновлять до последней версии",

View File

@ -8,7 +8,13 @@ import {
MdiLightbulbOn80 MdiLightbulbOn80
} from '@renderer/components/Icons/SVGIcon' } from '@renderer/components/Icons/SVGIcon'
import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel' import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { getThinkModelType, isDoubaoThinkingAutoModel, MODEL_SUPPORTED_OPTIONS } from '@renderer/config/models' import {
getThinkModelType,
isDoubaoThinkingAutoModel,
isGPT5SeriesReasoningModel,
isOpenAIWebSearchModel,
MODEL_SUPPORTED_OPTIONS
} from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { getReasoningEffortOptionsLabel } from '@renderer/i18n/label' import { getReasoningEffortOptionsLabel } from '@renderer/i18n/label'
import { Model, ThinkingOption } from '@renderer/types' import { Model, ThinkingOption } from '@renderer/types'
@ -61,6 +67,15 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistantId }): ReactElement =>
}) })
return return
} }
if (
isOpenAIWebSearchModel(model) &&
isGPT5SeriesReasoningModel(model) &&
assistant.enableWebSearch &&
option === 'minimal'
) {
window.toast.warning(t('chat.web_search.warning.openai'))
return
}
updateAssistantSettings({ updateAssistantSettings({
reasoning_effort: option, reasoning_effort: option,
reasoning_effort_cache: option, reasoning_effort_cache: option,
@ -68,7 +83,7 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistantId }): ReactElement =>
}) })
return return
}, },
[updateAssistantSettings] [updateAssistantSettings, assistant.enableWebSearch, model, t]
) )
const panelItems = useMemo(() => { const panelItems = useMemo(() => {

View File

@ -3,7 +3,12 @@ import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons' import { ActionIconButton } from '@renderer/components/Buttons'
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons' import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons'
import { QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel' import { QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { isGeminiModel, isWebSearchModel } from '@renderer/config/models' import {
isGeminiModel,
isGPT5SeriesReasoningModel,
isOpenAIWebSearchModel,
isWebSearchModel
} from '@renderer/config/models'
import { isGeminiWebSearchProvider } from '@renderer/config/providers' import { isGeminiWebSearchProvider } from '@renderer/config/providers'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
@ -115,6 +120,15 @@ const WebSearchButton: FC<Props> = ({ ref, assistantId }) => {
update.enableWebSearch = false update.enableWebSearch = false
window.toast.warning(t('chat.mcp.warning.gemini_web_search')) window.toast.warning(t('chat.mcp.warning.gemini_web_search'))
} }
if (
isOpenAIWebSearchModel(model) &&
isGPT5SeriesReasoningModel(model) &&
update.enableWebSearch &&
assistant.settings?.reasoning_effort === 'minimal'
) {
update.enableWebSearch = false
window.toast.warning(t('chat.web_search.warning.openai'))
}
setTimeoutTimer('updateSelectedWebSearchBuiltin', () => updateAssistant(update), 200) setTimeoutTimer('updateSelectedWebSearchBuiltin', () => updateAssistant(update), 200)
}, [assistant, setTimeoutTimer, t, updateAssistant]) }, [assistant, setTimeoutTimer, t, updateAssistant])

View File

@ -117,7 +117,8 @@ export async function fetchChatCompletion({
const { const {
params: aiSdkParams, params: aiSdkParams,
modelId, modelId,
capabilities capabilities,
webSearchPluginConfig
} = await buildStreamTextParams(messages, assistant, provider, { } = await buildStreamTextParams(messages, assistant, provider, {
mcpTools: mcpTools, mcpTools: mcpTools,
webSearchProviderId: assistant.webSearchProviderId, webSearchProviderId: assistant.webSearchProviderId,
@ -132,6 +133,7 @@ export async function fetchChatCompletion({
isPromptToolUse: isPromptToolUse(assistant), isPromptToolUse: isPromptToolUse(assistant),
isSupportedToolUse: isSupportedToolUse(assistant), isSupportedToolUse: isSupportedToolUse(assistant),
isImageGenerationEndpoint: isDedicatedImageGenerationModel(assistant.model || getDefaultModel()), isImageGenerationEndpoint: isDedicatedImageGenerationModel(assistant.model || getDefaultModel()),
webSearchPluginConfig: webSearchPluginConfig,
enableWebSearch: capabilities.enableWebSearch, enableWebSearch: capabilities.enableWebSearch,
enableGenerateImage: capabilities.enableGenerateImage, enableGenerateImage: capabilities.enableGenerateImage,
enableUrlContext: capabilities.enableUrlContext, enableUrlContext: capabilities.enableUrlContext,

View File

@ -41,6 +41,8 @@ export interface WebSearchState {
providerConfig: Record<string, any> providerConfig: Record<string, any>
} }
export type CherryWebSearchConfig = Pick<WebSearchState, 'searchWithTime' | 'maxResults' | 'excludeDomains'>
export const initialState: WebSearchState = { export const initialState: WebSearchState = {
defaultProvider: 'local-bing', defaultProvider: 'local-bing',
providers: WEB_SEARCH_PROVIDERS, providers: WEB_SEARCH_PROVIDERS,

View File

@ -26,7 +26,7 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { MatchPatternMap } from '../blacklistMatchPattern' import { mapRegexToPatterns, MatchPatternMap } from '../blacklistMatchPattern'
function get(map: MatchPatternMap<number>, url: string) { function get(map: MatchPatternMap<number>, url: string) {
return map.get(url).sort() return map.get(url).sort()
@ -161,3 +161,28 @@ describe('blacklistMatchPattern', () => {
expect(get(map, 'https://b.mozilla.org/path/')).toEqual([0, 1, 2, 6]) expect(get(map, 'https://b.mozilla.org/path/')).toEqual([0, 1, 2, 6])
}) })
}) })
describe('mapRegexToPatterns', () => {
it('extracts domains from regex patterns', () => {
const result = mapRegexToPatterns([
'/example\\.com/',
'/(?:www\\.)?sub\\.example\\.co\\.uk/',
'/api\\.service\\.io/',
'https://baidu.com'
])
expect(result).toEqual(['example.com', 'sub.example.co.uk', 'api.service.io', 'baidu.com'])
})
it('deduplicates domains across multiple patterns', () => {
const result = mapRegexToPatterns(['/example\\.com/', '/(example\\.com|test\\.org)/'])
expect(result).toEqual(['example.com', 'test.org'])
})
it('ignores patterns without domain matches', () => {
const result = mapRegexToPatterns(['', 'plain-domain.com', '/^https?:\\/\\/[^/]+$/'])
expect(result).toEqual(['plain-domain.com'])
})
})

View File

@ -202,6 +202,43 @@ export async function parseSubscribeContent(url: string): Promise<string[]> {
throw error throw error
} }
} }
export function mapRegexToPatterns(patterns: string[]): string[] {
const patternSet = new Set<string>()
const domainMatcher = /[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g
patterns.forEach((pattern) => {
if (!pattern) {
return
}
// Handle regex patterns (wrapped in /)
if (pattern.startsWith('/') && pattern.endsWith('/')) {
const rawPattern = pattern.slice(1, -1)
const normalizedPattern = rawPattern.replace(/\\\./g, '.').replace(/\\\//g, '/')
const matches = normalizedPattern.match(domainMatcher)
if (matches) {
matches.forEach((match) => {
patternSet.add(match.replace(/http(s)?:\/\//g, '').toLowerCase())
})
}
} else if (pattern.includes('://')) {
// Handle URLs with protocol (e.g., https://baidu.com)
const matches = pattern.match(domainMatcher)
if (matches) {
matches.forEach((match) => {
patternSet.add(match.replace(/http(s)?:\/\//g, '').toLowerCase())
})
}
} else {
patternSet.add(pattern.toLowerCase())
}
})
return Array.from(patternSet)
}
export async function filterResultWithBlacklist( export async function filterResultWithBlacklist(
response: WebSearchProviderResponse, response: WebSearchProviderResponse,
websearch: WebSearchState websearch: WebSearchState

View File

@ -2309,7 +2309,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@cherrystudio/ai-core@workspace:^1.0.0-alpha.16, @cherrystudio/ai-core@workspace:packages/aiCore": "@cherrystudio/ai-core@workspace:^1.0.0-alpha.17, @cherrystudio/ai-core@workspace:packages/aiCore":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" resolution: "@cherrystudio/ai-core@workspace:packages/aiCore"
dependencies: dependencies:
@ -13195,7 +13195,7 @@ __metadata:
"@aws-sdk/client-bedrock-runtime": "npm:^3.840.0" "@aws-sdk/client-bedrock-runtime": "npm:^3.840.0"
"@aws-sdk/client-s3": "npm:^3.840.0" "@aws-sdk/client-s3": "npm:^3.840.0"
"@biomejs/biome": "npm:2.2.4" "@biomejs/biome": "npm:2.2.4"
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.16" "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17"
"@cherrystudio/embedjs": "npm:^0.1.31" "@cherrystudio/embedjs": "npm:^0.1.31"
"@cherrystudio/embedjs-libsql": "npm:^0.1.31" "@cherrystudio/embedjs-libsql": "npm:^0.1.31"
"@cherrystudio/embedjs-loader-csv": "npm:^0.1.31" "@cherrystudio/embedjs-loader-csv": "npm:^0.1.31"