Project

squishling

0.0
The project is in a healthy, maintained state
Squishling makes Ruby classes elastic. Add a squishling to a class and it can send inputs straight to an LLM (OpenAI, Anthropic Claude, Google Gemini, or any provider RubyLLM supports) and return strict-schema-validated, typed results: the same type your Ruby code returns. Use it to gracefully maintain fickle integrations, build interfaces for unknown data formats, and rescue production errors by handing the failing call to the LLM with the source class as context. Write Ruby only for the paths where token costs justify maintaining it. Per-input routing, validated retries, and fallbacks for your LLM calls included.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

Squishling

CI

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?      # => true

When 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
end

How 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_when predicate 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 Data objects. squished? tells you which path served a call.
  • Your code as context: append_instructions adds 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, and squish_fallback decides what to return when the LLM can't deliver.
  • Opt-in context: only method arguments, the instance state you name with squish_context, the context: you pass to squish!, 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) + RuboCop

See AGENTS.md for repo conventions, and SECURITY.md for reporting vulnerabilities.

License

Apache-2.0