From 25a112e8279a84ffb37c65713e129b91dc97f94b Mon Sep 17 00:00:00 2001 From: Tomasz Maruszak Date: Wed, 25 Mar 2026 11:32:38 +0100 Subject: [PATCH] fix: remove empty Content block from tool-call-only messages The xAI gRPC API previously required at least one Content entry per message, including assistant messages that only carry tool calls (no text). A workaround padded those messages with an empty `new Content()` to satisfy that constraint. The API has since reversed this requirement and now rejects any message that contains an empty content block with: Status(StatusCode="InvalidArgument", Detail="Empty content block") Remove the padding. Messages with tool calls and no text content are now sent with an empty Content collection, which the API accepts. Signed-off-by: Tomasz Maruszak --- src/xAI.Tests/ChatClientTests.cs | 36 ++++++++++++++++++++++++++++++++ src/xAI/GrokChatClient.cs | 4 ---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/xAI.Tests/ChatClientTests.cs b/src/xAI.Tests/ChatClientTests.cs index 90ac5ce..93daa6d 100644 --- a/src/xAI.Tests/ChatClientTests.cs +++ b/src/xAI.Tests/ChatClientTests.cs @@ -705,6 +705,42 @@ public async Task GrokSetsToolCallIdOnlyWhenCallIdIsProvided() Assert.False(toolMessage.HasToolCallId); } + [Fact] + public async Task GrokDoesNotAddEmptyContentToToolCallOnlyMessages() + { + GetCompletionsRequest? capturedRequest = null; + var client = new Mock(MockBehavior.Strict); + client.Setup(x => x.GetCompletionAsync(It.IsAny(), null, null, CancellationToken.None)) + .Callback((req, _, _, _) => capturedRequest = req) + .Returns(CallHelpers.CreateAsyncUnaryCall(new GetChatCompletionResponse + { + Outputs = + { + new CompletionOutput + { + Message = new CompletionMessage { Content = "Done" } + } + } + })); + + var grok = new GrokChatClient(client.Object, "grok-4-1-fast-non-reasoning"); + var messages = new List + { + new(ChatRole.User, "What's the time?"), + // Assistant message with only a tool call and no text content + new(ChatRole.Assistant, [new FunctionCallContent("call-789", "get_time")]), + new(ChatRole.Tool, [new FunctionResultContent("call-789", "2024-01-01T00:00:00Z")]), + }; + + await grok.GetResponseAsync(messages); + + Assert.NotNull(capturedRequest); + // Every Content item in every message must be non-empty; an empty Content block + // causes the API to return StatusCode="InvalidArgument", Detail="Empty content block". + Assert.DoesNotContain(capturedRequest.Messages, + m => m.Content.Any(c => c.CalculateSize() == 0)); + } + [Fact] public async Task GrokSendsDataContentAsBase64ImageUrl() { diff --git a/src/xAI/GrokChatClient.cs b/src/xAI/GrokChatClient.cs index 553606b..eea6bdf 100644 --- a/src/xAI/GrokChatClient.cs +++ b/src/xAI/GrokChatClient.cs @@ -248,10 +248,6 @@ codeResult.RawRepresentation is ToolCall codeToolCall && if (gmsg.Content.Count == 0 && gmsg.ToolCalls.Count == 0) continue; - // If we have only tool calls and no content, the gRPC enpoint fails, so add an empty one. - if (gmsg.Content.Count == 0) - gmsg.Content.Add(new Content()); - request.Messages.Add(gmsg); }