# Anthropic Ruby SDK vs ruby_llm: Claude Gem Trade-offs

> Source: <https://nsinenko.com/rails/ai-agents/2026/07/03/anthropic-ruby-sdk-vs-ruby-llm/>
> Published: 2026-07-03 08:40:00+00:00

# Anthropic Ruby SDK vs ruby_llm: Claude Gem Trade-offs

Compare the official Anthropic Ruby SDK with ruby_llm for Claude in Rails, scoped to provider-specific features, Rails persistence, model registry behavior, and where each gem becomes awkward.

Ruby has two serious gems for talking to Claude: the official `anthropic`

SDK and [ruby_llm](https://rubyllm.com/), the community multi-provider framework. I would not choose between them on popularity. The question is which constraint is real for your app. Reach for the official `anthropic`

gem when it talks only to Claude and you want Claude-specific API features early. Reach for `ruby_llm`

when you need several providers behind one interface, or when its Rails persistence layer saves you real work.

Scope matters here because both projects move quickly. This comparison is based on ruby_llm 1.16.0 and the official `anthropic`

gem 1.55.0, with ruby_llm's Anthropic provider code read alongside the official SDK surface. Treat the table as a versioned decision note, not a permanent feature matrix.

Comparison scope: ruby_llm 1.16.0 and the official anthropic gem 1.55.0.

Official `anthropic` gem |
`ruby_llm` |
|
|---|---|---|
| Maintainer | Anthropic | Community (Carmine Paolino) |
| Providers | Claude only | Anthropic, OpenAI, Gemini, Bedrock, Mistral, Ollama, and 7 more |
| Streaming, tools, thinking | Yes, fully typed | Yes |
| Structured output | `output_config` with a JSON schema |
`with_schema` , compiled to `output_config` |
| Prompt caching | First-class | Via a provider-specific content block |
| Batches and Files APIs | Yes | No |
| Server-side tools (web search, code execution) | Yes | No |
| MCP connector, memory tool, Agent Skills | Yes | No |
| Rails persistence | You build it | `acts_as_chat` plus generators and a chat UI |
| New model names | Any string, passed through | Registry-validated; new models need a refresh |

A "Yes" here means first-class, documented support in the gem. A "No" is not always a hard wall, but mind which escape hatch actually applies: `RubyLLM::Content::Raw`

only forwards raw *message content* (that is what makes prompt caching reachable), while request-level features live in top-level fields and headers, which you reach through `with_params`

and `with_headers`

. Some of these gaps can be closed that way; for the MCP connector or server-side tools I would verify the exact request shape or just use the official gem. Either route means giving up the abstraction and writing Claude-specific code, which is the trade-off this post is about.

## How I Would Re-check This Before Choosing

Before treating this comparison as current, run these checks in the app that will ship the integration:

```
bundle info ruby_llm
bundle info anthropic
bundle exec ruby -e 'require "ruby_llm"; RubyLLM.models.refresh!; puts RubyLLM.models.find { |m| m.id == "claude-sonnet-5" }'
rg "mcp|web_search|web_fetch|code_execution|message_batches|files" "$(bundle show ruby_llm)/lib/ruby_llm/providers/anthropic"
```

The first two commands tell you which gem versions you are actually using. The model refresh checks whether a newly launched Claude model is in ruby_llm's registry. The `rg`

check tells you whether ruby_llm's Anthropic provider has first-class support for the Claude-specific features this post calls out, or whether you are about to rely on raw params, beta headers, or a second client.

## Two Different Ideas of What a Client Is

The official gem is a code-generated, typed wrapper around one API. You build a client, and every request and response is a typed object:

```
# config/initializers/anthropic.rb
ANTHROPIC = Anthropic::Client.new(api_key: ENV.fetch("ANTHROPIC_API_KEY"))

message = ANTHROPIC.messages.create(
  model: "claude-sonnet-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Summarize this order history." }]
)

message.content.first.text
```

The typing is not cosmetic. Response enums come back as Ruby Symbols (`message.stop_reason == :tool_use`

, not `"tool_use"`

), content blocks are typed classes, and tools are `Anthropic::BaseTool`

subclasses with validated input schemas. When the API grows a feature, the generated SDK is the place I would check first, because the wrapper is maintained against one provider instead of a shared abstraction. Check the changelog before you depend on a specific surface.

ruby_llm starts from the opposite premise: providers are interchangeable backends, and your app code should not care which one is behind the conversation. The whole surface hangs off one chat object:

```
RubyLLM.configure do |config|
  config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
end

chat = RubyLLM.chat(model: "claude-sonnet-4-6")
chat.ask "What changed in this order history?"
```

Swapping Claude for Gemini or GPT is a one-line model change. Tools are plain classes with an `execute`

method, and the tool loop runs for you inside `ask`

:

```
class LookupOrder < RubyLLM::Tool
  desc "Look up a single order by its ID"
  param :order_id, desc: "The order ID to look up"

  def execute(order_id:)
    Order.where(id: order_id).as_json(only: %i[id status total_cents])
  end
end

chat.with_tool(LookupOrder).ask "What's the status of order 5591?"
```

Streaming is a block argument (`chat.ask("...") { |chunk| print chunk.content }`

), and the same object model fronts embeddings, image generation, and transcription through other providers, none of which Anthropic sells.

## How Much of the Claude API ruby_llm Covers

ruby_llm's Anthropic provider covers more than a lowest-common-denominator layer of chat, streaming, and tools. Extended thinking, structured output, PDFs, images, and prompt caching all pass through.

ruby_llm 1.16.0's Anthropic provider handles extended thinking through `with_thinking(effort: :high)`

, structured output through `with_schema`

(compiled down to the API's native `output_config`

, not simulated with a tool call), and PDFs and images as attachments. Prompt caching works too, through an Anthropic-specific content builder:

```
system_block = RubyLLM::Providers::Anthropic::Content.new(
  "You are a release-notes assistant...",
  cache: true # shorthand for cache_control: { type: "ephemeral" }
)
chat.add_message(role: :system, content: system_block)
```

That builder is a thin wrapper over `RubyLLM::Content::Raw`

, the generic escape hatch for handing a provider a payload it forwards verbatim, so you can also assemble the `cache_control`

blocks by hand when you need finer control (a longer TTL, caching a user turn rather than the system prompt). Either way the namespace is the catch: `Providers::Anthropic::Content`

is Claude-specific, so the moment you reach for Claude's cost-saving features you are writing Anthropic-specific code inside the portability layer. The abstraction leaks exactly where Claude gets interesting, and the code you wrote for portability quietly stops being portable.

## Where ruby_llm Stops Covering Claude

The gaps are specific and easy to verify by grepping ruby_llm's Anthropic provider directory (`lib/ruby_llm/providers/anthropic/`

). Treat them as current verification targets, not timeless facts. As of ruby_llm 1.16.0, I did not find first-class support for Anthropic's server-side tools (web search, web fetch, code execution), the MCP connector, the memory tool, Agent Skills, or clients for the Message Batches and Files APIs. If your roadmap includes "the agent searches the web" or "the agent talks to our MCP server", that is the point where I would stop treating ruby_llm as the whole client. Either use the official gem for that part of the app or verify the raw request path yourself.

