{"slug": "squishling-0-5-0-hybrid-llm-ruby-code-classes", "title": "Squishling 0.5.0 – hybrid LLM/Ruby code classes", "summary": "Squishling 0.5.0, a Ruby library, lets a class run either as ordinary deterministic Ruby or hand its work to an LLM while returning the same checked, typed answer through a declared output schema. In the release's example, a Parser class with no implementation, configured to use Anthropic's claude-haiku-4-5-20251001, passed 73 of 100 synthetic patient records generated with Synthea. The library frames the tradeoff as a slider between flexible LLM handling at higher cost per request and near-zero-cost deterministic code, so teams can refactor high-volume paths to Ruby and price the remaining tech debt.", "body_md": "Don't just use AI to write code. Use AI to not need code.\n\nSquishling lets a class run as ordinary Ruby or hand the work to an LLM, and return the same checked,\ntyped answer either way. Write Ruby only where it's cheaper than the tokens.\n\nWith Squishling, every Ruby class can land somewhere between \"an LLM handles it\" and \"code handles it.\"\n\nNow you have an easy way to define, and adjust, that balance based off of the needs of a class, your velocity, or\nhow much tech debt your team is able to carry.\n\nDrag the slider to see which tradeoff makes the most sense for the class you are building.\n\nFlexibleThe model handles it. Higher cost per request.\n\nEfficientDeterministic code. Cost per request near zero.\n\nMost codebases today\n\n← Slide left to find the optimal spot\n\nTraditional software\n\nFully deterministic, cheap to scale. Logical complexity and failure states often balloon with features.\n\nRuby 100%LLM 0%\n\nWhy\n\nTech debt is leverage, if you can price it\n\nDebt isn't always bad. Used well, it gives you leverage, and tech debt works the same way: it can be an asset or a\nliability, depending on what you do with it.\n\nThe catch is that nobody can price it. It's hard to quantify the advantage tech debt gives you if you can't\nquantify what it costs. Here's a common place where it quietly gets expensive.\n\nThe problem\n\nSome requirements are too expensive to maintain in code\n\nSay you're in health tech. You sign a clinical client and need their patient data in your system. There's a\nstandard for that, FHIR, so it looks easy.\n\nDay one\n\n``` python\ndef call_parser(data)\n  fhir_parser.call(data)\nend\n```\n\nA hundred clients later\n\n``` python\ndef call_parser(client_name, data)\n  case client_name\n  when client_a then client_a_parser.call(data)\n  when client_b then client_b_parser.call(data)\n  when client_c then client_c_parser.call(data)\n  when client_d then client_d_parser.call(data)\n  when client_e then client_e_parser.call(data)\n  when client_f then client_f_parser.call(data)\n  when client_g then client_g_parser.call(data)\n  when client_h then client_h_parser.call(data)\n  # ... ad nauseam\n```\n\nNo matter whether people or AI wrote it, this is terrible architecture:\n\nIt's hard to DRY.\n\nAny change to a data format can break it, and every breakage is a scramble.\n\nIt adds cost to every new contract. The more clients you sell, the worse it gets.\n\nThe worst kind of debt: uncontrollable, unavoidable and unmanageable... and \"better\" code can't fix it.\n\nThe alternative\n\nSame job. Different implementation.\n\n```\nclass Parser\n  include Squishling\n\n  purpose \"Please parse this data into a patient record.\"\n  output_schema PatientRecord\n\n  # def call(data)\n  #   fhir_parser.call(data)\n  # end\nend\n```\n\nLet the model take the first version, and look at what you get. Flexible by default, and it bends instead of\nbreaking.\n\nZero brittle code\n\nNothing to break when a format shifts.\n\nInstant integration\n\nA new client works on day one.\n\nFlexibility\n\nIt copes with external factors you can't change.\n\nThe objection is cost: every request spends tokens. The answer:\n\nRefactor the high-volume bits to be deterministic.\n\nKeep AI for everything else.\n\nNow your tech debt has a quantity you can measure.\n\nHow · 1. Describe it\n\nDefine the goal and the shape of the answer\n\nMake a class squish. Give it a purpose and an output schema, and Squishling does the rest. This class has no\nimplementation at all, so every call is squished.\n\n```\nSquishling.configure do |config|\n  config.default_model = \"claude-haiku-4-5-20251001\"\n  # falls back to the adapter's default when nil\nend\n\nclass Parser\n  include Squishling\n\n  purpose \"Map the inbound record onto the schema...\"\n  output_schema do\n    string :patient_name\n    string :patient_gender, enum: %w[male female trans unknown]\n    array  :diagnosis_codes, of: :string\n    array  :symptoms, of: :string\n  end\n\n  # no call method: every invocation is squished.\nend\n```\n\nRun against 100 synthetic patient records (generated with Synthea):\n\n73 / 100pass\n\n$0.48per 100 calls\n\nEvery diagnosis code was right. The misses clustered in three places: names written HL7-style, gender codes, and\nsymptoms the model invented.\n\nHow · 2. Tune it\n\nOnly the purpose changed\n\n```\nclass Parser\n  include Squishling\n\n  purpose <<~PURPOSE\n    Map the record onto the schema below.\n\n    Name: output \"First Last\" order, even if the source writes\n    it as HL7's LAST^FIRST, often in all caps.\n\n    Gender: translate to \"male\", \"female\", \"trans\", or \"unknown\".\n    \"unknown\" for any code you don't recognize, including HL7's\n    bare O / U / A / N letter codes. Never guess.\n\n    Symptoms: only from an explicit symptom field. A diagnosis is\n    not a symptom. Never infer one from a diagnosis name.\n  PURPOSE\n\n  ...\nend\n```\n\n95 / 100pass\n\n$0.51per 100 calls\n\nThe two systematic failures that remain are HL7 letter codes and unlabeled SNOMED gender codes that need a\ntranslation table, plus one dictation transcript with spoken digits.\n\nBest practice: describe each field in the schema\n\nThis run kept the per-field rules in the purpose. A better home for them is a description: on\nthe field itself, as in this equivalent shape of the same class. The description travels with the field in the\nschema the model fills in, so the guidance sits right where the value goes, and the purpose stays short and about\nthe goal. It also lives next to the type and enum it explains, so the two don't drift apart.\n\n```\npurpose \"Map the record onto the schema.\"\n\noutput_schema do\n  string :patient_name,\n         description: \"First Last order, even if the source writes it as HL7's LAST^FIRST, often in all caps.\"\n  string :patient_gender, enum: %w[male female trans unknown],\n         description: \"\\\"unknown\\\" for any code you don't recognize, including HL7's bare O / U / A / N. Never guess.\"\n  array  :symptoms, of: :string,\n         description: \"Only from an explicit symptom field. A diagnosis is not a symptom.\"\nend\n```\n\nThe 95 / 100 above was measured with the rules in the purpose; this description version wasn't benchmarked.\nDescriptions are guidance to the model, not enforcement: for rules that must hold, use an enum or squish_validate.\n\nHow · 3. Pay for accuracy\n\nUse a harness to pay for higher accuracy\n\nSquishsum compares two Squishling results. It asks the model twice, at the same time, and accepts\nthe answer only when both agree. That costs about twice the requests, so reach for it when a confidently wrong\nanswer costs more than a second one.\n\n```\nclass Parser\n  include Squishling\n\n  ...\n\n  squishling harness: :squishsum\nend\n```\n\nWhen the two results match, you get the first one. When they differ, Squishling raises\nSquishling::DisagreementError, and squish_fallback decides what happens next. By default the\nresults must match exactly; pass compare: to say which fields have to agree.\n\nAdd a judge with judged_squishsum\n\nWhen the two results differ, a judge looks at both and picks one, or rejects both. The judge can be a bigger chat\nmodel, as here, or a fast decision model such as Jev.\n\n```\nclass Parser\n  include Squishling\n\n  ...\n\n  squishling model: \"claude-haiku-4-5\",\n    harness: { type: :judged_squishsum, judge: \"claude-opus-5-5\" }\n  # two Haiku samples; Opus judges when they differ\nend\n```\n\nRun against the same 100 records:\n\n99 / 100pass\n\n$1.02per 100 calls\n\nFour of the five misses are now right, for double the cost of the tuned purpose. The one that remains is a\nreminder that a second opinion helps most with random mistakes, less with systematic misreadings.\n\nValidation checks the shape of an answer, not that it's true.\n\nOr create your own harness\n\nRegister a harness under a name and use it like a built-in. It decides how to use the escalation, and gets\nsamples, judges, squawk reports and squish_fallback for free. This one asks two models, and\ncalls in a third to break a tie:\n\n```\nclass Tiebreaker < Squishling::Harness::Base\n  option :tiebreaker, :step, required: true\n\n  def call(run, tiebreaker:)\n    # Ask the first two models at the same time.\n    first, second = run.samples(run.stages.first(2))\n\n    # You decide what \"agree\" means.\n    return first if run.agree?(first, second, ->(a, b, **) { a.priority == b.priority })\n\n    # They disagree: ask the strongest model.\n    run.escalate(tiebreaker)\n  end\nend\n\nSquishling.register_harness(:tiebreaker, Tiebreaker)\n\nclass TicketTriager\n  include Squishling\n\n  squishling escalation: [\n      { model: \"claude-haiku-4-5\", priority: 10 },\n      { model: \"gemma-4b\", priority: 10 }\n    ],\n    harness: { type: :tiebreaker, tiebreaker: \"claude-opus-5-5\" }\n  purpose \"Assign a priority and a team.\"\n  output_schema do\n    string :priority, enum: %w[low medium high]\n    string :team\n  end\nend\n```\n\nEvery primitive returns the same validated, typed result, so you can compare fields directly.\n\nOptions are checked when you declare the harness, before any request is made.\n\nUse squish_validate for rules the schema can't express. A failed check rejects that sample or attempt.\n\nA harness that raises DisagreementError or InvalidOutputError goes to the method's\nsquish_fallback, like the built-ins.\n\nThe built-in options, and the rest of the plugin point, are covered in the\nharness docs.\n\nHow · 4. Harden the common shapes\n\nCode the standard shapes. Squish the rest.\n\nA quick look at the data shows 63% of the requests fit a standard format. So route those to the\nparser you already trust, and squish the rest:\n\n```\nclass Parser\n  include Squishling\n\n  ...\n  squishling model: \"claude-haiku-4-5\",\n    harness: { type: :judged_squishsum, judge: \"claude-opus-5-5\" }\n\n  def call(data)\n    return fhir_parser.call(data) if fhir_message?(data)\n\n    squish!\n  end\nend\n```\n\n95 / 100pass\n\n$0.12per 100 calls\n\nSame 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\nflexible to mostly efficient without a rewrite, and without betting up front.\n\nThe four runs, side by side\n\nRun\n\nPass\n\nCost per 100 calls\n\nBare purpose\n\n73 / 100\n\n$0.48\n\nTuned purpose\n\n95 / 100\n\n$0.51\n\nTwo samples and a judge\n\n99 / 100\n\n$1.02\n\nCode the 63%, squish the rest\n\n95 / 100\n\n$0.12\n\nResults from the talk's demo, using 100 synthetic records and Anthropic Claude models. They show the\nshape of the trade, not a benchmark of your data.\n\nThe result\n\nDay-one integrations at a fraction of the cost\n\nIntegrations work on day one.\n\nLow cost per request.\n\nTotal LLM cost well below maintenance cost.\n\nAnd you can keep revisiting this balance as the business evolves.\n\nIt runs backwards, too\n\nDrowning under a thousand legacy parsers? Find the ones at the tail of your usage curve. Squish them instead.\nThen delete the code. Stop living in fear that a tiny client finds the breaking change you shipped two quarters ago.\n\nThe philosophy\n\nSmall parts, composed\n\nThe steps above show what a squishling does. These three principles decide where one belongs in your system.\n\nSquishlings are atomic\n\nOne small job, one clear goal, one output shape, like the\nParser.\n\nSmall, fast, explainable\n\nStart with the cheapest model that reliably does the job, and tune before reaching for more.\nA tuned purpose on Haiku matched a second sample and a judge at half the cost.\n\nA reliable system isn't one with no squish in it. It's one where every squish is small, atomic and\neasy to replace.\n\nBeyond parsing\n\nWhen a service call fails, squish it\n\nNot 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\nmodel. Save yourself, and the engineers who come after you, uncountable headaches.\n\n``` python\nclass WebhookHandler\n  include Squishling\n\n  ...\n\n  def call(service:, body:)\n    VendorWebhooks.fetch(service).parse(body)\n  rescue VendorWebhooks::ParseError => e\n    squish!(context: { parse_error: e })\n  end\nend\n```\n\nWith squish!, the model gets the call's inputs plus whatever context you pass, such as the error, and\nstill has to return the same validated result. If it can't, squish_fallback decides what happens next.\n\nBeyond integrations\n\nEven a bug in your own code can squish\n\nAny rescued exception can be the trigger, including a plain old bug:\n\n``` python\nclass SomeHelperMethod\n  def call(payload)\n    payload.dig(\"user\", \"email\")\n  end\nend\n\npayload = { \"user\" => \"alice@example.com\" }\n# oops: the caller sent a string, not a hash\n\nSomeHelperMethod.new.call(payload)\n# => TypeError: String does not have #dig method\n```\n\nAnd after:\n\n```\nclass SomeHelperMethod\n  include Squishling\n\n  purpose \"Return the user's email address from this payload.\"\n  output_schema { string :email }\n\n  def call(payload)\n    payload.dig(\"user\", \"email\")\n  rescue => e\n    squish!(context: { error: e })\n  end\nend\n\nSomeHelperMethod.new.call(payload).email\n# => \"alice@example.com\"\n```\n\nIf the answer is in the input or the error message, squish can find and fix it.\n\nIt's a net, not the repair. Still fix the bug, and check the cases where a plausible wrong value\nwould be costly.\n\nPut a number on it\n\nMeasure your tech debt in tokens\n\nCode you haven't written is debt you haven't taken on. A squished path has a running cost you can read off a\nmeter, so \"should we write a parser for this?\" turns into arithmetic: what the path costs in tokens each month,\nagainst what the code costs to write and keep.\n\nPick a template, then change the numbers. The first one is deliberately a case where writing code is not\nworth it, and the last two show what changes when squishing earns its keep.\n\nThe model path\n\nThe code path\n\nWhat squishing gets you\n\n$0token cost per month\n\n$0value squishing returns per month\n\n$0code upkeep per month\n\n$0one-time cost to write it\n\nTo get your real numbers, you can read token usage a few ways:\n\nAdd the gem, pick an adapter (RubyLLM, or any OpenAI-compatible endpoint) and configure your provider keys\n(OpenAI, Anthropic Claude, Google Gemini and others work), and describe your first class.\n\n```\n# Gemfile\ngem \"squishling\"\n```\n\nRequires Ruby 3.3 or newer. The\nREADME covers each use case, and the\ndocs cover routing, schemas, configuration,\nfailure handling, harnesses and measuring token spend.\n\nOn the roadmap\n\nFramework-specific defaults.\n\nOutput format annotations.\n\nSquish the star button and leave issues on GitHub.", "url": "https://wpnews.pro/news/squishling-0-5-0-hybrid-llm-ruby-code-classes", "canonical_source": "https://coolhand-labs.github.io/squishling/", "published_at": "2026-10-29 20:35:23+00:00", "updated_at": "2026-10-29 20:46:57.918314+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "developer-tools", "ai-agents"], "entities": ["Squishling", "Ruby", "claude-haiku-4-5-20251001", "Anthropic", "Synthea", "FHIR"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/squishling-0-5-0-hybrid-llm-ruby-code-classes", "markdown": "https://wpnews.pro/news/squishling-0-5-0-hybrid-llm-ruby-code-classes.md", "text": "https://wpnews.pro/news/squishling-0-5-0-hybrid-llm-ruby-code-classes.txt", "jsonld": "https://wpnews.pro/news/squishling-0-5-0-hybrid-llm-ruby-code-classes.jsonld"}}