From 59294a5256839bb721419911580058015561e62f Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Fri, 20 Feb 2026 15:47:10 +0100 Subject: [PATCH] fix: use strict=False in json.loads for structured output parsing LLM-generated structured output (e.g. thinking fields) can contain literal control characters like \n. After the first JSON decode from the HTTP response, these become raw control characters in Python strings. The second json.loads() in parse_output() then rejects them under default strict=True mode. Fix by passing strict=False to json.loads in all 8 provider parse_output methods. --- src/celeste/providers/anthropic/messages/parameters.py | 2 +- src/celeste/providers/cohere/chat/parameters.py | 2 +- src/celeste/providers/deepseek/chat/parameters.py | 2 +- src/celeste/providers/google/generate_content/parameters.py | 4 +++- src/celeste/providers/google/interactions/parameters.py | 4 +++- src/celeste/providers/groq/chat/parameters.py | 4 +++- src/celeste/providers/mistral/chat/parameters.py | 2 +- src/celeste/providers/moonshot/chat/parameters.py | 2 +- 8 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/celeste/providers/anthropic/messages/parameters.py b/src/celeste/providers/anthropic/messages/parameters.py index da5d2cee..1dfd9d95 100644 --- a/src/celeste/providers/anthropic/messages/parameters.py +++ b/src/celeste/providers/anthropic/messages/parameters.py @@ -208,7 +208,7 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten if isinstance(content, dict): parsed = content elif isinstance(content, str): - parsed = json.loads(content) + parsed = json.loads(content, strict=False) else: parsed = content diff --git a/src/celeste/providers/cohere/chat/parameters.py b/src/celeste/providers/cohere/chat/parameters.py index a75dd560..73d55f7f 100644 --- a/src/celeste/providers/cohere/chat/parameters.py +++ b/src/celeste/providers/cohere/chat/parameters.py @@ -125,7 +125,7 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten return content if isinstance(content, str): - parsed = json.loads(content) + parsed = json.loads(content, strict=False) else: parsed = content diff --git a/src/celeste/providers/deepseek/chat/parameters.py b/src/celeste/providers/deepseek/chat/parameters.py index d2e12373..7d6f084d 100644 --- a/src/celeste/providers/deepseek/chat/parameters.py +++ b/src/celeste/providers/deepseek/chat/parameters.py @@ -79,7 +79,7 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten return content if isinstance(content, str): - parsed = json.loads(content) + parsed = json.loads(content, strict=False) else: parsed = content diff --git a/src/celeste/providers/google/generate_content/parameters.py b/src/celeste/providers/google/generate_content/parameters.py index 939b1c54..ed6f54d9 100644 --- a/src/celeste/providers/google/generate_content/parameters.py +++ b/src/celeste/providers/google/generate_content/parameters.py @@ -232,7 +232,9 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten if isinstance(content, list) and content and isinstance(content[0], BaseModel): return content - parsed = json.loads(content) if isinstance(content, str) else content + parsed = ( + json.loads(content, strict=False) if isinstance(content, str) else content + ) # For list[T], handle various formats Google might return origin = get_origin(value) diff --git a/src/celeste/providers/google/interactions/parameters.py b/src/celeste/providers/google/interactions/parameters.py index ba683b18..f966891a 100644 --- a/src/celeste/providers/google/interactions/parameters.py +++ b/src/celeste/providers/google/interactions/parameters.py @@ -210,7 +210,9 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten if isinstance(content, list) and content and isinstance(content[0], BaseModel): return content - parsed = json.loads(content) if isinstance(content, str) else content + parsed = ( + json.loads(content, strict=False) if isinstance(content, str) else content + ) # For list[T], handle various formats Google might return origin = get_origin(value) diff --git a/src/celeste/providers/groq/chat/parameters.py b/src/celeste/providers/groq/chat/parameters.py index 8b409530..26ad7170 100644 --- a/src/celeste/providers/groq/chat/parameters.py +++ b/src/celeste/providers/groq/chat/parameters.py @@ -108,7 +108,9 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten if isinstance(content, list) and content and isinstance(content[0], BaseModel): return content - parsed_json = json.loads(content) if isinstance(content, str) else content + parsed_json = ( + json.loads(content, strict=False) if isinstance(content, str) else content + ) # Unwrap list from items wrapper origin = get_origin(value) diff --git a/src/celeste/providers/mistral/chat/parameters.py b/src/celeste/providers/mistral/chat/parameters.py index a7f829ae..8a5e1a84 100644 --- a/src/celeste/providers/mistral/chat/parameters.py +++ b/src/celeste/providers/mistral/chat/parameters.py @@ -121,7 +121,7 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten return content if isinstance(content, str): - parsed = json.loads(content) + parsed = json.loads(content, strict=False) else: parsed = content return TypeAdapter(value).validate_python(parsed) diff --git a/src/celeste/providers/moonshot/chat/parameters.py b/src/celeste/providers/moonshot/chat/parameters.py index 9e191d1c..d0ab3eb8 100644 --- a/src/celeste/providers/moonshot/chat/parameters.py +++ b/src/celeste/providers/moonshot/chat/parameters.py @@ -79,7 +79,7 @@ def parse_output(self, content: TextContent, value: object | None) -> TextConten return content if isinstance(content, str): - parsed = json.loads(content) + parsed = json.loads(content, strict=False) else: parsed = content