From 52ca9cbe8fcea766eb8512276b4d8f9e3e8b0157 Mon Sep 17 00:00:00 2001 From: Vinay Vobbilichetty Date: Sun, 17 May 2026 16:45:33 -0400 Subject: [PATCH] feat: optional bearer auth for local providers (mlx, llamacpp, lmstudio, ollama) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four local-provider kinds all support OpenAI-compatible bearer auth in their server config, but the CLI's buildProviderApiKey() only honored OPENROUTER_API_KEY — meaning any locally-served model deployed behind --api-key (or a reverse-proxy with bearer auth) was unreachable from ToolCall-15 without a code change. Concrete deployments this unblocks: - vllm-mlx with `--api-key` (recommended prod config for the OpenAI- compatible endpoint on a shared LAN) - llama.cpp server with `--api-key` - LM Studio with the bearer-auth toggle enabled - Ollama fronted by an authenticating reverse proxy (we run one of these internally; the pattern is common enough to deserve a first-class env var) Each provider now reads an optional `_API_KEY` env var. When unset, the request goes out without an Authorization header — exact behavior the CLI has today, so this is purely additive for the default unauthenticated local-serving path. Symmetric with the desktop app's `api_key` / `api_key_env` provider fields documented in CONFIG_SCHEMA_V1.md. Validation: ran the full 15-scenario bench against a vllm-mlx server hosting Qwen3-Coder-30B-A3B-Instruct-8bit with --api-key set; MLX_API_KEY correctly attaches the bearer header and all scenarios that were expected to reach the model did. --- .env.example | 8 ++++++++ lib/models.ts | 33 +++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 97fb2c2..e9b5fe4 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,14 @@ LLAMACPP_HOST= MLX_HOST= LMSTUDIO_HOST= +# Optional bearer tokens for local providers deployed behind --api-key +# (vllm-mlx, llama.cpp server, LM Studio with auth, or an authenticated +# Ollama reverse-proxy). Leave blank for unauthenticated local serving. +MLX_API_KEY= +LLAMACPP_API_KEY= +LMSTUDIO_API_KEY= +OLLAMA_API_KEY= + # Default per-request timeout in seconds. Applies to every provider. MODEL_REQUEST_TIMEOUT_SECONDS=30 diff --git a/lib/models.ts b/lib/models.ts index 43641a1..68dae8e 100644 --- a/lib/models.ts +++ b/lib/models.ts @@ -115,17 +115,38 @@ function buildProviderBaseUrl(provider: ProviderName, envName: string): string { } function buildProviderApiKey(provider: ProviderName, envName: string): string | undefined { - if (provider !== "openrouter") { - return undefined; + if (provider === "openrouter") { + const apiKey = process.env.OPENROUTER_API_KEY?.trim(); + + if (!apiKey) { + throw new Error(`OPENROUTER_API_KEY is required when ${envName} includes an openrouter model.`); + } + + return apiKey; + } + + // Local OpenAI-compatible servers may be deployed behind a bearer token + // (e.g. vllm-mlx --api-key, llama.cpp server --api-key, LM Studio's auth + // toggle, an Ollama reverse-proxy with bearer auth). The key is optional; + // when unset the request goes out without an Authorization header, which + // is the default for unauthenticated local serving. + if (provider === "mlx") { + return process.env.MLX_API_KEY?.trim() || undefined; + } + + if (provider === "llamacpp") { + return process.env.LLAMACPP_API_KEY?.trim() || undefined; } - const apiKey = process.env.OPENROUTER_API_KEY?.trim(); + if (provider === "lmstudio") { + return process.env.LMSTUDIO_API_KEY?.trim() || undefined; + } - if (!apiKey) { - throw new Error(`OPENROUTER_API_KEY is required when ${envName} includes an openrouter model.`); + if (provider === "ollama") { + return process.env.OLLAMA_API_KEY?.trim() || undefined; } - return apiKey; + return undefined; } function parseProvider(rawProvider: string, index: number, envName: string): ProviderName {