The escape hatches do not erase that trade-off. With ruby_llm you can push raw request fields through `with_params`

and beta headers through `with_headers`

, but then you are hand-assembling Claude-specific request shapes inside the abstraction. `Content::Raw`

does not help for top-level request fields such as `mcp_servers`

, tool declarations, or headers; it forwards message content. The Batches and Files APIs are separate endpoints, so neither `with_params`

nor `Content::Raw`

turns them into ruby_llm features.

There is also a subtler operational difference. ruby_llm validates model names against a shipped registry, and the registry lags. Concretely, as I write this, ruby_llm 1.16.0 ships a registry that knows `claude-opus-4-8`

but not `claude-sonnet-5`

, so `RubyLLM.chat(model: "claude-sonnet-5")`

raises `ModelNotFoundError`

until you run `RubyLLM.models.refresh!`

or pass `assume_model_exists: true`

with an explicit provider. The official gem sends whatever model string you give it and lets the API be the judge. On a model launch day, that is the difference between changing one string and reading a troubleshooting page.

The broader pattern is architectural rather than emotional: a provider SDK can expose provider-specific features directly, while a multi-provider layer has to decide whether the feature belongs in its shared model. If being early to new Claude capabilities matters to your product, that abstraction delay is a risk you should budget for.

## The Rails Story Favors ruby_llm

