diff --git a/src/main/services/OpenClawService.ts b/src/main/services/OpenClawService.ts index 81b41848061..92c17c0262c 100644 --- a/src/main/services/OpenClawService.ts +++ b/src/main/services/OpenClawService.ts @@ -75,33 +75,119 @@ export interface OpenClawProviderConfig { } /** - * OpenClaw API types - * - 'openai-completions': For OpenAI-compatible chat completions API - * - 'anthropic-messages': For Anthropic Messages API format + * OpenClaw API protocol types. + * Add new protocol mappings here as OpenClaw adds support for them. */ const OPENCLAW_API_TYPES = { OPENAI: 'openai-completions', + OPENAI_RESPONSE: 'openai-responses', + OPENAI_CODEX_RESPONSE: 'openai-codex-responses', ANTHROPIC: 'anthropic-messages', - OPENAI_RESPOSNE: 'openai-responses' + GOOGLE: 'google-generative-ai', + COPILOT: 'github-copilot', + BEDROCK: 'bedrock-converse-stream', + OLLAMA: 'ollama' } as const /** - * Providers that always use Anthropic API format + * Mapping from Cherry Studio EndpointType to OpenClaw API protocol. + * Used when model has explicit endpoint_type metadata. */ -const ANTHROPIC_ONLY_PROVIDERS: ProviderType[] = ['anthropic', 'vertex-anthropic'] +const ENDPOINT_TO_OPENCLAW_API: Record = { + anthropic: OPENCLAW_API_TYPES.ANTHROPIC, + openai: OPENCLAW_API_TYPES.OPENAI, + 'openai-response': OPENCLAW_API_TYPES.OPENAI_RESPONSE, + gemini: OPENCLAW_API_TYPES.GOOGLE +} + +/** + * Mapping from Cherry Studio provider type to OpenClaw API protocol. + * Used for providers that always use a specific protocol regardless of model. + */ +const PROVIDER_TYPE_TO_OPENCLAW_API: Partial> = { + anthropic: OPENCLAW_API_TYPES.ANTHROPIC, + 'vertex-anthropic': OPENCLAW_API_TYPES.ANTHROPIC, + gemini: OPENCLAW_API_TYPES.GOOGLE, + ollama: OPENCLAW_API_TYPES.OLLAMA, + 'aws-bedrock': OPENCLAW_API_TYPES.BEDROCK, + 'openai-response': OPENCLAW_API_TYPES.OPENAI_RESPONSE +} /** - * Endpoint types that use Anthropic API format - * These are values from model.endpoint_type field + * Mapping from Cherry Studio provider id to OpenClaw API protocol. + * Add provider-specific protocol overrides here as needed. */ -const ANTHROPIC_ENDPOINT_TYPES = ['anthropic'] +const PROVIDER_ID_TO_OPENCLAW_API: Record = { + copilot: OPENCLAW_API_TYPES.COPILOT +} + +/** + * Get the base model name (last segment after '/') in lowercase. + * e.g. 'openrouter/anthropic/claude-opus-4.6' => 'claude-opus-4.6' + */ +function getModelBaseName(modelId: string): string { + const parts = modelId.split('/') + return (parts.pop() || modelId).toLowerCase() +} /** - * Check if a model should use Anthropic API based on endpoint_type + * Check if a model is an Anthropic model by its name. */ -function isAnthropicEndpointType(model: Model): boolean { - const endpointType = model.endpoint_type - return endpointType ? ANTHROPIC_ENDPOINT_TYPES.includes(endpointType) : false +function isAnthropicModel(modelId: string): boolean { + return getModelBaseName(modelId).startsWith('claude') +} + +/** + * Check if a model is a Gemini model by its name. + */ +function isGeminiModel(modelId: string): boolean { + return getModelBaseName(modelId).startsWith('gemini') +} + +/** + * Determine the appropriate OpenClaw API protocol for the given provider and model. + * + * Priority order: + * 1. Model's explicit endpoint_type (model knows best — set by new-api, etc.) + * 2. Provider id (provider-specific protocol overrides) + * 3. Provider type (anthropic, vertex-anthropic, gemini, ollama, bedrock, openai-response) + * 4. Model name inference for multi-protocol aggregators + * (only when provider has a dedicated host for that protocol) + * 5. Default to openai-completions + * + * @internal Exported for testing only. + */ +export function determineApiType( + provider: { id: string; type: string; anthropicApiHost?: string; geminiApiHost?: string }, + model: { id: string; endpoint_type?: string } +): string { + // 1. Model's explicit endpoint_type (highest priority — model declares its own protocol) + if (model.endpoint_type && ENDPOINT_TO_OPENCLAW_API[model.endpoint_type]) { + return ENDPOINT_TO_OPENCLAW_API[model.endpoint_type] + } + + // 2. Provider id specific protocol + if (PROVIDER_ID_TO_OPENCLAW_API[provider.id]) { + return PROVIDER_ID_TO_OPENCLAW_API[provider.id] + } + + // 3. Provider type specific protocol (anthropic, vertex-anthropic, gemini, etc.) + if (PROVIDER_TYPE_TO_OPENCLAW_API[provider.type as ProviderType]) { + return PROVIDER_TYPE_TO_OPENCLAW_API[provider.type as ProviderType]! + } + + // 4. Infer protocol from model name for multi-protocol aggregators. + // Each vendor-specific host (anthropicApiHost, geminiApiHost) independently + // signals that the provider can route to that vendor's native API. + if (provider.anthropicApiHost && isAnthropicModel(model.id)) { + return OPENCLAW_API_TYPES.ANTHROPIC + } + if (provider.geminiApiHost && isGeminiModel(model.id)) { + return OPENCLAW_API_TYPES.GOOGLE + } + + // 5. Default to OpenAI-compatible + return OPENCLAW_API_TYPES.OPENAI } /** @@ -942,38 +1028,8 @@ class OpenClawService { } } - /** - * Determine the API type based on model and provider - * This supports mixed providers (cherryin, aihubmix, new-api, etc.) that have both OpenAI and Anthropic endpoints - * - * Priority order: - * 1. Provider type (anthropic, vertex-anthropic always use Anthropic API) - * 2. Model endpoint_type (explicit endpoint configuration) - * 3. Provider has anthropicApiHost configured - * 4. Default to OpenAI-compatible - */ private determineApiType(provider: Provider, model: Model): string { - // 1. Check if provider type is always Anthropic - if (ANTHROPIC_ONLY_PROVIDERS.includes(provider.type)) { - return OPENCLAW_API_TYPES.ANTHROPIC - } - - // 2. Check model's endpoint_type (used by new-api and other mixed providers) - if (isAnthropicEndpointType(model)) { - return OPENCLAW_API_TYPES.ANTHROPIC - } - - // 3. Check if provider has anthropicApiHost configured - if (provider.anthropicApiHost) { - return OPENCLAW_API_TYPES.ANTHROPIC - } - - if (provider.type === 'openai-response') { - return OPENCLAW_API_TYPES.OPENAI_RESPOSNE - } - - // 4. Default to OpenAI-compatible - return OPENCLAW_API_TYPES.OPENAI + return determineApiType(provider, model) } /** @@ -983,11 +1039,13 @@ class OpenClawService { */ private getBaseUrlForApiType(provider: Provider, apiType: string): string { if (apiType === OPENCLAW_API_TYPES.ANTHROPIC) { - // For Anthropic API type, prefer anthropicApiHost if available const host = provider.anthropicApiHost || provider.apiHost return this.formatAnthropicUrl(host) } - // For OpenAI-compatible API type + if (apiType === OPENCLAW_API_TYPES.GOOGLE && provider.geminiApiHost) { + return withoutTrailingSlash(provider.geminiApiHost) + } + // TODO: Add dedicated URL formatters for ollama, bedrock, copilot protocols return this.formatOpenAIUrl(provider) } diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 2ec8de6a3e2..0f1feb9a428 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -281,7 +281,7 @@ export class WindowService { 'https://account.siliconflow.cn/oauth', 'https://cloud.siliconflow.cn/bills', 'https://cloud.siliconflow.cn/expensebill', - 'https://console.aihubmix.com/token', + 'https://console.aihubmix.com/sign-in', 'https://console.aihubmix.com/topup', 'https://console.aihubmix.com/statistics', 'https://dash.302.ai/sso/login', diff --git a/src/renderer/src/aiCore/legacy/clients/aihubmix/AihubmixAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/aihubmix/AihubmixAPIClient.ts index a8a0ca5ac66..ccb3870abf3 100644 --- a/src/renderer/src/aiCore/legacy/clients/aihubmix/AihubmixAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/aihubmix/AihubmixAPIClient.ts @@ -32,7 +32,10 @@ export class AihubmixAPIClient extends MixedBaseAPIClient { // 初始化各个client - 现在有类型安全 const claudeClient = new AnthropicAPIClient(providerExtraHeaders) - const geminiClient = new GeminiAPIClient({ ...providerExtraHeaders, apiHost: 'https://aihubmix.com/gemini' }) + const geminiClient = new GeminiAPIClient({ + ...providerExtraHeaders, + apiHost: 'https://aihubmix.com/gemini' + }) const openaiClient = new OpenAIResponseAPIClient(providerExtraHeaders) const defaultClient = new OpenAIAPIClient(providerExtraHeaders) diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 2771733e223..31cd25b2600 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -108,6 +108,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = apiKey: '', apiHost: 'https://aihubmix.com', anthropicApiHost: 'https://aihubmix.com', + geminiApiHost: 'https://aihubmix.com/gemini/v1beta', models: SYSTEM_MODELS.aihubmix, isSystem: true, enabled: false diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 0d5708c5099..d66b9ca12a1 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -86,7 +86,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 203, + version: 204, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 870482da390..e5b9296350f 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -3348,6 +3348,15 @@ const migrateConfig = { logger.error('migrate 203 error', error as Error) return state } + }, + '204': (state: RootState) => { + try { + updateProvider(state, 'aihubmix', { geminiApiHost: 'https://aihubmix.com/gemini/v1beta' }) + return state + } catch (error) { + logger.error('migrate 204 error', error as Error) + return state + } } } diff --git a/src/renderer/src/types/provider.ts b/src/renderer/src/types/provider.ts index 1cfee653811..9707cd6e26e 100644 --- a/src/renderer/src/types/provider.ts +++ b/src/renderer/src/types/provider.ts @@ -107,6 +107,7 @@ export type Provider = { apiKey: string apiHost: string anthropicApiHost?: string + geminiApiHost?: string isAnthropicModel?: (m: Model) => boolean apiVersion?: string models: Model[] diff --git a/src/renderer/src/utils/oauth.ts b/src/renderer/src/utils/oauth.ts index 60188d47f0f..be6781e556e 100644 --- a/src/renderer/src/utils/oauth.ts +++ b/src/renderer/src/utils/oauth.ts @@ -26,7 +26,7 @@ export const oauthWithSiliconFlow = async (setKey) => { } export const oauthWithAihubmix = async (setKey) => { - const authUrl = ` https://console.aihubmix.com/token?client_id=cherry_studio_oauth&lang=${getLanguageCode()}&aff=SJyh` + const authUrl = `https://console.aihubmix.com/sign-in?client_id=cherry_studio_oauth&lang=${getLanguageCode()}&aff=SJyh` const popup = window.open( authUrl,