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/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3", "@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3", "@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.35", "@ai-sdk/amazon-bedrock": "^3.0.42",
"@ai-sdk/google-vertex": "^3.0.40", "@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/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/mistral": "^2.0.19",
"@ai-sdk/perplexity": "^2.0.13", "@ai-sdk/perplexity": "^2.0.13",
@ -227,7 +227,7 @@
"@viz-js/lang-dot": "^1.0.5", "@viz-js/lang-dot": "^1.0.5",
"@viz-js/viz": "^3.14.0", "@viz-js/viz": "^3.14.0",
"@xyflow/react": "^12.4.4", "@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", "antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
@ -390,7 +390,8 @@
"undici": "6.21.2", "undici": "6.21.2",
"vite": "npm:rolldown-vite@7.1.5", "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", "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-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm": "0.34.3",

View File

@ -36,10 +36,10 @@
"ai": "^5.0.26" "ai": "^5.0.26"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^2.0.27", "@ai-sdk/anthropic": "^2.0.32",
"@ai-sdk/azure": "^2.0.49", "@ai-sdk/azure": "^2.0.53",
"@ai-sdk/deepseek": "^1.0.23", "@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/openai-compatible": "^1.0.22",
"@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.12", "@ai-sdk/provider-utils": "^3.0.12",

View File

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

View File

