Skip to content

Spurious "single system message" warning when multiple :system messages are passed to the Anthropic provider #767

Description

@timherby

Version: ruby_llm 1.15.0

Description

RubyLLM::Providers::Anthropic::Chat#build_system_content logs a warning whenever a chat has more than one :system-role message:

# lib/ruby_llm/providers/anthropic/chat.rb:34-39
if system_messages.length > 1
  RubyLLM.logger.warn(
    "Anthropic's Claude implementation only supports a single system message. " \
    'Multiple system messages will be combined into one.'
  )
end

The warning is misleading: Anthropic's system parameter accepts an array of text content blocks (each optionally with cache_control), and the gem already correctly serializes multiple system messages into that array via flat_map:

system_messages.flat_map do |msg|
  content = msg.content
  if content.is_a?(RubyLLM::Content::Raw)
    content.value
  else
    Media.format_content(content)
  end
end

So passing two :system messages via chat.with_instructions(a).with_instructions(b, append: true) produces a valid two-element system: [...] payload, with each block's cache_control preserved. The warning's claim that the gem is "combining them into one" is no longer accurate (and Anthropic's API has accepted multi-block system content for years).

Impact

Applications that intentionally split system content into separate blocks — e.g., a static prompt + a per-conversation context envelope — get spurious log spew on every request. In high-throughput agent settings, this floods logs.

Reproduction

chat = RubyLLM.chat(model: 'claude-sonnet-4-6')
chat.with_instructions("Static prompt.")
chat.with_instructions("Per-session context.", append: true)
chat.ask("Hello")
# Logs: "Anthropic's Claude implementation only supports a single system message. ..."
# Actual API payload:
#   system: [
#     { type: "text", text: "Static prompt." },
#     { type: "text", text: "Per-session context." }
#   ]
# — correctly multi-block.

Suggested fix

Remove the length > 1 warning block. Optional: replace it with a brief comment documenting that multi-block system content is supported and is forwarded faithfully to Anthropic's system array.

If there's appetite, also add a test asserting that given two :system messages the rendered Anthropic payload contains two distinct text blocks (and per-block cache_control is preserved through the round-trip).

References

Happy to PR the fix.

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