fix(api-server): rotate comma-separated provider API keys (#14346)

### 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 <sy20010504@gmail.com>
This commit is contained in:
SuYao
2026-04-22 17:30:54 +08:00
committed by GitHub
parent ec4e787d13
commit 3f463e178b
2 changed files with 10 additions and 2 deletions

View File

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

View File

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