Files
OmniRoute/open-sse/handlers/chatCore/cliproxyapiCredentials.ts
Ravi Tharuma 2da9ade59b fix(sse): honor CLIProxyAPI environment API key (#12099)
Honra a chave de API dedicada de ambiente do CLIProxyAPI, com teste próprio. Validado no worktree combinado. Obrigado!
2026-08-30 11:10:25 -03:00

79 lines
3.2 KiB
TypeScript

/**
* CLIProxyAPI dedicated-credential resolution (#7645).
*
* CLIProxyAPI requires its own separately-configured `api-keys:` credential
* and rejects any other token with 401. Before this fix, both the direct
* `mode: "cliproxyapi"` passthrough leg and the `mode: "fallback"` retry leg
* (`open-sse/handlers/chatCore/executorProxy.ts::resolveExecutorWithProxy`)
* reused the resolved connection's own credentials — the native provider's
* key — as the Authorization header sent to CLIProxyAPI, making the fallback
* path a permanent no-op for every provider configured this way.
*
* This module resolves and applies the dedicated `cliproxyapi_api_key`
* setting at the executor boundary, so `CliproxyapiExecutor` itself stays
* credential-source-agnostic (it just uses whatever `credentials` it's
* handed — see `buildHeaders()`).
*/
import type { ProviderCredentials } from "../../executors/base.ts";
type ExecutorInput = {
credentials: ProviderCredentials;
};
// No index signature: executors (BaseExecutor subclasses) must satisfy this
// structurally, and class instances don't carry index signatures.
type ExecutorLike = {
execute: (input: ExecutorInput) => Promise<unknown>;
};
/**
* 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<string, unknown> | null | undefined
): string | null {
const raw = settings?.cliproxyapi_api_key;
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;
}
/**
* Builds the credentials to use for a CLIProxyAPI-bound request. When a
* dedicated key is configured it always wins — CLIProxyAPI is a single
* shared instance serving every provider, so the resolved connection's own
* (provider-specific, and possibly already-failed) credential is never the
* right token for it. Falls back to the connection's own credentials only
* when no dedicated key is configured, preserving the pre-existing behavior
* for operators who previously worked around this by pasting a valid
* CLIProxyAPI key into the connection's own `apiKey` field.
*/
export function resolveCliproxyapiCredentials(
connectionCredentials: ProviderCredentials,
dedicatedApiKey: string | null
): ProviderCredentials {
if (!dedicatedApiKey) return connectionCredentials;
return { ...connectionCredentials, apiKey: dedicatedApiKey, accessToken: undefined };
}
/**
* Wraps an executor so every `execute()` call is routed with the dedicated
* CLIProxyAPI credential substituted in when one is configured. No-op
* wrapper when no dedicated key is set (returns the executor unchanged).
*/
export function wrapExecutorWithCliproxyapiCredentials<T extends ExecutorLike>(
executor: T,
dedicatedApiKey: string | null
): T {
if (!dedicatedApiKey) return executor;
const wrapped = Object.create(executor) as T;
wrapped.execute = (input: ExecutorInput) =>
executor.execute({
...input,
credentials: resolveCliproxyapiCredentials(input.credentials, dedicatedApiKey),
});
return wrapped;
}