RubyLLM 1.15: Image Editing + Cost Tracking + Less Glue Code πΌοΈπΈπ οΈ
RubyLLM 1.15 removes glue code around images, costs, tools, callbacks, and Rails persistence.
If Ruby can infer a tool signature, RubyLLM now infers it. If a provider reports usage, RubyLLM can turn it into cost. If Rails already has a blob, RubyLLM reuses it instead of downloading and uploading it again.
πΌοΈ Image Editing
Same method, same attachment shape: paint now paints from scratch or edits an existing image.
RubyLLM.paint can edit existing images with OpenAI's GPT Image models. Pass one or more source images with with:, add a mask: when you want to constrain the editable area, and use params: for provider-specific image options.
image = RubyLLM.paint(
"Turn the logo green and keep the background transparent",
model: "gpt-image-1",
with: "logo.png"
)with: accepts the same attachment sources RubyLLM supports elsewhere: local files, URLs, IO-like objects, and Active Storage attachments. Multiple source images work too:
image = RubyLLM.paint(
"Combine these references into a postcard illustration",
model: "gpt-image-1",
with: ["person.png", "style-reference.png"]
)Image responses now expose provider usage data, and GPT Image pricing is represented in the model registry so image input/output costs can be calculated with the same API shape used by chats and messages:
image.tokens.input
image.tokens.output
image.cost.input
image.cost.output
image.cost.totalπΈ Conversation Costs + Normalized Tokens
Token counts answer "how many?" Cost helpers answer the next question: "how much?"
RubyLLM now has first-class cost helpers for token-priced conversation usage:
response = chat.ask("Summarize Ruby's object model.")
response.cost.total
chat.cost.total
agent.cost.totalA response can tell you its cost. A chat can tell you the running total. An agent can too. Images use the same shape.
Under the hood, RubyLLM::Cost uses normalized token buckets plus pricing from the model registry: standard input, billable output, cache reads, cache writes, and separately priced thinking/reasoning tokens when the model exposes a distinct reasoning-token price.
Prompt caching made token counts messy, so 1.15 separates the buckets before exposing them:
response.tokens.input # Standard input tokens
response.tokens.output # Billable output tokens
response.tokens.cache_read # Prompt cache reads
response.tokens.cache_write # Prompt cache writesThe top-level token helpers still work for backwards compatibility, but new code should prefer response.tokens.*.
For Rails users, persisted messages and chats expose the same helpers. No new migration is required if you already ran the v1.9 token migration; the new names use the existing cached_tokens and cache_creation_tokens columns.
π οΈ Simpler Tool Definitions
Simple tools no longer need duplicated parameter declarations. If the execute signature already says what arguments exist, RubyLLM can infer the flat schema:
class Weather < RubyLLM::Tool
desc "Gets current weather for a location"
def execute(latitude:, longitude:, units: "metric")
# ...
end
endRequired keywords become required string parameters. Optional keywords become optional string parameters. Explicit param declarations and the full params DSL still win when you need descriptions, non-string types, nested objects, arrays, enums, or full JSON Schema control.
There are also small ergonomics improvements:
descis now an alias fordescriptionparamacceptsdescription:as an alias fordesc:- the tool generator now emits
desc - tools with no keyword arguments now get an empty object schema
π Additive Chat Callbacks
Callbacks now stack instead of replacing each other. Register five callbacks for the same event and all five run:
chat.before_message { ... }
chat.after_message { |message| ... }
chat.before_tool_call { |tool_call| ... }
chat.after_tool_result { |result| ... }Unlike the legacy on_* callbacks, multiple before_* / after_* callbacks can be registered for the same event and they all run. The old callbacks still work, but they now log deprecation warnings and keep their existing replacing behavior. They will be removed in RubyLLM 2.0.
Rails persistence now uses the additive callbacks internally, so application callbacks can be layered on top without disturbing message persistence.
π Rails Fixes
Rails got the boring, important fixes: fewer load-order surprises, better persistence behavior, and less duplicate file handling.
Action Text Content
Messages backed by has_rich_text :content now use to_plain_text before being sent to the model. This prevents Action Text HTML from leaking into LLM messages and still works when the message has attachments.
ActiveRecord Eager Loading
The optional ActiveRecord integration is no longer part of the core gem eager-load path. The Railtie now explicitly loads the ActiveRecord support files only after ActiveRecord loads, which fixes standalone require "ruby_llm" + Zeitwerk eager-loading failures while preserving normal Rails behavior. CI now includes an eager-load guard across Rails appraisals.
Rails Association Conventions
The new acts_as API now follows Rails association inference more closely. Association names determine default foreign keys, while *_class options only change class names. The install generator now emits explicit foreign key options only when Rails would not infer the intended key.
Active Storage Persistence
Existing ActiveStorage::Blob, ActiveStorage::Attachment, has_one_attached, and has_many_attached records are reused directly instead of being downloaded and re-uploaded when passed through with:. The Rails docs now clarify that RubyLLM message records need an :attachments association, but your own app models can use any Active Storage attachment name.
π Provider Fixes
Provider cleanup in this release is mostly about making edge cases boring.
Empty tool results are now handled consistently across Anthropic, Bedrock, and Gemini. When a tool returns no content, RubyLLM sends a small (no output) placeholder instead of provider-invalid empty content.
Streaming and non-streaming token usage is also normalized across OpenAI, OpenRouter, Bedrock, and Gemini so cache reads/writes are separated from standard input tokens before cost calculations.
π Docs + SEO
The docs have been updated for image editing, tool signature inference, additive callbacks, normalized token semantics, cost helpers, Active Storage attachment names, Rails association conventions, and using an existing Chat record with an Agent.
The docs site also gained richer SEO and AI-visible metadata: JSON-LD injection for collection pages, llms.txt support through jekyll-ai-visible-content, an About page, cleaner collection dates, and a gemspec changelog link that now points to GitHub Releases.
RubyLLM::Tribunal has been added to the ecosystem page for LLM evaluation and testing in Ruby.
π¦ Updated Model Registry
The model registry has been refreshed with the latest available models and pricing metadata. This update adds cache read/write pricing fields, reasoning output pricing, image pricing for GPT Image models, and new aliases including Claude Opus 4.7, DeepSeek V4 Flash/Pro, Gemini Embedding 2, Gemma 4, and GPT-5.5.
Installation
gem "ruby_llm", "1.15"Upgrading from 1.14.x
bundle update ruby_llmIf you display or store token counts directly, read the 1.15 upgrade guide section. tokens.input now means standard input tokens; add tokens.cache_read and tokens.cache_write when you need total request-side input activity.
Merged PRs
- Add RubyLLM::Tribunal to ecosystem page by @Florian95 in #571
- Remove Code Duplication by @Aesthetikx in #732
- Feat: Add support to Action Text enabled content by @chagel in #365
- Fix ActiveRecord dependency check for Zeitwerk eager loading by @trevorturk in #504
- Document usage of existing Chat record with Agent by @sarrietav-dev in #693
New Contributors
- @Florian95 made their first contribution in #571
- @Aesthetikx made their first contribution in #732
- @chagel made their first contribution in #365
- @sarrietav-dev made their first contribution in #693
Full Changelog: 1.14.1...1.15.0