Skip to content

[BUG] with_schema + with_tools: JSON parsing on intermediate tool-call responses causes Anthropic API error #649

@trouni

Description

@trouni

When using with_schema and with_tools together, if the LLM returns both text content and tool calls in the same turn, Chat#complete parses the text content into a Hash before checking tool_call?. On the continuation call, format_text(Hash) wraps it as { type: "text", text: <Hash> }, which Anthropic rejects with:

messages.1.content.0.text.text: Input should be a valid string

Steps to reproduce

chat = RubyLLM.chat(model: 'claude-haiku-4-5')
  .with_instructions("Extract data from the document.")
  .with_tools(MySearchTool)
  .with_schema(my_output_schema)

# When the LLM responds with both text AND tool calls in the same turn,
# the text content is parsed into a Hash, then the tool-call continuation
# sends that Hash where a String is expected.
chat.ask("Extract from this document", with: [pdf_path])
# => RubyLLM::BadRequestError: messages.1.content.0.text.text: Input should be a valid string

Root cause

In chat.rb#complete, the schema JSON parsing runs unconditionally before the tool_call? check:

def complete(&)
  response = @provider.complete(...)

  # This parses content into a Hash even for intermediate tool-call responses
  if @schema && response.content.is_a?(String)
    begin
      response.content = JSON.parse(response.content)
    rescue JSON::ParserError
    end
  end

  add_message response  # Hash is now stored in messages

  if response.tool_call?
    handle_tool_calls(response, &)  # continuation call sends Hash where String expected
  else
    response
  end
end

When handle_tool_calls triggers a continuation call, the stored Hash is formatted by format_text as { type: "text", text: <Hash> }, which Anthropic rejects because text must be a String.

Suggested fix

Only parse schema JSON for the final (non-tool-call) response:

if @schema && response.content.is_a?(String) && !response.tool_call?
  begin
    response.content = JSON.parse(response.content)
  rescue JSON::ParserError
  end
end

Environment

  • ruby_llm version: 1.13.0
  • Ruby version: 3.3.5
  • Provider: Anthropic (Claude claude-haiku-4-5)
  • Using with_schema + with_tools together

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions