Skip to content

Conversation

@arrdel
Copy link

@arrdel arrdel commented Dec 6, 2025

Fixes #34140

xAI API requires all messages to have at least an empty content field. When using LangGraph agents with tool calling, AIMessage objects can have tool_calls but no content, causing 400 errors.

This fix ensures content is set to empty string instead of None for AIMessages with tool_calls.

…tead of None

xAI's API is stricter than OpenAI's and requires all messages to have at least
an empty content field. When using LangGraph agents with tool calling, AIMessages
can have tool_calls but no content, which causes xAI to return a 400 error:

    'Invalid request content: Each message must have at least one content element.'

This fix overrides the message conversion logic in ChatXAI to ensure that
AIMessages with tool_calls or function_call always have at least an empty string
for content, rather than None (which is acceptable for OpenAI but not xAI).

Changes:
- Added _convert_message_to_dict_xai() function that wraps the OpenAI converter
  and ensures content is never None for AIMessages with tool calls
- Overrode _get_request_payload() to use the xAI-specific converter
- Added necessary imports from langchain_openai.chat_models.base

Fixes langchain-ai#34140
Copilot AI review requested due to automatic review settings December 6, 2025 05:15
@arrdel arrdel requested review from ccurme and mdrxy as code owners December 6, 2025 05:15
@github-actions github-actions bot added integration Related to a provider partner package integration xai labels Dec 6, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Dec 6, 2025

CodSpeed Performance Report

Merging #34229 will not alter performance

Comparing arrdel:fix-chatxai-empty-content-34140 (35c1c1f) with master (80c3970)

Summary

✅ 1 untouched
⏩ 33 skipped1

Footnotes

  1. 33 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a 400 error that occurs when using xAI's API with AIMessages that have tool_calls but no content. The xAI API differs from OpenAI in that it requires the content field to be an empty string rather than None for such messages.

Key Changes:

  • Introduces _convert_message_to_dict_xai function to wrap OpenAI's message converter and ensure content is empty string instead of None for AIMessages with tool_calls/function_call
  • Overrides _get_request_payload to use the xAI-specific message converter for the chat/completions API path

Comment on lines +602 to +610
if self._use_responses_api(payload):
if self.use_previous_response_id:
last_messages, previous_response_id = _get_last_messages(messages)
payload_to_use = last_messages if previous_response_id else messages
if previous_response_id:
payload["previous_response_id"] = previous_response_id
payload = _construct_responses_api_payload(payload_to_use, payload)
else:
payload = _construct_responses_api_payload(messages, payload)
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The responses API path doesn't use the xAI-specific message converter. When _construct_responses_api_payload is called, it internally uses _construct_responses_api_input which calls _convert_message_to_dict from langchain_openai, not _convert_message_to_dict_xai. This means AIMessages with tool_calls will still have content=None instead of content="" when using the responses API, potentially causing the same 400 error this PR aims to fix.

Consider either:

  1. Creating an xAI-specific version of _construct_responses_api_payload that uses _convert_message_to_dict_xai, or
  2. Overriding _construct_responses_api_input to use the xAI converter, or
  3. If xAI doesn't support the responses API, explicitly disable it by overriding _use_responses_api to return False

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +71
def _convert_message_to_dict_xai(
message: BaseMessage,
api: Literal["chat/completions", "responses"] = "chat/completions",
) -> dict:
"""Convert a LangChain message to dictionary format expected by xAI.
xAI's API requires that all messages have at least an empty content field,
unlike OpenAI which allows None for messages with tool_calls.
Args:
message: The LangChain message to convert
api: The API format to use (default: "chat/completions")
Returns:
Dictionary representation of the message compatible with xAI's API
"""
message_dict = _openai_convert_message_to_dict(message, api=api)

# xAI requires content to be at least an empty string, not None
# This is especially important for AIMessages with tool_calls but no content
if isinstance(message, AIMessage) and message_dict.get("content") is None:
if "tool_calls" in message_dict or "function_call" in message_dict:
message_dict["content"] = ""

return message_dict
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new _convert_message_to_dict_xai function. There should be unit tests that verify:

  1. AIMessage with tool_calls but no content gets content="" instead of content=None
  2. AIMessage with function_call but no content gets content="" instead of content=None
  3. AIMessage with both tool_calls and content preserves the content
  4. Other message types are not affected

The existing tests in test_chat_models.py import _convert_message_to_dict from langchain_openai, but don't test the xAI-specific converter. Consider adding tests similar to the existing message conversion tests but using the xAI version.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +61
xAI's API requires that all messages have at least an empty content field,
unlike OpenAI which allows None for messages with tool_calls.
Args:
message: The LangChain message to convert
api: The API format to use (default: "chat/completions")
Returns:
Dictionary representation of the message compatible with xAI's API
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring is missing type information in the Args and Returns sections. Following Google-style docstring conventions, the Args should include types and the Returns section should specify the dict structure or reference the type hint.

Example:

Args:
    message: The LangChain message to convert.
    api: The API format to use. Defaults to "chat/completions".
    
Returns:
    Dictionary representation of the message compatible with xAI's API.

Note: Types should be in the function signature (which they are), so the docstring can focus on describing the parameters rather than repeating types.

Suggested change
xAI's API requires that all messages have at least an empty content field,
unlike OpenAI which allows None for messages with tool_calls.
Args:
message: The LangChain message to convert
api: The API format to use (default: "chat/completions")
Returns:
Dictionary representation of the message compatible with xAI's API
xAI's API requires that all messages have at least an empty content field,
unlike OpenAI which allows None for messages with tool_calls.
Args:
message: The LangChain message to convert.
api: The API format to use. Defaults to "chat/completions".
Returns:
Dictionary representation of the message compatible with xAI's API.

Copilot uses AI. Check for mistakes.
Comment on lines +592 to +594
Overrides the base implementation to use xAI-specific message conversion
that ensures all messages have at least an empty content field.
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring is incomplete. It's missing the Args and Returns sections that are present in similar functions throughout the codebase. Following Google-style docstring conventions:

"""Prepare the request payload for xAI's API.

Overrides the base implementation to use xAI-specific message conversion
that ensures all messages have at least an empty content field.

Args:
    input_: The input to convert into messages.
    stop: List of stop sequences. Defaults to None.
    **kwargs: Additional keyword arguments to pass to the API.

Returns:
    Dictionary containing the request payload for xAI's API.
Suggested change
Overrides the base implementation to use xAI-specific message conversion
that ensures all messages have at least an empty content field.
Overrides the base implementation to use xAI-specific message conversion
that ensures all messages have at least an empty content field.
Args:
input_: The input to convert into messages.
stop: List of stop sequences. Defaults to None.
**kwargs: Additional keyword arguments to pass to the API.
Returns:
Dictionary containing the request payload for xAI's API.

Copilot uses AI. Check for mistakes.
_convert_from_v1_to_chat_completions,
_convert_message_to_dict as _openai_convert_message_to_dict,
_get_last_messages,
_use_responses_api,
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of '_use_responses_api' is not used.

Suggested change
_use_responses_api,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration Related to a provider partner package integration xai

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ChatXAI: 400 error when AIMessage has tool_calls but no content

1 participant