Squishling
Elastic Ruby classes. A squished method either runs its Ruby implementation or sends its inputs through an LLM (via RubyLLM), and either way returns the same strict-schema-validated, typed result. Callers can't tell the difference.
Inspired by Elastic Software: start flexible with AI, then harden high-volume paths into code as the economics justify it.
Use cases
Instant integration
Accept a new data source today, before anyone writes a parser. Declare what you want back and leave the method unimplemented: every call goes to the LLM, and its output is validated against your schema.
class PaymentWebhook
include Squishling
instructions "Normalize this payment provider's webhook into our payment event."
output_schema do
string :event, enum: %w[succeeded failed refunded disputed]
integer :amount_cents
string :currency
string :external_id
end
# No `def call` yet, so every webhook goes to the LLM.
end
event = PaymentWebhook.call(provider: "adyen", payload: request.raw_post)
event.event # => "refunded"
event.amount_cents # => 4200
event.squished? # => trueWhen one provider carries the volume, write def call for it and add
squish_when { |provider:, **| provider != "stripe" }. Stripe then runs in Ruby, everything else stays on the
LLM, and callers don't change. See Hardening a path.
Error recovery
Keep the Ruby you have for the inputs it understands, and hand the rest to the LLM instead of failing. When the
parser raises, squish! sends this call to the LLM with the error and the parser's own source as context.
class InvoiceParser
include Squishling
instructions "Extract the invoice fields from the vendor's document."
append_instructions "The Ruby parser that handles well-formed invoices:", self # this class's source
output_schema do
string :invoice_number
number :total
end
def call(vendor:, document:)
invoice = VendorFormats.fetch(vendor).parse(document)
result(invoice_number: invoice.number, total: invoice.total)
rescue VendorFormats::ParseError => e
squish!(append_instructions: "The parser failed on this document; the error is in the context.",
context: { parse_error: e })
end
end
InvoiceParser.call(vendor: "acme", document: pdf_text).squished? # => false (Ruby parsed it)
InvoiceParser.call(vendor: "acme", document: scanned_text).squished? # => true (recovered by the LLM)Both calls return the same result class. If the LLM can't deliver either, squish_fallback decides what to
return, or the error is raised with the original ParseError as its cause. See
Escalating from Ruby.
Installation
gem "squishling"Requires Ruby 3.3+ and RubyLLM 2.x. Configure your provider API keys in RubyLLM as usual, then optionally set a universal model:
Squishling.configure do |config|
config.default_model = "claude-sonnet-5-5" # falls back to RubyLLM's default when nil
endHow it works
A squishling class needs instructions (the system prompt) and an output schema (the shape of the result, validated on both paths). A squished call goes to the LLM when:
- its
squish_whenpredicate is truthy for these inputs, - the method has no implementation (it isn't defined, or raises
NotImplementedError), or - the Ruby implementation calls
squish!.
Otherwise the Ruby runs, and whatever it returns is validated and typed like LLM output.
Features
-
One contract, two paths: Ruby returns and LLM output are validated against the same strict schema and
returned as the same typed
Dataobjects.squished?tells you which path served a call. -
Your code as context:
append_instructionsadds sections to the prompt, including a class's or method's own Ruby source. - Any RubyLLM provider and model: OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, OpenRouter, and more. Set a universal, per-class, per-method, or per-call model, plus layered generation params (temperature, reasoning effort, top_p, …).
-
Defined failure behavior: invalid output is re-asked with the validation errors, provider errors become
Squishling::LLMError, andsquish_fallbackdecides what to return when the LLM can't deliver. -
Opt-in context: only method arguments, the instance state you name with
squish_context, thecontext:you pass tosquish!, and the source you choose to append are sent to the provider.
Documentation
- Configuration: options, model and provider resolution, generation params, inheritance
-
Routing: when a call goes to the LLM,
squish!,append_instructions, hardening a path, what the LLM sees - Output schemas: schema forms, strict mode, typed results, optional vs. empty
- Failure handling: retries, error classes, fallbacks
- Live examples: end-to-end tests against Anthropic Claude Haiku and OpenAI GPT-6 Luna
Development
bin/setup
bundle exec rake # RSpec (offline; RubyLLM is stubbed) + RuboCopSee AGENTS.md for repo conventions, and SECURITY.md for reporting vulnerabilities.
License
Apache-2.0