Squishling, a smiling ruby gem popping out of a bamboo steamer basket

Squishling ยท a Ruby gem for elastic software

Don't just use AI to write code.
Use AI to not need code.

Squishling lets a class run as ordinary Ruby or hand the work to an LLM, and return the same checked, typed answer either way.
Write Ruby only where it's cheaper than the tokens.

Install View on GitHub

Join the discussion on Hacker News.

On this page

    The idea

    Where does your software sit?

    With Squishling, every Ruby class can land somewhere between "an LLM handles it" and "code handles it."

    Now you have an easy way to define, and adjust, that balance based off of the needs of a class, your velocity, or how much tech debt your team is able to carry.

    Drag the slider to see which tradeoff makes the most sense for the class you are building.

    FlexibleThe model handles it. Higher cost per request.
    EfficientDeterministic code. Cost per request near zero.

    Most codebases today

    Slide left to find the optimal spot

    Traditional software

    Fully deterministic, cheap to scale. Logical complexity and failure states often balloon with features.

    Ruby 100%LLM 0%

    Why

    Tech debt is leverage, if you can price it

    Debt isn't always bad. Used well, it gives you leverage, and tech debt works the same way: it can be an asset or a liability, depending on what you do with it.

    The catch is that nobody can price it. It's hard to quantify the advantage tech debt gives you if you can't quantify what it costs. Here's a common place where it quietly gets expensive.

    The problem

    Some requirements are too expensive to maintain in code

    Say you're in health tech. You sign a clinical client and need their patient data in your system. There's a standard for that, FHIR, so it looks easy.

    Day one

    def call_parser(data)
      fhir_parser.call(data)
    end

    A hundred clients later

    def call_parser(client_name, data)
      case client_name
      when client_a then client_a_parser.call(data)
      when client_b then client_b_parser.call(data)
      when client_c then client_c_parser.call(data)
      when client_d then client_d_parser.call(data)
      when client_e then client_e_parser.call(data)
      when client_f then client_f_parser.call(data)
      when client_g then client_g_parser.call(data)
      when client_h then client_h_parser.call(data)
      # ... ad nauseam

    No matter whether people or AI wrote it, this is terrible architecture:

    The worst kind of debt: uncontrollable, unavoidable and unmanageable... and "better" code can't fix it.

    The alternative

    Same job. Different implementation.

    class Parser
      include Squishling
    
      purpose "Please parse this data into a patient record."
      output_schema PatientRecord
    
      # def call(data)
      #   fhir_parser.call(data)
      # end
    end

    Let the model take the first version, and look at what you get. Flexible by default, and it bends instead of breaking.

    A smiling ruby gem, squished flat and flexible

    Zero brittle code

    Nothing to break when a format shifts.

    Instant integration

    A new client works on day one.

    Flexibility

    It copes with external factors you can't change.

    The objection is cost: every request spends tokens. The answer:

    Now your tech debt has a quantity you can measure.

    How · 1. Describe it

    Define the goal and the shape of the answer

    Make a class squish. Give it a purpose and an output schema, and Squishling does the rest. This class has no implementation at all, so every call is squished.

    A closed bamboo steamer basket with something inside
    Squishling.configure do |config|
      config.default_model = "claude-haiku-4-5-20251001"
      # falls back to RubyLLM's default when nil
    end
    
    class Parser
      include Squishling
    
      purpose "Map the inbound record onto the schema..."
      output_schema do
        string :patient_name
        string :patient_gender, enum: %w[male female trans unknown]
        array  :diagnosis_codes, of: :string
        array  :symptoms, of: :string
      end
    
      # no call method: every invocation is squished.
    end

    Run against 100 synthetic patient records (generated with Synthea):

    73 / 100pass
    $0.48per 100 calls

    Every diagnosis code was right. The misses clustered in three places: names written HL7-style, gender codes, and symptoms the model invented.

    How · 2. Tune it

    Only the purpose changed

    class Parser
      include Squishling
    
      purpose <<~PURPOSE
        Map the record onto the schema below.
    
        Name: output "First Last" order, even if the source writes
        it as HL7's LAST^FIRST, often in all caps.
    
        Gender: translate to "male", "female", "trans", or "unknown".
        "unknown" for any code you don't recognize, including HL7's
        bare O / U / A / N letter codes. Never guess.
    
        Symptoms: only from an explicit symptom field. A diagnosis is
        not a symptom. Never infer one from a diagnosis name.
      PURPOSE
    
      ...
    end
    95 / 100pass
    $0.51per 100 calls

    The two systematic failures that remain are HL7 letter codes and unlabeled SNOMED gender codes that need a translation table, plus one dictation transcript with spoken digits.

    How · 3. Pay for accuracy

    Use a harness to pay for higher accuracy

    Squishsum compares two Squishling results. It asks the model twice, at the same time, and accepts the answer only when both agree. That costs about twice the requests, so reach for it when a confidently wrong answer costs more than a second one.

    Two smiling ruby gems cuddling together
    class Parser
      include Squishling
    
      ...
    
      squishling harness: :squishsum
    end

    When the two results match, you get the first one. When they differ, Squishling raises Squishling::DisagreementError, and squish_fallback decides what happens next. By default the results must match exactly; pass compare: to say which fields have to agree.

    Add a judge with judged_squishsum

    When the two results differ, a judge looks at both and picks one, or rejects both. The judge can be a bigger chat model, as here, or a fast decision model such as Jev.

    A skeptical ruby gem with a question mark floating above it
    class Parser
      include Squishling
    
      ...
    
      squishling model: "claude-haiku-4-5",
        harness: { type: :judged_squishsum, judge: "claude-opus-5-5" }
      # two Haiku samples; Opus judges when they differ
    end

    Run against the same 100 records:

    95 / 100pass
    $1.02per 100 calls

    Same five failures, double the cost, and the judge was never called. At temperature 0.1 the model agreed with itself on every record, including the five it got wrong. Those were systematic misreadings, not noise.

    Asking twice protects you from random mistakes, not from misunderstanding. And validation checks the shape of an answer, not that it's true.

    Or create your own harness

    Squishling doesn't have a plugin point for custom harnesses yet; it's on the roadmap. Today you can build one in plain Ruby, because squish! takes a model for each call. This one asks two models, and calls in a third to break a tie:

    class TicketTriager
      include Squishling
    
      purpose "Assign a priority and a team."
      output_schema do
        string :priority, enum: %w[low medium high]
        string :team
      end
    
      def call(ticket_text:)
        haiku  = squish!(model: "claude-haiku-4-5")
        sonnet = squish!(model: "claude-sonnet-5-5")
    
        # You decide what "agree" means.
        return haiku if haiku.priority == sonnet.priority
    
        # They disagree: ask the strongest model.
        squish!(model: "claude-opus-5-5")
      end
    end

    The built-in options are covered in the harness docs.

    How · 4. Harden the common shapes

    Code the standard shapes. Squish the rest.

    A quick look at the data shows 63% of the requests fit a standard format. So route those to the parser you already trust, and squish the rest:

    A smiling ruby gem with celebratory splashes
    class Parser
      include Squishling
    
      ...
      squishling model: "claude-haiku-4-5",
        harness: { type: :judged_squishsum, judge: "claude-opus-5-5" }
    
      def call(data)
        return fhir_parser.call(data) if fhir_message?(data)
    
        squish!
      end
    end
    95 / 100pass
    $0.12per 100 calls

    Same 95 out of 100, the same five misses, and 76% cheaper than the tuned-purpose run that squishes every request. The router moved this integration from fully flexible to mostly efficient without a rewrite, and without betting up front.

    The four runs, side by side

    RunPassCost per 100 calls
    Bare purpose73 / 100$0.48
    Tuned purpose95 / 100$0.51
    Two samples and a judge95 / 100$1.02
    Code the 63%, squish the rest95 / 100$0.12

    Results from the talk's demo, using 100 synthetic records and Anthropic Claude models. They show the shape of the trade, not a benchmark of your data.

    The result

    Day-one integrations at a fraction of the cost

    And you can keep revisiting this balance as the business evolves.

    It runs backwards, too

    Drowning under a thousand legacy parsers? Find the ones at the tail of your usage curve. Squish them instead. Then delete the code. Stop living in fear that a tiny client finds the breaking change you shipped two quarters ago.

    The philosophy

    Small parts, composed

    The steps above show what a squishling does. These three principles decide where one belongs in your system.

    Squishlings are atomic

    One small job, one clear goal, one output shape, like the Parser.

    Small, fast, explainable

    Start with the cheapest model that reliably does the job, and tune before reaching for more. A tuned purpose on Haiku matched a second sample and a judge at half the cost.

    Architecture over determinism

    Code where code is cheap, squishlings where it isn't, as in code the 63%, squish the rest.

    A reliable system isn't one with no squish in it. It's one where every squish is small, atomic and easy to replace.

    Beyond parsing

    When a service call fails, squish it

    Not parsing health data? Anything that talks to another service has the same shape. Keep your Ruby parser for well-formed payloads, and hand the rest to the model. Save yourself, and the engineers who come after you, uncountable headaches.

    class WebhookHandler
      include Squishling
    
      ...
    
      def call(service:, body:)
        VendorWebhooks.fetch(service).parse(body)
      rescue VendorWebhooks::ParseError => e
        squish!(context: { parse_error: e })
      end
    end
    A smiling ruby gem wearing a stacked red cap

    With squish!, the model gets the call's inputs plus whatever context you pass, such as the error, and still has to return the same validated result. If it can't, squish_fallback decides what happens next.

    Beyond integrations

    Even a bug in your own code can squish

    Any rescued exception can be the trigger, including a plain old bug:

    class SomeHelperMethod
      def call(payload)
        payload.dig("user", "email")
      end
    end
    
    payload = { "user" => "alice@example.com" }
    # oops: the caller sent a string, not a hash
    
    SomeHelperMethod.new.call(payload)
    # => TypeError: String does not have #dig method

    And after:

    class SomeHelperMethod
      include Squishling
    
      purpose "Return the user's email address from this payload."
      output_schema { string :email }
    
      def call(payload)
        payload.dig("user", "email")
      rescue => e
        squish!(context: { error: e })
      end
    end
    
    SomeHelperMethod.new.call(payload).email
    # => "alice@example.com"

    If the answer is in the input or the error message, squish can find and fix it.

    It's a net, not the repair. Still fix the bug, and check the cases where a plausible wrong value would be costly.

    A stack of three ruby gems with an anvil labeled critical on top

    Put a number on it

    Measure your tech debt in tokens

    Code you haven't written is debt you haven't taken on. A squished path has a running cost you can read off a meter, so "should we write a parser for this?" turns into arithmetic: what the path costs in tokens each month, against what the code costs to write and keep.

    Pick a template, then change the numbers. The first one is deliberately a case where writing code is not worth it, and the last two show what changes when squishing earns its keep.

    The model path

    The code path

    What squishing gets you

    $0token cost per month
    $0value squishing returns per month
    $0code upkeep per month
    $0one-time cost to write it

    To get your real numbers, you can read token usage a few ways:

    Coolhand Labs

    Add and initialize the gem, and it picks up your calls automatically, measuring cost and accuracy.

    coolhandlabs.com ↗

    Custom, with squawk

    A hook that logs tokens per method after every LLM attempt. No new dependency.

    OpenTelemetry

    Send traces to Datadog, Langfuse, Arize, Braintrust or any compatible backend.

    opentelemetry.io ↗

    What's next

    Try it, and tell us what breaks

    Add the gem, configure your provider keys in RubyLLM (OpenAI, Anthropic Claude, Google Gemini and others work), and describe your first class.

    # Gemfile
    gem "squishling"
    Squishling, a smiling ruby gem popping out of a bamboo steamer basket

    Requires Ruby 3.3 or newer. The README covers each use case, and the docs cover routing, schemas, configuration, failure handling, harnesses and measuring token spend.

    On the roadmap

    Squish the star button and leave issues on GitHub.

    Star Squishling on GitHub