mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 11:44:28 +08:00
Merge remote-tracking branch 'origin/main' into feat/proxy-api-server
This commit is contained in:
commit
313be4427b
@ -620,7 +620,7 @@ class McpService {
|
||||
tools.map((tool: SDKTool) => {
|
||||
const serverTool: MCPTool = {
|
||||
...tool,
|
||||
id: buildFunctionCallToolName(server.name, tool.name),
|
||||
id: buildFunctionCallToolName(server.name, tool.name, server.id),
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
type: 'mcp'
|
||||
|
||||
196
src/main/utils/__tests__/mcp.test.ts
Normal file
196
src/main/utils/__tests__/mcp.test.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildFunctionCallToolName } from '../mcp'
|
||||
|
||||
describe('buildFunctionCallToolName', () => {
|
||||
describe('basic functionality', () => {
|
||||
it('should combine server name and tool name', () => {
|
||||
const result = buildFunctionCallToolName('github', 'search_issues')
|
||||
expect(result).toContain('github')
|
||||
expect(result).toContain('search')
|
||||
})
|
||||
|
||||
it('should sanitize names by replacing dashes with underscores', () => {
|
||||
const result = buildFunctionCallToolName('my-server', 'my-tool')
|
||||
// Input dashes are replaced, but the separator between server and tool is a dash
|
||||
expect(result).toBe('my_serv-my_tool')
|
||||
expect(result).toContain('_')
|
||||
})
|
||||
|
||||
it('should handle empty server names gracefully', () => {
|
||||
const result = buildFunctionCallToolName('', 'tool')
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('uniqueness with serverId', () => {
|
||||
it('should generate different IDs for same server name but different serverIds', () => {
|
||||
const serverId1 = 'server-id-123456'
|
||||
const serverId2 = 'server-id-789012'
|
||||
const serverName = 'github'
|
||||
const toolName = 'search_repos'
|
||||
|
||||
const result1 = buildFunctionCallToolName(serverName, toolName, serverId1)
|
||||
const result2 = buildFunctionCallToolName(serverName, toolName, serverId2)
|
||||
|
||||
expect(result1).not.toBe(result2)
|
||||
expect(result1).toContain('123456')
|
||||
expect(result2).toContain('789012')
|
||||
})
|
||||
|
||||
it('should generate same ID when serverId is not provided', () => {
|
||||
const serverName = 'github'
|
||||
const toolName = 'search_repos'
|
||||
|
||||
const result1 = buildFunctionCallToolName(serverName, toolName)
|
||||
const result2 = buildFunctionCallToolName(serverName, toolName)
|
||||
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
it('should include serverId suffix when provided', () => {
|
||||
const serverId = 'abc123def456'
|
||||
const result = buildFunctionCallToolName('server', 'tool', serverId)
|
||||
|
||||
// Should include last 6 chars of serverId
|
||||
expect(result).toContain('ef456')
|
||||
})
|
||||
})
|
||||
|
||||
describe('character sanitization', () => {
|
||||
it('should replace invalid characters with underscores', () => {
|
||||
const result = buildFunctionCallToolName('test@server', 'tool#name')
|
||||
expect(result).not.toMatch(/[@#]/)
|
||||
expect(result).toMatch(/^[a-zA-Z0-9_-]+$/)
|
||||
})
|
||||
|
||||
it('should ensure name starts with a letter', () => {
|
||||
const result = buildFunctionCallToolName('123server', '456tool')
|
||||
expect(result).toMatch(/^[a-zA-Z]/)
|
||||
})
|
||||
|
||||
it('should handle consecutive underscores/dashes', () => {
|
||||
const result = buildFunctionCallToolName('my--server', 'my__tool')
|
||||
expect(result).not.toMatch(/[_-]{2,}/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('length constraints', () => {
|
||||
it('should truncate names longer than 63 characters', () => {
|
||||
const longServerName = 'a'.repeat(50)
|
||||
const longToolName = 'b'.repeat(50)
|
||||
const result = buildFunctionCallToolName(longServerName, longToolName, 'id123456')
|
||||
|
||||
expect(result.length).toBeLessThanOrEqual(63)
|
||||
})
|
||||
|
||||
it('should not end with underscore or dash after truncation', () => {
|
||||
const longServerName = 'a'.repeat(50)
|
||||
const longToolName = 'b'.repeat(50)
|
||||
const result = buildFunctionCallToolName(longServerName, longToolName, 'id123456')
|
||||
|
||||
expect(result).not.toMatch(/[_-]$/)
|
||||
})
|
||||
|
||||
it('should preserve serverId suffix even with long server/tool names', () => {
|
||||
const longServerName = 'a'.repeat(50)
|
||||
const longToolName = 'b'.repeat(50)
|
||||
const serverId = 'server-id-xyz789'
|
||||
|
||||
const result = buildFunctionCallToolName(longServerName, longToolName, serverId)
|
||||
|
||||
// The suffix should be preserved and not truncated
|
||||
expect(result).toContain('xyz789')
|
||||
expect(result.length).toBeLessThanOrEqual(63)
|
||||
})
|
||||
|
||||
it('should ensure two long-named servers with different IDs produce different results', () => {
|
||||
const longServerName = 'a'.repeat(50)
|
||||
const longToolName = 'b'.repeat(50)
|
||||
const serverId1 = 'server-id-abc123'
|
||||
const serverId2 = 'server-id-def456'
|
||||
|
||||
const result1 = buildFunctionCallToolName(longServerName, longToolName, serverId1)
|
||||
const result2 = buildFunctionCallToolName(longServerName, longToolName, serverId2)
|
||||
|
||||
// Both should be within limit
|
||||
expect(result1.length).toBeLessThanOrEqual(63)
|
||||
expect(result2.length).toBeLessThanOrEqual(63)
|
||||
|
||||
// They should be different due to preserved suffix
|
||||
expect(result1).not.toBe(result2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases with serverId', () => {
|
||||
it('should handle serverId with only non-alphanumeric characters', () => {
|
||||
const serverId = '------' // All dashes
|
||||
const result = buildFunctionCallToolName('server', 'tool', serverId)
|
||||
|
||||
// Should still produce a valid unique suffix via fallback hash
|
||||
expect(result).toBeTruthy()
|
||||
expect(result.length).toBeLessThanOrEqual(63)
|
||||
expect(result).toMatch(/^[a-zA-Z][a-zA-Z0-9_-]*$/)
|
||||
// Should have a suffix (underscore followed by something)
|
||||
expect(result).toMatch(/_[a-z0-9]+$/)
|
||||
})
|
||||
|
||||
it('should produce different results for different non-alphanumeric serverIds', () => {
|
||||
const serverId1 = '------'
|
||||
const serverId2 = '!!!!!!'
|
||||
|
||||
const result1 = buildFunctionCallToolName('server', 'tool', serverId1)
|
||||
const result2 = buildFunctionCallToolName('server', 'tool', serverId2)
|
||||
|
||||
// Should be different because the hash fallback produces different values
|
||||
expect(result1).not.toBe(result2)
|
||||
})
|
||||
|
||||
it('should handle empty string serverId differently from undefined', () => {
|
||||
const resultWithEmpty = buildFunctionCallToolName('server', 'tool', '')
|
||||
const resultWithUndefined = buildFunctionCallToolName('server', 'tool', undefined)
|
||||
|
||||
// Empty string is falsy, so both should behave the same (no suffix)
|
||||
expect(resultWithEmpty).toBe(resultWithUndefined)
|
||||
})
|
||||
|
||||
it('should handle serverId with mixed alphanumeric and special chars', () => {
|
||||
const serverId = 'ab@#cd' // Mixed chars, last 6 chars contain some alphanumeric
|
||||
const result = buildFunctionCallToolName('server', 'tool', serverId)
|
||||
|
||||
// Should extract alphanumeric chars: 'abcd' from 'ab@#cd'
|
||||
expect(result).toContain('abcd')
|
||||
})
|
||||
})
|
||||
|
||||
describe('real-world scenarios', () => {
|
||||
it('should handle GitHub MCP server instances correctly', () => {
|
||||
const serverName = 'github'
|
||||
const toolName = 'search_repositories'
|
||||
|
||||
const githubComId = 'server-github-com-abc123'
|
||||
const gheId = 'server-ghe-internal-xyz789'
|
||||
|
||||
const tool1 = buildFunctionCallToolName(serverName, toolName, githubComId)
|
||||
const tool2 = buildFunctionCallToolName(serverName, toolName, gheId)
|
||||
|
||||
// Should be different
|
||||
expect(tool1).not.toBe(tool2)
|
||||
|
||||
// Both should be valid identifiers
|
||||
expect(tool1).toMatch(/^[a-zA-Z][a-zA-Z0-9_-]*$/)
|
||||
expect(tool2).toMatch(/^[a-zA-Z][a-zA-Z0-9_-]*$/)
|
||||
|
||||
// Both should be <= 63 chars
|
||||
expect(tool1.length).toBeLessThanOrEqual(63)
|
||||
expect(tool2.length).toBeLessThanOrEqual(63)
|
||||
})
|
||||
|
||||
it('should handle tool names that already include server name prefix', () => {
|
||||
const result = buildFunctionCallToolName('github', 'github_search_repos')
|
||||
expect(result).toBeTruthy()
|
||||
// Should not double the server name
|
||||
expect(result.split('github').length - 1).toBeLessThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,7 +1,25 @@
|
||||
export function buildFunctionCallToolName(serverName: string, toolName: string) {
|
||||
export function buildFunctionCallToolName(serverName: string, toolName: string, serverId?: string) {
|
||||
const sanitizedServer = serverName.trim().replace(/-/g, '_')
|
||||
const sanitizedTool = toolName.trim().replace(/-/g, '_')
|
||||
|
||||
// Calculate suffix first to reserve space for it
|
||||
// Suffix format: "_" + 6 alphanumeric chars = 7 chars total
|
||||
let serverIdSuffix = ''
|
||||
if (serverId) {
|
||||
// Take the last 6 characters of the serverId for brevity
|
||||
serverIdSuffix = serverId.slice(-6).replace(/[^a-zA-Z0-9]/g, '')
|
||||
|
||||
// Fallback: if suffix becomes empty (all non-alphanumeric chars), use a simple hash
|
||||
if (!serverIdSuffix) {
|
||||
const hash = serverId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
|
||||
serverIdSuffix = hash.toString(36).slice(-6) || 'x'
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve space for suffix when calculating max base name length
|
||||
const SUFFIX_LENGTH = serverIdSuffix ? serverIdSuffix.length + 1 : 0 // +1 for underscore
|
||||
const MAX_BASE_LENGTH = 63 - SUFFIX_LENGTH
|
||||
|
||||
// Combine server name and tool name
|
||||
let name = sanitizedTool
|
||||
if (!sanitizedTool.includes(sanitizedServer.slice(0, 7))) {
|
||||
@ -20,9 +38,9 @@ export function buildFunctionCallToolName(serverName: string, toolName: string)
|
||||
// Remove consecutive underscores/dashes (optional improvement)
|
||||
name = name.replace(/[_-]{2,}/g, '_')
|
||||
|
||||
// Truncate to 63 characters maximum
|
||||
if (name.length > 63) {
|
||||
name = name.slice(0, 63)
|
||||
// Truncate base name BEFORE adding suffix to ensure suffix is never cut off
|
||||
if (name.length > MAX_BASE_LENGTH) {
|
||||
name = name.slice(0, MAX_BASE_LENGTH)
|
||||
}
|
||||
|
||||
// Handle edge case: ensure we still have a valid name if truncation left invalid chars at edges
|
||||
@ -30,5 +48,10 @@ export function buildFunctionCallToolName(serverName: string, toolName: string)
|
||||
name = name.slice(0, -1)
|
||||
}
|
||||
|
||||
// Now append the suffix - it will always fit within 63 chars
|
||||
if (serverIdSuffix) {
|
||||
name = `${name}_${serverIdSuffix}`
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ export function addAnthropicHeaders(assistant: Assistant, model: Model): string[
|
||||
if (
|
||||
isClaude45ReasoningModel(model) &&
|
||||
isToolUseModeFunction(assistant) &&
|
||||
!(isVertexProvider(provider) && isAwsBedrockProvider(provider))
|
||||
!(isVertexProvider(provider) || isAwsBedrockProvider(provider))
|
||||
) {
|
||||
anthropicHeaders.push(INTERLEAVED_THINKING_HEADER)
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
|
||||
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
|
||||
import { replacePromptVariables } from '@renderer/utils/prompt'
|
||||
import { isAwsBedrockProvider } from '@renderer/utils/provider'
|
||||
import type { ModelMessage, Tool } from 'ai'
|
||||
import { stepCountIs } from 'ai'
|
||||
|
||||
@ -175,7 +176,7 @@ export async function buildStreamTextParams(
|
||||
|
||||
let headers: Record<string, string | undefined> = options.requestOptions?.headers ?? {}
|
||||
|
||||
if (isAnthropicModel(model)) {
|
||||
if (isAnthropicModel(model) && !isAwsBedrockProvider(provider)) {
|
||||
const newBetaHeaders = { 'anthropic-beta': addAnthropicHeaders(assistant, model).join(',') }
|
||||
headers = combineHeaders(headers, newBetaHeaders)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Provider } from '@renderer/types'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getAiSdkProviderId } from '../factory'
|
||||
@ -68,6 +68,18 @@ function createTestProvider(id: string, type: string): Provider {
|
||||
} as Provider
|
||||
}
|
||||
|
||||
function createAzureProvider(id: string, apiVersion?: string, model?: string): Provider {
|
||||
return {
|
||||
id,
|
||||
type: 'azure-openai',
|
||||
name: `Azure Test ${id}`,
|
||||
apiKey: 'azure-test-key',
|
||||
apiHost: 'azure-test-host',
|
||||
apiVersion,
|
||||
models: [{ id: model || 'gpt-4' } as Model]
|
||||
}
|
||||
}
|
||||
|
||||
describe('Integrated Provider Registry', () => {
|
||||
describe('Provider ID Resolution', () => {
|
||||
it('should resolve openrouter provider correctly', () => {
|
||||
@ -111,6 +123,24 @@ describe('Integrated Provider Registry', () => {
|
||||
const result = getAiSdkProviderId(unknownProvider)
|
||||
expect(result).toBe('unknown-provider')
|
||||
})
|
||||
|
||||
it('should handle Azure OpenAI providers correctly', () => {
|
||||
const azureProvider = createAzureProvider('azure-test', '2024-02-15', 'gpt-4o')
|
||||
const result = getAiSdkProviderId(azureProvider)
|
||||
expect(result).toBe('azure')
|
||||
})
|
||||
|
||||
it('should handle Azure OpenAI providers response endpoint correctly', () => {
|
||||
const azureProvider = createAzureProvider('azure-test', 'v1', 'gpt-4o')
|
||||
const result = getAiSdkProviderId(azureProvider)
|
||||
expect(result).toBe('azure-responses')
|
||||
})
|
||||
|
||||
it('should handle Azure provider Claude Models', () => {
|
||||
const provider = createTestProvider('azure-anthropic', 'anthropic')
|
||||
const result = getAiSdkProviderId(provider)
|
||||
expect(result).toBe('azure-anthropic')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Backward Compatibility', () => {
|
||||
|
||||
@ -71,10 +71,11 @@ describe('mcp utils', () => {
|
||||
const result = setupToolsConfig(mcpTools)
|
||||
|
||||
expect(result).not.toBeUndefined()
|
||||
expect(Object.keys(result!)).toEqual(['test-tool'])
|
||||
expect(result!['test-tool']).toHaveProperty('description')
|
||||
expect(result!['test-tool']).toHaveProperty('inputSchema')
|
||||
expect(result!['test-tool']).toHaveProperty('execute')
|
||||
// Tools are now keyed by id (which includes serverId suffix) for uniqueness
|
||||
expect(Object.keys(result!)).toEqual(['test-tool-1'])
|
||||
expect(result!['test-tool-1']).toHaveProperty('description')
|
||||
expect(result!['test-tool-1']).toHaveProperty('inputSchema')
|
||||
expect(result!['test-tool-1']).toHaveProperty('execute')
|
||||
})
|
||||
|
||||
it('should handle multiple MCP tools', () => {
|
||||
@ -109,7 +110,8 @@ describe('mcp utils', () => {
|
||||
|
||||
expect(result).not.toBeUndefined()
|
||||
expect(Object.keys(result!)).toHaveLength(2)
|
||||
expect(Object.keys(result!)).toEqual(['tool1', 'tool2'])
|
||||
// Tools are keyed by id for uniqueness
|
||||
expect(Object.keys(result!)).toEqual(['tool1-id', 'tool2-id'])
|
||||
})
|
||||
})
|
||||
|
||||
@ -135,9 +137,10 @@ describe('mcp utils', () => {
|
||||
|
||||
const result = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
|
||||
expect(Object.keys(result)).toEqual(['get-weather'])
|
||||
// Tools are keyed by id for uniqueness when multiple server instances exist
|
||||
expect(Object.keys(result)).toEqual(['get-weather-id'])
|
||||
|
||||
const tool = result['get-weather'] as Tool
|
||||
const tool = result['get-weather-id'] as Tool
|
||||
expect(tool.description).toBe('Get weather information')
|
||||
expect(tool.inputSchema).toBeDefined()
|
||||
expect(typeof tool.execute).toBe('function')
|
||||
@ -160,8 +163,8 @@ describe('mcp utils', () => {
|
||||
|
||||
const result = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
|
||||
expect(Object.keys(result)).toEqual(['no-desc-tool'])
|
||||
const tool = result['no-desc-tool'] as Tool
|
||||
expect(Object.keys(result)).toEqual(['no-desc-tool-id'])
|
||||
const tool = result['no-desc-tool-id'] as Tool
|
||||
expect(tool.description).toBe('Tool from test-server')
|
||||
})
|
||||
|
||||
@ -202,13 +205,13 @@ describe('mcp utils', () => {
|
||||
|
||||
const result = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
|
||||
expect(Object.keys(result)).toEqual(['complex-tool'])
|
||||
const tool = result['complex-tool'] as Tool
|
||||
expect(Object.keys(result)).toEqual(['complex-tool-id'])
|
||||
const tool = result['complex-tool-id'] as Tool
|
||||
expect(tool.inputSchema).toBeDefined()
|
||||
expect(typeof tool.execute).toBe('function')
|
||||
})
|
||||
|
||||
it('should preserve tool names with special characters', () => {
|
||||
it('should preserve tool id with special characters', () => {
|
||||
const mcpTools: MCPTool[] = [
|
||||
{
|
||||
id: 'special-tool-id',
|
||||
@ -225,7 +228,8 @@ describe('mcp utils', () => {
|
||||
]
|
||||
|
||||
const result = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
expect(Object.keys(result)).toEqual(['tool_with-special.chars'])
|
||||
// Tools are keyed by id for uniqueness
|
||||
expect(Object.keys(result)).toEqual(['special-tool-id'])
|
||||
})
|
||||
|
||||
it('should handle multiple tools with different schemas', () => {
|
||||
@ -276,10 +280,11 @@ describe('mcp utils', () => {
|
||||
|
||||
const result = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
|
||||
expect(Object.keys(result).sort()).toEqual(['boolean-tool', 'number-tool', 'string-tool'])
|
||||
expect(result['string-tool']).toBeDefined()
|
||||
expect(result['number-tool']).toBeDefined()
|
||||
expect(result['boolean-tool']).toBeDefined()
|
||||
// Tools are keyed by id for uniqueness
|
||||
expect(Object.keys(result).sort()).toEqual(['boolean-tool-id', 'number-tool-id', 'string-tool-id'])
|
||||
expect(result['string-tool-id']).toBeDefined()
|
||||
expect(result['number-tool-id']).toBeDefined()
|
||||
expect(result['boolean-tool-id']).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@ -310,7 +315,7 @@ describe('mcp utils', () => {
|
||||
]
|
||||
|
||||
const tools = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
const tool = tools['test-exec-tool'] as Tool
|
||||
const tool = tools['test-exec-tool-id'] as Tool
|
||||
const result = await tool.execute!({}, { messages: [], abortSignal: undefined, toolCallId: 'test-call-123' })
|
||||
|
||||
expect(requestToolConfirmation).toHaveBeenCalled()
|
||||
@ -343,7 +348,7 @@ describe('mcp utils', () => {
|
||||
]
|
||||
|
||||
const tools = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
const tool = tools['cancelled-tool'] as Tool
|
||||
const tool = tools['cancelled-tool-id'] as Tool
|
||||
const result = await tool.execute!({}, { messages: [], abortSignal: undefined, toolCallId: 'cancel-call-123' })
|
||||
|
||||
expect(requestToolConfirmation).toHaveBeenCalled()
|
||||
@ -385,7 +390,7 @@ describe('mcp utils', () => {
|
||||
]
|
||||
|
||||
const tools = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
const tool = tools['error-tool'] as Tool
|
||||
const tool = tools['error-tool-id'] as Tool
|
||||
|
||||
await expect(
|
||||
tool.execute!({}, { messages: [], abortSignal: undefined, toolCallId: 'error-call-123' })
|
||||
@ -421,7 +426,7 @@ describe('mcp utils', () => {
|
||||
]
|
||||
|
||||
const tools = convertMcpToolsToAiSdkTools(mcpTools)
|
||||
const tool = tools['auto-approve-tool'] as Tool
|
||||
const tool = tools['auto-approve-tool-id'] as Tool
|
||||
const result = await tool.execute!({}, { messages: [], abortSignal: undefined, toolCallId: 'auto-call-123' })
|
||||
|
||||
expect(requestToolConfirmation).not.toHaveBeenCalled()
|
||||
|
||||
@ -154,6 +154,10 @@ vi.mock('../websearch', () => ({
|
||||
getWebSearchParams: vi.fn(() => ({ enable_search: true }))
|
||||
}))
|
||||
|
||||
vi.mock('../../prepareParams/header', () => ({
|
||||
addAnthropicHeaders: vi.fn(() => ['context-1m-2025-08-07'])
|
||||
}))
|
||||
|
||||
const ensureWindowApi = () => {
|
||||
const globalWindow = window as any
|
||||
globalWindow.api = globalWindow.api || {}
|
||||
@ -633,5 +637,64 @@ describe('options utils', () => {
|
||||
expect(result.providerOptions).toHaveProperty('anthropic')
|
||||
})
|
||||
})
|
||||
|
||||
describe('AWS Bedrock provider', () => {
|
||||
const bedrockProvider = {
|
||||
id: 'bedrock',
|
||||
name: 'AWS Bedrock',
|
||||
type: 'aws-bedrock',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://bedrock.us-east-1.amazonaws.com',
|
||||
models: [] as Model[]
|
||||
} as Provider
|
||||
|
||||
const bedrockModel: Model = {
|
||||
id: 'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
name: 'Claude Sonnet 4',
|
||||
provider: 'bedrock'
|
||||
} as Model
|
||||
|
||||
it('should build basic Bedrock options', () => {
|
||||
const result = buildProviderOptions(mockAssistant, bedrockModel, bedrockProvider, {
|
||||
enableReasoning: false,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.providerOptions).toHaveProperty('bedrock')
|
||||
expect(result.providerOptions.bedrock).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include anthropicBeta when Anthropic headers are needed', async () => {
|
||||
const { addAnthropicHeaders } = await import('../../prepareParams/header')
|
||||
vi.mocked(addAnthropicHeaders).mockReturnValue(['interleaved-thinking-2025-05-14', 'context-1m-2025-08-07'])
|
||||
|
||||
const result = buildProviderOptions(mockAssistant, bedrockModel, bedrockProvider, {
|
||||
enableReasoning: false,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.providerOptions.bedrock).toHaveProperty('anthropicBeta')
|
||||
expect(result.providerOptions.bedrock.anthropicBeta).toEqual([
|
||||
'interleaved-thinking-2025-05-14',
|
||||
'context-1m-2025-08-07'
|
||||
])
|
||||
})
|
||||
|
||||
it('should include reasoning parameters when enabled', () => {
|
||||
const result = buildProviderOptions(mockAssistant, bedrockModel, bedrockProvider, {
|
||||
enableReasoning: true,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.providerOptions.bedrock).toHaveProperty('reasoningConfig')
|
||||
expect(result.providerOptions.bedrock.reasoningConfig).toEqual({
|
||||
type: 'enabled',
|
||||
budgetTokens: 5000
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,7 +28,9 @@ export function convertMcpToolsToAiSdkTools(mcpTools: MCPTool[]): ToolSet {
|
||||
const tools: ToolSet = {}
|
||||
|
||||
for (const mcpTool of mcpTools) {
|
||||
tools[mcpTool.name] = tool({
|
||||
// Use mcpTool.id (which includes serverId suffix) to ensure uniqueness
|
||||
// when multiple instances of the same MCP server type are configured
|
||||
tools[mcpTool.id] = tool({
|
||||
description: mcpTool.description || `Tool from ${mcpTool.serverName}`,
|
||||
inputSchema: jsonSchema(mcpTool.inputSchema as JSONSchema7),
|
||||
execute: async (params, { toolCallId }) => {
|
||||
|
||||
@ -36,6 +36,7 @@ import { isSupportServiceTierProvider, isSupportVerbosityProvider } from '@rende
|
||||
import type { JSONValue } from 'ai'
|
||||
import { t } from 'i18next'
|
||||
|
||||
import { addAnthropicHeaders } from '../prepareParams/header'
|
||||
import { getAiSdkProviderId } from '../provider/factory'
|
||||
import { buildGeminiGenerateImageParams } from './image'
|
||||
import {
|
||||
@ -469,6 +470,11 @@ function buildBedrockProviderOptions(
|
||||
}
|
||||
}
|
||||
|
||||
const betaHeaders = addAnthropicHeaders(assistant, model)
|
||||
if (betaHeaders.length > 0) {
|
||||
providerOptions.anthropicBeta = betaHeaders
|
||||
}
|
||||
|
||||
return providerOptions
|
||||
}
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ const PopupContainer: React.FC<Props> = ({ model, apiFilter, modelFilter, showTa
|
||||
const [_searchText, setSearchText] = useState('')
|
||||
const searchText = useDeferredValue(_searchText)
|
||||
const { models, isLoading } = useApiModels(apiFilter)
|
||||
const adaptedModels = models.map((model) => apiModelAdapter(model))
|
||||
const adaptedModels = useMemo(() => models.map((model) => apiModelAdapter(model)), [models])
|
||||
|
||||
// 当前选中的模型ID
|
||||
const currentModelId = model ? model.id : ''
|
||||
|
||||
@ -309,11 +309,14 @@ describe('Ling Models', () => {
|
||||
describe('Claude & regional providers', () => {
|
||||
it('identifies claude 4.5 variants', () => {
|
||||
expect(isClaude45ReasoningModel(createModel({ id: 'claude-sonnet-4.5-preview' }))).toBe(true)
|
||||
expect(isClaude4SeriesModel(createModel({ id: 'claude-sonnet-4-5@20250929' }))).toBe(true)
|
||||
expect(isClaude45ReasoningModel(createModel({ id: 'claude-3-sonnet' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('identifies claude 4 variants', () => {
|
||||
expect(isClaude4SeriesModel(createModel({ id: 'claude-opus-4' }))).toBe(true)
|
||||
expect(isClaude4SeriesModel(createModel({ id: 'claude-sonnet-4@20250514' }))).toBe(true)
|
||||
expect(isClaude4SeriesModel(createModel({ id: 'anthropic.claude-sonnet-4-20250514-v1:0' }))).toBe(true)
|
||||
expect(isClaude4SeriesModel(createModel({ id: 'claude-4.2-sonnet-variant' }))).toBe(false)
|
||||
expect(isClaude4SeriesModel(createModel({ id: 'claude-3-haiku' }))).toBe(false)
|
||||
})
|
||||
|
||||
@ -396,7 +396,11 @@ export function isClaude45ReasoningModel(model: Model): boolean {
|
||||
|
||||
export function isClaude4SeriesModel(model: Model): boolean {
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
const regex = /claude-(sonnet|opus|haiku)-4(?:[.-]\d+)?(?:-[\w-]+)?$/i
|
||||
// Supports various formats including:
|
||||
// - Direct API: claude-sonnet-4, claude-opus-4-20250514
|
||||
// - GCP Vertex AI: claude-sonnet-4@20250514
|
||||
// - AWS Bedrock: anthropic.claude-sonnet-4-20250514-v1:0
|
||||
const regex = /claude-(sonnet|opus|haiku)-4(?:[.-]\d+)?(?:[@\-:][\w\-:]+)?$/i
|
||||
return regex.test(modelId)
|
||||
}
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ export function BashOutputTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashOutputToolInput
|
||||
input?: BashOutputToolInput
|
||||
output?: BashOutputToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
const parsedOutput = parseBashOutput(output)
|
||||
@ -144,7 +144,7 @@ export function BashOutputTool({
|
||||
label="Bash Output"
|
||||
params={
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="py-0 font-mono text-xs">{input.bash_id}</Tag>
|
||||
<Tag className="py-0 font-mono text-xs">{input?.bash_id}</Tag>
|
||||
{statusConfig && (
|
||||
<Tag
|
||||
color={statusConfig.color}
|
||||
|
||||
@ -11,14 +11,14 @@ export function BashTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashToolInputType
|
||||
input?: BashToolInputType
|
||||
output?: BashToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算输出行数
|
||||
const outputLines = output ? output.split('\n').length : 0
|
||||
|
||||
// 处理命令字符串的截断
|
||||
const command = input.command
|
||||
// 处理命令字符串的截断,添加空值检查
|
||||
const command = input?.command ?? ''
|
||||
const needsTruncate = command.length > MAX_TAG_LENGTH
|
||||
const displayCommand = needsTruncate ? `${command.slice(0, MAX_TAG_LENGTH)}...` : command
|
||||
|
||||
@ -31,7 +31,7 @@ export function BashTool({
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash"
|
||||
params={input.description}
|
||||
params={input?.description}
|
||||
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
<div className="mt-1">
|
||||
|
||||
@ -32,19 +32,19 @@ export function EditTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: EditToolInput
|
||||
input?: EditToolInput
|
||||
output?: EditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.Edit,
|
||||
label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />,
|
||||
label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input?.file_path} />,
|
||||
children: (
|
||||
<>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{renderCodeBlock(input?.old_string ?? '', 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{renderCodeBlock(input?.new_string ?? '', 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</>
|
||||
|
||||
@ -10,18 +10,19 @@ export function ExitPlanModeTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: ExitPlanModeToolInput
|
||||
input?: ExitPlanModeToolInput
|
||||
output?: ExitPlanModeToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
const plan = input?.plan ?? ''
|
||||
return {
|
||||
key: AgentToolsType.ExitPlanMode,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
stats={`${plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
),
|
||||
children: <ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>
|
||||
children: <ReactMarkdown>{plan + '\n\n' + (output ?? '')}</ReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ export function GlobTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GlobToolInputType
|
||||
input?: GlobToolInputType
|
||||
output?: GlobToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算文件数量
|
||||
@ -20,7 +20,7 @@ export function GlobTool({
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
params={input?.pattern}
|
||||
stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -8,7 +8,7 @@ export function GrepTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GrepToolInput
|
||||
input?: GrepToolInput
|
||||
output?: GrepToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果行数
|
||||
@ -22,8 +22,8 @@ export function GrepTool({
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
{input?.pattern}
|
||||
{input?.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
|
||||
@ -9,18 +9,19 @@ import { AgentToolsType } from './types'
|
||||
export function MultiEditTool({
|
||||
input
|
||||
}: {
|
||||
input: MultiEditToolInput
|
||||
input?: MultiEditToolInput
|
||||
output?: MultiEditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
const edits = Array.isArray(input?.edits) ? input.edits : []
|
||||
return {
|
||||
key: AgentToolsType.MultiEdit,
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />,
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input?.file_path} />,
|
||||
children: (
|
||||
<div>
|
||||
{input.edits.map((edit, index) => (
|
||||
{edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
{renderCodeBlock(edit.old_string ?? '', 'old')}
|
||||
{renderCodeBlock(edit.new_string ?? '', 'new')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -11,7 +11,7 @@ export function NotebookEditTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: NotebookEditToolInput
|
||||
input?: NotebookEditToolInput
|
||||
output?: NotebookEditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
@ -20,10 +20,10 @@ export function NotebookEditTool({
|
||||
<>
|
||||
<ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" />
|
||||
<Tag className="mt-1" color="blue">
|
||||
{input.notebook_path}{' '}
|
||||
{input?.notebook_path}{' '}
|
||||
</Tag>
|
||||
</>
|
||||
),
|
||||
children: <ReactMarkdown>{output}</ReactMarkdown>
|
||||
children: <ReactMarkdown>{output ?? ''}</ReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ export function ReadTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: ReadToolInputType
|
||||
input?: ReadToolInputType
|
||||
output?: ReadToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
const outputString = normalizeOutputString(output)
|
||||
@ -58,7 +58,7 @@ export function ReadTool({
|
||||
<ToolTitle
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
label="Read File"
|
||||
params={input.file_path.split('/').pop()}
|
||||
params={input?.file_path?.split('/').pop()}
|
||||
stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -8,7 +8,7 @@ export function SearchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: SearchToolInputType
|
||||
input?: SearchToolInputType
|
||||
output?: SearchToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果数量
|
||||
@ -20,13 +20,13 @@ export function SearchTool({
|
||||
<ToolTitle
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search"
|
||||
params={`"${input}"`}
|
||||
params={input ? `"${input}"` : undefined}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<StringInputTool input={input} label="Search Query" />
|
||||
{input && <StringInputTool input={input} label="Search Query" />}
|
||||
{output && (
|
||||
<div>
|
||||
<StringOutputTool output={output} label="Search Results" textColor="text-yellow-600 dark:text-yellow-400" />
|
||||
|
||||
@ -8,12 +8,12 @@ export function SkillTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: SkillToolInput
|
||||
input?: SkillToolInput
|
||||
output?: SkillToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />,
|
||||
label: <ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input?.command} />,
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,19 +9,20 @@ export function TaskTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: TaskToolInputType
|
||||
input?: TaskToolInputType
|
||||
output?: TaskToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />,
|
||||
label: <ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input?.description} />,
|
||||
children: (
|
||||
<div>
|
||||
{output?.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
{Array.isArray(output) &&
|
||||
output.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,9 +38,10 @@ const getStatusConfig = (status: TodoItem['status']) => {
|
||||
export function TodoWriteTool({
|
||||
input
|
||||
}: {
|
||||
input: TodoWriteToolInputType
|
||||
input?: TodoWriteToolInputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
const doneCount = input.todos.filter((todo) => todo.status === 'completed').length
|
||||
const todos = Array.isArray(input?.todos) ? input.todos : []
|
||||
const doneCount = todos.filter((todo) => todo.status === 'completed').length
|
||||
|
||||
return {
|
||||
key: AgentToolsType.TodoWrite,
|
||||
@ -49,12 +50,12 @@ export function TodoWriteTool({
|
||||
icon={<ListTodo className="h-4 w-4" />}
|
||||
label="Todo Write"
|
||||
params={`${doneCount} Done`}
|
||||
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
|
||||
stats={`${todos.length} ${todos.length === 1 ? 'item' : 'items'}`}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
{input.todos.map((todo, index) => {
|
||||
{todos.map((todo, index) => {
|
||||
const statusConfig = getStatusConfig(todo.status)
|
||||
return (
|
||||
<div key={index}>
|
||||
|
||||
@ -8,12 +8,12 @@ export function WebFetchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: WebFetchToolInput
|
||||
input?: WebFetchToolInput
|
||||
output?: WebFetchToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input.url} />,
|
||||
label: <ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input?.url} />,
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ export function WebSearchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: WebSearchToolInput
|
||||
input?: WebSearchToolInput
|
||||
output?: WebSearchToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果数量
|
||||
@ -20,7 +20,7 @@ export function WebSearchTool({
|
||||
<ToolTitle
|
||||
icon={<Globe className="h-4 w-4" />}
|
||||
label="Web Search"
|
||||
params={input.query}
|
||||
params={input?.query}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -7,12 +7,12 @@ import type { WriteToolInput, WriteToolOutput } from './types'
|
||||
export function WriteTool({
|
||||
input
|
||||
}: {
|
||||
input: WriteToolInput
|
||||
input?: WriteToolInput
|
||||
output?: WriteToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input.file_path} />,
|
||||
children: <div>{input.content}</div>
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input?.file_path} />,
|
||||
children: <div>{input?.content}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
DEFAULT_TEMPERATURE,
|
||||
MAX_CONTEXT_COUNT
|
||||
} from '@renderer/config/constant'
|
||||
import { isOpenAIModel } from '@renderer/config/models'
|
||||
import { isOpenAIModel, isSupportVerbosityModel } from '@renderer/config/models'
|
||||
import { UNKNOWN } from '@renderer/config/translate'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
@ -56,7 +56,7 @@ import type { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from
|
||||
import { isGroqSystemProvider, ThemeMode } from '@renderer/types'
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
|
||||
import { isSupportServiceTierProvider } from '@renderer/utils/provider'
|
||||
import { isSupportServiceTierProvider, isSupportVerbosityProvider } from '@renderer/utils/provider'
|
||||
import { Button, Col, InputNumber, Row, Slider, Switch } from 'antd'
|
||||
import { Settings2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
@ -183,7 +183,10 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
|
||||
const model = assistant.model || getDefaultModel()
|
||||
|
||||
const showOpenAiSettings = isOpenAIModel(model) || isSupportServiceTierProvider(provider)
|
||||
const showOpenAiSettings =
|
||||
isOpenAIModel(model) ||
|
||||
isSupportServiceTierProvider(provider) ||
|
||||
(isSupportVerbosityModel(model) && isSupportVerbosityProvider(provider))
|
||||
|
||||
return (
|
||||
<Container className="settings-tab">
|
||||
|
||||
@ -81,15 +81,21 @@ export const isSupportEnableThinkingProvider = (provider: Provider) => {
|
||||
)
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_SERVICE_TIER_PROVIDERS = ['github', 'copilot', 'cerebras'] as const satisfies SystemProviderId[]
|
||||
const SUPPORT_SERVICE_TIER_PROVIDERS = [
|
||||
SystemProviderIds.openai,
|
||||
SystemProviderIds['azure-openai'],
|
||||
SystemProviderIds.groq
|
||||
// TODO: 等待上游支持aws-bedrock
|
||||
]
|
||||
|
||||
/**
|
||||
* 判断提供商是否支持 service_tier 设置。 Only for OpenAI API.
|
||||
* 判断提供商是否支持 service_tier 设置
|
||||
*/
|
||||
export const isSupportServiceTierProvider = (provider: Provider) => {
|
||||
return (
|
||||
provider.apiOptions?.isSupportServiceTier === true ||
|
||||
(isSystemProvider(provider) && !NOT_SUPPORT_SERVICE_TIER_PROVIDERS.some((pid) => pid === provider.id))
|
||||
provider.type === 'azure-openai' ||
|
||||
(isSystemProvider(provider) && SUPPORT_SERVICE_TIER_PROVIDERS.some((pid) => pid === provider.id))
|
||||
)
|
||||
}
|
||||
|
||||
@ -112,6 +118,7 @@ const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
|
||||
'gemini',
|
||||
'vertexai',
|
||||
'anthropic',
|
||||
'azure-openai',
|
||||
'new-api'
|
||||
] as const satisfies ProviderType[]
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user