diff --git a/packages/shared/config/providers.ts b/packages/shared/config/providers.ts index f7744150e2..e03661bf0e 100644 --- a/packages/shared/config/providers.ts +++ b/packages/shared/config/providers.ts @@ -41,8 +41,3 @@ const SILICON_ANTHROPIC_COMPATIBLE_MODEL_SET = new Set(SILICON_ANTHROPIC_COMPATI export function isSiliconAnthropicCompatibleModel(modelId: string): boolean { return SILICON_ANTHROPIC_COMPATIBLE_MODEL_SET.has(modelId) } - -/** - * Silicon provider's Anthropic API host URL. - */ -export const SILICON_ANTHROPIC_API_HOST = 'https://api.siliconflow.cn' diff --git a/src/main/apiServer/routes/messages.ts b/src/main/apiServer/routes/messages.ts index f2590cf1d5..f0eaac8e4e 100644 --- a/src/main/apiServer/routes/messages.ts +++ b/src/main/apiServer/routes/messages.ts @@ -6,17 +6,34 @@ import express from 'express' import { messagesService } from '../services/messages' import { generateUnifiedMessage, streamUnifiedMessages } from '../services/unified-messages' -import { getProviderById, validateModelId } from '../utils' +import { getProviderById, isModelAnthropicCompatible, validateModelId } from '../utils' /** - * Check if provider should use direct Anthropic SDK + * Check if a specific model on a provider should use direct Anthropic SDK * - * A provider is considered "Anthropic-compatible" if: + * A provider+model combination is considered "Anthropic-compatible" if: * 1. It's a native Anthropic provider (type === 'anthropic'), OR - * 2. It has anthropicApiHost configured (aggregated providers routing to Anthropic-compatible endpoints) + * 2. It has anthropicApiHost configured AND the specific model supports Anthropic API + * (for aggregated providers like Silicon, only certain models support Anthropic endpoint) + * + * @param provider - The provider to check + * @param modelId - The model ID to check (without provider prefix) + * @returns true if should use direct Anthropic SDK, false for unified SDK */ -function shouldUseDirectAnthropic(provider: Provider): boolean { - return provider.type === 'anthropic' || !!(provider.anthropicApiHost && provider.anthropicApiHost.trim()) +function shouldUseDirectAnthropic(provider: Provider, modelId: string): boolean { + // Native Anthropic provider - always use direct SDK + if (provider.type === 'anthropic') { + return true + } + + // No anthropicApiHost configured - use unified SDK + if (!provider.anthropicApiHost?.trim()) { + return false + } + + // Has anthropicApiHost - check model-level compatibility + // For aggregated providers, only specific models support Anthropic API + return isModelAnthropicCompatible(provider, modelId) } const logger = loggerService.withContext('ApiServerMessagesRoutes') @@ -169,11 +186,12 @@ async function handleUnifiedProcessing({ } /** - * Handle message processing - routes to appropriate handler based on provider + * Handle message processing - routes to appropriate handler based on provider and model * * Routing logic: - * - Providers with anthropicApiHost OR type 'anthropic': Direct Anthropic SDK (no conversion) - * - Other providers: Unified AI SDK with Anthropic SSE conversion + * - Native Anthropic providers (type === 'anthropic'): Direct Anthropic SDK + * - Providers with anthropicApiHost AND model supports Anthropic API: Direct Anthropic SDK + * - Other providers/models: Unified AI SDK with Anthropic SSE conversion */ async function handleMessageProcessing({ res, @@ -181,7 +199,8 @@ async function handleMessageProcessing({ request, modelId }: HandleMessageProcessingOptions): Promise { - if (shouldUseDirectAnthropic(provider)) { + const actualModelId = modelId || request.model + if (shouldUseDirectAnthropic(provider, actualModelId)) { return handleDirectAnthropicProcessing({ res, provider, request, modelId }) } return handleUnifiedProcessing({ res, provider, request, modelId }) diff --git a/src/main/apiServer/utils/index.ts b/src/main/apiServer/utils/index.ts index e25b49e750..471e734c18 100644 --- a/src/main/apiServer/utils/index.ts +++ b/src/main/apiServer/utils/index.ts @@ -295,3 +295,32 @@ export const getProviderAnthropicModelChecker = (providerId: string): ((m: Model return () => true } } + +/** + * Check if a specific model is compatible with Anthropic API for a given provider. + * + * This is used for fine-grained routing decisions at the model level. + * For aggregated providers (like Silicon), only certain models support the Anthropic API endpoint. + * + * @param provider - The provider to check + * @param modelId - The model ID to check (without provider prefix) + * @returns true if the model supports Anthropic API endpoint + */ +export function isModelAnthropicCompatible(provider: Provider, modelId: string): boolean { + const checker = getProviderAnthropicModelChecker(provider.id) + + const model = provider.models?.find((m) => m.id === modelId) + + if (model) { + return checker(model) + } + + const minimalModel: Model = { + id: modelId, + name: modelId, + provider: provider.id, + group: '' + } + + return checker(minimalModel) +} diff --git a/src/renderer/src/aiCore/provider/__tests__/providerConfig.test.ts b/src/renderer/src/aiCore/provider/__tests__/providerConfig.test.ts index 430ff52869..22ef654da8 100644 --- a/src/renderer/src/aiCore/provider/__tests__/providerConfig.test.ts +++ b/src/renderer/src/aiCore/provider/__tests__/providerConfig.test.ts @@ -24,7 +24,17 @@ vi.mock('@renderer/services/AssistantService', () => ({ vi.mock('@renderer/store', () => ({ default: { - getState: () => ({ copilot: { defaultHeaders: {} } }) + getState: () => ({ + copilot: { defaultHeaders: {} }, + llm: { + settings: { + vertexai: { + projectId: 'test-project', + location: 'us-central1' + } + } + } + }) } })) @@ -33,7 +43,7 @@ vi.mock('@renderer/utils/api', () => ({ if (isSupportedAPIVersion === false) { return host // Return host as-is when isSupportedAPIVersion is false } - return `${host}/v1` // Default behavior when isSupportedAPIVersion is true + return host ? `${host}/v1` : '' // Default behavior when isSupportedAPIVersion is true }), routeToEndpoint: vi.fn((host) => ({ baseURL: host, @@ -41,6 +51,20 @@ vi.mock('@renderer/utils/api', () => ({ })) })) +// Also mock @shared/api since formatProviderApiHost uses it directly +vi.mock('@shared/api', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + formatApiHost: vi.fn((host, isSupportedAPIVersion = true) => { + if (isSupportedAPIVersion === false) { + return host || '' // Return host as-is when isSupportedAPIVersion is false + } + return host ? `${host}/v1` : '' // Default behavior when isSupportedAPIVersion is true + }) + } +}) + vi.mock('@renderer/utils/provider', async (importOriginal) => { const actual = (await importOriginal()) as any return { @@ -73,8 +97,8 @@ vi.mock('@renderer/services/AssistantService', () => ({ import { getProviderByModel } from '@renderer/services/AssistantService' import type { Model, Provider } from '@renderer/types' -import { formatApiHost } from '@renderer/utils/api' import { isCherryAIProvider, isPerplexityProvider } from '@renderer/utils/provider' +import { formatApiHost } from '@shared/api' import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants' import { getActualProvider, providerToAiSdkConfig } from '../providerConfig' diff --git a/src/renderer/src/utils/__tests__/api.test.ts b/src/renderer/src/utils/__tests__/api.test.ts index e854445fc5..f56fb53d00 100644 --- a/src/renderer/src/utils/__tests__/api.test.ts +++ b/src/renderer/src/utils/__tests__/api.test.ts @@ -300,18 +300,7 @@ describe('api', () => { }) it('uses global endpoint when location equals global', () => { - getStateMock.mockReturnValueOnce({ - llm: { - settings: { - vertexai: { - projectId: 'global-project', - location: 'global' - } - } - } - }) - - expect(formatVertexApiHost(createVertexProvider(''))).toBe( + expect(formatVertexApiHost(createVertexProvider(''), 'global-project', 'global')).toBe( 'https://aiplatform.googleapis.com/v1/projects/global-project/locations/global' ) })