mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 14:01:30 +08:00
hotfix: Custom params dropped by CherryIN/NewAPI — respect model.endpoint_type (#14409)
### What this PR does
Before this PR: Proxy providers (`CherryIN`, `NewAPI`, `AiHubMix`) share
a common `providerOptions` construction path, but that path —
`buildAIGatewayOptions` — dispatched purely by a short model-name
heuristic (`isAnthropicModel` / `isOpenAIModel` / `isGeminiModel` /
`isGrokModel`). Two failure modes resulted:
1. **CherryIN was missed entirely by #14352.** The `case 'cherryin':`
branch stayed on `buildCherryInProviderOptions`, which dispatched by
`actualProvider.type`. But CherryIN is hard-coded as `type: 'openai'` in
`SYSTEM_PROVIDERS_CONFIG.cherryin` and not exposed in the UI, so every
Gemini/Claude/GPT model fell into the `case 'openai'` branch and
produced `{ cherryin: {...} }` instead of the SDK-aligned key. Effect:
Gemini `imageConfig` (e.g. 4K image generation) and Anthropic `thinking`
params were silently dropped.
2. **`buildAIGatewayOptions` ignored `model.endpoint_type`.** CherryIN
and NewAPI annotate models with `endpoint_type` to force a specific
protocol — e.g. CherryIN routes `minimax/minimax-m2.7` through the
Anthropic endpoint despite the `minimax/` prefix. The SDK layer
(`cherryin-provider.ts` line 230-251, `newapi-provider.ts` line 103-117)
switches on `endpointType` when building the language-model class. But
the options layer didn't, so the `providerOptions` key didn't match the
language-model class's expected namespace, and AI SDK dropped the custom
options.
After this PR:
- `cherryin` is routed through `buildAIGatewayOptions` alongside
`newapi` / `aihubmix` / `gateway` (mirrors #14352's approach for the
three other proxy providers).
- `buildAIGatewayOptions` now dispatches on `model.endpoint_type` first,
with the short-name heuristic as fallback for untagged models. Mapping
is derived from the SDK-layer language-model class, so the
`providerOptions` key matches what AI SDK actually reads:
| `endpoint_type` | SDK language-model class | AI SDK `providerOptions`
key |
|---|---|---|
| `'anthropic'` | `AnthropicMessagesLanguageModel` | `anthropic` |
| `'gemini'` | `GoogleGenerativeAILanguageModel` | `google` |
| `'openai-response'` | `OpenAIResponsesLanguageModel` | `openai` |
| `'openai'` / `'image-generation'` |
`OpenAICompatibleChatLanguageModel` | `openai-compatible` |
- The now-unused `buildCherryInProviderOptions` helper is removed.
Fixes:
- CherryIN's 4K image generation via Gemini (`imageConfig`)
- CherryIN's mixed-routing models like `minimax/minimax-m2.7`
(endpoint_type-driven)
- NewAPI's non-claude-prefixed models with `endpoint_type='anthropic'`
(same class of bug)
Fixes #14301
### Why we need it and why it was done in this way
This PR unifies two related fixes into one:
**Part 1 — extend #14352 to CherryIN.** PR #14352 moved `newapi` and
`aihubmix` into `buildAIGatewayOptions` to avoid producing `{ newapi:
{...} }` / `{ aihubmix: {...} }`. CherryIN is structurally identical
(multi-protocol proxy provider with `type: 'openai'` default, introduced
in #10715) but was missed. The fix simply adds `cherryin` to the same
fall-through branch. Since `buildCherryInProviderOptions` has no other
callers after this, it is removed.
**Part 2 — stop ignoring `model.endpoint_type`.** `endpoint_type` was
introduced in #11367 (2025-11-21) specifically so CherryIN / NewAPI can
carry explicit protocol routing info per model. That data was plumbed
through `providerConfig.ts:258` into the SDK layer, which uses it
correctly. But `buildAIGatewayOptions` (added by #11605 on 2025-12-04)
never looked at it. #14352 preserved the same gap. The result is a
two-layer inconsistency: the SDK layer builds an Anthropic language
model, the options layer produces `{ openai-compatible: ... }`, and AI
SDK silently drops the options. Fixing the options layer to respect
`endpoint_type` is the minimum change that aligns the two layers.
The following tradeoffs were made: Non-Gemini/Claude/GPT/Grok model IDs
on CherryIN (e.g. `deepseek-chat`) without an `endpoint_type` now fall
into the `openai-compatible` bucket instead of the `cherryin` bucket.
This matches the behavior of NewAPI/AiHubMix post-#14352 and is the
desired cross-proxy consistency. User custom params written as
`cherryin: {...}` still correctly remap via the existing Case 2/3
proxy-mapping logic in `buildProviderOptions` — no user-facing API
change.
The following alternatives were considered:
- Keep `buildCherryInProviderOptions` and fix its dispatch key to use
model ID instead of `provider.type`. Rejected: duplicates
`buildAIGatewayOptions` logic and diverges two paths that should stay in
sync.
- Ship only Part 1 and handle `endpoint_type` in a follow-up. Rejected:
Part 1 alone leaves CherryIN's flagship "mixed routing" scenarios (e.g.
`minimax/minimax-m2.7`) still broken, making it a half-fix.
Links to places where the discussion took place: #14352, #14301, #11462,
#13755, #11367 (endpoint_type introduction), #11605
(buildAIGatewayOptions introduction), #10715 (CherryIN provider
introduction)
### Breaking changes
None for main user flows. Behavior changes for previously broken cases:
- `{ cherryin: {...} }` outputs are replaced with the correct AI SDK
provider key for Gemini/Claude/GPT/Grok/OpenAICompatible models, making
custom params actually reach AI SDK.
- Exotic model IDs on CherryIN without `endpoint_type` now land in
`openai-compatible` bucket instead of `cherryin`, matching post-#14352
behavior for NewAPI/AiHubMix.
- NewAPI models with `endpoint_type='anthropic'` but non-claude-prefixed
IDs now correctly produce `{ anthropic: {...} }` (was silently dropped
before).
### Special notes for your reviewer
- Hotfix scope follows `main` code-freeze guidance: only the dispatch
logic changes, plus dead-code cleanup of the helper that now has no
callers. No refactoring of the broader pipeline.
- `endpoint_type` mapping mirrors the SDK layer exactly
(cross-referenced with
`packages/ai-sdk-provider/src/cherryin-provider.ts` and
`src/renderer/src/aiCore/provider/custom/newapi-provider.ts`). The
inline comment in `buildAIGatewayOptions` documents this contract.
- Existing tests `should map cherryin provider ID to actual AI SDK
provider ID (Google/OpenAI)` continue to pass unchanged because their
Gemini / GPT model IDs route correctly under the new dispatch.
- The fallback test `should handle cherryin fallback to
openai-compatible with custom parameters` was updated to assert the new
`openai-compatible` bucket (previously asserted `cherryin`).
- The parametrized `it.each` test from #14352 now also covers `cherryin`
alongside `newapi` / `aihubmix`.
- A new parametrized test covers 6 `endpoint_type` cases across CherryIN
and NewAPI.
- Suggested backports: `v2` (has neither #14352 nor this fix) and
`DeJeune/ai-service` (code structure unchanged after the main→main
migration, same fix applies with minor line-number shifts).
### Checklist
This checklist is not enforcing, but it's a reminder of items that could
be relevant to every PR.
Approvers are expected to review this list.
- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is not required — bug fix only, no new user-visible
feature or setting.
- [x] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others
### Release note
```release-note
fix: Custom parameters were not being passed to Gemini/Anthropic APIs via `CherryIN` or `NewAPI` proxy providers when `model.endpoint_type` is set
```
---------
Signed-off-by: zhibisora <73344387+zhibisora@users.noreply.github.com>
This commit is contained in:
@@ -1125,7 +1125,8 @@ describe('options utils', () => {
|
||||
|
||||
it.each([
|
||||
{ providerId: 'newapi', providerName: 'NewAPI' },
|
||||
{ providerId: 'aihubmix', providerName: 'AiHubMix' }
|
||||
{ providerId: 'aihubmix', providerName: 'AiHubMix' },
|
||||
{ providerId: 'cherryin', providerName: 'CherryIN' }
|
||||
])(
|
||||
'should route Gemini models to google providerOptions through $providerName',
|
||||
async ({ providerId, providerName }) => {
|
||||
@@ -1166,10 +1167,88 @@ describe('options utils', () => {
|
||||
// Note: For proxy providers like aihubmix/newapi, users should write AI SDK provider ID (google/anthropic)
|
||||
// instead of the Cherry Studio provider ID for custom parameters to work correctly
|
||||
|
||||
// model.endpoint_type takes priority over the short-name heuristic so the providerOptions key
|
||||
// stays aligned with the SDK language-model class each proxy builds. Covers CherryIN's
|
||||
// mixed-routing models (e.g. `minimax/minimax-m2.7` using the Anthropic endpoint) and
|
||||
// NewAPI's endpoint_type-driven routing.
|
||||
it.each([
|
||||
{
|
||||
providerId: 'cherryin',
|
||||
modelId: 'minimax/minimax-m2.7',
|
||||
endpointType: 'anthropic' as const,
|
||||
expectedKey: 'anthropic'
|
||||
},
|
||||
{
|
||||
providerId: 'cherryin',
|
||||
modelId: 'custom-id',
|
||||
endpointType: 'gemini' as const,
|
||||
expectedKey: 'google'
|
||||
},
|
||||
{
|
||||
providerId: 'cherryin',
|
||||
modelId: 'gpt-5',
|
||||
endpointType: 'openai-response' as const,
|
||||
expectedKey: 'openai'
|
||||
},
|
||||
{
|
||||
providerId: 'cherryin',
|
||||
modelId: 'qwen-max',
|
||||
endpointType: 'openai' as const,
|
||||
expectedKey: 'openai-compatible'
|
||||
},
|
||||
{
|
||||
providerId: 'newapi',
|
||||
modelId: 'proxy/model',
|
||||
endpointType: 'anthropic' as const,
|
||||
expectedKey: 'anthropic'
|
||||
},
|
||||
{
|
||||
providerId: 'newapi',
|
||||
modelId: 'proxy/model',
|
||||
endpointType: 'openai' as const,
|
||||
expectedKey: 'openai-compatible'
|
||||
}
|
||||
])(
|
||||
'should honor model.endpoint_type=$endpointType for $providerId and produce providerOptions.$expectedKey',
|
||||
async ({ providerId, modelId, endpointType, expectedKey }) => {
|
||||
const { getCustomParameters } = await import('../reasoning')
|
||||
vi.mocked(getCustomParameters).mockReturnValue({
|
||||
customEndpointParam: 'custom_value'
|
||||
})
|
||||
|
||||
const provider: Provider = {
|
||||
id: providerId,
|
||||
name: providerId,
|
||||
type: 'openai',
|
||||
models: [] as Model[]
|
||||
} as Provider
|
||||
|
||||
const model: Model = {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
provider: providerId,
|
||||
endpoint_type: endpointType
|
||||
} as Model
|
||||
|
||||
const result = buildProviderOptions(mockAssistant, model, provider, {
|
||||
enableReasoning: false,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.providerOptions).toHaveProperty(expectedKey)
|
||||
expect(result.providerOptions).not.toHaveProperty(providerId)
|
||||
expect(result.providerOptions[expectedKey]).toMatchObject({
|
||||
customEndpointParam: 'custom_value'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('should handle cherryin fallback to openai-compatible with custom parameters', async () => {
|
||||
const { getCustomParameters } = await import('../reasoning')
|
||||
|
||||
// Mock cherryin provider that falls back to openai-compatible (default case)
|
||||
// Mock cherryin provider with a non-Gemini/Claude/GPT/Grok model that falls back
|
||||
// to openai-compatible via buildAIGatewayOptions
|
||||
const cherryinProvider = {
|
||||
id: 'cherryin',
|
||||
name: 'Cherry In',
|
||||
@@ -1196,10 +1275,11 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
// When cherryin falls back to default case, it should use rawProviderId (cherryin)
|
||||
// User's cherryin params should merge with the provider options
|
||||
expect(result.providerOptions).toHaveProperty('cherryin')
|
||||
expect(result.providerOptions.cherryin).toMatchObject({
|
||||
// Non-Gemini/Claude/GPT/Grok models fall back to openai-compatible via buildAIGatewayOptions.
|
||||
// User's custom params (not matching any AI SDK provider ID) merge into the primary bucket.
|
||||
expect(result.providerOptions).toHaveProperty('openai-compatible')
|
||||
expect(result.providerOptions).not.toHaveProperty('cherryin')
|
||||
expect(result.providerOptions['openai-compatible']).toMatchObject({
|
||||
customCherryinOption: 'cherryin_value'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -192,19 +192,10 @@ export function buildProviderOptions(
|
||||
case 'bedrock':
|
||||
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
case 'cherryin':
|
||||
providerSpecificOptions = buildCherryInProviderOptions(
|
||||
assistant,
|
||||
model,
|
||||
capabilities,
|
||||
actualProvider,
|
||||
serviceTier,
|
||||
textVerbosity
|
||||
)
|
||||
break
|
||||
case SystemProviderIds.ollama:
|
||||
providerSpecificOptions = buildOllamaProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
case 'cherryin':
|
||||
case 'newapi':
|
||||
case 'aihubmix':
|
||||
case SystemProviderIds.gateway:
|
||||
@@ -464,29 +455,6 @@ function buildXAIProviderOptions(
|
||||
}
|
||||
}
|
||||
|
||||
function buildCherryInProviderOptions(
|
||||
assistant: Assistant,
|
||||
model: Model,
|
||||
capabilities: Pick<ProviderCapabilities, 'enableReasoning' | 'enableWebSearch' | 'enableGenerateImage'>,
|
||||
actualProvider: Provider,
|
||||
serviceTier: OpenAIServiceTier,
|
||||
textVerbosity: OpenAIVerbosity
|
||||
): Record<string, OpenAIResponsesProviderOptions | AnthropicProviderOptions | GoogleGenerativeAIProviderOptions> {
|
||||
switch (actualProvider.type) {
|
||||
case 'openai':
|
||||
return buildGenericProviderOptions('cherryin', assistant, model, capabilities)
|
||||
case 'openai-response':
|
||||
return buildOpenAIProviderOptions(assistant, model, capabilities, serviceTier, textVerbosity)
|
||||
case 'anthropic':
|
||||
return buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||
case 'gemini':
|
||||
return buildGeminiProviderOptions(assistant, model, capabilities)
|
||||
|
||||
default:
|
||||
return buildGenericProviderOptions('cherryin', assistant, model, capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Bedrock providerOptions
|
||||
*/
|
||||
@@ -593,6 +561,32 @@ function buildAIGatewayOptions(
|
||||
| GoogleGenerativeAIProviderOptions
|
||||
| Record<string, unknown>
|
||||
> {
|
||||
// Proxy providers (CherryIN, NewAPI) may annotate model.endpoint_type to force a specific protocol.
|
||||
// Keys here must stay aligned with the language-model class each SDK layer builds, otherwise
|
||||
// AI SDK drops the custom provider options. See:
|
||||
// - packages/ai-sdk-provider/src/cherryin-provider.ts (createChatModel)
|
||||
// - src/renderer/src/aiCore/provider/custom/newapi-provider.ts (createChatModel)
|
||||
//
|
||||
// endpoint_type | SDK language-model class | AI SDK providerOptions key
|
||||
// -------------------+------------------------------------+---------------------------
|
||||
// 'anthropic' | AnthropicMessagesLanguageModel | anthropic
|
||||
// 'gemini' | GoogleGenerativeAILanguageModel | google
|
||||
// 'openai-response' | OpenAIResponsesLanguageModel | openai
|
||||
// 'openai' | OpenAICompatibleChatLanguageModel | openai-compatible
|
||||
// 'image-generation' | OpenAICompatibleChatLanguageModel | openai-compatible
|
||||
switch (model.endpoint_type) {
|
||||
case 'anthropic':
|
||||
return buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||
case 'gemini':
|
||||
return buildGeminiProviderOptions(assistant, model, capabilities)
|
||||
case 'openai-response':
|
||||
return buildOpenAIProviderOptions(assistant, model, capabilities, serviceTier, textVerbosity)
|
||||
case 'openai':
|
||||
case 'image-generation':
|
||||
return buildGenericProviderOptions('openai-compatible', assistant, model, capabilities)
|
||||
}
|
||||
|
||||
// Fallback: model-name heuristic (covers Vercel Gateway, AiHubMix, and untagged models)
|
||||
if (isAnthropicModel(model)) {
|
||||
return buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||
} else if (isOpenAIModel(model)) {
|
||||
|
||||
Reference in New Issue
Block a user