From 4fd9d6b6d5b741ede72439ddec6498eb021e8bd3 Mon Sep 17 00:00:00 2001 From: busya Date: Tue, 24 Feb 2026 14:22:08 +0300 Subject: [PATCH] fix(llm): skip unsupported json_object mode for openrouter stepfun models --- backend/src/plugins/llm_analysis/service.py | 51 +++++++++++++++++---- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/backend/src/plugins/llm_analysis/service.py b/backend/src/plugins/llm_analysis/service.py index b4b78bc..4ef46af 100644 --- a/backend/src/plugins/llm_analysis/service.py +++ b/backend/src/plugins/llm_analysis/service.py @@ -437,6 +437,26 @@ class LLMClient: self.client = AsyncOpenAI(api_key=api_key, base_url=base_url) # [/DEF:LLMClient.__init__:Function] + # [DEF:LLMClient._supports_json_response_format:Function] + # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object. + # @PRE: Client initialized with base_url and default_model. + # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors. + def _supports_json_response_format(self) -> bool: + base = (self.base_url or "").lower() + model = (self.default_model or "").lower() + + # OpenRouter routes to many upstream providers; some models reject json_object mode. + if "openrouter.ai" in base: + incompatible_tokens = ( + "stepfun/", + "step-", + ":free", + ) + if any(token in model for token in incompatible_tokens): + return False + return True + # [/DEF:LLMClient._supports_json_response_format:Function] + # [DEF:LLMClient.get_json_completion:Function] # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing. # @PRE: messages is a list of valid message dictionaries. @@ -460,19 +480,34 @@ class LLMClient: with belief_scope("get_json_completion"): response = None try: + use_json_mode = self._supports_json_response_format() try: - logger.info(f"[get_json_completion] Attempting LLM call with JSON mode for model: {self.default_model}") + logger.info( + f"[get_json_completion] Attempting LLM call for model: {self.default_model} " + f"(json_mode={'on' if use_json_mode else 'off'})" + ) logger.info(f"[get_json_completion] Base URL being used: {self.base_url}") logger.info(f"[get_json_completion] Number of messages: {len(messages)}") logger.info(f"[get_json_completion] API Key present: {bool(self.api_key and len(self.api_key) > 0)}") - - response = await self.client.chat.completions.create( - model=self.default_model, - messages=messages, - response_format={"type": "json_object"} - ) + + if use_json_mode: + response = await self.client.chat.completions.create( + model=self.default_model, + messages=messages, + response_format={"type": "json_object"} + ) + else: + response = await self.client.chat.completions.create( + model=self.default_model, + messages=messages + ) except Exception as e: - if "JSON mode is not enabled" in str(e) or "400" in str(e): + if use_json_mode and ( + "JSON mode is not enabled" in str(e) + or "json_object is not supported" in str(e).lower() + or "response_format" in str(e).lower() + or "400" in str(e) + ): logger.warning(f"[get_json_completion] JSON mode failed or not supported: {str(e)}. Falling back to plain text response.") response = await self.client.chat.completions.create( model=self.default_model,