Files
CherryHQ-cherry-studio/docs/references/ai/provider-resolution.md
SuYao 5706307451 refactor(ai-service): consolidate AI runtime to main process (#14911)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Signed-off-by: suyao <sy20010504@gmail.com>
2026-06-05 00:06:51 +08:00

5.7 KiB

Provider Resolution

The problem this solves

A request needs to know which @ai-sdk/* package to import, with which settings, hitting which URL. Three pieces of state determine that:

Field Lives on Example
provider.id Provider row minimax, silicon, my-relay
endpointType model.endpointTypes[0] or provider.defaultChatEndpoint openai-chat-completions, anthropic-messages
adapterFamily provider.endpointConfigs[endpointType].adapterFamily openai-compatible, anthropic, azure-responses

adapterFamily is the actual SDK selector. provider.id is the user-facing identity. endpointType is the protocol family. The mapping is written once at provider-creation time; runtime resolution is read-only.

See Adapter Family for the full design.

Resolver

src/main/ai/provider/endpoint.ts exposes three pure helpers:

resolveEffectiveEndpoint(provider, model): { endpointType, baseUrl }
resolveProviderVariant(baseProviderId, endpointType): AppProviderId
resolveAiSdkProviderId(provider, endpointType): AppProviderId

resolveAiSdkProviderId is the runtime hot-path entry. It reads provider.endpointConfigs[endpointType].adapterFamily, applies the variant suffix if the endpoint type has one, falls back to openai-compatible when no family is set.

// Full resolver — 6 lines
export function resolveAiSdkProviderId(provider, endpointType) {
  const adapterFamily = endpointType
    ? provider.endpointConfigs?.[endpointType]?.adapterFamily
    : undefined
  if (adapterFamily && adapterFamily in appProviderIds) {
    return resolveProviderVariant(appProviderIds[adapterFamily], endpointType)
  }
  return appProviderIds['openai-compatible']
}

Variants

Some bases expose variant ids (a different endpoint on the same base). resolveProviderVariant knows two suffix rules and applies one only when the resulting <base>-<suffix> id is actually registered — otherwise it returns the base unchanged:

Endpoint type Suffix tried
openai-chat-completions, ollama-chat -chat
openai-responses -responses

Variants registered today (declared in each provider extension's variants array, packages/aiCore/src/core/providers/core/initialization.ts):

Base Variant id(s)
openai openai-chat (the base openai is itself the Responses API)
azure azure-responses, azure-anthropic
xai xai-responses
cherryin cherryin-chat

ollama has no registered variant, so an ollama-chat endpoint resolves to the base ollama. Likewise there is no openai-responses variant (the base already is). azure-anthropic is not reached through the suffix rule — it is selected inside buildAzureConfig when the model is a Claude model (see below). resolveProviderVariant(baseId, endpointType) is idempotent when the base id is already a variant.

Provider config

providerToAiSdkConfig(provider, model) (src/main/ai/provider/config.ts) returns { providerId: AppProviderId, providerSettings: AppProviderSettingsMap[id] }. It calls resolveAiSdkProviderId internally, then dispatches through an ordered { match, build } table to build the provider-specific settings object (apiKey, baseURL, organization, headers, ...). There is no "gateway" branch.

The builder table (config.ts, first match wins):

Match Builder Notes
id === copilot buildCopilotConfig async — fetches a Copilot token
id === 'cherryai' buildCherryAIConfig
isOllamaProvider buildOllamaConfig
isAzureOpenAIProvider buildAzureConfig returns azure / azure-responses / azure-anthropic (Claude on Azure)
id === 'bedrock' buildBedrockConfig
id === 'google-vertex' buildVertexConfig returns google-vertex or google-vertex-anthropic for Claude; leaves baseURL undefined when no host is configured so the SDK derives the aiplatform host
provider.id === 'cherryin' buildCherryinConfig matches the provider id, not the resolved variant — the default chat endpoint resolves to cherryin-chat, so an id === 'cherryin' check never fires; async — resolves relay base URLs
id === 'newapi' buildNewApiConfig
id === 'aihubmix' buildAiHubMixConfig
(no match) buildGenericProviderConfig / buildOpenAICompatibleConfig generic fallback

Several builders are async (Copilot token, CherryIN relay URLs), which is why providerToAiSdkConfig returns a promise.

Custom providers

src/main/ai/provider/custom/:

  • aihubmix — multi-vendor relay. provider.id='aihubmix' but each model carries model.provider='aihubmix.<vendor>'; the registry's aggregator fallback uses the suffix to pick the right toolFactory.
  • newapi — same shape, different relay.

Both register through ProviderExtension.create(...) with their own AppProviderSettings shape.

Provider extensions

src/main/ai/provider/extensions/index.ts registers every @ai-sdk/* package Cherry uses with ProviderExtension.create. Each extension declares:

  • name (the AppProviderId for the base)
  • aliases (alternate ids that normalize to name)
  • variants (suffix entries — see above)
  • create (the SDK's factory)
  • toolFactories (per-capability factory functions for webSearch / urlContext etc.)
  • supportsImageGeneration (boolean flag)

Where to read more

  • Code: src/main/ai/provider/
  • Tests: provider/__tests__/endpoint.test.ts (54 cases)
  • Migration of legacy provider rows: src/main/data/migration/v2/migrators/mappings/ProviderModelMappings.ts
  • Catalog (new installs): packages/provider-registry/data/providers.json
  • Design: Adapter Family