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:
zhibisora
2026-04-22 14:39:21 +08:00
committed by GitHub
parent 4c0ce8f799
commit c2b3302581
2 changed files with 113 additions and 39 deletions

View File

@@ -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'
})
})

View File

@@ -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)) {