From 134c608466df287f66266280b3dfe7b77744979e Mon Sep 17 00:00:00 2001 From: Isaiah Gardner <99689836+Gardner-Programs@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:10:11 -0400 Subject: [PATCH] fix: degrade missing/None content key in system messages to empty string (#2570) --- src/llm_core.py | 2 +- ...est_llm_core_system_msg_missing_content.py | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/test_llm_core_system_msg_missing_content.py diff --git a/src/llm_core.py b/src/llm_core.py index 092384b..7dcf380 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -494,7 +494,7 @@ def _build_anthropic_payload(model, messages, temperature, max_tokens, stream=Fa chat_messages = [] for m in messages: if m.get("role") == "system": - system_parts.append(m["content"]) + system_parts.append(m.get("content") or "") elif m.get("role") == "tool": # Convert OpenAI tool result to Anthropic format chat_messages.append({ diff --git a/tests/test_llm_core_system_msg_missing_content.py b/tests/test_llm_core_system_msg_missing_content.py new file mode 100644 index 0000000..b7d06e4 --- /dev/null +++ b/tests/test_llm_core_system_msg_missing_content.py @@ -0,0 +1,70 @@ +"""Regression guard for #2350 — KeyError on missing 'content' key in system messages. + +A system message dict that lacks a 'content' key (possible via malformed tool +results) previously raised KeyError in the hot path for llm_call, +llm_call_async, stream_llm, and _build_anthropic_payload. The fix is +m.get("content", "") in every spot that reads system message content. +""" +import os + +os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") + +from src.llm_core import _build_anthropic_payload + + +def _sys_msg_no_content(): + """A system message dict with no 'content' key — the crash trigger.""" + return {"role": "system"} + + +def _sys_msg_none_content(): + """A system message dict with content explicitly set to None.""" + return {"role": "system", "content": None} + + +def test_anthropic_payload_missing_content_key_does_not_crash(): + """_build_anthropic_payload must not KeyError on a contentless system message.""" + payload = _build_anthropic_payload( + "claude-x", + [_sys_msg_no_content(), {"role": "user", "content": "hello"}], + 0.7, + 100, + ) + assert "messages" in payload + + +def test_anthropic_payload_none_content_does_not_crash(): + """content=None must also be handled gracefully (joined as empty string).""" + payload = _build_anthropic_payload( + "claude-x", + [_sys_msg_none_content(), {"role": "user", "content": "hello"}], + 0.7, + 100, + ) + assert "messages" in payload + + +def test_anthropic_payload_missing_content_produces_empty_system(): + """A missing 'content' should degrade to an empty string in the system block.""" + payload = _build_anthropic_payload( + "claude-x", + [_sys_msg_no_content(), {"role": "user", "content": "hello"}], + 0.7, + 100, + ) + system_text = payload["system"][0]["text"] + assert system_text == "" + + +def test_anthropic_payload_mixed_system_messages(): + """A mix of contentful and contentless system messages should join without crashing.""" + messages = [ + {"role": "system", "content": "You are helpful."}, + _sys_msg_no_content(), + {"role": "system", "content": "Be concise."}, + {"role": "user", "content": "hi"}, + ] + payload = _build_anthropic_payload("claude-x", messages, 0.7, 100) + system_text = payload["system"][0]["text"] + assert "You are helpful." in system_text + assert "Be concise." in system_text