Provider Config & anthropic-web-fetch (#10808)

* fix: update AI SDK dependencies to latest versions

* feat: Update provider configurations and API handling

- Refactor provider configuration to support new API types and enhance API host formatting.
- Introduce new utility functions for handling API versions and formatting Azure OpenAI hosts.
- Update system models to include new capabilities and adjust provider types for CherryIN and VertexAI.
- Enhance provider settings UI to accommodate new API types and improve user experience.
- Implement migration logic for provider type updates and default API host settings.
- Update translations for API host configuration tips across multiple languages.
- Fix various type checks and utility functions to ensure compatibility with new provider types.

* fix: update unsupported API version providers and add longcat to compatible provider IDs

* fix: 移除不再使用的 Azure OpenAI API 版本参数,优化 API 主机格式化逻辑
feat: 在选择器组件中添加样式属性,增强可定制性
feat: 更新提供者设置,支持动态选择 API 主机字段

* refactor: 优化测试用例

* 修复: 更新工具调用处理器以支持新的工具调用类型

* feat: 添加TODO注释以改进基于AI SDK的供应商内置工具展示和类型安全处理

* feat: 添加对Google SDK的支持,更新流式参数构建逻辑以包含Google工具的上下文

* feat: 更新web搜索模型判断逻辑,使用SystemProviderIds常量替代硬编码字符串

* feat: 添加对@renderer/store的mock以支持测试环境

* feat: 添加API主机地址验证功能,更新相关逻辑以支持端点提取

* fix: i18n

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

* Apply suggestion from @EurFelux

Co-authored-by: Phantom <eurfelux@gmail.com>

* Apply suggestion from @EurFelux

Co-authored-by: Phantom <eurfelux@gmail.com>

* Apply suggestion from @EurFelux

Co-authored-by: Phantom <eurfelux@gmail.com>

* refactor: Simplify provider type migration logic and enhance API version validation

* fix: Correct variable name from configedApiHost to configuredApiHost for consistency

* fix: Update package.json to remove deprecated @ai-sdk/google version and streamline @ai-sdk/openai versioning

* fix: 更新 hasAPIVersion 函数中的正则表达式以更准确地匹配 API 版本路径

* fix(api): 简化 validateApiHost 函数逻辑以始终返回 true
fix(yarn): 更新 @ai-sdk/openai 版本至 2.0.53 并添加依赖项

* fix(api): 修正 validateApiHost 函数在使用哈希后缀时的验证逻辑

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Phantom <eurfelux@gmail.com>
This commit is contained in:
SuYao 2025-10-29 14:47:21 +08:00 committed by GitHub
parent 5790c12011
commit e0a2ed0481
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 1064 additions and 491 deletions

View File

@ -1,13 +0,0 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 69ab1599c76801dc1167551b6fa283dded123466..f0af43bba7ad1196fe05338817e65b4ebda40955 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
// src/get-model-path.ts
function getModelPath(modelId) {
- return modelId.includes("/") ? modelId : `models/${modelId}`;
+ return modelId?.includes("models/") ? modelId : `models/${modelId}`;
}
// src/google-generative-ai-options.ts

View File

@ -0,0 +1,26 @@
diff --git a/dist/index.js b/dist/index.js
index 4cc66d83af1cef39f6447dc62e680251e05ddf9f..eb9819cb674c1808845ceb29936196c4bb355172 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
// src/get-model-path.ts
function getModelPath(modelId) {
- return modelId.includes("/") ? modelId : `models/${modelId}`;
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
}
// src/google-generative-ai-options.ts
diff --git a/dist/index.mjs b/dist/index.mjs
index a032505ec54e132dc386dde001dc51f710f84c83..5efada51b9a8b56e3f01b35e734908ebe3c37043 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
// src/get-model-path.ts
function getModelPath(modelId) {
- return modelId.includes("/") ? modelId : `models/${modelId}`;
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
}
// src/google-generative-ai-options.ts

View File

@ -0,0 +1,76 @@
diff --git a/dist/index.js b/dist/index.js
index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f318026d9b2 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -274,6 +274,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)(
message: import_v42.z.object({
role: import_v42.z.literal("assistant").nullish(),
content: import_v42.z.string().nullish(),
+ reasoning_content: import_v42.z.string().nullish(),
tool_calls: import_v42.z.array(
import_v42.z.object({
id: import_v42.z.string().nullish(),
@@ -340,6 +341,7 @@ var openaiChatChunkSchema = (0, import_provider_utils3.lazyValidator)(
delta: import_v42.z.object({
role: import_v42.z.enum(["assistant"]).nullish(),
content: import_v42.z.string().nullish(),
+ reasoning_content: import_v42.z.string().nullish(),
tool_calls: import_v42.z.array(
import_v42.z.object({
index: import_v42.z.number(),
@@ -785,6 +787,14 @@ var OpenAIChatLanguageModel = class {
if (text != null && text.length > 0) {
content.push({ type: "text", text });
}
+ const reasoning =
+ choice.message.reasoning_content;
+ if (reasoning != null && reasoning.length > 0) {
+ content.push({
+ type: 'reasoning',
+ text: reasoning,
+ });
+ }
for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) {
content.push({
type: "tool-call",
@@ -866,6 +876,7 @@ var OpenAIChatLanguageModel = class {
};
let isFirstChunk = true;
let isActiveText = false;
+ let isActiveReasoning = false;
const providerMetadata = { openai: {} };
return {
stream: response.pipeThrough(
@@ -920,6 +931,22 @@ var OpenAIChatLanguageModel = class {
return;
}
const delta = choice.delta;
+ const reasoningContent = delta.reasoning_content;
+ if (reasoningContent) {
+ if (!isActiveReasoning) {
+ controller.enqueue({
+ type: 'reasoning-start',
+ id: 'reasoning-0',
+ });
+ isActiveReasoning = true;
+ }
+
+ controller.enqueue({
+ type: 'reasoning-delta',
+ id: 'reasoning-0',
+ delta: reasoningContent,
+ });
+ }
if (delta.content != null) {
if (!isActiveText) {
controller.enqueue({ type: "text-start", id: "0" });
@@ -1032,6 +1059,9 @@ var OpenAIChatLanguageModel = class {
}
},
flush(controller) {
+ if (isActiveReasoning) {
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
+ }
if (isActiveText) {
controller.enqueue({ type: "text-end", id: "0" });
}

View File

@ -103,8 +103,8 @@
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.35",
"@ai-sdk/google-vertex": "^3.0.40",
"@ai-sdk/amazon-bedrock": "^3.0.42",
"@ai-sdk/google-vertex": "^3.0.48",
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
"@ai-sdk/mistral": "^2.0.19",
"@ai-sdk/perplexity": "^2.0.13",
@ -227,7 +227,7 @@
"@viz-js/lang-dot": "^1.0.5",
"@viz-js/viz": "^3.14.0",
"@xyflow/react": "^12.4.4",
"ai": "^5.0.68",
"ai": "^5.0.76",
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
@ -390,7 +390,8 @@
"undici": "6.21.2",
"vite": "npm:rolldown-vite@7.1.5",
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
"@ai-sdk/google@npm:2.0.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch",
"@ai-sdk/google@npm:2.0.23": "patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch",
"@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-linux-arm": "0.34.3",

View File

@ -36,10 +36,10 @@
"ai": "^5.0.26"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.27",
"@ai-sdk/azure": "^2.0.49",
"@ai-sdk/anthropic": "^2.0.32",
"@ai-sdk/azure": "^2.0.53",
"@ai-sdk/deepseek": "^1.0.23",
"@ai-sdk/openai": "^2.0.48",
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
"@ai-sdk/openai-compatible": "^1.0.22",
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.12",

View File

@ -1,7 +1,6 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import { ApiClient } from '@types'
import { VoyageEmbeddings } from './VoyageEmbeddings'
@ -9,7 +8,7 @@ import { VoyageEmbeddings } from './VoyageEmbeddings'
export default class EmbeddingsFactory {
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
const batchSize = 10
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
const { model, provider, apiKey, baseURL } = embedApiClient
if (provider === 'voyageai') {
return new VoyageEmbeddings({
modelName: model,
@ -38,16 +37,7 @@ export default class EmbeddingsFactory {
}
})
}
if (apiVersion !== undefined) {
return new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIEndpoint: baseURL,
dimensions,
batchSize
})
}
// NOTE: Azure OpenAI 也走 OpenAIEmbeddings, baseURL是https://xxxx.openai.azure.com/openai/v1
return new OpenAiEmbeddings({
model,
apiKey,

View File

@ -6,7 +6,14 @@
import { loggerService } from '@logger'
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService'
import { BaseTool, MCPTool, MCPToolResponse, NormalToolResponse } from '@renderer/types'
import {
BaseTool,
MCPCallToolResponse,
MCPTool,
MCPToolResponse,
MCPToolResultContent,
NormalToolResponse
} from '@renderer/types'
import { Chunk, ChunkType } from '@renderer/types/chunk'
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
@ -254,6 +261,7 @@ export class ToolCallChunkHandler {
type: 'tool-result'
} & TypedToolResult<ToolSet>
): void {
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
const { toolCallId, output, input } = chunk
if (!toolCallId) {
@ -299,12 +307,7 @@ export class ToolCallChunkHandler {
responses: [toolResponse]
})
const images: string[] = []
for (const content of toolResponse.response?.content || []) {
if (content.type === 'image' && content.data) {
images.push(`data:${content.mimeType};base64,${content.data}`)
}
}
const images = extractImagesFromToolOutput(toolResponse.response)
if (images.length) {
this.onChunk({
@ -351,3 +354,41 @@ export class ToolCallChunkHandler {
}
export const addActiveToolCall = ToolCallChunkHandler.addActiveToolCall.bind(ToolCallChunkHandler)
function extractImagesFromToolOutput(output: unknown): string[] {
if (!output) {
return []
}
const contents: unknown[] = []
if (isMcpCallToolResponse(output)) {
contents.push(...output.content)
} else if (Array.isArray(output)) {
contents.push(...output)
} else if (hasContentArray(output)) {
contents.push(...output.content)
}
return contents
.filter(isMcpImageContent)
.map((content) => `data:${content.mimeType ?? 'image/png'};base64,${content.data}`)
}
function isMcpCallToolResponse(value: unknown): value is MCPCallToolResponse {
return typeof value === 'object' && value !== null && Array.isArray((value as MCPCallToolResponse).content)
}
function hasContentArray(value: unknown): value is { content: unknown[] } {
return typeof value === 'object' && value !== null && Array.isArray((value as { content?: unknown }).content)
}
function isMcpImageContent(content: unknown): content is MCPToolResultContent & { data: string } {
if (typeof content !== 'object' || content === null) {
return false
}
const resultContent = content as MCPToolResultContent
return resultContent.type === 'image' && typeof resultContent.data === 'string'
}

View File

@ -14,6 +14,7 @@ import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
@ -77,7 +78,7 @@ export default class ModernAiProvider {
return this.actualProvider
}
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) {
public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
// 检查model是否存在
if (!this.model) {
throw new Error('Model is required for completions. Please use constructor with model parameter.')
@ -85,7 +86,10 @@ export default class ModernAiProvider {
// 每次请求时重新生成配置以确保API key轮换生效
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
logger.debug('Generated provider config for completions', this.config)
if (SUPPORTED_IMAGE_ENDPOINT_LIST.includes(this.config.options.endpoint)) {
providerConfig.isImageGenerationEndpoint = true
}
// 准备特殊配置
await prepareSpecialProviderConfig(this.actualProvider, this.config)
@ -96,13 +100,13 @@ export default class ModernAiProvider {
// 提前构建中间件
const middlewares = buildAiSdkMiddlewares({
...config,
...providerConfig,
provider: this.actualProvider,
assistant: config.assistant
assistant: providerConfig.assistant
})
logger.debug('Built middlewares in completions', {
middlewareCount: middlewares.length,
isImageGeneration: config.isImageGenerationEndpoint
isImageGeneration: providerConfig.isImageGenerationEndpoint
})
if (!this.localProvider) {
throw new Error('Local provider not created')
@ -110,7 +114,7 @@ export default class ModernAiProvider {
// 根据endpoint类型创建对应的模型
let model: AiSdkModel | undefined
if (config.isImageGenerationEndpoint) {
if (providerConfig.isImageGenerationEndpoint) {
model = this.localProvider.imageModel(modelId)
} else {
model = this.localProvider.languageModel(modelId)
@ -126,15 +130,15 @@ export default class ModernAiProvider {
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
}
if (config.topicId && getEnableDeveloperMode()) {
if (providerConfig.topicId && getEnableDeveloperMode()) {
// TypeScript类型窄化确保topicId是string类型
const traceConfig = {
...config,
topicId: config.topicId
...providerConfig,
topicId: providerConfig.topicId
}
return await this._completionsForTrace(model, params, traceConfig)
} else {
return await this._completionsOrImageGeneration(model, params, config)
return await this._completionsOrImageGeneration(model, params, providerConfig)
}
}

View File

@ -1,5 +1,4 @@
import { Provider } from '@renderer/types'
import { isOpenAIProvider } from '@renderer/utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
@ -202,36 +201,4 @@ describe('ApiClientFactory', () => {
expect(client).toBeDefined()
})
})
describe('isOpenAIProvider', () => {
it('should return true for openai type', () => {
const provider = createTestProvider('openai', 'openai')
expect(isOpenAIProvider(provider)).toBe(true)
})
it('should return true for azure-openai type', () => {
const provider = createTestProvider('azure-openai', 'azure-openai')
expect(isOpenAIProvider(provider)).toBe(true)
})
it('should return true for unknown type (fallback to OpenAI)', () => {
const provider = createTestProvider('unknown', 'unknown')
expect(isOpenAIProvider(provider)).toBe(true)
})
it('should return false for vertexai type', () => {
const provider = createTestProvider('vertex', 'vertexai')
expect(isOpenAIProvider(provider)).toBe(false)
})
it('should return false for anthropic type', () => {
const provider = createTestProvider('anthropic', 'anthropic')
expect(isOpenAIProvider(provider)).toBe(false)
})
it('should return false for gemini type', () => {
const provider = createTestProvider('gemini', 'gemini')
expect(isOpenAIProvider(provider)).toBe(false)
})
})
})

View File

@ -1,5 +1,5 @@
import { AiPlugin } from '@cherrystudio/ai-core'
import { createPromptToolUsePlugin, googleToolsPlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
import { createPromptToolUsePlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
import { loggerService } from '@logger'
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
import { Assistant } from '@renderer/types'
@ -68,9 +68,9 @@ export function buildPlugins(
)
}
if (middlewareConfig.enableUrlContext) {
plugins.push(googleToolsPlugin({ urlContext: true }))
}
// if (middlewareConfig.enableUrlContext && middlewareConfig.) {
// plugins.push(googleToolsPlugin({ urlContext: true }))
// }
logger.debug(
'Final plugin list:',

View File

@ -114,7 +114,7 @@ export async function handleGeminiFileUpload(file: FileMetadata, model: Model):
}
/**
* OpenAI大文件上传
* OpenAI兼容大文件上传
*/
export async function handleOpenAILargeFileUpload(
file: FileMetadata,

View File

@ -3,6 +3,8 @@
* AI SDK的流式和非流式参数
*/
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
import { vertex } from '@ai-sdk/google-vertex/edge'
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
@ -97,10 +99,6 @@ export async function buildStreamTextParams(
let tools = setupToolsConfig(mcpTools)
// if (webSearchProviderId) {
// tools['builtin_web_search'] = webSearchTool(webSearchProviderId)
// }
// 构建真正的 providerOptions
const webSearchConfig: CherryWebSearchConfig = {
maxResults: store.getState().websearch.maxResults,
@ -143,12 +141,34 @@ export async function buildStreamTextParams(
}
}
// google-vertex
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
if (enableUrlContext) {
if (!tools) {
tools = {}
}
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
switch (aiSdkProviderId) {
case 'google-vertex':
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
break
case 'google':
tools.url_context = google.tools.urlContext({}) as ProviderDefinedTool
break
case 'anthropic':
case 'google-vertex-anthropic':
tools.web_fetch = (
aiSdkProviderId === 'anthropic'
? anthropic.tools.webFetch_20250910({
maxUses: webSearchConfig.maxResults,
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
})
: vertexAnthropic.tools.webFetch_20250910({
maxUses: webSearchConfig.maxResults,
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
})
) as ProviderDefinedTool
break
}
}
// 构建基础参数

View File

@ -32,7 +32,8 @@ const AIHUBMIX_RULES: RuleSet = {
match: (model) =>
(startsWith('gemini')(model) || startsWith('imagen')(model)) &&
!model.id.endsWith('-nothink') &&
!model.id.endsWith('-search'),
!model.id.endsWith('-search') &&
!model.id.includes('embedding'),
provider: (provider: Provider) => {
return extraProviderConfig({
...provider,

View File

@ -6,26 +6,28 @@ import {
type ProviderSettingsMap
} from '@cherrystudio/ai-core/provider'
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import {
isAnthropicProvider,
isAzureOpenAIProvider,
isGeminiProvider,
isNewApiProvider
} from '@renderer/config/providers'
import {
getAwsBedrockAccessKeyId,
getAwsBedrockRegion,
getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock'
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI'
import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { loggerService } from '@renderer/services/LoggerService'
import store from '@renderer/store'
import { isSystemProvider, type Model, type Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { cloneDeep, trim } from 'lodash'
import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
import { formatApiHost, formatAzureOpenAIApiHost, formatVertexApiHost, routeToEndpoint } from '@renderer/utils/api'
import { cloneDeep } from 'lodash'
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
import { COPILOT_DEFAULT_HEADERS } from './constants'
import { getAiSdkProviderId } from './factory'
const logger = loggerService.withContext('ProviderConfigProcessor')
/**
* API key
* legacy架构的多key轮询逻辑
@ -56,13 +58,6 @@ function getRotatedApiKey(provider: Provider): string {
* provider的转换逻辑
*/
function handleSpecialProviders(model: Model, provider: Provider): Provider {
// if (provider.type === 'vertexai' && !isVertexProvider(provider)) {
// if (!isVertexAIConfigured()) {
// throw new Error('VertexAI is not configured. Please configure project, location and service account credentials.')
// }
// return createVertexProvider(provider)
// }
if (isNewApiProvider(provider)) {
return newApiResolverCreator(model, provider)
}
@ -79,43 +74,30 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
}
/**
* provider的API Host
* AISdk的BaseURL格式
* @param provider
* @returns
*/
function formatAnthropicApiHost(host: string): string {
const trimmedHost = host?.trim()
if (!trimmedHost) {
return ''
}
if (trimmedHost.endsWith('/')) {
return trimmedHost
}
if (trimmedHost.endsWith('/v1')) {
return `${trimmedHost}/`
}
return formatApiHost(trimmedHost)
}
function formatProviderApiHost(provider: Provider): Provider {
const formatted = { ...provider }
if (formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost)
formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
}
if (formatted.type === 'anthropic') {
if (isAnthropicProvider(provider)) {
const baseHost = formatted.anthropicApiHost || formatted.apiHost
formatted.apiHost = formatAnthropicApiHost(baseHost)
formatted.apiHost = formatApiHost(baseHost)
if (!formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatted.apiHost
}
} else if (formatted.id === 'copilot') {
const trimmed = trim(formatted.apiHost)
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
} else if (formatted.type === 'gemini') {
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
} else if (formatted.id === SystemProviderIds.copilot || formatted.id === SystemProviderIds.github) {
formatted.apiHost = formatApiHost(formatted.apiHost, false)
} else if (isGeminiProvider(formatted)) {
formatted.apiHost = formatApiHost(formatted.apiHost, true, 'v1beta')
} else if (isAzureOpenAIProvider(formatted)) {
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
} else if (isVertexProvider(formatted)) {
formatted.apiHost = formatVertexApiHost(formatted)
} else {
formatted.apiHost = formatApiHost(formatted.apiHost)
}
@ -149,15 +131,15 @@ export function providerToAiSdkConfig(
options: ProviderSettingsMap[keyof ProviderSettingsMap]
} {
const aiSdkProviderId = getAiSdkProviderId(actualProvider)
logger.debug('providerToAiSdkConfig', { aiSdkProviderId })
// 构建基础配置
const { baseURL, endpoint } = routeToEndpoint(actualProvider.apiHost)
const baseConfig = {
baseURL: trim(actualProvider.apiHost),
baseURL: baseURL,
apiKey: getRotatedApiKey(actualProvider)
}
const isCopilotProvider = actualProvider.id === 'copilot'
const isCopilotProvider = actualProvider.id === SystemProviderIds.copilot
if (isCopilotProvider) {
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
@ -178,6 +160,7 @@ export function providerToAiSdkConfig(
// 处理OpenAI模式
const extraOptions: any = {}
extraOptions.endpoint = endpoint
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
extraOptions.mode = 'responses'
} else if (aiSdkProviderId === 'openai') {
@ -199,13 +182,11 @@ export function providerToAiSdkConfig(
}
// azure
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
extraOptions.apiVersion = actualProvider.apiVersion
baseConfig.baseURL += '/openai'
// extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1不使用azure endpoint
if (actualProvider.apiVersion === 'preview') {
extraOptions.mode = 'responses'
} else {
extraOptions.mode = 'chat'
extraOptions.useDeploymentBasedUrls = true
}
}
@ -227,22 +208,7 @@ export function providerToAiSdkConfig(
...googleCredentials,
privateKey: formatPrivateKey(googleCredentials.privateKey)
}
// extraOptions.headers = window.api.vertexAI.getAuthHeaders({
// projectId: project,
// serviceAccount: {
// privateKey: googleCredentials.privateKey,
// clientEmail: googleCredentials.clientEmail
// }
// })
if (baseConfig.baseURL.endsWith('/v1/')) {
baseConfig.baseURL = baseConfig.baseURL.slice(0, -4)
} else if (baseConfig.baseURL.endsWith('/v1')) {
baseConfig.baseURL = baseConfig.baseURL.slice(0, -3)
}
if (baseConfig.baseURL && !baseConfig.baseURL.includes('publishers/google')) {
baseConfig.baseURL = `${baseConfig.baseURL}/v1/projects/${project}/locations/${location}/publishers/google`
}
baseConfig.baseURL += aiSdkProviderId === 'google-vertex' ? '/publishers/google' : '/publishers/anthropic/models'
}
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {

View File

@ -5,6 +5,16 @@ import { describe, expect, it, vi } from 'vitest'
import { DraggableList } from '../'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// mock @hello-pangea/dnd 组件
vi.mock('@hello-pangea/dnd', () => {
return {

View File

@ -3,6 +3,16 @@ import { describe, expect, it, vi } from 'vitest'
import { DraggableVirtualList } from '../'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// Mock 依赖项
vi.mock('@hello-pangea/dnd', () => ({
__esModule: true,

View File

@ -1,4 +1,4 @@
import { makeSvgSizeAdaptive } from '@renderer/utils'
import { makeSvgSizeAdaptive } from '@renderer/utils/image'
import DOMPurify from 'dompurify'
/**

View File

@ -16,6 +16,7 @@ interface BaseSelectorProps<V = string | number> {
options: SelectorOption<V>[]
placeholder?: string
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
style?: React.CSSProperties
/** 字体大小 */
size?: number
/** 是否禁用 */
@ -43,6 +44,7 @@ const Selector = <V extends string | number>({
placement = 'bottomRight',
size = 13,
placeholder,
style,
disabled = false,
multiple = false
}: SelectorProps<V>) => {
@ -135,7 +137,7 @@ const Selector = <V extends string | number>({
placement={placement}
open={open && !disabled}
onOpenChange={handleOpenChange}>
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
<Label style={style} $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
{label}
<LabelIcon size={size + 3} />
</Label>

View File

@ -23,6 +23,16 @@ const mocks = vi.hoisted(() => ({
}
}))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// Mock antd components to prevent flaky snapshot tests
vi.mock('antd', () => {
const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({

View File

@ -18,6 +18,15 @@ describe('Qwen Model Detection', () => {
vi.mock('@renderer/services/AssistantService', () => ({
getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' })
}))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
})
test('isQwenReasoningModel', () => {
expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true)

View File

@ -2,6 +2,16 @@ import { describe, expect, it, vi } from 'vitest'
import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isLingReasoningModel } from '../models/reasoning'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// FIXME: Idk why it's imported. Maybe circular dependency somewhere
vi.mock('@renderer/services/AssistantService.ts', () => ({
getDefaultAssistant: () => {

View File

@ -1741,6 +1741,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
id: 'DeepSeek-R1',
provider: 'cephalon',
name: 'DeepSeek-R1满血版',
capabilities: [{ type: 'reasoning' }],
group: 'DeepSeek'
}
],

View File

@ -1,7 +1,8 @@
import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types'
import { Model, SystemProviderIds } from '@renderer/types'
import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils'
import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers'
import { isEmbeddingModel, isRerankModel } from './embedding'
import { isAnthropicModel } from './utils'
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
@ -65,12 +66,16 @@ export function isWebSearchModel(model: Model): boolean {
const modelId = getLowerBaseModelName(model.id, '/')
// 不管哪个供应商都判断了
if (isAnthropicModel(model)) {
// bedrock和vertex不支持
if (
isAnthropicModel(model) &&
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
) {
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
}
if (provider.type === 'openai-response') {
// TODO: 当其他供应商采用Response端点时这个地方逻辑需要改进
if (isOpenAIProvider(provider)) {
if (isOpenAIWebSearchModel(model)) {
return true
}
@ -78,11 +83,11 @@ export function isWebSearchModel(model: Model): boolean {
return false
}
if (provider.id === 'perplexity') {
if (provider.id === SystemProviderIds.perplexity) {
return PERPLEXITY_SEARCH_MODELS.includes(modelId)
}
if (provider.id === 'aihubmix') {
if (provider.id === SystemProviderIds.aihubmix) {
// modelId 不以-search结尾
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
return true
@ -95,13 +100,13 @@ export function isWebSearchModel(model: Model): boolean {
return false
}
if (provider?.type === 'openai') {
if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
return true
}
}
if (provider.id === 'gemini' || provider?.type === 'gemini' || provider.type === 'vertexai') {
if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) {
return GEMINI_SEARCH_REGEX.test(modelId)
}

View File

@ -58,6 +58,7 @@ import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import {
AtLeast,
AzureOpenAIProvider,
isSystemProvider,
OpenAIServiceTiers,
Provider,
@ -355,7 +356,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
name: 'VertexAI',
type: 'vertexai',
apiKey: '',
apiHost: 'https://aiplatform.googleapis.com',
apiHost: '',
models: SYSTEM_MODELS.vertexai,
isSystem: true,
enabled: false,
@ -1295,7 +1296,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
},
vertexai: {
api: {
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview'
url: ''
},
websites: {
official: 'https://cloud.google.com/vertex-ai',
@ -1375,7 +1376,8 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
'baichuan',
'minimax',
'xirang',
'poe'
'poe',
'cephalon'
] as const satisfies SystemProviderId[]
/**
@ -1440,10 +1442,15 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
)
}
const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[]
const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
'gemini',
'vertexai',
'anthropic',
'new-api'
] as const satisfies ProviderType[]
export const isSupportUrlContextProvider = (provider: Provider) => {
return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
return SUPPORT_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
}
const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[]
@ -1456,3 +1463,37 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
}
/**
* OpenAI
* @param {Provider} provider
* @returns {boolean} OpenAI
*/
export function isOpenAICompatibleProvider(provider: Provider): boolean {
return ['openai', 'new-api', 'mistral'].includes(provider.type)
}
export function isAzureOpenAIProvider(provider: Provider): provider is AzureOpenAIProvider {
return provider.type === 'azure-openai'
}
export function isOpenAIProvider(provider: Provider): boolean {
return provider.type === 'openai-response'
}
export function isAnthropicProvider(provider: Provider): boolean {
return provider.type === 'anthropic'
}
export function isGeminiProvider(provider: Provider): boolean {
return provider.type === 'gemini'
}
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
export const isSupportAPIVersionProvider = (provider: Provider) => {
if (isSystemProvider(provider)) {
return !NOT_SUPPORT_API_VERSION_PROVIDERS.some((pid) => pid === provider.id)
}
return provider.apiOptions?.isNotSupportAPIVersion !== false
}

View File

@ -42,7 +42,7 @@ export function getVertexAIServiceAccount() {
* Provider VertexProvider
*/
export function isVertexProvider(provider: Provider): provider is VertexProvider {
return provider.type === 'vertexai' && 'googleCredentials' in provider
return provider.type === 'vertexai'
}
/**

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Anthropic API Host",
"anthropic_api_host_preview": "Anthropic preview: {{url}}",
"anthropic_api_host_tip": "Only configure this when your provider exposes an Anthropic-compatible endpoint. Ending with / ignores v1, ending with # forces use of input address.",
"anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "Preview: {{url}}",
"reset": "Reset",
"tip": "Ending with / ignores v1, ending with # forces use of input address"
"tip": "ending with # forces use of input address"
}
},
"api_host": "API Host",
"api_host_no_valid": "API address is invalid",
"api_host_preview": "Preview: {{url}}",
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Anthropic API 地址",
"anthropic_api_host_preview": "Anthropic 预览:{{url}}",
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1以 # 结尾则强制使用原始地址。",
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "预览: {{url}}",
"reset": "重置",
"tip": "/ 结尾忽略 v1 版本,# 结尾强制使用输入地址"
"tip": "# 结尾强制使用输入地址"
}
},
"api_host": "API 地址",
"api_host_no_valid": "API 地址不合法",
"api_host_preview": "预览:{{url}}",
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Anthropic API 主機地址",
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1以 # 結尾則強制使用原始地址。",
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "預覽:{{url}}",
"reset": "重設",
"tip": "/ 結尾忽略 v1 版本,# 結尾強制使用輸入位址"
"tip": "# 結尾強制使用輸入位址"
}
},
"api_host": "API 主機地址",
"api_host_no_valid": "API 位址不合法",
"api_host_preview": "預覽:{{url}}",
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Anthropic API-Adresse",
"anthropic_api_host_preview": "Anthropic-Vorschau: {{url}}",
"anthropic_api_host_tip": "Nur bei Anbietern, die ein Anthropic-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
"anthropic_api_host_tooltip": "Nur bei Anbietern, die ein Claude-kompatibles Basis-Endpunkt anbieten.",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "Vorschau: {{url}}",
"reset": "Zurücksetzen",
"tip": "/ am Ende ignorieren v1-Version, # am Ende erzwingt die Verwendung der Eingabe-Adresse"
"tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
}
},
"api_host": "API-Adresse",
"api_host_no_valid": "API-Adresse ist ungültig",
"api_host_preview": "Vorschau: {{url}}",
"api_host_tooltip": "Nur bei Anbietern, die ein OpenAI-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Διεύθυνση API Anthropic",
"anthropic_api_host_preview": "Προεπισκόπηση Anthropic: {{url}}",
"anthropic_api_host_tip": "Συμπληρώστε μόνο εάν ο πάροχος προσφέρει συμβατή με Anthropic διεύθυνση. Η λήξη με / αγνοεί το v1 που προστίθεται αυτόματα, η λήξη με # επιβάλλει τη χρήση της αρχικής διεύθυνσης.",
"anthropic_api_host_tooltip": "Συμπληρώστε μόνο όταν ο πάροχος παρέχει βασική διεύθυνση συμβατή με Claude.",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "Προεπισκόπηση: {{url}}",
"reset": "Επαναφορά",
"tip": "/τέλος αγνόηση v1 έκδοσης, #τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
"tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
}
},
"api_host": "Διεύθυνση API",
"api_host_no_valid": "Η διεύθυνση API δεν είναι έγκυρη",
"api_host_preview": "Προεπισκόπηση: {{url}}",
"api_host_tooltip": "Αντικατάσταση μόνο όταν ο πάροχος απαιτεί προσαρμοσμένη διεύθυνση συμβατή με OpenAI.",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Dirección API de Anthropic",
"anthropic_api_host_preview": "Vista previa de Anthropic: {{url}}",
"anthropic_api_host_tip": "Rellenar solo si el proveedor ofrece una dirección compatible con Anthropic. Terminar con / ignora el v1 añadido automáticamente, terminar con # fuerza el uso de la dirección original.",
"anthropic_api_host_tooltip": "Rellenar solo cuando el proveedor proporcione una dirección base compatible con Claude.",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "Vista previa: {{url}}",
"reset": "Restablecer",
"tip": "Ignorar v1 al final con /, forzar uso de dirección de entrada con # al final"
"tip": "forzar uso de dirección de entrada con # al final"
}
},
"api_host": "Dirección API",
"api_host_no_valid": "La dirección de la API no es válida",
"api_host_preview": "Vista previa: {{url}}",
"api_host_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Adresse API Anthropic",
"anthropic_api_host_preview": "Aperçu Anthropic : {{url}}",
"anthropic_api_host_tip": "Remplir seulement si le fournisseur propose une adresse compatible Anthropic. Se terminant par / ignore le v1 ajouté automatiquement, se terminant par # force l'utilisation de l'adresse originale.",
"anthropic_api_host_tooltip": "Remplir seulement lorsque le fournisseur propose une adresse de base compatible Claude.",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "Aperçu : {{url}}",
"reset": "Réinitialiser",
"tip": "Ignorer la version v1 si terminé par /, forcer l'utilisation de l'adresse d'entrée si terminé par #"
"tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #"
}
},
"api_host": "Adresse API",
"api_host_no_valid": "Adresse API invalide",
"api_host_preview": "Aperçu : {{url}}",
"api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Anthropic APIアドレス",
"anthropic_api_host_preview": "Anthropic プレビュー:{{url}}",
"anthropic_api_host_tip": "サービスプロバイダーがAnthropic互換のアドレスを提供する場合のみ入力してください。/で終わる場合は自動追加されるv1を無視し、#で終わる場合は元のアドレスを強制的に使用します。",
"anthropic_api_host_tooltip": "サービスプロバイダーがClaude互換のベースアドレスを提供する場合のみ入力してください。",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "プレビュー: {{url}}",
"reset": "リセット",
"tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します"
"tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
}
},
"api_host": "APIホスト",
"api_host_no_valid": "APIアドレスが無効です",
"api_host_preview": "プレビュー:{{url}}",
"api_host_tooltip": "サービスプロバイダーがカスタムOpenAI互換アドレスを必要とする場合のみ上書きしてください。",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Endereço da API Anthropic",
"anthropic_api_host_preview": "Pré-visualização Anthropic: {{url}}",
"anthropic_api_host_tip": "Preencher apenas se o fornecedor oferecer um endereço compatível com Anthropic. Terminar com / ignora o v1 adicionado automaticamente, terminar com # força o uso do endereço original.",
"anthropic_api_host_tooltip": "Preencher apenas quando o fornecedor fornece um endereço base compatível com Claude.",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "Pré-visualização: {{url}}",
"reset": "Redefinir",
"tip": "Ignorar v1 na versão finalizada com /, usar endereço de entrada forçado se terminar com #"
"tip": "e forçar o uso do endereço original quando terminar com '#'"
}
},
"api_host": "Endereço API",
"api_host_no_valid": "O endereço da API é inválido",
"api_host_preview": "Pré-visualização: {{url}}",
"api_host_tooltip": "Substituir apenas quando o fornecedor necessita de um endereço compatível com OpenAI personalizado.",
"api_key": {

View File

@ -4148,7 +4148,6 @@
},
"anthropic_api_host": "Адрес API Anthropic",
"anthropic_api_host_preview": "Предпросмотр Anthropic: {{url}}",
"anthropic_api_host_tip": "Заполняйте только если провайдер предоставляет совместимый с Anthropic адрес. Окончание на / игнорирует автоматически добавляемое v1, окончание на # принудительно использует оригинальный адрес.",
"anthropic_api_host_tooltip": "Заполняйте только когда провайдер предоставляет базовый адрес, совместимый с Claude.",
"api": {
"key": {
@ -4193,10 +4192,11 @@
"url": {
"preview": "Предпросмотр: {{url}}",
"reset": "Сброс",
"tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес"
"tip": "заканчивая на # принудительно использует введенный адрес"
}
},
"api_host": "Хост API",
"api_host_no_valid": "Недопустимый адрес API",
"api_host_preview": "Предпросмотр: {{url}}",
"api_host_tooltip": "Переопределяйте только когда провайдер требует пользовательский адрес, совместимый с OpenAI.",
"api_key": {

View File

@ -3,6 +3,7 @@ import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelListItem } from '@renderer/components/QuickPanel'
import {
isAnthropicModel,
isGeminiModel,
isGenerateImageModel,
isMandatoryWebSearchModel,
@ -385,7 +386,7 @@ const InputbarTools = ({
label: t('chat.input.url_context'),
component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />,
condition:
isGeminiModel(model) &&
(isGeminiModel(model) || isAnthropicModel(model)) &&
(isSupportUrlContextProvider(getProviderByModel(model)) || model.endpoint_type === 'gemini')
},
{

View File

@ -2,11 +2,22 @@ import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
import { LoadingIcon } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout'
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
import Selector from '@renderer/components/Selector'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { PROVIDER_URLS } from '@renderer/config/providers'
import {
isAnthropicProvider,
isAzureOpenAIProvider,
isGeminiProvider,
isNewApiProvider,
isOpenAICompatibleProvider,
isOpenAIProvider,
isSupportAPIVersionProvider,
PROVIDER_URLS
} from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
import { useTimer } from '@renderer/hooks/useTimer'
import { isVertexProvider } from '@renderer/hooks/useVertexAI'
import i18n from '@renderer/i18n'
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
@ -14,14 +25,15 @@ import { checkApi } from '@renderer/services/ApiService'
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store'
import { updateWebSearchProvider } from '@renderer/store/websearch'
import { isSystemProvider, isSystemProviderId, SystemProviderIds } from '@renderer/types'
import { isSystemProvider, isSystemProviderId, SystemProviderId, SystemProviderIds } from '@renderer/types'
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
import {
formatApiHost,
formatApiKeys,
formatAzureOpenAIApiHost,
formatVertexApiHost,
getFancyProviderName,
isAnthropicProvider,
isOpenAIProvider
validateApiHost
} from '@renderer/utils'
import { formatErrorMessage } from '@renderer/utils/error'
import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd'
@ -63,7 +75,9 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [
SystemProviderIds.dashscope,
SystemProviderIds.modelscope,
SystemProviderIds.aihubmix,
SystemProviderIds.grok
SystemProviderIds.grok,
SystemProviderIds.cherryin,
SystemProviderIds.longcat
] as const
type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number]
@ -72,6 +86,8 @@ const isAnthropicCompatibleProviderId = (id: string): id is AnthropicCompatibleP
return ANTHROPIC_COMPATIBLE_PROVIDER_ID_SET.has(id)
}
type HostField = 'apiHost' | 'anthropicApiHost'
const ProviderSetting: FC<Props> = ({ providerId }) => {
const { provider, updateProvider, models } = useProvider(providerId)
const allProviders = useAllProviders()
@ -79,19 +95,23 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const [apiHost, setApiHost] = useState(provider.apiHost)
const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost)
const [apiVersion, setApiVersion] = useState(provider.apiVersion)
const [activeHostField, setActiveHostField] = useState<HostField>('apiHost')
const { t } = useTranslation()
const { theme } = useTheme()
const { setTimeoutTimer } = useTimer()
const dispatch = useAppDispatch()
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
const isAzureOpenAI = isAzureOpenAIProvider(provider)
const isDmxapi = provider.id === 'dmxapi'
const hideApiInput = ['vertexai', 'aws-bedrock'].includes(provider.id)
const noAPIInputProviders = ['aws-bedrock'] as const satisfies SystemProviderId[]
const hideApiInput = noAPIInputProviders.some((id) => id === provider.id)
const noAPIKeyInputProviders = ['copilot', 'vertexai'] as const satisfies SystemProviderId[]
const hideApiKeyInput = noAPIKeyInputProviders.some((id) => id === provider.id)
const providerConfig = PROVIDER_URLS[provider.id]
const officialWebsite = providerConfig?.websites?.official
const apiKeyWebsite = providerConfig?.websites?.apiKey
const configedApiHost = providerConfig?.api?.url
const configuredApiHost = providerConfig?.api?.url
const fancyProviderName = getFancyProviderName(provider)
@ -151,7 +171,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
)
const onUpdateApiHost = () => {
if (apiHost.trim()) {
if (!validateApiHost(apiHost)) {
setApiHost(provider.apiHost)
window.toast.error(t('settings.provider.api_host_no_valid'))
return
}
if (isVertexProvider(provider) || apiHost.trim()) {
updateProvider({ apiHost })
} else {
setApiHost(provider.apiHost)
@ -238,27 +263,46 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
}
}
const onReset = () => {
setApiHost(configedApiHost)
updateProvider({ apiHost: configedApiHost })
}
const onReset = useCallback(() => {
setApiHost(configuredApiHost)
updateProvider({ apiHost: configuredApiHost })
}, [configuredApiHost, updateProvider])
const isApiHostResettable = useMemo(() => {
return !isEmpty(configuredApiHost) && apiHost !== configuredApiHost
}, [configuredApiHost, apiHost])
const hostPreview = () => {
if (apiHost.endsWith('#')) {
return apiHost.replace('#', '')
}
if (provider.type === 'openai') {
return formatApiHost(apiHost) + 'chat/completions'
if (isOpenAICompatibleProvider(provider)) {
return formatApiHost(apiHost, isSupportAPIVersionProvider(provider)) + '/chat/completions'
}
if (provider.type === 'azure-openai') {
return formatApiHost(apiHost) + 'openai/v1'
if (isAzureOpenAIProvider(provider)) {
const apiVersion = provider.apiVersion
const path = !['preview', 'v1'].includes(apiVersion)
? `/v1/chat/completion?apiVersion=v1`
: `/v1/responses?apiVersion=v1`
return formatAzureOpenAIApiHost(apiHost) + path
}
if (provider.type === 'anthropic') {
return formatApiHost(apiHost) + 'messages'
if (isAnthropicProvider(provider)) {
return formatApiHost(apiHost) + '/messages'
}
return formatApiHost(apiHost) + 'responses'
if (isGeminiProvider(provider)) {
return formatApiHost(apiHost, true, 'v1beta') + '/models'
}
if (isOpenAIProvider(provider)) {
return formatApiHost(apiHost) + '/responses'
}
if (isVertexProvider(provider)) {
return formatVertexApiHost(provider) + '/publishers/google'
}
return formatApiHost(apiHost)
}
// API key 连通性检查状态指示器,目前仅在失败时显示
@ -286,31 +330,44 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
}, [provider.anthropicApiHost])
const canConfigureAnthropicHost = useMemo(() => {
if (isNewApiProvider(provider)) {
return true
}
return (
provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id)
)
}, [provider])
const anthropicHostPreview = useMemo(() => {
const rawHost = (anthropicApiHost ?? provider.anthropicApiHost)?.trim()
if (!rawHost) {
return ''
}
if (/\/messages\/?$/.test(rawHost)) {
return rawHost.replace(/\/$/, '')
}
let normalizedHost = rawHost
if (/\/v\d+(?:\/)?$/i.test(normalizedHost)) {
normalizedHost = normalizedHost.replace(/\/$/, '')
} else {
normalizedHost = formatApiHost(normalizedHost).replace(/\/$/, '')
}
const rawHost = anthropicApiHost ?? provider.anthropicApiHost
const normalizedHost = formatApiHost(rawHost)
return `${normalizedHost}/messages`
}, [anthropicApiHost, provider.anthropicApiHost])
const hostSelectorOptions = useMemo(() => {
const options: { value: HostField; label: string }[] = [
{ value: 'apiHost', label: t('settings.provider.api_host') }
]
if (canConfigureAnthropicHost) {
options.push({ value: 'anthropicApiHost', label: t('settings.provider.anthropic_api_host') })
}
return options
}, [canConfigureAnthropicHost, t])
useEffect(() => {
if (!canConfigureAnthropicHost && activeHostField === 'anthropicApiHost') {
setActiveHostField('apiHost')
}
}, [canConfigureAnthropicHost, activeHostField])
const hostSelectorTooltip =
activeHostField === 'anthropicApiHost'
? t('settings.provider.anthropic_api_host_tooltip')
: t('settings.provider.api_host_tooltip')
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
return (
@ -367,105 +424,122 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
)}
{!hideApiInput && !isAnthropicOAuth() && (
<>
<SettingSubtitle
style={{
marginTop: 5,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
{t('settings.provider.api_key.label')}
{provider.id !== 'copilot' && (
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
<Button type="text" onClick={openApiKeyList} icon={<Settings2 size={16} />} />
</Tooltip>
)}
</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password
value={localApiKey}
placeholder={t('settings.provider.api_key.label')}
onChange={(e) => setLocalApiKey(e.target.value)}
spellCheck={false}
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)}
disabled={provider.id === 'copilot'}
suffix={renderStatusIndicator()}
/>
<Button
type={isApiKeyConnectable ? 'primary' : 'default'}
ghost={isApiKeyConnectable}
onClick={onCheckApi}
disabled={!apiHost || apiKeyConnectivity.checking}>
{apiKeyConnectivity.checking ? (
<LoadingIcon />
) : apiKeyConnectivity.status === 'success' ? (
<Check size={16} className="lucide-custom" />
) : (
t('settings.provider.check')
)}
</Button>
</Space.Compact>
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<HStack>
{apiKeyWebsite && !isDmxapi && (
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
)}
</HStack>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>
{!isDmxapi && !isAnthropicOAuth() && (
{!hideApiKeyInput && (
<>
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Tooltip title={t('settings.provider.api_host_tooltip')} mouseEnterDelay={0.3}>
<SubtitleLabel>{t('settings.provider.api_host')}</SubtitleLabel>
</Tooltip>
<Button
type="text"
onClick={() => CustomHeaderPopup.show({ provider })}
icon={<Settings2 size={16} />}
/>
<SettingSubtitle
style={{
marginTop: 5,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
{t('settings.provider.api_key.label')}
{provider.id !== 'copilot' && (
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
<Button type="text" onClick={openApiKeyList} icon={<Settings2 size={16} />} />
</Tooltip>
)}
</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
<Input.Password
value={localApiKey}
placeholder={t('settings.provider.api_key.label')}
onChange={(e) => setLocalApiKey(e.target.value)}
spellCheck={false}
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)}
disabled={provider.id === 'copilot'}
suffix={renderStatusIndicator()}
/>
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
<Button danger onClick={onReset}>
{t('settings.provider.api.url.reset')}
</Button>
)}
<Button
type={isApiKeyConnectable ? 'primary' : 'default'}
ghost={isApiKeyConnectable}
onClick={onCheckApi}
disabled={!apiHost || apiKeyConnectivity.checking}>
{apiKeyConnectivity.checking ? (
<LoadingIcon />
) : apiKeyConnectivity.status === 'success' ? (
<Check size={16} className="lucide-custom" />
) : (
t('settings.provider.check')
)}
</Button>
</Space.Compact>
{(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
{t('settings.provider.api_host_preview', { url: hostPreview() })}
</SettingHelpText>
<SettingHelpText style={{ minWidth: 'fit-content' }}>
{t('settings.provider.api.url.tip')}
</SettingHelpText>
</SettingHelpTextRow>
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<HStack>
{apiKeyWebsite && !isDmxapi && (
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
)}
</HStack>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>
</>
)}
{!isDmxapi && (
<>
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Tooltip title={hostSelectorTooltip} mouseEnterDelay={0.3}>
<Selector
size={14}
value={activeHostField}
onChange={(value) => setActiveHostField(value as HostField)}
options={hostSelectorOptions}
style={{ paddingLeft: 1, fontWeight: 'bold' }}
placement="bottomLeft"
/>
</Tooltip>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Button
type="text"
onClick={() => CustomHeaderPopup.show({ provider })}
icon={<Settings2 size={16} />}
/>
</div>
</SettingSubtitle>
{activeHostField === 'apiHost' && (
<>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
{isApiHostResettable && (
<Button danger onClick={onReset}>
{t('settings.provider.api.url.reset')}
</Button>
)}
</Space.Compact>
{isVertexProvider(provider) && (
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
</SettingHelpTextRow>
)}
{(isOpenAICompatibleProvider(provider) ||
isAzureOpenAIProvider(provider) ||
isAnthropicProvider(provider) ||
isGeminiProvider(provider) ||
isVertexProvider(provider) ||
isOpenAIProvider(provider)) && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText
style={{
marginLeft: 6,
marginRight: '1em',
whiteSpace: 'break-spaces',
wordBreak: 'break-all'
}}>
{t('settings.provider.api_host_preview', { url: hostPreview() })}
</SettingHelpText>
</SettingHelpTextRow>
)}
</>
)}
{canConfigureAnthropicHost && (
{activeHostField === 'anthropicApiHost' && canConfigureAnthropicHost && (
<>
<SettingSubtitle
style={{
marginTop: 5,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Tooltip title={t('settings.provider.anthropic_api_host_tooltip')} mouseEnterDelay={0.3}>
<SubtitleLabel>{t('settings.provider.anthropic_api_host')}</SubtitleLabel>
</Tooltip>
</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={anthropicApiHost ?? ''}
@ -480,9 +554,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
url: anthropicHostPreview || '—'
})}
</SettingHelpText>
<SettingHelpText style={{ marginLeft: 6 }}>
{t('settings.provider.anthropic_api_host_tip')}
</SettingHelpText>
</SettingHelpTextRow>
</>
)}
@ -512,21 +583,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
{provider.id === 'gpustack' && <GPUStackSettings />}
{provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />}
{provider.id === 'aws-bedrock' && <AwsBedrockSettings />}
{provider.id === 'vertexai' && <VertexAISettings providerId={provider.id} />}
{provider.id === 'vertexai' && <VertexAISettings />}
<ModelList providerId={provider.id} />
</SettingContainer>
)
}
const SubtitleLabel = styled.span`
display: inline-flex;
align-items: center;
line-height: inherit;
font-size: inherit;
font-weight: inherit;
color: inherit;
`
const ProviderName = styled.span`
font-size: 14px;
font-weight: 500;

View File

@ -1,18 +1,13 @@
import { HStack } from '@renderer/components/Layout'
import { PROVIDER_URLS } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider'
import { useVertexAISettings } from '@renderer/hooks/useVertexAI'
import { Alert, Input, Space } from 'antd'
import { FC, useState } from 'react'
import { Alert, Input } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
interface Props {
providerId: string
}
const VertexAISettings: FC<Props> = ({ providerId }) => {
const VertexAISettings = () => {
const { t } = useTranslation()
const {
projectId,
@ -27,16 +22,9 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
const [localProjectId, setLocalProjectId] = useState(projectId)
const [localLocation, setLocalLocation] = useState(location)
const { provider, updateProvider } = useProvider(providerId)
const [apiHost, setApiHost] = useState(provider.apiHost)
const providerConfig = PROVIDER_URLS['vertexai']
const apiKeyWebsite = providerConfig?.websites?.apiKey
const onUpdateApiHost = () => {
updateProvider({ apiHost })
}
const handleProjectIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalProjectId(e.target.value)
}
@ -72,18 +60,6 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
return (
<>
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
</Space.Compact>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
</SettingHelpTextRow>
<SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.provider.vertex_ai.service_account.title')}
</SettingSubtitle>

View File

@ -3,6 +3,7 @@ import { Span } from '@opentelemetry/api'
import AiProvider from '@renderer/aiCore'
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant'
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
import { isGeminiProvider } from '@renderer/config/providers'
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import store from '@renderer/store'
import {
@ -40,7 +41,7 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
let host = aiProvider.getBaseURL()
const rerankHost = rerankAiProvider.getBaseURL()
if (provider.type === 'gemini') {
if (isGeminiProvider(provider)) {
host = host + '/v1beta/openai/'
}

View File

@ -2716,7 +2716,6 @@ const migrateConfig = {
}
},
'166': (state: RootState) => {
// added after 1.6.5 and 1.7.0-beta.2
try {
if (state.assistants.presets === undefined) {
state.assistants.presets = []
@ -2733,6 +2732,18 @@ const migrateConfig = {
if (dashscopeProvider) {
dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic'
}
state.llm.providers.forEach((provider) => {
if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') {
provider.type = 'new-api'
}
if (provider.id === SystemProviderIds.longcat) {
// https://longcat.chat/platform/docs/zh/#anthropic-api-%E6%A0%BC%E5%BC%8F
if (!provider.anthropicApiHost) {
provider.anthropicApiHost = 'https://api.longcat.chat/anthropic'
}
}
})
return state
} catch (error) {
logger.error('migrate 166 error', error as Error)

View File

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

View File

@ -6,7 +6,6 @@ export const ProviderTypeSchema = z.enum([
'openai-response',
'anthropic',
'gemini',
'qwenlm',
'azure-openai',
'vertexai',
'mistral',
@ -37,6 +36,8 @@ export type ProviderApiOptions = {
isSupportServiceTier?: boolean
/** 是否不支持 enable_thinking 参数 */
isNotSupportEnableThinking?: boolean
/** 是否不支持 APIVersion */
isNotSupportAPIVersion?: boolean
}
export const OpenAIServiceTiers = {
@ -187,6 +188,11 @@ export type VertexProvider = Provider & {
location: string
}
export type AzureOpenAIProvider = Provider & {
type: 'azure-openai'
apiVersion: string
}
/**
* 使`provider.isSystem`
* @param provider - Provider对象

View File

@ -1,31 +1,106 @@
import { describe, expect, it } from 'vitest'
import store from '@renderer/store'
import type { VertexProvider } from '@renderer/types'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { formatApiHost, maskApiKey, splitApiKeyString } from '../api'
import {
formatApiHost,
formatApiKeys,
formatAzureOpenAIApiHost,
formatVertexApiHost,
hasAPIVersion,
maskApiKey,
routeToEndpoint,
splitApiKeyString,
validateApiHost
} from '../api'
vi.mock('@renderer/store', () => {
const getState = vi.fn()
return {
default: {
getState
}
}
})
const getStateMock = store.getState as unknown as ReturnType<typeof vi.fn>
const createVertexProvider = (apiHost: string): VertexProvider => ({
id: 'vertex-provider',
type: 'vertexai',
name: 'Vertex AI',
apiKey: '',
apiHost,
models: [],
googleCredentials: {
privateKey: '',
clientEmail: ''
},
project: '',
location: ''
})
beforeEach(() => {
getStateMock.mockReset()
getStateMock.mockReturnValue({
llm: {
settings: {
vertexai: {
projectId: 'test-project',
location: 'us-central1'
}
}
}
})
})
describe('api', () => {
describe('formatApiHost', () => {
it('should return original host when it ends with a slash', () => {
expect(formatApiHost('https://api.example.com/')).toBe('https://api.example.com/')
expect(formatApiHost('http://localhost:5173/')).toBe('http://localhost:5173/')
})
it('should return original host when it ends with volces.com/api/v3', () => {
expect(formatApiHost('https://api.volces.com/api/v3')).toBe('https://api.volces.com/api/v3')
expect(formatApiHost('http://volces.com/api/v3')).toBe('http://volces.com/api/v3')
})
it('should append /v1/ to hosts that do not match special conditions', () => {
expect(formatApiHost('https://api.example.com')).toBe('https://api.example.com/v1/')
expect(formatApiHost('http://localhost:5173')).toBe('http://localhost:5173/v1/')
expect(formatApiHost('https://api.openai.com')).toBe('https://api.openai.com/v1/')
})
it('should not modify hosts that already have a path but do not end with a slash', () => {
expect(formatApiHost('https://api.example.com/custom')).toBe('https://api.example.com/custom/v1/')
})
it('should handle empty string gracefully', () => {
it('returns empty string for falsy host', () => {
expect(formatApiHost('')).toBe('')
expect(formatApiHost(undefined)).toBe('')
})
it('appends api version when missing', () => {
expect(formatApiHost('https://api.example.com')).toBe('https://api.example.com/v1')
expect(formatApiHost('http://localhost:5173/')).toBe('http://localhost:5173/v1')
expect(formatApiHost(' https://api.openai.com ')).toBe('https://api.openai.com/v1')
})
it('keeps original host when api version already present', () => {
expect(formatApiHost('https://api.volces.com/api/v3')).toBe('https://api.volces.com/api/v3')
expect(formatApiHost('http://localhost:5173/v2beta')).toBe('http://localhost:5173/v2beta')
})
it('supports custom api version parameter', () => {
expect(formatApiHost('https://api.example.com', true, 'v2')).toBe('https://api.example.com/v2')
})
it('keeps host untouched when api version unsupported', () => {
expect(formatApiHost('https://api.example.com', false)).toBe('https://api.example.com')
})
})
describe('hasAPIVersion', () => {
it('detects numeric version suffix', () => {
expect(hasAPIVersion('https://api.example.com/v1')).toBe(true)
expect(hasAPIVersion('http://localhost:3000/v2beta')).toBe(true)
expect(hasAPIVersion('/v3alpha/resources')).toBe(true)
})
it('returns false when no version found', () => {
expect(hasAPIVersion('https://api.example.com')).toBe(false)
expect(hasAPIVersion('')).toBe(false)
expect(hasAPIVersion(undefined)).toBe(false)
})
it('return flase when starting without v character', () => {
expect(hasAPIVersion('https://api.example.com/a1v')).toBe(false)
expect(hasAPIVersion('/av1/users')).toBe(false)
})
it('return flase when starting with v- word', () => {
expect(hasAPIVersion('https://api.example.com/vendor')).toBe(false)
})
})
@ -123,4 +198,122 @@ describe('api', () => {
expect(result).toEqual(['key1', 'key2,withcomma', 'key3'])
})
})
describe('validateApiHost', () => {
it('accepts empty or whitespace-only host', () => {
expect(validateApiHost('')).toBe(true)
expect(validateApiHost(' ')).toBe(true)
})
it('rejects unsupported protocols', () => {
expect(validateApiHost('ftp://api.example.com')).toBe(false)
})
it('validates supported endpoint fragments when using hash suffix', () => {
expect(validateApiHost('https://api.example.com/v1/chat/completions#')).toBe(true)
expect(validateApiHost('https://api.example.com/v1/unknown#')).toBe(true)
})
})
describe('routeToEndpoint', () => {
it('returns host without endpoint when not using hash suffix', () => {
expect(routeToEndpoint(' https://api.example.com/v1 ')).toEqual({
baseURL: 'https://api.example.com/v1',
endpoint: ''
})
})
it('extracts known endpoint and base url when using hash suffix', () => {
expect(routeToEndpoint('https://api.example.com/v1/chat/completions#')).toEqual({
baseURL: 'https://api.example.com/v1',
endpoint: 'chat/completions'
})
})
it('returns empty endpoint when unsupported endpoint fragment is provided', () => {
expect(routeToEndpoint('https://api.example.com/v1/custom#')).toEqual({
baseURL: 'https://api.example.com/v1/custom',
endpoint: ''
})
})
it('prefers the most specific endpoint match when multiple matches exist', () => {
expect(routeToEndpoint('https://api.example.com/v1/streamGenerateContent#')).toEqual({
baseURL: 'https://api.example.com/v1',
endpoint: 'streamGenerateContent'
})
})
it('extract OpenAI images generations endpoint', () => {
expect(routeToEndpoint('https://open.cherryin.net/v1/images/generations#')).toEqual({
baseURL: 'https://open.cherryin.net/v1',
endpoint: 'images/generations'
})
})
it('extract Gemini images generation endpoint', () => {
expect(routeToEndpoint('https://open.cherryin.net/v1beta/models/imagen-4.0-generate-001:predict#')).toEqual({
baseURL: 'https://open.cherryin.net/v1beta/models/imagen-4.0-generate-001',
endpoint: 'predict'
})
})
})
describe('formatApiKeys', () => {
it('normalizes chinese commas and new lines', () => {
expect(formatApiKeys('key1key2\nkey3')).toBe('key1,key2,key3')
})
it('returns empty string unchanged', () => {
expect(formatApiKeys('')).toBe('')
})
})
describe('formatAzureOpenAIApiHost', () => {
it('normalizes trailing segments and disables auto version append', () => {
expect(formatAzureOpenAIApiHost('https://example.openai.azure.com/')).toBe(
'https://example.openai.azure.com/openai'
)
expect(formatAzureOpenAIApiHost('https://example.openai.azure.com/openai/')).toBe(
'https://example.openai.azure.com/openai'
)
})
})
describe('formatVertexApiHost', () => {
it('builds default google endpoint when host absent', () => {
expect(formatVertexApiHost(createVertexProvider(''))).toBe(
'https://us-central1-aiplatform.googleapis.com/v1/projects/test-project/locations/us-central1'
)
})
it('prefers default endpoint when host ends with google domain', () => {
expect(formatVertexApiHost(createVertexProvider('https://aiplatform.googleapis.com'))).toBe(
'https://us-central1-aiplatform.googleapis.com/v1/projects/test-project/locations/us-central1'
)
})
it('appends api version to custom host', () => {
expect(formatVertexApiHost(createVertexProvider('https://custom.googleapis.com/vertex'))).toBe(
'https://custom.googleapis.com/vertex/v1'
)
})
it('uses global endpoint when location equals global', () => {
getStateMock.mockReturnValueOnce({
llm: {
settings: {
vertexai: {
projectId: 'global-project',
location: 'global'
}
}
}
})
expect(formatVertexApiHost(createVertexProvider(''))).toBe(
'https://aiplatform.googleapis.com/v1/projects/global-project/locations/global'
)
})
})
})

View File

@ -1,8 +1,18 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { runAsyncFunction } from '../index'
import { hasPath, isValidProxyUrl, removeQuotes, removeSpecialCharacters } from '../index'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
describe('Unclassified Utils', () => {
describe('runAsyncFunction', () => {
it('should execute async function', async () => {

View File

@ -1,7 +1,17 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { isJSON, parseJSON } from '../index'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
describe('json', () => {
describe('isJSON', () => {
it('should return true for valid JSON string', () => {

View File

@ -1,3 +1,7 @@
import store from '@renderer/store'
import { VertexProvider } from '@renderer/types'
import { trim } from 'lodash'
/**
* API key
*
@ -9,30 +13,166 @@ export function formatApiKeys(value: string): string {
}
/**
* API
* host path /v1/v2beta
*
* host `/v1/`
* - host `/` `volces.com/api/v3`
* -
*
* @param {string} host - API
* @param {string} apiVersion - API
* @returns {string} API
* @param host - host path
* @returns path true false
*/
export function formatApiHost(host: string, apiVersion: string = 'v1'): string {
if (!host) {
export function hasAPIVersion(host?: string): boolean {
if (!host) return false
// 匹配路径中以 `/v<number>` 开头并可选跟随 `alpha` 或 `beta` 的版本段,
// 该段后面可以跟 `/` 或字符串结束(用于匹配诸如 `/v3alpha/resources` 的情况)。
const versionRegex = /\/v\d+(?:alpha|beta)?(?=\/|$)/i
try {
const url = new URL(host)
return versionRegex.test(url.pathname)
} catch {
// 若无法作为完整 URL 解析,则当作路径直接检测
return versionRegex.test(host)
}
}
/**
* Removes the trailing slash from a URL string if it exists.
*
* @template T - The string type to preserve type safety
* @param {T} url - The URL string to process
* @returns {T} The URL string without a trailing slash
*
* @example
* ```ts
* withoutTrailingSlash('https://example.com/') // 'https://example.com'
* withoutTrailingSlash('https://example.com') // 'https://example.com'
* ```
*/
export function withoutTrailingSlash<T extends string>(url: T): T {
return url.replace(/\/$/, '') as T
}
/**
* Formats an API host URL by normalizing it and optionally appending an API version.
*
* @param host - The API host URL to format. Leading/trailing whitespace will be trimmed and trailing slashes removed.
* @param isSupportedAPIVerion - Whether the API version is supported. Defaults to `true`.
* @param apiVersion - The API version to append if needed. Defaults to `'v1'`.
*
* @returns The formatted API host URL. If the host is empty after normalization, returns an empty string.
* If the host ends with '#', API version is not supported, or the host already contains a version, returns the normalized host as-is.
* Otherwise, returns the host with the API version appended.
*
* @example
* formatApiHost('https://api.example.com/') // Returns 'https://api.example.com/v1'
* formatApiHost('https://api.example.com#') // Returns 'https://api.example.com#'
* formatApiHost('https://api.example.com/v2', true, 'v1') // Returns 'https://api.example.com/v2'
*/
export function formatApiHost(host?: string, isSupportedAPIVerion: boolean = true, apiVersion: string = 'v1'): string {
const normalizedHost = withoutTrailingSlash(trim(host))
if (!normalizedHost) {
return ''
}
const forceUseOriginalHost = () => {
if (host.endsWith('/')) {
return true
}
return host.endsWith('volces.com/api/v3')
if (normalizedHost.endsWith('#') || !isSupportedAPIVerion || hasAPIVersion(normalizedHost)) {
return normalizedHost
}
return `${normalizedHost}/${apiVersion}`
}
return forceUseOriginalHost() ? host : `${host}/${apiVersion}/`
/**
* Azure OpenAI API
*/
export function formatAzureOpenAIApiHost(host: string): string {
const normalizedHost = withoutTrailingSlash(host)
?.replace(/\/v1$/, '')
.replace(/\/openai$/, '')
// NOTE: AISDK会添加上`v1`
return formatApiHost(normalizedHost + '/openai', false)
}
export function formatVertexApiHost(provider: VertexProvider): string {
const { apiHost } = provider
const { projectId: project, location } = store.getState().llm.settings.vertexai
const trimmedHost = withoutTrailingSlash(trim(apiHost))
if (!trimmedHost || trimmedHost.endsWith('aiplatform.googleapis.com')) {
const host =
location == 'global' ? 'https://aiplatform.googleapis.com' : `https://${location}-aiplatform.googleapis.com`
return `${formatApiHost(host)}/projects/${project}/locations/${location}`
}
return formatApiHost(trimmedHost)
}
// 目前对话界面只支持这些端点
export const SUPPORTED_IMAGE_ENDPOINT_LIST = ['images/generations', 'images/edits', 'predict'] as const
export const SUPPORTED_ENDPOINT_LIST = [
'chat/completions',
'responses',
'messages',
'generateContent',
'streamGenerateContent',
...SUPPORTED_IMAGE_ENDPOINT_LIST
] as const
/**
* Converts an API host URL into separate base URL and endpoint components.
*
* @param apiHost - The API host string to parse. Expected to be a trimmed URL that may end with '#' followed by an endpoint identifier.
* @returns An object containing:
* - `baseURL`: The base URL without the endpoint suffix
* - `endpoint`: The matched endpoint identifier, or empty string if no match found
*
* @description
* This function extracts endpoint information from a composite API host string.
* If the host ends with '#', it attempts to match the preceding part against the supported endpoint list.
* The '#' delimiter is removed before processing.
*
* @example
* routeToEndpoint('https://api.example.com/openai/chat/completions#')
* // Returns: { baseURL: 'https://api.example.com/v1', endpoint: 'chat/completions' }
*
* @example
* routeToEndpoint('https://api.example.com/v1')
* // Returns: { baseURL: 'https://api.example.com/v1', endpoint: '' }
*/
export function routeToEndpoint(apiHost: string): { baseURL: string; endpoint: string } {
const trimmedHost = trim(apiHost)
// 前面已经确保apiHost合法
if (!trimmedHost.endsWith('#')) {
return { baseURL: trimmedHost, endpoint: '' }
}
// 去掉结尾的 #
const host = trimmedHost.slice(0, -1)
const endpointMatch = SUPPORTED_ENDPOINT_LIST.find((endpoint) => host.endsWith(endpoint))
if (!endpointMatch) {
const baseURL = withoutTrailingSlash(host)
return { baseURL, endpoint: '' }
}
const baseSegment = host.slice(0, host.length - endpointMatch.length)
const baseURL = withoutTrailingSlash(baseSegment).replace(/:$/, '') // 去掉结尾可能存在的冒号(gemini的特殊情况)
return { baseURL, endpoint: endpointMatch }
}
/**
* API
*
* @param {string} apiHost - API
* @returns {boolean} URL true false
*/
export function validateApiHost(apiHost: string): boolean {
// 允许apiHost为空
if (!apiHost || !trim(apiHost)) {
return true
}
try {
const url = new URL(trim(apiHost))
// 验证协议是否为 http 或 https
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return false
}
return true
} catch {
return false
}
}
/**

View File

@ -1,5 +1,5 @@
import { loggerService } from '@logger'
import { Model, ModelType, Provider } from '@renderer/types'
import { Model, ModelType } from '@renderer/types'
import { ModalFuncProps } from 'antd'
import { isEqual } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
@ -196,19 +196,6 @@ export function getMcpConfigSampleFromReadme(readme: string): Record<string, any
return null
}
/**
* OpenAI
* @param {Provider} provider
* @returns {boolean} OpenAI
*/
export function isOpenAIProvider(provider: Provider): boolean {
return !['anthropic', 'gemini', 'vertexai'].includes(provider.type)
}
export function isAnthropicProvider(provider: Provider): boolean {
return provider.type === 'anthropic'
}
/**
*
* @param {Model} model

137
yarn.lock
View File

@ -74,11 +74,11 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/amazon-bedrock@npm:^3.0.35":
version: 3.0.35
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.35"
"@ai-sdk/amazon-bedrock@npm:^3.0.42":
version: 3.0.42
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.42"
dependencies:
"@ai-sdk/anthropic": "npm:2.0.27"
"@ai-sdk/anthropic": "npm:2.0.32"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
"@smithy/eventstream-codec": "npm:^4.0.1"
@ -86,32 +86,32 @@ __metadata:
aws4fetch: "npm:^1.0.20"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/0e3e0ed1730fa6a14d8d7ca14b7823ec0b80c9d666435d97a505e7fb0c1818378343cdb647e3cc08d7f15d201cbeb04272c5128065f6cc6858b4404961eca761
checksum: 10c0/659de3d62f1907489bb14cd7fe049274c0a5f754222eda41b500d66573422ddaad3380cf8fc6eaae8a39ab25445e81aca7664ca2068b4a93c49bcb605889b2ba
languageName: node
linkType: hard
"@ai-sdk/anthropic@npm:2.0.27, @ai-sdk/anthropic@npm:^2.0.27":
version: 2.0.27
resolution: "@ai-sdk/anthropic@npm:2.0.27"
"@ai-sdk/anthropic@npm:2.0.32, @ai-sdk/anthropic@npm:^2.0.32":
version: 2.0.32
resolution: "@ai-sdk/anthropic@npm:2.0.32"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/b568b3b8639af8ec7ea9b766061a4f18bcdef16f2bb12da3a4c4124c751bd6aab1b96dbe1a0eb8e38831d305871ce0787a6536d1a4d8a8ab8aaf03aca3e48e3f
checksum: 10c0/f83ec81fe150dacd9207b67a173f7e150b44a0b2b57e6361c061e35b663bbb95240ea18066bd2bce73df722b85772ca174c4f1546b29eb6e6d1fcf4f349e756b
languageName: node
linkType: hard
"@ai-sdk/azure@npm:^2.0.49":
version: 2.0.49
resolution: "@ai-sdk/azure@npm:2.0.49"
"@ai-sdk/azure@npm:^2.0.53":
version: 2.0.53
resolution: "@ai-sdk/azure@npm:2.0.53"
dependencies:
"@ai-sdk/openai": "npm:2.0.48"
"@ai-sdk/openai": "npm:2.0.52"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/d4dc5a8e0cbe0cefc8db987c4a7b784a9898d40cc55ef38618c71eba7f40dbef77b754aec1d507559f643fed49e538ffe2b677b327f001a2efc0474f6b544ba9
checksum: 10c0/39346f50434c3568b40bb57aa64010261ae767d9aa49b4477999ca78431326275b111879b9c5431ce35ca4ca376c47455618c8bf528c54402b0dad1b03e10487
languageName: node
linkType: hard
@ -128,55 +128,55 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/gateway@npm:1.0.39":
version: 1.0.39
resolution: "@ai-sdk/gateway@npm:1.0.39"
"@ai-sdk/gateway@npm:2.0.0":
version: 2.0.0
resolution: "@ai-sdk/gateway@npm:2.0.0"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
"@vercel/oidc": "npm:3.0.2"
"@vercel/oidc": "npm:3.0.3"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/1b6eedf12ac641c96a1eb75e48e43474694b60eb7dca273f76a636a4e2bfc89efda1d9855d5abf9cc464e23cdbf5a3119fed65c3d22cec726e29a2bad3c3318b
checksum: 10c0/720cfb827bc64f3eb6bb86d17e7e7947c54bdc7d74db7f6e9e162be0973a45368c05829e4b257704182ca9c4886e7f3c74f6b64841e88359930f48f288aa958a
languageName: node
linkType: hard
"@ai-sdk/google-vertex@npm:^3.0.40":
version: 3.0.40
resolution: "@ai-sdk/google-vertex@npm:3.0.40"
"@ai-sdk/google-vertex@npm:^3.0.48":
version: 3.0.48
resolution: "@ai-sdk/google-vertex@npm:3.0.48"
dependencies:
"@ai-sdk/anthropic": "npm:2.0.27"
"@ai-sdk/google": "npm:2.0.20"
"@ai-sdk/anthropic": "npm:2.0.32"
"@ai-sdk/google": "npm:2.0.23"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
google-auth-library: "npm:^9.15.0"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/680a06e1b80bc036744e2f13e1a55b57661c3674000ab82b863d6536730edfc3696b1b0b2235f6354de11fa323c4ef817d8edbd2dbf94dc4037ea882e560c9ea
checksum: 10c0/79f0ccb78c4930ea57a41e81f31a1935531d8f02b738d0aae13fa865272f4dac6b1c31b2e1c8b8ca65671a96b90cd4f14fabaa9d60ab0252c6c0e6a1828e7f09
languageName: node
linkType: hard
"@ai-sdk/google@npm:2.0.20":
version: 2.0.20
resolution: "@ai-sdk/google@npm:2.0.20"
"@ai-sdk/google@npm:2.0.23":
version: 2.0.23
resolution: "@ai-sdk/google@npm:2.0.23"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/9c73bb67061673b16f0996c85bf4e79ab9968c8a203c4f9731bf569e45960db88950dfc227aca69661ea805d381b285697ba1763faa03a38c01b86e6d2e90629
checksum: 10c0/402b78f392196c3e23c75cc35fc1d701f9521b57aace2fb1bbae6a0d57bbb3894a778b0485305bd6674998403e44c3883dca2416f2d48377722351debead9f11
languageName: node
linkType: hard
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch":
version: 2.0.20
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch::version=2.0.20&hash=1f2ccb"
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch":
version: 2.0.23
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch::version=2.0.23&hash=df67ed"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/2d567361d533a4e2be83aa135cb5f01f09ea54c255d7751171855ef4244cfaeff73fe7b3c7b044b384a9c170e89d053160a26933176ad68dcaf03bd3c69c0be3
checksum: 10c0/e7fda169f04190b3ef37937e61219dcf8dade735cf76a9af8f1a1def83a43846659a361835814f0b68a2c392bc840a457a693cb69fed42af375771dd210ebdbe
languageName: node
linkType: hard
@ -242,27 +242,39 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/openai@npm:2.0.48, @ai-sdk/openai@npm:^2.0.48":
version: 2.0.48
resolution: "@ai-sdk/openai@npm:2.0.48"
"@ai-sdk/openai@npm:2.0.52":
version: 2.0.52
resolution: "@ai-sdk/openai@npm:2.0.52"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/6c584d7ffb80025da6b7253106a83f8c7a023e8ca322fd32e6858453782d6a0a6d268d7afa7145e3ea743a9c6cbc882932bb59eb1a659750f5205639c414fb49
checksum: 10c0/253125303235dc677e272eaffbcd5c788373e12f897e42da7cce827bcc952f31e4bb11b72ba06931f37d49a2588f6cba8526127d539025bbd58d78d7bcfc691d
languageName: node
linkType: hard
"@ai-sdk/openai@npm:^2.0.42":
version: 2.0.47
resolution: "@ai-sdk/openai@npm:2.0.47"
version: 2.0.53
resolution: "@ai-sdk/openai@npm:2.0.53"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.11"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/7fabcdda707134971bcc2b285705d4595f8bf419285dbdd9266b3b0858ea11b6ac200e63dd2eeb1822f99571910093d64d4a76154a365331cf184f56452933c6
checksum: 10c0/acb014c7e4d99be0502fe2190c3b91c76ee86ade25e80dad939ffd113a5f013f29a81f06e13fa0e6a76b49fcb8cc524aab180fc1a622ceb8d3dac58fd655de1c
languageName: node
linkType: hard
"@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch":
version: 2.0.52
resolution: "@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch::version=2.0.52&hash=c7ceb9"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/a3ac267a645ffd50952c312318d0ea6190e1ca961f910f9e3067211df731ac4ba0eb89face21b5cc195770b643326b295a6fece91f07b60db8aef32f45d4664e
languageName: node
linkType: hard
@ -291,19 +303,6 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.11":
version: 3.0.11
resolution: "@ai-sdk/provider-utils@npm:3.0.11"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/31081b127b48f3eefb448eaca59574b4631da9577aa0778622d28669c71bbde0361c9b37962c5edbb1d0c163ed1479755fc889da9251a03e906b1e27d0d2eb24
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.12, @ai-sdk/provider-utils@npm:^3.0.12":
version: 3.0.12
resolution: "@ai-sdk/provider-utils@npm:3.0.12"
@ -2469,10 +2468,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "@cherrystudio/ai-core@workspace:packages/aiCore"
dependencies:
"@ai-sdk/anthropic": "npm:^2.0.27"
"@ai-sdk/azure": "npm:^2.0.49"
"@ai-sdk/anthropic": "npm:^2.0.32"
"@ai-sdk/azure": "npm:^2.0.53"
"@ai-sdk/deepseek": "npm:^1.0.23"
"@ai-sdk/openai": "npm:^2.0.48"
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch"
"@ai-sdk/openai-compatible": "npm:^1.0.22"
"@ai-sdk/provider": "npm:^2.0.0"
"@ai-sdk/provider-utils": "npm:^3.0.12"
@ -13578,10 +13577,10 @@ __metadata:
languageName: node
linkType: hard
"@vercel/oidc@npm:3.0.2":
version: 3.0.2
resolution: "@vercel/oidc@npm:3.0.2"
checksum: 10c0/8d4c8553baa5aed339ab7614d775139bc124a6d443b76877ab17e98c156daa4dbeb3cf2f3bf21fabfae2ac0dd3ff462ab43b9398708e02483e5923d302a1c4c8
"@vercel/oidc@npm:3.0.3":
version: 3.0.3
resolution: "@vercel/oidc@npm:3.0.3"
checksum: 10c0/c8eecb1324559435f4ab8a955f5ef44f74f546d11c2ddcf28151cb636d989bd4b34e0673fd8716cb21bb21afb34b3de663bacc30c9506036eeecbcbf2fd86241
languageName: node
linkType: hard
@ -13888,8 +13887,8 @@ __metadata:
"@agentic/exa": "npm:^7.3.3"
"@agentic/searxng": "npm:^7.3.3"
"@agentic/tavily": "npm:^7.3.3"
"@ai-sdk/amazon-bedrock": "npm:^3.0.35"
"@ai-sdk/google-vertex": "npm:^3.0.40"
"@ai-sdk/amazon-bedrock": "npm:^3.0.42"
"@ai-sdk/google-vertex": "npm:^3.0.48"
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch"
"@ai-sdk/mistral": "npm:^2.0.19"
"@ai-sdk/perplexity": "npm:^2.0.13"
@ -14017,7 +14016,7 @@ __metadata:
"@viz-js/lang-dot": "npm:^1.0.5"
"@viz-js/viz": "npm:^3.14.0"
"@xyflow/react": "npm:^12.4.4"
ai: "npm:^5.0.68"
ai: "npm:^5.0.76"
antd: "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch"
archiver: "npm:^7.0.1"
async-mutex: "npm:^0.5.0"
@ -14284,17 +14283,17 @@ __metadata:
languageName: node
linkType: hard
"ai@npm:^5.0.68":
version: 5.0.68
resolution: "ai@npm:5.0.68"
"ai@npm:^5.0.76":
version: 5.0.76
resolution: "ai@npm:5.0.76"
dependencies:
"@ai-sdk/gateway": "npm:1.0.39"
"@ai-sdk/gateway": "npm:2.0.0"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
"@opentelemetry/api": "npm:1.9.0"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/0c042cd58c7193a47b06b3074a9e62790c4d5a8134e8e12bbb750714151e9aa217c641ee60c8cbe59d9869bade52ccbb283f9fcbf6d79711ebf1f774fa3feee3
checksum: 10c0/167a191354b72106b1af6cfc8b53975637ca43919b8f48db81c0cf542ef0172f55958ed9331adcd08d017a608a98cb0b4a253c62732038322c78091e32595771
languageName: node
linkType: hard