Module: Rubino::LLM::AnthropicRoleMerge
- Defined in:
- lib/rubino/llm/anthropic_role_merge.rb
Overview
Enforce strict user/assistant alternation on the Anthropic-family wire.
ruby_llm renders each conversation message to ONE wire message 1:1 and never coalesces (providers/anthropic/chat.rb ‘chat_messages.map`). An Anthropic-family endpoint requires the messages to alternate; a tool result is a `user` message on the wire (a `tool_result` block), so a tool result followed by ANY other user/tool message — a queued human input, an injected notice, a second tool result with no assistant turn between —serialises as TWO consecutive `user` messages, which the provider rejects with a request-validation 400 (“invalid params”), killing the turn.
Mirror the reference agent’s ‘_merge_consecutive_roles`: after ruby_llm builds the payload, merge consecutive same-role wire messages by concatenating their content blocks. (rubino’s port had dropped this whole Anthropic-normalisation layer — the tool-id sanitiser was its sibling.) The merge only ever fires on an already-invalid consecutive pair, so it can never alter a well-formed alternating sequence.
Constant Summary collapse
- THINKING_TYPES =
%w[thinking redacted_thinking].freeze
Class Method Summary collapse
-
.as_blocks(content) ⇒ Object
Normalise a message’s content to an array of block hashes so two can be concatenated regardless of whether either was a plain string.
- .combine_content(left, right) ⇒ Object
- .drop_thinking(content) ⇒ Object
-
.merge_consecutive(messages) ⇒ Object
Coalesce consecutive same-role messages, concatenating content.
Instance Method Summary collapse
-
#render_payload(*args, **kwargs) ⇒ Object
Prepended over RubyLLM::Providers::Anthropic#render_payload.
Class Method Details
.as_blocks(content) ⇒ Object
Normalise a message’s content to an array of block hashes so two can be concatenated regardless of whether either was a plain string.
59 60 61 62 63 64 |
# File 'lib/rubino/llm/anthropic_role_merge.rb', line 59 def self.as_blocks(content) return [] if content.nil? return [{ type: "text", text: content }] if content.is_a?(String) Array(content) end |
.combine_content(left, right) ⇒ Object
53 54 55 |
# File 'lib/rubino/llm/anthropic_role_merge.rb', line 53 def self.combine_content(left, right) as_blocks(left) + as_blocks(right) end |
.drop_thinking(content) ⇒ Object
66 67 68 69 70 |
# File 'lib/rubino/llm/anthropic_role_merge.rb', line 66 def self.drop_thinking(content) return content unless content.is_a?(Array) content.reject { |b| b.is_a?(Hash) && THINKING_TYPES.include?(b[:type].to_s) } end |
.merge_consecutive(messages) ⇒ Object
Coalesce consecutive same-role messages, concatenating content. On a merged-in ASSISTANT message thinking blocks are dropped: their signature was computed against a different turn boundary and is invalid once merged (mirrors the reference agent).
40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/rubino/llm/anthropic_role_merge.rb', line 40 def self.merge_consecutive() .each_with_object([]) do |msg, acc| prev = acc.last if prev && prev[:role] && prev[:role] == msg[:role] incoming = msg[:content] incoming = drop_thinking(incoming) if msg[:role].to_s == "assistant" prev[:content] = combine_content(prev[:content], incoming) else acc << msg.dup end end end |
Instance Method Details
#render_payload(*args, **kwargs) ⇒ Object
Prepended over RubyLLM::Providers::Anthropic#render_payload.
28 29 30 31 32 33 34 |
# File 'lib/rubino/llm/anthropic_role_merge.rb', line 28 def render_payload(*args, **kwargs) payload = super if payload.is_a?(Hash) && payload[:messages].is_a?(Array) payload[:messages] = AnthropicRoleMerge.merge_consecutive(payload[:messages]) end payload end |