LLM::Object

Introduction

Overview

LLM::Object is the hash-like object that llm.rb uses everywhere structured data flows through the runtime. Response bodies, tool arguments, schema results, usage and cost data, and function parameters all come back as LLM::Object instances. It is similar in spirit to OpenStruct, and it was introduced after OpenStruct became a bundled gem rather than a default gem in Ruby 3.5.

How it works

When you want to read a value from an LLM::Object, use either method-style or bracket access. Keys are indifferent, so strings and symbols work interchangeably:

obj = LLM::Object.from(city: "Paris", temperature: 15.0)

obj.city          # => "Paris"
obj["city"]       # => "Paris"
obj[:city]        # => "Paris"
obj[:temperature] # => 15.0

Nested hashes and arrays are converted recursively, so deep chains read naturally:

obj = LLM::Object.from(person: {name: "John"})
obj.person.name   # => "John"
obj.person.class  # => LLM::Object

Why would I use it?

Most of the time you do not construct LLM::Object instances yourself. They come back from talk, ask, embed, and every other call that returns structured data. Knowing how they behave lets you read response fields, pass tool arguments, and inspect usage without reaching for to_h on every line.

Notes

An LLM::Object is enumerable and supports the usual Hash operations: keys, values, key?, fetch, dig, slice, merge, merge!, delete, size, and empty?. Use to_h for a plain Hash and to_hash for one with symbol keys. A missing key returns nil rather than raising. Because it subclasses BasicObject, to_json is defined explicitly and serializes through the configured JSON adapter via LLM.json.dump.

Reading and writing

Overview

Beyond simple reads, LLM::Object supports assignment, mutation, and iteration, so you can treat it like a Hash in place.

How it works

Assign values with method or bracket syntax, mutate in place, and iterate like a Hash:

obj = LLM::Object.from({})

obj.city = "Paris"       # method-style write
obj["country"] = "France"

obj.key?(:city)          # => true
obj.keys                 # => ["city", "country"]
obj.merge!(population: 2_100_000)

obj.each { |key, value| puts "#{key}: #{value}" }
obj.transform_values!(&:upcase) if obj.any?

Why would I use it?

Tool implementations receive their arguments as an LLM::Object and often build a result Hash from them. Merging defaults, deleting optional keys, and transforming values in place keeps that code concise without converting back and forth between Hash and LLM::Object.

Notes

merge returns a new LLM::Object; merge! mutates in place. Assignment always stores the key as a string internally, which is why string and symbol lookups both work. Equality compares against anything that responds to to_h.