Where ruby_llm pulls ahead is everything around the API call. ruby_llm ships Rails generators that create migrations, models, and an `acts_as_chat`

concern, so chats and messages persist to your database with streaming wired through, and there is an optional generated chat UI. With the official gem you assemble that yourself: an initializer for the client, your own `Chat`

and `Message`

models, a background job for the agent loop, and Turbo Streams for the incremental output. None of it is difficult, and you keep full control, but it is an afternoon of plumbing that ruby_llm gives you in a generator.

If you go the official-gem route in Rails, the assembly is exactly what I walk through in the [agent-building guide](/rails/ai-agents/architecture/2026/06/09/building-ai-agents-ruby-anthropic-sdk/), with [Solid Queue handling the background execution](/rails/ai-agents/background-jobs/2026/06/15/solid-queue-ai-agents-background-jobs/).

## When Each Gem Is the Wrong Choice

The official gem is the wrong choice when your product genuinely runs, or will plausibly run, more than one model provider. Maintaining two provider clients with two different object models in one codebase is exactly the fragmentation ruby_llm was built to remove, and its Claude support is complete enough that most chat-and-tools apps will never notice a difference.

ruby_llm is the wrong choice when the app is Claude-only and leans on Claude-specific capabilities: server-side tools, the MCP connector, Agent Skills, or day-one access to new API features. It is also the wrong choice if typed responses matter to how you test and reason about the boundary; ruby_llm's friendlier surface trades away the generated types that make the official SDK's responses self-documenting.

One thing that is not a trade-off: you can run both. They are independent gems with separate namespaces and configuration, so a Gemfile can carry ruby_llm for the multi-provider chat features and the official gem for a Claude-specific corner of the app. That is not an architecture I would design toward on day one, but it beats forcing either gem into a job it is wrong for.

## So Which Gem Goes in the Gemfile?

For a Claude-only Rails app, my default remains the official `anthropic`

gem: typed responses, no provider abstraction to debug, and no local model registry between the app and a new Claude model name. Setup and the core calls are covered in my [Anthropic Ruby SDK reference](/anthropic-ruby-sdk/). For a product where provider flexibility is a real requirement rather than a hypothetical, ruby_llm is the better starting point: the Rails persistence and shared chat API can save more work than the Claude-specific gaps cost.

Before I would put either gem into an app, I would write down the constraint: Claude-only feature depth, or provider portability. If the answer is not obvious, build one real tool call, one streamed response, and one persisted conversation in both libraries. That small spike will tell you more than another feature table.

The gem choice comes down to one list: the Claude-specific features you actually call, not the ones you might. Give me that list and whether a second provider is a real requirement, and the answer is usually clear before it becomes costly to reverse.

### Further Reading

[Anthropic Ruby SDK: Install, Client, Streaming, Tools](/anthropic-ruby-sdk/)- the official gem reference this comparison builds on[Rails AI Agents with the Anthropic SDK](/rails/ai-agents/architecture/2026/06/09/building-ai-agents-ruby-anthropic-sdk/)- the full agent build on the official gem[Claude Agent SDK in Ruby: Your 3 Real Options](/claude-agent-sdk-ruby/)- what to do about the agent-framework layer Ruby does not officially have[ruby_llm documentation](https://rubyllm.com/)- the official docs for chat, tools, thinking, and the Rails integration[anthropic-sdk-ruby on GitHub](https://github.com/anthropics/anthropic-sdk-ruby)- the official gem's README and examples
