diff --git a/letta/llm_api/openai_client.py b/letta/llm_api/openai_client.py index 991e8d84..48201362 100644 --- a/letta/llm_api/openai_client.py +++ b/letta/llm_api/openai_client.py @@ -620,12 +620,20 @@ class OpenAIClient(LLMClientBase): client = OpenAI(**self._prepare_client_kwargs(llm_config)) # Route based on payload shape: Responses uses 'input', Chat Completions uses 'messages' - if "input" in request_data and "messages" not in request_data: - resp = client.responses.create(**request_data) - return resp.model_dump() - else: - response: ChatCompletion = client.chat.completions.create(**request_data) - return response.model_dump() + try: + if "input" in request_data and "messages" not in request_data: + resp = client.responses.create(**request_data) + return resp.model_dump() + else: + response: ChatCompletion = client.chat.completions.create(**request_data) + return response.model_dump() + except json.JSONDecodeError as e: + logger.error(f"[OpenAI] Failed to parse API response as JSON: {e}") + raise LLMServerError( + message=f"OpenAI API returned invalid JSON response (likely an HTML error page): {str(e)}", + code=ErrorCode.INTERNAL_SERVER_ERROR, + details={"json_error": str(e), "error_position": f"line {e.lineno} column {e.colno}"}, + ) @trace_method async def request_async(self, request_data: dict, llm_config: LLMConfig) -> dict: @@ -638,12 +646,20 @@ class OpenAIClient(LLMClientBase): kwargs = await self._prepare_client_kwargs_async(llm_config) client = AsyncOpenAI(**kwargs) # Route based on payload shape: Responses uses 'input', Chat Completions uses 'messages' - if "input" in request_data and "messages" not in request_data: - resp = await client.responses.create(**request_data) - return resp.model_dump() - else: - response: ChatCompletion = await client.chat.completions.create(**request_data) - return response.model_dump() + try: + if "input" in request_data and "messages" not in request_data: + resp = await client.responses.create(**request_data) + return resp.model_dump() + else: + response: ChatCompletion = await client.chat.completions.create(**request_data) + return response.model_dump() + except json.JSONDecodeError as e: + logger.error(f"[OpenAI] Failed to parse API response as JSON: {e}") + raise LLMServerError( + message=f"OpenAI API returned invalid JSON response (likely an HTML error page): {str(e)}", + code=ErrorCode.INTERNAL_SERVER_ERROR, + details={"json_error": str(e), "error_position": f"line {e.lineno} column {e.colno}"}, + ) def is_reasoning_model(self, llm_config: LLMConfig) -> bool: return is_openai_reasoning_model(llm_config.model) diff --git a/letta/schemas/providers/base.py b/letta/schemas/providers/base.py index 73e4a239..bad77164 100644 --- a/letta/schemas/providers/base.py +++ b/letta/schemas/providers/base.py @@ -263,6 +263,11 @@ class ProviderCreate(ProviderBase): base_url: str | None = Field(None, description="Base URL used for requests to the provider.") api_version: str | None = Field(None, description="API version used for requests to the provider.") + @field_validator("api_key", "access_key", mode="before") + @classmethod + def strip_whitespace(cls, v: str | None) -> str | None: + return v.strip() if isinstance(v, str) else v + class ProviderUpdate(ProviderBase): api_key: str = Field(..., description="API key or secret key used for requests to the provider.") @@ -271,6 +276,11 @@ class ProviderUpdate(ProviderBase): base_url: str | None = Field(None, description="Base URL used for requests to the provider.") api_version: str | None = Field(None, description="API version used for requests to the provider.") + @field_validator("api_key", "access_key", mode="before") + @classmethod + def strip_whitespace(cls, v: str | None) -> str | None: + return v.strip() if isinstance(v, str) else v + class ProviderCheck(BaseModel): provider_type: ProviderType = Field(..., description="The type of the provider.") @@ -279,3 +289,8 @@ class ProviderCheck(BaseModel): region: str | None = Field(None, description="Region used for requests to the provider.") base_url: str | None = Field(None, description="Base URL used for requests to the provider.") api_version: str | None = Field(None, description="API version used for requests to the provider.") + + @field_validator("api_key", "access_key", mode="before") + @classmethod + def strip_whitespace(cls, v: str | None) -> str | None: + return v.strip() if isinstance(v, str) else v diff --git a/letta/services/mcp/base_client.py b/letta/services/mcp/base_client.py index d297e83f..6b4a0536 100644 --- a/letta/services/mcp/base_client.py +++ b/letta/services/mcp/base_client.py @@ -87,6 +87,9 @@ class AsyncBaseMCPClient: # Log at debug level to avoid triggering production alerts for expected failures if e.__class__.__name__ in ("McpError", "ToolError"): logger.debug(f"MCP tool '{tool_name}' execution failed: {str(e)}") + # Return error message with failure status instead of raising to avoid Datadog alerts + return str(e), False + # Re-raise unexpected errors raise parsed_content = []