From b20453855be65d66de16e303918b97cf08809444 Mon Sep 17 00:00:00 2001 From: Praveen Sampath Date: Thu, 30 Jul 2026 09:49:01 +0000 Subject: [PATCH 1/2] fix(providers): strip empty text blocks from API requests - anthropic_message_adapter: filter empty/whitespace-only text blocks from content lists and tool_result content (fixes Anthropic + Bedrock) - openai: skip empty text blocks when building response input items - openai_compatible: skip empty text blocks in tool result content - Gemini already handled this; Vertex/Azure Foundry delegate to providers above so they're covered --- .../ai/providers/anthropic_message_adapter.py | 24 +++++++++++++++++-- services/ai/providers/openai.py | 8 +++++-- services/ai/providers/openai_compatible.py | 3 ++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/services/ai/providers/anthropic_message_adapter.py b/services/ai/providers/anthropic_message_adapter.py index ba55d1458..5445d5301 100644 --- a/services/ai/providers/anthropic_message_adapter.py +++ b/services/ai/providers/anthropic_message_adapter.py @@ -33,6 +33,22 @@ class OmniSearchResultBlockParam(SearchResultBlockParam, total=False): type AnthropicMessageContent = str | Iterable[OmniContentBlockParam] +def _is_empty_text_block(block: object) -> bool: + """Return True if *block* is a text content block with empty/whitespace-only text.""" + return bool( + isinstance(block, dict) + and block.get("type") == "text" + and not (block.get("text") or "").strip() + ) + + +def _drain_empty_text_blocks(blocks: list) -> bool: + """Remove empty text blocks in-place; return True if any were removed.""" + before = len(blocks) + blocks[:] = [b for b in blocks if not _is_empty_text_block(b)] + return len(blocks) < before + + def extract_text_document(block: DocumentBlockParam) -> str | None: """Convert a text-backed document block into provider-neutral text.""" if block.get("type") != "document": @@ -70,7 +86,9 @@ def _build_content_for_api( ) -> str | list[ContentBlockParam]: if isinstance(content, str): return content - return [_build_block_for_api(block) for block in content] + blocks = [_build_block_for_api(block) for block in content] + _drain_empty_text_blocks(blocks) + return blocks def _build_block_for_api(block: OmniContentBlockParam) -> ContentBlockParam: @@ -104,7 +122,9 @@ def _build_tool_result_content_for_api( ) -> str | list[ToolResultContentBlockParam]: if isinstance(content, str): return content - return [_build_tool_result_content_block_for_api(block) for block in content] + blocks = [_build_tool_result_content_block_for_api(block) for block in content] + _drain_empty_text_blocks(blocks) + return blocks def _build_tool_result_content_block_for_api( diff --git a/services/ai/providers/openai.py b/services/ai/providers/openai.py index 9fe423672..c9965d083 100644 --- a/services/ai/providers/openai.py +++ b/services/ai/providers/openai.py @@ -338,7 +338,9 @@ def _convert_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, An block_type = block.get("type") if block_type == "text": - text_parts.append(block.get("text", "")) + text = block.get("text", "") + if text: + text_parts.append(text) elif block_type == "document" and role == "user": document_text = extract_text_document(block) if document_text is not None: @@ -363,7 +365,9 @@ def _convert_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, An for rb in result_content: if isinstance(rb, dict): if rb.get("type") == "text": - parts.append(rb.get("text", "")) + t = rb.get("text", "") + if t: + parts.append(t) elif rb.get("type") == "search_result": title = rb.get("title", "") source = rb.get("source", "") diff --git a/services/ai/providers/openai_compatible.py b/services/ai/providers/openai_compatible.py index 3c2fd2b09..af554a73a 100644 --- a/services/ai/providers/openai_compatible.py +++ b/services/ai/providers/openai_compatible.py @@ -208,7 +208,8 @@ def _convert_messages_to_openai( continue if rb.get("type") == "text": rb = cast(TextBlockParam, rb) - parts.append(rb["text"]) + if rb["text"]: + parts.append(rb["text"]) elif rb.get("type") == "search_result": title = rb.get("title", "") source = rb.get("source", "") From ccd746dce7a6dc3df272d41d59d2047b2cd01f9d Mon Sep 17 00:00:00 2001 From: Praveen Sampath Date: Thu, 30 Jul 2026 09:57:30 +0000 Subject: [PATCH 2/2] fix(providers): use proper types in _is_empty_text_block and _drain_empty_text_blocks --- services/ai/providers/anthropic_message_adapter.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/services/ai/providers/anthropic_message_adapter.py b/services/ai/providers/anthropic_message_adapter.py index 5445d5301..bd1c2e7c4 100644 --- a/services/ai/providers/anthropic_message_adapter.py +++ b/services/ai/providers/anthropic_message_adapter.py @@ -33,16 +33,14 @@ class OmniSearchResultBlockParam(SearchResultBlockParam, total=False): type AnthropicMessageContent = str | Iterable[OmniContentBlockParam] -def _is_empty_text_block(block: object) -> bool: +def _is_empty_text_block(block: ContentBlockParam) -> bool: """Return True if *block* is a text content block with empty/whitespace-only text.""" return bool( - isinstance(block, dict) - and block.get("type") == "text" - and not (block.get("text") or "").strip() + block.get("type") == "text" and not (block.get("text") or "").strip() ) -def _drain_empty_text_blocks(blocks: list) -> bool: +def _drain_empty_text_blocks(blocks: list[ContentBlockParam]) -> bool: """Remove empty text blocks in-place; return True if any were removed.""" before = len(blocks) blocks[:] = [b for b in blocks if not _is_empty_text_block(b)]