From 745dd1e124fea3a47ad405dae7748e93b8bb8150 Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:38:14 -0800 Subject: [PATCH] fix(core): reject empty API keys in Bearer auth headers (#9350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Empty or None API keys resulted in "Bearer " header values which cause httpx.LocalProtocolError. Use truthiness checks instead of `is not None` to also reject empty strings before constructing Authorization headers. Datadog: https://us5.datadoghq.com/error-tracking/issue/ad3c1e38-d557-11f0-a65d-da7ad0900000 🤖 Generated with [Letta Code](https://letta.com) Co-authored-by: Letta --- letta/functions/mcp_client/types.py | 6 +++--- letta/llm_api/chatgpt_oauth_client.py | 5 +++++ letta/llm_api/mistral.py | 2 +- letta/llm_api/openai.py | 6 +++--- letta/local_llm/utils.py | 8 ++++---- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/letta/functions/mcp_client/types.py b/letta/functions/mcp_client/types.py index 74293d81..18174e8e 100644 --- a/letta/functions/mcp_client/types.py +++ b/letta/functions/mcp_client/types.py @@ -176,11 +176,11 @@ class HTTPBasedServerConfig(BaseServerConfig): Returns: Dictionary of headers or None if no headers are configured """ - if self.custom_headers is not None or (self.auth_header is not None and self.auth_token is not None): + if self.custom_headers is not None or (self.auth_header and self.auth_token): headers = self.custom_headers.copy() if self.custom_headers else {} - # Add auth header if specified - if self.auth_header is not None and self.auth_token is not None: + # Add auth header if specified (skip if either is empty to avoid illegal header values) + if self.auth_header and self.auth_token: headers[self.auth_header] = self.auth_token return headers diff --git a/letta/llm_api/chatgpt_oauth_client.py b/letta/llm_api/chatgpt_oauth_client.py index 47e61eec..9f5a3db5 100644 --- a/letta/llm_api/chatgpt_oauth_client.py +++ b/letta/llm_api/chatgpt_oauth_client.py @@ -154,6 +154,11 @@ class ChatGPTOAuthClient(LLMClientBase): Returns: Dictionary of HTTP headers. """ + if not creds.access_token: + raise LLMAuthenticationError( + message="ChatGPT OAuth access_token is empty or missing", + code=ErrorCode.UNAUTHENTICATED, + ) return { "Authorization": f"Bearer {creds.access_token}", "ChatGPT-Account-Id": creds.account_id, diff --git a/letta/llm_api/mistral.py b/letta/llm_api/mistral.py index 8d5b8b10..ec75e0a3 100644 --- a/letta/llm_api/mistral.py +++ b/letta/llm_api/mistral.py @@ -10,7 +10,7 @@ async def mistral_get_model_list_async(url: str, api_key: str) -> dict: url = smart_urljoin(url, "models") headers = {"Content-Type": "application/json"} - if api_key is not None: + if api_key: headers["Authorization"] = f"Bearer {api_key}" logger.debug("Sending request to %s", url) diff --git a/letta/llm_api/openai.py b/letta/llm_api/openai.py index 69bcaa54..ae4e3451 100644 --- a/letta/llm_api/openai.py +++ b/letta/llm_api/openai.py @@ -59,7 +59,7 @@ async def openai_get_model_list_async( url = smart_urljoin(url, "models") headers = {"Content-Type": "application/json"} - if api_key is not None: + if api_key: headers["Authorization"] = f"Bearer {api_key}" if "openrouter.ai" in url: if model_settings.openrouter_referer: @@ -478,7 +478,7 @@ def openai_chat_completions_request_stream( data = prepare_openai_payload(chat_completion_request) data["stream"] = True - kwargs = {"api_key": api_key, "base_url": url, "max_retries": 0} + kwargs = {"api_key": api_key or "DUMMY_API_KEY", "base_url": url, "max_retries": 0} if "openrouter.ai" in url: headers = {} if model_settings.openrouter_referer: @@ -511,7 +511,7 @@ def openai_chat_completions_request( https://platform.openai.com/docs/guides/text-generation?lang=curl """ data = prepare_openai_payload(chat_completion_request) - kwargs = {"api_key": api_key, "base_url": url, "max_retries": 0} + kwargs = {"api_key": api_key or "DUMMY_API_KEY", "base_url": url, "max_retries": 0} if "openrouter.ai" in url: headers = {} if model_settings.openrouter_referer: diff --git a/letta/local_llm/utils.py b/letta/local_llm/utils.py index 0bbfcb10..905736c2 100644 --- a/letta/local_llm/utils.py +++ b/letta/local_llm/utils.py @@ -25,15 +25,15 @@ def post_json_auth_request(uri, json_payload, auth_type, auth_key): # Used by OpenAI, together.ai, Mistral AI elif auth_type == "bearer_token": - if auth_key is None: - raise ValueError(f"auth_type is {auth_type}, but auth_key is null") + if not auth_key: + raise ValueError(f"auth_type is {auth_type}, but auth_key is null or empty") headers = {"Content-Type": "application/json", "Authorization": f"Bearer {auth_key}"} response = requests.post(uri, json=json_payload, headers=headers) # Used by OpenAI Azure elif auth_type == "api_key": - if auth_key is None: - raise ValueError(f"auth_type is {auth_type}, but auth_key is null") + if not auth_key: + raise ValueError(f"auth_type is {auth_type}, but auth_key is null or empty") headers = {"Content-Type": "application/json", "api-key": f"{auth_key}"} response = requests.post(uri, json=json_payload, headers=headers)