From f0b7c8451e02b7dee4c4eaa53a604a220af1ebc9 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Sat, 26 Sep 2026 06:23:15 -0700 Subject: [PATCH] Omit blank response text from Anthropic message history --- .../Models/AnthropicLanguageModel.swift | 9 ++++- .../AnthropicLanguageModelTests.swift | 33 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift b/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift index 9371ba7b..2f7dd7cc 100644 --- a/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift @@ -999,10 +999,17 @@ extension Transcript { ) ) case .response(let response): + // Anthropic rejects text blocks without non-whitespace text, + // such as the empty response of a turn that only called tools. + let content = convertSegmentsToAnthropicContent(response.segments).filter { block in + guard case .text(let text) = block else { return true } + return !text.text.allSatisfy(\.isWhitespace) + } + guard !content.isEmpty else { continue } messages.append( .init( role: .assistant, - content: convertSegmentsToAnthropicContent(response.segments) + content: content ) ) case .toolCalls(let toolCalls): diff --git a/Tests/AnyLanguageModelTests/AnthropicLanguageModelTests.swift b/Tests/AnyLanguageModelTests/AnthropicLanguageModelTests.swift index 77d73eee..5c16e13f 100644 --- a/Tests/AnyLanguageModelTests/AnthropicLanguageModelTests.swift +++ b/Tests/AnyLanguageModelTests/AnthropicLanguageModelTests.swift @@ -165,7 +165,7 @@ struct AnthropicLanguageModelTests { @Suite("Anthropic request encoding", .serialized) struct AnthropicRequestTests { - private func makeSession() -> LanguageModelSession { + private func makeSession(transcript: Transcript = Transcript()) -> LanguageModelSession { let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [AnthropicRequestURLProtocol.self] let model = AnthropicLanguageModel( @@ -173,7 +173,7 @@ struct AnthropicLanguageModelTests { model: "test-model", session: URLSession(configuration: configuration) ) - return LanguageModelSession(model: model) + return LanguageModelSession(model: model, transcript: transcript) } private func requestBody() throws -> [String: JSONValue] { @@ -262,6 +262,35 @@ struct AnthropicLanguageModelTests { #expect(body["output_config"] == nil) } + @Test(arguments: ["", " \n"]) + func blankResponseIsOmittedFromHistory(responseText: String) async throws { + let arguments = try GeneratedContent(json: #"{"city":"Cupertino"}"#) + let transcript = Transcript(entries: [ + .prompt(Transcript.Prompt(segments: [.text(.init(content: "Weather?"))])), + .toolCalls( + Transcript.ToolCalls([ + Transcript.ToolCall(id: "call-1", toolName: "getWeather", arguments: arguments) + ]) + ), + .toolOutput( + Transcript.ToolOutput( + id: "call-1", + toolName: "getWeather", + segments: [.text(.init(content: "Sunny"))] + ) + ), + .response(Transcript.Response(assetIDs: [], segments: [.text(.init(content: responseText))])), + ]) + _ = try await makeSession(transcript: transcript).respond(to: "Thanks") + + let messages = try #require(requestBody()["messages"]?.arrayValue) + let roles = messages.map { $0.objectValue?["role"]?.stringValue } + #expect(roles == ["user", "assistant", "user", "user"]) + let texts = messages.flatMap { $0.objectValue?["content"]?.arrayValue ?? [] } + .compactMap { $0.objectValue?["text"]?.stringValue } + #expect(texts == ["Weather?", "Thanks"]) + } + @Test func extraBodyOverridesCustomOptions() async throws { var options = GenerationOptions() options[custom: AnthropicLanguageModel.self] = .init(