fix: test

This commit is contained in:
suyao 2025-11-27 20:55:36 +08:00
parent f225fbe3e3
commit 36ed062b84
No known key found for this signature in database
5 changed files with 86 additions and 30 deletions

View File

@ -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'

View File

@ -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<void> {
if (shouldUseDirectAnthropic(provider)) {
const actualModelId = modelId || request.model
if (shouldUseDirectAnthropic(provider, actualModelId)) {
return handleDirectAnthropicProcessing({ res, provider, request, modelId })
}
return handleUnifiedProcessing({ res, provider, request, modelId })

View File

@ -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)
}

View File

@ -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'

View File

@ -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'
)
})