From 2da9ade59b618dfda06c253b007fddc542d6de2e Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:10:25 +0200 Subject: [PATCH] fix(sse): honor CLIProxyAPI environment API key (#12099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Honra a chave de API dedicada de ambiente do CLIProxyAPI, com teste próprio. Validado no worktree combinado. Obrigado! --- .env.example | 2 + docs/reference/ENVIRONMENT.md | 1 + .../chatCore/cliproxyapiCredentials.ts | 8 ++-- open-sse/handlers/chatCore/executorProxy.ts | 7 +++- ...proxyapi-dedicated-credential-7645.test.ts | 38 +++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 1dba15a22b..f8ff0f39d8 100644 --- a/.env.example +++ b/.env.example @@ -2042,6 +2042,8 @@ APP_LOG_TO_FILE=true # CLIPROXYAPI_HOST=127.0.0.1 # CLIPROXYAPI_PORT=5544 # CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api +# Data-plane key fallback; the cliproxyapi_api_key setting takes precedence. +# CLIPROXYAPI_API_KEY= # Management key for an externally managed instance. Embedded instances use # OmniRoute's encrypted service key. # CLIPROXYAPI_MANAGEMENT_KEY= diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 53b232196e..a622e175e2 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1046,6 +1046,7 @@ desktop install. | `EMBED_WS_PROXY_PORT` | `20131` | `src/lib/services/embedWsProxy.ts` | Port for the embedded-service WebSocket proxy server. | | `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). | | `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. | +| `CLIPROXYAPI_API_KEY` | _(empty)_ | `open-sse/handlers/chatCore/cliproxyapiCredentials.ts` | Data-plane key fallback when the `cliproxyapi_api_key` setting is absent. | | `CLIPROXYAPI_MANAGEMENT_KEY` | _(empty)_ | `src/lib/services/cliproxyAccountHealth.ts` | Management key for account-health reads from an externally managed CLIProxyAPI instance. | | `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. | | `MUX_SERVICE_PORT` | `8322` | `src/lib/services/bootstrap.ts` | Override the port where the embedded Mux (coder/mux) agent-orchestration daemon listens (always 127.0.0.1). | diff --git a/open-sse/handlers/chatCore/cliproxyapiCredentials.ts b/open-sse/handlers/chatCore/cliproxyapiCredentials.ts index e9fc5fbe7b..6ee6b2433f 100644 --- a/open-sse/handlers/chatCore/cliproxyapiCredentials.ts +++ b/open-sse/handlers/chatCore/cliproxyapiCredentials.ts @@ -28,14 +28,16 @@ type ExecutorLike = { }; /** - * Reads the dedicated CLIProxyAPI key out of a settings blob (as returned by - * `getCachedSettings()`), trimmed and normalized to `null` when absent/blank. + * Reads the dedicated CLIProxyAPI key from settings, then falls back to the + * environment. Values are trimmed and normalized to `null` when absent/blank. */ export function resolveDedicatedCliproxyapiApiKey( settings: Record | null | undefined ): string | null { const raw = settings?.cliproxyapi_api_key; - return typeof raw === "string" && raw.trim() ? raw.trim() : null; + if (typeof raw === "string" && raw.trim()) return raw.trim(); + const envKey = process.env.CLIPROXYAPI_API_KEY; + return typeof envKey === "string" && envKey.trim() ? envKey.trim() : null; } /** diff --git a/open-sse/handlers/chatCore/executorProxy.ts b/open-sse/handlers/chatCore/executorProxy.ts index 9fb51a6895..4eb35098d9 100644 --- a/open-sse/handlers/chatCore/executorProxy.ts +++ b/open-sse/handlers/chatCore/executorProxy.ts @@ -56,7 +56,7 @@ function parseFallbackCodes(raw: unknown): number[] | null { * Reads the CLIProxyAPI-related settings shared by both the direct * `mode: "cliproxyapi"` passthrough leg and the `mode: "fallback"` retry leg: * the custom fallback status codes and the dedicated credential (#7645). - * Falls back to defaults / no dedicated key on any read failure. + * Falls back to defaults and the environment key on any read failure. */ async function loadCliproxyapiSettings(): Promise<{ fallbackCodes: number[]; @@ -71,7 +71,10 @@ async function loadCliproxyapiSettings(): Promise<{ dedicatedApiKey: resolveDedicatedCliproxyapiApiKey(allSettings), }; } catch { - return { fallbackCodes: [...DEFAULT_FALLBACK_CODES], dedicatedApiKey: null }; + return { + fallbackCodes: [...DEFAULT_FALLBACK_CODES], + dedicatedApiKey: resolveDedicatedCliproxyapiApiKey(null), + }; } } diff --git a/tests/unit/cliproxyapi-dedicated-credential-7645.test.ts b/tests/unit/cliproxyapi-dedicated-credential-7645.test.ts index f539067596..c1e4762b05 100644 --- a/tests/unit/cliproxyapi-dedicated-credential-7645.test.ts +++ b/tests/unit/cliproxyapi-dedicated-credential-7645.test.ts @@ -29,18 +29,27 @@ const settingsDb = await import("../../src/lib/db/settings.ts"); const upstreamProxyDb = await import("../../src/lib/db/upstreamProxy.ts"); const { resolveExecutorWithProxy } = await import("../../open-sse/handlers/chatCore/executorProxy.ts"); +const { resolveDedicatedCliproxyapiApiKey } = + await import("../../open-sse/handlers/chatCore/cliproxyapiCredentials.ts"); const { clearUpstreamProxyConfigCache } = await import("../../open-sse/handlers/chatCore/comboContextCache.ts"); const { updateSettingsSchema } = await import("../../src/shared/validation/settingsSchemas.ts"); const NATIVE_KEY = "sk-native-provider-key-cliproxyapi-must-not-see"; const DEDICATED_KEY = "cpa-dedicated-key-configured-by-operator"; +const ENV_KEY = "cpa-dedicated-key-from-environment"; +const originalEnvKey = process.env.CLIPROXYAPI_API_KEY; before(async () => { await coreDb.ensureDbInitialized(); }); afterEach(async () => { + if (originalEnvKey === undefined) { + delete process.env.CLIPROXYAPI_API_KEY; + } else { + process.env.CLIPROXYAPI_API_KEY = originalEnvKey; + } clearUpstreamProxyConfigCache(); const { dbCache } = await import("../../src/lib/db/readCache.ts"); dbCache?.invalidate?.("settings"); @@ -111,6 +120,34 @@ describe("#7645 — settingsSchemas has a dedicated cliproxyapi_api_key field", }); describe("#7645 — CLIProxyAPI fallback leg authenticates with the dedicated key", () => { + it("uses CLIPROXYAPI_API_KEY when settings are unavailable", () => { + process.env.CLIPROXYAPI_API_KEY = ` ${ENV_KEY} `; + assert.equal(resolveDedicatedCliproxyapiApiKey(null), ENV_KEY); + }); + + it("uses CLIPROXYAPI_API_KEY when no settings key is configured", async () => { + process.env.CLIPROXYAPI_API_KEY = ENV_KEY; + await settingsDb.updateSettings({ cliproxyapi_api_key: "" }); + await upstreamProxyDb.upsertUpstreamProxyConfig({ + providerId: "anthropic-7645-env-key", + mode: "cliproxyapi", + enabled: true, + }); + + const executor = await resolveExecutorWithProxy("anthropic-7645-env-key", undefined, null); + const { headers, called } = await withCapturedCliproxyapiRequest(() => + (executor as ExecutorLike).execute({ + model: "claude-3-opus", + body: { model: "claude-3-opus", messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: { apiKey: NATIVE_KEY }, + }) + ); + + assert.equal(called, true); + assert.equal(headers.Authorization, `Bearer ${ENV_KEY}`); + }); + it("uses the dedicated cliproxyapi_api_key, not the failed native provider's own credential", async () => { await settingsDb.updateSettings({ cliproxyapi_api_key: DEDICATED_KEY }); await upstreamProxyDb.upsertUpstreamProxyConfig({ @@ -210,6 +247,7 @@ describe("#7645 — CLIProxyAPI fallback leg authenticates with the dedicated ke }); it("falls back to the connection's own credential when no dedicated key is configured (no regression)", async () => { + delete process.env.CLIPROXYAPI_API_KEY; await settingsDb.updateSettings({ cliproxyapi_api_key: "" }); await upstreamProxyDb.upsertUpstreamProxyConfig({ providerId: "anthropic-7645-no-dedicated-key",