mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 07:00:09 +08:00
* fix(anthropic): prevent duplicate /v1 in API endpoints Anthropic SDK automatically appends /v1 to endpoints, so we should not add it in our formatting. This change ensures URLs are correctly formatted without duplicate path segments. * fix(anthropic): strip /v1 suffix in getSdkClient to prevent duplicate in models endpoint The issue was: - AI SDK (for chat) needs baseURL with /v1 suffix - Anthropic SDK (for listModels) automatically appends /v1 to all endpoints Solution: - Keep /v1 in formatProviderApiHost for AI SDK compatibility - Strip /v1 in getSdkClient before passing to Anthropic SDK - This ensures chat works correctly while preventing /v1/v1/models duplication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(anthropic): correct preview URL to match actual request behavior The preview now correctly shows: - Input: https://api.siliconflow.cn/v2 - Preview: https://api.siliconflow.cn/v2/messages (was incorrectly showing /v2/v1/messages) - Actual: https://api.siliconflow.cn/v2/messages This matches the actual behavior where getSdkClient strips /v1 suffix before passing to Anthropic SDK, which then appends /v1/messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(anthropic): strip all API version suffixes, not just /v1 The Anthropic SDK always appends /v1 to endpoints, regardless of the baseURL. Previously we only stripped /v1 suffix, causing issues with custom versions like /v2. Now we strip all version suffixes (/v1, /v2, /v1beta, etc.) before passing to Anthropic SDK. Examples: - Input: https://api.siliconflow.cn/v2/ - After strip: https://api.siliconflow.cn - Actual request: https://api.siliconflow.cn/v1/messages ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(anthropic): correct preview to show AI SDK behavior, not Anthropic SDK The preview was showing the wrong URL because it was reflecting Anthropic SDK behavior (which strips versions and uses /v1), but checkApi and chat use AI SDK which preserves the user's version path. Now preview correctly shows: - Input: https://api.siliconflow.cn/v2/ - AI SDK (checkApi/chat): https://api.siliconflow.cn/v2/messages ✅ - Preview: https://api.siliconflow.cn/v2/messages ✅ Note: Anthropic SDK (for listModels) still strips versions to use /v1/models, but this is not shown in preview since it's a different code path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(checkApi): remove unnecessary legacy fallback The legacy fallback logic in checkApi was: 1. Complex and hard to maintain 2. Never actually triggered in practice for Modern SDK supported providers 3. Could cause duplicate API requests Since Modern AI SDK now handles all major providers correctly, we can simplify by directly throwing errors instead of falling back. This also removes unused imports: AiProvider and CompletionsParams. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(anthropic): restore version stripping in getSdkClient for Anthropic SDK The Anthropic SDK (used for listModels) always appends /v1 to endpoints, so we need to strip version suffixes from baseURL to avoid duplication. This only affects Anthropic SDK operations (like listModels). AI SDK operations (chat/checkApi) use provider.apiHost directly via providerToAiSdkConfig, which preserves the user's version path. Examples: - AI SDK (chat): https://api.siliconflow.cn/v1 -> /v1/messages ✅ - Anthropic SDK (models): https://api.siliconflow.cn/v1 -> strip v1 -> /v1/models ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(anthropic): ensure AI SDK gets /v1 in baseURL, strip for Anthropic SDK The correct behavior is: 1. formatProviderApiHost: Add /v1 to apiHost (for AI SDK compatibility) 2. AI SDK (chat/checkApi): Use apiHost with /v1 -> /v1/messages ✅ 3. Anthropic SDK (listModels): Strip /v1 from baseURL -> SDK adds /v1/models ✅ 4. Preview: Show AI SDK behavior (main use case) -> /v1/messages ✅ Examples: - Input: https://api.siliconflow.cn - Formatted: https://api.siliconflow.cn/v1 (added by formatApiHost) - AI SDK: https://api.siliconflow.cn/v1/messages ✅ - Anthropic SDK: https://api.siliconflow.cn (stripped) + /v1/models ✅ - Preview: https://api.siliconflow.cn/v1/messages ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(ai): simplify AiProviderNew initialization and improve docs Update AiProviderNew constructor to automatically format URLs by default Add comprehensive documentation explaining constructor behavior and usage * chore: remove unused play.ts file * fix(anthropic): strip api version from baseURL to avoid endpoint duplication --------- Co-authored-by: Claude <noreply@anthropic.com>
176 lines
6.0 KiB
TypeScript
176 lines
6.0 KiB
TypeScript
/**
|
|
* @fileoverview Shared Anthropic AI client utilities for Cherry Studio
|
|
*
|
|
* This module provides functions for creating Anthropic SDK clients with different
|
|
* authentication methods (OAuth, API key) and building Claude Code system messages.
|
|
* It supports both standard Anthropic API and Anthropic Vertex AI endpoints.
|
|
*
|
|
* This shared module can be used by both main and renderer processes.
|
|
*/
|
|
|
|
import Anthropic from '@anthropic-ai/sdk'
|
|
import type { TextBlockParam } from '@anthropic-ai/sdk/resources'
|
|
import { loggerService } from '@logger'
|
|
import type { Provider } from '@types'
|
|
import type { ModelMessage } from 'ai'
|
|
|
|
const logger = loggerService.withContext('anthropic-sdk')
|
|
|
|
const defaultClaudeCodeSystemPrompt = `You are Claude Code, Anthropic's official CLI for Claude.`
|
|
|
|
const defaultClaudeCodeSystem: Array<TextBlockParam> = [
|
|
{
|
|
type: 'text',
|
|
text: defaultClaudeCodeSystemPrompt
|
|
}
|
|
]
|
|
|
|
/**
|
|
* Creates and configures an Anthropic SDK client based on the provider configuration.
|
|
*
|
|
* This function supports two authentication methods:
|
|
* 1. OAuth: Uses OAuth tokens passed as parameter
|
|
* 2. API Key: Uses traditional API key authentication
|
|
*
|
|
* For OAuth authentication, it includes Claude Code specific headers and beta features.
|
|
* For API key authentication, it uses the provider's configuration with custom headers.
|
|
*
|
|
* @param provider - The provider configuration containing authentication details
|
|
* @param oauthToken - Optional OAuth token for OAuth authentication
|
|
* @returns An initialized Anthropic or AnthropicVertex client
|
|
* @throws Error when OAuth token is not available for OAuth authentication
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // OAuth authentication
|
|
* const oauthProvider = { authType: 'oauth' };
|
|
* const oauthClient = getSdkClient(oauthProvider, 'oauth-token-here');
|
|
*
|
|
* // API key authentication
|
|
* const apiKeyProvider = {
|
|
* authType: 'apikey',
|
|
* apiKey: 'your-api-key',
|
|
* apiHost: 'https://api.anthropic.com'
|
|
* };
|
|
* const apiKeyClient = getSdkClient(apiKeyProvider);
|
|
* ```
|
|
*/
|
|
export function getSdkClient(
|
|
provider: Provider,
|
|
oauthToken?: string | null,
|
|
extraHeaders?: Record<string, string | string[]>
|
|
): Anthropic {
|
|
if (provider.authType === 'oauth') {
|
|
if (!oauthToken) {
|
|
throw new Error('OAuth token is not available')
|
|
}
|
|
return new Anthropic({
|
|
authToken: oauthToken,
|
|
baseURL: 'https://api.anthropic.com',
|
|
dangerouslyAllowBrowser: true,
|
|
defaultHeaders: {
|
|
'Content-Type': 'application/json',
|
|
'anthropic-version': '2023-06-01',
|
|
'anthropic-beta':
|
|
'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14',
|
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
'user-agent': 'claude-cli/1.0.118 (external, sdk-ts)',
|
|
'x-app': 'cli',
|
|
'x-stainless-retry-count': '0',
|
|
'x-stainless-timeout': '600',
|
|
'x-stainless-lang': 'js',
|
|
'x-stainless-package-version': '0.60.0',
|
|
'x-stainless-os': 'MacOS',
|
|
'x-stainless-arch': 'arm64',
|
|
'x-stainless-runtime': 'node',
|
|
'x-stainless-runtime-version': 'v22.18.0',
|
|
...extraHeaders
|
|
}
|
|
})
|
|
}
|
|
let baseURL =
|
|
provider.type === 'anthropic'
|
|
? provider.apiHost
|
|
: (provider.anthropicApiHost && provider.anthropicApiHost.trim()) || provider.apiHost
|
|
|
|
// Anthropic SDK automatically appends /v1 to all endpoints (like /v1/messages, /v1/models)
|
|
// We need to strip api version from baseURL to avoid duplication (e.g., /v3/v1/models)
|
|
// formatProviderApiHost adds /v1 for AI SDK compatibility, but Anthropic SDK needs it removed
|
|
baseURL = baseURL.replace(/\/v\d+(?:alpha|beta)?(?=\/|$)/i, '')
|
|
|
|
logger.debug('Anthropic API baseURL', { baseURL, providerId: provider.id })
|
|
|
|
if (provider.id === 'aihubmix') {
|
|
return new Anthropic({
|
|
apiKey: provider.apiKey,
|
|
baseURL,
|
|
dangerouslyAllowBrowser: true,
|
|
defaultHeaders: {
|
|
'anthropic-beta': 'output-128k-2025-02-19',
|
|
'APP-Code': 'MLTG2087',
|
|
...provider.extra_headers,
|
|
...extraHeaders
|
|
}
|
|
})
|
|
}
|
|
|
|
return new Anthropic({
|
|
apiKey: provider.apiKey,
|
|
authToken: provider.apiKey,
|
|
baseURL,
|
|
dangerouslyAllowBrowser: true,
|
|
defaultHeaders: {
|
|
'anthropic-beta': 'output-128k-2025-02-19',
|
|
...provider.extra_headers
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Builds and prepends the Claude Code system message to user-provided system messages.
|
|
*
|
|
* This function ensures that all interactions with Claude include the official Claude Code
|
|
* system prompt, which identifies the assistant as "Claude Code, Anthropic's official CLI for Claude."
|
|
*
|
|
* The function handles three cases:
|
|
* 1. No system message provided: Returns only the default Claude Code system message
|
|
* 2. String system message: Converts to array format and prepends Claude Code message
|
|
* 3. Array system message: Checks if Claude Code message exists and prepends if missing
|
|
*
|
|
* @param system - Optional user-provided system message (string or TextBlockParam array)
|
|
* @returns Combined system message with Claude Code prompt prepended
|
|
*
|
|
* ```
|
|
*/
|
|
export function buildClaudeCodeSystemMessage(system?: string | Array<TextBlockParam>): Array<TextBlockParam> {
|
|
if (!system) {
|
|
return defaultClaudeCodeSystem
|
|
}
|
|
|
|
if (typeof system === 'string') {
|
|
if (system.trim() === defaultClaudeCodeSystemPrompt || system.trim() === '') {
|
|
return defaultClaudeCodeSystem
|
|
} else {
|
|
return [...defaultClaudeCodeSystem, { type: 'text', text: system }]
|
|
}
|
|
}
|
|
if (Array.isArray(system)) {
|
|
const firstSystem = system[0]
|
|
if (firstSystem.type === 'text' && firstSystem.text.trim() === defaultClaudeCodeSystemPrompt) {
|
|
return system
|
|
} else {
|
|
return [...defaultClaudeCodeSystem, ...system]
|
|
}
|
|
}
|
|
|
|
return defaultClaudeCodeSystem
|
|
}
|
|
|
|
export function buildClaudeCodeSystemModelMessage(system?: string | Array<TextBlockParam>): Array<ModelMessage> {
|
|
const textBlocks = buildClaudeCodeSystemMessage(system)
|
|
return textBlocks.map((block) => ({
|
|
role: 'system',
|
|
content: block.text
|
|
}))
|
|
}
|