From 3f463e178b471a8f9fa68d1d3d00aae139b6a74d Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 22 Apr 2026 17:30:54 +0800 Subject: [PATCH] fix(api-server): rotate comma-separated provider API keys (#14346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What this PR does Before this PR: The local OpenAI-compatible API server (http://127.0.0.1:23333) forwarded `provider.apiKey` verbatim to the upstream client. For providers configured with multiple comma-separated keys (e.g. `key1,key2,key3`), the whole string was sent as the bearer token, causing upstream 403 `Authorization failed` even though the same provider worked from the Cherry Studio UI chat path. After this PR: The API server now takes the first comma-separated key before constructing the upstream client, in the two call sites that previously used the raw string: - `src/main/apiServer/services/chat-completion.ts` — the OpenAI client built for `/v1/chat/completions`. - `src/main/apiServer/routes/knowledge/handlers.ts` — the embed/rerank provider config used by the knowledge routes. This matches the existing main-process convention at `src/main/services/OpenClawService.ts:783`: ```ts let apiKey = provider.apiKey ? provider.apiKey.split(',')[0].trim() : '' ``` Fixes #14344 ### Why we need it and why it was done in this way The UI chat path rotates comma-separated keys via `getRotatedApiKey` (which depends on `window.keyv`, a renderer-only API). The API server path didn't split the string at all, producing inconsistent behavior. Rather than introduce a second rotation implementation in the main process, this PR matches the existing main-process convention of taking the first key. That fixes the reported 403 with a minimal, one-line change per call site — appropriate for a frozen-main hotfix. The following tradeoffs were made: - No rotation in main. Users with multiple keys will always hit the first one from the API server. Rotation remains UI-only; reaching parity would require extracting a shared helper and replacing the `window.keyv` dependency, which is a refactor blocked by the main-branch freeze policy. The following alternatives were considered: - A main-side rotation helper with per-provider in-memory state. Rejected: duplicates renderer logic, expands scope beyond the bug fix, and contradicts the existing `OpenClawService` pattern. - Extracting a shared rotation helper into `packages/shared`. Deferred to v2: requires replacing `window.keyv` with a runtime-agnostic store. Links to places where the discussion took place: https://github.com/CherryHQ/cherry-studio/issues/14344 ### Breaking changes None. Single-key providers behave identically; multi-key providers now succeed instead of returning 403. Behavior matches `OpenClawService` (always first key). ### Special notes for your reviewer Two commits on this branch: 1. Initial attempt that added a main-side rotation helper and tests. 2. Follow-up that removes the helper and switches to the `.split(',')[0].trim()` pattern matching `OpenClawService`. Recommend squash-merge so the final history shows just the one-line-per-call-site fix. ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: Write code that humans can understand and Keep it simple - [x] Refactor: You have left the code cleaner than you found it (Boy Scout Rule) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A user-guide update was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code before requesting review from others ### Release note ```release-note Fix the local OpenAI-compatible API server (http://127.0.0.1:23333) returning 403 Authorization failed for providers configured with multiple comma-separated API keys. The API server now takes the first key, matching the existing main-process behavior used by other services. ``` --------- Signed-off-by: suyao --- src/main/apiServer/routes/knowledge/handlers.ts | 6 +++++- src/main/apiServer/services/chat-completion.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/apiServer/routes/knowledge/handlers.ts b/src/main/apiServer/routes/knowledge/handlers.ts index 6af64725e0..990fa5625f 100644 --- a/src/main/apiServer/routes/knowledge/handlers.ts +++ b/src/main/apiServer/routes/knowledge/handlers.ts @@ -142,8 +142,12 @@ async function getProviderConfig(providerId: string): Promise<{ apiKey: string; baseURL = baseURL.replace(/\/+$/, '') baseURL = baseURL.replace(/#$/, '') + // If multiple API keys are configured (comma-separated), use the first one. + // Matches the main-process convention in OpenClawService. + const apiKey = provider.apiKey ? provider.apiKey.split(',')[0].trim() : '' + return { - apiKey: provider.apiKey || '', + apiKey, baseURL } } diff --git a/src/main/apiServer/services/chat-completion.ts b/src/main/apiServer/services/chat-completion.ts index 644bd2778f..9ad08b3f00 100644 --- a/src/main/apiServer/services/chat-completion.ts +++ b/src/main/apiServer/services/chat-completion.ts @@ -67,9 +67,13 @@ export class ChatCompletionService { const modelId = modelValidation.modelId! + // If multiple API keys are configured (comma-separated), use the first one. + // Matches the main-process convention in OpenClawService. + const apiKey = provider.apiKey ? provider.apiKey.split(',')[0].trim() : '' + const client = new OpenAI({ baseURL: provider.apiHost, - apiKey: provider.apiKey + apiKey }) return {