Skip to content

Fix Message#to_h to deep convert tool_calls#611

Closed
sevginuroksuz wants to merge 1 commit intocrmne:mainfrom
sevginuroksuz:fix/message-to_h-deep-tool_calls
Closed

Fix Message#to_h to deep convert tool_calls#611
sevginuroksuz wants to merge 1 commit intocrmne:mainfrom
sevginuroksuz:fix/message-to_h-deep-tool_calls

Conversation

@sevginuroksuz
Copy link
Copy Markdown

Closes #610

Summary

This PR improves serialization consistency for RubyLLM::Message.

Changes

  • Message#to_h now deep-converts tool_calls using ToolCall#to_h.
  • Message#initialize now accepts both RubyLLM::ToolCall instances and hashes in the tool_calls array.
  • Hash inputs (with symbol or string keys: id, name, arguments, thought_signature) are automatically converted into ToolCall objects.
  • Invalid types or unexpected keys raise a clear ArgumentError.

Why

Previously:

  • Message#to_h performed shallow serialization.
  • Passing hashes to tool_calls did not normalize them into ToolCall objects.

This ensures:

  • Consistent internal state
  • Predictable serialization
  • Improved compatibility when integrating with external APIs

Tests

  • Added coverage for deep serialization of tool_calls
  • Added coverage for hash input normalization

Comment thread lib/ruby_llm/message.rb
@thinking = options[:thinking]

ensure_valid_role
def normalize_tool_calls(tool_calls)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@sevginuroksuz This seems like a copy/paste issue or an AI hallucination

Comment thread lib/ruby_llm/message.rb
h[:arguments] = h[:arguments].transform_keys(&:to_sym)
end
filtered = h.select { |k, _| allowed_keys.include?(k) }
puts "ToolCall.new args: #{filtered.inspect}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shouldn't have "puts" statements

Comment thread lib/ruby_llm/message.rb
if h[:arguments].is_a?(Hash)
h[:arguments] = h[:arguments].transform_keys(&:to_sym)
end
filtered = h.select { |k, _| allowed_keys.include?(k) }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can simplify this to: filtered = h.slice(*allowed_keys)

https://ruby-doc.org/core-3.1.0/Hash.html#method-i-slice

signature: extract_thought_signature(parts)
),
tool_calls: tool_calls,
tool_calls: tool_calls&.map { |tc| tc.respond_to?(:to_h) ? tc.to_h : tc },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why was this added? tool_calls comes from a extract_tool_calls which provides ToolCall records, and deals with building Message records, not converting to hashes. This seems like an unneeded change.

@crmne
Copy link
Copy Markdown
Owner

crmne commented Feb 27, 2026

Thanks for the contribution. I’m closing this PR for now because it introduces regressions and also does not follow the repository process requirements.

Process issues:

  1. The PR description does not follow .github/pull_request_template.md (required sections/checklists are missing).
  2. Contributing checklist assertions are missing from the PR body (for example, scope/process and quality checks expected by CONTRIBUTING.md + template).

Technical blockers:

  1. tool_calls contract is broken in Message (Hash expected across the codebase, but this change maps it into arrays in key paths), causing runtime failures where each_value is called.
  2. normalize_tool_calls is defined both inside initialize and in private, which creates conflicting/order-dependent behavior.
  3. Debug output (puts) was added in library code.
  4. Gemini chat path also transforms tool_calls shape in a way that conflicts with existing expectations.

I validated this locally with targeted specs, including failures in:

  • spec/ruby_llm/chat_tools_spec.rb:583
  • spec/ruby_llm/providers/gemini/chat_spec.rb:454

If you want to reopen with a follow-up PR:

  • keep tool_calls internal shape consistent (Hash{id => ToolCall}),
  • keep a single normalize_tool_calls implementation,
  • remove debug logging,
  • follow the PR template fully and complete the contributing checklist items.

@crmne crmne closed this Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] chat.messages.map(&:to_h) doesn't convert ToolCall values

3 participants