@ -6,7 +6,14 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService' 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 { Chunk, ChunkType } from '@renderer/types/chunk'
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai' import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
@ -254,6 +261,7 @@ export class ToolCallChunkHandler {
type: 'tool-result' type: 'tool-result'
} & TypedToolResult<ToolSet> } & TypedToolResult<ToolSet>
): void { ): void {
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
const { toolCallId, output, input } = chunk const { toolCallId, output, input } = chunk
if (!toolCallId) { if (!toolCallId) {
@ -299,12 +307,7 @@ export class ToolCallChunkHandler {
responses: [toolResponse] responses: [toolResponse]
}) })
const images: string[] = [] const images = extractImagesFromToolOutput(toolResponse.response)
for (const content of toolResponse.response?.content || []) {
if (content.type === 'image' && content.data) {
images.push(`data:${content.mimeType};base64,${content.data}`)
}
}
if (images.length) { if (images.length) {
this.onChunk({ this.onChunk({
@ -351,3 +354,41 @@ export class ToolCallChunkHandler {
} }
export const addActiveToolCall = ToolCallChunkHandler.addActiveToolCall.bind(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 { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types' import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes' import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic' import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai' import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
@ -77,7 +78,7 @@ export default class ModernAiProvider {
return this.actualProvider return this.actualProvider
} }
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) { public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
// 检查model是否存在 // 检查model是否存在
if (!this.model) { if (!this.model) {
throw new Error('Model is required for completions. Please use constructor with model parameter.') throw new Error('Model is required for completions. Please use constructor with model parameter.')
@ -85,7 +86,10 @@ export default class ModernAiProvider {
// 每次请求时重新生成配置以确保API key轮换生效 // 每次请求时重新生成配置以确保API key轮换生效
this.config = providerToAiSdkConfig(this.actualProvider, this.model) 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) await prepareSpecialProviderConfig(this.actualProvider, this.config)
@ -96,13 +100,13 @@ export default class ModernAiProvider {
// 提前构建中间件 // 提前构建中间件
const middlewares = buildAiSdkMiddlewares({ const middlewares = buildAiSdkMiddlewares({
...config, ...providerConfig,
provider: this.actualProvider, provider: this.actualProvider,
assistant: config.assistant assistant: providerConfig.assistant
}) })
logger.debug('Built middlewares in completions', { logger.debug('Built middlewares in completions', {
middlewareCount: middlewares.length, middlewareCount: middlewares.length,
isImageGeneration: config.isImageGenerationEndpoint isImageGeneration: providerConfig.isImageGenerationEndpoint
}) })
if (!this.localProvider) { if (!this.localProvider) {
throw new Error('Local provider not created') throw new Error('Local provider not created')
@ -110,7 +114,7 @@ export default class ModernAiProvider {
// 根据endpoint类型创建对应的模型 // 根据endpoint类型创建对应的模型
let model: AiSdkModel | undefined let model: AiSdkModel | undefined
if (config.isImageGenerationEndpoint) { if (providerConfig.isImageGenerationEndpoint) {
model = this.localProvider.imageModel(modelId) model = this.localProvider.imageModel(modelId)
} else { } else {
model = this.localProvider.languageModel(modelId) model = this.localProvider.languageModel(modelId)
@ -126,15 +130,15 @@ export default class ModernAiProvider {
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])] params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
} }
if (config.topicId && getEnableDeveloperMode()) { if (providerConfig.topicId && getEnableDeveloperMode()) {
// TypeScript类型窄化确保topicId是string类型 // TypeScript类型窄化确保topicId是string类型
const traceConfig = { const traceConfig = {
...config, ...providerConfig,
topicId: config.topicId topicId: providerConfig.topicId
} }
return await this._completionsForTrace(model, params, traceConfig) return await this._completionsForTrace(model, params, traceConfig)
} else { } 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 { Provider } from '@renderer/types'
import { isOpenAIProvider } from '@renderer/utils'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient' import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
@ -202,36 +201,4 @@ describe('ApiClientFactory', () => {
expect(client).toBeDefined() 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 { 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 { loggerService } from '@logger'
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings' import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
@ -68,9 +68,9 @@ export function buildPlugins(
) )
} }
if (middlewareConfig.enableUrlContext) { // if (middlewareConfig.enableUrlContext && middlewareConfig.) {
plugins.push(googleToolsPlugin({ urlContext: true })) // plugins.push(googleToolsPlugin({ urlContext: true }))
} // }
logger.debug( logger.debug(
'Final plugin list:', 'Final plugin list:',

View File

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

View File

@ -3,6 +3,8 @@
* AI SDK的流式和非流式参数 * AI SDK的流式和非流式参数
*/ */
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
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 { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
@ -97,10 +99,6 @@ export async function buildStreamTextParams(
let tools = setupToolsConfig(mcpTools) let tools = setupToolsConfig(mcpTools)
// if (webSearchProviderId) {
// tools['builtin_web_search'] = webSearchTool(webSearchProviderId)
// }
// 构建真正的 providerOptions // 构建真正的 providerOptions
const webSearchConfig: CherryWebSearchConfig = { const webSearchConfig: CherryWebSearchConfig = {
maxResults: store.getState().websearch.maxResults, maxResults: store.getState().websearch.maxResults,
@ -143,12 +141,34 @@ export async function buildStreamTextParams(
} }
} }
// google-vertex if (enableUrlContext) {
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
if (!tools) { if (!tools) {
tools = {} tools = {}
} }
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
switch (aiSdkProviderId) {
case 'google-vertex':
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool 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) => match: (model) =>
(startsWith('gemini')(model) || startsWith('imagen')(model)) && (startsWith('gemini')(model) || startsWith('imagen')(model)) &&
!model.id.endsWith('-nothink') && !model.id.endsWith('-nothink') &&
!model.id.endsWith('-search'), !model.id.endsWith('-search') &&
!model.id.includes('embedding'),
provider: (provider: Provider) => { provider: (provider: Provider) => {
return extraProviderConfig({ return extraProviderConfig({
...provider, ...provider,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,15 @@ describe('Qwen Model Detection', () => {
vi.mock('@renderer/services/AssistantService', () => ({ vi.mock('@renderer/services/AssistantService', () => ({
getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' }) getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' })
})) }))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
}) })
test('isQwenReasoningModel', () => { test('isQwenReasoningModel', () => {
expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true) 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' 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 // FIXME: Idk why it's imported. Maybe circular dependency somewhere
vi.mock('@renderer/services/AssistantService.ts', () => ({ vi.mock('@renderer/services/AssistantService.ts', () => ({
getDefaultAssistant: () => { getDefaultAssistant: () => {

View File

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

View File

@ -1,7 +1,8 @@
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types' import { Model, SystemProviderIds } from '@renderer/types'
import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils' import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils'
import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers'
import { isEmbeddingModel, isRerankModel } from './embedding' import { isEmbeddingModel, isRerankModel } from './embedding'
import { isAnthropicModel } from './utils' import { isAnthropicModel } from './utils'
import { isPureGenerateImageModel, isTextToImageModel } from './vision' import { isPureGenerateImageModel, isTextToImageModel } from './vision'
@ -65,12 +66,16 @@ export function isWebSearchModel(model: Model): boolean {
const modelId = getLowerBaseModelName(model.id, '/') const modelId = getLowerBaseModelName(model.id, '/')
// 不管哪个供应商都判断了 // bedrock和vertex不支持
if (isAnthropicModel(model)) { if (
isAnthropicModel(model) &&
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
) {
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId) return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
} }
if (provider.type === 'openai-response') { // TODO: 当其他供应商采用Response端点时这个地方逻辑需要改进
if (isOpenAIProvider(provider)) {
if (isOpenAIWebSearchModel(model)) { if (isOpenAIWebSearchModel(model)) {
return true return true
} }
@ -78,11 +83,11 @@ export function isWebSearchModel(model: Model): boolean {
return false return false
} }
if (provider.id === 'perplexity') { if (provider.id === SystemProviderIds.perplexity) {
return PERPLEXITY_SEARCH_MODELS.includes(modelId) return PERPLEXITY_SEARCH_MODELS.includes(modelId)
} }
if (provider.id === 'aihubmix') { if (provider.id === SystemProviderIds.aihubmix) {
// modelId 不以-search结尾 // modelId 不以-search结尾
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) { if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
return true return true
@ -95,13 +100,13 @@ export function isWebSearchModel(model: Model): boolean {
return false return false
} }
if (provider?.type === 'openai') { if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) { if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
return true 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) 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 ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import { import {
AtLeast, AtLeast,
AzureOpenAIProvider,
isSystemProvider, isSystemProvider,
OpenAIServiceTiers, OpenAIServiceTiers,
Provider, Provider,
@ -355,7 +356,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
name: 'VertexAI', name: 'VertexAI',
type: 'vertexai', type: 'vertexai',
apiKey: '', apiKey: '',
apiHost: 'https://aiplatform.googleapis.com', apiHost: '',
models: SYSTEM_MODELS.vertexai, models: SYSTEM_MODELS.vertexai,
isSystem: true, isSystem: true,
enabled: false, enabled: false,
@ -1295,7 +1296,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
}, },
vertexai: { vertexai: {
api: { api: {
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview' url: ''
}, },
websites: { websites: {
official: 'https://cloud.google.com/vertex-ai', official: 'https://cloud.google.com/vertex-ai',
@ -1375,7 +1376,8 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
'baichuan', 'baichuan',
'minimax', 'minimax',
'xirang', 'xirang',
'poe' 'poe',
'cephalon'
] as const satisfies SystemProviderId[] ] 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) => { 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[] 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) => { export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api' 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 * Provider VertexProvider
*/ */
export function isVertexProvider(provider: Provider): provider is 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": "Anthropic API Host",
"anthropic_api_host_preview": "Anthropic preview: {{url}}", "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.", "anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.",
"api": { "api": {
"key": { "key": {
@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Preview: {{url}}", "preview": "Preview: {{url}}",
"reset": "Reset", "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": "API Host",
"api_host_no_valid": "API address is invalid",
"api_host_preview": "Preview: {{url}}", "api_host_preview": "Preview: {{url}}",
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.", "api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
"api_key": { "api_key": {

View File

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

View File

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

View File

@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Anthropic API-Adresse", "anthropic_api_host": "Anthropic API-Adresse",
"anthropic_api_host_preview": "Anthropic-Vorschau: {{url}}", "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.", "anthropic_api_host_tooltip": "Nur bei Anbietern, die ein Claude-kompatibles Basis-Endpunkt anbieten.",
"api": { "api": {
"key": { "key": {
@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Vorschau: {{url}}", "preview": "Vorschau: {{url}}",
"reset": "Zurücksetzen", "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": "API-Adresse",
"api_host_no_valid": "API-Adresse ist ungültig",
"api_host_preview": "Vorschau: {{url}}", "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_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": { "api_key": {

View File

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

View File

@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Dirección API de Anthropic", "anthropic_api_host": "Dirección API de Anthropic",
"anthropic_api_host_preview": "Vista previa de Anthropic: {{url}}", "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.", "anthropic_api_host_tooltip": "Rellenar solo cuando el proveedor proporcione una dirección base compatible con Claude.",
"api": { "api": {
"key": { "key": {
@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Vista previa: {{url}}", "preview": "Vista previa: {{url}}",
"reset": "Restablecer", "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": "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_preview": "Vista previa: {{url}}",
"api_host_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.", "api_host_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.",
"api_key": { "api_key": {

View File

@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Adresse API Anthropic", "anthropic_api_host": "Adresse API Anthropic",
"anthropic_api_host_preview": "Aperçu Anthropic : {{url}}", "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.", "anthropic_api_host_tooltip": "Remplir seulement lorsque le fournisseur propose une adresse de base compatible Claude.",
"api": { "api": {
"key": { "key": {
@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Aperçu : {{url}}", "preview": "Aperçu : {{url}}",
"reset": "Réinitialiser", "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": "Adresse API",
"api_host_no_valid": "Adresse API invalide",
"api_host_preview": "Aperçu : {{url}}", "api_host_preview": "Aperçu : {{url}}",
"api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.", "api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.",
"api_key": { "api_key": {

View File

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

View File

@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Endereço da API Anthropic", "anthropic_api_host": "Endereço da API Anthropic",
"anthropic_api_host_preview": "Pré-visualização Anthropic: {{url}}", "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.", "anthropic_api_host_tooltip": "Preencher apenas quando o fornecedor fornece um endereço base compatível com Claude.",
"api": { "api": {
"key": { "key": {
@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Pré-visualização: {{url}}", "preview": "Pré-visualização: {{url}}",
"reset": "Redefinir", "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": "Endereço API",
"api_host_no_valid": "O endereço da API é inválido",
"api_host_preview": "Pré-visualização: {{url}}", "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_host_tooltip": "Substituir apenas quando o fornecedor necessita de um endereço compatível com OpenAI personalizado.",
"api_key": { "api_key": {

View File

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

View File

@ -3,6 +3,7 @@ import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons' import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelListItem } from '@renderer/components/QuickPanel' import { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { import {
isAnthropicModel,
isGeminiModel, isGeminiModel,
isGenerateImageModel, isGenerateImageModel,
isMandatoryWebSearchModel, isMandatoryWebSearchModel,
@ -385,7 +386,7 @@ const InputbarTools = ({
label: t('chat.input.url_context'), label: t('chat.input.url_context'),
component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />, component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />,
condition: condition:
isGeminiModel(model) && (isGeminiModel(model) || isAnthropicModel(model)) &&
(isSupportUrlContextProvider(getProviderByModel(model)) || model.endpoint_type === 'gemini') (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 { LoadingIcon } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup' import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
import Selector from '@renderer/components/Selector'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models' 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 { useTheme } from '@renderer/context/ThemeProvider'
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider' import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { isVertexProvider } from '@renderer/hooks/useVertexAI'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings' import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList' 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 { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { updateWebSearchProvider } from '@renderer/store/websearch' 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 { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
import { import {
formatApiHost, formatApiHost,
formatApiKeys, formatApiKeys,
formatAzureOpenAIApiHost,
formatVertexApiHost,
getFancyProviderName, getFancyProviderName,
isAnthropicProvider, validateApiHost
isOpenAIProvider
} from '@renderer/utils' } from '@renderer/utils'
import { formatErrorMessage } from '@renderer/utils/error' import { formatErrorMessage } from '@renderer/utils/error'
import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd' import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd'
@ -63,7 +75,9 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [
SystemProviderIds.dashscope, SystemProviderIds.dashscope,
SystemProviderIds.modelscope, SystemProviderIds.modelscope,
SystemProviderIds.aihubmix, SystemProviderIds.aihubmix,
SystemProviderIds.grok SystemProviderIds.grok,
SystemProviderIds.cherryin,
SystemProviderIds.longcat
] as const ] as const
type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number] 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) return ANTHROPIC_COMPATIBLE_PROVIDER_ID_SET.has(id)
} }
type HostField = 'apiHost' | 'anthropicApiHost'
const ProviderSetting: FC<Props> = ({ providerId }) => { const ProviderSetting: FC<Props> = ({ providerId }) => {
const { provider, updateProvider, models } = useProvider(providerId) const { provider, updateProvider, models } = useProvider(providerId)
const allProviders = useAllProviders() const allProviders = useAllProviders()
@ -79,19 +95,23 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const [apiHost, setApiHost] = useState(provider.apiHost) const [apiHost, setApiHost] = useState(provider.apiHost)
const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost) const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost)
const [apiVersion, setApiVersion] = useState(provider.apiVersion) const [apiVersion, setApiVersion] = useState(provider.apiVersion)
const [activeHostField, setActiveHostField] = useState<HostField>('apiHost')
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai' const isAzureOpenAI = isAzureOpenAIProvider(provider)
const isDmxapi = provider.id === 'dmxapi' 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 providerConfig = PROVIDER_URLS[provider.id]
const officialWebsite = providerConfig?.websites?.official const officialWebsite = providerConfig?.websites?.official
const apiKeyWebsite = providerConfig?.websites?.apiKey const apiKeyWebsite = providerConfig?.websites?.apiKey
const configedApiHost = providerConfig?.api?.url const configuredApiHost = providerConfig?.api?.url
const fancyProviderName = getFancyProviderName(provider) const fancyProviderName = getFancyProviderName(provider)
@ -151,7 +171,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
) )
const onUpdateApiHost = () => { 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 }) updateProvider({ apiHost })
} else { } else {
setApiHost(provider.apiHost) setApiHost(provider.apiHost)
@ -238,27 +263,46 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
} }
} }
const onReset = () => { const onReset = useCallback(() => {
setApiHost(configedApiHost) setApiHost(configuredApiHost)
updateProvider({ apiHost: configedApiHost }) updateProvider({ apiHost: configuredApiHost })
} }, [configuredApiHost, updateProvider])
const isApiHostResettable = useMemo(() => {
return !isEmpty(configuredApiHost) && apiHost !== configuredApiHost
}, [configuredApiHost, apiHost])
const hostPreview = () => { const hostPreview = () => {
if (apiHost.endsWith('#')) { if (apiHost.endsWith('#')) {
return apiHost.replace('#', '') 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') { if (isAzureOpenAIProvider(provider)) {
return formatApiHost(apiHost) + 'openai/v1' 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') { if (isAnthropicProvider(provider)) {
return formatApiHost(apiHost) + 'messages' 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 连通性检查状态指示器,目前仅在失败时显示 // API key 连通性检查状态指示器,目前仅在失败时显示
@ -286,31 +330,44 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
}, [provider.anthropicApiHost]) }, [provider.anthropicApiHost])
const canConfigureAnthropicHost = useMemo(() => { const canConfigureAnthropicHost = useMemo(() => {
if (isNewApiProvider(provider)) {
return true
}
return ( return (
provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id) provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id)
) )
}, [provider]) }, [provider])
const anthropicHostPreview = useMemo(() => { const anthropicHostPreview = useMemo(() => {
const rawHost = (anthropicApiHost ?? provider.anthropicApiHost)?.trim() const rawHost = anthropicApiHost ?? provider.anthropicApiHost
if (!rawHost) { const normalizedHost = formatApiHost(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(/\/$/, '')
}
return `${normalizedHost}/messages` return `${normalizedHost}/messages`
}, [anthropicApiHost, provider.anthropicApiHost]) }, [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' const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
return ( return (
@ -366,6 +423,8 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
</> </>
)} )}
{!hideApiInput && !isAnthropicOAuth() && ( {!hideApiInput && !isAnthropicOAuth() && (
<>
{!hideApiKeyInput && (
<> <>
<SettingSubtitle <SettingSubtitle
style={{ style={{
@ -415,18 +474,31 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
</HStack> </HStack>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText> <SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow> </SettingHelpTextRow>
{!isDmxapi && !isAnthropicOAuth() && ( </>
)}
{!isDmxapi && (
<> <>
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Tooltip title={t('settings.provider.api_host_tooltip')} mouseEnterDelay={0.3}> <Tooltip title={hostSelectorTooltip} mouseEnterDelay={0.3}>
<SubtitleLabel>{t('settings.provider.api_host')}</SubtitleLabel> <Selector
size={14}
value={activeHostField}
onChange={(value) => setActiveHostField(value as HostField)}
options={hostSelectorOptions}
style={{ paddingLeft: 1, fontWeight: 'bold' }}
placement="bottomLeft"
/>
</Tooltip> </Tooltip>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Button <Button
type="text" type="text"
onClick={() => CustomHeaderPopup.show({ provider })} onClick={() => CustomHeaderPopup.show({ provider })}
icon={<Settings2 size={16} />} icon={<Settings2 size={16} />}
/> />
</div>
</SettingSubtitle> </SettingSubtitle>
{activeHostField === 'apiHost' && (
<>
<Space.Compact style={{ width: '100%', marginTop: 5 }}> <Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input <Input
value={apiHost} value={apiHost}
@ -434,38 +506,40 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
onChange={(e) => setApiHost(e.target.value)} onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost} onBlur={onUpdateApiHost}
/> />
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && ( {isApiHostResettable && (
<Button danger onClick={onReset}> <Button danger onClick={onReset}>
{t('settings.provider.api.url.reset')} {t('settings.provider.api.url.reset')}
</Button> </Button>
)} )}
</Space.Compact> </Space.Compact>
{isVertexProvider(provider) && (
{(isOpenAIProvider(provider) || isAnthropicProvider(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' }}> <SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText <SettingHelpText
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}> style={{
marginLeft: 6,
marginRight: '1em',
whiteSpace: 'break-spaces',
wordBreak: 'break-all'
}}>
{t('settings.provider.api_host_preview', { url: hostPreview() })} {t('settings.provider.api_host_preview', { url: hostPreview() })}
</SettingHelpText> </SettingHelpText>
<SettingHelpText style={{ minWidth: 'fit-content' }}>
{t('settings.provider.api.url.tip')}
</SettingHelpText>
</SettingHelpTextRow> </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 }}> <Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input <Input
value={anthropicApiHost ?? ''} value={anthropicApiHost ?? ''}
@ -480,9 +554,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
url: anthropicHostPreview || '—' url: anthropicHostPreview || '—'
})} })}
</SettingHelpText> </SettingHelpText>
<SettingHelpText style={{ marginLeft: 6 }}>
{t('settings.provider.anthropic_api_host_tip')}
</SettingHelpText>
</SettingHelpTextRow> </SettingHelpTextRow>
</> </>
)} )}
@ -512,21 +583,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
{provider.id === 'gpustack' && <GPUStackSettings />} {provider.id === 'gpustack' && <GPUStackSettings />}
{provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />} {provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />}
{provider.id === 'aws-bedrock' && <AwsBedrockSettings />} {provider.id === 'aws-bedrock' && <AwsBedrockSettings />}
{provider.id === 'vertexai' && <VertexAISettings providerId={provider.id} />} {provider.id === 'vertexai' && <VertexAISettings />}
<ModelList providerId={provider.id} /> <ModelList providerId={provider.id} />
</SettingContainer> </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` const ProviderName = styled.span`
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;

View File

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

View File

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

View File

@ -2716,7 +2716,6 @@ const migrateConfig = {
} }
}, },
'166': (state: RootState) => { '166': (state: RootState) => {
// added after 1.6.5 and 1.7.0-beta.2
try { try {
if (state.assistants.presets === undefined) { if (state.assistants.presets === undefined) {
state.assistants.presets = [] state.assistants.presets = []
@ -2733,6 +2732,18 @@ const migrateConfig = {
if (dashscopeProvider) { if (dashscopeProvider) {
dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' 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 return state
} catch (error) { } catch (error) {
logger.error('migrate 166 error', error as Error) logger.error('migrate 166 error', error as Error)

View File

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

View File

@ -6,7 +6,6 @@ export const ProviderTypeSchema = z.enum([
'openai-response', 'openai-response',
'anthropic', 'anthropic',
'gemini', 'gemini',
'qwenlm',
'azure-openai', 'azure-openai',
'vertexai', 'vertexai',
'mistral', 'mistral',
@ -37,6 +36,8 @@ export type ProviderApiOptions = {
isSupportServiceTier?: boolean isSupportServiceTier?: boolean
/** 是否不支持 enable_thinking 参数 */ /** 是否不支持 enable_thinking 参数 */
isNotSupportEnableThinking?: boolean isNotSupportEnableThinking?: boolean
/** 是否不支持 APIVersion */
isNotSupportAPIVersion?: boolean
} }
export const OpenAIServiceTiers = { export const OpenAIServiceTiers = {
@ -187,6 +188,11 @@ export type VertexProvider = Provider & {
location: string location: string
} }
export type AzureOpenAIProvider = Provider & {
type: 'azure-openai'
apiVersion: string
}
/** /**
* 使`provider.isSystem` * 使`provider.isSystem`
* @param provider - Provider对象 * @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('api', () => {
describe('formatApiHost', () => { describe('formatApiHost', () => {
it('should return original host when it ends with a slash', () => { it('returns empty string for falsy host', () => {
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', () => {
expect(formatApiHost('')).toBe('') 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']) 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 { runAsyncFunction } from '../index'
import { hasPath, isValidProxyUrl, removeQuotes, removeSpecialCharacters } from '../index' import { hasPath, isValidProxyUrl, removeQuotes, removeSpecialCharacters } from '../index'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
describe('Unclassified Utils', () => { describe('Unclassified Utils', () => {
describe('runAsyncFunction', () => { describe('runAsyncFunction', () => {
it('should execute async function', async () => { 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' import { isJSON, parseJSON } from '../index'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
describe('json', () => { describe('json', () => {
describe('isJSON', () => { describe('isJSON', () => {
it('should return true for valid JSON string', () => { 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 * API key
* *
@ -9,30 +13,166 @@ export function formatApiKeys(value: string): string {
} }
/** /**
* API * host path /v1/v2beta
* *
* host `/v1/` * @param host - host path
* - host `/` `volces.com/api/v3` * @returns path true false
* -
*
* @param {string} host - API
* @param {string} apiVersion - API
* @returns {string} API
*/ */
export function formatApiHost(host: string, apiVersion: string = 'v1'): string { export function hasAPIVersion(host?: string): boolean {
if (!host) { 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 '' return ''
} }
const forceUseOriginalHost = () => { if (normalizedHost.endsWith('#') || !isSupportedAPIVerion || hasAPIVersion(normalizedHost)) {
if (host.endsWith('/')) { return normalizedHost
}
return `${normalizedHost}/${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 return true
} }
try {
return host.endsWith('volces.com/api/v3') const url = new URL(trim(apiHost))
// 验证协议是否为 http 或 https
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return false
}
return true
} catch {
return false
} }
return forceUseOriginalHost() ? host : `${host}/${apiVersion}/`
} }
/** /**

View File

@ -1,5 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { Model, ModelType, Provider } from '@renderer/types' import { Model, ModelType } from '@renderer/types'
import { ModalFuncProps } from 'antd' import { ModalFuncProps } from 'antd'
import { isEqual } from 'lodash' import { isEqual } from 'lodash'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -196,19 +196,6 @@ export function getMcpConfigSampleFromReadme(readme: string): Record<string, any
return null 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 * @param {Model} model

137
yarn.lock
View File

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