From c2b3302581aa143ffbfceaeea356aaa5a9f18f11 Mon Sep 17 00:00:00 2001 From: zhibisora <73344387+zhibisora@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:39:21 +0800 Subject: [PATCH] =?UTF-8?q?hotfix:=20Custom=20params=20dropped=20by=20Cher?= =?UTF-8?q?ryIN/NewAPI=20=E2=80=94=20respect=20model.endpoint=5Ftype=20(#1?= =?UTF-8?q?4409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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> --- .../aiCore/utils/__tests__/options.test.ts | 92 +++++++++++++++++-- src/renderer/src/aiCore/utils/options.ts | 60 ++++++------ 2 files changed, 113 insertions(+), 39 deletions(-) diff --git a/src/renderer/src/aiCore/utils/__tests__/options.test.ts b/src/renderer/src/aiCore/utils/__tests__/options.test.ts index d3622e8696..77b4c1811f 100644 --- a/src/renderer/src/aiCore/utils/__tests__/options.test.ts +++ b/src/renderer/src/aiCore/utils/__tests__/options.test.ts @@ -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' }) }) diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index 0bee602478..ec1e48882a 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -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, - actualProvider: Provider, - serviceTier: OpenAIServiceTier, - textVerbosity: OpenAIVerbosity -): Record { - 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 > { + // 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)) {