Skip to content

aiproperties endpoint crashes with Anthropic Claude providers (markdown-wrapped JSON not stripped) #4659

@mosandlt

Description

@mosandlt

Tandoor Version

2.6.9 (Home Assistant add-on by alexbelgium)

Setup

Others (please state below)

Reverse Proxy

Others (please state below)

Other

Cloudflare Tunnel in front of Home Assistant Add-on (alexbelgium/hassio-addons tandoor_recipes 2.6.9). Add-on serves nginx → uwsgi internally.

Bug description

The POST /api/recipe/{id}/aiproperties/?provider={n} endpoint crashes with JSONDecodeError: Expecting value: line 1 column 1 (char 0) whenever the configured AI provider is an Anthropic Claude model (tested with anthropic/claude-sonnet-4-5 via LiteLLM).

Root cause: in cookbook/views/api.py (around line 2084 on the 2.6.9 tag) the code does:

ai_request = {
    'api_key': ai_provider.api_key,
    'model': ai_provider.model_name,
    'response_format': {"type": "json_object"},
    'messages': messages,
}
ai_response = completion(**ai_request)
response_text = ai_response.choices[0].message.content
return Response(json.loads(response_text), status=status.HTTP_200_OK)

Although response_format={"type": "json_object"} is passed to LiteLLM, the Anthropic backend in LiteLLM does not enforce JSON mode the way OpenAI does — Claude is free to return JSON wrapped in a Markdown code fence:

```json
{...}

Calling `json.loads()` on that raw string fails with `Expecting value: line 1 column 1 (char 0)` because the first character is a backtick, not `{`. The same code path works for OpenAI / Gemini providers because those backends actually enforce JSON-only responses on the wire.

The same pattern exists in the AI import path; the import endpoint happens to handle Claude correctly only because the upstream parser on that branch is more lenient.

### Steps to reproduce

1. Add an Anthropic AI Provider in Django admin: model name `anthropic/claude-sonnet-4-5` (or any Claude 4.x model), valid API key.
2. Set the new provider as the space's default AI provider.
3. Open any recipe and trigger the AI property generation.

Result: 500, traceback below.

### Suggested fix

Strip Markdown code fences (and optional `json` language tag) from `response_text` before `json.loads()`. A minimal robust version:

```python
import re

def _strip_json_fence(text: str) -> str:
    text = text.strip()
    m = re.match(r"^```(?:json)?\s*\n(.*)\n```\s*$", text, re.DOTALL)
    return m.group(1) if m else text

response_text = _strip_json_fence(ai_response.choices[0].message.content)
return Response(json.loads(response_text), status=status.HTTP_200_OK)

Even better: switch to Anthropic's structured outputs / tool-use schema enforcement (LiteLLM supports it for Claude 4.x), so the server-side guarantee is preserved. The fence-stripping fix is enough for the immediate crash though, and protects against any future provider that occasionally fences its output.

Relevant logs

ThreadPoolExecutor-0_0 ERROR django.request Internal Server Error: /api/recipe/22/aiproperties/
Traceback (most recent call last):
  File "/opt/recipes/cookbook/views/api.py", line 2084, in aiproperties
    return Response(json.loads(response_text), status=status.HTTP_200_OK)
                    ~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/json/__init__.py", line 352, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.13/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/json/decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

POST /api/recipe/22/aiproperties/?provider=7 HTTP/1.1 500 131

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions