mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
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:
parent
5790c12011
commit
e0a2ed0481
@ -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
|
||||
26
.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch
vendored
Normal file
26
.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch
vendored
Normal 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
|
||||
76
.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch
vendored
Normal file
76
.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch
vendored
Normal 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" });
|
||||
}
|
||||
@ -103,8 +103,8 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.35",
|
||||
"@ai-sdk/google-vertex": "^3.0.40",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.42",
|
||||
"@ai-sdk/google-vertex": "^3.0.48",
|
||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
|
||||
"@ai-sdk/mistral": "^2.0.19",
|
||||
"@ai-sdk/perplexity": "^2.0.13",
|
||||
@ -227,7 +227,7 @@
|
||||
"@viz-js/lang-dot": "^1.0.5",
|
||||
"@viz-js/viz": "^3.14.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"ai": "^5.0.68",
|
||||
"ai": "^5.0.76",
|
||||
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
@ -390,7 +390,8 @@
|
||||
"undici": "6.21.2",
|
||||
"vite": "npm:rolldown-vite@7.1.5",
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"@ai-sdk/google@npm:2.0.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch",
|
||||
"@ai-sdk/google@npm:2.0.23": "patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch",
|
||||
"@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
|
||||
"@img/sharp-darwin-arm64": "0.34.3",
|
||||
"@img/sharp-darwin-x64": "0.34.3",
|
||||
"@img/sharp-linux-arm": "0.34.3",
|
||||
|
||||
@ -36,10 +36,10 @@
|
||||
"ai": "^5.0.26"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.27",
|
||||
"@ai-sdk/azure": "^2.0.49",
|
||||
"@ai-sdk/anthropic": "^2.0.32",
|
||||
"@ai-sdk/azure": "^2.0.53",
|
||||
"@ai-sdk/deepseek": "^1.0.23",
|
||||
"@ai-sdk/openai": "^2.0.48",
|
||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
|
||||
"@ai-sdk/openai-compatible": "^1.0.22",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@ai-sdk/provider-utils": "^3.0.12",
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
|
||||
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
|
||||
import { ApiClient } from '@types'
|
||||
|
||||
import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
@ -9,7 +8,7 @@ import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
export default class EmbeddingsFactory {
|
||||
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
|
||||
const batchSize = 10
|
||||
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
|
||||
const { model, provider, apiKey, baseURL } = embedApiClient
|
||||
if (provider === 'voyageai') {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
@ -38,16 +37,7 @@ export default class EmbeddingsFactory {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (apiVersion !== undefined) {
|
||||
return new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIEndpoint: baseURL,
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
}
|
||||
// NOTE: Azure OpenAI 也走 OpenAIEmbeddings, baseURL是https://xxxx.openai.azure.com/openai/v1
|
||||
return new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
|
||||
@ -6,7 +6,14 @@
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||
import { BaseTool, MCPTool, MCPToolResponse, NormalToolResponse } from '@renderer/types'
|
||||
import {
|
||||
BaseTool,
|
||||
MCPCallToolResponse,
|
||||
MCPTool,
|
||||
MCPToolResponse,
|
||||
MCPToolResultContent,
|
||||
NormalToolResponse
|
||||
} from '@renderer/types'
|
||||
import { Chunk, ChunkType } from '@renderer/types/chunk'
|
||||
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
|
||||
|
||||
@ -254,6 +261,7 @@ export class ToolCallChunkHandler {
|
||||
type: 'tool-result'
|
||||
} & TypedToolResult<ToolSet>
|
||||
): void {
|
||||
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
|
||||
const { toolCallId, output, input } = chunk
|
||||
|
||||
if (!toolCallId) {
|
||||
@ -299,12 +307,7 @@ export class ToolCallChunkHandler {
|
||||
responses: [toolResponse]
|
||||
})
|
||||
|
||||
const images: string[] = []
|
||||
for (const content of toolResponse.response?.content || []) {
|
||||
if (content.type === 'image' && content.data) {
|
||||
images.push(`data:${content.mimeType};base64,${content.data}`)
|
||||
}
|
||||
}
|
||||
const images = extractImagesFromToolOutput(toolResponse.response)
|
||||
|
||||
if (images.length) {
|
||||
this.onChunk({
|
||||
@ -351,3 +354,41 @@ export class ToolCallChunkHandler {
|
||||
}
|
||||
|
||||
export const addActiveToolCall = ToolCallChunkHandler.addActiveToolCall.bind(ToolCallChunkHandler)
|
||||
|
||||
function extractImagesFromToolOutput(output: unknown): string[] {
|
||||
if (!output) {
|
||||
return []
|
||||
}
|
||||
|
||||
const contents: unknown[] = []
|
||||
|
||||
if (isMcpCallToolResponse(output)) {
|
||||
contents.push(...output.content)
|
||||
} else if (Array.isArray(output)) {
|
||||
contents.push(...output)
|
||||
} else if (hasContentArray(output)) {
|
||||
contents.push(...output.content)
|
||||
}
|
||||
|
||||
return contents
|
||||
.filter(isMcpImageContent)
|
||||
.map((content) => `data:${content.mimeType ?? 'image/png'};base64,${content.data}`)
|
||||
}
|
||||
|
||||
function isMcpCallToolResponse(value: unknown): value is MCPCallToolResponse {
|
||||
return typeof value === 'object' && value !== null && Array.isArray((value as MCPCallToolResponse).content)
|
||||
}
|
||||
|
||||
function hasContentArray(value: unknown): value is { content: unknown[] } {
|
||||
return typeof value === 'object' && value !== null && Array.isArray((value as { content?: unknown }).content)
|
||||
}
|
||||
|
||||
function isMcpImageContent(content: unknown): content is MCPToolResultContent & { data: string } {
|
||||
if (typeof content !== 'object' || content === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const resultContent = content as MCPToolResultContent
|
||||
|
||||
return resultContent.type === 'image' && typeof resultContent.data === 'string'
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
|
||||
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
|
||||
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
|
||||
|
||||
@ -77,7 +78,7 @@ export default class ModernAiProvider {
|
||||
return this.actualProvider
|
||||
}
|
||||
|
||||
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) {
|
||||
public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
|
||||
// 检查model是否存在
|
||||
if (!this.model) {
|
||||
throw new Error('Model is required for completions. Please use constructor with model parameter.')
|
||||
@ -85,7 +86,10 @@ export default class ModernAiProvider {
|
||||
|
||||
// 每次请求时重新生成配置以确保API key轮换生效
|
||||
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
|
||||
|
||||
logger.debug('Generated provider config for completions', this.config)
|
||||
if (SUPPORTED_IMAGE_ENDPOINT_LIST.includes(this.config.options.endpoint)) {
|
||||
providerConfig.isImageGenerationEndpoint = true
|
||||
}
|
||||
// 准备特殊配置
|
||||
await prepareSpecialProviderConfig(this.actualProvider, this.config)
|
||||
|
||||
@ -96,13 +100,13 @@ export default class ModernAiProvider {
|
||||
|
||||
// 提前构建中间件
|
||||
const middlewares = buildAiSdkMiddlewares({
|
||||
...config,
|
||||
...providerConfig,
|
||||
provider: this.actualProvider,
|
||||
assistant: config.assistant
|
||||
assistant: providerConfig.assistant
|
||||
})
|
||||
logger.debug('Built middlewares in completions', {
|
||||
middlewareCount: middlewares.length,
|
||||
isImageGeneration: config.isImageGenerationEndpoint
|
||||
isImageGeneration: providerConfig.isImageGenerationEndpoint
|
||||
})
|
||||
if (!this.localProvider) {
|
||||
throw new Error('Local provider not created')
|
||||
@ -110,7 +114,7 @@ export default class ModernAiProvider {
|
||||
|
||||
// 根据endpoint类型创建对应的模型
|
||||
let model: AiSdkModel | undefined
|
||||
if (config.isImageGenerationEndpoint) {
|
||||
if (providerConfig.isImageGenerationEndpoint) {
|
||||
model = this.localProvider.imageModel(modelId)
|
||||
} else {
|
||||
model = this.localProvider.languageModel(modelId)
|
||||
@ -126,15 +130,15 @@ export default class ModernAiProvider {
|
||||
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
|
||||
}
|
||||
|
||||
if (config.topicId && getEnableDeveloperMode()) {
|
||||
if (providerConfig.topicId && getEnableDeveloperMode()) {
|
||||
// TypeScript类型窄化:确保topicId是string类型
|
||||
const traceConfig = {
|
||||
...config,
|
||||
topicId: config.topicId
|
||||
...providerConfig,
|
||||
topicId: providerConfig.topicId
|
||||
}
|
||||
return await this._completionsForTrace(model, params, traceConfig)
|
||||
} else {
|
||||
return await this._completionsOrImageGeneration(model, params, config)
|
||||
return await this._completionsOrImageGeneration(model, params, providerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Provider } from '@renderer/types'
|
||||
import { isOpenAIProvider } from '@renderer/utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
|
||||
@ -202,36 +201,4 @@ describe('ApiClientFactory', () => {
|
||||
expect(client).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOpenAIProvider', () => {
|
||||
it('should return true for openai type', () => {
|
||||
const provider = createTestProvider('openai', 'openai')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for azure-openai type', () => {
|
||||
const provider = createTestProvider('azure-openai', 'azure-openai')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for unknown type (fallback to OpenAI)', () => {
|
||||
const provider = createTestProvider('unknown', 'unknown')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for vertexai type', () => {
|
||||
const provider = createTestProvider('vertex', 'vertexai')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for anthropic type', () => {
|
||||
const provider = createTestProvider('anthropic', 'anthropic')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for gemini type', () => {
|
||||
const provider = createTestProvider('gemini', 'gemini')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { AiPlugin } from '@cherrystudio/ai-core'
|
||||
import { createPromptToolUsePlugin, googleToolsPlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { createPromptToolUsePlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { loggerService } from '@logger'
|
||||
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
|
||||
import { Assistant } from '@renderer/types'
|
||||
@ -68,9 +68,9 @@ export function buildPlugins(
|
||||
)
|
||||
}
|
||||
|
||||
if (middlewareConfig.enableUrlContext) {
|
||||
plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
}
|
||||
// if (middlewareConfig.enableUrlContext && middlewareConfig.) {
|
||||
// plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
// }
|
||||
|
||||
logger.debug(
|
||||
'Final plugin list:',
|
||||
|
||||
@ -114,7 +114,7 @@ export async function handleGeminiFileUpload(file: FileMetadata, model: Model):
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理OpenAI大文件上传
|
||||
* 处理OpenAI兼容大文件上传
|
||||
*/
|
||||
export async function handleOpenAILargeFileUpload(
|
||||
file: FileMetadata,
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
* 构建AI SDK的流式和非流式参数
|
||||
*/
|
||||
|
||||
import { anthropic } from '@ai-sdk/anthropic'
|
||||
import { google } from '@ai-sdk/google'
|
||||
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
|
||||
import { vertex } from '@ai-sdk/google-vertex/edge'
|
||||
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
@ -97,10 +99,6 @@ export async function buildStreamTextParams(
|
||||
|
||||
let tools = setupToolsConfig(mcpTools)
|
||||
|
||||
// if (webSearchProviderId) {
|
||||
// tools['builtin_web_search'] = webSearchTool(webSearchProviderId)
|
||||
// }
|
||||
|
||||
// 构建真正的 providerOptions
|
||||
const webSearchConfig: CherryWebSearchConfig = {
|
||||
maxResults: store.getState().websearch.maxResults,
|
||||
@ -143,12 +141,34 @@ export async function buildStreamTextParams(
|
||||
}
|
||||
}
|
||||
|
||||
// google-vertex
|
||||
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
|
||||
if (enableUrlContext) {
|
||||
if (!tools) {
|
||||
tools = {}
|
||||
}
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
|
||||
switch (aiSdkProviderId) {
|
||||
case 'google-vertex':
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'google':
|
||||
tools.url_context = google.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'anthropic':
|
||||
case 'google-vertex-anthropic':
|
||||
tools.web_fetch = (
|
||||
aiSdkProviderId === 'anthropic'
|
||||
? anthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
: vertexAnthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
) as ProviderDefinedTool
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 构建基础参数
|
||||
|
||||
@ -32,7 +32,8 @@ const AIHUBMIX_RULES: RuleSet = {
|
||||
match: (model) =>
|
||||
(startsWith('gemini')(model) || startsWith('imagen')(model)) &&
|
||||
!model.id.endsWith('-nothink') &&
|
||||
!model.id.endsWith('-search'),
|
||||
!model.id.endsWith('-search') &&
|
||||
!model.id.includes('embedding'),
|
||||
provider: (provider: Provider) => {
|
||||
return extraProviderConfig({
|
||||
...provider,
|
||||
|
||||
@ -6,26 +6,28 @@ import {
|
||||
type ProviderSettingsMap
|
||||
} from '@cherrystudio/ai-core/provider'
|
||||
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import { isNewApiProvider } from '@renderer/config/providers'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
} from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI'
|
||||
import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import store from '@renderer/store'
|
||||
import { isSystemProvider, type Model, type Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
import { cloneDeep, trim } from 'lodash'
|
||||
import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
|
||||
import { formatApiHost, formatAzureOpenAIApiHost, formatVertexApiHost, routeToEndpoint } from '@renderer/utils/api'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
|
||||
import { COPILOT_DEFAULT_HEADERS } from './constants'
|
||||
import { getAiSdkProviderId } from './factory'
|
||||
|
||||
const logger = loggerService.withContext('ProviderConfigProcessor')
|
||||
|
||||
/**
|
||||
* 获取轮询的API key
|
||||
* 复用legacy架构的多key轮询逻辑
|
||||
@ -56,13 +58,6 @@ function getRotatedApiKey(provider: Provider): string {
|
||||
* 处理特殊provider的转换逻辑
|
||||
*/
|
||||
function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
// if (provider.type === 'vertexai' && !isVertexProvider(provider)) {
|
||||
// if (!isVertexAIConfigured()) {
|
||||
// throw new Error('VertexAI is not configured. Please configure project, location and service account credentials.')
|
||||
// }
|
||||
// return createVertexProvider(provider)
|
||||
// }
|
||||
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
@ -79,43 +74,30 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化provider的API Host
|
||||
* 主要用来对齐AISdk的BaseURL格式
|
||||
* @param provider
|
||||
* @returns
|
||||
*/
|
||||
function formatAnthropicApiHost(host: string): string {
|
||||
const trimmedHost = host?.trim()
|
||||
|
||||
if (!trimmedHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/')) {
|
||||
return trimmedHost
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/v1')) {
|
||||
return `${trimmedHost}/`
|
||||
}
|
||||
|
||||
return formatApiHost(trimmedHost)
|
||||
}
|
||||
|
||||
function formatProviderApiHost(provider: Provider): Provider {
|
||||
const formatted = { ...provider }
|
||||
if (formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost)
|
||||
formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
|
||||
}
|
||||
|
||||
if (formatted.type === 'anthropic') {
|
||||
if (isAnthropicProvider(provider)) {
|
||||
const baseHost = formatted.anthropicApiHost || formatted.apiHost
|
||||
formatted.apiHost = formatAnthropicApiHost(baseHost)
|
||||
formatted.apiHost = formatApiHost(baseHost)
|
||||
if (!formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatted.apiHost
|
||||
}
|
||||
} else if (formatted.id === 'copilot') {
|
||||
const trimmed = trim(formatted.apiHost)
|
||||
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
|
||||
} else if (formatted.type === 'gemini') {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
|
||||
} else if (formatted.id === SystemProviderIds.copilot || formatted.id === SystemProviderIds.github) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else if (isGeminiProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, true, 'v1beta')
|
||||
} else if (isAzureOpenAIProvider(formatted)) {
|
||||
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
|
||||
} else if (isVertexProvider(formatted)) {
|
||||
formatted.apiHost = formatVertexApiHost(formatted)
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
@ -149,15 +131,15 @@ export function providerToAiSdkConfig(
|
||||
options: ProviderSettingsMap[keyof ProviderSettingsMap]
|
||||
} {
|
||||
const aiSdkProviderId = getAiSdkProviderId(actualProvider)
|
||||
logger.debug('providerToAiSdkConfig', { aiSdkProviderId })
|
||||
|
||||
// 构建基础配置
|
||||
const { baseURL, endpoint } = routeToEndpoint(actualProvider.apiHost)
|
||||
const baseConfig = {
|
||||
baseURL: trim(actualProvider.apiHost),
|
||||
baseURL: baseURL,
|
||||
apiKey: getRotatedApiKey(actualProvider)
|
||||
}
|
||||
|
||||
const isCopilotProvider = actualProvider.id === 'copilot'
|
||||
const isCopilotProvider = actualProvider.id === SystemProviderIds.copilot
|
||||
if (isCopilotProvider) {
|
||||
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
|
||||
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
|
||||
@ -178,6 +160,7 @@ export function providerToAiSdkConfig(
|
||||
|
||||
// 处理OpenAI模式
|
||||
const extraOptions: any = {}
|
||||
extraOptions.endpoint = endpoint
|
||||
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
|
||||
extraOptions.mode = 'responses'
|
||||
} else if (aiSdkProviderId === 'openai') {
|
||||
@ -199,13 +182,11 @@ export function providerToAiSdkConfig(
|
||||
}
|
||||
// azure
|
||||
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
|
||||
extraOptions.apiVersion = actualProvider.apiVersion
|
||||
baseConfig.baseURL += '/openai'
|
||||
// extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1,不使用azure endpoint
|
||||
if (actualProvider.apiVersion === 'preview') {
|
||||
extraOptions.mode = 'responses'
|
||||
} else {
|
||||
extraOptions.mode = 'chat'
|
||||
extraOptions.useDeploymentBasedUrls = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,22 +208,7 @@ export function providerToAiSdkConfig(
|
||||
...googleCredentials,
|
||||
privateKey: formatPrivateKey(googleCredentials.privateKey)
|
||||
}
|
||||
// extraOptions.headers = window.api.vertexAI.getAuthHeaders({
|
||||
// projectId: project,
|
||||
// serviceAccount: {
|
||||
// privateKey: googleCredentials.privateKey,
|
||||
// clientEmail: googleCredentials.clientEmail
|
||||
// }
|
||||
// })
|
||||
if (baseConfig.baseURL.endsWith('/v1/')) {
|
||||
baseConfig.baseURL = baseConfig.baseURL.slice(0, -4)
|
||||
} else if (baseConfig.baseURL.endsWith('/v1')) {
|
||||
baseConfig.baseURL = baseConfig.baseURL.slice(0, -3)
|
||||
}
|
||||
|
||||
if (baseConfig.baseURL && !baseConfig.baseURL.includes('publishers/google')) {
|
||||
baseConfig.baseURL = `${baseConfig.baseURL}/v1/projects/${project}/locations/${location}/publishers/google`
|
||||
}
|
||||
baseConfig.baseURL += aiSdkProviderId === 'google-vertex' ? '/publishers/google' : '/publishers/anthropic/models'
|
||||
}
|
||||
|
||||
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
|
||||
|
||||
@ -5,6 +5,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DraggableList } from '../'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// mock @hello-pangea/dnd 组件
|
||||
vi.mock('@hello-pangea/dnd', () => {
|
||||
return {
|
||||
|
||||
@ -3,6 +3,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DraggableVirtualList } from '../'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock 依赖项
|
||||
vi.mock('@hello-pangea/dnd', () => ({
|
||||
__esModule: true,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils'
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils/image'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
/**
|
||||
|
||||
@ -16,6 +16,7 @@ interface BaseSelectorProps<V = string | number> {
|
||||
options: SelectorOption<V>[]
|
||||
placeholder?: string
|
||||
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
|
||||
style?: React.CSSProperties
|
||||
/** 字体大小 */
|
||||
size?: number
|
||||
/** 是否禁用 */
|
||||
@ -43,6 +44,7 @@ const Selector = <V extends string | number>({
|
||||
placement = 'bottomRight',
|
||||
size = 13,
|
||||
placeholder,
|
||||
style,
|
||||
disabled = false,
|
||||
multiple = false
|
||||
}: SelectorProps<V>) => {
|
||||
@ -135,7 +137,7 @@ const Selector = <V extends string | number>({
|
||||
placement={placement}
|
||||
open={open && !disabled}
|
||||
onOpenChange={handleOpenChange}>
|
||||
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
<Label style={style} $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
{label}
|
||||
<LabelIcon size={size + 3} />
|
||||
</Label>
|
||||
|
||||
@ -23,6 +23,16 @@ const mocks = vi.hoisted(() => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock antd components to prevent flaky snapshot tests
|
||||
vi.mock('antd', () => {
|
||||
const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({
|
||||
|
||||
@ -18,6 +18,15 @@ describe('Qwen Model Detection', () => {
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' })
|
||||
}))
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
test('isQwenReasoningModel', () => {
|
||||
expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true)
|
||||
|
||||
@ -2,6 +2,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isLingReasoningModel } from '../models/reasoning'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// FIXME: Idk why it's imported. Maybe circular dependency somewhere
|
||||
vi.mock('@renderer/services/AssistantService.ts', () => ({
|
||||
getDefaultAssistant: () => {
|
||||
|
||||
@ -1741,6 +1741,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
id: 'DeepSeek-R1',
|
||||
provider: 'cephalon',
|
||||
name: 'DeepSeek-R1满血版',
|
||||
capabilities: [{ type: 'reasoning' }],
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
],
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Model, SystemProviderIds } from '@renderer/types'
|
||||
import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils'
|
||||
|
||||
import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers'
|
||||
import { isEmbeddingModel, isRerankModel } from './embedding'
|
||||
import { isAnthropicModel } from './utils'
|
||||
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
|
||||
@ -65,12 +66,16 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
|
||||
// 不管哪个供应商都判断了
|
||||
if (isAnthropicModel(model)) {
|
||||
// bedrock和vertex不支持
|
||||
if (
|
||||
isAnthropicModel(model) &&
|
||||
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
|
||||
) {
|
||||
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
if (provider.type === 'openai-response') {
|
||||
// TODO: 当其他供应商采用Response端点时,这个地方逻辑需要改进
|
||||
if (isOpenAIProvider(provider)) {
|
||||
if (isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
@ -78,11 +83,11 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider.id === 'perplexity') {
|
||||
if (provider.id === SystemProviderIds.perplexity) {
|
||||
return PERPLEXITY_SEARCH_MODELS.includes(modelId)
|
||||
}
|
||||
|
||||
if (provider.id === 'aihubmix') {
|
||||
if (provider.id === SystemProviderIds.aihubmix) {
|
||||
// modelId 不以-search结尾
|
||||
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
|
||||
return true
|
||||
@ -95,13 +100,13 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider?.type === 'openai') {
|
||||
if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
|
||||
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.id === 'gemini' || provider?.type === 'gemini' || provider.type === 'vertexai') {
|
||||
if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) {
|
||||
return GEMINI_SEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import {
|
||||
AtLeast,
|
||||
AzureOpenAIProvider,
|
||||
isSystemProvider,
|
||||
OpenAIServiceTiers,
|
||||
Provider,
|
||||
@ -355,7 +356,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
name: 'VertexAI',
|
||||
type: 'vertexai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aiplatform.googleapis.com',
|
||||
apiHost: '',
|
||||
models: SYSTEM_MODELS.vertexai,
|
||||
isSystem: true,
|
||||
enabled: false,
|
||||
@ -1295,7 +1296,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
},
|
||||
vertexai: {
|
||||
api: {
|
||||
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview'
|
||||
url: ''
|
||||
},
|
||||
websites: {
|
||||
official: 'https://cloud.google.com/vertex-ai',
|
||||
@ -1375,7 +1376,8 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
|
||||
'baichuan',
|
||||
'minimax',
|
||||
'xirang',
|
||||
'poe'
|
||||
'poe',
|
||||
'cephalon'
|
||||
] as const satisfies SystemProviderId[]
|
||||
|
||||
/**
|
||||
@ -1440,10 +1442,15 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
|
||||
)
|
||||
}
|
||||
|
||||
const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[]
|
||||
const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
|
||||
'gemini',
|
||||
'vertexai',
|
||||
'anthropic',
|
||||
'new-api'
|
||||
] as const satisfies ProviderType[]
|
||||
|
||||
export const isSupportUrlContextProvider = (provider: Provider) => {
|
||||
return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
return SUPPORT_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
}
|
||||
|
||||
const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[]
|
||||
@ -1456,3 +1463,37 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
|
||||
export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
* @returns {boolean} 是否为 OpenAI 兼容提供商
|
||||
*/
|
||||
export function isOpenAICompatibleProvider(provider: Provider): boolean {
|
||||
return ['openai', 'new-api', 'mistral'].includes(provider.type)
|
||||
}
|
||||
|
||||
export function isAzureOpenAIProvider(provider: Provider): provider is AzureOpenAIProvider {
|
||||
return provider.type === 'azure-openai'
|
||||
}
|
||||
|
||||
export function isOpenAIProvider(provider: Provider): boolean {
|
||||
return provider.type === 'openai-response'
|
||||
}
|
||||
|
||||
export function isAnthropicProvider(provider: Provider): boolean {
|
||||
return provider.type === 'anthropic'
|
||||
}
|
||||
|
||||
export function isGeminiProvider(provider: Provider): boolean {
|
||||
return provider.type === 'gemini'
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
|
||||
|
||||
export const isSupportAPIVersionProvider = (provider: Provider) => {
|
||||
if (isSystemProvider(provider)) {
|
||||
return !NOT_SUPPORT_API_VERSION_PROVIDERS.some((pid) => pid === provider.id)
|
||||
}
|
||||
return provider.apiOptions?.isNotSupportAPIVersion !== false
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export function getVertexAIServiceAccount() {
|
||||
* 类型守卫:检查 Provider 是否为 VertexProvider
|
||||
*/
|
||||
export function isVertexProvider(provider: Provider): provider is VertexProvider {
|
||||
return provider.type === 'vertexai' && 'googleCredentials' in provider
|
||||
return provider.type === 'vertexai'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API Host",
|
||||
"anthropic_api_host_preview": "Anthropic preview: {{url}}",
|
||||
"anthropic_api_host_tip": "Only configure this when your provider exposes an Anthropic-compatible endpoint. Ending with / ignores v1, ending with # forces use of input address.",
|
||||
"anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Preview: {{url}}",
|
||||
"reset": "Reset",
|
||||
"tip": "Ending with / ignores v1, ending with # forces use of input address"
|
||||
"tip": "ending with # forces use of input address"
|
||||
}
|
||||
},
|
||||
"api_host": "API Host",
|
||||
"api_host_no_valid": "API address is invalid",
|
||||
"api_host_preview": "Preview: {{url}}",
|
||||
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 地址",
|
||||
"anthropic_api_host_preview": "Anthropic 预览:{{url}}",
|
||||
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1,以 # 结尾则强制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "预览: {{url}}",
|
||||
"reset": "重置",
|
||||
"tip": "/ 结尾忽略 v1 版本,# 结尾强制使用输入地址"
|
||||
"tip": "# 结尾强制使用输入地址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 地址",
|
||||
"api_host_no_valid": "API 地址不合法",
|
||||
"api_host_preview": "预览:{{url}}",
|
||||
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 主機地址",
|
||||
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
|
||||
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1,以 # 結尾則強制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "預覽:{{url}}",
|
||||
"reset": "重設",
|
||||
"tip": "/ 結尾忽略 v1 版本,# 結尾強制使用輸入位址"
|
||||
"tip": "# 結尾強制使用輸入位址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 主機地址",
|
||||
"api_host_no_valid": "API 位址不合法",
|
||||
"api_host_preview": "預覽:{{url}}",
|
||||
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API-Adresse",
|
||||
"anthropic_api_host_preview": "Anthropic-Vorschau: {{url}}",
|
||||
"anthropic_api_host_tip": "Nur bei Anbietern, die ein Anthropic-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
|
||||
"anthropic_api_host_tooltip": "Nur bei Anbietern, die ein Claude-kompatibles Basis-Endpunkt anbieten.",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Vorschau: {{url}}",
|
||||
"reset": "Zurücksetzen",
|
||||
"tip": "/ am Ende ignorieren v1-Version, # am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
"tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
}
|
||||
},
|
||||
"api_host": "API-Adresse",
|
||||
"api_host_no_valid": "API-Adresse ist ungültig",
|
||||
"api_host_preview": "Vorschau: {{url}}",
|
||||
"api_host_tooltip": "Nur bei Anbietern, die ein OpenAI-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Διεύθυνση API Anthropic",
|
||||
"anthropic_api_host_preview": "Προεπισκόπηση Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Συμπληρώστε μόνο εάν ο πάροχος προσφέρει συμβατή με Anthropic διεύθυνση. Η λήξη με / αγνοεί το v1 που προστίθεται αυτόματα, η λήξη με # επιβάλλει τη χρήση της αρχικής διεύθυνσης.",
|
||||
"anthropic_api_host_tooltip": "Συμπληρώστε μόνο όταν ο πάροχος παρέχει βασική διεύθυνση συμβατή με Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Προεπισκόπηση: {{url}}",
|
||||
"reset": "Επαναφορά",
|
||||
"tip": "/τέλος αγνόηση v1 έκδοσης, #τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
|
||||
"tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
|
||||
}
|
||||
},
|
||||
"api_host": "Διεύθυνση API",
|
||||
"api_host_no_valid": "Η διεύθυνση API δεν είναι έγκυρη",
|
||||
"api_host_preview": "Προεπισκόπηση: {{url}}",
|
||||
"api_host_tooltip": "Αντικατάσταση μόνο όταν ο πάροχος απαιτεί προσαρμοσμένη διεύθυνση συμβατή με OpenAI.",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Dirección API de Anthropic",
|
||||
"anthropic_api_host_preview": "Vista previa de Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Rellenar solo si el proveedor ofrece una dirección compatible con Anthropic. Terminar con / ignora el v1 añadido automáticamente, terminar con # fuerza el uso de la dirección original.",
|
||||
"anthropic_api_host_tooltip": "Rellenar solo cuando el proveedor proporcione una dirección base compatible con Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Vista previa: {{url}}",
|
||||
"reset": "Restablecer",
|
||||
"tip": "Ignorar v1 al final con /, forzar uso de dirección de entrada con # al final"
|
||||
"tip": "forzar uso de dirección de entrada con # al final"
|
||||
}
|
||||
},
|
||||
"api_host": "Dirección API",
|
||||
"api_host_no_valid": "La dirección de la API no es válida",
|
||||
"api_host_preview": "Vista previa: {{url}}",
|
||||
"api_host_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Adresse API Anthropic",
|
||||
"anthropic_api_host_preview": "Aperçu Anthropic : {{url}}",
|
||||
"anthropic_api_host_tip": "Remplir seulement si le fournisseur propose une adresse compatible Anthropic. Se terminant par / ignore le v1 ajouté automatiquement, se terminant par # force l'utilisation de l'adresse originale.",
|
||||
"anthropic_api_host_tooltip": "Remplir seulement lorsque le fournisseur propose une adresse de base compatible Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Aperçu : {{url}}",
|
||||
"reset": "Réinitialiser",
|
||||
"tip": "Ignorer la version v1 si terminé par /, forcer l'utilisation de l'adresse d'entrée si terminé par #"
|
||||
"tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #"
|
||||
}
|
||||
},
|
||||
"api_host": "Adresse API",
|
||||
"api_host_no_valid": "Adresse API invalide",
|
||||
"api_host_preview": "Aperçu : {{url}}",
|
||||
"api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic APIアドレス",
|
||||
"anthropic_api_host_preview": "Anthropic プレビュー:{{url}}",
|
||||
"anthropic_api_host_tip": "サービスプロバイダーがAnthropic互換のアドレスを提供する場合のみ入力してください。/で終わる場合は自動追加されるv1を無視し、#で終わる場合は元のアドレスを強制的に使用します。",
|
||||
"anthropic_api_host_tooltip": "サービスプロバイダーがClaude互換のベースアドレスを提供する場合のみ入力してください。",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "プレビュー: {{url}}",
|
||||
"reset": "リセット",
|
||||
"tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します"
|
||||
"tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
|
||||
}
|
||||
},
|
||||
"api_host": "APIホスト",
|
||||
"api_host_no_valid": "APIアドレスが無効です",
|
||||
"api_host_preview": "プレビュー:{{url}}",
|
||||
"api_host_tooltip": "サービスプロバイダーがカスタムOpenAI互換アドレスを必要とする場合のみ上書きしてください。",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Endereço da API Anthropic",
|
||||
"anthropic_api_host_preview": "Pré-visualização Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Preencher apenas se o fornecedor oferecer um endereço compatível com Anthropic. Terminar com / ignora o v1 adicionado automaticamente, terminar com # força o uso do endereço original.",
|
||||
"anthropic_api_host_tooltip": "Preencher apenas quando o fornecedor fornece um endereço base compatível com Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Pré-visualização: {{url}}",
|
||||
"reset": "Redefinir",
|
||||
"tip": "Ignorar v1 na versão finalizada com /, usar endereço de entrada forçado se terminar com #"
|
||||
"tip": "e forçar o uso do endereço original quando terminar com '#'"
|
||||
}
|
||||
},
|
||||
"api_host": "Endereço API",
|
||||
"api_host_no_valid": "O endereço da API é inválido",
|
||||
"api_host_preview": "Pré-visualização: {{url}}",
|
||||
"api_host_tooltip": "Substituir apenas quando o fornecedor necessita de um endereço compatível com OpenAI personalizado.",
|
||||
"api_key": {
|
||||
|
||||
@ -4148,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Адрес API Anthropic",
|
||||
"anthropic_api_host_preview": "Предпросмотр Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Заполняйте только если провайдер предоставляет совместимый с Anthropic адрес. Окончание на / игнорирует автоматически добавляемое v1, окончание на # принудительно использует оригинальный адрес.",
|
||||
"anthropic_api_host_tooltip": "Заполняйте только когда провайдер предоставляет базовый адрес, совместимый с Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@ -4193,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Предпросмотр: {{url}}",
|
||||
"reset": "Сброс",
|
||||
"tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес"
|
||||
"tip": "заканчивая на # принудительно использует введенный адрес"
|
||||
}
|
||||
},
|
||||
"api_host": "Хост API",
|
||||
"api_host_no_valid": "Недопустимый адрес API",
|
||||
"api_host_preview": "Предпросмотр: {{url}}",
|
||||
"api_host_tooltip": "Переопределяйте только когда провайдер требует пользовательский адрес, совместимый с OpenAI.",
|
||||
"api_key": {
|
||||
|
||||
@ -3,6 +3,7 @@ import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { QuickPanelListItem } from '@renderer/components/QuickPanel'
|
||||
import {
|
||||
isAnthropicModel,
|
||||
isGeminiModel,
|
||||
isGenerateImageModel,
|
||||
isMandatoryWebSearchModel,
|
||||
@ -385,7 +386,7 @@ const InputbarTools = ({
|
||||
label: t('chat.input.url_context'),
|
||||
component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />,
|
||||
condition:
|
||||
isGeminiModel(model) &&
|
||||
(isGeminiModel(model) || isAnthropicModel(model)) &&
|
||||
(isSupportUrlContextProvider(getProviderByModel(model)) || model.endpoint_type === 'gemini')
|
||||
},
|
||||
{
|
||||
|
||||
@ -2,11 +2,22 @@ import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
isOpenAIProvider,
|
||||
isSupportAPIVersionProvider,
|
||||
PROVIDER_URLS
|
||||
} from '@renderer/config/providers'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { isVertexProvider } from '@renderer/hooks/useVertexAI'
|
||||
import i18n from '@renderer/i18n'
|
||||
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
|
||||
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
|
||||
@ -14,14 +25,15 @@ import { checkApi } from '@renderer/services/ApiService'
|
||||
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateWebSearchProvider } from '@renderer/store/websearch'
|
||||
import { isSystemProvider, isSystemProviderId, SystemProviderIds } from '@renderer/types'
|
||||
import { isSystemProvider, isSystemProviderId, SystemProviderId, SystemProviderIds } from '@renderer/types'
|
||||
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
|
||||
import {
|
||||
formatApiHost,
|
||||
formatApiKeys,
|
||||
formatAzureOpenAIApiHost,
|
||||
formatVertexApiHost,
|
||||
getFancyProviderName,
|
||||
isAnthropicProvider,
|
||||
isOpenAIProvider
|
||||
validateApiHost
|
||||
} from '@renderer/utils'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd'
|
||||
@ -63,7 +75,9 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [
|
||||
SystemProviderIds.dashscope,
|
||||
SystemProviderIds.modelscope,
|
||||
SystemProviderIds.aihubmix,
|
||||
SystemProviderIds.grok
|
||||
SystemProviderIds.grok,
|
||||
SystemProviderIds.cherryin,
|
||||
SystemProviderIds.longcat
|
||||
] as const
|
||||
type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number]
|
||||
|
||||
@ -72,6 +86,8 @@ const isAnthropicCompatibleProviderId = (id: string): id is AnthropicCompatibleP
|
||||
return ANTHROPIC_COMPATIBLE_PROVIDER_ID_SET.has(id)
|
||||
}
|
||||
|
||||
type HostField = 'apiHost' | 'anthropicApiHost'
|
||||
|
||||
const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const { provider, updateProvider, models } = useProvider(providerId)
|
||||
const allProviders = useAllProviders()
|
||||
@ -79,19 +95,23 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost)
|
||||
const [apiVersion, setApiVersion] = useState(provider.apiVersion)
|
||||
const [activeHostField, setActiveHostField] = useState<HostField>('apiHost')
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
|
||||
const isAzureOpenAI = isAzureOpenAIProvider(provider)
|
||||
const isDmxapi = provider.id === 'dmxapi'
|
||||
const hideApiInput = ['vertexai', 'aws-bedrock'].includes(provider.id)
|
||||
const noAPIInputProviders = ['aws-bedrock'] as const satisfies SystemProviderId[]
|
||||
const hideApiInput = noAPIInputProviders.some((id) => id === provider.id)
|
||||
const noAPIKeyInputProviders = ['copilot', 'vertexai'] as const satisfies SystemProviderId[]
|
||||
const hideApiKeyInput = noAPIKeyInputProviders.some((id) => id === provider.id)
|
||||
|
||||
const providerConfig = PROVIDER_URLS[provider.id]
|
||||
const officialWebsite = providerConfig?.websites?.official
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
const configedApiHost = providerConfig?.api?.url
|
||||
const configuredApiHost = providerConfig?.api?.url
|
||||
|
||||
const fancyProviderName = getFancyProviderName(provider)
|
||||
|
||||
@ -151,7 +171,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
)
|
||||
|
||||
const onUpdateApiHost = () => {
|
||||
if (apiHost.trim()) {
|
||||
if (!validateApiHost(apiHost)) {
|
||||
setApiHost(provider.apiHost)
|
||||
window.toast.error(t('settings.provider.api_host_no_valid'))
|
||||
return
|
||||
}
|
||||
if (isVertexProvider(provider) || apiHost.trim()) {
|
||||
updateProvider({ apiHost })
|
||||
} else {
|
||||
setApiHost(provider.apiHost)
|
||||
@ -238,27 +263,46 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
setApiHost(configedApiHost)
|
||||
updateProvider({ apiHost: configedApiHost })
|
||||
}
|
||||
const onReset = useCallback(() => {
|
||||
setApiHost(configuredApiHost)
|
||||
updateProvider({ apiHost: configuredApiHost })
|
||||
}, [configuredApiHost, updateProvider])
|
||||
|
||||
const isApiHostResettable = useMemo(() => {
|
||||
return !isEmpty(configuredApiHost) && apiHost !== configuredApiHost
|
||||
}, [configuredApiHost, apiHost])
|
||||
|
||||
const hostPreview = () => {
|
||||
if (apiHost.endsWith('#')) {
|
||||
return apiHost.replace('#', '')
|
||||
}
|
||||
if (provider.type === 'openai') {
|
||||
return formatApiHost(apiHost) + 'chat/completions'
|
||||
|
||||
if (isOpenAICompatibleProvider(provider)) {
|
||||
return formatApiHost(apiHost, isSupportAPIVersionProvider(provider)) + '/chat/completions'
|
||||
}
|
||||
|
||||
if (provider.type === 'azure-openai') {
|
||||
return formatApiHost(apiHost) + 'openai/v1'
|
||||
if (isAzureOpenAIProvider(provider)) {
|
||||
const apiVersion = provider.apiVersion
|
||||
const path = !['preview', 'v1'].includes(apiVersion)
|
||||
? `/v1/chat/completion?apiVersion=v1`
|
||||
: `/v1/responses?apiVersion=v1`
|
||||
return formatAzureOpenAIApiHost(apiHost) + path
|
||||
}
|
||||
|
||||
if (provider.type === 'anthropic') {
|
||||
return formatApiHost(apiHost) + 'messages'
|
||||
if (isAnthropicProvider(provider)) {
|
||||
return formatApiHost(apiHost) + '/messages'
|
||||
}
|
||||
return formatApiHost(apiHost) + 'responses'
|
||||
|
||||
if (isGeminiProvider(provider)) {
|
||||
return formatApiHost(apiHost, true, 'v1beta') + '/models'
|
||||
}
|
||||
if (isOpenAIProvider(provider)) {
|
||||
return formatApiHost(apiHost) + '/responses'
|
||||
}
|
||||
if (isVertexProvider(provider)) {
|
||||
return formatVertexApiHost(provider) + '/publishers/google'
|
||||
}
|
||||
return formatApiHost(apiHost)
|
||||
}
|
||||
|
||||
// API key 连通性检查状态指示器,目前仅在失败时显示
|
||||
@ -286,31 +330,44 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
}, [provider.anthropicApiHost])
|
||||
|
||||
const canConfigureAnthropicHost = useMemo(() => {
|
||||
if (isNewApiProvider(provider)) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id)
|
||||
)
|
||||
}, [provider])
|
||||
|
||||
const anthropicHostPreview = useMemo(() => {
|
||||
const rawHost = (anthropicApiHost ?? provider.anthropicApiHost)?.trim()
|
||||
if (!rawHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (/\/messages\/?$/.test(rawHost)) {
|
||||
return rawHost.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
let normalizedHost = rawHost
|
||||
if (/\/v\d+(?:\/)?$/i.test(normalizedHost)) {
|
||||
normalizedHost = normalizedHost.replace(/\/$/, '')
|
||||
} else {
|
||||
normalizedHost = formatApiHost(normalizedHost).replace(/\/$/, '')
|
||||
}
|
||||
const rawHost = anthropicApiHost ?? provider.anthropicApiHost
|
||||
const normalizedHost = formatApiHost(rawHost)
|
||||
|
||||
return `${normalizedHost}/messages`
|
||||
}, [anthropicApiHost, provider.anthropicApiHost])
|
||||
|
||||
const hostSelectorOptions = useMemo(() => {
|
||||
const options: { value: HostField; label: string }[] = [
|
||||
{ value: 'apiHost', label: t('settings.provider.api_host') }
|
||||
]
|
||||
|
||||
if (canConfigureAnthropicHost) {
|
||||
options.push({ value: 'anthropicApiHost', label: t('settings.provider.anthropic_api_host') })
|
||||
}
|
||||
|
||||
return options
|
||||
}, [canConfigureAnthropicHost, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (!canConfigureAnthropicHost && activeHostField === 'anthropicApiHost') {
|
||||
setActiveHostField('apiHost')
|
||||
}
|
||||
}, [canConfigureAnthropicHost, activeHostField])
|
||||
|
||||
const hostSelectorTooltip =
|
||||
activeHostField === 'anthropicApiHost'
|
||||
? t('settings.provider.anthropic_api_host_tooltip')
|
||||
: t('settings.provider.api_host_tooltip')
|
||||
|
||||
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
|
||||
|
||||
return (
|
||||
@ -367,105 +424,122 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
)}
|
||||
{!hideApiInput && !isAnthropicOAuth() && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
{t('settings.provider.api_key.label')}
|
||||
{provider.id !== 'copilot' && (
|
||||
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
|
||||
<Button type="text" onClick={openApiKeyList} icon={<Settings2 size={16} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input.Password
|
||||
value={localApiKey}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)}
|
||||
disabled={provider.id === 'copilot'}
|
||||
suffix={renderStatusIndicator()}
|
||||
/>
|
||||
<Button
|
||||
type={isApiKeyConnectable ? 'primary' : 'default'}
|
||||
ghost={isApiKeyConnectable}
|
||||
onClick={onCheckApi}
|
||||
disabled={!apiHost || apiKeyConnectivity.checking}>
|
||||
{apiKeyConnectivity.checking ? (
|
||||
<LoadingIcon />
|
||||
) : apiKeyConnectivity.status === 'success' ? (
|
||||
<Check size={16} className="lucide-custom" />
|
||||
) : (
|
||||
t('settings.provider.check')
|
||||
)}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
{apiKeyWebsite && !isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
{!isDmxapi && !isAnthropicOAuth() && (
|
||||
{!hideApiKeyInput && (
|
||||
<>
|
||||
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Tooltip title={t('settings.provider.api_host_tooltip')} mouseEnterDelay={0.3}>
|
||||
<SubtitleLabel>{t('settings.provider.api_host')}</SubtitleLabel>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => CustomHeaderPopup.show({ provider })}
|
||||
icon={<Settings2 size={16} />}
|
||||
/>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
{t('settings.provider.api_key.label')}
|
||||
{provider.id !== 'copilot' && (
|
||||
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
|
||||
<Button type="text" onClick={openApiKeyList} icon={<Settings2 size={16} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
<Input.Password
|
||||
value={localApiKey}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)}
|
||||
disabled={provider.id === 'copilot'}
|
||||
suffix={renderStatusIndicator()}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type={isApiKeyConnectable ? 'primary' : 'default'}
|
||||
ghost={isApiKeyConnectable}
|
||||
onClick={onCheckApi}
|
||||
disabled={!apiHost || apiKeyConnectivity.checking}>
|
||||
{apiKeyConnectivity.checking ? (
|
||||
<LoadingIcon />
|
||||
) : apiKeyConnectivity.status === 'success' ? (
|
||||
<Check size={16} className="lucide-custom" />
|
||||
) : (
|
||||
t('settings.provider.check')
|
||||
)}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
{(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{t('settings.provider.api_host_preview', { url: hostPreview() })}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>
|
||||
{t('settings.provider.api.url.tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
{apiKeyWebsite && !isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</>
|
||||
)}
|
||||
{!isDmxapi && (
|
||||
<>
|
||||
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Tooltip title={hostSelectorTooltip} mouseEnterDelay={0.3}>
|
||||
<Selector
|
||||
size={14}
|
||||
value={activeHostField}
|
||||
onChange={(value) => setActiveHostField(value as HostField)}
|
||||
options={hostSelectorOptions}
|
||||
style={{ paddingLeft: 1, fontWeight: 'bold' }}
|
||||
placement="bottomLeft"
|
||||
/>
|
||||
</Tooltip>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => CustomHeaderPopup.show({ provider })}
|
||||
icon={<Settings2 size={16} />}
|
||||
/>
|
||||
</div>
|
||||
</SettingSubtitle>
|
||||
{activeHostField === 'apiHost' && (
|
||||
<>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{isApiHostResettable && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isVertexProvider(provider) && (
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
{(isOpenAICompatibleProvider(provider) ||
|
||||
isAzureOpenAIProvider(provider) ||
|
||||
isAnthropicProvider(provider) ||
|
||||
isGeminiProvider(provider) ||
|
||||
isVertexProvider(provider) ||
|
||||
isOpenAIProvider(provider)) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{
|
||||
marginLeft: 6,
|
||||
marginRight: '1em',
|
||||
whiteSpace: 'break-spaces',
|
||||
wordBreak: 'break-all'
|
||||
}}>
|
||||
{t('settings.provider.api_host_preview', { url: hostPreview() })}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{canConfigureAnthropicHost && (
|
||||
{activeHostField === 'anthropicApiHost' && canConfigureAnthropicHost && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Tooltip title={t('settings.provider.anthropic_api_host_tooltip')} mouseEnterDelay={0.3}>
|
||||
<SubtitleLabel>{t('settings.provider.anthropic_api_host')}</SubtitleLabel>
|
||||
</Tooltip>
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={anthropicApiHost ?? ''}
|
||||
@ -480,9 +554,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
url: anthropicHostPreview || '—'
|
||||
})}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ marginLeft: 6 }}>
|
||||
{t('settings.provider.anthropic_api_host_tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</>
|
||||
)}
|
||||
@ -512,21 +583,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
{provider.id === 'gpustack' && <GPUStackSettings />}
|
||||
{provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />}
|
||||
{provider.id === 'aws-bedrock' && <AwsBedrockSettings />}
|
||||
{provider.id === 'vertexai' && <VertexAISettings providerId={provider.id} />}
|
||||
{provider.id === 'vertexai' && <VertexAISettings />}
|
||||
<ModelList providerId={provider.id} />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const SubtitleLabel = styled.span`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
`
|
||||
|
||||
const ProviderName = styled.span`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { useVertexAISettings } from '@renderer/hooks/useVertexAI'
|
||||
import { Alert, Input, Space } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { Alert, Input } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
|
||||
|
||||
interface Props {
|
||||
providerId: string
|
||||
}
|
||||
|
||||
const VertexAISettings: FC<Props> = ({ providerId }) => {
|
||||
const VertexAISettings = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
projectId,
|
||||
@ -27,16 +22,9 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
|
||||
const [localProjectId, setLocalProjectId] = useState(projectId)
|
||||
const [localLocation, setLocalLocation] = useState(location)
|
||||
|
||||
const { provider, updateProvider } = useProvider(providerId)
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
|
||||
const providerConfig = PROVIDER_URLS['vertexai']
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
|
||||
const onUpdateApiHost = () => {
|
||||
updateProvider({ apiHost })
|
||||
}
|
||||
|
||||
const handleProjectIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setLocalProjectId(e.target.value)
|
||||
}
|
||||
@ -72,18 +60,6 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>
|
||||
{t('settings.provider.vertex_ai.service_account.title')}
|
||||
</SettingSubtitle>
|
||||
|
||||
@ -3,6 +3,7 @@ import { Span } from '@opentelemetry/api'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant'
|
||||
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
||||
import { isGeminiProvider } from '@renderer/config/providers'
|
||||
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import store from '@renderer/store'
|
||||
import {
|
||||
@ -40,7 +41,7 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
||||
|
||||
let host = aiProvider.getBaseURL()
|
||||
const rerankHost = rerankAiProvider.getBaseURL()
|
||||
if (provider.type === 'gemini') {
|
||||
if (isGeminiProvider(provider)) {
|
||||
host = host + '/v1beta/openai/'
|
||||
}
|
||||
|
||||
|
||||
@ -2716,7 +2716,6 @@ const migrateConfig = {
|
||||
}
|
||||
},
|
||||
'166': (state: RootState) => {
|
||||
// added after 1.6.5 and 1.7.0-beta.2
|
||||
try {
|
||||
if (state.assistants.presets === undefined) {
|
||||
state.assistants.presets = []
|
||||
@ -2733,6 +2732,18 @@ const migrateConfig = {
|
||||
if (dashscopeProvider) {
|
||||
dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic'
|
||||
}
|
||||
|
||||
state.llm.providers.forEach((provider) => {
|
||||
if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') {
|
||||
provider.type = 'new-api'
|
||||
}
|
||||
if (provider.id === SystemProviderIds.longcat) {
|
||||
// https://longcat.chat/platform/docs/zh/#anthropic-api-%E6%A0%BC%E5%BC%8F
|
||||
if (!provider.anthropicApiHost) {
|
||||
provider.anthropicApiHost = 'https://api.longcat.chat/anthropic'
|
||||
}
|
||||
}
|
||||
})
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 166 error', error as Error)
|
||||
|
||||
@ -41,7 +41,7 @@ export type Assistant = {
|
||||
/** enableWebSearch 代表使用模型内置网络搜索功能 */
|
||||
enableWebSearch?: boolean
|
||||
webSearchProviderId?: WebSearchProvider['id']
|
||||
// enableUrlContext 是 Gemini 的特有功能
|
||||
// enableUrlContext 是 Gemini/Anthropic 的特有功能
|
||||
enableUrlContext?: boolean
|
||||
enableGenerateImage?: boolean
|
||||
mcpServers?: MCPServer[]
|
||||
|
||||
@ -6,7 +6,6 @@ export const ProviderTypeSchema = z.enum([
|
||||
'openai-response',
|
||||
'anthropic',
|
||||
'gemini',
|
||||
'qwenlm',
|
||||
'azure-openai',
|
||||
'vertexai',
|
||||
'mistral',
|
||||
@ -37,6 +36,8 @@ export type ProviderApiOptions = {
|
||||
isSupportServiceTier?: boolean
|
||||
/** 是否不支持 enable_thinking 参数 */
|
||||
isNotSupportEnableThinking?: boolean
|
||||
/** 是否不支持 APIVersion */
|
||||
isNotSupportAPIVersion?: boolean
|
||||
}
|
||||
|
||||
export const OpenAIServiceTiers = {
|
||||
@ -187,6 +188,11 @@ export type VertexProvider = Provider & {
|
||||
location: string
|
||||
}
|
||||
|
||||
export type AzureOpenAIProvider = Provider & {
|
||||
type: 'azure-openai'
|
||||
apiVersion: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为系统内置的提供商。比直接使用`provider.isSystem`更好,因为该数据字段不会随着版本更新而变化。
|
||||
* @param provider - Provider对象,包含提供商的信息
|
||||
|
||||
@ -1,31 +1,106 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import store from '@renderer/store'
|
||||
import type { VertexProvider } from '@renderer/types'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { formatApiHost, maskApiKey, splitApiKeyString } from '../api'
|
||||
import {
|
||||
formatApiHost,
|
||||
formatApiKeys,
|
||||
formatAzureOpenAIApiHost,
|
||||
formatVertexApiHost,
|
||||
hasAPIVersion,
|
||||
maskApiKey,
|
||||
routeToEndpoint,
|
||||
splitApiKeyString,
|
||||
validateApiHost
|
||||
} from '../api'
|
||||
|
||||
vi.mock('@renderer/store', () => {
|
||||
const getState = vi.fn()
|
||||
return {
|
||||
default: {
|
||||
getState
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getStateMock = store.getState as unknown as ReturnType<typeof vi.fn>
|
||||
|
||||
const createVertexProvider = (apiHost: string): VertexProvider => ({
|
||||
id: 'vertex-provider',
|
||||
type: 'vertexai',
|
||||
name: 'Vertex AI',
|
||||
apiKey: '',
|
||||
apiHost,
|
||||
models: [],
|
||||
googleCredentials: {
|
||||
privateKey: '',
|
||||
clientEmail: ''
|
||||
},
|
||||
project: '',
|
||||
location: ''
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
getStateMock.mockReset()
|
||||
getStateMock.mockReturnValue({
|
||||
llm: {
|
||||
settings: {
|
||||
vertexai: {
|
||||
projectId: 'test-project',
|
||||
location: 'us-central1'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('api', () => {
|
||||
describe('formatApiHost', () => {
|
||||
it('should return original host when it ends with a slash', () => {
|
||||
expect(formatApiHost('https://api.example.com/')).toBe('https://api.example.com/')
|
||||
expect(formatApiHost('http://localhost:5173/')).toBe('http://localhost:5173/')
|
||||
})
|
||||
|
||||
it('should return original host when it ends with volces.com/api/v3', () => {
|
||||
expect(formatApiHost('https://api.volces.com/api/v3')).toBe('https://api.volces.com/api/v3')
|
||||
expect(formatApiHost('http://volces.com/api/v3')).toBe('http://volces.com/api/v3')
|
||||
})
|
||||
|
||||
it('should append /v1/ to hosts that do not match special conditions', () => {
|
||||
expect(formatApiHost('https://api.example.com')).toBe('https://api.example.com/v1/')
|
||||
expect(formatApiHost('http://localhost:5173')).toBe('http://localhost:5173/v1/')
|
||||
expect(formatApiHost('https://api.openai.com')).toBe('https://api.openai.com/v1/')
|
||||
})
|
||||
|
||||
it('should not modify hosts that already have a path but do not end with a slash', () => {
|
||||
expect(formatApiHost('https://api.example.com/custom')).toBe('https://api.example.com/custom/v1/')
|
||||
})
|
||||
|
||||
it('should handle empty string gracefully', () => {
|
||||
it('returns empty string for falsy host', () => {
|
||||
expect(formatApiHost('')).toBe('')
|
||||
expect(formatApiHost(undefined)).toBe('')
|
||||
})
|
||||
|
||||
it('appends api version when missing', () => {
|
||||
expect(formatApiHost('https://api.example.com')).toBe('https://api.example.com/v1')
|
||||
expect(formatApiHost('http://localhost:5173/')).toBe('http://localhost:5173/v1')
|
||||
expect(formatApiHost(' https://api.openai.com ')).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('keeps original host when api version already present', () => {
|
||||
expect(formatApiHost('https://api.volces.com/api/v3')).toBe('https://api.volces.com/api/v3')
|
||||
expect(formatApiHost('http://localhost:5173/v2beta')).toBe('http://localhost:5173/v2beta')
|
||||
})
|
||||
|
||||
it('supports custom api version parameter', () => {
|
||||
expect(formatApiHost('https://api.example.com', true, 'v2')).toBe('https://api.example.com/v2')
|
||||
})
|
||||
|
||||
it('keeps host untouched when api version unsupported', () => {
|
||||
expect(formatApiHost('https://api.example.com', false)).toBe('https://api.example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasAPIVersion', () => {
|
||||
it('detects numeric version suffix', () => {
|
||||
expect(hasAPIVersion('https://api.example.com/v1')).toBe(true)
|
||||
expect(hasAPIVersion('http://localhost:3000/v2beta')).toBe(true)
|
||||
expect(hasAPIVersion('/v3alpha/resources')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when no version found', () => {
|
||||
expect(hasAPIVersion('https://api.example.com')).toBe(false)
|
||||
expect(hasAPIVersion('')).toBe(false)
|
||||
expect(hasAPIVersion(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('return flase when starting without v character', () => {
|
||||
expect(hasAPIVersion('https://api.example.com/a1v')).toBe(false)
|
||||
expect(hasAPIVersion('/av1/users')).toBe(false)
|
||||
})
|
||||
|
||||
it('return flase when starting with v- word', () => {
|
||||
expect(hasAPIVersion('https://api.example.com/vendor')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@ -123,4 +198,122 @@ describe('api', () => {
|
||||
expect(result).toEqual(['key1', 'key2,withcomma', 'key3'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateApiHost', () => {
|
||||
it('accepts empty or whitespace-only host', () => {
|
||||
expect(validateApiHost('')).toBe(true)
|
||||
expect(validateApiHost(' ')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects unsupported protocols', () => {
|
||||
expect(validateApiHost('ftp://api.example.com')).toBe(false)
|
||||
})
|
||||
|
||||
it('validates supported endpoint fragments when using hash suffix', () => {
|
||||
expect(validateApiHost('https://api.example.com/v1/chat/completions#')).toBe(true)
|
||||
expect(validateApiHost('https://api.example.com/v1/unknown#')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('routeToEndpoint', () => {
|
||||
it('returns host without endpoint when not using hash suffix', () => {
|
||||
expect(routeToEndpoint(' https://api.example.com/v1 ')).toEqual({
|
||||
baseURL: 'https://api.example.com/v1',
|
||||
endpoint: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('extracts known endpoint and base url when using hash suffix', () => {
|
||||
expect(routeToEndpoint('https://api.example.com/v1/chat/completions#')).toEqual({
|
||||
baseURL: 'https://api.example.com/v1',
|
||||
endpoint: 'chat/completions'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns empty endpoint when unsupported endpoint fragment is provided', () => {
|
||||
expect(routeToEndpoint('https://api.example.com/v1/custom#')).toEqual({
|
||||
baseURL: 'https://api.example.com/v1/custom',
|
||||
endpoint: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('prefers the most specific endpoint match when multiple matches exist', () => {
|
||||
expect(routeToEndpoint('https://api.example.com/v1/streamGenerateContent#')).toEqual({
|
||||
baseURL: 'https://api.example.com/v1',
|
||||
endpoint: 'streamGenerateContent'
|
||||
})
|
||||
})
|
||||
|
||||
it('extract OpenAI images generations endpoint', () => {
|
||||
expect(routeToEndpoint('https://open.cherryin.net/v1/images/generations#')).toEqual({
|
||||
baseURL: 'https://open.cherryin.net/v1',
|
||||
endpoint: 'images/generations'
|
||||
})
|
||||
})
|
||||
|
||||
it('extract Gemini images generation endpoint', () => {
|
||||
expect(routeToEndpoint('https://open.cherryin.net/v1beta/models/imagen-4.0-generate-001:predict#')).toEqual({
|
||||
baseURL: 'https://open.cherryin.net/v1beta/models/imagen-4.0-generate-001',
|
||||
endpoint: 'predict'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatApiKeys', () => {
|
||||
it('normalizes chinese commas and new lines', () => {
|
||||
expect(formatApiKeys('key1,key2\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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { runAsyncFunction } from '../index'
|
||||
import { hasPath, isValidProxyUrl, removeQuotes, removeSpecialCharacters } from '../index'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
describe('Unclassified Utils', () => {
|
||||
describe('runAsyncFunction', () => {
|
||||
it('should execute async function', async () => {
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { isJSON, parseJSON } from '../index'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
describe('json', () => {
|
||||
describe('isJSON', () => {
|
||||
it('should return true for valid JSON string', () => {
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import store from '@renderer/store'
|
||||
import { VertexProvider } from '@renderer/types'
|
||||
import { trim } from 'lodash'
|
||||
|
||||
/**
|
||||
* 格式化 API key 字符串。
|
||||
*
|
||||
@ -9,30 +13,166 @@ export function formatApiKeys(value: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 API 主机地址。
|
||||
* 判断 host 的 path 中是否包含形如版本的字符串(例如 /v1、/v2beta 等),
|
||||
*
|
||||
* 根据传入的 host 判断是否需要在其末尾加 `/v1/`。
|
||||
* - 不加:host 以 `/` 结尾,或以 `volces.com/api/v3` 结尾。
|
||||
* - 要加:其余情况。
|
||||
*
|
||||
* @param {string} host - 需要格式化的 API 主机地址。
|
||||
* @param {string} apiVersion - 需要添加的 API 版本。
|
||||
* @returns {string} 格式化后的 API 主机地址。
|
||||
* @param host - 要检查的 host 或 path 字符串
|
||||
* @returns 如果 path 中包含版本字符串则返回 true,否则 false
|
||||
*/
|
||||
export function formatApiHost(host: string, apiVersion: string = 'v1'): string {
|
||||
if (!host) {
|
||||
export function hasAPIVersion(host?: string): boolean {
|
||||
if (!host) return false
|
||||
|
||||
// 匹配路径中以 `/v<number>` 开头并可选跟随 `alpha` 或 `beta` 的版本段,
|
||||
// 该段后面可以跟 `/` 或字符串结束(用于匹配诸如 `/v3alpha/resources` 的情况)。
|
||||
const versionRegex = /\/v\d+(?:alpha|beta)?(?=\/|$)/i
|
||||
|
||||
try {
|
||||
const url = new URL(host)
|
||||
return versionRegex.test(url.pathname)
|
||||
} catch {
|
||||
// 若无法作为完整 URL 解析,则当作路径直接检测
|
||||
return versionRegex.test(host)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the trailing slash from a URL string if it exists.
|
||||
*
|
||||
* @template T - The string type to preserve type safety
|
||||
* @param {T} url - The URL string to process
|
||||
* @returns {T} The URL string without a trailing slash
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* withoutTrailingSlash('https://example.com/') // 'https://example.com'
|
||||
* withoutTrailingSlash('https://example.com') // 'https://example.com'
|
||||
* ```
|
||||
*/
|
||||
export function withoutTrailingSlash<T extends string>(url: T): T {
|
||||
return url.replace(/\/$/, '') as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an API host URL by normalizing it and optionally appending an API version.
|
||||
*
|
||||
* @param host - The API host URL to format. Leading/trailing whitespace will be trimmed and trailing slashes removed.
|
||||
* @param isSupportedAPIVerion - Whether the API version is supported. Defaults to `true`.
|
||||
* @param apiVersion - The API version to append if needed. Defaults to `'v1'`.
|
||||
*
|
||||
* @returns The formatted API host URL. If the host is empty after normalization, returns an empty string.
|
||||
* If the host ends with '#', API version is not supported, or the host already contains a version, returns the normalized host as-is.
|
||||
* Otherwise, returns the host with the API version appended.
|
||||
*
|
||||
* @example
|
||||
* formatApiHost('https://api.example.com/') // Returns 'https://api.example.com/v1'
|
||||
* formatApiHost('https://api.example.com#') // Returns 'https://api.example.com#'
|
||||
* formatApiHost('https://api.example.com/v2', true, 'v1') // Returns 'https://api.example.com/v2'
|
||||
*/
|
||||
export function formatApiHost(host?: string, isSupportedAPIVerion: boolean = true, apiVersion: string = 'v1'): string {
|
||||
const normalizedHost = withoutTrailingSlash(trim(host))
|
||||
if (!normalizedHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const forceUseOriginalHost = () => {
|
||||
if (host.endsWith('/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return host.endsWith('volces.com/api/v3')
|
||||
if (normalizedHost.endsWith('#') || !isSupportedAPIVerion || hasAPIVersion(normalizedHost)) {
|
||||
return normalizedHost
|
||||
}
|
||||
return `${normalizedHost}/${apiVersion}`
|
||||
}
|
||||
|
||||
return forceUseOriginalHost() ? host : `${host}/${apiVersion}/`
|
||||
/**
|
||||
* 格式化 Azure OpenAI 的 API 主机地址。
|
||||
*/
|
||||
export function formatAzureOpenAIApiHost(host: string): string {
|
||||
const normalizedHost = withoutTrailingSlash(host)
|
||||
?.replace(/\/v1$/, '')
|
||||
.replace(/\/openai$/, '')
|
||||
// NOTE: AISDK会添加上`v1`
|
||||
return formatApiHost(normalizedHost + '/openai', false)
|
||||
}
|
||||
|
||||
export function formatVertexApiHost(provider: VertexProvider): string {
|
||||
const { apiHost } = provider
|
||||
const { projectId: project, location } = store.getState().llm.settings.vertexai
|
||||
const trimmedHost = withoutTrailingSlash(trim(apiHost))
|
||||
if (!trimmedHost || trimmedHost.endsWith('aiplatform.googleapis.com')) {
|
||||
const host =
|
||||
location == 'global' ? 'https://aiplatform.googleapis.com' : `https://${location}-aiplatform.googleapis.com`
|
||||
return `${formatApiHost(host)}/projects/${project}/locations/${location}`
|
||||
}
|
||||
return formatApiHost(trimmedHost)
|
||||
}
|
||||
|
||||
// 目前对话界面只支持这些端点
|
||||
export const SUPPORTED_IMAGE_ENDPOINT_LIST = ['images/generations', 'images/edits', 'predict'] as const
|
||||
export const SUPPORTED_ENDPOINT_LIST = [
|
||||
'chat/completions',
|
||||
'responses',
|
||||
'messages',
|
||||
'generateContent',
|
||||
'streamGenerateContent',
|
||||
...SUPPORTED_IMAGE_ENDPOINT_LIST
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Converts an API host URL into separate base URL and endpoint components.
|
||||
*
|
||||
* @param apiHost - The API host string to parse. Expected to be a trimmed URL that may end with '#' followed by an endpoint identifier.
|
||||
* @returns An object containing:
|
||||
* - `baseURL`: The base URL without the endpoint suffix
|
||||
* - `endpoint`: The matched endpoint identifier, or empty string if no match found
|
||||
*
|
||||
* @description
|
||||
* This function extracts endpoint information from a composite API host string.
|
||||
* If the host ends with '#', it attempts to match the preceding part against the supported endpoint list.
|
||||
* The '#' delimiter is removed before processing.
|
||||
*
|
||||
* @example
|
||||
* routeToEndpoint('https://api.example.com/openai/chat/completions#')
|
||||
* // Returns: { baseURL: 'https://api.example.com/v1', endpoint: 'chat/completions' }
|
||||
*
|
||||
* @example
|
||||
* routeToEndpoint('https://api.example.com/v1')
|
||||
* // Returns: { baseURL: 'https://api.example.com/v1', endpoint: '' }
|
||||
*/
|
||||
export function routeToEndpoint(apiHost: string): { baseURL: string; endpoint: string } {
|
||||
const trimmedHost = trim(apiHost)
|
||||
// 前面已经确保apiHost合法
|
||||
if (!trimmedHost.endsWith('#')) {
|
||||
return { baseURL: trimmedHost, endpoint: '' }
|
||||
}
|
||||
// 去掉结尾的 #
|
||||
const host = trimmedHost.slice(0, -1)
|
||||
const endpointMatch = SUPPORTED_ENDPOINT_LIST.find((endpoint) => host.endsWith(endpoint))
|
||||
if (!endpointMatch) {
|
||||
const baseURL = withoutTrailingSlash(host)
|
||||
return { baseURL, endpoint: '' }
|
||||
}
|
||||
const baseSegment = host.slice(0, host.length - endpointMatch.length)
|
||||
const baseURL = withoutTrailingSlash(baseSegment).replace(/:$/, '') // 去掉结尾可能存在的冒号(gemini的特殊情况)
|
||||
return { baseURL, endpoint: endpointMatch }
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 API 主机地址是否合法。
|
||||
*
|
||||
* @param {string} apiHost - 需要验证的 API 主机地址。
|
||||
* @returns {boolean} 如果是合法的 URL 则返回 true,否则返回 false。
|
||||
*/
|
||||
export function validateApiHost(apiHost: string): boolean {
|
||||
// 允许apiHost为空
|
||||
if (!apiHost || !trim(apiHost)) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const url = new URL(trim(apiHost))
|
||||
// 验证协议是否为 http 或 https
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { Model, ModelType, Provider } from '@renderer/types'
|
||||
import { Model, ModelType } from '@renderer/types'
|
||||
import { ModalFuncProps } from 'antd'
|
||||
import { isEqual } from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@ -196,19 +196,6 @@ export function getMcpConfigSampleFromReadme(readme: string): Record<string, any
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
* @returns {boolean} 是否为 OpenAI 兼容提供商
|
||||
*/
|
||||
export function isOpenAIProvider(provider: Provider): boolean {
|
||||
return !['anthropic', 'gemini', 'vertexai'].includes(provider.type)
|
||||
}
|
||||
|
||||
export function isAnthropicProvider(provider: Provider): boolean {
|
||||
return provider.type === 'anthropic'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断模型是否为用户手动选择
|
||||
* @param {Model} model 模型对象
|
||||
|
||||
137
yarn.lock
137
yarn.lock
@ -74,11 +74,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/amazon-bedrock@npm:^3.0.35":
|
||||
version: 3.0.35
|
||||
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.35"
|
||||
"@ai-sdk/amazon-bedrock@npm:^3.0.42":
|
||||
version: 3.0.42
|
||||
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.42"
|
||||
dependencies:
|
||||
"@ai-sdk/anthropic": "npm:2.0.27"
|
||||
"@ai-sdk/anthropic": "npm:2.0.32"
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
"@smithy/eventstream-codec": "npm:^4.0.1"
|
||||
@ -86,32 +86,32 @@ __metadata:
|
||||
aws4fetch: "npm:^1.0.20"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/0e3e0ed1730fa6a14d8d7ca14b7823ec0b80c9d666435d97a505e7fb0c1818378343cdb647e3cc08d7f15d201cbeb04272c5128065f6cc6858b4404961eca761
|
||||
checksum: 10c0/659de3d62f1907489bb14cd7fe049274c0a5f754222eda41b500d66573422ddaad3380cf8fc6eaae8a39ab25445e81aca7664ca2068b4a93c49bcb605889b2ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/anthropic@npm:2.0.27, @ai-sdk/anthropic@npm:^2.0.27":
|
||||
version: 2.0.27
|
||||
resolution: "@ai-sdk/anthropic@npm:2.0.27"
|
||||
"@ai-sdk/anthropic@npm:2.0.32, @ai-sdk/anthropic@npm:^2.0.32":
|
||||
version: 2.0.32
|
||||
resolution: "@ai-sdk/anthropic@npm:2.0.32"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/b568b3b8639af8ec7ea9b766061a4f18bcdef16f2bb12da3a4c4124c751bd6aab1b96dbe1a0eb8e38831d305871ce0787a6536d1a4d8a8ab8aaf03aca3e48e3f
|
||||
checksum: 10c0/f83ec81fe150dacd9207b67a173f7e150b44a0b2b57e6361c061e35b663bbb95240ea18066bd2bce73df722b85772ca174c4f1546b29eb6e6d1fcf4f349e756b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/azure@npm:^2.0.49":
|
||||
version: 2.0.49
|
||||
resolution: "@ai-sdk/azure@npm:2.0.49"
|
||||
"@ai-sdk/azure@npm:^2.0.53":
|
||||
version: 2.0.53
|
||||
resolution: "@ai-sdk/azure@npm:2.0.53"
|
||||
dependencies:
|
||||
"@ai-sdk/openai": "npm:2.0.48"
|
||||
"@ai-sdk/openai": "npm:2.0.52"
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/d4dc5a8e0cbe0cefc8db987c4a7b784a9898d40cc55ef38618c71eba7f40dbef77b754aec1d507559f643fed49e538ffe2b677b327f001a2efc0474f6b544ba9
|
||||
checksum: 10c0/39346f50434c3568b40bb57aa64010261ae767d9aa49b4477999ca78431326275b111879b9c5431ce35ca4ca376c47455618c8bf528c54402b0dad1b03e10487
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -128,55 +128,55 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/gateway@npm:1.0.39":
|
||||
version: 1.0.39
|
||||
resolution: "@ai-sdk/gateway@npm:1.0.39"
|
||||
"@ai-sdk/gateway@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@ai-sdk/gateway@npm:2.0.0"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
"@vercel/oidc": "npm:3.0.2"
|
||||
"@vercel/oidc": "npm:3.0.3"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/1b6eedf12ac641c96a1eb75e48e43474694b60eb7dca273f76a636a4e2bfc89efda1d9855d5abf9cc464e23cdbf5a3119fed65c3d22cec726e29a2bad3c3318b
|
||||
checksum: 10c0/720cfb827bc64f3eb6bb86d17e7e7947c54bdc7d74db7f6e9e162be0973a45368c05829e4b257704182ca9c4886e7f3c74f6b64841e88359930f48f288aa958a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/google-vertex@npm:^3.0.40":
|
||||
version: 3.0.40
|
||||
resolution: "@ai-sdk/google-vertex@npm:3.0.40"
|
||||
"@ai-sdk/google-vertex@npm:^3.0.48":
|
||||
version: 3.0.48
|
||||
resolution: "@ai-sdk/google-vertex@npm:3.0.48"
|
||||
dependencies:
|
||||
"@ai-sdk/anthropic": "npm:2.0.27"
|
||||
"@ai-sdk/google": "npm:2.0.20"
|
||||
"@ai-sdk/anthropic": "npm:2.0.32"
|
||||
"@ai-sdk/google": "npm:2.0.23"
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
google-auth-library: "npm:^9.15.0"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/680a06e1b80bc036744e2f13e1a55b57661c3674000ab82b863d6536730edfc3696b1b0b2235f6354de11fa323c4ef817d8edbd2dbf94dc4037ea882e560c9ea
|
||||
checksum: 10c0/79f0ccb78c4930ea57a41e81f31a1935531d8f02b738d0aae13fa865272f4dac6b1c31b2e1c8b8ca65671a96b90cd4f14fabaa9d60ab0252c6c0e6a1828e7f09
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/google@npm:2.0.20":
|
||||
version: 2.0.20
|
||||
resolution: "@ai-sdk/google@npm:2.0.20"
|
||||
"@ai-sdk/google@npm:2.0.23":
|
||||
version: 2.0.23
|
||||
resolution: "@ai-sdk/google@npm:2.0.23"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/9c73bb67061673b16f0996c85bf4e79ab9968c8a203c4f9731bf569e45960db88950dfc227aca69661ea805d381b285697ba1763faa03a38c01b86e6d2e90629
|
||||
checksum: 10c0/402b78f392196c3e23c75cc35fc1d701f9521b57aace2fb1bbae6a0d57bbb3894a778b0485305bd6674998403e44c3883dca2416f2d48377722351debead9f11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch":
|
||||
version: 2.0.20
|
||||
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch::version=2.0.20&hash=1f2ccb"
|
||||
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch":
|
||||
version: 2.0.23
|
||||
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch::version=2.0.23&hash=df67ed"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/2d567361d533a4e2be83aa135cb5f01f09ea54c255d7751171855ef4244cfaeff73fe7b3c7b044b384a9c170e89d053160a26933176ad68dcaf03bd3c69c0be3
|
||||
checksum: 10c0/e7fda169f04190b3ef37937e61219dcf8dade735cf76a9af8f1a1def83a43846659a361835814f0b68a2c392bc840a457a693cb69fed42af375771dd210ebdbe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -242,27 +242,39 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/openai@npm:2.0.48, @ai-sdk/openai@npm:^2.0.48":
|
||||
version: 2.0.48
|
||||
resolution: "@ai-sdk/openai@npm:2.0.48"
|
||||
"@ai-sdk/openai@npm:2.0.52":
|
||||
version: 2.0.52
|
||||
resolution: "@ai-sdk/openai@npm:2.0.52"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/6c584d7ffb80025da6b7253106a83f8c7a023e8ca322fd32e6858453782d6a0a6d268d7afa7145e3ea743a9c6cbc882932bb59eb1a659750f5205639c414fb49
|
||||
checksum: 10c0/253125303235dc677e272eaffbcd5c788373e12f897e42da7cce827bcc952f31e4bb11b72ba06931f37d49a2588f6cba8526127d539025bbd58d78d7bcfc691d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/openai@npm:^2.0.42":
|
||||
version: 2.0.47
|
||||
resolution: "@ai-sdk/openai@npm:2.0.47"
|
||||
version: 2.0.53
|
||||
resolution: "@ai-sdk/openai@npm:2.0.53"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.11"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/7fabcdda707134971bcc2b285705d4595f8bf419285dbdd9266b3b0858ea11b6ac200e63dd2eeb1822f99571910093d64d4a76154a365331cf184f56452933c6
|
||||
checksum: 10c0/acb014c7e4d99be0502fe2190c3b91c76ee86ade25e80dad939ffd113a5f013f29a81f06e13fa0e6a76b49fcb8cc524aab180fc1a622ceb8d3dac58fd655de1c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch":
|
||||
version: 2.0.52
|
||||
resolution: "@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch::version=2.0.52&hash=c7ceb9"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/a3ac267a645ffd50952c312318d0ea6190e1ca961f910f9e3067211df731ac4ba0eb89face21b5cc195770b643326b295a6fece91f07b60db8aef32f45d4664e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -291,19 +303,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/provider-utils@npm:3.0.11":
|
||||
version: 3.0.11
|
||||
resolution: "@ai-sdk/provider-utils@npm:3.0.11"
|
||||
dependencies:
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@standard-schema/spec": "npm:^1.0.0"
|
||||
eventsource-parser: "npm:^3.0.5"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/31081b127b48f3eefb448eaca59574b4631da9577aa0778622d28669c71bbde0361c9b37962c5edbb1d0c163ed1479755fc889da9251a03e906b1e27d0d2eb24
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ai-sdk/provider-utils@npm:3.0.12, @ai-sdk/provider-utils@npm:^3.0.12":
|
||||
version: 3.0.12
|
||||
resolution: "@ai-sdk/provider-utils@npm:3.0.12"
|
||||
@ -2469,10 +2468,10 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@cherrystudio/ai-core@workspace:packages/aiCore"
|
||||
dependencies:
|
||||
"@ai-sdk/anthropic": "npm:^2.0.27"
|
||||
"@ai-sdk/azure": "npm:^2.0.49"
|
||||
"@ai-sdk/anthropic": "npm:^2.0.32"
|
||||
"@ai-sdk/azure": "npm:^2.0.53"
|
||||
"@ai-sdk/deepseek": "npm:^1.0.23"
|
||||
"@ai-sdk/openai": "npm:^2.0.48"
|
||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch"
|
||||
"@ai-sdk/openai-compatible": "npm:^1.0.22"
|
||||
"@ai-sdk/provider": "npm:^2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:^3.0.12"
|
||||
@ -13578,10 +13577,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vercel/oidc@npm:3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "@vercel/oidc@npm:3.0.2"
|
||||
checksum: 10c0/8d4c8553baa5aed339ab7614d775139bc124a6d443b76877ab17e98c156daa4dbeb3cf2f3bf21fabfae2ac0dd3ff462ab43b9398708e02483e5923d302a1c4c8
|
||||
"@vercel/oidc@npm:3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "@vercel/oidc@npm:3.0.3"
|
||||
checksum: 10c0/c8eecb1324559435f4ab8a955f5ef44f74f546d11c2ddcf28151cb636d989bd4b34e0673fd8716cb21bb21afb34b3de663bacc30c9506036eeecbcbf2fd86241
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -13888,8 +13887,8 @@ __metadata:
|
||||
"@agentic/exa": "npm:^7.3.3"
|
||||
"@agentic/searxng": "npm:^7.3.3"
|
||||
"@agentic/tavily": "npm:^7.3.3"
|
||||
"@ai-sdk/amazon-bedrock": "npm:^3.0.35"
|
||||
"@ai-sdk/google-vertex": "npm:^3.0.40"
|
||||
"@ai-sdk/amazon-bedrock": "npm:^3.0.42"
|
||||
"@ai-sdk/google-vertex": "npm:^3.0.48"
|
||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch"
|
||||
"@ai-sdk/mistral": "npm:^2.0.19"
|
||||
"@ai-sdk/perplexity": "npm:^2.0.13"
|
||||
@ -14017,7 +14016,7 @@ __metadata:
|
||||
"@viz-js/lang-dot": "npm:^1.0.5"
|
||||
"@viz-js/viz": "npm:^3.14.0"
|
||||
"@xyflow/react": "npm:^12.4.4"
|
||||
ai: "npm:^5.0.68"
|
||||
ai: "npm:^5.0.76"
|
||||
antd: "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch"
|
||||
archiver: "npm:^7.0.1"
|
||||
async-mutex: "npm:^0.5.0"
|
||||
@ -14284,17 +14283,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ai@npm:^5.0.68":
|
||||
version: 5.0.68
|
||||
resolution: "ai@npm:5.0.68"
|
||||
"ai@npm:^5.0.76":
|
||||
version: 5.0.76
|
||||
resolution: "ai@npm:5.0.76"
|
||||
dependencies:
|
||||
"@ai-sdk/gateway": "npm:1.0.39"
|
||||
"@ai-sdk/gateway": "npm:2.0.0"
|
||||
"@ai-sdk/provider": "npm:2.0.0"
|
||||
"@ai-sdk/provider-utils": "npm:3.0.12"
|
||||
"@opentelemetry/api": "npm:1.9.0"
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
checksum: 10c0/0c042cd58c7193a47b06b3074a9e62790c4d5a8134e8e12bbb750714151e9aa217c641ee60c8cbe59d9869bade52ccbb283f9fcbf6d79711ebf1f774fa3feee3
|
||||
checksum: 10c0/167a191354b72106b1af6cfc8b53975637ca43919b8f48db81c0cf542ef0172f55958ed9331adcd08d017a608a98cb0b4a253c62732038322c78091e32595771
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user